From 9cb9cbe47d5b5b898f72aeb4702512af891a22b3 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sun, 26 Feb 2023 19:13:24 -0600 Subject: [PATCH 001/172] Tests: test that worlds don't create regions or locations after `create_items` (#1465) * Tests: test that worlds don't create regions or locations after `create_items` * recache during the location counts just to be extra safe * adjust typing and use a Tuple instead of a list * remove unused import --- test/general/TestLocations.py | 32 +++++++++++++++++++++++++++++++- test/general/__init__.py | 9 +++++---- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/test/general/TestLocations.py b/test/general/TestLocations.py index 5dbb1d55fc..f1b3349eeb 100644 --- a/test/general/TestLocations.py +++ b/test/general/TestLocations.py @@ -1,6 +1,6 @@ import unittest from collections import Counter -from worlds.AutoWorld import AutoWorldRegister +from worlds.AutoWorld import AutoWorldRegister, call_all from . import setup_solo_multiworld @@ -23,3 +23,33 @@ class TestBase(unittest.TestCase): for location in locations: self.assertIn(location.name, world_type.location_name_to_id) self.assertEqual(location.address, world_type.location_name_to_id[location.name]) + + def testLocationCreationSteps(self): + """Tests that Regions and Locations aren't created after `create_items`.""" + gen_steps = ("generate_early", "create_regions", "create_items") + for game_name, world_type in AutoWorldRegister.world_types.items(): + with self.subTest("Game", game_name=game_name): + multiworld = setup_solo_multiworld(world_type, gen_steps) + multiworld._recache() + region_count = len(multiworld.get_regions()) + location_count = len(multiworld.get_locations()) + + call_all(multiworld, "set_rules") + self.assertEqual(region_count, len(multiworld.get_regions()), + f"{game_name} modified region count during rule creation") + self.assertEqual(location_count, len(multiworld.get_locations()), + f"{game_name} modified locations count during rule creation") + + multiworld._recache() + call_all(multiworld, "generate_basic") + self.assertEqual(region_count, len(multiworld.get_regions()), + f"{game_name} modified region count during generate_basic") + self.assertGreaterEqual(location_count, len(multiworld.get_locations()), + f"{game_name} modified locations count during generate_basic") + + multiworld._recache() + call_all(multiworld, "pre_fill") + self.assertEqual(region_count, len(multiworld.get_regions()), + f"{game_name} modified region count during pre_fill") + self.assertGreaterEqual(location_count, len(multiworld.get_locations()), + f"{game_name} modified locations count during pre_fill") diff --git a/test/general/__init__.py b/test/general/__init__.py index 970c4ef936..b0fb7ca32e 100644 --- a/test/general/__init__.py +++ b/test/general/__init__.py @@ -1,12 +1,13 @@ from argparse import Namespace +from typing import Type, Tuple from BaseClasses import MultiWorld -from worlds.AutoWorld import call_all +from worlds.AutoWorld import call_all, World -gen_steps = ["generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill"] +gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill") -def setup_solo_multiworld(world_type) -> MultiWorld: +def setup_solo_multiworld(world_type: Type[World], steps: Tuple[str, ...] = gen_steps) -> MultiWorld: multiworld = MultiWorld(1) multiworld.game[1] = world_type.game multiworld.player_name = {1: "Tester"} @@ -16,6 +17,6 @@ def setup_solo_multiworld(world_type) -> MultiWorld: setattr(args, name, {1: option.from_any(option.default)}) multiworld.set_options(args) multiworld.set_default_common_options() - for step in gen_steps: + for step in steps: call_all(multiworld, step) return multiworld From 79b8733b137524ee19299a632d4d61d5e219f1b7 Mon Sep 17 00:00:00 2001 From: TheLynk <44308308+TheLynk@users.noreply.github.com> Date: Mon, 27 Feb 2023 23:17:54 +0100 Subject: [PATCH 002/172] OoT, MC: add new translation setup in french (#1410) * add new translation * Add translation for OOT Setup in french * Update setup_fr.md * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update setup_fr.md Fix treu to true * Update worlds/oot/docs/setup_fr.md Co-authored-by: Marech * Update OOT Init and Update Minecraft Init * Fix formatting errors --------- Co-authored-by: Ludovic Marechal Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- worlds/minecraft/__init__.py | 11 +- worlds/minecraft/docs/minecraft_fr.md | 74 +++++ worlds/oot/__init__.py | 11 +- worlds/oot/docs/setup_fr.md | 422 ++++++++++++++++++++++++++ 4 files changed, 516 insertions(+), 2 deletions(-) create mode 100644 worlds/minecraft/docs/minecraft_fr.md create mode 100644 worlds/oot/docs/setup_fr.md diff --git a/worlds/minecraft/__init__.py b/worlds/minecraft/__init__.py index cbd274ba84..3d84f098d6 100644 --- a/worlds/minecraft/__init__.py +++ b/worlds/minecraft/__init__.py @@ -47,7 +47,16 @@ class MinecraftWebWorld(WebWorld): ["Albinum"] ) - tutorials = [setup, setup_es, setup_sv] + setup_fr = Tutorial( + setup.tutorial_name, + setup.description, + "Français", + "minecraft_fr.md", + "minecraft/fr", + ["TheLynk"] + ) + + tutorials = [setup, setup_es, setup_sv, setup_fr] class MinecraftWorld(World): diff --git a/worlds/minecraft/docs/minecraft_fr.md b/worlds/minecraft/docs/minecraft_fr.md new file mode 100644 index 0000000000..e25febba42 --- /dev/null +++ b/worlds/minecraft/docs/minecraft_fr.md @@ -0,0 +1,74 @@ +# Guide de configuration du randomiseur Minecraft + +## Logiciel requis + +- Minecraft Java Edition à partir de + la [page de la boutique Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-edition) +- Archipelago depuis la [page des versions d'Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) + - (sélectionnez `Minecraft Client` lors de l'installation.) + +## Configuration de votre fichier YAML + +### Qu'est-ce qu'un fichier YAML et pourquoi en ai-je besoin ? + +Voir le guide sur la configuration d'un YAML de base lors de la configuration d'Archipelago +guide : [Guide de configuration de base de Multiworld](/tutorial/Archipelago/setup/en) + +### Où puis-je obtenir un fichier YAML ? + +Vous pouvez personnaliser vos paramètres Minecraft en allant sur la [page des paramètres de joueur](/games/Minecraft/player-settings) + +## Rejoindre une partie MultiWorld + +### Obtenez votre fichier de données Minecraft + +**Un seul fichier yaml doit être soumis par monde minecraft, quel que soit le nombre de joueurs qui y jouent.** + +Lorsque vous rejoignez un jeu multimonde, il vous sera demandé de fournir votre fichier YAML à l'hébergeur. Une fois cela fait, +l'hébergeur vous fournira soit un lien pour télécharger votre fichier de données, soit un fichier zip contenant les données de chacun +des dossiers. Votre fichier de données doit avoir une extension `.apmc`. + +Double-cliquez sur votre fichier `.apmc` pour que le client Minecraft lance automatiquement le serveur forge installé. Assurez-vous de +laissez cette fenêtre ouverte car il s'agit de votre console serveur. + +### Connectez-vous au multiserveur + +Ouvrez Minecraft, accédez à "Multijoueur> Connexion directe" et rejoignez l'adresse du serveur "localhost". + +Si vous utilisez le site Web pour héberger le jeu, il devrait se connecter automatiquement au serveur AP sans avoir besoin de `/connect` + +sinon, une fois que vous êtes dans le jeu, tapez `/connect (Port) (Password)` où `` est l'adresse du +Serveur Archipelago. `(Port)` n'est requis que si le serveur Archipelago n'utilise pas le port par défaut 38281. Notez qu'il n'y a pas de deux-points entre `` et `(Port)` mais un espace. +`(Mot de passe)` n'est requis que si le serveur Archipelago que vous utilisez a un mot de passe défini. + +### Jouer le jeu + +Lorsque la console vous indique que vous avez rejoint la salle, vous êtes prêt. Félicitations pour avoir rejoint avec succès un +jeu multimonde ! À ce stade, tous les joueurs minecraft supplémentaires peuvent se connecter à votre serveur forge. Pour commencer le jeu une fois +que tout le monde est prêt utilisez la commande `/start`. + +## Installation non Windows + +Le client Minecraft installera forge et le mod pour d'autres systèmes d'exploitation, mais Java doit être fourni par l' +utilisateur. Rendez-vous sur [minecraft_versions.json sur le MC AP GitHub](https://raw.githubusercontent.com/KonoTyran/Minecraft_AP_Randomizer/master/versions/minecraft_versions.json) +pour voir quelle version de Java est requise. Les nouvelles installations utiliseront par défaut la version "release" la plus élevée. +- Installez le JDK Amazon Corretto correspondant + - voir les [Liens d'installation manuelle du logiciel](#manual-installation-software-links) + - ou gestionnaire de paquets fourni par votre OS/distribution +- Ouvrez votre `host.yaml` et ajoutez le chemin vers votre Java sous la clé `minecraft_options` + - ` java : "chemin/vers/java-xx-amazon-corretto/bin/java"` +- Exécutez le client Minecraft et sélectionnez votre fichier .apmc + +## Installation manuelle complète + +Il est fortement recommandé d'utiliser le programme d'installation d'Archipelago pour gérer l'installation du serveur forge pour vous. +Le support ne sera pas fourni pour ceux qui souhaitent installer manuellement forge. Pour ceux d'entre vous qui savent comment faire et qui souhaitent le faire, +les liens suivants sont les versions des logiciels que nous utilisons. + +### Liens d'installation manuelle du logiciel + +- [Page de téléchargement de Minecraft Forge] (https://files.minecraftforge.net/net/minecraftforge/forge/) +- [Page des versions du mod Minecraft Archipelago Randomizer] (https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases) + - **NE PAS INSTALLER CECI SUR VOTRE CLIENT** +- [Amazon Corretto](https://docs.aws.amazon.com/corretto/) + - choisissez la version correspondante et sélectionnez "Téléchargements" sur la gauche \ No newline at end of file diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 20b3ccb02d..cae67e1e65 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -85,7 +85,16 @@ class OOTWeb(WebWorld): setup.authors ) - tutorials = [setup, setup_es] + setup_fr = Tutorial( + setup.tutorial_name, + setup.description, + "Français", + "setup_fr.md", + "setup/fr", + ["TheLynk"] + ) + + tutorials = [setup, setup_es, setup_fr] class OOTWorld(World): diff --git a/worlds/oot/docs/setup_fr.md b/worlds/oot/docs/setup_fr.md new file mode 100644 index 0000000000..6248f8c44b --- /dev/null +++ b/worlds/oot/docs/setup_fr.md @@ -0,0 +1,422 @@ +# Guide d'installation Archipelago pour Ocarina of Time + +## Important + +Comme nous utilisons Bizhawk, ce guide ne s'applique qu'aux systèmes Windows et Linux. + +## Logiciel requis + +- Bizhawk : [Bizhawk sort de TASVideos] (https://tasvideos.org/BizHawk/ReleaseHistory) + - Les versions 2.3.1 et ultérieures sont prises en charge. La version 2.7 est recommandée pour la stabilité. + - Des instructions d'installation détaillées pour Bizhawk peuvent être trouvées sur le lien ci-dessus. + - Les utilisateurs Windows doivent d'abord exécuter le programme d'installation prereq, qui peut également être trouvé sur le lien ci-dessus. +- Le client Archipelago intégré, qui peut être installé [ici](https://github.com/ArchipelagoMW/Archipelago/releases) + (sélectionnez `Ocarina of Time Client` lors de l'installation). +- Une ROM Ocarina of Time v1.0. + +## Configuration de Bizhawk + +Une fois Bizhawk installé, ouvrez Bizhawk et modifiez les paramètres suivants : + +- Allez dans Config > Personnaliser. Basculez vers l'onglet Avancé, puis basculez le Lua Core de "NLua+KopiLua" vers + "Interface Lua+Lua". Redémarrez ensuite Bizhawk. Ceci est nécessaire pour que le script Lua fonctionne correctement. + **REMARQUE : Même si "Lua+LuaInterface" est déjà sélectionné, basculez entre les deux options et resélectionnez-le. Nouvelles installations** + ** des versions plus récentes de Bizhawk ont tendance à afficher "Lua+LuaInterface" comme option sélectionnée par défaut mais se chargent toujours ** + **"NLua+KopiLua" jusqu'à ce que cette étape soit terminée.** +- Sous Config > Personnaliser > Avancé, assurez-vous que la case pour AutoSaveRAM est cochée et cliquez sur le bouton 5s. + Cela réduit la possibilité de perdre des données de sauvegarde en cas de plantage de l'émulateur. +- Sous Config > Personnaliser, cochez les cases "Exécuter en arrière-plan" et "Accepter la saisie en arrière-plan". Cela vous permettra de + continuer à jouer en arrière-plan, même si une autre fenêtre est sélectionnée. +- Sous Config> Raccourcis clavier, de nombreux raccourcis clavier sont répertoriés, dont beaucoup sont liés aux touches communes du clavier. Vous voudrez probablement + désactiver la plupart d'entre eux, ce que vous pouvez faire rapidement en utilisant `Esc`. +- Si vous jouez avec une manette, lorsque vous liez les commandes, désactivez "P1 A Up", "P1 A Down", "P1 A Left" et "P1 A Right" + car ceux-ci interfèrent avec la visée s'ils sont liés. Définissez l'entrée directionnelle à l'aide de l'onglet Analogique à la place. +- Sous N64, activez "Utiliser l'emplacement d'extension". Ceci est nécessaire pour que les sauvegardes fonctionnent. + (Le menu N64 n'apparaît qu'après le chargement d'une ROM.) + +Il est fortement recommandé d'associer les extensions de rom N64 (\*.n64, \*.z64) au Bizhawk que nous venons d'installer. +Pour ce faire, nous devons simplement rechercher n'importe quelle rom N64 que nous possédons, faire un clic droit et sélectionner "Ouvrir avec ...", dépliez +la liste qui apparaît et sélectionnez l'option du bas "Rechercher une autre application", puis naviguez jusqu'au dossier Bizhawk +et sélectionnez EmuHawk.exe. + +Un guide de configuration Bizhawk alternatif ainsi que divers conseils de dépannage peuvent être trouvés +[ici](https://wiki.ootrandomizer.com/index.php?title=Bizhawk). + +## Configuration de votre fichier YAML + +### Qu'est-ce qu'un fichier YAML et pourquoi en ai-je besoin ? + +Votre fichier YAML contient un ensemble d'options de configuration qui fournissent au générateur des informations sur la façon dont il doit +générer votre jeu. Chaque joueur d'un multimonde fournira son propre fichier YAML. Cette configuration permet à chaque joueur de profiter +d'une expérience personnalisée à leur goût, et différents joueurs dans le même multimonde peuvent tous avoir des options différentes. + +### Où puis-je obtenir un fichier YAML ? + +Un yaml OoT de base ressemblera à ceci. Il y a beaucoup d'options cosmétiques qui ont été supprimées pour le plaisir de ce +tutoriel, si vous voulez voir une liste complète, téléchargez Archipelago depuis +la [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) et recherchez l'exemple de fichier dans +le dossier "Lecteurs". + +``` yaml +description: Modèle par défaut d'Ocarina of Time # Utilisé pour décrire votre yaml. Utile si vous avez plusieurs fichiers +# Votre nom dans le jeu. Les espaces seront remplacés par des underscores et il y a une limite de 16 caractères +name: VotreNom +game: + Ocarina of Time: 1 +requires: + version: 0.1.7 # Version d'Archipelago requise pour que ce yaml fonctionne comme prévu. +# Options partagées prises en charge par tous les jeux : +accessibility: + items: 0 # Garantit que vous pourrez acquérir tous les articles, mais vous ne pourrez peut-être pas accéder à tous les emplacements + locations: 50 # Garantit que vous pourrez accéder à tous les emplacements, et donc à tous les articles + none: 0 # Garantit seulement que le jeu est battable. Vous ne pourrez peut-être pas accéder à tous les emplacements ou acquérir tous les objets +progression_balancing: # Un système pour réduire le BK, comme dans les périodes où vous ne pouvez rien faire, en déplaçant vos éléments dans une sphère d'accès antérieure + 0: 0 # Choisissez un nombre inférieur si cela ne vous dérange pas d'avoir un multimonde plus long, ou si vous pouvez glitch / faire du hors logique. + 25: 0 + 50: 50 # Faites en sorte que vous ayez probablement des choses à faire. + 99: 0 # Obtenez les éléments importants tôt et restez en tête de la progression. +Ocarina of Time: + logic_rules: # définit la logique utilisée pour le générateur. + glitchless: 50 + glitched: 0 + no_logic: 0 + logic_no_night_tokens_without_suns_song: # Les skulltulas nocturnes nécessiteront logiquement le Chant du soleil. + false: 50 + true: 0 + open_forest: # Définissez l'état de la forêt de Kokiri et du chemin vers l'arbre Mojo. + open: 50 + closed_deku: 0 + closed: 0 + open_kakariko: # Définit l'état de la porte du village de Kakariko. + open: 50 + zelda: 0 + closed: 0 + open_door_of_time: # Ouvre la Porte du Temps par défaut, sans le Chant du Temps. + false: 0 + true: 50 + zora_fountain: # Définit l'état du roi Zora, bloquant le chemin vers la fontaine de Zora. + open: 0 + adult: 0 + closed: 50 + gerudo_fortress: # Définit les conditions d'accès à la forteresse Gerudo. + normal: 0 + fast: 50 + open: 0 + bridge: # Définit les exigences pour le pont arc-en-ciel. + open: 0 + vanilla: 0 + stones: 0 + medallions: 50 + dungeons: 0 + tokens: 0 + trials: # Définit le nombre d'épreuves requises dans le Château de Ganon. + # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum + 0: 50 # valeur minimale + 6: 0 # valeur maximale + random: 0 + random-low: 0 + random-higt: 0 + starting_age: # Choisissez l'âge auquel Link commencera. + child: 50 + adult: 0 + triforce_hunt: # Rassemblez des morceaux de la Triforce dispersés dans le monde entier pour terminer le jeu. + false: 50 + true: 0 + triforce_goal: # Nombre de pièces Triforce nécessaires pour terminer le jeu. Nombre total placé déterminé par le paramètre Item Pool. + # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum + 1: 0 # valeur minimale + 50: 0 # valeur maximale + random: 0 + random-low: 0 + random-higt: 0 + 20: 50 + bombchus_in_logic: # Les Bombchus sont correctement pris en compte dans la logique. Le premier pack trouvé aura 20 chus ; Kokiri Shop et Bazaar vendent des recharges ; bombchus ouvre Bombchu Bowling. + false: 50 + true: 0 + bridge_stones: # Définissez le nombre de pierres spirituelles requises pour le pont arc-en-ciel. + # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum + 0: 0 # valeur minimale + 3: 50 # valeur maximale + random: 0 + random-low: 0 + random-high: 0 + bridge_medallions: # Définissez le nombre de médaillons requis pour le pont arc-en-ciel. + # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum + 0: 0 # valeur minimale + 6: 50 # valeur maximale + random: 0 + random-low: 0 + random-high: 0 + bridge_rewards: # Définissez le nombre de récompenses de donjon requises pour le pont arc-en-ciel. + # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum + 0: 0 # valeur minimale + 9: 50 # valeur maximale + random: 0 + random-low: 0 + random-high: 0 + bridge_tokens: # Définissez le nombre de jetons Gold Skulltula requis pour le pont arc-en-ciel. + # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum + 0: 0 # valeur minimale + 100: 50 # valeur maximale + random: 0 + random-low: 0 + random-high: 0 + shuffle_mapcompass: # Contrôle où mélanger les cartes et boussoles des donjons. + remove: 0 + startwith: 50 + vanilla: 0 + dungeon: 0 + overworld: 0 + any_dungeon: 0 + keysanity: 0 + shuffle_smallkeys: # Contrôle où mélanger les petites clés de donjon. + remove: 0 + vanilla: 0 + dungeon: 50 + overworld: 0 + any_dungeon: 0 + keysanity: 0 + shuffle_hideoutkeys: # Contrôle où mélanger les petites clés de la Forteresse Gerudo. + vanilla: 50 + overworld: 0 + any_dungeon: 0 + keysanity: 0 + shuffle_bosskeys: # Contrôle où mélanger les clés du boss, à l'exception de la clé du boss du château de Ganon. + remove: 0 + vanilla: 0 + dungeon: 50 + overworld: 0 + any_dungeon: 0 + keysanity: 0 + shuffle_ganon_bosskey: # Contrôle où mélanger la clé du patron du château de Ganon. + remove: 50 + vanilla: 0 + dungeon: 0 + overworld: 0 + any_dungeon: 0 + keysanity: 0 + on_lacs: 0 + enhance_map_compass: # La carte indique si un donjon est vanille ou MQ. La boussole indique quelle est la récompense du donjon. + false: 50 + true: 0 + lacs_condition: # Définissez les exigences pour la cinématique de la Flèche lumineuse dans le Temple du temps. + vanilla: 50 + stones: 0 + medallions: 0 + dungeons: 0 + tokens: 0 + lacs_stones: # Définissez le nombre de pierres spirituelles requises pour le LACS. + # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum + 0: 0 # valeur minimale + 3: 50 # valeur maximale + random: 0 + random-low: 0 + random-high: 0 + lacs_medallions: # Définissez le nombre de médaillons requis pour LACS. + # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum + 0: 0 # valeur minimale + 6: 50 # valeur maximale + random: 0 + random-low: 0 + random-high: 0 + lacs_rewards: # Définissez le nombre de récompenses de donjon requises pour LACS. + # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum + 0: 0 # valeur minimale + 9: 50 # valeur maximale + random: 0 + random-low: 0 + random-high: 0 + lacs_tokens: # Définissez le nombre de jetons Gold Skulltula requis pour le LACS. + # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum + 0: 0 # valeur minimale + 100: 50 # valeur maximale + random: 0 + random-low: 0 + random-high: 0 + shuffle_song_items: # Définit où les chansons peuvent apparaître. + song: 50 + dungeon: 0 + any: 0 + shopsanity: # Randomise le contenu de la boutique. Réglez sur "off" pour ne pas mélanger les magasins ; "0" mélange les magasins mais ne n'autorise pas les articles multimonde dans les magasins. + 0: 0 + 1: 0 + 2: 0 + 3: 0 + 4: 0 + random_value: 0 + off: 50 + tokensanity : # les récompenses en jetons des Skulltulas dorées sont mélangées dans la réserve. + off: 50 + dungeons: 0 + overworld: 0 + all: 0 + shuffle_scrubs: # Mélangez les articles vendus par Business Scrubs et fixez les prix. + off: 50 + low: 0 + regular: 0 + random_prices: 0 + shuffle_cows: # les vaches donnent des objets lorsque la chanson d'Epona est jouée. + false: 50 + true: 0 + shuffle_kokiri_sword: # Mélangez l'épée Kokiri dans la réserve d'objets. + false: 50 + true: 0 + shuffle_ocarinas: # Mélangez l'Ocarina des fées et l'Ocarina du temps dans la réserve d'objets. + false: 50 + true: 0 + shuffle_weird_egg: # Mélangez l'œuf bizarre de Malon au château d'Hyrule. + false: 50 + true: 0 + shuffle_gerudo_card: # Mélangez la carte de membre Gerudo dans la réserve d'objets. + false: 50 + true: 0 + shuffle_beans: # Ajoute un paquet de 10 haricots au pool d'objets et change le vendeur de haricots pour qu'il vende un objet pour 60 roupies. + false: 50 + true: 0 + shuffle_medigoron_carpet_salesman: # Mélangez les objets vendus par Medigoron et le vendeur de tapis Haunted Wasteland. + false: 50 + true: 0 + skip_child_zelda: # le jeu commence avec la lettre de Zelda, l'objet de la berceuse de Zelda et les événements pertinents déjà terminés. + false: 50 + true: 0 + no_escape_sequence: # Ignore la séquence d'effondrement de la tour entre les combats de Ganondorf et de Ganon. + false: 50 + true: 0 + no_guard_stealth: # Le vide sanitaire du château d'Hyrule passe directement à Zelda. + false: 50 + true: 0 + no_epona_race: # Epona peut toujours être invoquée avec Epona's Song. + false: 50 + true: 0 + skip_some_minigame_phases: # Dampe Race et Horseback Archery donnent les deux récompenses si la deuxième condition est remplie lors de la première tentative. + false: 50 + true: 0 + complete_mask_quest: # Tous les masques sont immédiatement disponibles à l'emprunt dans la boutique Happy Mask. + false: 50 + true: 0 + useful_cutscenes: # Réactive la cinématique Poe dans le Temple de la forêt, Darunia dans le Temple du feu et l'introduction de Twinrova. Surtout utile pour les pépins. + false: 50 + true: 0 + fast_chests: # Toutes les animations des coffres sont rapides. Si désactivé, les éléments principaux ont une animation lente. + false: 50 + true: 0 + free_scarecrow: # Sortir l'ocarina près d'un point d'épouvantail fait apparaître Pierre sans avoir besoin de la chanson. + false: 50 + true: 0 + fast_bunny_hood: # Bunny Hood vous permet de vous déplacer 1,5 fois plus vite comme dans Majora's Mask. + false: 50 + true: 0 + chicken_count: # Contrôle le nombre de Cuccos pour qu'Anju donne un objet en tant qu'enfant. + \# vous pouvez ajouter des valeurs supplémentaires entre le minimum et le maximum + 0: 0 # valeur minimale + 7: 50 # valeur maximale + random: 0 + random-low: 0 + random-high: 0 + hints: # les pierres à potins peuvent donner des indices sur l'emplacement des objets. + none: 0 + mask: 0 + agony: 0 + always: 50 + hint_dist: # Choisissez la distribution d'astuces à utiliser. Affecte la fréquence des indices forts, quels éléments sont toujours indiqués, etc. + balanced: 50 + ddr: 0 + league: 0 + mw2: 0 + scrubs: 0 + strong: 0 + tournament: 0 + useless: 0 + very_strong: 0 + text_shuffle: # Randomise le texte dans le jeu pour un effet comique. + none: 50 + except_hints: 0 + complete: 0 + damage_multiplier: # contrôle la quantité de dégâts subis par Link. + half: 0 + normal: 50 + double: 0 + quadruple: 0 + ohko: 0 + no_collectible_hearts: # les cœurs ne tomberont pas des ennemis ou des objets. + false: 50 + true: 0 + starting_tod: # Changer l'heure de début de la journée. + default: 50 + sunrise: 0 + morning: 0 + noon: 0 + afternoon: 0 + sunset: 0 + evening: 0 + midnight: 0 + witching_hour: 0 + start_with_consumables: # Démarrez le jeu avec des Deku Sticks et des Deku Nuts pleins. + false: 50 + true: 0 + start_with_rupees: # Commencez avec un portefeuille plein. Les mises à niveau de portefeuille rempliront également votre portefeuille. + false: 50 + true: 0 + item_pool_value: # modifie le nombre d'objets disponibles dans le jeu. + plentiful: 0 + balanced: 50 + scarce: 0 + minimal: 0 + junk_ice_traps: # Ajoute des pièges à glace au pool d'objets. + off: 0 + normal: 50 + on: 0 + mayhem: 0 + onslaught: 0 + ice_trap_appearance: # modifie l'apparence des pièges à glace en tant qu'éléments autonomes. + major_only: 50 + junk_only: 0 + anything: 0 + logic_earliest_adult_trade: # premier élément pouvant apparaître dans la séquence d'échange pour adultes. + pocket_egg: 0 + pocket_cucco: 0 + cojiro: 0 + odd_mushroom: 0 + poachers_saw: 0 + broken_sword: 0 + prescription: 50 + eyeball_frog: 0 + eyedrops: 0 + claim_check: 0 + logic_latest_adult_trade: # Dernier élément pouvant apparaître dans la séquence d'échange pour adultes. + pocket_egg: 0 + pocket_cucco: 0 + cojiro: 0 + odd_mushroom: 0 + poachers_saw: 0 + broken_sword: 0 + prescription: 0 + eyeball_frog: 0 + eyedrops: 0 + claim_check: 50 + +``` + +## Rejoindre une partie MultiWorld + +### Obtenez votre fichier de correctif OOT + +Lorsque vous rejoignez un jeu multimonde, il vous sera demandé de fournir votre fichier YAML à l'hébergeur. Une fois que c'est Fini, +l'hébergeur vous fournira soit un lien pour télécharger votre fichier de données, soit un fichier zip contenant les données de chacun +des dossiers. Votre fichier de données doit avoir une extension `.apz5`. + +Double-cliquez sur votre fichier `.apz5` pour démarrer votre client et démarrer le processus de patch ROM. Une fois le processus terminé +(cela peut prendre un certain temps), le client et l'émulateur seront lancés automatiquement (si vous avez associé l'extension +à l'émulateur comme recommandé). + +### Connectez-vous au multiserveur + +Une fois le client et l'émulateur démarrés, vous devez les connecter. Dans l'émulateur, cliquez sur "Outils" +menu et sélectionnez "Console Lua". Cliquez sur le bouton du dossier ou appuyez sur Ctrl+O pour ouvrir un script Lua. + +Accédez à votre dossier d'installation Archipelago et ouvrez `data/lua/OOT/oot_connector.lua`. + +Pour connecter le client au multiserveur, mettez simplement `:` dans le champ de texte en haut et appuyez sur Entrée (si le +le serveur utilise un mot de passe, saisissez dans le champ de texte inférieur `/connect : [mot de passe]`) + +Vous êtes maintenant prêt à commencer votre aventure à Hyrule. \ No newline at end of file From 2c4658a7e0ba842db906767f83ab55a16bad088e Mon Sep 17 00:00:00 2001 From: Jarno Date: Mon, 27 Feb 2023 15:15:00 +0100 Subject: [PATCH 003/172] Docs: More games more fun --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 42493b5904..a3c03065d0 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,9 @@ Currently, the following games are supported: * Overcooked! 2 * Zillion * Lufia II Ancient Cave +* Blasphemous +* Stardew Valley +* Wargroove For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled From ce7e6bcf3324fa7103fd5bb90eaa3a0ddf99eed2 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 28 Feb 2023 01:23:40 +0100 Subject: [PATCH 004/172] Readme: fix order --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a3c03065d0..8b72dcf685 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,8 @@ Currently, the following games are supported: * Zillion * Lufia II Ancient Cave * Blasphemous -* Stardew Valley * Wargroove +* Stardew Valley For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled From d6f3b27695a971ae0b0a18356709c0ef6d40e709 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 28 Feb 2023 09:11:17 +0100 Subject: [PATCH 005/172] DKC3, SMW: use user_path for file Same as for other games, this will resolve to ~/Archipelago on Linux, if the install folder is read-only --- worlds/dkc3/Rom.py | 2 +- worlds/smw/Rom.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/dkc3/Rom.py b/worlds/dkc3/Rom.py index 37b8ecf04c..c193e909eb 100644 --- a/worlds/dkc3/Rom.py +++ b/worlds/dkc3/Rom.py @@ -740,5 +740,5 @@ def get_base_rom_path(file_name: str = "") -> str: if not file_name: file_name = options["dkc3_options"]["rom_file"] if not os.path.exists(file_name): - file_name = Utils.local_path(file_name) + file_name = Utils.user_path(file_name) return file_name diff --git a/worlds/smw/Rom.py b/worlds/smw/Rom.py index 5a4e9b5352..ffd8923786 100644 --- a/worlds/smw/Rom.py +++ b/worlds/smw/Rom.py @@ -971,5 +971,5 @@ def get_base_rom_path(file_name: str = "") -> str: if not file_name: file_name = options["smw_options"]["rom_file"] if not os.path.exists(file_name): - file_name = Utils.local_path(file_name) + file_name = Utils.user_path(file_name) return file_name From 0733775f2cd02b17c984ced54cea9a00dda72549 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 26 Feb 2023 10:15:40 +0100 Subject: [PATCH 006/172] Subnautica: Allow either utility room for progression --- worlds/subnautica/Items.py | 7 +++---- worlds/subnautica/__init__.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/worlds/subnautica/Items.py b/worlds/subnautica/Items.py index a5ccc1fb59..cf1f8ed24a 100644 --- a/worlds/subnautica/Items.py +++ b/worlds/subnautica/Items.py @@ -223,7 +223,7 @@ item_table: Dict[int, ItemDict] = { 'name': 'Observatory', 'tech_type': 'BaseObservatory'}, 35053: {'classification': ItemClassification.progression, - 'count': 2, + 'count': 1, 'name': 'Multipurpose Room', 'tech_type': 'BaseRoom'}, 35054: {'classification': ItemClassification.useful, @@ -338,12 +338,11 @@ item_table: Dict[int, ItemDict] = { 'count': 1, 'name': 'Ultra High Capacity Tank', 'tech_type': 'HighCapacityTank'}, - # these currently unlock through some special sauce in Subnautica, unlike any established other - # keeping here for later 35082: {'classification': ItemClassification.progression, - 'count': 0, + 'count': 1, 'name': 'Large Room', 'tech_type': 'BaseLargeRoom'}, + # awarded with their rooms, keeping that as-is as they're cosmetic 35083: {'classification': ItemClassification.filler, 'count': 0, 'name': 'Large Room Glass Dome', diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index 53c04fb3d8..b786bcc474 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -42,7 +42,7 @@ class SubnauticaWorld(World): option_definitions = Options.options data_version = 9 - required_client_version = (0, 3, 8) + required_client_version = (0, 3, 9) creatures_to_scan: List[str] From 1d2f7d86696270808860e17babea58c49aaf9172 Mon Sep 17 00:00:00 2001 From: FlySniper Date: Tue, 28 Feb 2023 10:26:48 -0500 Subject: [PATCH 007/172] Wargroove: Fixed the find all dogs check activating prematurely (#1486) --- ...paign-c40a6e5b0cdf86ddac03b276691c483d.cmp | Bin 113792 -> 113792 bytes ...n-c40a6e5b0cdf86ddac03b276691c483d.cmp.bak | Bin 113792 -> 113792 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/worlds/wargroove/data/save/campaign-c40a6e5b0cdf86ddac03b276691c483d.cmp b/worlds/wargroove/data/save/campaign-c40a6e5b0cdf86ddac03b276691c483d.cmp index cf0c5304cdd972be2da8cae21669d92eb9481a7b..8525595c65ebd5af38bea9c218f6a89cb79461e2 100644 GIT binary patch literal 113792 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�{KPerH1ck~1tWFhEZ{|w(R$S*?Z>X>MqHM+D& z0->YIvCbx7A-_mv?!8ek6fmD8Zgn27A%T>SIm{1l;Nu|SNdU$1zXo&rb$}!A3)Y!i zIa)_o9C#Y>j}%dsoe5p@^MwviItOWm25)jWVSb%{M`UE0ASXjZ)=~C5m_M03KJ_y7 zyt=J)e&17iA@x{{+pT>89VSCP1E-2Op1#ag2b_+9uj zNVYlX9qg$XVFo(5S7hUN$f?YN{ENn_Q!q^06M^J#po63`3;zDYl z7%$9X6bip0x3bk1D!!V@G=FSbTOuAS<485rgXUxuZ{tJH3w#iYPEbp??VX27Ud_2L zFwTeoEcBbIljSPFm)lC(-lGO&mc32yI4Fj2qZJ?%?|sh$AJ$JR>*)CkD%$&~2^}+$ zgCADfV&ZEVapX8h7Xbs;90c!HoT5oFQ-+hDHvMzLBQd>cK4_<+{J!8b;5S~f5$tQU z9K4QQJ*=>M=du7qf`Qn$z}=v-_+l>N-EKXUHua@z_EU-sdRon)P&{2ppp92U@#gsE z6KQsAsU!sANt6o3`;yPnIUqxI4V0nV8wtWID9YZYvyt^&NM74PO3( z%gT{eBz#7{77$cx@nzG4FIxH215R!SP@P;x9n<>&VR}$w!dyKn9#I-hLl$+%WIymf z3#NNUJ8kN3hZBs^jn+t#geepEpu+5}vM^Z=HXx(~f)a+~`;;`fr~jy`v}He4-9*xz zAktBf4tBWO*xIMvrd6JwI>5%Yfl=tTAqfR+zBgipoNr3oJ}E|J6L}^TFy!=t48$5HACU3dD+w`z4R*I*hXCi z+k$O%;Bcu>W3y{GM+OD;(xaP>G=sui*hruA-qxb+&=)+S87WiaVD?~FTx>BNw}Mru zdBn4zI220!L}Lw)%L6x7t0nh5*q+e2ibr}2@sV;0vQ1s(H(CL&6h7%-j?}ReO$tYv z15W}QFaP`Oe0Zo$r=-10z{Gh9JNn(Q)$g!o`C*hs@vEUS zZp*B?cT9=#+EUmN>~7YNU+E^*m0BLi6DK8)koECp&cn{0FTO z-a@5iVYaUdhcbG^+gYlh>M_wU(?GO(^PVi1sI$+Y&UoH?DTG38n`UU~4S;LBb zcdQ`yXIOOrHQn~T_55=hVBtMZW>EszJU)J?2^3ZISV?SnjkE2FeG;5WV_qh!{I*sz zONPUtRz&CiE{k$WY}YPL?MY5so5IdA4o9vp1r^f~u}{8Rnj%sgJF~Llv5(79I|%hF zH<2eGmg@#R{ih&NW)Ux)A$mFq?EIyoht81B>4DfLFZ@L|ed`O=`FZbHEgCb?uSmYJ z^ISOMTpe&W#*#hTmk(wyb@8Gr8;_0{a z-W*8DrZ*7Cj^c1wOrlp&UcB=I{c_?SZcMhCIav_%j0BDl*6l5I=lAoU?nhJd`?3tc zyQ|>PyL(KS+8(sBf@R77c_;UCpw*~iOH2q#rqIT3oUqOE;qVX*V{j8$q4!&0-Jvy0g+)gBl^*yLcN`N2JdxrBb zdXy)UAosKIpFt6nVpW%^lYUoTeNd%9=T0RFPX5+hbxOw)of-LxMXpE{S(kyq3=glKkoz_MHUce zP@@=vFuxsvWcPYjYESE2XHkfZFvNof;h8&p9PoA+$ySv-Z)0M8)w*N0G8ef_%h_86 zUBW&X2HfzXj9v8D?(W)i#inzkqm8VwO)OG0pB4$MZ4Z(MwU{~5}l#v?Vn3Fj{ z>!e3?ke!1wNvZqv6|D&}kpshqvB)G?cQBL!)F#IkA3FvlH;2oi%r8xJJH8aet~&>od}DlIRNgp44@ zzbTrkVx@0<#-nt|8YW%}U?a-q_m0D97LyaV2Yws7j3WY%g`s8IqlhgDuK-2H4MH3hb^PFEDw3+#s)(%P8|nFEop^b;Y@svn%x zQ&PX;ReS0d|N188MFZV7V(rcfv~**S;lZPG zd2|(rtDx_G+2rwHjQkQ%Ax;X;+p>9prP4Wtvyz5S2qK^Bj&3uq9?d|;4*FotFZ+MQ z2ncv&goQ|K00Vv2@{HQQ#5%e^9SpfI?|sUE>iwTX`1}pcpUttF2CDWmzp{pu`+lz7 zhLQ-KO zk5WUn4P>!xZo?ADr`bL&{F|?_{_S8>QGw@qG`j|G`+G!j1MZNi=Nl<61zisNwOF|H zy-=oWBmnWSR`UD{u8wsfpz?qHd>AZW$u|v27kv09E=G~{>Es|@5X_g%DXLYe@4|AE zOw0yoD`KS$j9+mHpWu^c`SUG zR+r8k{XG7V6h6@VeK-{v7jTnHu&6HM-lZ|I)5B51RR1FH4sF(z!r(8t(KWq7?$jQG za`CMWpCby6K$p~ie%oG(0{sCwoCVcg`(4(mW`E%__qx{99 zpwm@{OR3yGEqCCoe&mD;5k<#?A}0sODhgn9rmb9cr(Uu9$akdH^t#Ir*;WqG4~YZOW=uxozAgJWJg&Z2RByxem121-8LHOD0~TM{S+fFN6Tm zDHMuP3BH*{JE-BB4@X2OF{YBJ;;pLZw<1u5(LX$em{H++_Xce`Y~oNTw) zY}8D>+j{ONfxKYfVoO!&^SW)-J4`3N&5@!`K*qJ);u@xb%d!B&U4wQK7tgP>;k7pK zg{|7Q+>xYo#DE$7bsu6`*NCOAA&OyCp>oUtkug?&^bD4-u1#b(Yy8=-(%`#raxQx^ zKH6wQ+sFk0O$945vStJb;vU~nYHf{nl%~4$!V9t$< z?&EUhI&_Zg_O=EQ@twg`2~dPrR@WTzos|4DyYp!C(eD2?V`Av5d+=3z$~`ru59pST zYJ@Tb-~IU&Tzu>!|5lI+dR9I3%AnzmDVkmk=tEO_Nt%G21h1N4ND$F-L)2B-esLG- z_kxnit-=7#3HTH{_@|k{KQ~fd)8VIUN@6ZlyX@)F1pMdYbsAaCB1P~;C}jm30Q+rm z`V)2aC9qnTH$|&51Nj=(UdAlX;J*PbLJ5OYe`KvKR+N1;;bh_&mKq-GE%jyYC!jM_ ziU16%lA@Z1aa6OquTiN@wxQMFLgq>xRpA3ImkmM9A>P={WE1=t55%Y!K}O~hB5l|u zP#cq8)dS0q#;Isn89IeI)@?7l!sb79_9uvmOy)pCo=NGQlCn9!N=VEa+bw z9eu#p+Q%lGQ$CQ}n;-u4L(P`kT)g|ZC8YER0-WuxHuyID0lMs?drHVJS_sWqCF)Gm zP3RWj$4j>Rk{zr^BzCooldX}Y5z9r^XQGJSuv+k}D0p0)Q+LZ>HG0;aBkj%Hw-HCv zY?NqUq2?f|;6IOm5NF;8X0b2i#@0jNunr_qxy#Qd@$Y{_uI|@#HLxJQ(iZ#^9|nGx z?!u;XBRqsPdJ8iwBLz_UYfF>Ht0+i2kQ0u#Sso1KE~|%IW+nn&kzTLYJk7kuMfuP8 zur|$FO#wD8ld-cfSde;cFdsyAGIlIs@Apr`$@*JvO5-QLC(KJg+2{gLwyf>~3%Hv? z)+1E;SRKe_HR@A9LQL;totth7@Q91#G>Y&bXAtD0ee2f(cqb=A6LxJD#M z^cS}!&kX}=ti4UhTg!If*Y3LE-})oE#NDXUB)G#qr(#JDZ}FY`=C|j)BhPngR_xTZ zARZ9uS|)u?#Muk+L*Vrz{Wn1jU$Zs&?A){blTiAc@iQ?gr1p-}<+^!fX|d+E&H-V- ztZdUh8rove24omik6Oo!b}5kV61J!FSKk(}wR~~kmS0Ag0&fb6>Xs;^Z$t{qg!Fm* z@(barFEV|{%ub-XjH4nsDp|>~wq<(&9zzI!nph_$+g;9kBRSt-^*fwpolU`z1kth(_&qF0|)Y!ZH$36&=U7Df_orFMwRE zQBcw>PWW&ZJMLya+q7V2(Yx1r`c4fYt9~+AL{2q^=q&Zkx0X# z*5fd|mb{eU)>{|`ic0Px;+p5wg^9ZR-~UM`3Cr|(Kj zc=rITKvc`5Mw^+B&YU0Fg>G?=A>Pp-FbTGOSC8EbRcw|17`2qst^M_Lxe#N=FwpJzI88gw>BSbs)y`exaP4{dN)!k_Z@}e2!F5BB5w24S zEN-t^Hm$=D!}mW-RD;A0Ik~)VxeL~Vq-|l6W85v7RYZy z%C=B>A*T&P1KVoAySE(KbVUq3WVwZ))hA^UcXmeB#bn+g@EzQ0>ru?;7qt(ORRCj- zP}W$DKa%BW@8Ofo<_I2mPnBA|0wb2k_V@}j5+qp zSa_;|S>hiSxm{MMT#Yd`ec!5;u_?%7+Ga&l!|=>)b{@E)CMtX9Ytm-T`^MjT?^$^K z9M<-=nu(7&xP!%7^tKQv>XsPz6ChxPrrOR#5T_ZrkUdaWpeZ*iO8XK(WCR$Qt0Z2d4weA$KQm6QUNiYh;II> zqiDm@nk_}}8mjxBj&jTl!^Vo74kPi8?M|IO64J+Z{}%_=!?gN(Xtey0_x*rH!Jt*$ zQy?=TVj=U){(JF{tUz8ju_=j+8)tOHhJEt;%I3{$8w_)&KbMDv9>&kR^3)kyG>^?53%^3suWv>>JDIq;vdUyoI{$-LA?u;l^rf_L^-$~F8}-X(+Bp;PA+PI7EyX=S&)@ovUD$d z$N6?HM(Wxwd=gJX|7(lxzfsO7SL?AnEr18j<5lkk?OOaxfYt;a!_%*rCWtr>eg=KU z?Jni-&#OVkR`5<|(bRy-d9TsmM8(WuFYNl59y++SdK2vzV+#y_y5_Bc`#ro|5;vVa z=*2viJkg&E9d`!nU?L`{{j@Mao99mWBq9~o%}@83eQ3!$?&D3m(GqL2QIt&14@S&l z6^8drbvg@`)pu8g-uH;Gj?JWZEeXPB+ym6s8VKkFw~9{$7Jw#%oMh-FT3EtldDtN! zee{h%l%Q|a8p{z;1dp3?ZTlw&`kl@z?7F$CaS4T4UJL`QC@(*-Nsv# zw5r~~?JmHmiW_?ftb+!!MavP?69l?~Y7rn+TG7ZF9aYLu#K{%_h<$Icf% z7$6bjnxN1MJ;zj5TVCz>0S;ddFKv&=$Q>PBM|gcj*{}>I$4i+C`$~-8st-z~V22^= zNS}#?cM9s81Oz6N%tEeGZsiao%Oqa`k6V%I2JUl5b-GJn5(a6l^cuih9ON#Zo*(h8 z>Vf1uInov^a(f2vhcGBi-40h z&eE*=`Mo%>Rt?GL`!K;xxHf}`P>$yIE&hRp%~UWoGXB>2%1RRf-E~7p3|X7Oe#Ua$ zaZ#C`;E6c6bZ)m`|De>U0q1rcXoN7hDLAC<#GMJ?4|_CR;E zg;>KONG$=OtnE31b2BhgQx+jldnHnsGH~FPR)F)a7n-;cY-y*`OLQlLpY-lO(N^)# zsrkgSlvJEMNZ@Vbt5e=<>mkktAvW6scgQU74$!Gg%jWMs=D)}bX_PG03Q-y?p0|cl z>l0|vDufe3KZvmWz7Q{nwy|Ja7OckB0+G`3EmZv#D&bHEV4p`1@yGx5yoD>0+3@}! zOxebX)UH}-(w^ZFRyw{f+ZsGY^D8@(s`BQ~YmrhU)TU|7Y|1Y?Beyk4%>IH49!($( z+ls>(IZZZG@GlvGR%sR~nPLyB&?ftUj#xc<(B2`z+HxW9Vw&^@Ez;OBsh*|t7g&eXWo5Mq8B#Y5z_if+3AdhMG;#IymlV zO_8^_dN1MHS~%Fi$8ZQ(^)VR$wm-iJwsYi$W5{ta3g17E!rBYFVxnR$h^#NhO)ZSO zVV*_@V+Q#0*GS&7h_wh5eU(0Hp~o??a#MqZU4%tf#f-;PR0x!XpxT^g!{Hod4;Ug` ze07su$_yzGdfOsSv~e_cbhuK=WgnM+1rW#tZ%U#!9py=lyE6UNDnqgHoB&K@a|{89 zN%eJ#{40d94p68UgC==ww->Q-zZ94u@|m>~&>Dz3QEBUF3*7xrQoV@#=CE@m9p!uS zwEF}>0~wgIE}X9ba}lO=x2NH-p%^2{Ct>~uYZ-}_V80PFwA`jHUM<_Y{I)l@K&&F8j#F511uE zh1&9cEpiUp%3k5Wz_qMR zk7qH%3Fc26?j7qAUYNKOLJ;MK>ItP+F}R~pyJb`TY1-zdmSEF+&1Vejni*w<%Ivn?rl%;2TN&Y2_0Z8Z72uZ&{`udObHNZ39+EJF4> z^|e7ixMG&vae%VkNM2vc3i;=A_4U+r-oTOrVtj&Z%_|CdwGls3j&H*fZNSkPctIK+ z3^apAp8x@!g;9#rSA6Fi;FT~M0!c3Ye@gihB#};}OL{bRKYF5@qERr`M9#q8k8;3RhhK-9_;0z3rasoK>)0vw<0-)a3HS zM#4*HKWjc0YykwLt{N;C4D1^s2Plrj47(G6ve&tjyE07B!6YEjo(yqPAdru4p88MEGfQ|aO8S>2xK7r{uSFITM*hBTedEWF}NQ<&)|MF@-J!}hLEpk&<4G$Jv}3O z%-`4#S;4^~#_2t!v6-2bvxf#Ds0B&ZWIYVHCdr=^xKoGt@loz+Wq;qouQHG4rj$gr^5Lhlk=ZHR?(%fI%F1*x|R zgHpnJe9pKM!H5qBt<-y&8r25Ga2USDcAuQ83&hy`Sx;bcc=i5P$FO`KKCby&y66|>FwFirA0-i zPI^;kn6(?#33p1Gu%>L6FEBZPg~KqIP_s&e1wB0wqxR0J=1$_W{yza;nIx9iQ4IMk zxDTxxF##i^?M4`6R40%>JEvtU?L8}1t^SRfSN@V>PI$6f)zJ85aR}wmQ1y1@CW5N# z-{_RepG5gV?C%^Vo2P?WZvEiOrJ5O87>4EoK{JO`kWwT$TlD_#McBFu!fShsNSzwW zx#cVNHcH|kc^t4k%11U2Jb!jR7pk0sKz!eMEJMo7mpw)30qE}yC%0%Kn{?8uvxPE& z(spZPoKDc4g|a29;EZHkmA6g+YV0r zWark^d~2^rPF<4lQc+4Oo?`eH^xrG?jOs>8i!y_5i5y)jv#zt5pX8s6BbwS>ni>3+Aa&lx@Ec{k04 zr69Id%3eL6t5uipx`*KN^T)p-+AeUF;f*j0tcw0V4zuC#t?IJgX{KgvqI}Y!5?qjR zobdlpibp7No1Ly|4HENg!Gp3YXx=KWloPW^K0k@Voy&of6I|d@=;ZSm){UI(yTTlJ zg{gB1W(QSB+fbR+pyd6)Y>IUoM2iIUwVzn}Iig8DuLLwepSq(trmh9?W#-GNI->0c zPD!FXZc76cI%qpp9iEFvWuyG9N4}k#k7Nm>4BYQ3E|J`Z_|{BAo-hb z>kV!{#y5b@fy*>1$vZmgmx)_z zbDEp?U)JI)fY&RTQc0bQO*483T{_>yY(};Zn(0Fm`D@Et!iQNfqxUKoHheq!QUp5- zQ0bvNyed{m3-;>W>-&v+E0@w9D4u0FPOtiW>7KPl(W6X-R?Z6mIV!{~)k7IKbh@*2 z6ORRv4nF8QLFrI2aH+2hC&A7*su7MFTm)PG3TY(;TMjzPLbR&w zZ{uk+?wRtL3{QOrv_2$OwYh_PLn4%ymTGO5mHz7h08hVsVR0xK#S4bm`N=r67jnzB`? zdhUu^Ru#D6o%F%*<*oN+h`w|n+7p7QZHy12MN3xFj3tZMQVI4g6shNgrGz)=fW-b| zmmQPJf-P>B!xnF!c>J=PIOIiLXPG>#g*^=vYqiv?lWUtBY2K6X2sxKsdopGncq)2) zJ3JsU7`+3`P?uW~@~~qeaK851Pb4V1)1$n%fr05dx*6h&$y_spOJv-3!}KmY`=g`pFF!Cb2)9nzZcM(fCXJ6t2~teB|zH04E&~`s_jjS+GG-F zKC9{)OQNezDYdM5@tqZG;*%)hT6h-Fo&{7Q=k^A5Cv;>qKj^9;KFl!;nXX}jZ2=LF zoA%AwJAye;)BIn&FENg51#%X6dls%@uS15%cb9RzR7SECcm*OPuQ$w`seRqT zkOK+_C)gEFeTgPqJ9DY0XA{q&_?eDn384*W{L9yy@noW#51IdK$kh}GR@P(h*n54n zKw8aNqK6vJ78|pqBeLS^sio@83c<#(tep5g;4NU0p7D3rb^}d#zaOhFua5i&M&U3! z6z|D-|Gt^2SOC(v^9W;Hu9c?7ZkwzPg~KM)b7k1%&ta4SK{Sl$S^DY}(FNz<=5?QE zZTM9fl-MwKq#b+;5p3fvTkE@XNKH~Rv9{^a3`;rw0=&$VP=2fO^Mui@I^%?*UADhy z1d&|{6BQ^LN(tM-#NPQ=rP~Nx1{XsAj5L52n;>0{!8V1-rM$CJ)LAya-`k|2Vsu`Z zL8nz7AvQYJ$24#M;07~il#fUp$snAo%a*D5v_)k-nHveSD4-FWJfrKly1ka6eEI2r zVGWVk09P*)w9>8bpG|*xP2^9#NU6~ri?9?z%HV1XGYtolsy1$rA5pyBeEk{Do80e;|I8%J5GtS>VgXF%-!NfEaA(OlLY!KX=7od3;2@AKRkP+T;KL5Z z3Rleh;%?e&s$@QdpAT?HKfWLaH7bqXLzESs^GKsoQ$X#P5b$)zxN4@5T|kHJ1{Q_a zQT}c!#_#bQb{%37)&;b^CA1s3w!nK>*Mki$Nq_SuJ|T{c$Rl3th4h%>Xc_TmS&&#o zr4}SVU}&igdX#LFF2K+x-##u+0RdjJJSJ5+?^*keS9sP-lyec0{&@!&_{0E@AyBKg zfT`QPVJT*JWN%XfY)h+N{mj-0Mkc|q4E$)_zLy2d5bfj493LuUt3A&{AAyYoQ2K&t z(K?8C_brIyX8mF&NgVI=uZEJ%;@VKXkmPIIIcTN8`gWaGyrPANOFRMa5axkj0c=oM z0JFvj!4B`!93uSK8jVII)k&On!e6CWg1}P#9Qp{X1?^D}-z8tl;%Ju%is1oBfVn-l z8^ca{8a7QJ0i~zq+6uKY8{Wu;?9tiqb5`G(FHbvTboXBDrrY$Qxbz<~C^|n^0V6cKTtC zPMiok8m$craNb{JaHp(PoWg^O-dAuuFbi`T6Q_~o0UZ$kciaT3a05aw7UPfol2I0D zs31u_2arrxxfW&n|EX#wo1DxhNFzB0odUIVNVUeemyCcF+BN2ZgOEO9&MY>Cj4Jg5 zQQ<$}$A?#K^W=#EcWluR3?mTxpk81~>XzBgPObZeDbA<(L_6w1w`lENRQ=9}NhGyX zI*~6R!XJ9LBsXhDq(ZABp{5bh1JxUv3A>cPcA93MwZF0*I|wm)=06M^nN}i&BgzO6 z@|gCx=7%#Ea{+5Z1B(5@hH!d!wmHrYK~0O`=#Pp=Xq`8dmi>8k9(?wGm?TjAmJ5N| zu=_q_)v&&+sYoxv@|i81p1KjSlh3V(F;^7-8X7nqs{;_=Ezj{;0Y?}nCeL#z5S$6F zH=O2ltUMglPnBj>=^5+##4u*JjQyp2t^epeHYFKpxR&0kVfJk1ep81$R=h@1aw)~g zy3M1CLifXVIHDPl*m+!N=g0`t)qOe!GYjNV7tumwTrB5JRf3mP{H?oTpWqL7Gp*ZK z>|@(e+3QpR=m1>%*JDM+A<@iwbl64?IEavW$*ppnk&`+E+w*&3gYI{aNDA59SE($lgD~yf!nu z*d`;IoJWStrcnrGXbPkD*w+zk=|L<@p~{R!8>TSsin zy*4}7t#D4H$@dt}^>>UiD{rS;2Gp_~kNz8{w1~fNEdCHKV^7E#yaJBnqn1omw-jg} zW!jxjCuZgl|51g=8RN>Ca$VIsLW#e?eRAmCDjm+wbLRq zA|L$dYpEE5BkkA86Guv%wC}3(7xMIv2mcPpkB}_=!OO44yA1aaCaQs>2;FH&iOF3I zsg6$#m}hI+z@i9e)dDkfbLpS;kUbZ$7~t&Y2l6UlLyt9)i|H^)>Ja8U&-QKsVrNTv zzADp*$mF83(kjh@PwSGiUcijPA4M%01Nzd-8f z87?InA<;fxQp^l0DWcw}zA(a;4;injvG7n|!ZAh(GlQ=S7m@i!xcm7X;g+{(Yuxi! zT?(ZpT7gCBT(GT(bwi_NI?RHO^dWrNCFm$z>>kzslvTn;$n$#4J5^<_Wz08O;3_9s z#kagT-bAbd>{nC6{G5}n{L5sC9(U20l=@XbxlOY0&G&9fIt+&nDbA-I(_0MffDd-l zMHVD(4^_W2Igf-+O>M?V=&XBu%?4v-;8abW_FW|4ZtpCuVURePxz2PPebtAf96R)lpO1aG_ec^eRMipW;xF-Azm%`7 zO=}IArzXgsU+>AVYxDwUU3U`cPAV{71}ues+2$Ox+ExMRvu_%*a4=SE=MvfdX~JQ3 zPpj#VH{kY|Wx0x=mE!Vl8K;}U2(`u9LlwMiRPP9h?O9)!&1_3_;qC6uw^Kwy;R?6e zwY@>cl&E>U#X15K$mZYvr*nIZgeq-ye@G2x$>1WKY~Mzj|_|u0DSCvZ}&r zlhQ5}V;N+{Ramb6>XZVqGeg2*-<-Yb&-*85A(h6O22cUT|L7it8`KdmTe>ik%k{nO zL+R+~HJd5;@B7IenX}qFPb*Juty@@NqYaYBu|5f$*)eOU5|_@^7!eEwSzmd(>6<>_ z^3ED*MI;lbyJABESVytxzuyG8itpl2-0%2@4Kr98W>m{BH|QPQtjykW<=v*Gs*{qm zrjswd`=KkiPy#7*leN8OYH z=>kFZ@KXL!F@W=1boQE`NSIGOf1(I4tDKVM#K;mTVE_^A`{+m}SLJSslz6JvU|#Ed z&5CHOKnEIMp^?#90Eb&0HJte9 zQfJ25Hu0dc^f+by%Y2Zcv3fys-W?3f*K|0l*Z}#6Sedh`Ws!OZpY>@(QS9(z80b%A zl&iqpYOH(*?S1vMX9i5U%V&wvwf9u!Hzf+h0T|Ik35d~ysNl+rCYpHaa)*&F-(L~5 z$tlDR{PEnUX0yUC%#@s;nuc#4>wWQrOxx(rXDX4 zkNk=Dad4rf|MtLLS9&sq);F)goZ5k2?_}O(w_+4n{J;A^kwn7>Su~TiV3--?SfHBx zbHeRXO&#PQy(^l*3ujHc=0y$f3FyXsnL(mfQIMj7kOa_fy_iMkjEn96t(Tfu1;P)Z z_0W1IH?yf>dZfdahsxQSCNlceFz;b`DHd*bMcNRS>%V~rWagYk(2rTkq8R6hHr}Xt z-HV$l7agNXOTF+Tl;-L}swz0Ek{8DOE`G}Yc(rMhIDVl~?;?~IScjOK^iU!ul`X?U z8$`_%+=%QK94KxD=x|*qxh5t|`o)k11VRF#B)(VY7aWRkRTE{-|0ZvFWPG&AR0?%& zm){0V^ihPx$%BsEm;ta)DfpXi$>xW;-Dbj_sMX<3QXwSMlDp%rUJkyk_GYM~?@v~n zQo(#cGc>E&yB6eCS-$-)tU15idQ$Yo8|pyYOxA_pK5uO7`1y57=z+v*Mk4mYS{X&9 zcgKllU@rK<3++aJ1}?}ZiynFUp`KMSyvEq7-gQmO+ zer`TrX98KQ+*5wX@Nc=eRxdcwD0JznFjk?`}{(&L&)Y2u6& z9wi2RrIBBO=s#GRQFWrGe$uakP;ho(MtUQq=k_;Z$)3_(f)o-$Xy=~GU%Rkgu_tkJ zKlk9O%TE=AMKe6wY?3N>}A-Kdpl&>62T7{pHxdEa@J})?9S~{M$lB?s)7(JDeYI zSApSDj}a;vaA@Ab+7BlZNJ=R=w!-J+^s0HsiQv%W&EoBJ!TX(wg&G?cpkf=m{GL3ucg=2EMRH~k5wUxb`` zG~)xNb{KtZT_LJ16?v?K2D-_|=?`?10gYXw?hAR5FN1H)F$|M(7X%YyLqTPZ?xFHmIwnl%1-E%Gkudc*#1{6 zC7{4OkFuzSdQU0I+!I~&*yC$0UZ_<2;`XDZ$PRh`sS~r@#?+gH>^%zE;AZc!H_mN! zdtmVGNo?7lJ%o`f=XK$Iy~+Qo84RNWs?|jy-bno-+~MkQHH-g=;QIe5=j7hOVr(YO z5h99U%+Ooc;-$P+@=5m_C|nSFpdpUZsSe_Ugpw2!2c}6+_@zX7`NxbS1;W1anX+_ zggAF`vRkbC94ADBEzQwd*K(78FUQ`HEoME)^}a@qpyN43h4Ly!_sr3U{o^r8PbABv z`3Eo`ZZCD1-p@H3K@`VG&+rJMkn2?-tq-q$3QFUv2} z3QB;LW24oc|JD0gw2w&x3GpC>YymzwfXh`5glgh*xOr!{^el(674$J z^z&&amI)kHqEi@^WZEY{fqb1O$A~igqlH88tkIY-nD*KeSXJ zCostvZJb12a_0 zJF9jjW#~nC@;MXLdxeMWG{xSTy`TsVAxnr6wjD(5LL(I7FLIwaR`Js2oY~$0P=n|8 z7sf==hsN{{%F#MHtD!l!d3C^D)8f}5!)7ZW-A?vKWq8^t`O+;V?eIV{voswxI!`TY zm|1Ff=(w-KQ+On->e6LC@gNeMl5roy$?egW`yo0t|J3-NS0nQzVi0Nj@>$E(UTCum zX?)VTeBWp!0bY5Ca5$t*jUSl=_E><*V{dO46*I>hr;rCOxo0L9t|vA#6Z!aS2n zazz%bL=(;Sj1pN};;inBiYWO#OTfXcqRvXiuxHG4&>Op~3)}lHA*;WlGXX-Vy3E{& zCj-qCxo{wO&)zJ!q-5x603_+5in=$8!7GlK!th~(C*X;Qh0qG~XGLA7Z9CO^Q&+Sg zpQ3{}%|e75fqU6V0zavymm#>EU0UF-WDSA+K|d+%(s9!b{=7e1_(Pk$MJ4aISI!FG zG~27Mjb>>p=z*;e+*p0GtBz6p6i`gmRzG-lBX*nr@B2Ml!GWOkBe0U<=*}6Hn%Mxu7z8zxF;f1v*ZtCw1nGuohJf0H|j$+hkdhwFtH zdh6a0OXd8kqQ?GDSC#re$&FS?u!-UQPFgVGB;85eXV}Cb|DmXYMORo-Y8C&d%91Fk z{T&ya|26pRm{O}MlPm&r0{cq*$!_S~dh7ORN-9x%3vp3xAc7I1>_AP8D= zmasP!dFXI`QepUUPM6X=cuBs1ze0r@S*a{x2wlPKt4bB@;@)xNj642VQM1l&`~P-e zr!S5nbs~QCCR6iz-MuPO3JKSGr5@oAe zvV%V8sg*myu@RST%;MNLJ&SxX^2+wgJdQJ3TuMqO#DZpyEpqkGaUNd>q=OS*(iXj% z^MK)FVH)C<^r%PD#WnC@QMH0dE$OO&a!1;7_08JRd9NG#Ic4$F7}XC#5~J(mVF+bZ zWtprdH}8AXLJTruC~QU#FQZRF3AYQnpz)XN_)WrDKQLfo%Z>rG@&MYc8bJHR@%WBB3 zKD7lD6UL=vc_TePFY%V!1YqwXp+ARDP9PT>04DB3L8;C&T2uy>2vYWZQn7ZOZ z%7B+;D7#JHxPMEg!~=P31{P(Am4?@Q}(rq@=ceWux++ozE%Mk|H{Qkv#S%y ztQX=jSWl-SwDCHhWo{JjnJ`!r+(HSKLGo1@tA`cU8RN$lx1TA>opQx_jRMrz5t;11z}Y||k8cwO#CQm~_%_vvj#EL)^;$s1c8o#1B-!ynZprr2w{ zb6kva=l7ss0qG)Avl_JOIu{j}K90pi8K=;1n5qKN105iGb{Sm-hOdT%8dvhe>0L-9 zaiGJX$Lf274#853tu7r(k0*i{_<31fI&D4$8$YSU1DI_dfq5&nx#=b@p`HhX{?GFU zVoqo42h!b5xLc?^Xh%1rK7~}g3G0ihbQz_DBqdX!)Cjz=x$n>c5S_VYK zBUU4n;`2{1SDXFCZS<(E6hb*PimmaDKjtZB@wn&6bEejqRy%m}o=<_MGf)#~iQSUw zJ&>*njQsaP5K`7R%$x7^I59VvITeBQ0F%UC;L}60j1;ZK*8D&uK|-4Sr+ILGz&(zf zKT%gS%=7QM-+o5Z=p1mW={k!7(|-+87k8tHhT;rB=mt0OG$f29>?+baFo%wPe(RXb z_0>>Y2o{eQ5~|X_-nO(EU0us}BU?wFHGNE$waN0fm~4T;P{CRpQNbU@lcBw|ovqGg zZG{rJazmjXkA*4BeAdA3U@Cf)*W>{3TK6A9;ZP$AiSSFB+V$FE|D8>3{!%c8s{ z$RU9EECv5gg!8|$y#`$UJ@eL|>V0wYo+yr-(J^*M`X^v23RmPyqK6W|@ zw~&;{<}@BNRQQy}$B4n!`Vu`cnE0GI>cW?z*jkuoHa8RdZcBBE&c$2)g(0xrvf_W3 z^a6vVY~{?hKkw`gK=csOPdL{NPMVj3Fzr_ueuz%1(D8!BtX402Y($!2YJV_|RRBKK z$P#nv5&OHVcHlk*1J5$8D*${VVKl<`T;`@6BNHc89#5qq;t^}hDvo4JsUId{szPv* z0gm3X;x%MMCRoCfmjS-X-&mdv@uS!6l`vkzBJGP!v0;K;{N%^AOi}zC^;WM#v!7kx z{*tr`BxsiI*9ctA3i+F>R>8(9Po_W(>V%-B_d7ZwER5Y>C9(mXRMRKat&cYf=Z+pQ zqb2#p=^}GaLXRAMzV`n(a1q?VVwS)cQzOWwRzmYSY*CmQd=l!Sv2+D~{~hY&m2E#N zhjRP^V8Sz@q>_~A!c733RLOA}E3$Snml%f zf3H*|opcOh;pF9)vj_9FK3^*BGmMtL!T*eiKWXb}z9PBiNzW-QcdA zl_p{JsA)BsZ~-*zitwuBS&0uL6cC21R+1Z$&YHG9sD&M1n`4j3U1HGSQ!Tj2VR;*} zW(r&Y+*5Q^a->tOi54MEa@Cb$d?aZ>e_{}`+1O9RR9iNj_M!?r3{;uh6*4I;JvCuSw=5g90Fz`8 z6?D&Ap*G&V?r^VL?{6Ro+>9&9OwSz@Viu_=NYij6yjPZoAPENEwSBjdUbZcAr@H@~ z&5dIm|EAu2Ci0~d;#>i7?yqtw{M}$DFExCz{8j9#=kQHpE}Lv zax7`Rs2VnYVX9Rw&P_)cVC_j|=7dy|J1Xs%l?xf^zg;PS$%?zFTL)9rl|0bvra-X1 z9nbx&AgkkToIC-brGZuCo#(KQIf4~V*2U$V(_fkQ)}gC?rM)Hjz@ zrgr9v_Q`E^!qx$)c=A)Y-;pC(q%3^+UnN$O?n=Hi5zZ)rf!3Tgn^zW_hZBad`~6Ch z$9ydBYEzO^B~v{3;ef7{>T*VY!08@dl4-`utArau)KB7Bd6t?`etZ@%PXP5;ODx#D z^|xR>?cC|*rJ-ePMYOYu7~IYjj4+G7NzvsrnSJ=gi@<#&>0{zjlvULrz8;BB3I-L9 zJNhl71Kv2uP7t&Cp7gNdX2q5aIEGkSi}m5jaQf7ftNed;?yNcZWJ!HlnmBPHe@E5r zEpLA;N?>XQvTef=-!bqrwxS_9shG%8-7MQ28-iB6!&YteETV7BNs}BUG5+#_Nd1}r zl&@o2Hn8RdjiD?%wwuzJNY9M-tc?Cu2K!#E4yM@_2UUOzDphyC+oAET$__q=MfGS+ zB^vQyrP$YiTh_1bofkIiYFJY4H?~*3V=dMMj{U$hLjcAzAe*7VYKcE2>i3Qmh@{FFTpTBUWAAJ!+zzsp)Sr``IV&=ItDI>XqB(g#kn$0ulCn3u=c;NqH1{o(1* zjFvWErI9`9YC5$X@7s&3RJ!O|6`)CEPrNP9CBIw;UeMa8|teEnJqlv0qi z)6GZ8aJ}XZx9syBaGF>OPnLKb+uqsvZNtZ!dvKZp zDw1K{4$ut}p7%q!)OAja8Fk_vAu zy(Q|E;IN!!>!l7_SfuCzoDbtb{|_>*flx%OA}MP*v7H%?;;RLmqGH5dj(F98igdhE zNZ6NN%pjI2i5*O9LyEj|3rXH~2W2V*8b^FVyfs@la6e9@w~a3eAMTgwkfl&I7w zo<`Xk)hF^W3--VHVm|bH&HwT&_pFkt4E|)D?XyT?`;gRDP-}Rl{ZFMizpz5s0EJOj z1XU)9(Zq=y#dm1qaV(__mSy9AzCEAkvO1W0^SFf8&Y6*}%}{Ckgg#p2w=!FP_%kN! z*=O)hQh5`II{`D=O9qbz^R4MwoG~+YZetlwJ}JNR*m+*H%r43fJ%u|Cl&U4!vm_Nw zu`d5uZ9W%O;Eisto@y(sp&pQ4?`9LJf*CrV{>G=Yn9q%S=Abh9kP3{!dvJK{D*Ze% zef@HQT^VDqbA03awht?JT*dOxY6%W@ZQY}a+O@~plD6=GEe>deaJW8g|Jfo6YexLx zj(OLY(1+U_q-7HSFhb~m=HHN#9h_qc2`4{BY-bP(HkPItcDo$;LW?g`xxyU=)5woR z*8Vc>RVkOmA=CtH!W$jMBV1XKZ&E7{SaDBUnfD)d0>)w2zk4RosQNyH6+6|Al#z5e zwVbB3+=1aAiK({>W6iD7R`4>T=+@ z+%b{~eBVGd_lFUQ_7JR<_?5?*h3oXFq_lY3pY!!~75JdJaG7FdmSJBYuzRBn!%IY_ zr#+thZ2{Vz74JZNiG(0e{Q^Z!}Tzgzd6lEZiSlX!YoV&cFHey+%Lki2*e?uVZQXsd|H>a zr{T^eZu2UhY5Lye1LEE5GBY$Dc4GSg?io=tSC=48QdsKaLDt)Bt;x%reZo44edJWb zp~Zz)z-+pLw?!%7KI^~Eo-pcejh(Mr0Upzu`01^TtEI?z5}6!I@Cp{jSIsH-ELskZyIeEW2CsE21jOB>8Z_53U~PtYGXpGS=UL&bvefvUJ+$OQ z>jVgR0l=?x{PrZf2>IwDF5G8O!9-NB$xjlYJ9{GYrjGQAV-7@dU;`W4zE_z3d4F#s zrpkcH4fmK1=^I$TUoeHFRh`d|iWg3OvH=eP<#*^)TKx5ESj>k12655c+M_W!^hkF& znDYjfspkGLSlGOBW+PiKLgP>%!kOg&+f*IgEEkB+y9%Cb~qr@$P9oor*IDMO` zg1&+9tMsK~7WchT;x1y%G+lIB@7u}}0hw5J+<2sHo{$3(EIiDpsw*r`DX%bo>F9eW zIOP{;z&4atiX~em_2ckvV`WO_n-<{ia@E~9T%U4JOyVpHAZh*!N!!GlwxoeWeE>LN z;NkbY6)JD9I`qMpk3nZqN`KfDwgpG7OeHBO9`m1dhnfnUQ85wcsvuTf;xh@w2yC*0 z%5ssrj+V4-Dc>WZ1Q!$@0Nkon5~iI)WE+7WifR~qMqxiE92PYDY4Sy9R1_(JsQFjg zsKa!xM@k%-Is+hqTPj?9x!l~WXCgLYwz6>|#@BjN?3&o7#bL@CLHMv~DpCQ}p z9X_&(ov?9IxqWH1$9bO!x&{o`sBgz!aId-L$;Cizo#iI0)~Q43j~sFjj#hxfU^@{f zn{OL8lTK;%b!|{U&wZpbH92tu}pp4KUIvM6~UC&Mvrwm@~E04R|uC|-|lOYbZ!njr&4YQ|@{Y<}3w6_?e!?g-ZRJ1T+c+fxxVZpkP zFHGDHL%Y+#|EW0TrXpR=%axQUXSj<$Gs!5QA!eP8-En3Vg2p-ai^5+#9d((7DG3U; z42R9DZ+T13_P(7HqYy<`;zPnb-N`DX8U773eok}J=rlO`vwyhPrbfWm6Mu7h8?2PwlHqNesiL^D8={9>XN0y+ZQ$zW!_^5RJE!l)vClA%?lg?F^GRHnm02d|iv%M!b} z7X?n4Crsm1yj9fIWwSv9;>SiD;YY&Jq}q9XbFe!7Fw?CP>@^Q{O}8749G9XseCT~= z3@8RcrUa0E>9m=wR2TkA^`&N&GYnmCVWEOymA>C- zc*1YJ$5LAh-(JWfs4s_}67qUXj>P>{ZqKr|EpU2L%URfW7J!SDIc5QaM8kk=xhxV) za!ET$?A|$VpEkJ>3K?ca*a4KZsbjwp3Lqi0kq&nKr1fKDP|?$iLmbRBlcryFxx0y& z!j3>|YurJV7qW7g2;SvwFEvl?%AT*Ite?t^_KId(tweddzdqPph1IlINXyJ@KCh*X z{^>dJ@*m~yd^4B$g!>rN;BDDvZ37E2#3p|V)6-eJj`<}~g%KFq)G2V5p0YoWw;_;> zbhBJdGw>QayBIRiWd9Gq1N~a|!`YSfPrVt}1rVH~0P6UHKMo&Sg=w4EaA`spq8qiT z3oX^T)&72o{<~Mu9^C=yU!{)>1V=U<>Zo&%RGmMaL&eM&79Mr7KR^6}t)hxc(OjyE ztnULyE?rE?K72zjEg`IrYT>eRPM#d;mZo^BDud` zsUFlkJor~nfW73DMvvFLZe2$QU;OJ6h_x~4yyy#czgu-L?~la;jqs*Sq&Mk$q*n&d zDN9;ZxrH~8=+ovL+47)J$cv|vp7HJ~B_Y=s>~oGss?*5T2U;qKqyR zL@1In8QK82G!U*aMrqRpCS_t3>yLK+OynAVkVHY?%i*DLauo*{&)Kbh&x#`x%B|in zT;XgToG!ltZ3hs{QK3EGhAEDjQX+*=--uBb3~O~g;V3RD`;I1?wFxgU#Vp4CNOuU#7-I9WH6D1Hh%)Nqh z)((`nOW?>cYICW3n~2;*K@jZ~-tp4;MA?$0!R$9JfHu~(BFss1D1REb>97D8A}QML z%y9FU89WnA5s+qx_o~C8K7JD{B;Z4t<4(9SHqMIBG8LAT3wn;KI=781=zv`AG$~05 zLRjil!uFtNAurtei!3^Gz#)1Q=Pu0J$^0Aa6zN4}LqyOHFY(^r!&yfK$|6#Zn}v}l`1>Ky0QU*y*@Zp$Nehewl`s1j@8H!h5Rof>~%^t8VM^Ro_S;8d8eu zP_hpSceG^u<|ew}+qDfLNRin23EVyDB9EbDp)t~9aNpaFjHVC2RS*lT%gnAELU-(> zxKrEAQIoaqJ(LhBgJTb$4pZg+8CvJiE-}KLnPmu9we~hmpmS=sGGWST6&55to3X#epo}Tl6+5;BG8(}^eadzn| z)RUOkr7Er+mzF*&j-&V`DHavq>+#OJJn+GXq1*9J@xbxg@9=d--7&#oII^~@iL^2{ zLy0;M9D*sle;E=C+Y@1BLXj0?datz%&-4FyhwehA|B?n=gVCNnwxNpedt)PYU87ZT zvDN%{EEqlAZ-;w%q%5`6>A|}vd9L5X{XU?v^WW-A+&%dCoUDr7*ad5Wyt-z>!Pmbt@IbD1z(NuuE;M^o@u7B%VuMW`(%EAmc98vN*01WA1=X1|Iferb!L6 zX}I~X{!Frmo^)rXkdVC_wz-|(K!rbwXSqbYd&|SXtot~SOErj(BPTm2sVAgV5_s5g zy3Tta!MX7e{Ao*-Dq%*)In~FdbK9}8oBg^F)l&umQuafC2y(__P>Zc187{V+*bQbY zPC-({C#f+1LY;g%v_VqvYG!)lB8n^iINx6)|FE2Kk?BE8i5J2YMWYF)dN=Syiz!>M zTPFwGF9!mD!)Rf4*!LMKh0O3X&J{{Db$I5h^{I?&=V{X&s3m%NxXSN%8OeTOha2u+ zzzB@>@DBi&Y1s-SyHWriSQMTFHX|_rlF5^IPN|>CdT6*yot9p}r_J14u^Lyq%M4*6 z&wb2{;ckxT`F?eX?=$IBD+-QS_Ddp4NIeZ~16XWmp=DwPm*}W7QrzTY!}ze7VD#D^ zCRE%4O8wH1m37&($-#@!RI(||XjID{oeTg%fDt377v?JLn_I)pSyY`H49p7IXzfI+ z2Q^l}LLvCwJI3(zD3fsVupN*zU}w$6NEI%SoId_Ketxzmwu)L$YgR!UTo$2+Fw9wu zZ!`+mk+_I`RK!+SkB%!oyMI-i4<;;hWXBxwuss=;4f;m(Pcg{F7brBQ!+BeCJkP=m zGJn{Pwhd6`@P&D0=`6VuMz;x4!X@yzIWZATY&Q03(A`mHAcCM@dy=JF)Cf_V3R8Qj-ey;o%-4-Ws_pU-xG;o*qS}c+u}}1!)o9Zg4UWOHh3yM zPs00-g*w`hXYQS_&g`u^L)1M=^L~=8<3|vg!tDQO5K*_{j*Xs6C145t7t^};U^sz!kYyAfM$Bsr!R_`eIVUP4*JdLbe#sMYDjx68D$*U2s*V|=~9vV>oV zG9}jP@yFC0pWhDf?6oKIe&8~8#!TI&hv{XV(qPP&1d5a0Z;8Zy9XP^GDk;nRbT`j{ zvWp>wz}DhyMSIU}tYU~-|A0`Pk4zcz%6XXe=3uP7I)A`R60=pNde8i&t3&re$%CC- z?=GYD-qvS1l5m$}QOZ6Ep=I1DWhH~S9lxXydajQn`);9sJ)DCiKd$~{PI5xd2)ogQ z^Q3Tq3UBYu;V(x;CY=hgfce~)e*QFma zN!C%WeFju_T?U`PX}~#^q?^Fss9tbyQ3@m7AFtmMKW@jVWLPc$8E@U``k{-?detdNUDsf${1CA7z0xi`U${f@2P%z~?9*V1 zWF-(5-Jn2Qy3f}_EEcIMRYe&_MOrhmdje<5x_SoFaakFb7qvQyXLxt}IA9QLI_VuS zPmdS`IONu<08!2UN{Tu#7ht{B>I-FO-PZ(7=*xspIR%oS#_$4><AkSCh7SZWa@Yq|3h6eQL#njj^$=Kc)>C2=)Kp#=Q30P)yx-># zQ?zNKdJh&Fm!iQ@X_4M(7KnoXfjRP|v6Mf$GztaW6(Gu=b#$78aDIUL&iM zo*x?)^ZSC^p}mGw;tg!EHDoZo@QeSb@t%~*QPTo8s-BH09%)M}%h|JPkHhe*LyUS9 z=q)qj&6pRte-Ngl4G;})zDY~Z|6KHbc|rgu1T(u-Hz)?yv=g52mtyvE-H;+{hz(Vl zVdf{IP zmRA){mg)%K#I9;u)E(27vRT`TsrnYD;3b-=5bdQFWQn^N2oqMjN|p!-on`7a!=ZXa*lZ|XwjDHqX-CUXaeA)0A zxGZhzrJO9M$(rxuwYMZfJ|SW(D|+)v^I!nr<qr^6!-_^4|Q%~NxOTW!qMNxgjBV{p9oi8h7;e= zfE42@gU~Ax!uvmgbzSL=X^lNjyp%x{bm$^v_95{eAZX*(ift3dSkMesQQ^dCZEZto z3p7J>*IxS}R;@@?+dQB&s+~UX2)W@HU;7Pxj!H_KF{m z+!+^roMTqwoZP zcL`7alBiP;v4P-(K@is;v_>cG{aGnLMxhii3yrYlo5L^-|6@&XQM6_+wre`s~Y;~<-+9fn&r60phyRsSk zzrQ!xUwq?SZZCtZ&KeetG++wEOAwvg(AizB01@TshJtvSbmpJf#p0rJGK_Ln#KsyV z%nU<;s?q%#GylIp>Dluof9Sa7#(yYBHsP#uB3}i%O)TF3%45obUfGb)YM>MwLb&vj zjAY8aE6aY@)}EKSX`Lo4y9DIf=9bN-D0*>)Pp(6Tq!cl6Ut+&0dqCS4136lwl}uM% zjS&5A`bvz~kd(Ol&(U?Q>z%zn|Nk9o&cNI=o@h5+y5BTYV3u@umN4N5bUKZ2`tfRsg38JOj9J8oYkdyt z8%IFveo+GeQ;d}l*#)GSswHX%srkKyH>IMgK<)bj9d|rN6g8leb6C8K{N7PMT*M9thu)8Cb8OD@ ztGfjNo(5MiB&b%N+GHI8jg-}oq zN(xzA+Zcb^?tNI&a?K4d(puU=vg6JIf0O*)Q@kp-QsFjHUD);B_|H-35uln7$E@4q zCY+`$3c^`&HF8}jY~0mwDkQ+_43^P4LhaZG&HzC`zQ2?%HCRgmOF5nXhDJs<2kr*7 z4V1%c2d|Nsl*9*;8J0LkKCG+qgbZ)Lj@I#^z2QFJ2(zt{G8A2g>|i5Pr&jJcv_8*F zh0qO28(BEwX~>nz#1Gw!iQF)ba@9bOr#wCfr6v2m*gCd=J%KfnMC5U%(0Tg&V`dK> z(TTehos0qj^yL%DDMuZ01b}=Qc+8EI8JJ!NwO)oB92AtH+s{k`2~fmN>ns&@lkEhI zDy!}4q3vSi-9iPT9zHWizTVrL81>Wuv!8^H@bDppRh3E3AZH?Momd?k0fmK#o*;b1 z`G$@zBJ7D{Cyq32jd@EO-vVX?@Ei_;y7UmKGQp!MTC?q(Te39@FIVzE*00lu@K6wd zD$!K^>dOv*VD56Xi#5h(8zI z?J7{asCyqmZP)dTL?gR+#nag6fE_4d?AyPP02F$wP(8{Wp}872gO#o;@r6O~B4T zq58q18U&Y8#prnj_~K5xyQXXYJa)wMPUK5%jOsxjW(fzDT`;~v{~Uo0?(t&aN-~$y zXe1R#Nt$CYTVIaS)pO^iicHmCZO|y?Ur%iqI+&oG@L|E z*qC1Ih_zyQga>zaK{-TzV(M$6R*eg5mU2r1al{>CqiUm$yCZ3wOc}ng8-lH zqcqzIh4oX2F7QGHtzBQ#D;UCbs0---r^Q!lr$}j|r{_@{?_S%1yxch`TItbGt656! z41=YzeY8EXR{_50$B~LbWq4F+ZH&v;ju(Q>mlFIiV0^uW7}9cPM9ssDEMLTUpVZ2( z|9`Z7LXA7J`(9Wfs}M(Tm8EhkKW=@YC#NC@q%ti9$-C=sleg}y_bephlhwZPpDAZg zAyo<7Vx!I0+iHwPzF;N-On$ui98+Yi$iot%f#pW|XOnNPd)7`*pVK}+Ib>#Q4G%*N zw;s8_Y2qG{!j>QYQK4nQFFAe;T`ovzw0{M`v-L$7Wb>L-n>B+PBrY;8EWndn4^Hme(Y-)JC|bY~g#3wgZ=&>wk4KU9fLYfs)1cn%e6Tu{GTj^YoF zmW!J3<_jqCKxl>V^l~d-A#3`I0k>92JZC!07S6IPfGg3-dRVt_I5|upUY}jldPrY! zgN21W^2<){aBES~cgLbZ5qe6a(}waRs`L+)ga_vBaq#~<$akMI5EiXThs z{_@ryKs$bFj7jW{m`GVTa4>Z%6 zn?n}|pQoz;t;8^ywo5&Wm0J#c>_|EN5pQpwM;Q6+QiN3STsz<{qHf)hO)@?{n2VfR@x7^nPoR%thQpNq;i;= ze+PK598RZc$P1Bnl+$aFD96Jh#YwsA2mH)Ue1MM>HJ`X%e*8ZPHjjk&rhr7lGxE_WX-D!gJ>ga&7$@-IjW6r=>CU8rGK zH5FAO!`D>bt0oh?U&j&2UU6n01zqQw5V6ZYc6R?FpEtM}p(me(y3E&cn8(f|Y^HwB z{beTc0>FGrg>U}RHNl^KZ3u?gG2uXeqfo~&q zBpgbx#b=oH<27(x?e#uYfsVBUOZ!c2eY=SkLY&7;xz=D4)BrZ^!AF=-Z5^cxS2|-< zO3d%Bv6tk+rZZSh&E4w~5RkAA#TBVo?b3*bRJib?hj3fyM0rRK;f9}o#IZ4xO)ED; zf*aP+t)Q;@6`p|!=ei#=lD9fM=s1o53s97XSegkXYXylUCxG6(`erF}!<6qch&~Z5 zPQkV0DtK#vn$@^Ae$BgvC?^pJ5ah+H;neSrv>JwU_I$TBi#2 zRev_}hcqakSA32-hTXgtB-!c!)B4Y(w;vNg#sc*$sHdt(z@ip(u%dfo3X;b5{}Hd; z*x$pC%Vvf~y&RDFHIX$U)nxVo4#-Y?bHQXB_Egr9i%=syTvSFW35{W8LFkEdw|ThtN?{$IOC2%YMe;|fS`ui zdC92rfwGpIN(b!BJ1O+INMDTHIR~X;TvG2uL{YIH1%UfipMPtx$$p$*``ycArafY> zFqWaaj=)dMsNmnY|NLed)ZW`jU?j-!F5!vTf}GOIOC?!ydkoebP?d(oQwL@3`0V;y zbqgD-J29c2mo4`Hk1v@DJYf9FcK}{EgWbI*X%_YR%mUf$)pixUnd6$y!m3Sm2B^4q zh9u(DMcy_tyT?oQQ&RzmSvbcndUZlVXScU3W|mq8Sb6lP58iNIdT7(GP1mP^-uRTU zttWEv5|F)B;@o34!BkA=L!HQlhqg(#mqDT* zVQp!1_K`E}D9dV(b9;iaKb& z0+!(g4qI*!2;vi*j3txXL~uHS(bi9Cd8qG|trU-)6&BkYk_j{HowyH_7qD&|B0Pt| zT6Jqa@Y?ps!Ico=+|#fT5A+-S>W>`_X*ifQA$^Jcr4ip8|JDROTGT*rSqN9%-zmgM zVk+&^(2Y|nDSe^#VX!g#9NiNS`DlE}zxX_MWdlnDHt|qYv(}J&L%@4o#=bQmHy->Z zn;3#RVUJ2qFuTq{3r2U}6&S+pk)VMuv1nF9*t%CQDo`#uk!?{}sb+AMh`weEN33yA zx}7WFcwNmj^T{AX)3zO0bcSQpHNDHzB>gz@m$ zL8hw%hKhrS;7;mngfubTN+RXgUudfQ#`Ff9n&f=I@N*z8XF8z7! z-lMx%yiGJy?a{A0zfF%AA6}1O=}<-6oOPN7DiGhZ@Ga>>)ra-?%PI1Hc|)6hgf3scj@aF^mhCX38^h)!PRX zoJB+w@F#U71c%uL91($ zx(?0#?WSR2cxy5%5p_|U`6L}A0?RmvA9sD&V%rk@YE7P4lqMw+twyq9BjGqn6X9LF zZA-0;-LorP)T?P!M}cF#)hf9n6nxTN<~0F?%q|~0r8mArZK1j1jIMuUWp@{SL=#O9 z$c?|HsYENlYd=0r2ljCKJE_RFU8yJFLfC<%XH@*nVosuPuTIn!3)Lxga073U5?7c9 zC6XS^6fd816S2Yv>thP1^|5NmLJ^+jk~L}rtSf4 zDh|OiiqY;SG#S_fi~i#$`EY?DHV{Ug_^Wlx!^rWUs(?5&aZnWE3?w3J!`iMgP4XMK>>K1l^Dd1~sb{KpxNI}oe{Zq}4o zCv~AM3NYGTANn+=4>5E~yCZB>k}m|a%yw6sKP#PYt8QW|AIe?vPs4z)nWnBFrJD&See|J z0)d92j!m) z)h*I2VTlT=r?1pU>jp_aagr_9TJ~e4axn8;$F0cee_#o_wWV2ha zj)dHBwjDhl)mMnb6WKFqN1rj+uWghfHfPO0 z-unwo8iyU`grQd&m;5=eBr>+wjM7LN`RE9%K+DPnyb;|IWVMYT!8D;DkOi#G z`<}>x;^jOvE|Uib1jx*ZvWOFT58_=;p{0zIYHqKr4EV1V;R8GfQAkHS)<(eo8N0-n@pBM-SAFMx2=C!#J4g}|x-$)Hnnj`FxS z)Eg7gZxF>K{T&G2irZTqsS1G0tgTb!o#H%+ch_fK%CRo^RH522iC=Pw>BbU_@)P;n z+|+kG8GuifOm9roCWfY^f%0&SAg0njs=3b;e^0SY7-E~-e)-M|)G)v+OOmWNs@E32 z2!0~1s0TGcg4SrS@uov2X}rCKW&4G_ku5<}TZt`Oob`1yI!sy9Vhv0}t8iq8$=2pY ziuj={b*Cq&Ec^sFesz?%Lysww65mVjhH>2n+Q~da?yp^7t=}D21SZP78-b01xcf-+WgvU`Jz_ZvN~A&3T}N=imG@l_uMC=w+h zzfZa=CixOcH}iq~CY&+xURifnZIb7KB(a;JrwpFGNMxwp)m7>5@6&>i!!V4~&#_LY zvZw!fxyS@6B<>?XLcO(j?;cTwmUXN9E+*0{=Q#MUt=OH>L;HK+vDxzG=viy{0!m`z zMi$I*?T}bGTl&5^o1fsGwOekZ=5Voz4Mm6KSq2>WyxF+%2>Qh>-aSi`J{S^+y`f%G zB|u0wO+=!-YG?UDWxbFrFA7Eplp)y8kSoz$jro=;oqVA~DC1_25-cZ4CHZp!VTN>p zo1qL94g*8RdHnP8FlSmo^=N>mg{lUgmXrh=f3Kfn!J99%T(FQb+ z)Fo1t9+xI7CiB?{K! zabj_6mVA{P5jG?HH_#YZgtJv}G&i*^#=^#@Nx|aTV)`U?#+iu=7li;-NFR;?YK(6k zj4O(U90oZ-#;QfpoaR6Af{KrOf+oO;dd!wdmlSb#S&cSazHP;g=(2pF0&+?3jbFrr z>6I{IUmn~5I?=cbyjx)s{;KU3vtN!de4T#|0ROtGg&-4|IOz!Uz6HDYe5`$qWABFU zU$zr`$;4V6&5G}G)NPBJH?lRVaIi43egJ!qJXs$4oEX-&Vv8k-cK2Ph`;8T;oX{En z9gHG5b}baC~3~_Jv|*|5T zvHPimvF~rS7J(_46fY86iAmYW^v}9*HvL=j(Vs5`C}WNWeM5cZaz|g)3qjV)HekW@ zYV}SniIa9GvIjN`mU~UC-}mbi zsm`;|*#5>Phr9#v0kRBWg-Jf7P4G3{NEvTY=PK{e-qN~$U=ApOxd>g;32h`DI0L@K zzc3zTY-C$ksR_cC>wrnNk&laNmc2Cv-Hpr#a>MPVC5Iy2C+Iq2QztHk-5gwX7t8l8li=ra7{se&1?*n{MXJ-0QAimNZgthTXIfRJA<+6)Cz~ zZ7#kn_csf_3=SAK$luOKM0TwcbSIb_x+CJ?`2o{^8hel>Fz;yHB{zesGdOQR{L-B7 ztj6d;y3k~yHsqEpu;acyNM?be{t$;oezfl)lMmu2OB+G?9)>UMJaa+^(~ zQr6pJk~<+Kfl;YTvL+J%_o6^^Or^8vRSb;RR-Cy{NFeVnvfcMUs>SRwSvl)epaGF96o|B=7dbGuEE5%Bz~ zH#kV#CrR~;cB=VPmB~^GFW%Zj(^!%DB1`e=sEb9K+_Lz{juA4(!~Lm}Z~$2lQJRj4 z8P;=;sDi-30N{m0zB%zJL(ZD_wB}Gv}E~Z^_XYmlc<6M|8^BexB%s_WZmW zb#o1FWK~r;j1P0b89)rADBe#TbkFZ6+fgk1Yl^DzrF1JbZwcGTd`n&XfCfUhDI{w+ZOw18LxtGuIQRqABTBy`s5hn2IZr z1udnmBFdc+ualchJSGZ;(anBTVA(UY>~cY0T(`J>Wy$X1KhS7uY3K{98>tw|To)WG zvVc!Ks`aWfRgPDO)0dMkxyqS=uGKBWSQ32@lF#%}CYfPU)Z)4)Is!~T0E5H+q*Nbq z#H<_bEtr=n#y{Z7xW0kLOzHfxbDz~eAew~iw0LE7yT*lx?=?_%FtPI@*pwO`2kE3e zy=ej7pHwMc57sM58)?{69*od{&FTA%FKgbntmAW_UUI zWr3~^AErQx>aL;~nPa0K=;>&-gQZ~cBxipw#%X9(?1Uv)d`*Q?V_kJy%Gtuvqzcnv zCl>(!vd!*CtQb|LHRwO%*-GoGD~1MPX!_uBB6%Fa^fXMT|DzIG=~hLi1H0LL#TOFd z$TSqR(FmJcfotM~?hcrbhXu}H zubQ=jB@OvVx;K zD*D2q+Mm+E8;15L`Xf_?9nt{7`(Z+5SK1J>kXdEqS<7ZuxHLaAqYOj57noNZ%+D`a^olu>nqSDN|;}lh5~Z;<3AC{Hc)V+ zxV=|Q*MNDKnf{(LFxYKI=DtF6OaL$yC!Pwz}fDWJjJQ zFaMao;Hn7ufPctXDPK6W0Izd&7_LN5HYDj zrUPIfy2mI1k3f;3>Gave88D7lF8QCRqC@JEu4_=1mp^KZOTj}jrHs3Y3l3jq%na!S zRLlJ+qT=4STw2g&DpvJZRd?{w@@9`{u^}A=*t;leFA(F_x_m}ZxcP*i$-0&_g*^9@ za~84<}sUIzmkW0{_iJt`@(U=2B4&Gq!o;b%A1L|)g(W>^) z;GQO7;rE4|%!Asr#Q_cM4Aw%LNXJy$J2 zr6i#iIF5~8fB*2A12zZjpRZskX*9%2o7DHw0v#0_JTOpT$3hUe$S8g#3#`BrhP^ch z0G!_nI|ANy+-verzrYdi&z)-P7}Le%LDUke#>qt*J%SnZN*iC1=gAK889~~?0~cJb zv+S^l+Pz$`T#pK7#jk)`V8k6u3NTh|WH~JIrD+$J&dg;bue>Y_F`kB?X1OQ4Cx0GK z+Nq-q5u4$rhzXw2>r`sCuSMd!v~AC^ri(%oMr5>D9i@~*CcnVaXdmBhl)Pr`^{4}C zoT?IHyyz(|FNi!p2#Ij`pPG!;)X%p5$&#wBQ%M>A5~=)A^As5CoqsIUf* z+XOZ-^&*3>?iPF9=Poa_q6o4Byl+~=>?JFU`cb1$G?Rb2c3Vq$08qb^Q(mN5PPue} z?Sm$96y0~whl?0n7(8GfxIN#cPGugHH zKJGT;k7_=a#<=^}Oab}luvIQ#lUU^L;%Nz3Z-r^wB7%QTZ?!42h&($KC$9U6SQN5A zZSi(4F_GXX5_pUX;n6FLV_Z)~h1>aJkYqzE&5#=Zd)b~uya}z>n2fi4TiXpH3kJbx zYV})dd45(we68dHkD_5>ikAEyLTcRs^5+HypDmRF!5!Yk!b>H8stJR34=j_&m$JZN z4(#_Ah~}7_=+rao&<0*?>)=bEp<7*4X}vw7G%Vqr`BOh{PmjiffZ7H-%M-`W`&nGxvHKA0M@39LAr zDM#C!bRNOFCEb_eD#Y6L_)YX`Y}- znVf?n{PDwl$B4m^^wC)Yl>_oJuV$uf4L*M53IBH<1@eAZjIcrqjiVZLprr(usb$;P zx8hpp+ajElIwI%q*3bwJG^lc8;CXDp-ec|aWZGv7CYI+nb@ib(H`zl9)t*3@F!=dM znfAMW?jSh5a=oRW9Bj$DiT0}6x;Is<_zJOIcyJ#7l~+ueg69)`oA&BiluUr01>L`f zt}?~R8107SNBSnTa@4%N= z8E?s+7Vbu^LzP_w^wmE)S)L;bcGCtTixv?7NWA2bBBWQ&TLLT58^*|}O&QARG8$Q# zr{?BUf*p(&%x~o;KR4Da=00dv`rh1+k(Nw1BAa-jNA(ZMG#|(7Wn5v+cN)W}u$H=>-q}9Bo2_VzY6+$}rK;(YC{N_Uoq@CCs#i z<7=wkY$kY3jK(&?rmj2ikWik`CpF0DK;Roj2mlbAfujSmrUw*t&)4Z2jeg6;indx; zn|(@=1myCK&|1D7?vA9SHfj)LNLMR-iEg`W2mTFyHJ|_@1snz=qsV!^P=;|3IqaXT z1IM!gT%2Kb^(ueQ4`)*X2LntZn3EEYPW>7k>*<9&3HIY{(g~5NDcB$m;mlodqL&A; zpaR4QB!7GQ11Dp=46Oy*T&_5g{&8UPW{&&g$69FHp5>lfY264p;~=zu@BvH+38J;X zwzh9bmd8)Fd@9K|H0->`P-4F@%uS8}F>yZE&Mf8hpUwz?uv87q%G! zK5;+n>5CxY`W4nWt|ltrwT-pD#7KX1CtK#?S-{qiibRadIDF6@SvlriHHeh02&ySq zj0F*|EeOi8DxuJin~0K4(Ul52@U%H`Zp(GKy2FB}_ZF?Z5rpVrq2m0+JX1LwS!vu? z$qT(;+46{-Sve;4B2nme1g**eAUiZ=VXjRX&#dm=Nh7{)4eaHGKg+&lzmBAo^3q0m zfXM$&Pk#2nmd;nqnaueK8grZ(<8OnWwVcw86~T%?IdEz68QC>kP$09_%wQ-#E4zl0DhZK6akcN1k!2Jhh2GU0Vv+ygK+|gkx zDlNrZxYH&TZbcuopR&P8>~fNWb&TF+0wYHU#0`t@1Wn9>Mw(HvaNQ7UUep6u)Zh~g zc+xg*g^LOc;c2i0>kLIk)W)n0*a^6XLRpz+^xgSi+S!$*Qh}j1 z$X1KACaNI`x0Wr|o4Jwko`~}DLh^EfE*&ww723XFCiVCb5>~C|TcqQ0hOT`n@Q2Dtjvxt3?b!O-f?g$^E@#>~#ATe3a|4ip5E*v?Eb{n4t8IHtQD z0AZipR;B*U>YaF0gX5_ik;I1lGx=dwH(HW24RZR!Sbp#Owm*z{bmoKL>E_ST<(VlG zh=CC*G@+YifnYZJT{JYZDPs4?r+6Ag&so9OvTfjgF)dsdn+jvsOJit)$~XO{SfyMx7S7_YyW8T4iAtUuq*7j9{g`Gjg62AJXtsImODLV= z1PzQ)ISTU?{wWUzCcFa*8%o-^&qCFO=)TubmnpcxpfaU^kk2*&)DK{{yh&{&Jd2|(hqtui;j=IW7Yf>i#ysS5rl?4-04hS$WVQf`24Y6m!Pv?U8y7OkR)cn@n(lZ>ty zq~iT9cjA_xnW?g@nbKjjPLpbt&r6+CRWP;;g>zzO27vu9hVcRbVKk_k2paFv0n2*; zoZ_rX@VKwFV#92=38CVXl4dJ(Y1nEKeE9^PlwEinnbNO2=jhqbwUxS5=3&v5om~a`QP1O+)k(oL@GX&T zWn2~r^KHTcLN>$H$!yg{lOCeIWlYk4-;{zIgf``~c0@N2OrZ#uZ>xbxpFw)>Kq8-o zfB_A5?3?uwx8Z`t!MFc>Q$wW>N#4fdl|!1VhBSYZRn#)Rx|}R{8HRY=Xa^Q~tQCfU zk5FU|NV#FgM;AIbf(F(J)wzTCft|$P`g9L)DO+N<74~k$i5z7K?Ppf!Tg$$f3=L6u z3Z~SV%8o4T*kxg*M0uApD|d3N(NeL!N*$N)LPh<27?5QxcCZC?6Jm`SFFeRc+juy| z$Ynee18@bH2SE2dqjdtHw_?)S7;RIMJkud<=;RBGn)zPayArtESLQr&RzeDD_$ct| z`EqJwzncvKN|@A+nw^nc2T^z-?n6qehN#s96F2?_-SXFh{MWcIY(jVkz=>&Q_Y5;v zAO>cDk^otV{n=H}BX@b^bqUf5_xiC55g3fokq79n?Zc^ExmdeRdZ%x;w+p<`kBK_r z-tX ziRJ!LAPu>r&Fq=r>ZRy%JeR+`tHu^*TiM|jowuOA;Oc&+JpfS(%Lb0zQ2SSDwc+;quLv#~d~EA_(qWq*|R z2tWy^s-+o#dn(CXaK-OVi)dN+`r=EYCdY%=$cR>7-8mD(EKDfFNtHgDGj_mcawuo1 zRfjxjUYq0?ngRCX*yW)>lWJj*-eB_X2j#0cc~S(Olho2BIJpc;&zgnBxG@MXqe2mH zexUKrlfQF-lHK%lN$=7`oe!liBlr$AyXuTbifig1igVq6ma%f<`;wK_p1!wyA+8{1 z*NB8a8PN^@+VmLR&~Gzg;+l^%6yqRj{!~y(el;?N5p+l5aF5OTb}jL?*J_w=u1y(Q zkJi5(gjuaP0yNld75+P*H1FxCce+B(nTTU`w?Ltm+Ytnp(;P`CFdh* z_ZgddAn50Zg@!oI5$mQ1c%)|pz;BD9Hu^$WHnP@%^ngj{7O+%z7k7X(03X{u+`G|l z?y*r3tv{tS4vhzw(oxglCImPxV56f47jas!tfGJ61|9Ilo62~i3%EE6Br~#DJc8hhPjevGL=^L9Ee04H2KRu(*Y^D}KUA3<>gDjMPvyGwp&uj6L_AfgK>M6oa)h_w|Uib71@X$c)!2PRt-nNlOdsLmnw zLC*$>WW|`aga|=cQs*OEq_YZ>@v&&BUv7bSs(BKSeu%y}ocrX<$UI{f60NNJAhO2H zncN>*1DNK^?Ori(`F{u15ZDHC&SnTW-PgGgqwxEc_qESppCOn3oQbVojW%Y){enh9 z^4>gyEKgF6Tq{@fhjgGqp8zLrkuU7Wwnc#lGX^BQ1ePf(q;-@S=bb7bXsC#+I>t^e zXjV4yI*mqjo)vra0w%`^gEyH#G8?fBXMZ0JM*W0&g~yP)ePDJIp8E6I;$3cnS`)zj zNI=nceXiwE6J$e(IQ15w@uwAqC$e7-p{rIlU%q()c2;T>&Z~x%$~jiVaz>+xzr^Ry z>G+16f)I33Ql29bd|K%by_C+VL;lv_vp#C6NCJhV7Lk+kdyv6IEmB(F5II%K-&`wN zg<5$tRNUH|$zOVS7oIzR3B*7y!FJ+I__o2NGXQ3+@eXp@b8Ktr*MyY~u=(zdotxwL zB5wTOlL*f|TiB3ACIgBW0N)|h5_9iFO7>3bm?`@1PWaz8bAZ00kzThX3+Ur z=@7wk2*!$d3IOtmO*9iC;;E+qu5z=8G56rrrWIEHwt<*Q?QAZKl9_@;3&c*@CI6mlqcHgc{<J=litF@;-biBA8I{(l@hZb*Cb&b#s^v$ z1g42*LOw0qY;=DbXLr~IW6qXM6rqDP8Y|6Oj|9B`d(_%=Ar(cqzkwC#((beuaQTja z7MGe_4L@_=B?R$>iW7sEwdcuMjP2#jk=Om(s<^}|QICEmrOya&ZgHSVx}6x;Fn%sf z@M*KeWcPSiPq3+a4nXSbh)2b1agxz>{`RdmMj?$F)aM68Kc{M+X+8tF)pWcJ{sU%g zceJ7>HeI{lF-5J1nWLy?QrEPNquRMsg1h$U_$cSYXuoEN_JxnWb-qGR?wjB5w)bT> zgWt^10UAw{!3URgIUX}BG7p=SRQ) zuVmapXo}BO5@L2=H1+dT=>QoE@!2)MsU^TU=~Tr#Gck^vD#Qv5P|KzpRWVrHLd2fq z$zs!}1UaDXuuqI~(C(>m5@l?=cuY5_!%s#MwDDT?8btExnK>NM%Gfw>0> z-)_+DZ*4DNq3Ml)ujgskZK3#N|A+P53f+%tN3aGoO9D(tfy#@gGBpXyZe`sRMW2<_ zg2m%2EG=(+r;r%t2zsu6E*#Nh zvjP;_50E_q^*9l%{9jFvOwM@%DB&hBu&6y0Y>lYQu<8R+zj{BPEj0}!Rx&&u9Eu7u zC2VDZGqY{DKY0OjSRCy|J|y)6y0_DU)|QzF%ms0D`wU6vlGyB`g=oO9cZiBB6_2I? zc7D#knF2h@4-Xl6ArI&w1niX=Gpmg{tvZ5}7RV$oD|E_^eqt(KI32vw^7u$GS;fgAy;joy%QT<1zzHo3}X zF2v=|YR3B)Oe#4~c(9mD2qy<2Wm>Wk&|u zneL8hz`sdx{`It9tn`@*6vbA73#(dBp#YqhK?sm+P!ex-Ly9&E7jK| z=x?Q!Tbu^)L<6NpMmbUr$Qk9M{^Izt>4X_{-vh6$Q+BQ03or*N)eib0JHiGlS~*1}?IR^)@^vuZQv1W_wis#H{$Qcd5y5pXQ+2`rq`O=zF%OBX^Fk%4!H=@sw%C0b|NZy>6WVGfm#dW0K zebr;3;f3P@*vcRVQKM#{AdG~(3r#?poppl~y5Fbb5sur$mZsutK82a^M$kk)<#!SQmmL)MVS=1X2_9LEmn*Kqzyh z9i1eftwC`?FfCk&RK{=o)@vAqD`5xDzsK_DE@M_ZOH=FSL5?9Pvm+umlj4)h=$7Ah zH6^6nD~0kkmsd7xy{WtO)`q&y6Kxp+vfdFrxJ{arx#*M-H$EVkuhWc=p@;Y)W_1{- zJ0y$LUFWekZYDGbkvYxf`_oM(yPlpayK;-cM69c~=3&?Tz_gM~`IN~g^~%u(i9t{? zG~xVV7S|swq9S$BNHyuPnFEKV&2GEU-0ziGG;t1Bm(6z7qA8ZX)jvnnq2S(KYt9GH zc<*Wmo`Q6m#*_!!?p>VTQR)&svZPKR3NV@TA30i9Th8>a--;==Dz{P4R7!S}*@`tM zp>MNjNzRQN<{{y{uE=&Q2Sik}LKCEWgt?x66SAuUc!6aBl|&)GD$uSX)7)!^&I63U zXsmysmg)OPub#Q26(9Gbi`SJ)6@1*(@)tEVu{vbg5q`VE{D0ELha#Da=Q6}-ZH+Ex zDgFgZBj8N^<)Jn%Z62w{S(f+x3*4`6d z7|uy5jN5;za&>5Ne(50n5hZ?Q%Uj08&@~;)7adw|T&})ZpB=010!2N)8^UDnL|UN$ zH9*S0SmGkvJiRwwDf*Ic?R6-Zp>0OfV^lw#3}40}S_yCEtG4A6fM$WmK~y(9g!y37c8hUztOI9$!%LaJeMkUO`s)z0Ra@*ywLcTm-jr0y0&fVWil;-5NPk z4SBj5byYxEi!Q{;mNPI`8cgoPorqFsY~|!Mz?=4E#X46*;8)z)&F?vvKy`C9*xou4 zGTqnv^FCzpGC@vpb#D|V80?vrp&h)ypu}*M?}g`}Sc2C*oZ4-)%x^6?gD~KOb7+Ad zt_5yRs{@ao=u$v{$yr3(O4h+ki8j~dvzHnGmpn5OGtd#oE8Af)!CpV=C`s&w`h*Dz zOe8c;lUU2V<$wwEO{i@utwJtPCHeRG6CQEGoQ<>0)dpI_mblveIX9CZQZ9Dm0Ve|xJ9TOmZFd#1TkwI>t%biWE|Qz?{-`ZB90JSpPp zVFiVOP3!DZ3@c+(_kK?b*vHQy<01%@bC>zG zj-J%0&VV`JvjaTTSNxDc@IbD>;mMG6%iFi7of6!8*2E~0DfRcd7Ya?D?{-3!z+wFq zZK9j)D)YEV0CvfX@z9&A1<_&3pXq-#M^~v3za{5ErJ5tF zMKQ;Y(2pBVOU~UByIOm@vl6`NcT!J_@{B?qfDG1x{{~QYru>gy^&eAK3!(}Fp+m-Q z0|*;NS<|$6%fq>{{H;b>66eQoOtDu7WC@604=K?M7^~K2!&+Su1`k~9g$!O+TGXUW z)Jz4mr6SVmgXxg_jeA-JwToeb52{K%n&L=ZeGbfvEH}dky^fX7JF2gp&&zrJUgzDf zUC7d+)`KT1?VtmOT^MMELGxPN>vdU(4zV4MD{4jymRyQbj^XcM9ra=S+s|u%ScV&q zsU?O!>vag2_;k!9zFPd@6_DYMm?297$46!)P61mF?3HsQ>)m<8| z_l(yoTe?YOD#z`&n>9(}>1~ry zzf1MS><_N=9KC&nYD17PPk2xPka5N%%WJwvYo$@tqLbJ+QuiKNZdOo}wS&oZbIKHT z(Vk_?M4x|5qZPGJp@F2rY=XxKGsR0t%8Nnd0>?2dhSL~+LtC;=Lty8lJxbia+`UR&A4BWJ}`%pG2q7nVO$>`6)G1IrZUWi%<3Ae|Uu^j*iLy4EQ(){*5mv&to(JDZcYO{j&~@+={e@L<^`y|ES>xjZEG< z5KW)^Vmd}hz-APC6MUh#i)24K!APbS-HDGTIPOZmP+kLD!uk>^)Ha#8t8|Fw7z3IX?{?$qR+aQGQj*&^2t<;$Q z4{2LO+bkVM`T6OgY%6A-hf8Wd^{EUsL+%Y&?SPCL6^`o$s$5?BI36{_cq37L!AG}X z3J-)1Jh|P|F7)UW8OxBmLF@|x)xs|x6`GudpR>{shJ=(=ag^Ty2Sb#85qNrP!C=2?Jh0%R&lLZNk+CU z6l8AwI|yuODcrXsIV4o&U4C_w&vDZcmMW@#Kp0nE_sI-(N90{ zDy?^;MiI*eQ9kkzb^?+c5($&SMffFD+X>_u@<0y2?te2)DQFgrh)f_-fcD|nDUcz5?pao~Njd@`*oV`Vg! zCPrcovCwNFU8bO88Ox-!Ku)~MZ6BDkM5ysA zIUVbD$QV!-sP4$~hNKg*mZ#_^lccyLS5$=1n~wrwlbaW?S?J+KFwHkEvU9hn#9;gA zP@wB6Gb_Gs-rT7Tl;lWc1YA7*jpJRaMBVGDg<@aUTS96#gKrj{G;pDe2sO`H#oA6Jscd(gsDD@{2}(wTno77(L|c%eLsg) zZ810ZIDIl`!Tk^s%jsyT-=5hIhfKtN)DJ6)y5ks$+un3~;(~Z?zSMTrO~?Z;s*3fR z%=nU|zTV!hfB0c?Hen|XtfwC9dx94ro+^)<}lZ7)55R>>4PC>(Cyj9wx zygh;>qS%7&eV&-7cGz51>^<&5o#OQ%7`~v##nMhgeRBdOX-IfD%NgOq9bo?9*h0ZT zhEWT)wDAtFawqmmr5Kb45MWqAjInRam|hnKhiaa-As8vLNS_*`fj@+`z-4hoZ$D$O z^Gvly3@0M_NvKe5FiVOT=V%z+-kWb3`61Kc82~^GtNU`#M6pbjgtiIp6)Ckb;seUA zF*9r*yeo@?v71agKB3$OLSQC*lHg0W|645YA1BTo%$q@dml zs2HzKa2CA!GHtkHsxe8IIAXQSc2E;0XML^sFD~oX*7FAUnzm5 zeDqG&kIf3cdQub>r*l}qML-IFD_+pSs?FVghodH z9Z3=@lAz2YtTR{(TEoQCIjBq@9n<$9ESn-0iioLE>s5UXd6io&dMG}ODpR?=Io5ZH z<6y?zP1)U5$ywO7VE5*a9#d`s!r%AJoJ^;?k_4e<0&)|Nv3n+|MtEvr9Pqyf1Lx*D z*nn7(CjjY2s=TIC`|I5$*{wp9#za!4@Q3D8d%C3A;H6`FbT8HUTB=W?#P$HU-J76Q zJ1|i1lUO^Wj_gpt5>Q#sc6(WX7>rU2Aqemy5Ela|e5zodqJzEm27|mpbZm=Jk)yA5 z2w)5w?25(${o(PoyV;_{+ymvh_{hJL&^USe8_>(a3rqXH`T>tqIROK}2eII9V7rf` z2CxRoF>q&he0yCa#oOuJ47BGVNiha!ojI{j|LLH2b8%<*wx?KXSNREGIT_6wYiXyhS}c!NFG2GhOZ z$(`2&O_eQ=x5%}cbB61Z63`Yy2`$VtK@?-`$Z)pXyWiW|9E(f^X0`{fR>DZ9w(7M^ zwS*=);Wbohh74{Ly=b2tAyHH(W@-}A{Sa|`i;#+HsUIqM4-7_?*{jO^oP5mhUlH*5HV(b(}6-HzswChgOYse<+#=HpdIo0#{Dnc{SMrYb#fE z#LO$fNIgi3r$e9+{$jsCR_4CHlCwi2-7WuhtL1nu869-7s&cE=osV77;|=oV1rStiT7XYw8hrl2XM)J#}3 zbYpe63i!yasJyIO7cQK0og7j(Vr}nK%<(@)+V5Rs;5Y$kt;e+zW?fa#`~%{i7{Wb$BMzl|^x0CmjT9uW}B)Tt@BBw9?|Vs31js0_**V z1eRf_?WgyI2hvMQA^NZD+o`w|B?I$*EdxC~0h;ucE?#-=30nrnIlaW`%8xY8anD*w zcR-8<5%;@IQ0C8jqM~M_2}e3GtS;L-so{;q0Xfm9Tz%wSkSsr57@pwmR_pSey&HgK zm?&~;)??>Kowqv%M-#H&0e=&<@C;os|Cv8-7$|qU1p`eTCTN;Awvy(7&6YmfN7>Bs z9jjO;24xTyRrm2~f4%gMZj@a>J_cZyOyQ3VU)><$IJJ#9-P=RmbH8CZr}WTb{UX5L zrB&G3L>iy9ukvZOgt}K#h32b;`Y|l26?fgL$=jW1`fb(Ubb$0sH`Ui~UiZUP^w&+< zMZ(jBuJ0Z8uN@iYm$Fh-$*P(W*GSSu?=@&Fs>R4IHf_Q!3EKvf(+!faYrF%bTB)J1 zEnGn{p>Sk)feD7#o(+vk8Gcl=w>0B$IwGj&!~< z^N;keH$$fgJM>7(20I?)qZSH1RKg> z;zczr7Q<)I^nvbZ56+GQH6PC6ca50KzLFbO%UMfX5Svo2RZ!NGu+uI`igMK5IR zi7Sn5%^<_K00Cys&!sTUF4~a;{8j0llDff2y17oeJ)!7XpN-btX|jDa%-{?X z<_+XhqEcajle+*>-*Sm=Gow~wN6lq)ZX!tYmX@JKQy<&E4UdGzMo0uw?4QU1h-HnE zv7PEm6gR)Mm4tOqcd_*)F+Xy?yf)tK1B?!=b9D(-{px4jePTEta)H-bLL2y>m}t4* zI*TIXE+k7FX%{YH`ZhE;SXDOf%-m-0 zgGtTDv)B7-K{y7q&(|CQuS{n{Omwv~m$kOwPcP zkLAd+gv|P~v5Q*V3y(Rn*9xhw)a#?4l#5o!q0pv*r8jwA{_qNb#N#+I9N=zbo0d%+ ze9GNhmC)S2T4~}9nB0Pr^?&yK6s=aX#}u`=kuaN>e@f&$`InvK+Czwe2h`#zL6O0s z_Vycc-{?pl-hyJRUcA}%?l<=MKQum^?RTY>4i6L?%I`o?vlII(c7xNX6UO^WK4wt& zeI!em+L9;Y`2d4LSQ*d1Ro|C@Zv+y|4eL~9iec*~!4BCZ19|fkiWgoRGz0x(kxu(Q z>2ATL+Zhecs-snfaDRX==RQqTv`!jV5$=dh3~oz@ou6#HKbjFXUI=Q)V#^aPgz zA#L3?7GBBZtjzuhSf(-94C#yU1t0SgXr$Hbotwa3L#%gGW?L21f&Sa1 z=`(#RjHOHzy`1Yy9m}CcXR_E??Jf9{hzu6-CZ>emOmWy0}$(5OXm3LN;c_FVG!W$ z&g`r6F^j_j6AT}j6xrEUn7*E4SDGRzS)y2q zZ#K!$@bs3;4mluP$t;gd5Z{KzUg@xpcamQH%0f1VNf<7U$LRw* z7LL?^x>DQM-IMdS>M8)BeyYX?lGSemsSYB-HG~!U5PINI@1Lq8XjVEGH4`Cw_F*BB z*bAtDjXF(T_rv~iR*&_0CMw$NBw2xf{9kDqTB35w=6-94pH(b&uoX7FT$^0AN67e< zk#w__lsF&u zZ;{H>sFRSKjxbv3Ia(Ul$7E-_w}1CPw~{d|KU0E@UMAJ-*RBJ*jSMh9c3RU$&58S> zs(ngwt71^Gh{tI-Lg19~=GgjLL8>#;LQ6ZdBluqn49LFnN$Mf;7dnW*QW?(cBL-~7 z`c44p=O8XA4Mg)p)J!l=c_RomKb6~Qso*zOGla3ZUl?CO=%U##TVZ9P`TTRFkTKq& zulmF?HgAXh`ff^#I0+?>1qtB-mNI^D58wN$jb20k%#Ws9RvHdZ$ zyx&l-tslWkKvmuBQS`Ux$Ah<2s6F=(WN=_%4t3S+F6--j~ zy!#Q&JyIfoUkZU68l1sV3`d4Y>&p8X1JU?hdWtNSm#Aa`-QQpDh7-B0S}sOHCEIHp?FVqVq?XwqV3OLa4E|4XhC{V8 z@k|&<%tBpK3yzfBpXn>RhU}ROWp`4w-5RJ2x8Ytx$@dEH(~rg4jSt}UC)8mK$7;>9 zsR$_TPQg_dQrxO=SYbI?e; zVkUwxXIr_kW%$k~uQV+jPGySf+tDj;Q@0&jy#q6-s!F}5dRV>f%`6W=+!U8`{V)rU=;c$+k( zl6P`?ZU`y-+Psl(h8@7$j&%pi?RV2>!t=-QYgZ{ydwAkLVY>)u&4coft0rCrO@lNVBV`)#*f~QpT;fRDUMBh{x+C1GfzfBw2*Sb0(JA=T$AFK^w2G8y0Mrk+^$Y4 zE^z5PRN?&>@W;yAd~l#Ui;D3XF4nEgn=%*lul4-n)$g%EuU_fm!06U}U872nE-QdP zC`*7GIt_l5B8UxpDQM8tkBwdU1qs6eM zW=8f#5MZ~jD8{fC7*`jHk?WQ)upU&s$j7>&a1dgs#LGD~E^Sc{+Z4wg2=|X|ZD}UP z5GJIN>tm0gv8Oy_wD4FSAkkf|G}u`@#!L(7{81%_S?YROv$q;H&bVa_-BDvNcP%Wb z8pXcv7`@JsM!<9hEFO5g06SlRMV=Xx-4@?#IV{kQ8(7W(2@T8TD|Lsiptkp&C`D1% znVWqEmkfGM?Td{Oy-H#yC6jsvrSaK8W#702kDqn2t@fHEj4!u5u_RaRlawjEHCT!B z3n%wLg?r19hvYYT4=1NBdy>hCN^GUJ^X&7AJ2Y6iBjhyA~Ej|%o?=#e6T zZK^IGH9~3ux($F@HTW2HyA0Twsn@Lg8mTipFEq?vZAOZbnT8?7^N??)H+rS$vIu@?ihLE0WdX;i zB^fr{=?kAjoFanD>r)2-0S)2tbB3dBWOIC!3PTq5B9|E-9GfY{5WDy_OqY%GiTnF&p?LOSc z{zMyK-9v8lcecg#I8$s(yGQcJ>-)-1ODIT=uEc=xIpM)gi`3piH_d_9n?7Onr@gTtus^b~<7{)_Upmz0KX^GLJh38Nk zE14I&Zqy1~PdB^|$oD1vn;9@dbjq~jpE}S+l^+>uNkV)p(xBdvs3_nzcl}lbPYiMO zi1#_!Bw;>fME%>{p3Jd;uP;^lSSN>!Qm9oq`q6pcB6dt$SHizt#bI!e-u z)=i&aeTYPa9j_xeB&=*&s0d1j=%3IuRbFRw`9@c{)FhHweIRlHp4Io*-)(rdldz!e z>5y8ZYM0*yB(FVJ@w&8G{E!29=1bVE=@78l?{AFmtnDU{%S$mMBOw~b!=|Z&liSxvD}gQFc}~VD!DU_0^;I6&dD9iaOcmhL&tlPq zsQ)P^J@6@aDk;p;UtOP%##QcmvnVS=m~ov-(i-$S*tM*nwfwdC+Bv7fVGtE!z8Hwg zyEAvz3ZBVLuSaF{_b6?bn^QRET%pKY7#T{7dsXe|UT_R+QpJpKz6WdU8RghMT@O!b z&``km^HL9Dj5{g{=tYGp*lSvCTZ&~Nsdi0wc4nu-^1wzG=aW|4OYOLbnjujjLZr^~ z!h9k?D3Xo9x`lUNN$_-?YeY&;DDDOZVD~rs2@zg;_Z@9Fm;BZLd^gIKY`3I^)KGz) zmSCW&rWELkWp|fX7XNiiAbq!E#x4f2Mc-tmKQil9-*jh;Y&}9IGgWL{6;Lu|;^ILo|x?PwpG3H*g(y8s9Y5#RG znrMpX?e?<+=%Z@qAT&ZRpOU?|$13lVQM@&N|8998M2E_#I~4<&qkj?>d^xh?ol#Rl z?*b~TMu|zzM@%rYG{12D`st_Hd{vpVnTOEMzw3CQD#cetflKp zedpKW0rXuq@LxfDd1s3$kC>sQ+arY`+u+j=_`?}l8oXs=?H>q5Mvhz;>|KJzd>w1o z6gg*nM7Lktce>j}jFhW*^W*Dg+2Iue;#dVwl3>wD1;mhMD_z2^s@uDW^;WL_(s+c! zb^YIc;Rw&VyYli4vsiA+C5p+hfJ^5^cWNKAvJ{mFVdy1K0~GdMj+-Ke!F{Lv4)c4y zFunI0jxK3P&BfmJ_mN>=KXErD5{w8dlfUQLaf2SYxtM4NX&U)!+I%a2x>YGHaZ%iK zFdXm|zv6_|UvD9}r6rx5s5FYDc9#GVA=2n^sb>dTzZN>VWJbGR8?jd=h@JNINn!O( zirR1&hVR+(T#97Cc-*pP)7Y6a;`6j|dqr(`a&r%tON0!$?c(23~BdGpC^#WyI`6)v?r{*&74*HPvL2R)w4&b}VHI7oLjOMQbE15j_Cj?n2>@I?YW53_+3 zGjj)Y#{+E5%gPbxg20%mNATW3V}K8ann4ubx5Wm*&*fE)J67k(aPn$r#%^{8{eHwV zN8%{50+yMRnp(NTC5lG1)%6{sZpM|dp7VhMH09TQ8h8&qK6tG$B;>u$)A^m~Bp;Zz z`o8_XZy@*@ND#rE3knV!jrN~`@s74`R~8Q*L)r0C|In;b1W_g#{VdDm%_ZIMmtkWD|9;rk#t1+8AQA5uRh5**iH5b+1L#$g0}s(g#9A!Mf$ zQZ^hyR`YhcDhI-W-V}>s34@jJh!`Iu=nEjlMu1je!{@z z)ZC(!5LhXI#f?Fw#9zvFUhvUQ^Qq-gje5?yNA8%FB4`=|7l2YF2joVVStjbsmQ|~2 z|61&XrE_)r^?3m4-s4kszXtgr(NyB9N!kmJ5uTKunfyb?wU{K>w#(#Xa+aAU!VTef zi}(Ii`cB|rjA!A?%EA1{#6lm&qm%8pYJ-tv#4qlc1~0^NKru!C$4J4K+J-Vf=mn{C z9)U(dLX~oGkN&khx|mmweQ1g0fwD01a=-ihpZ)?uPkrYVY^nuVb*JeO1JNX2zGVSF zfC*+NPUJIok_B9Q5+Ztq!Hh@JSoPOJ4QKkwWD^qg%ioF;oW8nU=tVQO_{kj}|Dy+( zHuEV~0;&@mv*?B^#*f>K)GTx-&H0}Z<&=u_IfX+J8KtY>YY|ifJds{1CKMS4t-fd2 zRgOOH##!d#1&C;2)7xH=$@c2Ypz<#)Q=M{G=Jy_W+s_zq)JDukltWbHZMEj(}UcN7Z37v|0Q_csR3`FD5gcAnIH3)yH+*iqLwJH3tYZTwNIbT{8Y|3Ecq) z?(%W+Y^F4rHtS7lF_%qWVnCkMPQ?X#C9$P6u&h&L&|J zf*_z5pU_@k89fJ*B)5C$=08=hTjmXWrnTl|w?H0`Kv`bzYsp5Yh|0x7yU2Ob5xZ z&|{&i+p)G%eVBFlWV_``3(Dlo##&}5QwP;$nqH90;BFqxGX|I5li``tcWDvCFwQZ@ zpGNXudu%2FWjj-eHM=ZoARw-+T^aCV?8V)8c4gi>{+_XRd~p3S4m#P^4B?ILKlONHl$(+cPc# zM}~L7;y>3bx7B#(`otM8mMc2T^4^)|0!xJA;Uh4XejaAOMufKo&{ z9)R#mNsVcHjBEt-9IV=y`$=sUskV@^Nn{Fi^GMb&(>J)*=5Ui6`czBVf6?jN(7z;03rr!)pXd;1%#`$;9DhoNUJJF6Y98GHH|5hr&t(2?R4|wJRzObeXyJ`v z5+g{0C@kSlj;2D{k<->H*PytVPsY9?7^{TSa{=~hd}h>1owr3g@V;l($$ey_#=8|v zofyNEmrrw#@uNsBCYz;U{ckZ9nj@Rs7_Q8=@R+jkB*J66aTH(39P(%8@4o&5#gXPjVgzf`T_9HzQF3ST+id z^^FZPn)OQT?$@aVSSY6zleFgd|NX}4H-k3o`_ zuZ#g_xatf-p36@Lx{e_{pi&gv8Lm=2u#7{a>(me!=WDf-Sqs2eYY8rhPS-wTLng92 zqkXb{_!LGzY+`RzYM0r|&>^s1+V}#utx=>gY;)1_%lzxftkwi+*>5VxNzwu zoxgJ}`yQIyOx<{?*}9uF#*ak+wzeL1M>k+Kpz#GLtyjaEPmOo=iSHik zjHxXD+{CU3H6|HEjH#S<@tVDU9=8z&xi-2h^1>^eHc0O6V#yCmR;SBiQnkPcYoy|= zxv#|sPqD>rx4|)i8TP(_yR%1|+h zmqTA2<1bglR)@aCsLR615*7Kixxgi>{_Q=VlTby$#zVOSPu_|{W^Qn~9uay{&hiye zh?0q=5bs#fWIiU6?8s#dkzYRu!maJchwd9J9`b7jD2+4N!~1GF0L6T`tpf%`LDD{5 zLHYB=gR6#=vT_!B^cUuCSsT4-lVi4Q$87jQITJ2i=Y&%O{7N{)f+mr+rnZk5Y9&fB zCT6Ba6E0H&vtyL>s5i)IkdSH6RcH0TUe!BD@AL0r%XSGkESFHlS01J3wjSc3{ZbI! zite4RCvJ2cz@QJw?AZ5?gosfG7ewqCmI4bZ#rcU)y6!)&c{ZV6zG70>W_uTtpn_K< z5eDP7RO{1MXgDV!)Ef@*O*eC4!vPvm&3CviU|W4Qv#0>hc~%n0q{{an4+EqRmG2Ll z-2{@-<~sc~xA8_|{mUeUoPFUq&v0zP`r|lI&ikpwm!#j;q1i`HlVAHX+3oM^?VL|9 zIkg8U*#cI)gc$&l_MD{`&lE86^}ub}oxXZ845vv;DuOthG$=*o_fmUxec*A$wo_7$ zCnfhBTC&@7sJD;e>JWM46*SA8@6=P4NeVStyw*9`R2zq<8yY=L(C|8nvAl8E)By%) zbZ_j;Zi2jgbZ^0#OFyUtM}2#wuEAkWvV|2$^VPDp|10%xQJI%o-XJ$Q3|G!O%m5wv+wh$L zRRRB@ZHH*j#P+4;Gn$#MV&Bx;lh@GZB7)+V%MFXYcXt>RCwe9UrSdSPB2|vB|1b%w zz)HPbr_P&6y3+UK2C%bXe(_=Lh;S54Fc6X%2fp)ISRL0s?wq`LEl7QoQHNk%dA>Q4 zu4+N2(vS#n-;AFN$V5`YN2-UJ0&_S=TQpXBL^(=HS;J5zPW~^GDB{qmWKwVM&F;OK zZf3>135%pgsI2R0*{OpMJIo)g8L^VN#<>a9J5#j4XkrW6lRTt)XG>?QhtBXcGN833 zTKV_m$NX^3^%I2dDjYBN#1C#ICvCs0NfP}U9${j5KA%wj^#Fo=Y;|DYVj%2wbUNc| z*kGv8sskhQ1_MNJ#J$CscVu0Upu6?{xNi~2mEfkKe2+7V3%hkPQOcz=t*B6Ti%t5u z?h={XZdA zA9|QcbzE*CL6j!J70l_$GtHf(>ET@2NI#MuH{?^{%|%9zQ5LxFK~Q>ysHaI9Ea}B7gGiGk7T z8Dm^+F75mIN6Y151I%uasN=u%MCyS@4;S?q$#{b1i|TE;i$o3g3n^E#P70#@?fLxql${$h?|TnA&C8*TbbeC}ME-sQCwv;eeu_GS(;X{*yMzr$g4$a$ z(DXcs;p7B|ux8884CLUFe2P?PdKfCY9u|lcf@yKOU=vBNdBK&yTM)H*5(=R~3nIb@ zl`C)NzX9@pj-J;l=)|4U_gl#iWY_7|alE%+ZGGwa{*>0G(7BdY%i2ZCsTV8 ztRO5b@a=6*_}x*By9V!W&DabgXdps8gh|*pmahoL^-d^FNb}qP5IaR zr9zvyv2yk1_Q;q! zKjU}d=Xn;R1*92qTKs54^L`J|rN@{F^JKudLaPTm5iP&7?zq~sL(6Uo;?Bhi6qOPF zT_54+)`F89@%@tvK<)gv$pT=?m{obh%^OT zTJKMD-Az)oMB>o2F|M}zPP7oj)eeT`G!uQA3GjqAhWkX3ZC1o(hJ~HoB1Kumb#cuw z_Xs!GIQ7BTwdGp+h{_~5y%iAi zByx&usK?%Z{^c~K4!_aPo01&iD zIrqb@IP&MxI@TDn`D09SFaU*)D>AP|m`7g9Al+0^wu$woWQxm z{(U@J2#aQm0CFZ8vfoBw;Cs*D^tj}D)^P+cTGF{Mr2d^Jot!ErE{voPT%+1$ZxlXS zA7{_4QANLCGo0J=SoyM($9gpkjEF~IcdS_TB3PRbiXfEa&+lb)>+!3`qJ4J%>X>3KO&c! zYv7F`pOi1(>n{5A1L`&6KzXeQ%Q&2hrRTPak(vp;-?_Uc6Xdh;9wyRK3|6%-J+#A) z49zuzh_QazB1g%;xCT9Gl#dsKx6WH(h^lXSk+-$gBQu}4#*ww+YI_kk@Z=xHDQ41_h#z{f2h`tGHV z|3y*!^n6HaqzcLbySnR>Oc3btfwGI?GAuz(4?U-#fs%|570?Gje?bqCN3 zYOdVvo5S0vD>=}a^#H8&vNdTZS|92iDmL4jlr`YCi9_L836~UT3VENe>|dSk5I7PZ z4(GZ{%hM>ZA11`_2h8kIVr%6Z6|6VFA2VfYFODfgTDdR4)ztROI@DM^?yMsb;rn$#VPFXbho}iqEskcx(C6MG#k$iVyEy_@2T3rBt8b+> z0b-S=D<-x;m)>S!K!hol;SW(Xfp*Co<>|k?>6D(g0L}N&zCvewL%0PZYARS0M_=0> z74Rxe{vsSaq$u8wy6k90=Va|V$`&a}03!DphnZ$>+SRXM`K)*q8SR+buUY88K1Cq> z1>~b5h*8o6FVAML@|dtze?0PPc{4=y2S1-;PF0kjK0Zn z2xz^QOuze@J)9k+tn^GTg$EvKnA`$<%+h7PE4(DF4y(lxf4X_PBUOkC$B)^reEsVX z;g@?XAhh!g4!l?nJipZp=`$O1yP?$aG)N3}sdJipSJo%HlB{k22To0oCT5 zKxK`=UeAjLF~LhSwmAu^Xb1-sebK;Otd>Ct%}??gOk)i5YAP^^=z~H~Za~$hIbw(! z(VZ=tE^xqsD*T5%#0x!7Ics+oiwz29^jk-U*Z7`M+r*d znohApr1e~cd6P~zwR!AtN>wEF1>KA}Q)sABRH(kB*4~o(*o@I3w4>QAgep~>XL0sc zX%e}&S4iS2MmBm?f^Jbm+j^cSA{;;t;$4ck-@}&j8l_dc&O41GXW&ZBLG_z`OCRl@ zOff@DeoWysd}bHFmqnR(U+(xKLbr7pA)13dl)s* zwb>@YRN{U9Y@r;ZPZwk`131w`;qwoI_m}%9Sne9qkLyfi_~8B;?rtt^Ru67GW-krU ztmc6k&W8~{p!uV*#tyZ;>&!n=4$JXE5!+sYF;AGDXe&nv5OMXW!x0`Si=Josh*5qz z7~OKl2+}Vn!r}&?S5{5-SUm06h{4EbPG(I^SZorvcY6*ICE}N4t(X9|zv{YhKpJ=3 ztv0AevP3R80}R`$%4W8Aq*n`!z$rg;>MN|<$0Gd?j0vO_#$eQfAoR8Plp{CH)Zu2{ z)dW5j}%VqF6ub$5ci1ofl1U5;3I|SEz&7cRUa&Q5!=E7s;Vor;02(lNMEXs}eT}y7PFOP$KV1R~}FBM5Y3+du!0qyuO>l1woB? z@o#iedpQiIV`tH^Pj?LoC&TAomdRG8M<$@+W!mZ=j$v(2$kQ$rK4*4V^}LE}YZiYy zLCdg{X^)83H;blzFbU-}D^t&=1%->EL%#Jv1EaRVe5zP3A7AScl9ZhQyAS&oIl5Z@ zbke;*Ldi~>G7Hd}&r=uaAQ3K(1 zqV)Gj(LOSD`}>mm*Z;6ZnCr{(YvZoie=SW3qOo-GhIzgW$*P^!mg~<4q1Y99ff}_# zi4MJK|El?49xxpWb{ALOp12FMxqC_kaZMBsP-VQ;uOK+97#=l5H#bCplpRa3rjvSS zxx2OMH9Q7R*woENYJBX_Z_8FJ9={b9K5Hz_L5ijIw|8V#7kSv=$%K-3D&*W75FU$P zWDSRkq#&-6^%)jL5bE4*uO$PL<{^`h$>q&YAWBHKN2?`SG4W+IWWqN2 zAPRNbPU%eQ|LWB^Bq}X&08_?AETG)Z>|Z!ge6(WoM#;6zUQ(`nwvt}awcs^zP8r*m zf_!llq?>_C2Tm_xg#kX0g)`zB#9tGrZnyd2Nh&aLd3vloXoaYTzwe}F@ck}dxzQ={LurFHek^Xl_cvbEo>Yi7Mv{>NS(;yAcM!iXKgEHSnk= zO_r8^K(ZDak79zii#fPq(-_obe@q5^25e1mV29?8$SF+f)D72liUO=BlHM`w+}nyi zBo(0M983@9X&3VQ-!h3fjWU@y9Psb=g8FBT(`LOx7y^bG4%+dKg75Io;m2X&* zB`~}X$%M0`Q?%AohBmK0hhc7?)oP?FGgPnvI@b9%-1%dK(A3Q&D*58?C=~buwqF5P zYu&eFA zjBo`dF=~r7%{t3WxpFD5?ar;aw((=g9ebbXX_0?e)(Pt(HUi~{e=N}dkc|;{dPFLr zW^%cSLVT|m-42cBVP4E5{+i_d#$6btGE9S}Cj^l0F4xX(e70v8;HZ z`+{0<4|+$t>L5Wn9c@V$25SSQRalO{PF>vHEIjx_4rp!b5H}S&jU|%*b3`+=;X-j8 zFSLJ}NS{C=P+D}H6=io_=?e<4qakF~(GR~AmavDrz7YsGiMoD-S~UI1vl^_M;i{Ca zGi4SeF5I`v$6)-HPpVN2ri8ISkY9t5r+u3n$t*#BtoSfyKEA+2Ujv@9EyWjG1PlSi z{7^K?v{4CW%WK96xcGg&QC1A(c04h3Ovhir?^g-*+Iu4cs?1Mq;)pG-#IUP!PjFgn znl5ONgkL`k|xL5~-UrDpf>)3xyXMA|2-kW3}33 zXb+VclooKlm-DhSKWET5{;9;DzTv0XzTo5MW`B@eEM-qNHmlK4EIN7(J~F z-H_$KQJL#D==OUg8Q!n7L8bA>X%4fJlx~EmbGN$B@+wI&Y`DmtSe^Th)-b!-QDm22 zItI9WfxfH!4;29FHAhOiSCK{Y&V`Dvk>H+)S@dw1FJ@G&z&LJjj9S5QA z$o2ro)W;~-a=#-T?4b=m2)u5B&2B{6s%rE-s^dwZ>+)qiXjmL^)-l(TnMEe7zxtVb z6x!@P=mSHjUx0gcQ$A#K`yp^xfkMR+V6c)0kY979=diUGv|h8PhN{lM0XkHxZwWfDI80DjpHrZtBk$piMgx4QsU&AvTuS}Ug>F1emY3&!Q+dHDpfZOg3Sv+ z8OZC(A;FV6i2@y$!FM0*uxG~Ncx#|lm;zFE`iZM`)T->bTy#i%qTNIxlZ#N(KJg!7 ziHrE$RX`qFRsrUpt~O!U65fzhEpf?22C^yfmX87WleORCESCW2Tvbl3veAvj=bw$& zBaSiu?}4}sv8KU54Lj3z)PfZNgLk475G{&6|E1ZoV|3q&qcWpV3H;8GTs znLQ8j@W`%R8rV!y@!i#lKK#&W-5deffpHb1AQ{5|5!;=&>TtZqp_5n6wzL|1IRJ-= z3frbV7D7Pq$y}8K>qHgy@*a2JY&`jxU(ISJ9wkiuiWGpp}fg6|PIa$h0 zU9&|g93XEtQTvl0D68FNHKv4eI4d+|EPC@2_>(#v$cu0EbLm|`m?PVfXmp|=>$+~N z`VP+fBv+^#tO&RVl!Q_{2uFnOx=)rqqEE;m_BoTxT~_jc5W9Bua8LJP(w{1t4Ijo% zXBpj~fO-)T`9m#rNGjCBoG=beKjH#L7zceJwyOmfkL~3~)qvDP8_M4~UyomDz3kcC zYPqY6hJppBEYbGr0)Pirwdz*G0=QS|ijq&-V_8Tt_@{nlB-@iI^Yqavg<_+@)W zM;Z0wW0q7}(d1%wydxXb?5J!xN#<~`zpm#}V9>Gh*kXC29%*KqIw<%Ii8M*(PJae4 z1L$wHIe7dMJ?WeNc)VM0N#OWzY}IF_neMuWH(f_QFyU?SR(w&C5uaA})Xm<90ouXt zM~4dfL+NDx?mZhRHk({wR}lbFL%B0<0XvPyCb$5TDknlTv*&8W70NZogXVA)nzrCr zgx(oJ8%kk)a5$!)I!iN6d8?a<+SW#2HU)X14AMg664EEnV;CV)m*OCUtaE^+BnCb> z?#Ln?=Q@T4cl9?d2iL^hZ%h^Bu#TU|U_M=hcnW-+?}$!qzJ)8GaUj=Gc^{N!5zF#S z#59f>j^1>4M)`gmtC!nr$q!aW+UOmHwD)V#^a$lV8*2TQ5_x?0Q@qdpPj>$s09#MP zG6K?1)T*!@`Q60 z_k=lX$v@>Ll-&94@fxu(!|sEY&nDnJ$%&G7=C0Ta=q_|%5j9ch2=!xMr5V1GAH9~D zyIB0q&2y}Vbn0IHRp*mw*|Q4a+-P&kK}}pcTY2ICkzh&IujZPd0)He|D4qoJz?^X72zUM^kva!c?hvuOt6 zt|Y;ZR+cM%++X1~i>AnzU znbXwt$5bkHAZQ2t0oh)NqH4earT%B`X1mpwJ9NQ;EBn{)fM-f+7bArD09hBCy*+a2 z|I?d^{hfRbekWhrZeMFIBeUz$o4N%deY`zn`M!vY#t1Y7y#+^LB~nVy3dsYUx1ae# zFSB&;qRjQ_47dB21%Y0u&n>bD%j}z*Tz6-B>TdUA$&l<2HJZTtYBW<*9vQc*ANcJ} zt5&P)+$9-dt6U%_pN?4dglNnF$%sVP*W{X>$zatrnhgtS1np*VnAx<|e%FZLM}*wB zq7Q}LqR{&yL#wt|qhvw(=fnTB`{$1a-{9&MO3(CYynoK(d-{|pFhr8$sF?xmwR zaAORUa}w7gqxw8Zh?4&oUBfSm({k#uunOT81JDsSSOB9e{+G;UD~ms}sD3U%Ub=2^ z4u-waDM!E+aH|p5v!e!=3NphH!5=n06gJr-i?cddT#m&rGEwSRtz-}>8Q)+8 z>Wt=8Q(ggxK_zfYcWzc_#fVi4CA2egV{UEs*;-H6p7>J{AWK3!ppX1x46m%+ogWdRG=?}i zxRk!2i}C9!6@%#cU(MR@)OvsGa)pY|^Ek8AM_AMSrXgv>cT7|xoo{Z;OoBstwzg237J>Limk z81)gt=y(8BSrbBe#4jHG!ZDUl-i0B9%>b-=jcz0b>c)S5p`KyD9&r}YB6$H2LhHwh z;fe(6c2{CBda;{Fz2V_8=DCN5do~^9)lVoBOpAA5mNHn8GTmhD+fFj*iOmo+e=<8b z+q&rm`gGmjC=={MyZxnNj(~s9!?yLxWEC-*uMacr(P<~OJ-TMsz=V{D5B_%(T7;g5Lq)v8aarap|7WyZpMzvK@sg8=22hHgm=m?UYA;Q1(}T|NRpI$Hj+kk@ zrSF2+_D6@qkU|tk#b8AXlCNRQY&9vyO%g9pFTmIuu;-Jm&8%VhV>ua&_k}x*4NaH* zSpRV8tGf=sve2)8`UPW(A@9eEPEkzRKB8zRC-sA_}dX?gifMJ4}xFdlmJhKE_on$bRufJurq;-h_s}gH_z!ASX zokxmYB#N?gWAG>^gGb}2%FC>)fMzRTOaVz^>v~nvDAnnrAh`3H4T<(Ot*G!fD$+NU zDB6IYYJG%hxM}z-pM`22cIUmDI@{r#3#!WWoHKI~TdN8N{{0Pyu%}Z1mgs`WKpq^S z%^uYZQDpfRCxR0VK`S1biK)%yhR#vg3&8!RE?4uKQ!FjjdAB%OuaWI;M52)?5_06k zd@|TE?>$ud+Uu~Ww0WJF2n)8xH*KLViLU@w1R|zYs2mjVT5m}D&aqz@5X(bu8+#(x zhSS(Z<#Wly{06=5p)7{wyA;yJ0h%jwF;89gWi3amCgiJ>^~-Q1=?@Gr%6Swo=1~Dv z!mseooP9&h@Z4=t(c9$v@8sLyatXGmRW7$FjX~Xo0SNFAAPvpFxV-OcMLoe=Ca#J~If2^ulMo@(Fm>3Kr5 zQa^E`LUv;V17&3MEw==rwVWY*pb3u9QA(9nv7(>n!bFE_by74^Kb8!Civ`l-p5X;o zt-3DDBkA7&)da!h1b#Z9HQiGO)ovsNzqs=$upIs)0n7u%VApeDi{CuCI4u-*l ze3lW#VMEz{Au9Mpem!z|==GDE{HOcRa{=RP-m6RZ8N+Z5yQO@H3oyr#gGd|Fh3@NZ z@mz}b?yC?TXOc#3KVIJ7BM7X<1lIj?uvrU_Dk4x8xy#c~<4~*vt_l#>T;U)xJN30J z20Cp9w%v$AuES8Q>?~=evRsU%!%% zbZk{e2tc$0+uw{5HoL|y`|b@9?I~-io_@B1%c-q0^bY#4g--@$R8in|G@!H~8^Ms4 z;7*{{DlyRa4$M`I4*SFkKx1AR9C}TWRn>xmHu9^KHgA1oYds^;`qIz6F&MHaN-fN^ zKMNXPoY?8ej2;4oAPsF-o zIMWyD%iCGaN|}gthwtYRv?;iEJcssn)}YP<1c0F~s3jzoXQrm~dA0B82}wsua2@_` zVS)Rxu6B47Ky;-mSRG<5Gyije1Af2XBmw6tC+K`WOgOk#{##}L3$OdG>gO)Nrh5NL zLyw(v!gg?4F2sFk=O?kg1pvc(1ZG+9Q30^)M!$q$)-znJY+p^$2F11HEO>{oWA*~? zJbM;N-htM@8$Aa~fi( zN;*OXKYPn-Y}PipDPsj(J}V9tXS?Dq2A7yu(7Jk1bJgPBv>p$#qHa$kxNTlPl^4crB83==E|1u>8^#iDj~-XK#%P!gC^m%bDR_!hbeqrz{<{a z?Ny%JtQm3MirWDY8HfC9AAjuvVDluYGR2lge=O!kGHAkiQ>c-Z`INR0hUh5JwY1k< zE=C5PEKO50iN{x##Vf=nexpHTi8`KyAk!Z+pxzoHw9s~45g=KhV7ECs>4AYST@Gg; z*H>Eb)viN3*|>ukSs0lNt{U9xeH2s2)Aq>`*ri}k;9?-j57?;m`E4D5J5>E>9o(Y~ zA;iBv5RDC*OhEclrW1uo3{(qp>_gtUXUoHF?gR7+_>vj0KP_i)=2{|Mff!XXDN-IJ zUU&}jz2YD}z>#tpbp~Y49;tz$)>NE?hkvCr@sX-UR26II6H($j)Ph|q z0dP;WZc7JHdWEy^2;Du*2Rg8^dazAEbp$A-`4l_6 ze340|&h{b0ZJz`tMtnB2GbIWpAyR$iD{P;qRBjMH`U^iibS_yifBm;p*Z9x+gL4$} zCt|;1S|cGy(Iu_$*Desf7*PcDGm93j^eEuQNc@b-0qog-0yHVDfzIv8#`tu%!>c3~ zej^%$8mc=M)l)#T6{GHV=m1{p;P|Bhwz^xjztbFDtHRwrv}7cu50E5(q*NgaFw~X< z092qdmnn#iMr?BQuk;1l-WVGyP$FXDBer+YPD20V4N;~6LW`%{6N z;dMR<9*G0d7g+xW(fIj{?Tyqz(cb?-?5n&qvq58B4=MimEsp!L5o(CQZC^j%PSq@K zHvWp*P4ZBbJKFW)3;qn(3Pp`F&@lO6qU~7*xy@gey$07&80A$Z zQ=OhUS%hS7$jbkJ$O1)gn5f#u<5z`dBq`m0o^=Wnu-Xg?mzGpP<)DYaQB_x2l@n+~ zf&e0$+_fWL`Gx%G!!4lBsh^AD%lN!L0dVMJn zYsC=*5)xqk=7qCYCVh*Ozwh+HIq2YT5_i2R)uNZ@(e;NZuD%>0mH1Z=%Ld47V9iXd z1>nq8d7ecJ74V9mG9pj@Z@%6=#_<(_lvAO|PzyV(K*0YdZF894*7PFs&!bIL&@EMNmB8;1*Y&@zyk zUvuiC9^9+v(fx*plQGEQ266Y_;bTn6g6_E#GK5R`Gw7g*Oj3Q$A4ULZqR4@~g?`9G zPg86WR#~9Qg1)t(e~}*5ezm2Fb*J!ncc)^vJLRVoMMZ$MTHFZFxZ#Z*P?@8*n#U;) z2BQ^-dJi`@795m2aR zY^K}1O48XIqbaTwo;Dw^G+ZLlt2$KzCk7S%E={K18N%fmiSMUJi@~R=a!nIeCZL#t zq$RBKz`_49wAXu{AIe(ep(>sH%E01AMWi?fe$08;s%X*#)Q6|8jNOVha;0p9V+IiF z013(C!1)GnKfdBg%)_EzdI*Jf0@{{miy1}YjY5;Jbx^AHa}bdhY_27sd;2V0W}nY= z_2YH}w`26?DXEhFK5OYL%##s@DSn9){AwGnh(XGCU@^73-cOCA;Te8{x%gl>>q^jy zD*dcu_4*<*m$n`+o3BpTx+xjGWyNfbZtN>#EPye$ifLllt+N5>FYS|zWO%9pG@NhE z(|=sWy{tg1@@n2ssPN*?1M{w*grMNCAo*FJOH`4@MlyFnsSty>X-^HfG|axaiEGDu z^JC`(+0D|r=Mj~LL%Rah3~5}Oi8_B4Lsc=JorS-oDS4@zF< zKOQrt+d$b6`r_f%^3XJsS{B&~;{q7qc>E@KMXP=>YB2NeH*$T)gNqo|#REX_yig0y z_l;`Q`sbRKFZP&yCm*>9Ka&ZT$zft)DDBl6%5x%g!rEPdYM%^3aM=NCEk{vzjAWv4 zO%WfVH%S89{3ci29Trhqa&6-M*H2_;d*|qj39aO_fkck&mz_CUH4F9cJUg&ee!nLB zN(?TRseip0T6AXuLmw-zsZ)FTBkb!=Sf$ZrF(+JAU*OivhdaZ?s~xJ*w;wAo z*z;qe=Dq_vf<~1t0Ea}0cv~CG9Gv(@IK<$V^uBupYh{eM&E&`VCK6*z=bGFomN$j> zKLN|q%IXL^Z-t1-6C>Yvs<0}om|Ehq)t>eN4f0i#ed1gfn>>Sm3(mT~m0X?$w67gk zUg-u21$t&+*J<~&xvC{D!ndiE2b{-!w4Lm{#+yFf@J4j&_{EgB!{&cmU6s8RXN6|) zR}$HHz43Et(nwc`_xbaFU5jhI*UN1J@I#^@2Zbtu8K=3?7LBwv7<^X*M#Xv9v{8fT`l#XY!-uCi9+6&z@^QWP! z|L2oI@8U|jaL{p(eq#n1yaaril?`E%oId?b*{LqH0=GUbTdhiVs4pdZ9JCV z>0{0eAW7u?W=gPd6Wmmo<1gBSKhtd4C+dD;apRaF!E4b&n@)QJLbcPJ+nm2Lak9dL1+@17MNofPts~$AQ*vJ&8~U6^fq#JR7=c8MkrO-N+=-oF_qIgHehE z9`d%gMy8Id7XjZl2QuN0f9SBE$`K`AbB$%E)y_$=u^J)<%a!b>^W~7i@PZD3{FWdR zvl)fHehX|iC&09No`<}WA^{are_(Buc#|8S#hxAn4hXID#Z-cqbY7uMW&6zT=wLP# z;*bTEBE_6nJg74zr2Y&tB6FU4!FH6z4@b3718fha;ZvOGO9su{Cnv?P4^fp5haYLE zu7QEqTSDOTt>4zM3Dg1}|RO=U6Y3BChhqj&8weRbgY+DHxYcz8C zwEH)Wjq#u)B8SdO*U=bRtk_2tz@!_~j-X^(anmV%a>_vH<%w>g602DNOkUWax5{9@_e2V(Weh zu0>Nywo+mHNEhNfC)@%)_JCy)tmWH3su8hT$X*3wb<+*lOq>mWLpw3FB(3_N-`(k# zW|hF*#I;9LfuQLF13 zUhgGv-%Z$O}+f;s@n5fngEp*Q%E-#5*8(0R2cN5!VWGSR|oCS(|0r!{6&V=9s$IQ2Q zas;Ka4NNg65+QPq22R_xl@x@z_2J0F;uIb?&nRJlEV84h{bBxiP4OJUhZxK(S;Cb2>&zYT1DUqL09~gG7QA zv-frbA6mPnr}UsT%dH<9z)S&e$$2qbid?s3$i2Aoo>-{LW9#)NbcWg#oezQel-#KAGO0C&>7cdo5cH;>8-pEUUKATTPB9+eBn7}P}ej1 z?9WJQk8d1|<1TSbb2UjU$qlF_5(uvj%rZH3Yz?D1gjL)XpCYf~5s8lNlA1BC`jWuW z28fAn+vKXE!x(4!f+5%EC2ApQBsE?7`}UYW z?Z1RHvs2AF=;uu8I-0UDrF~ugSVy9x__h-A^Udh_2!gh&X6oEJ5FDV(lUdqW`J>%_P z=JEPHrREkpEI+R}5^XNW|5BU`&^LNf!bz)douKMOa{+!#qY>8d&*O&}xRK}85>ZBt zeHij>JR~OUnUV~qGlHZ`ax!^ZW(Cv(pjE`gTJ7ebtKmkIU;Vn4m88zYp%fRd21hG^ zO&qK@>%KnD@K9=xKCZ@_5^1ab0mgLW{)xoL`9jUsQ$})Oj zLCp`=Tt#vt(@^{(*aTe@$mqnXl})CxVq;X)1{sw$@dP%v^CEdi>~vv3B`7cNGKKMI zmn%I=ru*Wl5VIRNF46SE@Nr_hgovH_FmHNK081mB>x}b0V6u`a+xOB6F;@ZdW^?!V zEy#uyO0Bvs2c>=&n(&_=>-h64pdJk#Aj)g%hy)wZlh&f z`0q{4**8!=$%4Rd~JUmSSIClZ&O5 zeVo*GX=Lt7xAY@q-Ou_|cFxXbE(-looWcChMAOc`G{#B|%`!fwSoOkwxeO)EE6{J1VNl|1Q2GHsps?RB=eq8TT1A_@n-RCj1-+E-1NyLsMiAPGmne6S zKO}5fwcgkrEp%&Z=%+wY#WKC_euV`?Hq?tcQ2`nMdBJzCgFu9l?k0!>W`6%xq;1V1 zQZ&mmH4JoxraW>9gHzj6CJZ*ZUF^dP_Wr;2Q0?c1 zJ`=EYz#!rayndR{XVsD~y2681px6&{ueX5hKms%WK{K z;O|Ol@PBHXc$Z9 zc+6roywjrqpm(bZ%%9Fcd*qj#^FR=m`eVJgkc|$U%VxM*H6iy4hIn7a?z~3*IETUK zyFn`qu)r=@hc`fT?Sv>NimjST*RsaMiyW(Q`;=!#>4uP6zM}uYDIv*np@Z^3$gCBH z+2j`g-4-2k!>;qgQb!Cn<%i&oXhf@HWQi|eL8czz@}i%+Vb}b4oJj{$NTQ{Xmg>^s zbVWU3YmYw%SD$d* zyDX}SYw;%B43}j}t3O`KAHRArv#S(0pGm~b%QJ5}vA$+}*7b@z9&Qzh?r(Dud3kyIIgR<1!#mV8#^`QXN`c%}Hw4Zv zR}l*BaI!0uO|aaJo`ndY$ZL1@VF$Rq-e267@rU(fKLynak&W-DDg`%g|280s250PF zf*e&G1%Bu^3)Va+)20v&CbJH@r^Y{DhtYT<9oo9&!l6NSEuwYsN|9V&Ef{(J*56;3$1^4NF{*djO2*9BR+tZ&`1T_DP!0oC1p z0zLDAuFG^FcpwZW6(d*A<oyVSwS(8p5_h`GfNOMWI*A8nHJOANL4+iTTs@g+ks@I@D_=ycqr!TCyo-uOHqK z0roTM6rJR`kbTWcoN`goh$TGzw-OL%Hl4>KOZeX=4HkD-1~&V-Vkwkk&3I7h#=LYi zh&W;mb51#gXB{hqm{`y()S`N5?Wz6>q?`I70gA%duvAA;Ij8hSqj+{IgL7F@AOFfbLu zKZiCN2_ud)^zt?+(-=Ba>#D4M>%I%Vj*2cZukXOGnrnh6+`+lLNMvD*nO0; zi-z?^B_*`lXj_|3WK{~N17geqbBflNnujxm4eIYBM`K|})YAO9+!1}>#&nlg!GYgn zt(dBvwH$XRv1p;^H|@#*WuI~SV07sIWd1#`+rE9>^SS@4@(x~R`+?r8;@LKr{FAqg z**WmLZ+5(u^)=ez~rKv60efA>7ssG4-YSg!$rA&WnEq0Q_ zw?gpBE~D!PDBQ6bdv&0qVT3#9y7{;39c}=C?qSM8G-2=-+!|DevYKy~Bsn|gohmW8 z5Eukz13+M%nY+#x3dGdci5-U|Qw>Bh-3q~IS?Du~L~G3iz`Nu*qq!NHwFj?ONA!JP z@<}G|=C@&lI>hLcEM0E_URB>|M~Ku?U?(6ALn%XALfIfx}p zFyfUsJ)6a9VX_&d!ulg(w#CP!i+9H-nJLb%)7F@;kST|cXEl&Ar-l*seKI@CkR>sJ z&%r=m9tA9#`T9SzSjWol*6e0X zDS#EVLyMK+2%~X!1h?&SqXWp9YD6~8b3Y?rJD<^v2iC#*0~wEype0+3yiglk%3AME zygPoXLQa^&rFcoAk0j4*AfFkPYqF#aX_Kg|)=X%6o@;&;$v#v05wBRdg(R%?o@=Eq z=b$n1ISTS6sxQ~Q_t+sfS-4hGp28JI&I!bE`K|=fZnoEsBeq$9u;$-^!Z}(bW@#uS z`RPGey};77X9O-QYR{*E;tIa%!XBe;9n+%X`dk^+8!KE``d|E`Ncvt-9uLIsnQ@7x z`sScQV`ZX}fXTPE1+a2F|9Y$z&<8C*k!(_n9INf~GCFJf*n_r9SaiVUAr2fccLi|DsN-gy9t5aoX0Y#4 z<`HBNkuQx~T^7x%HBxaDp|YK^c^HZBMu8m9XUdCBGTvelXco~XCLa;*?;7TNo^-D(!R@gffRhC$wn+7lOW z+CJd6{;Y-6xxgR4MM*2ThFnIQ49DmcCS;QheR5JakNtx=*a@ZrMMTt?hhaaoisg9m zbhD9;abh1!o&SXW4SC_4u}!fc=VkiYwqvUzEv< z{6iuV>VC_h{VOzTw0!!pObls<&v*S%^I)XkfeVc?!vr{yeOcY4mc)d8jD3oE2YTo| zC+=ccE7K61f7)X1cg$mh)qu3fBKP};`3yj1vPt}MiS_4t2)&%%JmnCdq5ycJI!l~32mhn(bAtHWk^zved>cu$CN|5v=vHuhG+-jp z5<8LSL>IocNa<+K!?0iwJ#e0c9L($42=NpN;c>f)#=gs^uZVp*ZkI!9so3|v^!+qh zH3W@z?MwT>xDtiN1p9b|x>+*xo(*MxI(Hyw+DygFTV$agW+h}XM7WZ#;FEwXqCUTVUw zXl&^~I59x~F+Y#hV))O-zhRD3WKg(VL10Ec#Txrs7o%@;2u76XPkT{b(q@x(q`3M# zr;{RipdD&wKl$pXOW?)KV{*B|K6C>Wa4&SAbs@E*f51e2aTzIkf5nXs{!PxlD#@A$ zmC1C&K09#>ztz|im_^{GkNhwxU3Hx0r);VQ4Pe-ZBU86 zeM$A+wqA`ONUmn#ds?#9JUht})9yQIi_llx-0$`6-Tw@eP?~yB30c0nz<&2a11e5D zx=ZpvU+eTL04#CwE-wqV&>a*p>hcx>{UIv$?~U-le&O zI5Czg^!E4-Dgva-lA7nGOv+kHcDAH3ETcT>j`GSjLq)m#s{e2-l1zzkM%CchaDee$ zVYFnjnxWmMYBor22WJ%jVO8k&abJ=Er9P1G2#_y(RGpqFZm?d0qn%$*Xt~q0b&U?+ zibBOMGJ_88$UleJxX;g#knCM5X8r(#>xg!z0E0V{~f+YuY0?vwS!ELb9Zg z_-LchFsNgN(xd`oDKa`~Vgcs^iQKy|URhvjOJYk#pw1%QxjpTrhT6a=@Pq;K?kLEhJh0<-G5?GB|EoO`AldAsl z&-LQBJxE>1YhM*b%J7J*`?+}`)-PO}fFY!YaoWKL;9DWEbOc$nT_A?ftCD4>$FerPce}Y+138Y!kNiAa`X0C$ zE9Mj{_ofbuI4PXD7dkig9SC<#2qa4U6*W-X5P*$!oit$^Y~}=@m01N-gjXN4wwO6U zC;ZFhf_TD9hbZJK*KiUCft#5heN*Iy%f%9l4vTX%Hyq6qraZb#KqK7`o#AFbvAECq zI6xVTU|DK1T%TK#sOCTWkN6Rjz<$sjq|$~$F;|Nw3$xQ)65S_e=L9H2+|si+1maq~u<;5F0It+=hJf^)`i0G@4`EgGwcs=P3JiP*a( z&e)&{q`(lRpH#81pEW2M)`H#sv2W%YhAQ_}w!QX_O8)2k6(mbiqyY7_Y!ErKEon~_ za_keR=bvsjsSLr9GLM?MZjZw?l70&|GKnoM5tvK#Bq>)7HyXoTa=p<8{c(t8jrp=o zk`cpZQe`16CaRd@iZeJ4ueq^@PYQvlK71kh0_{>+V|~x`LGBJv)hi zy-xu*Z-lGrbFsc@ph3*&6OX7fSKlC;mW0hu{3NepGwJ*WJ9+dLxN~zYn`^zBJ?w(~ zVOiTw8T(ulVYKjMGzVY{gY_@ zXGggf!+bNEsx2C0(rMGCD+jQ-_3TFmbuER9*Nq${YO9LP!{?yRxmW*((z+MLUqvtR z^WZ?6g*q34BJ6TCOv4f4WF|cl?G$dIyuki`bR8IdCq(Z_|Jg5kk3)?@e{qRvzap%6rVqu zyhJQ0YqM)MG)#?m&yrnm&5L(^tbJ}F53<5VJ?7~IW2@yVI?EDNWg|(P*Kce{|0#rN zcJ%@tGLb(g?a4=Kyd_Stl1yV0ONe2fQ9?dpqb5s7ryt> zLVMP`cw3Uqp9Lre8LNz4LmI8P{ylj;m7r_K(F~HQ1Y^T85*jOL0OtALu*n=fkEi;AYrhyRq^B-9sKcD$j$n)#D;j(~M)&KOGU1;`I|BNcw z!{~OtiK}Nawon;$g>hBQH*~}B^5Hzk` z2v&Lkpn!1-n?mxpQywlf4^{#yE{=Yh92EmiFx|@95v>D#g5~W9ncWC!S-)>z!SwVYx%i@1nPzMdE(`H<@ zBJX~~3u-M+fNB8|>*u+5kea7^A2z zP^Y%#NV&gZJ7Novmf9*VuqGtQg3sPnm>>YbGU8#uLI?HDyUsNK)8rscQglDpAtD?o zUa?Jwy{=*ebYw4&KryASZV^NYxZ2}=^pL&EG0Evxx%+k6B^$yL?O)20b!gI_dYb#! zbCZXs>*rTJ@VkXSeNefLj}+|#B67f^_|M&JGd?Nhyc$R8`rL0HSX98ouvNJBoWt_? zfV9#}M%g>&(3H7J-MS^_ntdfi$RKgZP>H8phNr7+18;IBt)i3yL{u$U3yw0#)+&e# z-#L=i;2gM1Z^!Q~UghWT2>%{ctK;z1r$kpl&VGbdZG`aC^!&_6C}L;nRZKx);XGGn z`7ntCln|qTUZIFQqnhkY*EDLp0mB$Lucvs+7k*l1(X$=L%^!pdi!d6UF$zG|wk!+va$k|{9Kj!^min;KB^yno7t@o+Gsi3+a3 z!{Lv`o!EJgpq2!)YKok|CSc4<9vlB-9|hedY;I8TNhS;iUKmg0n$@!Q~K(&EWi&^l?LW zVJd!9RVN3=DP$`8lJH2W8l09WsB`7@Nyxbca9*iUU`UNbW1<~YK}4HZe2gso0Kj7o zZafK&p0h?;1#MV*?m~;%Z5zp!uJzSb7>D5hK_Mw7R|O2@oVmFu|SUHGV8ClL7>df z=|R&@QCWow%2QYit(5tIVZC!z9UP}gX(JG(55hVSb~c$yuWL&U08NnijDq4Pnh#36 z_>H`JSeIc}K;x4U-FL(^@%=(YP9N&ENmy)GFu#ySJ}S_HXTxT^cW#-$p2NtRAvH>V zOuTC=&d<*9ohe)(1C^)7G|NkB3j17}(n2V#HSZ6xI!C==ny; z^yN!3QwjTZA1RbUq_{@05LRAmzC2MphXPksQY)4Rw@iUilL&MFy!yZ2F*e%!{brFg9@7Y4yH`LFG*-iuYgh~NsL|~PF{;%#;c14}mP?J{Q z;pdbnXe@Hu0MqGg#PyT;WD)#k)KkX}aNy<-{A!C_!MjRmMtVF#c(|Q1re_8yXpdIh zLJQIKwAj~xPRHRj75q1A{5Mv%_Ik;_IE0)35AOrp`spFbqtw|0D?lzy{;eI=#V)H?mWtN56gEc?S|OILUcm>n#9eB@-&>#!~7@o&8R zL6arJ#Xsx}_I&rY@RmlyZ~0XX2YrD|-)hgbIh+MR9(wAQ2xUgt^CBVMxC^kU+H5wy zc8{+yte#Smf0el_av`QPMCBL@)p=Lwrds}bA0epJ`sHA{z+4V(fwB`kS%#B&?-bnU zWAr*f*bVEIr~MR3wEL1+_dsriyh1;BXqvZgi6d8GOB?)*n2qUO;#FzUW ziZ%)h!1Lvm`iI?!&%Zi*v!i0{2NpE!LOO-J0YXd9{;<$I-{?(e{qn~3;-rCoA^kjL4yz-l+MoUlkLPm-c{Q-n>9-+>M5GW*m+qf zDlyvi34Q4RN*$=k2DRa5BskPf$Z~k~>bZwQf9U5r@W{VFEinwADpZkAGBWzZKS~8f z0S)5)4`wl{F(~U{sdcQ%AyM{IZofQs-sMZP+Mvb*iCf{5@&Q#^iSrq>tly>xH0XzZ z9D^d-YKqUBq9L}nUbk@c>x=u=1jn_t3$nUv*9`yA=SHBuT3qgl;|>3N1k^u2#{5Xm!p*2d%%HT=ElEhaE!bTIY9|akdrj#?i+z(4s)qVh zdcI59T{Ai*=f<$N09ciIdnU)5*lFHUZ?)t^3eB67W)L$!yLPY~D^c`fP;lW9mQ*m- z6sVnHW2HsgZ@wk?iSYUS;t{C(U=WN0udulJjXncZ`H5UZsw(CLHw?bTJ7dg$rra~j zt@Q!Z@Cb%s@;)g8hNaU~Tiwi*hA7Zr#3ZUah9DI(=U zQoxEsUHcdd^DpAC7dYpl2!9Wwhm)_o+|QC7T)xa(|sZVQEHuu zeA7-lo)=9z8S=kZTO_WWDI4A1j?k?B^(rUF)P>8_?=$MR#SWu3f!_1YSw``#GUP$_ z1O&T=xiw+p!s|?r_|m4+OERBHIr8VlUl-}-xBE%{2`L>2;36G|R@ZoPCl zU~~%}VZTpJw~NfnML}^%ZH_8@BbkJCyQIFrtB{8th?0&X4;|LOe9{ieN6&D3@aqx2t|ibayg&OHt|VAAEvG{cOV(;&S45lPtUSgwk zD(G&WAM{I$$m4Y*Md@d&H;T7u!J_+5cO_A`sM_15mkEdUS)2h)VoE(tWb}>F&0bnf zygEf}I!~cW2R#Yj2GSa2^k(REcL>|-^3-6{OAILa`xe)+6l3&K2su9ud*vQn)47|SVC$5i)RerUI7T26wGz+7$ib0e{qfbW zpCg33W{W|s(_3@!F2J>^3rN8Z1u*V+qJp-YrFgtDE3N_Hqn$AGRs}9aS>%R*U3|UK z#8(7tBjFTI$1k5gn6mt^4Wp^k^WtMYau@cAT2!KnwGfOpPKd<~DY-z%wlyj8U_X_} z2e@1UWgcLu$jzdCik$MiVCJ{tV83?|5z$+@p1g9h4BI4O@NkakF%HqrblLXdtFs64 zeM4{K!1I1yu;#$H`L<#eTl(VhRAqP?oSv-wsXr|)2^_vM#>rNfv#5;7&+eTHPw2S* zz9F3ci07ro{-b>Bc&ej0fkV?~Vs+3ZR|!@17VJyz=~3^-D^=0fRekw?hmdA~eqL}&$L~HI79Tp=YDnj6G4OGULvB zV+d=RDKv{aa1f`=cByI5^taJH|Cj=iB`IQ1!z+MlhK$eB>&h;L3+64E{Ytsk=mm7* ze|r^2DRq|*uJiJ<4%s`#AlURRYj$%V;-nv~FTLl&i}}kldHNOaoIUV26Z$i+V5%oU z(iz-S6Xog08O1vl7;&-&=c~n76(ODnKX8%(Ir;hq;Eh5ZQsa`9s`+sK#qu4u!?P&g zR!Pl=6R&nM{*Ldq$+mWgQSMcfPrae`-vQg!nS!Y@V0!9NB*8Q>wgWJimuozqZ&IeOoRbU#Ds^Eq_anh|nfVRN ze+n&dWHJ@BFFD13H90h6O48()nZeH-lcD`D! zVFs=@Pg>+K>Qu#Ir50M@7~o52H2i(&2MBpeOlV$kZfEP-R$U*SH5T}hD8zVCRKe#D z@weBsNyRIyd%v_=VK6|Qd7}Hm<3nikJ%MtzjdyY1)brTEQr0t`O@L&c(B><;p<&jG zcXi~N=$^hIB@{pRcHd0xy)L;%f00dgUUKAS(2Wl_F6%7CS7IT)*BI1)y+fz3v8mI& z>TA`k>hg` zwhR=s?A$X4F(Y&ncF+~!88*Yg8wPx~L&0qUFgq8uBe9}tl zaAS$T7-26$Pb6G#_uM95GzU*0dAIG$OZVz$=Sq{_`8CWJ1!NoMMdnO1 zwugE+YRcY-xIh0Zev&O)BZ7^nTK~ z;5#|sS;u;_luG!9#Ug{mv--TKZ?n(NOMkSnNOP6=>OP;Gyt z7PiolBge}s<;G8`bK4m@q*fB&0dw^M`dit)fp2^0kmHUwag1cUw|cm#1g7K{!J;x9 zf#?e{n0^Vj%La`wDPohH{j6Hkncx2jVBwQx4|j9pv(%A7YA@9hvX7}ajg1(}xS4WX zS=;dO%=ifhfp_+aCgv9HEd5yc3pc%x=uFZ}I-fTo8Kba(9t;7*Wyw<{t4rRvCL3`h ziQtt*xX;32^rT^`9bDpE`d4r`MJ#Np+V^17Qscb>!@z0!oZ=+fq81Ezmaxh!D~`iC zH}Mj&SVkKnKzw0lI$8&bgK=p^7bN5jH;ZA?Ql*2T^Kl4c)z7`cF97h^VV2 z3D}648oluYGV@9#+|#xY`MV-99lNV9u=yqc)jDSZ^EO(qIRdvT|9-PS-O>(FL#}ZD zs;HuF4F!+ite#jyOnaLhKx{sNTx~V2d`yDXYR@dBU&J#`(hKe!@i^!;d>ue-M}-49 zSQgII9`cGVRl6mdaGQy+)-_@h;#E2|Mv$c(jYfZ1+iB6Tt``$1mm4m!{1F8n`kcRw z#`lMMa=%=9(NKtpnOb%EnSL1t!KdVWK>R6)B~YKAh(lFioM*I z;htlO?7~9r3$;>Hpep|Hd!`kopolmSNuzMjxUfE19(mF0Or=jqWAxIbA>cRSYNqCX zyN5=fYwpwn+6dM53BPCwoAc&bD|a&aKv<35+*1{%s)y*XCn6RDHo2*b)#$y2!L-XUd19}GZoZxlV2 z9WSCt$>nOV`B#4>D`nLx=a|z~b|kALbRr9~TVcZ@sm#|cm!g!j-3ADb%Ks@cMZM9X zSVf)y5|tu)5}hOOQ+M6o^YN4`G%G7=aQ!S^xS9J=1T$&AF3vq8_=sMPJJw+>R*DYH zVoxd3;izcIWHuo<485|?-)l5D;lsRJs85kENqZ`w>oDon&3L`}hhB#zsfRi9l&FBm z!-87vHme_tnuxexl!8*;I30J3XqvTA$#uWk1F=~rgT`!E_D<6hJy6%NqF%4% z+(2RcgOyI+8L8FQ=LFrvb-~xZJf+uB%6=1t-9Dl)^qG;6&zJ+YOdr>=@0;|OBM@?k z7@knE^A)n-b_=t%q`VM0-RX)U1J!DDd5BCxe*)DJR_@W?qq(ugFhlAI0 z2NjbSh`|ok1#9k^O%FKhNxqBk>JB0-dYpapR13 zu$FOcV;bVnUzugv%<1BjA>SVc-V)Ek_K3dlOG%}YCfVCVVuqATR8h&CyjSA26^w1Q zOz_XR9{0Z}ERj3ZDZlrn(&~bg1lr9sek$xro#&eH79mHodLj9A|H;!tl{6Ddyme?>)!EZ{ZzWDE)4u=Wn0tq!8)XDTEP5H z+62CkIF=bCY_al=iWY5|!XSRskJ=+`$wfS%o}vGHQ_&Is5v9cCaA>^)$R%rbIe_N7 z=CMZ@&G+Xb1K14c+a5!FRn6)`X(%(ypzeAVof8E(x^;IH{;em;I_D4#XgxG}znI)( zQk^QoIQ|0PKZ!VXZgStM62`|nRyPOw(Fx*w=zM9gDYa3l_??sdv6-%)B`1P4fb7eQ zB~uGS!m7~H@dcv_Yo6Tp;szm`BE6u)v`8h+G~ z-c09A$@a|Fzz~a7WtO;;pIXCOOic~rYwCMOnV;K%Ym z4PzaRsK)LVGmH|G3E+1!Ks)-U8l8Ak{;YFwHX!hoc@h!cMyk(Nr+RObbMeXhdU=gj zXY=?!ZP5l#qe}Qha58d|x^z${8`E69=rPRDK*)ZrJ ztAr~8Cqh=j@`M1A3xb}QiDvV3yjqv(6J-ugj}7(Hvp^&JtE64Hm*XP^7!NDwkz?z& zet)oe2gRwV^Q`_RPTGK;%xV-RNq77waY8Y*}j+#T~i?Bt0n-2#vZSU2b+18tv`KUAD;{8Am~75!5v~7%7K(nY z`F1b{Z*amP(x@i}++3{fc4!*_V#b1>Q6YLlQ(ezYn_@5hiQeOVhY2C46Z&Ls7eUHp zi32>wrg;>hg(JzCr%$);n8sspy4<%~U--=3H~AbYi{lLF%2Kckig_DeGc2y?2SfrV zbm}n`7M5ass+KyD@ukUh+MGcL+%%)LD)t(z0cXp9GqNHDYF7#Cj{i{y5+ulS)oSAb zs$p0Q@rsX<_Q+Qqsm9aD4NaIjQOwUXtc=AL5X4Q4F$y%7Z9AtydTT9sepGc&;zI{8 z&)Lg~`a-h9{(cJDc5P$Nc{zj+063VYKXZ~JI+9YkXBTg|J8Ium#Mg|AdICTU?fDQd z=>${Hx@mJFX1;=`zYnJa&3t$wMUl3e z)Oj8(4`HdN%0?KKaggu$J5`I=86NX)IG2AT9;ks3l{k=dTd!zj>?r(U=OmnTV{l{0 zJrMSiz3zh|#fB>UF!j2uR{&p%&48;K_lG{7)7?u;8xCnJA<|(8iEtU5tC7TUr>Zh7 z$NMyL1H4Ua9hJb3WP!blH{7PU|5v_pq+(=h04k{o(f`kH5W?IZkjDAF@6`hrozxd1 zf=p_46g>nEr&VkRzw=87fu>S0hIJgg%-OtTvBa>s;j z=v0NAUPpg9>B1d4ojkuJ2wVSOC0LW*T2wq2o!6oiVtlL$W1&-^GxZVAD)gWtKo` z*s27~JD~2s7>5xcvFlq`{-9+YJe>Y5*8(F3O9Eaqo*308E)>IVI?i;S2%-81(wuHq z#n~7m<}ksk6=x#z;Evt9iRUzF29`UIrXfuYMpE8`d_-j2aBmzMzcW{?^Y!y}9#ONe z_Qc$n{|P0Fc>r?5SsLV$$E<>v6#AE?%KpezOuS6WYer%f9@wwAJ1(#Y%VCoy&j89eVD~ksbQQ zu0|2?rh~SSdYHZ|lmVw(aC-W`Hi&04R5-B}>dM1}RzkmT0S89RV9#MnlnS0R%Wmy{ zg(}j!12ksf9a6>^{|Df}+918}_P;p8gfk2NZG`H_A3*>l3v*36}?9<+&a?Rqgn(vtbHgE6^9Ew%l%4_b*|Pe|ohC=b-??YW0(*vR13(b>UA+=P+YhY`Wq0-#tNGAhHx2 zjm&)c!0OJ(owdr9QFF{}8__=3Qb|oNK;B`6hm3^pY6hNPCQ!j1OZ2$!hZ!YRnpV20 zxP6ilqRCrCjIN|$cKUm9yaK4_=k6vhRs;_c%pIK5gAK@;+gngw#m}#?aQe<^pM6%a zm7#l=-W~qL3!~LXhHbYZozN6_?jN~;Bz9}k@(8=Z=QEo@1!22?Cu#0*SuWumF7Jw+V!H~`H>M0UC>u)MpP16j$T zER26I2_}70q!niOt$EQnf5KA}1;V2z^?rB0bClXS2QYv?3zosgH^~{_$8rxLCQ;r& zDAD=nXk)-}I~aHCfFZIC{%uVAI$-1_NxwtpC z{fh63&6+`+={>`A7HetT&lfjH8fT-{fE6Jx|7L?6*OSJ{X6m1tTh>k$2W_gmlJY2$?4hZ9Y)$YP514kIXLmIrQG-T}B>sMYHJJ<1UmWjpT-_e;Mll z$EjVDYg6r=#x|z*sIavg!cbO>;RW4G6TB|8J+2Rme=V&7`p#hMpb86^Cch@k5S@#I zcBiTxFEiHY-F!xynt(LCLvzpx;O^d@a^*n!=mU_pugNh+XN8r?g5R)yGw)}sYB?;Y zZEDe#hiG-_cD9JnXpVaMZKErnVZVGdGrs>v39TOfudHn)!hs(ROzbQEJ7Cn}6@&V& z9^gTw<@e7SY*;PQ`>Z}Bo_$Wn0m2p0Y<}!XZE`QbQ=0iMos~bX=mY-BR;&)RAdCd0 z2{y=)w4GBI?Ue#|5LXZ)QJf>dI;T1I`1~}d0?C2M6C*s}_dhVS;M+Nx+~4(q?pKd! z)k|c)CVq3I!0cPRKDCN~^oS*#T3QhdH{uAc;NB0hj+TjdpI;1C-8SfHnd<|e>3~zG zpgtf;zbG4~9^Rweft?=#MiYUwNzLdx*#{T?;kC+iMlB7ocX!(Vt|j&TSGce+rZdgS zjEdMMy4TXBPO*kgu{@fF8nCGH%!g@5oQvO@8fnWmHnw-2gK{G=M)nYi?1B%lTv=Yp#qU(Fc&YI{OKli=$(6gRR4LD~V!M#~+jWKkawy}4a>-;WD%987$MZN8n zEBD?09rZej!qkC`mx0dv{~7^3bjpt|spHK> zFwD$fl5n{5N|b6ktI$g~GNEGizUulf@pWJH>}sq#kGofm!J^=;PY9y43G*+vdg^I} z0B^&{c+#Ra-y%U(S#zH6GBCVmC}+)z2_?e&2|!2HcdoaVntjnn;buDDg1h=}jga%| z{H+&p!}1LVMN?{>kCC~7IVJn z(6)g=l068nqaT7U*l~?0246}0m^TyY_5`&9-RhADy*{Z*11YGKC-p!>hh@XVIh2ZM z4KCt2v9p505bKTaXt*@aoA70mjO|ehWNK&^Ye3ztpuC~ZysrcR$ zRRRm5!m%0sVGr=k!nJ(p#?{X=uC(1GHkJ#NyR%qvI4nnfh=UQHlZ&!Ca0GUGqwYM< zEWZzEmCBXdKAf~Q(85Q@DlpkUwWS26l*ijWNZ>Au}MUB0GmjT7Xnw` zfVMjr`*7(zG3>dfandKxw%jZWX%P0jQ*Il2bS5h)SRq~}=VT6DOFsiry!X_7zq_zc z_DP|;t(&vmb+loUQhW5ia1ty3hiu3{S|6vj!^OlP2^qtO{bMhZMY1O#ljsrM0v0SOfkV&_fl%i9;MG82$7$zD z`r{>P!*^p&NwwmjIzKam!LLrRmyk)x!dwPEuQIew|C`-dkt?tiJ@0POI|@bnX*8aw z445dW5(t)7stIY*?{ILT>1gdQB4cMs44$m-7OD+f;A>e|d=X^+s)Cppa2$W3|CW+V zbreKDL{VtUk!_Jp8)g9a!(v$IGHrrj0k4*5aQ>wTb?`C~Vvs6H^J3(bYUU^al7)(R z(ZvS6KRzm-o+&lKo9lHXsm8ivfNODc3BvCC41_bF_-hlTl4IFvgIJ;P7~BSUBI%dMAH)jN6LhUmV=}x{U;+L>b^Kcp* z&w=j|@qTkSdv}Ux4wn_agxY;+mAV89sA~fg#cf10TF|*+O`*cdq6OUVW9h z3!&oBm4Br7HV8?ex%~Uj>LMR>694=-Zv*c(E;K#P7z$#`>QM}LYWKi@Op-Q6LU~_~ zuInEay4&5&c>k`RJiI#9dZww2niXpxkrehCjfKn;4U7RKJKBE|&-Up_*05@t)Ooxk z1jECFPd;k%Rs*Qbq?3>8EB0HZ3zK#gv}6B;%0=;s$@wmHILg%aCAUogqqMSjc`GMU z(GOR;xv==TMQ9cFyf~r%h&{e>@nYwYM32nAKH-{-c0ZBD1tb^yua~*kCM8c!6 zEo&}!Br7mm)VD{ywz5(bxkvZ=`L`l%DNr55r-y0_^!fD&7c^eYxF&_y?wPNC0^E1s zVXOrUR2)BUEjT90GU&rJHz_DhVYR8DKmD_VEH_Jt+tH||9fU6{VJd}5!psNC&Z^+G)3QD55f7KjVxn34&4?7Frvu3r`Ih z?|a5j*!S1;lxP0Mv}}0x*QDk4hTWKzH+1J-qO1PvbY_eppbeN?)aZD2QcN>>Yy?1G zeqWLP8<_5pSIXJqsx-wntfxk_*5)K7THB$;C}DyMm;`D#IUJib{?i+B87g2DF(5sm ztur0?P`E%a29TDKJZ(LTPd&rz=VcV>)myD51EY$;t#YlJ^=U7kWqW=mQCGRI@Cj|a zP`IoFdpzQbBp$WOxuZ7fk24LCO5R8`59}dO;hA$FkZEZ;_!n`-V%V*l-~jXo7^Cww2Ta16`CD>@c?x1j!N$voGSw)eQ))m&Fb4p<;GR{ur7|CX3qSR zfT+G;H)pz3y!a0d1cGas`@^cuG9g>O`@%?L3@&M4OL;<)RSbi&ctH?GF*~+|uP#G9 zF5G^dVfyhUr{gQHU+}V?87JDC(}GR*os$w_N1{&FRLmItkIYa6)8@i&KhrKTTw)}+ z5;q7ahF1uFfNM2O$2qI*B9uH+pMlg%N_kQNOeSp5r_1YjjR z;rjilDV+ySRPkQ!NmcS1Ph7uxphJpoeOe6{Y2@-NN-tK-h9{ zfPHS_YJpryD{3tx-Nmv6faprxIKBq$I|UPzkyZ5&ra1}BrSYoX0wFV!Zvq2RdCQ2- zOpE6gZM+mVi}z&uCUbgBwa9%Ns2+H4W<5ePC&WxI;nI(C3jaMSn11iDFl-Nuq$RoH zi+#Kt01mJEfBE#SG&Jj#O zLE+Dhn)EmNh#Ad#6Q99++qT&)P-o@h_x|}=DfZhA=b@=q{SR$gV3Yr1zxq^t&g4W0 zgk=07+c2%a+hA~K)Oye@D4BE!^W54Ew>ACYjzF|}@TU8P>hV1C@vae;DHnl|h^z$G zoa#FVXZ`GRB@|*zVAcE&>|{E#-vI5RnASWxty|)F2Qoqj6P_i95VmZWc_H#$g)F`4 z3RK)kt90&SqKbTdztwFQv}yc50yo#{@Ol7hfOP4>D@#NzpTc_WJP5iEw9mVeGjRe| zGdM;<&)5D^sf}L@sH97_Vsm8ZAHrNS*R%&k-|Y!CV?z<}V77uFh0w%fLgwO;!yIX$)qRcy zBBZzaREb=q1c&4eAOJPK2^QPZ zxu`{oHVeIf7RUWvt+iIi2#3u$MadGc0*qMBr6BssP(-#2XjP_>CZ6xhzG*F3aYlKt zs%;7JLMpY9xQbhFKUao*u`F&4IhuQ?chm)?&8?pR5F5YKuYJu<7jGqAI0gx0-2*^X zC`!S#KL*6gs5SjdG@1?3>&)QV&{}5Rjb8Tem>@1xsWPcNYm}uhLD?}J~wc~g6E=4Qui@FpbpCYYnz6&#Vx17{SHExXajHoJz3W<{o&bOLY= z{88F%g*CErWd&&wTliu&$e|D}=c&A=NzKw!^%(P()%j&&Yi&6(`WG&7XvbKSI>j$N z`NN+i@dBau<(NgUKaE2zkgVo(GSi_t!#L=a1z7$zv>;;G)n`id!fXjebJ}ahJ}=$- zsE)Y+3~aZgt;$m2QgZ}0YVoDuyQjs>&L*I&X`q;=3g8@G|2!1&Z%$!2I^;UIQ@e*E zgBGTPa53f8(67&g|I!VNN+-0V9E>&Lgz++}{Mg32Q&QAh*^wzM5@b^HItl$|cT>mM zBX&_LL&y9p&t}WmJ35Io0|y3QGEG~zFl2a@dc(4vS~8g-rT7x<crcjm>D}#hZ{h8Dkg5Wn*F$rH9XB}eK z`tbzPWOmL<>R3S&dQOy1G)7O1A@{{uc)H*q=i=fuxXDpbb@CwffdD^JvYKvzjqb&M z$`Z3K>CD;hkt{TAaTi}PF=Rz9D)Bw^uD3<^$Nyo6JMf|kI6dS<>Amms8-aRabefkx zG%F_=sfu1~XXx!%Ap>`dWqmwRuCAlv5yUOMs#u$TL2rGc=yt`=c4S4?dyB^jL7#5L z{-M9zT7O363O0L%{0RW7qm{V%^i)L~d+=<;yGePHiXU9` z90f*oi?YowVXv5+JjRndG29e3yAbQu-pxK)Xm$?!e~B3W4?B;3U;b1=yl@q-r^m;_ z`ePN1F8Mw5Kl2wxtJCRJ36B~21I8sLLAb?+SRAA}qs)nq;L197kZ5HKoBFqCN}^D$ zgN>`XHfuH46{6;A2f{-X|DqP5ATk|Vf=KKeu1&!9%Y^QWp7}Ca; z)9QPB9F1df;0DnjZ|djVmx|lL`sy-Ex&j!2eg6*rRyY$frb+ex&nL#p4t95-`Mj3N ziBW}D0g?+|eRgAg^B;4O?!7=NzDS3p=)C*OP*7T{?2AyMY9%&nQq&c);x?h^U*hByWd%5K9yAIt7*Gl{l~~d}^4Z8!c{Svymu`zk^zyf4 zTcO6#%soc6BcxD_1ekt(>_MSYE*)f}+N%)DXcNseDBHeE6?7;2(=yb&y@UGVWl$Wh z<8w^(k!B0iJlBBiHwj-uhkJ~D>CwTJ>pzFI^2%3Yk`Lo?hR?FnDwI%9X9}Z=2DK|z zhtU96F#H8QX!~Sy&<@`U4$DNHs1*SdEIews00i|$KA9d4K6L+qofSJ?v+@cVL*%UQM(GPJGlALJiYtw!rfkA}LnEtXM6xzGZJ*QGTKN-*8vd zg&s2}c&wzIOQ!@F31?)xASPot-qve7tdh^HZ}z>BJcSVDJ~;9$V{m-W!Dht-Uoop^ zf59U0!n1Hu-oc)oBnDTS>O#mb=x!^%Ss-05ptlda{wGf2q!&;oS|(Pg*4zKr4ePl0 z`{6fexBfTzJ#Bo#c~zolsiFiKk}mjY-2S`he6~ER0=*s@#|yV&pq+0>7S@!XsiXN7 zBNl%40>lpd_Eakq3f~m;7yOMwm}v3CKCv(0z%m9mddC;CSqj<1&;3e*`C50VEqSCN zGN$L5zjHPrqWrtf6I-Q930K>_ za#5qjEHKLSGIe_{H%MDY;mIY_i*MV8{1Q47)BO=Bd3M7|Z3Q@+bqH4nD{XX^S*7hf z-u?l%sBLW#*4n@ZROUwjT~be5)5F7|hzC@He-@g1;O>xCsUWP_l-a5jo@tRaOzT5PhZ5y&og=m9u}3jk@XP5Xw-JIK z;N!_X68pHu!rb$-2dNA6O;iMV5ioAkP`QTHZp4f{rd+Lxq5`uj%9Dbb(lDFjFA#>h zF@-=Sk?);mAnghe`RUZ~I4b?yF^3fXXY=NFIgN^nvY%SGV0C3>$?bWyllU0~k#PSI zWy?tVEntszl#Dnp;#}soDOaKNyqfmQ=>uMGI-P}%qT*Ov5U3&t%wt*q;d3f~n7z`c zM2cspu9Za=T*T6~N1m;BwizyDYig0Ybh?7tC2E*#S7%%lNF+=~Lp`FGYt57iR8A`1 zDKuA##VcY+RKaRBov$WMZsooFxC@PJiJ*GXT|7L9n(#ojy2xBKTvHmn;=T0qUzpfs z8JZuNY&qc%_7~hi^?3zc;akq}l~1(BT$uq!*j}~8`Z_RVuSl`y4C|N5=B9=Ah+MqH zW7kW3j(l4J_&yITvOCcyu|MX`X#?1`FaOHi1Xchk$Kt(kkwWlQ&lYwL<%>5E;PLzi z-M3BgGKWcx1Vu~h*-E4ndMT@E?#5=p#{j#2V{dz?49T7p8nUyv;?o)TSPD|BFz zGN2Ic8<^HxL#poi0VYDnS#+`(YmY~#@VC@n)%Sj1d~#l%U(^u?P>UymcVmqJ1Oh*| z?w1GmlUmz|sJf(;O}Cd*uK%lT&Q2b7L4rsa{9}&jVFWTN>L56&-v8O;H-T8Uk*eY{ ziYH2gV38Ft7wSmBuCaqvYa0=RPtt9{-h$nJrv6L&0SN=NfZUp?0Y`;=>4o!J+uNRJ z`AEQ+(6#r%Qv}Z!QDqE9#I+*2(K74ov#kcrvQ6HS7#dlCIxm$l zkvvvHira8T@dNxdS&d}BI`{`)K?xNM zVofPO=z2t(QD*6ec|8^h0Zj0)nSy^QKB%F!7IbWvD9iB-2b64&Ku%-xpUh(=6QraX z>h5ZoTusoYo-+Z}a(Z<5J;?$bL8J$-4Ipj!T5tQ=J+`EsYd9QIKc9__sw`On^*aD5w-n>1hxXl41?JKeQuOr>FuX zcn`lIk0O=Z2uHo&#%`)5VvA#TP1f^&CnH{Sldki{Bq6FI@(P+NRpNG*qC0qPf-;S!VgV|2OA7YU4vm%CeR7QCcu zK0D(1$Y}R2)=hxBYEq2DXRDxe>8g$69z)8M4HiZ-Fx}zx6;zZz0*z2%;)O^X!Y<9?j}$ z;YsDTy|`Q^KdFFQjN;V5f8Sri^g;bB_C*mDB_>e4N*k`wzlw^XRHWxs9R+A3Ln=E* zHan=Pa&henW|VJ>^9dBxIT>&W7%?ci4$C;T(>VMX!$wr}ExyOBexTjv!`YaC*7jvK4t9mDm>@`wW(mDLbtW96lLyO5s`f!eS$Zb*x&dut}m61rGZcSW? zn9Pv`SZ{|g+x-y(THHA(GriSZ+UbaLe@5!Rpy0=HiFo<19i%S|b!cr>y~~<#tB_|F zcqh^7s#7&k03e>5a{*)IzE1wRW@Fq9?F{Pk#LK^u0bVFxqr?hm#Qr~Pr>_2aLRRDG zLAtblgX0itm8u(z{l+=>l$&y;}Q2HQ4dIRod{)oVR2MX?KOy)>j@1k@<+9 z25A4CxwO!KznuHdn_Mqw-|^mzB*IxToQ9cRId=qcAo}GVb*kM&aqtF`)Hrs=zemzy zy8$3TalzuUs>WhfJb6e|Y!KG`v|-0$vz6K?S}LlN74P@&IF8dQ&}rRZ@y{#yIRQ^Q zu*9Z#r$X#5D?ENY=!Q7*i{)nz5xI~7zsRwVY-X0U?oY-wnQ0d;W(U+w&Rdo`IIhF* z@h_y4sWzQsKf(!!i;}Nsie|!90QkJ<?9md%mSS^ChSUx>5b z@Ge!d;P?5W;eoob99BkG7rtTQl!ETuBCX(C{!gbzk14O($I9 zKpFdDZst_|-wAv3i4z}W>Z~s}od!Kav&a}abGa71tH)t7_QceC&{l>0QHHZ24#TR^ zot~bdbD=WPQs8uxIFm?CpXO+=pK_G3`(&`u@9Q``q2&86ts_zVAcvxVVJ3ctCwLPYL5Mvmi)INQOk&Lx58Rd9 z`vJ^#H()oaXi(RwmgSyX1Rt^Az1&6L(+U|_B&^?h1JKlfEw%~$f0_<+yxP4tI?OLw z#}LsV4xE$UMLaDw289imAyVxe^Y)aTu_VWzVA+;b@#CD@V*zQjyRbtD#Ufs;s(WH( z63(xUE?Vvv)P$BGoOi_}TmZK}%B!@JwggS3Rc zzW+M3te-PfPUDugWJ0rm#H&r7J}Ch~o#g0Zf=jQ?LVS}q4CMR~L@zFaM`&Wo@*uzC zj53U{hLAHB)-Mim_F>IotK_c13UTJ|Ppiz6LTXb|79tRjx^R>h0dj(G9+$Dq(Ur|v zC1I{}lso>`vNUrwT#ANPEfECdTR@;rDu!>UuQQ)3)u7eKeFqplQzhoG_1QCQoYpQ1`h9*$-4S2-|#b9BN%g0L{8s|y$2 zI!PAE>VP}3upG6wU^Fm?iRZ($M(>hKn<^oxc__S&9`qc_A!S0v_O`e7z*} z8R^Y@(+$KyLsV|FJb3!F;JNbw`6$#BFzO@m$pKL;SCdfyej41CRatEf!nAku5c2wi zvoXt>snTz9<9+7-OS%n%8L}534KWe5@^hZTzaVCCKpJ5vPb!|5j#dtM)oB$WK(4F% zeJ}ADAp7KBI*K-ija&47^lQ&V86n|Q;tl=n*|}yYCU?j$$h_Y7NNRy=9_v;+z&o-F zFscfLMXUjdNCFGr|2$z_7)k=y#Wsntd+0C^tb*|rl}1&sGa!c6=VtunS?+9FNzhC@^18Bf9|*)BDJ%W!y zj0JX%ngLcx5dNk?Eg)wUTvwJ$ZB<|DM$xkmMgH~8U8gcpVw^|^3H58F?<)Hii|~30 zK(nf^u?{#bP)5HTqzQWw^W`M&3OtYB71G+mXr+L+_~G`Y*WljTc3@}Q8=n>4CTh~H zmL0jW4^uJJ_Br8|(lI8-K4x9mUaH*cN>P|@AUPcKgUiki# zfFj7{Zr4e1q)?J*Pp8+U7qU^4%VEFbg^AEC7-OSfn#})xdwE1+$|n?SR`xO<);Jg; zq)f$^Kv4KtS5VXrWxl!AB}Kp_IK4TK=dDx(A78VBQm6mCv z1GfoLrh5KLtzb4I6nl650bG9Bs8X76#8Bzi#4xeXPxZ%pErxvC!Yf7Zv|PDD4$3H| z3TPq->#0~D5EASS=W3P@62Fsds`5L?BdBDQO!Z|>tln6VA0l8YW-g$XxeYrbH4zT+ zPudfw{p6SR@CE2qiEPz{0od@j7RuYUS0@q9yk0MI1W!z0Wp_jE1WWicU~C*GnU>dU zM$#RfgkI5Ne0;zJ21LIP(ySg*LptG2xHpz<+IhAPZ?_T4ZPz`JSVyjcXdfJg3 z_|*3~paqqei3Lzvx;I8?I0N zLB?gcD+S(~JeZy!O2fN@rG2FC8pTL0>#s2hOq-eWwe^ohX>H7X6@NX!;Zt7i*p1yV zR_yM%VlzDt8tqNN-k4W5?7reRVnv|~94r`?T#K}ZmnFQaqxz|%c&5Q49L37W8KaW3 z6?Kc4)~&3V?LURy;EAM6kN%Ywr2ys%sk5+jnG2CSWHD2O3_2qJ(N0?^HoPf~%be3ves)Jz?LswxF|h3Jg)d5o+-+Qr!Xg7wi|It}Qg z9qUx#ZkO%zLVli_-r1#yYz2-T3o-8vL55N>9J>bKkKN~4ew&r`n13WGx<9*!F5|Q8 zlFH&zE4!_B)`~!iTiQGLf0XEa)Qb|{@Uz=I)rOQtXdi3lj09U04* z{$@ebK9{&(B)@?74D3{71Wk!3=tiwWRX?)>$q7!Id1$(cOBjHl{UPBd9B<%xCBj&j z%J*gh*ik#;{5n|t{S{!8M(^}ve%%%o58_GZ7E65Vc#924x(xPJnO9E&uB z^RlQ_rOMs6I4FD7re(rCHu25vUXzb-eluwws_AdPG9SmBd5>p|&o|?)wU8DQ3-$(U z^R>d?&>;#M z4py)hx>edZAxo=_e?^oJn<>ZOz^?1QDCa3{19PMm0PV00P*&jtUt zqq~O4G0`jkfX!%Z(M4zt=rt;;b?E{t9ssYWeQtHD*hY>Z;NHjU6`ogVagVyUDvoZ} zQAL7BoAf4rBPsyBPxr-i331L}CiEYT0Wa3N4nd!@oh}naN_U^Ct)V(tbZL2axDa!|V=$e<0C_mB=-?HuEFp-*Nso)O>1PeSc4@>&PX;&YJ zFQQ!VOvrGMm(MVC)s)U&GKc#h6llCu&&xOq#rSA|3{ZfmK2u*nAGaJ6!UA)@wmkNm z!?Yd@HDewmo+TFd+8A!A89a&mVBDofoh!$Pqa0oUl|WUI*ERerUFx`n@VFW~Ox{So z4A{C5Cubn1{_|=ocfop`T7A3L_vTJ7{24QC1|~ik+->|1zv*?%TFTp2P31tV z?8T67ov?h8UM5}cguMmObXw+P6@`=zyVX%z>V5S6T6&;Tq3`i{2642Ru&nMAxDC9VqH=Dv$X0p#wf(gH*9#@Y zi~W669wv;N-sr0($FPY8#X6{K=kjZtoAxdStujCsX1Eipm3bbTJOa4HR6rk~MaqG! zzkL#26TaSq0b_p>GXC5-Sc&~$*LX#yvlzW?xkRRqKy1fkrld$}b}uW<0nN|u>flSu zHI^gs|5#hW(Cqnjqt~tdk zskg zqrzJ+>)Ab{LM=VsFHhs5sJXNURdLY_Ge8~wbhr*i>*#k8vgz)45!ZOkW>Z6rb6LC4 zgAHZlwBulUr|bpgd>_gmHN@kyrSZwN!a&EqwSvZIREND{$h`C=_}+|aE1{+yIlk{9 zjnp^$0`55OWT_=-g4uI3`h4gghf}IkI*T(-$$V=@L2<76SL|HM_xr9$6wy54F<@4; z`9jD#RZ8<7*N0Osg9qD%%njsfIcT>Bq%l53!|1{2ftLj~_Ue41AK0c-Y*N|hy!Qe5 zb@gtd9X@Z;ECRI6zVlaRWU$ko6@JvLLk`vDB+&OG!zPcstGm5oTv-P@sCo?qO#qk? zOltbL=E#^L^u7#XSs4WwtT&D?+3>$%#uCagzW-2PZcUVT7lLs{u3#hoJ9{;eD3a|= z^EXQRUW@>N|APa)PrAvWXTSmYEhyC&&2HB6)`0gzL)V)u(Hc~OWdy0nZQtuHFF2A| zYs&Z2%0xXnDHpbHLz4eihKvmjys&0gTI1bCbA+2u> zmgJY~Re(zmyjP3JBfUG>bFVC1q=or-7sj$!PaC;3at=;#88y~5IEFVHgPNXSAik4J zGTuMJV!n&B6{eSR0&Dj^rbVhbx1K{y{0%-AL7Ls}>rr(C8n}dD*E*HIbpRsNClct{WBdRmnX+9H@Vu|+(8S&Zn!%jcg>DokS)m_~No|FL9ZV9PWvTh92G2d`eFb;Jq zke-NuaY1d+(DaX)1Ergg$r;d6kiCUe`eR^^tRrA4QQZLKZiA{zRp*j(>D=m2>l}Ow zIS6Fj)0n2MdBt((g=L#+7lw)OCa-v(TQ|wBlnQO0Y7Z0LH836+4>#=7y}4?;$KK&=1T{>;N?yXT zp0U@k7rU-8rn!2p^z(mfZm^w|cJHt()@4<#kcvy4%5baYvrzSN#t=Dc_RMIBWM-Z_ z(15#5#*Xb?;KH0(xIPqkFKlnp-Kt1g3I!BS=p?C^_cT*LiyB_p>R0=xw6+6stT?0u zMC42AwORfDP1TX}efX+P#zW5xmq@%X8b~hsJ1F>WSL2~HA0!x?s_@}L*h3<9f$82k zlOUWlVb^K(mLQSY!>L^U8-X~fMm4^mB7Z+yUh$?RV1lKqhLnRw8w_SN51WJR0*bX+ zsifbYD9!LgZ1=bKNe$+_FsrY*rLD$>FAxO$L)CYZVo^r2#qM$Cs#mVCx0u-zurf3e z3yttV=4SpK?HPgzcR->x6D-*1#z`Z8e}nFtQ1rf?MH)P&okbH1x3>Bo?*Wp^o#iB1 zAZs0HBTX}OM_JJ>b#4-D^nHD?Cv@+EAswv7WtgCb2;x_Kd;cF!6iyx@y3aT|gR;pO z@(7owO6@X3FKS(wWY|K(0fbIkQQm7%$aL_-SV^041x|}rNxOK9ue}?}66s^be(D?T zJ-hb9&&@{-C)mO!ga~c>i9fZpEu3+qPQYCc(mh6MYG;s8DJaWr7^1!jVp98;Rp?iC z-C!cLuK@R!<_+MT+@hJb6`%Kp#Gk@&A|_jn!>*JWFI+0k+Rlvg(fcpn!Sj`wj&@@a z#fFslX~aP8MuUs0UYxnL171|MfV>qRx`V4E!WQ}iYF}zmKv*0T{f!qb+rLj0LZ(o8 zj6}3NoNIT@q#HGt;=!-gSCZTd;Xp6JXK@U4qjJVzaHT>_4Qt&4z`x@E9+?uA||?!bd(?d@rc&<-Cb;Bw7$S_qhcICmj?v`=`g z`Jzdy%Ef*I{7=LT;{}g)kXMP0cy<s!D(}q0|1}P8V+1; zj!rBzSIK7b*H%c_QBPA&sG|Q5Xjy9zyNX>USCrEOcT}4lRmY<6i?s)hjAc>mfsYb` zPYuJrE`VMLe`oXdb2ngb zac#WCe5`0CL>cMKI&M5qJI?igVApk^rbeo*=OxLIiwo8luItO~G?P7J>IT%=RG3@^ zP*$|Gp6`6fn;UPY8DFv2X(37^!?G87{1f!vT*&pkq!2K__!MWJO>(GSU@l%pBfml)T27NEO>|u6`DO7?Ntgb|#&F$`Ulz zQg(mjzHt(n3GqjoyMv?+&7l1Wi^-#`cigLa1sh7IKi{9NW-+-USUfNHO)e-;;jCOJ zII#>=OL^VW&@!g_fK@x9PTaM)ZC9$e5z#wHe~oOaXJP@Q_RxNCivdnM)@`LO}%cU&K7uNmGJ?GMjcj?peg^DlFv12*k?&afoxc zoq~;xE5RhSxNIP%F_wy@p|Y@hGtP=cm{6n0Xq(ou8+EiFy`AVEW~$3wVrON74Q0t( z0v-`fsj0hoVmIq$!BZyEbFui~l=CeqjK|0ehBZ#6=6EupGr0}l=a9-@vwYD$TcW`* zx==S~%O~GyrX5$~$kVCXX@M=6k+BK!-D3-+8Ur*@mz^{qEoIqT17JR2S+Hiec_*{r zo!Z3tgXffvDL|&WV+|gqmm*`)5g^K|Kw6EJIFM1!6J~_=^|9=P(|csBSOiqh^1nVJnQB+ z#d(_K5PifJ%nt1Ty2P@tRkBx26b3g9sfU9A=p-V%RFgPh6!j80GulI#j#DZEj2;gz zP`&;js!yM!uINql7|A;5C2Tgb@femIDbzbpbx7vqdxpY#6g#c#svio&f`h+rbyDrv z)+*$@I6F&$6Q^3UrbZIK+VFT?&$@_sM6&(md?Z$khsxUF5$qcn{M1RmbjrgG=UVA zqE_Mc`997xhmi16=F+sG+|Aj*yVmjd ze0s3|M9G{1Xg#Z*8>xgXm3CkaFkil1)-_#IX{-t-^lpu7AhEa$>R`hcb$TCO1S2r| zl76R}p5N0RmH_y)(!q(L*rgwDFUt6`aM0ro$T@t+r62g)CQx)A76ddBeBu(M0QI{E zT_)Oy!AwzVihN0}URuj&j|OZgD)Aa1>w*Jzvej*lmD;lg0C;tEi!r>j zPm<5>Kj5~@gkfhaJI4mEa07A={y_g&&z~@@C>-D0{x!K1vD;zX=uKn_!kUjcs_Q1w z0Q)7a&CdWl*^!o((~X&*MP@ub-xhh-iwu5Yt>cA=(miaXze)hXXZ)^Bz=GG4}QGISMg`k z^dLkw9<-m1)&b!|aKUV#Z<+v*1F&fUm%tBZHv`?FSCfsTx6O24&$iY^9BS>M=N~Wy z$OuAJdecc?jlX!&T9S{&^Ol}64w)68snf6AIJBrdhb9>ZsSiLrM|oEiw%r%W*{D%9 zVxX=e=eIx|$iF+rCM3HhuAM|$o_rs6)~x#fhlt1%)z!bI$&z9Zl>}$Aqp3!|Pb>4J z-?rsA#?b3-TQYqC=z7g{GJzY6?O6hdo0xsrGkj*z7;G>{0M#rBDGC@YN^8!KL?z=# zs6e@O>}{4OYcZ%*{)~v(%}uAtore2=CiYmH_kca`7biF%Z0`8Zn2^o)a>Sm!2*D*t zBASaQmsa@Suo9@YLcZY{njyiCD4SQc<&-(&?LZ`tSoEE~Vn$DaSaEOcjr=8F_By0)o1oAgJH!eyYjZ!f4|0G^3Vozxb+qhj3uE@ zy(~KK&^FK_yDq=#GvZzxhfxfx#Z=b`H-gAj~ybt@G zjA7_opb0-?0Ap|L;*frEAfz+m4f z=_+Zw{FwuWKzKax>6<~{#9u4xhZ_Z>Zo2=z4*Ed_y%Y$xr087mbAL{ohprBUmNsEQ z8i&O&2#y4j=kl|is^Vi&>MX9t$AIY7!zqp&p9KaZ?ws*VnTP(iUo{x?gPG7B)Fs(R zc&jG7dl`o$cMCeAF8w*0P(v~s3tya+K@UfwVJFhe+EIq8FL+D91n&F~=Vz_mMiV7K z#Xk)+Ymzw`Ws%Pdb?vA!Fx^ZmS(3{0Sj4k zpkE+LI@%NRTYlwAWlY(`E6|osVY7p&?d9xS0!l?B?FzC;DdCHx{aBHw@LuWa#F}%W z1w!^m*Ud{REv((f4(d0)wj{@4W7~OwbFHQ$${i5If2YvPT3e&(tixFfr1ivpwSBef zt0b94y6IWVp^QL~Wjyr|!I5$Q{w*WzOgf`s*xDeQ3G38B6V5%Js(u7a8JKf zt|+nvak7YUHsgpM`Vc8n^mDhkZ7;`?X{LtJ)8gzh7AQV)Tv^~|Hdxh{Aao*k{=C=3 zM7}m0?Y2`u*8aYuOWIBUDsPLipKaJP7`q`Q*ZI5aZqoiwkoi5CCRwEx0j99Q+-00r zD*ZU0i!X<{6|UC8e5I7v>`Ipn^GkU7qz6k)q!ZN(xH$~oO%e~beZ=W4=}@YDQW{&@ z?pcHp1beqsq6qrOf7!Jqach9Y23cnnxCB?EET!s)=L<(J%apSN8^fIwm=#cO=EE{h zg%7)$c0Ki2Y8Ef|m>Ptu)gI=GmJ$Mk=#EAE^7ndbkvO{*Nw-Datk>;6A;Xz2vlkj= zSU37U@9E~w-L36B{@c~3-YoRr!Pc+}t{mWjtigU zrd8+fOzRAbJG)mIV+adFhA_A8x&TDD5HNDBs}943B%;R$AQG@+0M~8hyLclZ|8>Dk zy{;`&6ZmD%W;WTzvVZwO6fuuCV-ucumrg+8vrz>oh zS(|4Ela?}>T8}FkhgThiJz5F4rtsnqW~_RvnnW&ff*}NbK;-uVOfS7<&rh%5B5W>2 zh>dPGa&M1?yy!lU$X?bm?QqEoL=@NEziq-GhRVByc_$XD$jZEM5p@z>yQqqLI#C&% zu(IiJd~S}Xea=9|Ig<}U50!v{ERCYzMkaHjF9Fr_)I+kzG-kRqm@e+x2k)5>G^Z;9 zWC)Y|bmyK z7QNI@&DEpP!b=6x=moXuARjcT2;bDIXQ$@uzaM#R zbRN4+i<+lga{a8KWV@V4rpiJdv+E;7US%_jxjTQW*4+nE0cM{jZfd~jZGh2eQ=7aL z)4pTN5At6?j<@A5K89Z|ONp0;*%iC0<+MX@);=e_i*g%V zPbL7=0wPIS;AFv6+4p8$?r!1$GCjCU+K4F5rc`@Lj*k9K~6(MU%(9Tg&bSBjr zaA#|R-NssuBVns)iqJ>eIFJ--R|WZBE7(T=yn6TLg7odJZ&dh9Lo-JR>|||b+Lc7H zG@RxD_C8VRk7SWBj@J~`SigTigb=5$&>P178bB+kCT!VV{VM55ph;9iNl($z^BSQr z`rwzSzCFVVw1K{D?h`gcCI=B$Ryh~R@ToH4t0bI$CLJ3&f^Tt&PWpZTvIpw%G0#Wn zs+hl6nGja)B>6Et@W}JwofU0I`=}D}z$&z*FPW-k9Wg~BAg#$MaO0cLceTPTwVHERmCs_v&HH=jNwy9_~eF(@Q5@yC$<7K#JMr$`3s zY7uVDa5%^4#nG5V{zBYq8ATA^_r15;YKT}~V1#BUIU!aQsUArx)WV0Rpg4oRPHQ-Z zJBbc+bvmd@P`e1o_u+#Jz&k&pGgs?UT&0Y%>Wy9Q>@3SJznz$mL%ASIJ{fBC*+QWK z$dE3wBcb#N*!4^7zC8^zElE-B>27RIwKx1OJsYlxw444JO@Tfs>Qy2Ag%7v4bu@;e z>pJjriqz}2BkV!XAKa^%c37HRfY90ReRgFy5K!R*JeZxL9_8A%9DP20A#eIc1ZQyh z2b=xIV1;Wig8;Ly`MCN zIs_?UrYHyjOwTu?dS(=mrZ)0op$tkcPXmwbyA;D71lZDupLQG+)MlY#rayV1y1M>4 z`U_|f%%Kjq3e5e^2FiNxYi55VV--fo4TF(f`wl@=Aot3DF@xIaWC2XbP%8ZfO=AUM>OzENHH%S06?`USEHwwlA|4NSX#cp3bV9G^4F5lEa zeJE{nsm{ZHR`h6`B*N4Dy_Y7x1*vOkvP>zj@w;kbkGg6`BLc<9uB~iJk6oB8xe4IV zj={a0#5twa`&$B(&T%=109i5QLwna-E7E}*3ZN1<9Fr=4QQ8Zna(B1aT{<;;jpoWdgp&w$35>QYq#P=ll-L;h z=o1dW)Nx~^a2>@t+0FAuL0xU zBs8^p{DP1mZacW}8^OKmLkg3JV_~nkZF}*P4@BBS!B!ox`_4v%K+9?DsjQ!v9XH17 zSJ8~}byMeZY??1ixHUyfOLMU*ZCIsCSYzOCKF0&wGx3f4FZI9sXJ~^osC$Wq))%4W8;-WqoVVn!< zI}P5}o!Md;d{KckyHfDP6$M2ZPztwBeypmM*~Ycr@NJS8Uf7{(_%;vYY0AneB@c@aeFkG8vuo4kYH+nx~-I8)#VY@KuHWFouFN~=6?{!r0ee~mlBCs&Cf_9#lr8-0?FRlHXmi*%yBt2F1s&ZPlp`c`XqbpV{w1_ zpnb@O*h_>N41PBOl2VjR4yf7Q=Q6kUrmRZGKHYP$wgdG~qBBNV{V!kf3p>;aU42H^ z;}NYngzWW)To#F%B%a4{(9*G<@vtDha&OmLSJ;vzSpk+nw)V|KUoJgI7N9sg1$;fd ztm|cr-ov!S!(`J-hc<8LJWD1Z8)VR&~^@!@w1dm3K9{=BNTIk7y& z9)Q?uBt@O4#5Ei5%+;2!5q7Lv)-`WvtHqSo>a&2!fo`;3U;^%HP5{wU!;yZfYvkfv z^N`vHc#><;`P)=us|`Bw_s*1tN9e;{^DMxZPi)XMA&maMPx*Tq}4&@mPnOl#g}~+slI&C@d{zn zu;ZM#Zb_*i>mGL5e<0 z-(F=O_yck76(S?W6kEmr+W91)|9-@qWuGgYYjT&Mg`c+iK^k7+Qd=%CRL^J=B5W9w zz}POC4M*8ZLT;nzmAA8rGvpU-g^er*F$l~z5_f_GHlsVdZjto(^8`m^U0aCGkjQbTVS!z+)@>~)me>W z`J}?Ujr5w9()nDC2cuCCPbUq(r146hV*NQ~Ff996ljHtJlWVqmNa^H`)`u z_3{6@=mVmBKar%P!$>HYH$|Rk$KcAqQZVRvHeEWRjia>W69PjWampOMjN4wle+LYo z&s+SS_U+K+0Qg|o_My}q{Q<6z9pB?nI1rqUaN-1A63g|33MD^ErGb0{0(@!17$49SuL-%q-bf=>x z!{a#C;qbJ&XPX@)Fv?2SiM8wXT(;1M06Ad!NTX^xmO3a-QD4&1^&ax5+5&bg)8}l` zA~+Cjr2M<9VQYI|7{h~S1Zdpj-U${5BYr=#QyPxU=bpify|1FhlRRqAukbKi>hFHx zkwTabi7_7kxyezE<5mK!H%LqjL|-^fE3|K^fI8Giyl~BLqVj=98G(C)eG!bsvrPS$ z(5rFs?nXuW2;S&F^cbXB@{uHar^)aQqUhWG9OYZGQRtlw`{x#TQu!=c?}IM;U77E5 z&sEC>g!VX^U+LLTH_4e9-v1{g__m^dTv3#lc8?rg1@yZT8A?t_!IAfLM#yEIiM@(hSx}d^c7h83Iw~2$f7&U#{ni+A z{ad-UDFCzC)9ajd)Hq2}gKVT=TDiP>?u0chVUTC(OxAhQc}^!W0~TVSre(W#=a>w0 z_{v&%zRByQnb2;07c(1?=e@`pbR#cF`qR{-B2|{Q$kP%_#UZ{HXa(T!H8$ zWlt55T<@qh!uRRi4qWRi$H74u!ESct=%x&abza0PId9#$01kG}6x0!GB27njc_Y_- z9*3@}KDg-`hLYTs4_frj=G$n45h>ULSjvm>*rwznk$lo*d=0~w2EwC!m^UaVRU!{v z&$CTn08MakT$bmV;N0Z;a`VlGLMwF~2r4OGS|v!GfLHJd$4cOv3(0QFx;LPwo}81d zfsNSI4A8xB9atQNhPiRJav)$NjJq5ueM$O{6Np>Q9S3hJ_*B`U;ZLiXPs)%Z)<}$U zI+GEJrwy@(OSMzXerYh3@_^-2zKfX3u;wEWNJjwCLMnzTIyMbcuvYj?IMVW!VU3UA zN*yJ%vZ?&nu18guf`Ph)&0;qwi|%=L7)nk<1uNfUup6pgs*~%3g|w5X(Ku8W0f|?n zfRCZ!?hg?knokp~#OLRLLlz!!@6+I=e{&rlDwaoJRJ5Z4S8bh%10&%aeo>`BkGNdK z_eNFa6pdjIh(t{R)Fz@q68Z2ou5aD}e8FDG5R8<&so}@+`&DhhcWDBlPj`Yo2(ou# zYE~7oKYr&UTO)0R)20DoVMP1XL6+R8mfcrNg)KdA_2|e12-O^tFIZhB_fc1_fK!Z0 zS?Jkxh>P;26LCX^XNQ83(Z?80mHpu}L&%M+ZRFoReK zK*ek{VJfaDYDY0pJnM{!qNjlqNv^?%&Os|HO;$j(^2ux6437*1nwq*kd;?rL_}2BB zjF#ZN!AtwBLtJVSadJn$YJo)DkFN|LvJbD_LZ|ljyB?Cf$2Jmv{w2qv(GwgA(r1(t z{i6ZK(-d;v7wTu8kHLr(RuJ0*lp+_6{WEj^MWFt9Wl#ZJo#SVjoM@o&w80e6i3$8(As|S?%M+>2)Ql))l zp2P6hG8!dtj0Q{Bq{gx9FS`&+F-Kb^>oeA7Wt4Md%j*D!w7Ma;;wIJiILXfHOu{*7 z)g2-c+Os?kFCk*prX6!z${E3i%)R5Bc|G$l$>Kl1*2m+86b&zfREuA(2r4x@-- z#}6F?3hT$elOsspaU-w2LklL3s#Ozosoa@$IQd~UY!2W^_(!#8X*s(-LHQ^=xE&x^ zIX<>EtET>9P7>NPwRX~XP_%<8Dfnf^)w+T(7aP(mW7aq#rjb<@Hs*82Nl!u~i%&zP zrQ>dfwZC2spk8S{_ziS#dE^6;$_d~fb?VkPnNs`0YX6%#zLqY^+@GL$%J4MoTZ;^G zH@(?4a-Yz~&n;S6O4X-1$8gtn6C*LB+aHzS+}>4ypFETvJ0Y`TlqmvR-H$`NUp3`QeY}9;JZU_OX8`M z!vOl>&GplSNJM>Al;l8*Sqof2Sr96KHltALV~9xqp}O_7v#);%wd{G=xL9z({V!nG zq+pmRZ11`rBP_@QrU5<(7%BpoZ%F7&!^6Be7VRqV+EEWauQ8o*`KsspdRsSt2+Ji0 zf-b!r->BD%>u(cNiYOY79(Ny%Ig;5EE_Pj#{(ueg8MsBa2)ij%L@FSnxcMDrxvMP$lSvPZnJo~Le5TX~zY#!2>S`*h!tet9o@U8!y;A3}IOgxpZ$2JdEK z6)YF*(WxH!3==Ghjam*zqx1QGPA+yc_iMmL1AZV zVk3V#;wSB=uk|x&O3A{`>FCzo`P528{?iH2H|8OAA_O*mPRwicI%!IyOp0Ef;(zKQif Eov1;+rT_o{ literal 113792 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�!#G@7|#0lNH`-giiQ^@uFXaD-5?H;5?vF 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⋙O4IJ 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�hmi5d9>_!yQUPknbKoTaJ^U%Pk3FPT>9g7;3SNj-vLa z&J`xL&cB&$Kj{x}6|c(mddB*EkVV8!T-bN~vTLDH_7Z3`1KY2*nEuX*(wkEdVrrh2 zHq3ie57yS@5W_$$%=86kFwD4pzorrI;1#zcKc*TWvAlHM_|Syf<90e;&Lw(eaLwPX zlv#<3gxLv)Z=lDMj{ExvIGN&_R{!xl_y;s>Y79|a+FE#^nt2*Y9!?zHF&5T{XKHw? zS8y^{QoSb6ths@Y!6-7RV1oHd_O{R>x>I9f2KerGKT`}N;HDeD{%Fl_c2C32Xh;AX zN)sALo4$Ra+?BolsZi_lcd1@zo`9sHYuN zD@AFH>La*fUGUvJKgJO~FVw4&$g1CcL>?g`4CS!5ZwSkIeVQFdN!EiKdVIQxXDMLK z|FIRT=s8&lw11Fp-c}kK;=+yj{|@LeastIV$zR+&bA#fm_zNI%x|Zd^2TVA4fc92q zsZPQKeY>W;3e%kPX)PTuaC}+%m84|GilWx{^SiTJFO$tS`p)tL7iy_3Sdl<@(RDZ@ zX5JpjEq-WaE7CbRMV47NeqUKs2vc@t?#1FLm_61*pN17)!2#7%x4dbqhF+22P0aI_ z1js|p54XgTYkC*;j!M16Låm*xG7*ru(NatIilT!>12K_G_K9qAK2Uce8aHmZ` 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 E!gK)nI{*Lx diff --git a/worlds/wargroove/data/save/campaign-c40a6e5b0cdf86ddac03b276691c483d.cmp.bak b/worlds/wargroove/data/save/campaign-c40a6e5b0cdf86ddac03b276691c483d.cmp.bak index 5eb2c244e64b3e1d247387364ad50bd941ddc515..815d57726f7bc2a52ac60a91c4c39d16a9cd2ffa 100644 GIT binary patch literal 113792 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⁢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

+ + + + + + + + + + + + + + + + + + + + + +
Checks Available:Map Bombs:
Checks Available{{ checks_available }}Bombs Remaining{{ bombs_display }}/20
Map Width:Map Height:
Map Width{{ width_display }}/10Map Height{{ height_display }}/10
+
+ + diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 4565f6083b..f5cddfcab9 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -838,7 +838,7 @@ def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations: timespinner_location_ids["Past"].append(1337176) if(slot_data["LoreChecks"]): timespinner_location_ids["Present"] += [ - 1337177, 1337178, 1337179, + 1337177, 1337178, 1337179, 1337180, 1337181, 1337182, 1337183, 1337184, 1337185, 1337186, 1337187] timespinner_location_ids["Past"] += [ 1337188, 1337189, @@ -1219,6 +1219,84 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info, **display_data) +def __renderChecksfinder(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], + inventory: Counter, team: int, player: int, playerName: str, + seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict, saving_second: int) -> str: + + icons = { + "Checks Available": "https://0rganics.org/archipelago/cf/spr_tiles_3.png", + "Map Width": "https://0rganics.org/archipelago/cf/spr_tiles_4.png", + "Map Height": "https://0rganics.org/archipelago/cf/spr_tiles_5.png", + "Map Bombs": "https://0rganics.org/archipelago/cf/spr_tiles_6.png", + + "Nothing": "", + } + + checksfinder_location_ids = { + "Tile 1": 81000, + "Tile 2": 81001, + "Tile 3": 81002, + "Tile 4": 81003, + "Tile 5": 81004, + "Tile 6": 81005, + "Tile 7": 81006, + "Tile 8": 81007, + "Tile 9": 81008, + "Tile 10": 81009, + "Tile 11": 81010, + "Tile 12": 81011, + "Tile 13": 81012, + "Tile 14": 81013, + "Tile 15": 81014, + "Tile 16": 81015, + "Tile 17": 81016, + "Tile 18": 81017, + "Tile 19": 81018, + "Tile 20": 81019, + "Tile 21": 81020, + "Tile 22": 81021, + "Tile 23": 81022, + "Tile 24": 81023, + "Tile 25": 81024, + } + + display_data = {} + + # Multi-items + multi_items = { + "Map Width": 80000, + "Map Height": 80001, + "Map Bombs": 80002 + } + for item_name, item_id in multi_items.items(): + base_name = item_name.split()[-1].lower() + count = inventory[item_id] + display_data[base_name + "_count"] = count + display_data[base_name + "_display"] = count + 5 + + # Get location info + checked_locations = multisave.get("location_checks", {}).get((team, player), set()) + lookup_name = lambda id: lookup_any_location_id_to_name[id] + location_info = {tile_name: {lookup_name(tile_location): (tile_location in checked_locations)} for tile_name, tile_location in checksfinder_location_ids.items() if tile_location in set(locations[player])} + checks_done = {tile_name: len([tile_location]) for tile_name, tile_location in checksfinder_location_ids.items() if tile_location in checked_locations and tile_location in set(locations[player])} + checks_done['Total'] = len(checked_locations) + checks_in_area = checks_done + + # Calculate checks available + display_data["checks_unlocked"] = min(display_data["width_count"] + display_data["height_count"] + display_data["bombs_count"] + 5, 25) + display_data["checks_available"] = max(display_data["checks_unlocked"] - len(checked_locations), 0) + + # Victory condition + game_state = multisave.get("client_game_state", {}).get((team, player), 0) + display_data['game_finished'] = game_state == 30 + + return render_template("checksfinderTracker.html", + inventory=inventory, icons=icons, + acquired_items={lookup_any_item_id_to_name[id] for id in inventory if + id in lookup_any_item_id_to_name}, + player=player, team=team, room=room, player_name=playerName, + checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info, + **display_data) def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], inventory: Counter, team: int, player: int, playerName: str, @@ -1351,6 +1429,7 @@ game_specific_trackers: typing.Dict[str, typing.Callable] = { "Ocarina of Time": __renderOoTTracker, "Timespinner": __renderTimespinnerTracker, "A Link to the Past": __renderAlttpTracker, + "ChecksFinder": __renderChecksfinder, "Super Metroid": __renderSuperMetroidTracker, "Starcraft 2 Wings of Liberty": __renderSC2WoLTracker } From 8ca25fed638c2399724220899b88685032d4feb1 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 6 Mar 2023 12:54:32 +0100 Subject: [PATCH 026/172] Setup: clean up imports --- setup.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index df2d43ec6a..1d80377f08 100644 --- a/setup.py +++ b/setup.py @@ -11,12 +11,13 @@ import urllib.request import io import json import threading -import platform +import subprocess +import pkg_resources + from collections.abc import Iterable from hashlib import sha3_512 from pathlib import Path -import subprocess -import pkg_resources + # This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it try: From e6109394adc3df9be66bf2d25430c64f3db5ce75 Mon Sep 17 00:00:00 2001 From: beauxq Date: Mon, 6 Mar 2023 19:14:25 -0800 Subject: [PATCH 027/172] Zillion: use Option.current_key and other minor fixes --- worlds/zillion/__init__.py | 15 +++++++-------- worlds/zillion/logic.py | 1 + worlds/zillion/options.py | 14 +++++++++----- worlds/zillion/region.py | 2 +- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index 44d80cff50..78e20ce737 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -2,13 +2,12 @@ from collections import deque, Counter from contextlib import redirect_stdout import functools import threading -from typing import Any, Dict, List, Set, Tuple, Optional, cast +from typing import Any, Dict, List, Literal, Set, Tuple, Optional, cast import os import logging from BaseClasses import ItemClassification, LocationProgressType, \ MultiWorld, Item, CollectionState, Entrance, Tutorial -from Options import AssembleOptions from .logic import cs_to_zz_locs from .region import ZillionLocation, ZillionRegion from .options import ZillionStartChar, zillion_options, validate @@ -48,17 +47,17 @@ class ZillionWorld(World): game = "Zillion" web = ZillionWebWorld() - option_definitions: Dict[str, AssembleOptions] = zillion_options - topology_present: bool = True # indicate if world type has any meaningful layout/pathing + option_definitions = zillion_options + topology_present = True # indicate if world type has any meaningful layout/pathing # map names to their IDs - item_name_to_id: Dict[str, int] = _item_name_to_id - location_name_to_id: Dict[str, int] = _loc_name_to_id + 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: int = 1 + data_version = 1 logger: logging.Logger @@ -250,7 +249,7 @@ class ZillionWorld(World): if group["game"] == "Zillion": assert "item_pool" in group item_pool = group["item_pool"] - to_stay = "JJ" + to_stay: Literal['Apple', 'Champ', 'JJ'] = "JJ" if "JJ" in item_pool: assert "players" in group group_players = group["players"] diff --git a/worlds/zillion/logic.py b/worlds/zillion/logic.py index 204f242500..225076da09 100644 --- a/worlds/zillion/logic.py +++ b/worlds/zillion/logic.py @@ -42,6 +42,7 @@ def item_counts(cs: CollectionState, p: int) -> Tuple[Tuple[str, int], ...]: LogicCacheType = Dict[int, Tuple[_Counter[Tuple[str, int]], FrozenSet[Location]]] +""" { hash: (cs.prog_items, accessible_locations) } """ def cs_to_zz_locs(cs: CollectionState, p: int, zz_r: Randomizer, id_to_zz_item: Dict[int, Item]) -> FrozenSet[Location]: diff --git a/worlds/zillion/options.py b/worlds/zillion/options.py index 2c5a9dd8e7..6aa88f5b22 100644 --- a/worlds/zillion/options.py +++ b/worlds/zillion/options.py @@ -276,14 +276,14 @@ def validate(world: "MultiWorld", p: int) -> "Tuple[ZzOptions, Counter[str]]": skill = wo.skill[p].value jump_levels = cast(ZillionJumpLevels, wo.jump_levels[p]) - jump_option = jump_levels.get_current_option_name().lower() + jump_option = jump_levels.current_key required_level = char_to_jump["Apple"][cast(ZzVBLR, jump_option)].index(3) + 1 if skill == 0: # because of hp logic on final boss required_level = 8 gun_levels = cast(ZillionGunLevels, wo.gun_levels[p]) - gun_option = gun_levels.get_current_option_name().lower() + gun_option = gun_levels.current_key guns_required = char_to_gun["Champ"][cast(ZzVBLR, gun_option)].index(3) floppy_req = cast(ZillionFloppyReq, wo.floppy_req[p]) @@ -347,10 +347,14 @@ def validate(world: "MultiWorld", p: int) -> "Tuple[ZzOptions, Counter[str]]": # that should be all of the level requirements met + name_capitalization = { + "jj": "JJ", + "apple": "Apple", + "champ": "Champ", + } + start_char = cast(ZillionStartChar, wo.start_char[p]) - start_char_name = start_char.get_current_option_name() - if start_char_name == "Jj": - start_char_name = "JJ" + start_char_name = name_capitalization[start_char.current_key] assert start_char_name in chars start_char_name = cast(Chars, start_char_name) diff --git a/worlds/zillion/region.py b/worlds/zillion/region.py index 29ffb01d2b..cf5aa65889 100644 --- a/worlds/zillion/region.py +++ b/worlds/zillion/region.py @@ -15,7 +15,7 @@ class ZillionRegion(Region): name: str, hint: str, player: int, - multiworld: Optional[MultiWorld] = None) -> None: + multiworld: MultiWorld) -> None: super().__init__(name, player, multiworld, hint) self.zz_r = zz_r From 414166f6a27d92cf9104b8fff900e180a8b3586c Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 7 Mar 2023 01:44:20 -0600 Subject: [PATCH 028/172] Core: Minor Options cleanup (#1182) * Options.py cleanup * TextChoice cleanup * make Option.current_option_name a property * title the TextChoce option names * satisfy the linter * a little more options cleanup * move the typing import * typing should be PlandoSettings * fix incorrect conflict merging * make imports local * the tests seem to want me to import these twice though i hate it. * changes from review. Make the various Location verifying Options `LocationSet` * remove unnecessary fluff * begrudgingly support get_current_option_name. Leave a comment that worlds shouldn't be touching this * log a deprecation warning and return the property for `get_current_option_name()` --------- Co-authored-by: beauxq Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- BaseClasses.py | 2 +- Options.py | 66 ++++++++++--------- worlds/alttp/test/options/TestPlandoBosses.py | 10 +-- worlds/zillion/__init__.py | 2 +- 4 files changed, 43 insertions(+), 37 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 16a0600595..11eb1f870c 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1261,7 +1261,7 @@ class Spoiler(): res = getattr(self.multiworld, option_key)[player] display_name = getattr(option_obj, "display_name", option_key) try: - outfile.write(f'{display_name + ":":33}{res.get_current_option_name()}\n') + outfile.write(f'{display_name + ":":33}{res.current_option_name}\n') except: raise Exception diff --git a/Options.py b/Options.py index 10add11be6..5ce0901a7f 100644 --- a/Options.py +++ b/Options.py @@ -1,5 +1,6 @@ from __future__ import annotations import abc +import logging from copy import deepcopy import math import numbers @@ -9,6 +10,10 @@ import random from schema import Schema, And, Or, Optional from Utils import get_fuzzy_results +if typing.TYPE_CHECKING: + from BaseClasses import PlandoOptions + from worlds.AutoWorld import World + class AssembleOptions(abc.ABCMeta): def __new__(mcs, name, bases, attrs): @@ -95,11 +100,11 @@ class Option(typing.Generic[T], metaclass=AssembleOptions): supports_weighting = True # filled by AssembleOptions: - name_lookup: typing.Dict[int, str] + name_lookup: typing.Dict[T, str] options: typing.Dict[str, int] def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.get_current_option_name()})" + return f"{self.__class__.__name__}({self.current_option_name})" def __hash__(self) -> int: return hash(self.value) @@ -109,7 +114,14 @@ class Option(typing.Generic[T], metaclass=AssembleOptions): return self.name_lookup[self.value] def get_current_option_name(self) -> str: - """For display purposes.""" + """Deprecated. use current_option_name instead. TODO remove around 0.4""" + logging.warning(DeprecationWarning(f"get_current_option_name for {self.__class__.__name__} is deprecated." + f" use current_option_name instead. Worlds should use {self}.current_key")) + return self.current_option_name + + @property + def current_option_name(self) -> str: + """For display purposes. Worlds should be using current_key.""" return self.get_option_name(self.value) @classmethod @@ -131,17 +143,14 @@ class Option(typing.Generic[T], metaclass=AssembleOptions): ... if typing.TYPE_CHECKING: - from Generate import PlandoOptions - from worlds.AutoWorld import World - - def verify(self, world: World, player_name: str, plando_options: PlandoOptions) -> None: + def verify(self, world: typing.Type[World], player_name: str, plando_options: PlandoOptions) -> None: pass else: def verify(self, *args, **kwargs) -> None: pass -class FreeText(Option): +class FreeText(Option[str]): """Text option that allows users to enter strings. Needs to be validated by the world or option definition.""" @@ -162,7 +171,7 @@ class FreeText(Option): return cls.from_text(str(data)) @classmethod - def get_option_name(cls, value: T) -> str: + def get_option_name(cls, value: str) -> str: return value @@ -424,6 +433,7 @@ class Choice(NumericOption): class TextChoice(Choice): """Allows custom string input and offers choices. Choices will resolve to int and text will resolve to string""" + value: typing.Union[str, int] def __init__(self, value: typing.Union[str, int]): assert isinstance(value, str) or isinstance(value, int), \ @@ -434,8 +444,7 @@ class TextChoice(Choice): def current_key(self) -> str: if isinstance(self.value, str): return self.value - else: - return self.name_lookup[self.value] + return super().current_key @classmethod def from_text(cls, text: str) -> TextChoice: @@ -450,7 +459,7 @@ class TextChoice(Choice): def get_option_name(cls, value: T) -> str: if isinstance(value, str): return value - return cls.name_lookup[value] + return super().get_option_name(value) def __eq__(self, other: typing.Any): if isinstance(other, self.__class__): @@ -573,12 +582,11 @@ class PlandoBosses(TextChoice, metaclass=BossMeta): def valid_location_name(cls, value: str) -> bool: return value in cls.locations - def verify(self, world, player_name: str, plando_options) -> None: + def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None: if isinstance(self.value, int): return - from Generate import PlandoOptions + from BaseClasses import PlandoOptions if not(PlandoOptions.bosses & plando_options): - import logging # plando is disabled but plando options were given so pull the option and change it to an int option = self.value.split(";")[-1] self.value = self.options[option] @@ -716,7 +724,7 @@ class VerifyKeys: value: typing.Any @classmethod - def verify_keys(cls, data): + def verify_keys(cls, data: typing.List[str]): if cls.valid_keys: data = set(data) dataset = set(word.casefold() for word in data) if cls.valid_keys_casefold else set(data) @@ -725,7 +733,7 @@ class VerifyKeys: raise Exception(f"Found unexpected key {', '.join(extra)} in {cls}. " f"Allowed keys: {cls.valid_keys}.") - def verify(self, world, player_name: str, plando_options) -> None: + def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None: if self.convert_name_groups and self.verify_item_name: new_value = type(self.value)() # empty container of whatever value is for item_name in self.value: @@ -830,7 +838,9 @@ class OptionSet(Option[typing.Set[str]], VerifyKeys): return item in self.value -local_objective = Toggle # local triforce pieces, local dungeon prizes etc. +class ItemSet(OptionSet): + verify_item_name = True + convert_name_groups = True class Accessibility(Choice): @@ -866,11 +876,6 @@ common_options = { } -class ItemSet(OptionSet): - verify_item_name = True - convert_name_groups = True - - class LocalItems(ItemSet): """Forces these items to be in their native world.""" display_name = "Local Items" @@ -892,22 +897,23 @@ class StartHints(ItemSet): display_name = "Start Hints" -class StartLocationHints(OptionSet): +class LocationSet(OptionSet): + verify_location_name = True + + +class StartLocationHints(LocationSet): """Start with these locations and their item prefilled into the !hint command""" display_name = "Start Location Hints" - verify_location_name = True -class ExcludeLocations(OptionSet): +class ExcludeLocations(LocationSet): """Prevent these locations from having an important item""" display_name = "Excluded Locations" - verify_location_name = True -class PriorityLocations(OptionSet): +class PriorityLocations(LocationSet): """Prevent these locations from having an unimportant item""" display_name = "Priority Locations" - verify_location_name = True class DeathLink(Toggle): @@ -948,7 +954,7 @@ class ItemLinks(OptionList): pool |= {item_name} return pool - def verify(self, world, player_name: str, plando_options) -> None: + def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None: link: dict super(ItemLinks, self).verify(world, player_name, plando_options) existing_links = set() diff --git a/worlds/alttp/test/options/TestPlandoBosses.py b/worlds/alttp/test/options/TestPlandoBosses.py index a6c3485f60..83c1510a3e 100644 --- a/worlds/alttp/test/options/TestPlandoBosses.py +++ b/worlds/alttp/test/options/TestPlandoBosses.py @@ -1,5 +1,5 @@ import unittest -import Generate +from BaseClasses import PlandoOptions from Options import PlandoBosses @@ -123,14 +123,14 @@ class TestPlandoBosses(unittest.TestCase): regular = MultiBosses.from_any(regular_string) # plando should work with boss plando - plandoed.verify(None, "Player", Generate.PlandoOptions.bosses) + plandoed.verify(None, "Player", PlandoOptions.bosses) self.assertTrue(plandoed.value.startswith(plandoed_string)) # plando should fall back to default without boss plando - plandoed.verify(None, "Player", Generate.PlandoOptions.items) + plandoed.verify(None, "Player", PlandoOptions.items) self.assertEqual(plandoed, MultiBosses.option_vanilla) # mixed should fall back to mode - mixed.verify(None, "Player", Generate.PlandoOptions.items) # should produce a warning and still work + mixed.verify(None, "Player", PlandoOptions.items) # should produce a warning and still work self.assertEqual(mixed, MultiBosses.option_shuffle) # mode stuff should just work - regular.verify(None, "Player", Generate.PlandoOptions.items) + regular.verify(None, "Player", PlandoOptions.items) self.assertEqual(regular, MultiBosses.option_shuffle) diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index 78e20ce737..241cb452a9 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -255,7 +255,7 @@ class ZillionWorld(World): group_players = group["players"] start_chars = cast(Dict[int, ZillionStartChar], getattr(multiworld, "start_char")) players_start_chars = [ - (player, start_chars[player].get_current_option_name()) + (player, start_chars[player].current_option_name) for player in group_players ] start_char_counts = Counter(sc for _, sc in players_start_chars) From 5b64c5f9341f31c4243d009c13a79cc1392b97d7 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 7 Mar 2023 09:09:24 +0100 Subject: [PATCH 029/172] Subnautica: fix exported radiation logic (#1507) --- worlds/subnautica/Locations.py | 9 +++++++-- worlds/subnautica/Rules.py | 8 ++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/worlds/subnautica/Locations.py b/worlds/subnautica/Locations.py index da92f8d481..e0a33966f0 100644 --- a/worlds/subnautica/Locations.py +++ b/worlds/subnautica/Locations.py @@ -580,9 +580,14 @@ if False: # turn to True to export for Subnautica mod with open("locations.json", "w") as f: json.dump(payload, f) - def radiated(pos: Vector): - aurora_dist = math.sqrt((pos["x"] - 1038.0) ** 2 + (pos["y"] - -3.4) ** 2 + (pos["y"] - -163.1) ** 2) + # copy-paste from Rules + def is_radiated(x: float, y: float, z: float) -> bool: + aurora_dist = math.sqrt((x - 1038.0) ** 2 + y ** 2 + (z - -163.1) ** 2) return aurora_dist < 950 + # end of copy-paste + + def radiated(pos: Vector): + return is_radiated(pos["x"], pos["y"], pos["z"]) def far_away(pos: Vector): return (pos["x"] ** 2 + pos["z"] ** 2) > (800 ** 2) diff --git a/worlds/subnautica/Rules.py b/worlds/subnautica/Rules.py index 48db25a815..793c85be41 100644 --- a/worlds/subnautica/Rules.py +++ b/worlds/subnautica/Rules.py @@ -221,6 +221,11 @@ def get_max_depth(state: "CollectionState", player: int): ) +def is_radiated(x: float, y: float, z: float) -> bool: + aurora_dist = math.sqrt((x - 1038.0) ** 2 + y ** 2 + (z - -163.1) ** 2) + return aurora_dist < 950 + + def can_access_location(state: "CollectionState", player: int, loc: LocationDict) -> bool: need_laser_cutter = loc.get("need_laser_cutter", False) if need_laser_cutter and not has_laser_cutter(state, player): @@ -235,8 +240,7 @@ def can_access_location(state: "CollectionState", player: int, loc: LocationDict pos_y = pos["y"] pos_z = pos["z"] - aurora_dist = math.sqrt((pos_x - 1038.0) ** 2 + (pos_y - -3.4) ** 2 + (pos_z - -163.1) ** 2) - need_radiation_suit = aurora_dist < 950 + need_radiation_suit = is_radiated(pos_x, pos_y, pos_z) if need_radiation_suit and not state.has("Radiation Suit", player): return False From 016157a0eb75702cddf65e1c621d6c85d5dc35d7 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Mon, 6 Mar 2023 16:23:13 +0100 Subject: [PATCH 030/172] Witness: Fixed settings combination not rolling (see description) Settings combination: - EP Shuffle - disable_non_randomized - doors: panels or doors: none An Event Item was being created that is inaccessible. This is fixed now. (The fix makes sure that player_logic is not trying to create events for the sake of EPs that are disabled) Note: These two sets should probably be merged anyway, they used to behave differenty but no longer really do. But that will require some extra care on the client side as well. --- worlds/witness/player_logic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index eb5b7934db..3e81993dc9 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -40,7 +40,7 @@ class WitnessPlayerLogic: Panels outside of the same region will still be checked manually. """ - if panel_hex in self.COMPLETELY_DISABLED_CHECKS: + if panel_hex in self.COMPLETELY_DISABLED_CHECKS or panel_hex in self.PRECOMPLETED_LOCATIONS: return frozenset() check_obj = self.REFERENCE_LOGIC.CHECKS_BY_HEX[panel_hex] From 17e90ce12c9b8f4f20aa90a9fb3945318f61f692 Mon Sep 17 00:00:00 2001 From: Magnemania <89949176+Magnemania@users.noreply.github.com> Date: Tue, 7 Mar 2023 08:14:49 -0500 Subject: [PATCH 031/172] SC2: Greater variety on short generations (#1367) Originally, short generations used an artificial cull to create balanced mission distributions. This resulted in campaigns that were somewhat too consistent, and on some standard settings combinations, this resulted in campaigns having The Outlaws as the second mission 100% of the time. It also caused generation to fail a bit too easily if the player excluded too many missions. This removes the cull and adds an additional early Easy mission slot to all of the reduced sized campaigns. When playing on No Build settings, this also pushes many of the missions down a difficulty level to ensure greater variety, and pushes additional missions down on Advanced Tactics. Additional small fixes: The in-world Excluded Missions validation check is replaced by the core OptionSet check. Fixed issue with Existing Items not getting their upgrades locked with Units Always Have Upgrades on. --- Starcraft2Client.py | 8 +- worlds/sc2wol/Items.py | 6 +- worlds/sc2wol/LogicMixin.py | 8 +- worlds/sc2wol/MissionTables.py | 217 ++++++++++++++++----------------- worlds/sc2wol/Options.py | 23 ++-- worlds/sc2wol/PoolFilter.py | 130 ++++++++++---------- worlds/sc2wol/Regions.py | 138 +++++++++++---------- worlds/sc2wol/__init__.py | 7 +- 8 files changed, 273 insertions(+), 264 deletions(-) diff --git a/Starcraft2Client.py b/Starcraft2Client.py index 3b05f5aa87..cf16405766 100644 --- a/Starcraft2Client.py +++ b/Starcraft2Client.py @@ -52,9 +52,9 @@ class StarcraftClientProcessor(ClientCommandProcessor): """Overrides the current difficulty set for the seed. Takes the argument casual, normal, hard, or brutal""" options = difficulty.split() num_options = len(options) - difficulty_choice = options[0].lower() if num_options > 0: + difficulty_choice = options[0].lower() if difficulty_choice == "casual": self.ctx.difficulty_override = 0 elif difficulty_choice == "normal": @@ -71,7 +71,11 @@ class StarcraftClientProcessor(ClientCommandProcessor): return True else: - self.output("Difficulty needs to be specified in the command.") + if self.ctx.difficulty == -1: + self.output("Please connect to a seed before checking difficulty.") + else: + self.output("Current difficulty: " + ["Casual", "Normal", "Hard", "Brutal"][self.ctx.difficulty]) + self.output("To change the difficulty, add the name of the difficulty after the command.") return False def _cmd_disable_mission_check(self) -> bool: diff --git a/worlds/sc2wol/Items.py b/worlds/sc2wol/Items.py index aae83f5031..9776e4fed1 100644 --- a/worlds/sc2wol/Items.py +++ b/worlds/sc2wol/Items.py @@ -182,9 +182,11 @@ filler_items: typing.Tuple[str, ...] = ( '+15 Starting Vespene' ) +# Defense rating table +# Commented defense ratings are handled in LogicMixin defense_ratings = { "Siege Tank": 5, - "Maelstrom Rounds": 2, + # "Maelstrom Rounds": 2, "Planetary Fortress": 3, # Bunker w/ Marine/Marauder: 3, "Perdition Turret": 2, @@ -193,7 +195,7 @@ defense_ratings = { } zerg_defense_ratings = { "Perdition Turret": 2, - # Bunker w/ Firebat: 2 + # Bunker w/ Firebat: 2, "Hive Mind Emulator": 3, "Psi Disruptor": 3 } diff --git a/worlds/sc2wol/LogicMixin.py b/worlds/sc2wol/LogicMixin.py index dac9d856e7..c803835f63 100644 --- a/worlds/sc2wol/LogicMixin.py +++ b/worlds/sc2wol/LogicMixin.py @@ -17,10 +17,12 @@ class SC2WoLLogic(LogicMixin): or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has('Wraith', player) def _sc2wol_has_competent_anti_air(self, multiworld: MultiWorld, player: int) -> bool: - return self.has_any({'Marine', 'Goliath'}, player) or self._sc2wol_has_air_anti_air(multiworld, player) + return self.has('Goliath', player) \ + or self.has('Marine', player) and self.has_any({'Medic', 'Medivac'}, player) \ + or self._sc2wol_has_air_anti_air(multiworld, player) def _sc2wol_has_anti_air(self, multiworld: MultiWorld, player: int) -> bool: - return self.has_any({'Missile Turret', 'Thor', 'War Pigs', 'Spartan Company', "Hel's Angel", 'Battlecruiser', 'Wraith'}, player) \ + return self.has_any({'Missile Turret', 'Thor', 'War Pigs', 'Spartan Company', "Hel's Angel", 'Battlecruiser', 'Marine', 'Wraith'}, player) \ or self._sc2wol_has_competent_anti_air(multiworld, player) \ or get_option_value(multiworld, player, 'required_tactics') > 0 and self.has_any({'Ghost', 'Spectre'}, player) @@ -28,6 +30,8 @@ class SC2WoLLogic(LogicMixin): defense_score = sum((defense_ratings[item] for item in defense_ratings if self.has(item, player))) if self.has_any({'Marine', 'Marauder'}, player) and self.has('Bunker', player): defense_score += 3 + if self.has_all({'Siege Tank', 'Maelstrom Rounds'}, player): + defense_score += 2 if zerg_enemy: defense_score += sum((zerg_defense_ratings[item] for item in zerg_defense_ratings if self.has(item, player))) if self.has('Firebat', player) and self.has('Bunker', player): diff --git a/worlds/sc2wol/MissionTables.py b/worlds/sc2wol/MissionTables.py index d926ea6251..6db9354768 100644 --- a/worlds/sc2wol/MissionTables.py +++ b/worlds/sc2wol/MissionTables.py @@ -1,7 +1,5 @@ -from typing import NamedTuple, Dict, List, Set - -from BaseClasses import MultiWorld -from .Options import get_option_value +from typing import NamedTuple, Dict, List +from enum import IntEnum no_build_regions_list = ["Liberation Day", "Breakout", "Ghost of a Chance", "Piercing the Shroud", "Whispers of Doom", "Belly of the Beast"] @@ -13,6 +11,14 @@ hard_regions_list = ["Maw of the Void", "Engine of Destruction", "In Utter Darkn "Shatter the Sky"] +class MissionPools(IntEnum): + STARTER = 0 + EASY = 1 + MEDIUM = 2 + HARD = 3 + FINAL = 4 + + class MissionInfo(NamedTuple): id: int required_world: List[int] @@ -23,119 +29,119 @@ class MissionInfo(NamedTuple): class FillMission(NamedTuple): - type: str + type: int connect_to: List[int] # -1 connects to Menu category: str number: int = 0 # number of worlds need beaten completion_critical: bool = False # missions needed to beat game or_requirements: bool = False # true if the requirements should be or-ed instead of and-ed - relegate: bool = False # true if this is a slot no build missions should be relegated to. + removal_priority: int = 0 # how many missions missing from the pool required to remove this mission vanilla_shuffle_order = [ - FillMission("no_build", [-1], "Mar Sara", completion_critical=True), - FillMission("easy", [0], "Mar Sara", completion_critical=True), - FillMission("easy", [1], "Mar Sara", completion_critical=True), - FillMission("easy", [2], "Colonist"), - FillMission("medium", [3], "Colonist"), - FillMission("hard", [4], "Colonist", number=7), - FillMission("hard", [4], "Colonist", number=7, relegate=True), - FillMission("easy", [2], "Artifact", completion_critical=True), - FillMission("medium", [7], "Artifact", number=8, completion_critical=True), - FillMission("hard", [8], "Artifact", number=11, completion_critical=True), - FillMission("hard", [9], "Artifact", number=14, completion_critical=True), - FillMission("hard", [10], "Artifact", completion_critical=True), - FillMission("medium", [2], "Covert", number=4), - FillMission("medium", [12], "Covert"), - FillMission("hard", [13], "Covert", number=8, relegate=True), - FillMission("hard", [13], "Covert", number=8, relegate=True), - FillMission("medium", [2], "Rebellion", number=6), - FillMission("hard", [16], "Rebellion"), - FillMission("hard", [17], "Rebellion"), - FillMission("hard", [18], "Rebellion"), - FillMission("hard", [19], "Rebellion", relegate=True), - FillMission("medium", [8], "Prophecy"), - FillMission("hard", [21], "Prophecy"), - FillMission("hard", [22], "Prophecy"), - FillMission("hard", [23], "Prophecy", relegate=True), - FillMission("hard", [11], "Char", completion_critical=True), - FillMission("hard", [25], "Char", completion_critical=True), - FillMission("hard", [25], "Char", completion_critical=True), - FillMission("all_in", [26, 27], "Char", completion_critical=True, or_requirements=True) + FillMission(MissionPools.STARTER, [-1], "Mar Sara", completion_critical=True), + FillMission(MissionPools.EASY, [0], "Mar Sara", completion_critical=True), + FillMission(MissionPools.EASY, [1], "Mar Sara", completion_critical=True), + FillMission(MissionPools.EASY, [2], "Colonist"), + FillMission(MissionPools.MEDIUM, [3], "Colonist"), + FillMission(MissionPools.HARD, [4], "Colonist", number=7), + FillMission(MissionPools.HARD, [4], "Colonist", number=7, removal_priority=1), + FillMission(MissionPools.EASY, [2], "Artifact", completion_critical=True), + FillMission(MissionPools.MEDIUM, [7], "Artifact", number=8, completion_critical=True), + FillMission(MissionPools.HARD, [8], "Artifact", number=11, completion_critical=True), + FillMission(MissionPools.HARD, [9], "Artifact", number=14, completion_critical=True), + FillMission(MissionPools.HARD, [10], "Artifact", completion_critical=True), + FillMission(MissionPools.MEDIUM, [2], "Covert", number=4), + FillMission(MissionPools.MEDIUM, [12], "Covert"), + FillMission(MissionPools.HARD, [13], "Covert", number=8, removal_priority=3), + FillMission(MissionPools.HARD, [13], "Covert", number=8, removal_priority=2), + FillMission(MissionPools.MEDIUM, [2], "Rebellion", number=6), + FillMission(MissionPools.HARD, [16], "Rebellion"), + FillMission(MissionPools.HARD, [17], "Rebellion"), + FillMission(MissionPools.HARD, [18], "Rebellion"), + FillMission(MissionPools.HARD, [19], "Rebellion", removal_priority=5), + FillMission(MissionPools.MEDIUM, [8], "Prophecy", removal_priority=9), + FillMission(MissionPools.HARD, [21], "Prophecy", removal_priority=8), + FillMission(MissionPools.HARD, [22], "Prophecy", removal_priority=7), + FillMission(MissionPools.HARD, [23], "Prophecy", removal_priority=6), + FillMission(MissionPools.HARD, [11], "Char", completion_critical=True), + FillMission(MissionPools.HARD, [25], "Char", completion_critical=True, removal_priority=4), + FillMission(MissionPools.HARD, [25], "Char", completion_critical=True), + FillMission(MissionPools.FINAL, [26, 27], "Char", completion_critical=True, or_requirements=True) ] mini_campaign_order = [ - FillMission("no_build", [-1], "Mar Sara", completion_critical=True), - FillMission("easy", [0], "Colonist"), - FillMission("medium", [1], "Colonist"), - FillMission("medium", [0], "Artifact", completion_critical=True), - FillMission("medium", [3], "Artifact", number=4, completion_critical=True), - FillMission("hard", [4], "Artifact", number=8, completion_critical=True), - FillMission("medium", [0], "Covert", number=2), - FillMission("hard", [6], "Covert"), - FillMission("medium", [0], "Rebellion", number=3), - FillMission("hard", [8], "Rebellion"), - FillMission("medium", [4], "Prophecy"), - FillMission("hard", [10], "Prophecy"), - FillMission("hard", [5], "Char", completion_critical=True), - FillMission("hard", [5], "Char", completion_critical=True), - FillMission("all_in", [12, 13], "Char", completion_critical=True, or_requirements=True) + FillMission(MissionPools.STARTER, [-1], "Mar Sara", completion_critical=True), + FillMission(MissionPools.EASY, [0], "Colonist"), + FillMission(MissionPools.MEDIUM, [1], "Colonist"), + FillMission(MissionPools.EASY, [0], "Artifact", completion_critical=True), + FillMission(MissionPools.MEDIUM, [3], "Artifact", number=4, completion_critical=True), + FillMission(MissionPools.HARD, [4], "Artifact", number=8, completion_critical=True), + FillMission(MissionPools.MEDIUM, [0], "Covert", number=2), + FillMission(MissionPools.HARD, [6], "Covert"), + FillMission(MissionPools.MEDIUM, [0], "Rebellion", number=3), + FillMission(MissionPools.HARD, [8], "Rebellion"), + FillMission(MissionPools.MEDIUM, [4], "Prophecy"), + FillMission(MissionPools.HARD, [10], "Prophecy"), + FillMission(MissionPools.HARD, [5], "Char", completion_critical=True), + FillMission(MissionPools.HARD, [5], "Char", completion_critical=True), + FillMission(MissionPools.FINAL, [12, 13], "Char", completion_critical=True, or_requirements=True) ] gauntlet_order = [ - FillMission("no_build", [-1], "I", completion_critical=True), - FillMission("easy", [0], "II", completion_critical=True), - FillMission("medium", [1], "III", completion_critical=True), - FillMission("medium", [2], "IV", completion_critical=True), - FillMission("hard", [3], "V", completion_critical=True), - FillMission("hard", [4], "VI", completion_critical=True), - FillMission("all_in", [5], "Final", completion_critical=True) + FillMission(MissionPools.STARTER, [-1], "I", completion_critical=True), + FillMission(MissionPools.EASY, [0], "II", completion_critical=True), + FillMission(MissionPools.EASY, [1], "III", completion_critical=True), + FillMission(MissionPools.MEDIUM, [2], "IV", completion_critical=True), + FillMission(MissionPools.MEDIUM, [3], "V", completion_critical=True), + FillMission(MissionPools.HARD, [4], "VI", completion_critical=True), + FillMission(MissionPools.FINAL, [5], "Final", completion_critical=True) ] grid_order = [ - FillMission("no_build", [-1], "_1"), - FillMission("medium", [0], "_1"), - FillMission("medium", [1, 6, 3], "_1", or_requirements=True), - FillMission("hard", [2, 7], "_1", or_requirements=True), - FillMission("easy", [0], "_2"), - FillMission("medium", [1, 4], "_2", or_requirements=True), - FillMission("hard", [2, 5, 10, 7], "_2", or_requirements=True), - FillMission("hard", [3, 6, 11], "_2", or_requirements=True), - FillMission("medium", [4, 9, 12], "_3", or_requirements=True), - FillMission("hard", [5, 8, 10, 13], "_3", or_requirements=True), - FillMission("hard", [6, 9, 11, 14], "_3", or_requirements=True), - FillMission("hard", [7, 10], "_3", or_requirements=True), - FillMission("hard", [8, 13], "_4", or_requirements=True), - FillMission("hard", [9, 12, 14], "_4", or_requirements=True), - FillMission("hard", [10, 13], "_4", or_requirements=True), - FillMission("all_in", [11, 14], "_4", or_requirements=True) + FillMission(MissionPools.STARTER, [-1], "_1"), + FillMission(MissionPools.EASY, [0], "_1"), + FillMission(MissionPools.MEDIUM, [1, 6, 3], "_1", or_requirements=True), + FillMission(MissionPools.HARD, [2, 7], "_1", or_requirements=True), + FillMission(MissionPools.EASY, [0], "_2"), + FillMission(MissionPools.MEDIUM, [1, 4], "_2", or_requirements=True), + FillMission(MissionPools.HARD, [2, 5, 10, 7], "_2", or_requirements=True), + FillMission(MissionPools.HARD, [3, 6, 11], "_2", or_requirements=True), + FillMission(MissionPools.MEDIUM, [4, 9, 12], "_3", or_requirements=True), + FillMission(MissionPools.HARD, [5, 8, 10, 13], "_3", or_requirements=True), + FillMission(MissionPools.HARD, [6, 9, 11, 14], "_3", or_requirements=True), + FillMission(MissionPools.HARD, [7, 10], "_3", or_requirements=True), + FillMission(MissionPools.HARD, [8, 13], "_4", or_requirements=True), + FillMission(MissionPools.HARD, [9, 12, 14], "_4", or_requirements=True), + FillMission(MissionPools.HARD, [10, 13], "_4", or_requirements=True), + FillMission(MissionPools.FINAL, [11, 14], "_4", or_requirements=True) ] mini_grid_order = [ - FillMission("no_build", [-1], "_1"), - FillMission("medium", [0], "_1"), - FillMission("medium", [1, 5], "_1", or_requirements=True), - FillMission("easy", [0], "_2"), - FillMission("medium", [1, 3], "_2", or_requirements=True), - FillMission("hard", [2, 4], "_2", or_requirements=True), - FillMission("medium", [3, 7], "_3", or_requirements=True), - FillMission("hard", [4, 6], "_3", or_requirements=True), - FillMission("all_in", [5, 7], "_3", or_requirements=True) + FillMission(MissionPools.STARTER, [-1], "_1"), + FillMission(MissionPools.EASY, [0], "_1"), + FillMission(MissionPools.MEDIUM, [1, 5], "_1", or_requirements=True), + FillMission(MissionPools.EASY, [0], "_2"), + FillMission(MissionPools.MEDIUM, [1, 3], "_2", or_requirements=True), + FillMission(MissionPools.HARD, [2, 4], "_2", or_requirements=True), + FillMission(MissionPools.MEDIUM, [3, 7], "_3", or_requirements=True), + FillMission(MissionPools.HARD, [4, 6], "_3", or_requirements=True), + FillMission(MissionPools.FINAL, [5, 7], "_3", or_requirements=True) ] blitz_order = [ - FillMission("no_build", [-1], "I"), - FillMission("easy", [-1], "I"), - FillMission("medium", [0, 1], "II", number=1, or_requirements=True), - FillMission("medium", [0, 1], "II", number=1, or_requirements=True), - FillMission("medium", [0, 1], "III", number=2, or_requirements=True), - FillMission("medium", [0, 1], "III", number=2, or_requirements=True), - FillMission("hard", [0, 1], "IV", number=3, or_requirements=True), - FillMission("hard", [0, 1], "IV", number=3, or_requirements=True), - FillMission("hard", [0, 1], "V", number=4, or_requirements=True), - FillMission("hard", [0, 1], "V", number=4, or_requirements=True), - FillMission("hard", [0, 1], "Final", number=5, or_requirements=True), - FillMission("all_in", [0, 1], "Final", number=5, or_requirements=True) + FillMission(MissionPools.STARTER, [-1], "I"), + FillMission(MissionPools.EASY, [-1], "I"), + FillMission(MissionPools.MEDIUM, [0, 1], "II", number=1, or_requirements=True), + FillMission(MissionPools.MEDIUM, [0, 1], "II", number=1, or_requirements=True), + FillMission(MissionPools.MEDIUM, [0, 1], "III", number=2, or_requirements=True), + FillMission(MissionPools.MEDIUM, [0, 1], "III", number=2, or_requirements=True), + FillMission(MissionPools.HARD, [0, 1], "IV", number=3, or_requirements=True), + FillMission(MissionPools.HARD, [0, 1], "IV", number=3, or_requirements=True), + FillMission(MissionPools.HARD, [0, 1], "V", number=4, or_requirements=True), + FillMission(MissionPools.HARD, [0, 1], "V", number=4, or_requirements=True), + FillMission(MissionPools.HARD, [0, 1], "Final", number=5, or_requirements=True), + FillMission(MissionPools.FINAL, [0, 1], "Final", number=5, or_requirements=True) ] mission_orders = [vanilla_shuffle_order, vanilla_shuffle_order, mini_campaign_order, grid_order, mini_grid_order, blitz_order, gauntlet_order] @@ -176,40 +182,21 @@ vanilla_mission_req_table = { lookup_id_to_mission: Dict[int, str] = { data.id: mission_name for mission_name, data in vanilla_mission_req_table.items() if data.id} -no_build_starting_mission_locations = { +starting_mission_locations = { "Liberation Day": "Liberation Day: Victory", "Breakout": "Breakout: Victory", "Ghost of a Chance": "Ghost of a Chance: Victory", "Piercing the Shroud": "Piercing the Shroud: Victory", "Whispers of Doom": "Whispers of Doom: Victory", "Belly of the Beast": "Belly of the Beast: Victory", -} - -build_starting_mission_locations = { "Zero Hour": "Zero Hour: First Group Rescued", "Evacuation": "Evacuation: First Chysalis", - "Devil's Playground": "Devil's Playground: Tosh's Miners" -} - -advanced_starting_mission_locations = { + "Devil's Playground": "Devil's Playground: Tosh's Miners", "Smash and Grab": "Smash and Grab: First Relic", "The Great Train Robbery": "The Great Train Robbery: North Defiler" } -def get_starting_mission_locations(multiworld: MultiWorld, player: int) -> Set[str]: - if get_option_value(multiworld, player, 'shuffle_no_build') or get_option_value(multiworld, player, 'mission_order') < 2: - # Always start with a no-build mission unless explicitly relegating them - # Vanilla and Vanilla Shuffled always start with a no-build even when relegated - return no_build_starting_mission_locations - elif get_option_value(multiworld, player, 'required_tactics') > 0: - # Advanced Tactics/No Logic add more starting missions to the pool - return {**build_starting_mission_locations, **advanced_starting_mission_locations} - else: - # Standard starting missions when relegate is on - return build_starting_mission_locations - - alt_final_mission_locations = { "Maw of the Void": "Maw of the Void: Victory", "Engine of Destruction": "Engine of Destruction: Victory", diff --git a/worlds/sc2wol/Options.py b/worlds/sc2wol/Options.py index 4526328f53..4f2032d662 100644 --- a/worlds/sc2wol/Options.py +++ b/worlds/sc2wol/Options.py @@ -1,6 +1,7 @@ -from typing import Dict +from typing import Dict, FrozenSet, Union from BaseClasses import MultiWorld from Options import Choice, Option, Toggle, DefaultOnToggle, ItemSet, OptionSet, Range +from .MissionTables import vanilla_mission_req_table class GameDifficulty(Choice): @@ -110,6 +111,7 @@ class ExcludedMissions(OptionSet): Only applies on shortened mission orders. It may be impossible to build a valid campaign if too many missions are excluded.""" display_name = "Excluded Missions" + valid_keys = {mission_name for mission_name in vanilla_mission_req_table.keys() if mission_name != 'All-In'} # noinspection PyTypeChecker @@ -130,19 +132,10 @@ sc2wol_options: Dict[str, Option] = { } -def get_option_value(multiworld: MultiWorld, player: int, name: str) -> int: - option = getattr(multiworld, name, None) +def get_option_value(multiworld: MultiWorld, player: int, name: str) -> Union[int, FrozenSet]: + if multiworld is None: + return sc2wol_options[name].default - if option is None: - return 0 + player_option = getattr(multiworld, name)[player] - return int(option[player].value) - - -def get_option_set_value(multiworld: MultiWorld, player: int, name: str) -> set: - option = getattr(multiworld, name, None) - - if option is None: - return set() - - return option[player].value + return player_option.value diff --git a/worlds/sc2wol/PoolFilter.py b/worlds/sc2wol/PoolFilter.py index c4aa1098bb..16cc51f243 100644 --- a/worlds/sc2wol/PoolFilter.py +++ b/worlds/sc2wol/PoolFilter.py @@ -2,8 +2,8 @@ from typing import Callable, Dict, List, Set from BaseClasses import MultiWorld, ItemClassification, Item, Location from .Items import item_table from .MissionTables import no_build_regions_list, easy_regions_list, medium_regions_list, hard_regions_list,\ - mission_orders, get_starting_mission_locations, MissionInfo, vanilla_mission_req_table, alt_final_mission_locations -from .Options import get_option_value, get_option_set_value + mission_orders, MissionInfo, alt_final_mission_locations, MissionPools +from .Options import get_option_value from .LogicMixin import SC2WoLLogic # Items with associated upgrades @@ -21,34 +21,33 @@ STARPORT_UNITS = {"Medivac", "Wraith", "Viking", "Banshee", "Battlecruiser", "He PROTOSS_REGIONS = {"A Sinister Turn", "Echoes of the Future", "In Utter Darkness"} -def filter_missions(multiworld: MultiWorld, player: int) -> Dict[str, List[str]]: +def filter_missions(multiworld: MultiWorld, player: int) -> Dict[int, List[str]]: """ Returns a semi-randomly pruned tuple of no-build, easy, medium, and hard mission sets """ mission_order_type = get_option_value(multiworld, player, "mission_order") + shuffle_no_build = get_option_value(multiworld, player, "shuffle_no_build") shuffle_protoss = get_option_value(multiworld, player, "shuffle_protoss") - excluded_missions = set(get_option_set_value(multiworld, player, "excluded_missions")) - invalid_mission_names = excluded_missions.difference(vanilla_mission_req_table.keys()) - if invalid_mission_names: - raise Exception("Error in locked_missions - the following are not valid mission names: " + ", ".join(invalid_mission_names)) + excluded_missions = get_option_value(multiworld, player, "excluded_missions") mission_count = len(mission_orders[mission_order_type]) - 1 - # Vanilla and Vanilla Shuffled use the entire mission pool - if mission_count == 28: - return { - "no_build": no_build_regions_list[:], - "easy": easy_regions_list[:], - "medium": medium_regions_list[:], - "hard": hard_regions_list[:], - "all_in": ["All-In"] - } - - mission_pools = [ - [], - easy_regions_list, - medium_regions_list, - hard_regions_list - ] + mission_pools = { + MissionPools.STARTER: no_build_regions_list[:], + MissionPools.EASY: easy_regions_list[:], + MissionPools.MEDIUM: medium_regions_list[:], + MissionPools.HARD: hard_regions_list[:], + MissionPools.FINAL: [] + } + if mission_order_type == 0: + # Vanilla uses the entire mission pool + mission_pools[MissionPools.FINAL] = ['All-In'] + return mission_pools + elif mission_order_type == 1: + # Vanilla Shuffled ignores the player-provided excluded missions + excluded_missions = set() + # Omitting No-Build missions if not shuffling no-build + if not shuffle_no_build: + excluded_missions = excluded_missions.union(no_build_regions_list) # Omitting Protoss missions if not shuffling protoss if not shuffle_protoss: excluded_missions = excluded_missions.union(PROTOSS_REGIONS) @@ -58,46 +57,35 @@ def filter_missions(multiworld: MultiWorld, player: int) -> Dict[str, List[str]] excluded_missions.add(final_mission) else: final_mission = 'All-In' - # Yaml settings determine which missions can be placed in the first slot - mission_pools[0] = [mission for mission in get_starting_mission_locations(multiworld, player).keys() if mission not in excluded_missions] - # Removing the new no-build missions from their original sets - for i in range(1, len(mission_pools)): - mission_pools[i] = [mission for mission in mission_pools[i] if mission not in excluded_missions.union(mission_pools[0])] - # If the first mission is a build mission, there may not be enough locations to reach Outbreak as a second mission + # Excluding missions + for difficulty, mission_pool in mission_pools.items(): + mission_pools[difficulty] = [mission for mission in mission_pool if mission not in excluded_missions] + mission_pools[MissionPools.FINAL].append(final_mission) + # Mission pool changes on Build-Only if not get_option_value(multiworld, player, 'shuffle_no_build'): - # Swapping Outbreak and The Great Train Robbery - if "Outbreak" in mission_pools[1]: - mission_pools[1].remove("Outbreak") - mission_pools[2].append("Outbreak") - if "The Great Train Robbery" in mission_pools[2]: - mission_pools[2].remove("The Great Train Robbery") - mission_pools[1].append("The Great Train Robbery") - # Removing random missions from each difficulty set in a cycle - set_cycle = 0 - current_count = sum(len(mission_pool) for mission_pool in mission_pools) + def move_mission(mission_name, current_pool, new_pool): + if mission_name in mission_pools[current_pool]: + mission_pools[current_pool].remove(mission_name) + mission_pools[new_pool].append(mission_name) + # Replacing No Build missions with Easy missions + move_mission("Zero Hour", MissionPools.EASY, MissionPools.STARTER) + move_mission("Evacuation", MissionPools.EASY, MissionPools.STARTER) + move_mission("Devil's Playground", MissionPools.EASY, MissionPools.STARTER) + # Pushing Outbreak to Normal, as it cannot be placed as the second mission on Build-Only + move_mission("Outbreak", MissionPools.EASY, MissionPools.MEDIUM) + # Pushing extra Normal missions to Easy + move_mission("The Great Train Robbery", MissionPools.MEDIUM, MissionPools.EASY) + move_mission("Echoes of the Future", MissionPools.MEDIUM, MissionPools.EASY) + move_mission("Cutthroat", MissionPools.MEDIUM, MissionPools.EASY) + # Additional changes on Advanced Tactics + if get_option_value(multiworld, player, "required_tactics") > 0: + move_mission("The Great Train Robbery", MissionPools.EASY, MissionPools.STARTER) + move_mission("Smash and Grab", MissionPools.EASY, MissionPools.STARTER) + move_mission("Moebius Factor", MissionPools.MEDIUM, MissionPools.EASY) + move_mission("Welcome to the Jungle", MissionPools.MEDIUM, MissionPools.EASY) + move_mission("Engine of Destruction", MissionPools.HARD, MissionPools.MEDIUM) - if current_count < mission_count: - raise Exception("Not enough missions available to fill the campaign on current settings. Please exclude fewer missions.") - while current_count > mission_count: - if set_cycle == 4: - set_cycle = 0 - # Must contain at least one mission per set - mission_pool = mission_pools[set_cycle] - if len(mission_pool) <= 1: - if all(len(mission_pool) <= 1 for mission_pool in mission_pools): - raise Exception("Not enough missions available to fill the campaign on current settings. Please exclude fewer missions.") - else: - mission_pool.remove(multiworld.random.choice(mission_pool)) - current_count -= 1 - set_cycle += 1 - - return { - "no_build": mission_pools[0], - "easy": mission_pools[1], - "medium": mission_pools[2], - "hard": mission_pools[3], - "all_in": [final_mission] - } + return mission_pools def get_item_upgrades(inventory: List[Item], parent_item: Item or str): @@ -135,7 +123,21 @@ class ValidInventory: requirements = mission_requirements cascade_keys = self.cascade_removal_map.keys() units_always_have_upgrades = get_option_value(self.multiworld, self.player, "units_always_have_upgrades") - if self.min_units_per_structure > 0: + + # Locking associated items for items that have already been placed when units_always_have_upgrades is on + if units_always_have_upgrades: + existing_items = self.existing_items[:] + while existing_items: + existing_item = existing_items.pop() + items_to_lock = self.cascade_removal_map.get(existing_item, [existing_item]) + for item in items_to_lock: + if item in inventory: + inventory.remove(item) + locked_items.append(item) + if item in existing_items: + existing_items.remove(item) + + if self.min_units_per_structure > 0 and self.has_units_per_structure(): requirements.append(lambda state: state.has_units_per_structure()) def attempt_removal(item: Item) -> bool: @@ -151,6 +153,10 @@ class ValidInventory: return False return True + # Determining if the full-size inventory can complete campaign + if not all(requirement(self) for requirement in requirements): + raise Exception("Too many items excluded - campaign is impossible to complete.") + while len(inventory) + len(locked_items) > inventory_size: if len(inventory) == 0: raise Exception("Reduced item pool generation failed - not enough locations available to place items.") diff --git a/worlds/sc2wol/Regions.py b/worlds/sc2wol/Regions.py index bcf6434aa5..033636662b 100644 --- a/worlds/sc2wol/Regions.py +++ b/worlds/sc2wol/Regions.py @@ -2,7 +2,7 @@ from typing import List, Set, Dict, Tuple, Optional, Callable from BaseClasses import MultiWorld, Region, Entrance, Location from .Locations import LocationData from .Options import get_option_value -from .MissionTables import MissionInfo, mission_orders, vanilla_mission_req_table, alt_final_mission_locations +from .MissionTables import MissionInfo, mission_orders, vanilla_mission_req_table, alt_final_mission_locations, MissionPools from .PoolFilter import filter_missions @@ -14,34 +14,18 @@ def create_regions(multiworld: MultiWorld, player: int, locations: Tuple[Locatio mission_order = mission_orders[mission_order_type] mission_pools = filter_missions(multiworld, player) - final_mission = mission_pools['all_in'][0] - used_regions = [mission for mission_pool in mission_pools.values() for mission in mission_pool] regions = [create_region(multiworld, player, locations_per_region, location_cache, "Menu")] - for region_name in used_regions: - regions.append(create_region(multiworld, player, locations_per_region, location_cache, region_name)) - # Changing the completion condition for alternate final missions into an event - if final_mission != 'All-In': - final_location = alt_final_mission_locations[final_mission] - # Final location should be near the end of the cache - for i in range(len(location_cache) - 1, -1, -1): - if location_cache[i].name == final_location: - location_cache[i].locked = True - location_cache[i].event = True - location_cache[i].address = None - break - else: - final_location = 'All-In: Victory' - - if __debug__: - if mission_order_type in (0, 1): - throwIfAnyLocationIsNotAssignedToARegion(regions, locations_per_region.keys()) - - multiworld.regions += regions names: Dict[str, int] = {} if mission_order_type == 0: + + # Generating all regions and locations + for region_name in vanilla_mission_req_table.keys(): + regions.append(create_region(multiworld, player, locations_per_region, location_cache, region_name)) + multiworld.regions += regions + connect(multiworld, player, names, 'Menu', 'Liberation Day'), connect(multiworld, player, names, 'Liberation Day', 'The Outlaws', lambda state: state.has("Beat Liberation Day", player)), @@ -110,31 +94,32 @@ def create_regions(multiworld: MultiWorld, player: int, locations: Tuple[Locatio lambda state: state.has('Beat Gates of Hell', player) and ( state.has('Beat Shatter the Sky', player) or state.has('Beat Belly of the Beast', player))) - return vanilla_mission_req_table, 29, final_location + return vanilla_mission_req_table, 29, 'All-In: Victory' else: missions = [] + remove_prophecy = mission_order_type == 1 and not get_option_value(multiworld, player, "shuffle_protoss") + + final_mission = mission_pools[MissionPools.FINAL][0] + + # Determining if missions must be removed + mission_pool_size = sum(len(mission_pool) for mission_pool in mission_pools.values()) + removals = len(mission_order) - mission_pool_size + # Removing entire Prophecy chain on vanilla shuffled when not shuffling protoss + if remove_prophecy: + removals -= 4 + # Initial fill out of mission list and marking all-in mission for mission in mission_order: - if mission is None: + # Removing extra missions if mission pool is too small + if 0 < mission.removal_priority <= removals or mission.category == 'Prophecy' and remove_prophecy: missions.append(None) - elif mission.type == "all_in": + elif mission.type == MissionPools.FINAL: missions.append(final_mission) - elif mission.relegate and not get_option_value(multiworld, player, "shuffle_no_build"): - missions.append("no_build") else: missions.append(mission.type) - # Place Protoss Missions if we are not using ShuffleProtoss and are in Vanilla Shuffled - if get_option_value(multiworld, player, "shuffle_protoss") == 0 and mission_order_type == 1: - missions[22] = "A Sinister Turn" - mission_pools['medium'].remove("A Sinister Turn") - missions[23] = "Echoes of the Future" - mission_pools['medium'].remove("Echoes of the Future") - missions[24] = "In Utter Darkness" - mission_pools['hard'].remove("In Utter Darkness") - no_build_slots = [] easy_slots = [] medium_slots = [] @@ -144,79 +129,108 @@ def create_regions(multiworld: MultiWorld, player: int, locations: Tuple[Locatio for i in range(len(missions)): if missions[i] is None: continue - if missions[i] == "no_build": + if missions[i] == MissionPools.STARTER: no_build_slots.append(i) - elif missions[i] == "easy": + elif missions[i] == MissionPools.EASY: easy_slots.append(i) - elif missions[i] == "medium": + elif missions[i] == MissionPools.MEDIUM: medium_slots.append(i) - elif missions[i] == "hard": + elif missions[i] == MissionPools.HARD: hard_slots.append(i) # Add no_build missions to the pool and fill in no_build slots - missions_to_add = mission_pools['no_build'] + missions_to_add = mission_pools[MissionPools.STARTER] + if len(no_build_slots) > len(missions_to_add): + raise Exception("There are no valid No-Build missions. Please exclude fewer missions.") for slot in no_build_slots: filler = multiworld.random.randint(0, len(missions_to_add) - 1) missions[slot] = missions_to_add.pop(filler) # Add easy missions into pool and fill in easy slots - missions_to_add = missions_to_add + mission_pools['easy'] + missions_to_add = missions_to_add + mission_pools[MissionPools.EASY] + if len(easy_slots) > len(missions_to_add): + raise Exception("There are not enough Easy missions to fill the campaign. Please exclude fewer missions.") for slot in easy_slots: filler = multiworld.random.randint(0, len(missions_to_add) - 1) missions[slot] = missions_to_add.pop(filler) # Add medium missions into pool and fill in medium slots - missions_to_add = missions_to_add + mission_pools['medium'] + missions_to_add = missions_to_add + mission_pools[MissionPools.MEDIUM] + if len(medium_slots) > len(missions_to_add): + raise Exception("There are not enough Easy and Medium missions to fill the campaign. Please exclude fewer missions.") for slot in medium_slots: filler = multiworld.random.randint(0, len(missions_to_add) - 1) missions[slot] = missions_to_add.pop(filler) # Add hard missions into pool and fill in hard slots - missions_to_add = missions_to_add + mission_pools['hard'] + missions_to_add = missions_to_add + mission_pools[MissionPools.HARD] + if len(hard_slots) > len(missions_to_add): + raise Exception("There are not enough missions to fill the campaign. Please exclude fewer missions.") for slot in hard_slots: filler = multiworld.random.randint(0, len(missions_to_add) - 1) missions[slot] = missions_to_add.pop(filler) + # Generating regions and locations from selected missions + for region_name in missions: + regions.append(create_region(multiworld, player, locations_per_region, location_cache, region_name)) + multiworld.regions += regions + + # Mapping original mission slots to shifted mission slots when missions are removed + slot_map = [] + slot_offset = 0 + for position, mission in enumerate(missions): + slot_map.append(position - slot_offset + 1) + if mission is None: + slot_offset += 1 + # Loop through missions to create requirements table and connect regions # TODO: Handle 'and' connections mission_req_table = {} - for i in range(len(missions)): + + for i, mission in enumerate(missions): + if mission is None: + continue connections = [] for connection in mission_order[i].connect_to: + required_mission = missions[connection] if connection == -1: - connect(multiworld, player, names, "Menu", missions[i]) + connect(multiworld, player, names, "Menu", mission) + elif required_mission is None: + continue else: - connect(multiworld, player, names, missions[connection], missions[i], + connect(multiworld, player, names, required_mission, mission, (lambda name, missions_req: (lambda state: state.has(f"Beat {name}", player) and state._sc2wol_cleared_missions(multiworld, player, missions_req))) (missions[connection], mission_order[i].number)) - connections.append(connection + 1) + connections.append(slot_map[connection]) - mission_req_table.update({missions[i]: MissionInfo( - vanilla_mission_req_table[missions[i]].id, connections, mission_order[i].category, + mission_req_table.update({mission: MissionInfo( + vanilla_mission_req_table[mission].id, connections, mission_order[i].category, number=mission_order[i].number, completion_critical=mission_order[i].completion_critical, or_requirements=mission_order[i].or_requirements)}) final_mission_id = vanilla_mission_req_table[final_mission].id - return mission_req_table, final_mission_id, final_mission + ': Victory' + # Changing the completion condition for alternate final missions into an event + if final_mission != 'All-In': + final_location = alt_final_mission_locations[final_mission] + # Final location should be near the end of the cache + for i in range(len(location_cache) - 1, -1, -1): + if location_cache[i].name == final_location: + location_cache[i].locked = True + location_cache[i].event = True + location_cache[i].address = None + break + else: + final_location = 'All-In: Victory' -def throwIfAnyLocationIsNotAssignedToARegion(regions: List[Region], regionNames: Set[str]): - existingRegions = set() - - for region in regions: - existingRegions.add(region.name) - - if (regionNames - existingRegions): - raise Exception("Starcraft: the following regions are used in locations: {}, but no such region exists".format( - regionNames - existingRegions)) - + return mission_req_table, final_mission_id, final_location def create_location(player: int, location_data: LocationData, region: Region, location_cache: List[Location]) -> Location: diff --git a/worlds/sc2wol/__init__.py b/worlds/sc2wol/__init__.py index 878f3882dc..60de200804 100644 --- a/worlds/sc2wol/__init__.py +++ b/worlds/sc2wol/__init__.py @@ -7,10 +7,10 @@ from .Items import StarcraftWoLItem, item_table, filler_items, item_name_groups, get_basic_units from .Locations import get_locations from .Regions import create_regions -from .Options import sc2wol_options, get_option_value, get_option_set_value +from .Options import sc2wol_options, get_option_value from .LogicMixin import SC2WoLLogic from .PoolFilter import filter_missions, filter_items, get_item_upgrades -from .MissionTables import get_starting_mission_locations, MissionInfo +from .MissionTables import starting_mission_locations, MissionInfo class Starcraft2WoLWebWorld(WebWorld): @@ -137,7 +137,6 @@ def assign_starter_items(multiworld: MultiWorld, player: int, excluded_items: Se # The first world should also be the starting world first_mission = list(multiworld.worlds[player].mission_req_table)[0] - starting_mission_locations = get_starting_mission_locations(multiworld, player) if first_mission in starting_mission_locations: first_location = starting_mission_locations[first_mission] elif first_mission == "In Utter Darkness": @@ -174,7 +173,7 @@ def get_item_pool(multiworld: MultiWorld, player: int, mission_req_table: Dict[s locked_items = [] # YAML items - yaml_locked_items = get_option_set_value(multiworld, player, 'locked_items') + yaml_locked_items = get_option_value(multiworld, player, 'locked_items') for name, data in item_table.items(): if name not in excluded_items: From 5a8e6e61f5df34583af8ff4d50b2d494be6e945f Mon Sep 17 00:00:00 2001 From: t3hf1gm3nt <59876300+t3hf1gm3nt@users.noreply.github.com> Date: Wed, 8 Mar 2023 05:22:14 -0500 Subject: [PATCH 032/172] TLOZ: Code Cleanup (#1514) - consolidated declaration and population of level location lists - moved floor_location_game_ids_late declaration for consistency - moved generate_itempool to create_items, where it belongs - mention that expanded pool includes take any caves in the option description again - removed unnecessary StartingPosition check regarding Take Any Caves (leftover from older StartingPosition behavior I believe) - use proper comparisons to option keys instead of hardcoded ints --- worlds/tloz/ItemPool.py | 12 +++++++----- worlds/tloz/Locations.py | 13 +++---------- worlds/tloz/Options.py | 2 +- worlds/tloz/Rules.py | 27 ++++++++++++++------------- worlds/tloz/__init__.py | 9 ++++++--- 5 files changed, 31 insertions(+), 32 deletions(-) diff --git a/worlds/tloz/ItemPool.py b/worlds/tloz/ItemPool.py index 21ceafdc42..9dcf1b7aef 100644 --- a/worlds/tloz/ItemPool.py +++ b/worlds/tloz/ItemPool.py @@ -1,5 +1,6 @@ from BaseClasses import ItemClassification from .Locations import level_locations, all_level_locations, standard_level_locations, shop_locations +from .Options import TriforceLocations, StartingPosition # Swords are in starting_weapons overworld_items = { @@ -92,10 +93,11 @@ def get_pool_core(world): # Starting Weapon starting_weapon = random.choice(starting_weapons) - if world.multiworld.StartingPosition[world.player] == 0: + if world.multiworld.StartingPosition[world.player] == StartingPosition.option_safe: placed_items[starting_weapon_locations[0]] = starting_weapon - elif world.multiworld.StartingPosition[world.player] in [1, 2]: - if world.multiworld.StartingPosition[world.player] == 2: + elif world.multiworld.StartingPosition[world.player] in \ + [StartingPosition.option_unsafe, StartingPosition.option_dangerous]: + if world.multiworld.StartingPosition[world.player] == StartingPosition.option_dangerous: for location in dangerous_weapon_locations: if world.multiworld.ExpandedPool[world.player] or "Drop" not in location: starting_weapon_locations.append(location) @@ -115,9 +117,9 @@ def get_pool_core(world): possible_level_locations = [location for location in standard_level_locations if location not in level_locations[8]] for level in range(1, 9): - if world.multiworld.TriforceLocations[world.player] == 0: + if world.multiworld.TriforceLocations[world.player] == TriforceLocations.option_vanilla: placed_items[f"Level {level} Triforce"] = fragment - elif world.multiworld.TriforceLocations[world.player] == 1: + elif world.multiworld.TriforceLocations[world.player] == TriforceLocations.option_dungeons: placed_items[possible_level_locations.pop(random.randint(0, len(possible_level_locations) - 1))] = fragment else: pool.append(fragment) diff --git a/worlds/tloz/Locations.py b/worlds/tloz/Locations.py index bbd22abad4..3e46c43833 100644 --- a/worlds/tloz/Locations.py +++ b/worlds/tloz/Locations.py @@ -87,16 +87,9 @@ level_locations = [ ] ] -all_level_locations = [] -for level in level_locations: - for location in level: - all_level_locations.append(location) +all_level_locations = [location for level in level_locations for location in level] -standard_level_locations = [] -for level in level_locations: - for location in level: - if "Drop" not in location: - standard_level_locations.append(location) +standard_level_locations = [location for level in level_locations for location in level if "Drop" not in location] shop_locations = [ "Arrow Shop Item Left", "Arrow Shop Item Middle", "Arrow Shop Item Right", @@ -190,7 +183,6 @@ floor_location_game_offsets_early = { } floor_location_game_ids_early = {} -floor_location_game_ids_late = {} for key, value in floor_location_game_offsets_early.items(): floor_location_game_ids_early[key] = value + Rom.first_quest_dungeon_items_early @@ -254,6 +246,7 @@ floor_location_game_offsets_late = { "Level 9 Rupee Drop (Gels East)": 0x26 } +floor_location_game_ids_late = {} for key, value in floor_location_game_offsets_late.items(): floor_location_game_ids_late[key] = value + Rom.first_quest_dungeon_items_late diff --git a/worlds/tloz/Options.py b/worlds/tloz/Options.py index 47eb9509aa..96bd3e296d 100644 --- a/worlds/tloz/Options.py +++ b/worlds/tloz/Options.py @@ -3,7 +3,7 @@ from Options import Option, DefaultOnToggle, Choice class ExpandedPool(DefaultOnToggle): - """Puts room clear drops into the pool of items and locations.""" + """Puts room clear drops and take any caves into the pool of items and locations.""" display_name = "Expanded Item Pool" diff --git a/worlds/tloz/Rules.py b/worlds/tloz/Rules.py index 631a23d067..c73ea470b5 100644 --- a/worlds/tloz/Rules.py +++ b/worlds/tloz/Rules.py @@ -3,6 +3,7 @@ from typing import TYPE_CHECKING from ..generic.Rules import add_rule from .Locations import food_locations, shop_locations from .ItemPool import dangerous_weapon_locations +from .Options import StartingPosition if TYPE_CHECKING: from . import TLoZWorld @@ -22,7 +23,8 @@ def set_rules(tloz_world: "TLoZWorld"): # No dungeons without weapons except for the dangerous weapon locations if we're dangerous, no unsafe dungeons for i, level in enumerate(tloz_world.levels[1:10]): for location in level.locations: - if world.StartingPosition[player] < 1 or location.name not in dangerous_weapon_locations: + if world.StartingPosition[player] < StartingPosition.option_dangerous \ + or location.name not in dangerous_weapon_locations: add_rule(world.get_location(location.name, player), lambda state: state.has_group("weapons", player)) if i > 0: # Don't need an extra heart for Level 1 @@ -107,18 +109,17 @@ def set_rules(tloz_world: "TLoZWorld"): add_rule(world.get_location(location, player), lambda state: state.has("Stepladder", player)) - if world.StartingPosition[player] != 2: - # Don't allow Take Any Items until we can actually get in one - if world.ExpandedPool[player]: - add_rule(world.get_location("Take Any Item Left", player), - lambda state: state.has_group("candles", player) or - state.has("Raft", player)) - add_rule(world.get_location("Take Any Item Middle", player), - lambda state: state.has_group("candles", player) or - state.has("Raft", player)) - add_rule(world.get_location("Take Any Item Right", player), - lambda state: state.has_group("candles", player) or - state.has("Raft", player)) + # Don't allow Take Any Items until we can actually get in one + if world.ExpandedPool[player]: + add_rule(world.get_location("Take Any Item Left", player), + lambda state: state.has_group("candles", player) or + state.has("Raft", player)) + add_rule(world.get_location("Take Any Item Middle", player), + lambda state: state.has_group("candles", player) or + state.has("Raft", player)) + add_rule(world.get_location("Take Any Item Right", player), + lambda state: state.has_group("candles", player) or + state.has("Raft", player)) for location in tloz_world.levels[4].locations: add_rule(world.get_location(location.name, player), lambda state: state.has("Raft", player) or state.has("Recorder", player)) diff --git a/worlds/tloz/__init__.py b/worlds/tloz/__init__.py index 3304569cbd..551d6588ef 100644 --- a/worlds/tloz/__init__.py +++ b/worlds/tloz/__init__.py @@ -14,7 +14,7 @@ from .Locations import location_table, level_locations, major_locations, shop_lo standard_level_locations, shop_price_location_ids, secret_money_ids, location_ids, food_locations from .Options import tloz_options from .Rom import TLoZDeltaPatch, get_base_rom_path, first_quest_dungeon_items_early, first_quest_dungeon_items_late -from .Rules import set_rules +from .Rules import set_rules from worlds.AutoWorld import World, WebWorld from worlds.generic.Rules import add_rule @@ -134,6 +134,11 @@ class TLoZWorld(World): self.multiworld.regions.append(menu) self.multiworld.regions.append(overworld) + def create_items(self): + # refer to ItemPool.py + generate_itempool(self) + + # refer to Rules.py set_rules = set_rules def generate_basic(self): @@ -144,9 +149,7 @@ class TLoZWorld(World): self.multiworld.get_location("Zelda", self.player).place_locked_item(self.create_event("Rescued Zelda!")) add_rule(self.multiworld.get_location("Zelda", self.player), lambda state: ganon in state.locations_checked) - self.multiworld.completion_condition[self.player] = lambda state: state.has("Rescued Zelda!", self.player) - generate_itempool(self) def apply_base_patch(self, rom): # The base patch source is on a different repo, so here's the summary of changes: From d57314a40790c97db8ff7d5223ec2710f0be8080 Mon Sep 17 00:00:00 2001 From: Jarno Date: Wed, 8 Mar 2023 20:05:30 +0100 Subject: [PATCH 033/172] Timespinner: Bring back starter progression item (#1508) --- worlds/timespinner/Items.py | 16 ++++++++++++++++ worlds/timespinner/__init__.py | 31 ++++++++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/worlds/timespinner/Items.py b/worlds/timespinner/Items.py index add8beabf5..45c67c2547 100644 --- a/worlds/timespinner/Items.py +++ b/worlds/timespinner/Items.py @@ -239,6 +239,22 @@ starter_spells: Tuple[str, ...] = ( 'Corruption' ) +# weighted +starter_progression_items: Tuple[str, ...] = ( + 'Talaria Attachment', + 'Talaria Attachment', + 'Succubus Hairpin', + 'Succubus Hairpin', + 'Timespinner Wheel', + 'Timespinner Wheel', + 'Twin Pyramid Key', + 'Celestial Sash', + 'Lightwall', + 'Modern Warp Beacon', + 'Timeworn Warp Beacon', + 'Mysterious Warp Beacon' +) + filler_items: Tuple[str, ...] = ( 'Potion', 'Ether', diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index d9640727d7..de1d58e961 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -1,6 +1,7 @@ from typing import Dict, List, Set, Tuple, TextIO, Union from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification -from .Items import get_item_names_per_category, item_table, starter_melee_weapons, starter_spells, filler_items +from .Items import get_item_names_per_category +from .Items import item_table, starter_melee_weapons, starter_spells, filler_items, starter_progression_items from .Locations import get_location_datas, EventId from .Options import is_option_enabled, get_option_value, timespinner_options from .PreCalculatedWeights import PreCalculatedWeights @@ -70,6 +71,7 @@ class TimespinnerWorld(World): excluded_items: Set[str] = self.get_excluded_items() self.assign_starter_items(excluded_items) + self.place_first_progression_item(excluded_items) self.multiworld.itempool += self.get_item_pool(excluded_items) @@ -241,9 +243,32 @@ class TimespinnerWorld(World): def assign_starter_item(self, excluded_items: Set[str], location: str, item_list: Tuple[str, ...]) -> None: item_name = self.multiworld.random.choice(item_list) - excluded_items.add(item_name) + self.place_locked_item(excluded_items, location, item_name) - item = self.create_item(item_name) + def place_first_progression_item(self, excluded_items: Set[str]) -> None: + if self.is_option_enabled("QuickSeed") or self.is_option_enabled("Inverted") \ + or self.precalculated_weights.flood_lake_desolation: + return + + for item in self.multiworld.precollected_items[self.player]: + if item.name in starter_progression_items and not item.name in excluded_items: + return + + local_starter_progression_items = tuple( + item for item in starter_progression_items + if item not in excluded_items and item not in self.multiworld.non_local_items[self.player].value) + + if not local_starter_progression_items: + return + + progression_item = self.multiworld.random.choice(local_starter_progression_items) + + self.multiworld.local_early_items[self.player][progression_item] = 1 + + def place_locked_item(self, excluded_items: Set[str], location: str, item: str) -> None: + excluded_items.add(item) + + item = self.create_item(item) self.multiworld.get_location(location, self.player).place_locked_item(item) From e3deb822ad4d27ea88e68f8959849e48e5f8379e Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Wed, 8 Mar 2023 15:15:28 -0600 Subject: [PATCH 034/172] Core: implement location_name_groups (#1502) --- Main.py | 1 + MultiServer.py | 19 ++++++++++- Options.py | 5 +++ worlds/AutoWorld.py | 3 ++ worlds/alttp/__init__.py | 69 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 96 insertions(+), 1 deletion(-) diff --git a/Main.py b/Main.py index ba1787b078..372cadc5fd 100644 --- a/Main.py +++ b/Main.py @@ -361,6 +361,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No if game_world.data_version == 0 and game_world.game not in datapackage: datapackage[game_world.game] = worlds.network_data_package["games"][game_world.game] datapackage[game_world.game]["item_name_groups"] = game_world.item_name_groups + datapackage[game_world.game]["location_name_groups"] = game_world.location_name_groups multidata = { "slot_data": slot_data, diff --git a/MultiServer.py b/MultiServer.py index faeb1b220b..6c3106a93d 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -163,7 +163,9 @@ class Context: item_names: typing.Dict[int, str] = 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_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]] non_hintable_names: typing.Dict[str, typing.Set[str]] def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int, @@ -232,7 +234,9 @@ class Context: # init empty to satisfy linter, I suppose self.gamespackage = {} self.item_name_groups = {} + self.location_name_groups = {} self.all_item_and_group_names = {} + self.all_location_and_group_names = {} self.non_hintable_names = collections.defaultdict(frozenset) self._load_game_data() @@ -244,6 +248,8 @@ class Context: self.item_name_groups = {world_name: world.item_name_groups for world_name, world in worlds.AutoWorldRegister.world_types.items()} + self.location_name_groups = {world_name: world.location_name_groups for world_name, world in + worlds.AutoWorldRegister.world_types.items()} for world_name, world in worlds.AutoWorldRegister.world_types.items(): self.non_hintable_names[world_name] = world.hint_blacklist @@ -255,6 +261,8 @@ class Context: self.location_names[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[game_name]) 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 @@ -428,10 +436,14 @@ class Context: logging.info(f"Loading custom datapackage for game {game_name}") self.gamespackage[game_name] = data self.item_name_groups[game_name] = data["item_name_groups"] + self.location_name_groups[game_name] = data["location_name_groups"] del data["item_name_groups"] # remove from datapackage, but keep in self.item_name_groups + del data["location_name_groups"] self._init_game_data() for game_name, data in self.item_name_groups.items(): self.read_data[f"item_name_groups_{game_name}"] = lambda lgame=game_name: self.item_name_groups[lgame] + for game_name, data in self.location_name_groups.items(): + self.read_data[f"location_name_groups_{game_name}"] = lambda lgame=game_name: self.location_name_groups[lgame] # saving @@ -1403,7 +1415,7 @@ class ClientMessageProcessor(CommonCommandProcessor): if game not in self.ctx.all_item_and_group_names: self.output("Can't look up item/location for unknown game. Hint for ID instead.") return False - names = self.ctx.location_names_for_game(game) \ + names = self.ctx.all_location_and_group_names[game] \ if for_location else \ self.ctx.all_item_and_group_names[game] hint_name, usable, response = get_intended_text(input_text, names) @@ -1419,6 +1431,11 @@ class ClientMessageProcessor(CommonCommandProcessor): hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name)) elif not for_location and hint_name in self.ctx.item_names_for_game(game): # item name hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name) + elif hint_name in self.ctx.location_name_groups[game]: # location group name + hints = [] + for loc_name in self.ctx.location_name_groups[game][hint_name]: + if loc_name in self.ctx.location_names_for_game(game): + hints.extend(collect_hint_location_name(self.ctx, self.client.team, self.client.slot, loc_name)) else: # location name hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name) diff --git a/Options.py b/Options.py index 5ce0901a7f..8c73962602 100644 --- a/Options.py +++ b/Options.py @@ -739,6 +739,11 @@ class VerifyKeys: for item_name in self.value: new_value |= world.item_name_groups.get(item_name, {item_name}) self.value = new_value + elif self.convert_name_groups and self.verify_location_name: + new_value = type(self.value)() + for loc_name in self.value: + new_value |= world.location_name_groups.get(loc_name, {loc_name}) + self.value = new_value if self.verify_item_name: for item_name in self.value: if item_name not in world.item_names: diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index f2a639eebf..3fb705bdf3 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -149,6 +149,9 @@ class World(metaclass=AutoWorldRegister): item_name_groups: ClassVar[Dict[str, Set[str]]] = {} """maps item group names to sets of items. Example: {"Weapons": {"Sword", "Bow"}}""" + 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] = 1 """ increment this every time something in your world's names/id mappings changes. diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 4121dbadf2..78f12e5119 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -117,6 +117,75 @@ class ALTTPWorld(World): option_definitions = alttp_options topology_present = True item_name_groups = item_name_groups + location_name_groups = { + "Blind's Hideout": {"Blind's Hideout - Top", "Blind's Hideout - Left", "Blind's Hideout - Right", + "Blind's Hideout - Far Left", "Blind's Hideout - Far Right"}, + "Kakariko Well": {"Kakariko Well - Top", "Kakariko Well - Left", "Kakariko Well - Middle", + "Kakariko Well - Right", "Kakariko Well - Bottom"}, + "Mini Moldorm Cave": {"Mini Moldorm Cave - Far Left", "Mini Moldorm Cave - Left", "Mini Moldorm Cave - Right", + "Mini Moldorm Cave - Far Right", "Mini Moldorm Cave - Generous Guy"}, + "Paradox Cave": {"Paradox Cave Lower - Far Left", "Paradox Cave Lower - Left", "Paradox Cave Lower - Right", + "Paradox Cave Lower - Far Right", "Paradox Cave Lower - Middle", "Paradox Cave Upper - Left", + "Paradox Cave Upper - Right"}, + "Hype Cave": {"Hype Cave - Top", "Hype Cave - Middle Right", "Hype Cave - Middle Left", + "Hype Cave - Bottom", "Hype Cave - Generous Guy"}, + "Hookshot Cave": {"Hookshot Cave - Top Right", "Hookshot Cave - Top Left", "Hookshot Cave - Bottom Right", + "Hookshot Cave - Bottom Left"}, + "Hyrule Castle": {"Hyrule Castle - Boomerang Chest", "Hyrule Castle - Map Chest", + "Hyrule Castle - Zelda's Chest", "Sewers - Dark Cross", "Sewers - Secret Room - Left", + "Sewers - Secret Room - Middle", "Sewers - Secret Room - Right"}, + "Eastern Palace": {"Eastern Palace - Compass Chest", "Eastern Palace - Big Chest", + "Eastern Palace - Cannonball Chest", "Eastern Palace - Big Key Chest", + "Eastern Palace - Map Chest", "Eastern Palace - Boss"}, + "Desert Palace": {"Desert Palace - Big Chest", "Desert Palace - Torch", "Desert Palace - Map Chest", + "Desert Palace - Compass Chest", "Desert Palace Big Key Chest", "Desert Palace - Boss"}, + "Tower of Hera": {"Tower of Hera - Basement Cage", "Tower of Hera - Map Chest", "Tower of Hera - Big Key Chest", + "Tower of Hera - Compass Chest", "Tower of Hera - Big Chest", "Tower of Hera - Boss"}, + "Palace of Darkness": {"Palace of Darkness - Shooter Room", "Palace of Darkness - The Arena - Bridge", + "Palace of Darkness - Stalfos Basement", "Palace of Darkness - Big Key Chest", + "Palace of Darkness - The Arena - Ledge", "Palace of Darkness - Map Chest", + "Palace of Darkness - Compass Chest", "Palace of Darkness - Dark Basement - Left", + "Palace of Darkness - Dark Basement - Right", "Palace of Darkness - Dark Maze - Top", + "Palace of Darkness - Dark Maze - Bottom", "Palace of Darkness - Big Chest", + "Palace of Darkness - Harmless Hellway", "Palace of Darkness - Boss"}, + "Swamp Palace": {"Swamp Palace - Entrance", "Swamp Palace - Swamp Palace - Map Chest", + "Swamp Palace - Big Chest", "Swamp Palace - Compass Chest", "Swamp Palace - Big Key Chest", + "Swamp Palace - West Chest", "Swamp Palace - Flooded Room - Left", + "Swamp Palace - Flooded Room - Right", "Swamp Palace - Waterfall Room", "Swamp Palace - Boss"}, + "Thieves' Town": {"Thieves' Town - Big Key Chest", "Thieves' Town - Map Chest", "Thieves' Town - Compass Chest", + "Thieves' Town - Ambush Chest", "Thieves' Town - Attic", "Thieves' Town - Big Chest", + "Thieves' Town - Blind's Cell", "Thieves' Town - Boss"}, + "Skull Woods": {"Skull Woods - Map Chest", "Skull Woods - Pinball Room", "Skull Woods - Compass Chest", + "Skull Woods - Pot Prison", "Skull Woods - Big Chest", "Skull Woods - Big Key Chest", + "Skull Woods - Bridge Room", "Skull Woods - Boss"}, + "Ice Palace": {"Ice Palace - Compass Chest", "Ice Palace - Freezor Chest", "Ice Palace - Big Chest", + "Ice Palace - Freezor Chest", "Ice Palace - Big Chest", "Ice Palace - Iced T Room", + "Ice Palace - Spike Room", "Ice Palace - Big Key Chest", "Ice Palace - Map Chest", + "Ice Palace - Boss"}, + "Misery Mire": {"Misery Mire - Big Chest", "Misery Mire - Map Chest", "Misery Mire - Main Lobby", + "Misery Mire - Bridge Chest", "Misery Mire - Spike Chest", "Misery Mire - Compass Chest", + "Misery Mire - Big Key Chest", "Misery Mire - Boss"}, + "Turtle Rock": {"Turtle Rock - Compass Chest", "Turtle Rock - Roller Room - Left", + "Turtle Rock - Roller Room - Right", "Turtle Room - Chain Chomps", "Turtle Rock - Big Key Chest", + "Turtle Rock - Big Chest", "Turtle Rock - Crystaroller Room", + "Turtle Rock - Eye Bridge - Bottom Left", "Turtle Rock - Eye Bridge - Bottom Right", + "Turtle Rock - Eye Bridge - Top Left", "Turtle Rock - Eye Bridge - Top Right", "Turtle Rock - Boss"}, + "Ganons Tower": {"Ganons Tower - Bob's Torch", "Ganon's Tower - Hope Room - Left", + "Ganons Tower - Hope Room - Right", "Ganons Tower - Tile Room", + "Ganons Tower - Compass Room - Top Left", "Ganons Tower - Compass Room - Top Right", + "Ganons Tower - Compass Room - Bottom Left", "Ganons Tower - Compass Room - Bottom Left", + "Ganons Tower - DMs Room - Top Left", "Ganons Tower - DMs Room - Top Right", + "Ganons Tower - DMs Room - Bottom Left", "Ganons Tower - DMs Room - Bottom Right", + "Ganons Tower - Map Chest", "Ganons Tower - Firesnake Room", + "Ganons Tower - Randomizer Room - Top Left", "Ganons Tower - Randomizer Room - Top Right", + "Ganons Tower - Randomizer Room - Bottom Left", "Ganons Tower - Randomizer Room - Bottom Right", + "Ganons Tower - Bob's Chest", "Ganons Tower - Big Chest", "Ganons Tower - Big Key Room - Left", + "Ganons Tower - Big Key Room - Right", "Ganons Tower - Big Key Chest", + "Ganons Tower - Mini Helmasaur Room - Left", "Ganons Tower - Mini Helmasaur Room - Right", + "Ganons Tower - Pre-Moldorm Room", "Ganons Tower - Validation Chest"}, + "Ganons Tower Climb": {"Ganons Tower - Mini Helmasaur Room - Left", "Ganons Tower - Mini Helmasaur Room - Right", + "Ganons Tower - Pre-Moldorm Room", "Ganons Tower - Validation Chest"}, + } hint_blacklist = {"Triforce"} item_name_to_id = {name: data.item_code for name, data in item_table.items() if type(data.item_code) == int} From 738319462d29fe4835b49390b78e64245f6e3a7f Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Wed, 8 Mar 2023 15:19:38 -0600 Subject: [PATCH 035/172] Spoiler: Don't double print if world overrides common options (#1505) --- BaseClasses.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 11eb1f870c..a375699fb9 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -2,20 +2,20 @@ from __future__ import annotations import copy import functools -import json import logging import random import secrets import typing # this can go away when Python 3.8 support is dropped from argparse import Namespace -from collections import OrderedDict, Counter, deque -from enum import unique, IntEnum, IntFlag +from collections import OrderedDict, Counter, deque, ChainMap +from enum import IntEnum, IntFlag from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple, TypedDict, Callable, NamedTuple import NetUtils import Options import Utils + class Group(TypedDict, total=False): name: str game: str @@ -1278,12 +1278,11 @@ class Spoiler(): if self.multiworld.players > 1: outfile.write('\nPlayer %d: %s\n' % (player, self.multiworld.get_player_name(player))) outfile.write('Game: %s\n' % self.multiworld.game[player]) - for f_option, option in Options.per_game_common_options.items(): + + options = ChainMap(Options.per_game_common_options, self.multiworld.worlds[player].option_definitions) + for f_option, option in options.items(): write_option(f_option, option) - options = self.multiworld.worlds[player].option_definitions - if options: - for f_option, option in options.items(): - write_option(f_option, option) + AutoWorld.call_single(self.multiworld, "write_spoiler_header", player, outfile) if self.entrances: From a95e51dedadc5f97e4bf78def81d1c3f92d4df1d Mon Sep 17 00:00:00 2001 From: Freya Arbjerg Date: Wed, 8 Mar 2023 22:39:15 +0100 Subject: [PATCH 036/172] Add generic multiworld tracker, move lttp multiworld tracker (#1478) Co-authored-by: Berserker --- .gitignore | 1 + WebHostLib/static/assets/lttpMultiTracker.js | 6 + .../{tracker.js => multiTrackerCommon.js} | 7 +- WebHostLib/static/styles/tracker.css | 27 ++++ WebHostLib/templates/hostRoom.html | 2 +- .../{tracker.html => lttpMultiTracker.html} | 6 +- WebHostLib/templates/multiTracker.html | 87 ++++++++++ .../templates/multiTrackerNavigation.html | 12 ++ WebHostLib/tracker.py | 149 ++++++++++++++---- 9 files changed, 256 insertions(+), 41 deletions(-) create mode 100644 WebHostLib/static/assets/lttpMultiTracker.js rename WebHostLib/static/assets/{tracker.js => multiTrackerCommon.js} (97%) rename WebHostLib/templates/{tracker.html => lttpMultiTracker.html} (97%) create mode 100644 WebHostLib/templates/multiTracker.html create mode 100644 WebHostLib/templates/multiTrackerNavigation.html diff --git a/.gitignore b/.gitignore index c964b929f1..4b1cf4a0cd 100644 --- a/.gitignore +++ b/.gitignore @@ -139,6 +139,7 @@ ENV/ env.bak/ venv.bak/ .code-workspace +shell.nix # Spyder project settings .spyderproject diff --git a/WebHostLib/static/assets/lttpMultiTracker.js b/WebHostLib/static/assets/lttpMultiTracker.js new file mode 100644 index 0000000000..e90331028d --- /dev/null +++ b/WebHostLib/static/assets/lttpMultiTracker.js @@ -0,0 +1,6 @@ +window.addEventListener('load', () => { + $(".table-wrapper").scrollsync({ + y_sync: true, + x_sync: true + }); +}); diff --git a/WebHostLib/static/assets/tracker.js b/WebHostLib/static/assets/multiTrackerCommon.js similarity index 97% rename from WebHostLib/static/assets/tracker.js rename to WebHostLib/static/assets/multiTrackerCommon.js index 1a24b95c79..c08590cbf7 100644 --- a/WebHostLib/static/assets/tracker.js +++ b/WebHostLib/static/assets/multiTrackerCommon.js @@ -110,7 +110,7 @@ window.addEventListener('load', () => { const update = () => { const target = $("
"); console.log("Updating Tracker..."); - target.load("/tracker/" + tracker, function (response, status) { + 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"); @@ -137,10 +137,5 @@ window.addEventListener('load', () => { tables.draw(); }); - $(".table-wrapper").scrollsync({ - y_sync: true, - x_sync: true - }); - adjustTableHeight(); }); diff --git a/WebHostLib/static/styles/tracker.css b/WebHostLib/static/styles/tracker.css index e203d9e97d..0e00553c72 100644 --- a/WebHostLib/static/styles/tracker.css +++ b/WebHostLib/static/styles/tracker.css @@ -119,6 +119,33 @@ img.alttp-sprite { background-color: #d3c97d; } +#tracker-navigation { + display: inline-flex; + background-color: #b0a77d; + margin: 0.5rem; + border-radius: 4px; +} + +.tracker-navigation-button { + display: block; + margin: 4px; + padding-left: 12px; + padding-right: 12px; + border-radius: 4px; + text-align: center; + font-size: 14px; + color: #000; + font-weight: lighter; +} + +.tracker-navigation-button:hover { + background-color: #e2eabb !important; +} + +.tracker-navigation-button.selected { + background-color: rgb(220, 226, 189); +} + @media all and (max-width: 1700px) { table.dataTable thead th.upper-row{ position: -webkit-sticky; diff --git a/WebHostLib/templates/hostRoom.html b/WebHostLib/templates/hostRoom.html index 1c8f3de255..0a4ca70edd 100644 --- a/WebHostLib/templates/hostRoom.html +++ b/WebHostLib/templates/hostRoom.html @@ -14,7 +14,7 @@
{% endif %} {% if room.tracker %} - This room has a Multiworld Tracker enabled. + This room has a Multiworld Tracker enabled.
{% endif %} The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity. diff --git a/WebHostLib/templates/tracker.html b/WebHostLib/templates/lttpMultiTracker.html similarity index 97% rename from WebHostLib/templates/tracker.html rename to WebHostLib/templates/lttpMultiTracker.html index 96148e3454..a86c6851cb 100644 --- a/WebHostLib/templates/tracker.html +++ b/WebHostLib/templates/lttpMultiTracker.html @@ -1,14 +1,16 @@ {% extends 'tablepage.html' %} {% block head %} {{ super() }} - Multiworld Tracker + ALttP Multiworld Tracker - + + {% endblock %} {% block body %} {% include 'header/dirtHeader.html' %} + {% include 'multiTrackerNavigation.html' %}
diff --git a/WebHostLib/templates/multiTracker.html b/WebHostLib/templates/multiTracker.html new file mode 100644 index 0000000000..25eeec8d78 --- /dev/null +++ b/WebHostLib/templates/multiTracker.html @@ -0,0 +1,87 @@ +{% extends 'tablepage.html' %} +{% block head %} + {{ super() }} + Multiworld Tracker + + +{% endblock %} + +{% block body %} + {% include 'header/dirtHeader.html' %} + {% include 'multiTrackerNavigation.html' %} +
+
+ + + + Multistream + + + Clicking on a slot's number will bring up a slot-specific auto-tracker. This tracker will automatically update itself periodically. +
+
+ {% for team, players in checks_done.items() %} +
+ + + + + + + + + + + + {%- for player, checks in players.items() -%} + + + + + + {%- if activity_timers[(team, player)] -%} + + {%- else -%} + + {%- endif -%} + + {%- endfor -%} + +
#NameChecks%Last
Activity
{{ loop.index }}{{ player_names[(team, loop.index)]|e }}{{ checks["Total"] }}/{{ checks_in_area[player]["Total"] }}{{ percent_total_checks_done[team][player] }}{{ activity_timers[(team, player)].total_seconds() }}None
+
+ {% endfor %} + {% for team, hints in hints.items() %} +
+ + + + + + + + + + + + + {%- for hint in hints -%} + + + + + + + + + {%- endfor -%} + +
FinderReceiverItemLocationEntranceFound
{{ long_player_names[team, hint.finding_player] }}{{ long_player_names[team, hint.receiving_player] }}{{ hint.item|item_name }}{{ hint.location|location_name }}{% if hint.entrance %}{{ hint.entrance }}{% else %}Vanilla{% endif %}{% if hint.found %}✔{% endif %}
+
+ {% endfor %} +
+
+{% endblock %} diff --git a/WebHostLib/templates/multiTrackerNavigation.html b/WebHostLib/templates/multiTrackerNavigation.html new file mode 100644 index 0000000000..c6498fc68d --- /dev/null +++ b/WebHostLib/templates/multiTrackerNavigation.html @@ -0,0 +1,12 @@ +{%- if enabled_multiworld_trackers|length > 1 -%} +
+ {% for enabled_tracker in enabled_multiworld_trackers %} + {% set tracker_url = url_for(enabled_tracker.endpoint, tracker=room.tracker) %} + {%- if enabled_tracker.current -%} + {{ enabled_tracker.name }} + {%- else -%} + {{ enabled_tracker.name }} + {%- endif -%} + {% endfor %} +
+{%- endif -%} diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index f5cddfcab9..c2a44324b6 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -210,14 +210,6 @@ del data del item -def attribute_item(inventory, team, recipient, item): - target_item = links.get(item, item) - if item in levels: # non-progressive - inventory[team][recipient][target_item] = max(inventory[team][recipient][target_item], levels[item]) - else: - inventory[team][recipient][target_item] += 1 - - def attribute_item_solo(inventory, item): """Adds item to inventory counter, converts everything to progressive.""" target_item = links.get(item, item) @@ -486,7 +478,7 @@ def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: D "Water Bottle": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/75/Water_Bottle_JE2_BE2.png", "Spyglass": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c1/Spyglass_JE2_BE1.png", "Dragon Egg Shard": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/38/Dragon_Egg_JE4.png", - "Lead": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/1/1f/Lead_JE2_BE2.png", + "Lead": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/1/1f/Lead_JE2_BE2.png", "Saddle": "https://i.imgur.com/2QtDyR0.png", "Channeling Book": "https://i.imgur.com/J3WsYZw.png", "Silk Touch Book": "https://i.imgur.com/iqERxHQ.png", @@ -494,7 +486,7 @@ def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: D } minecraft_location_ids = { - "Story": [42073, 42023, 42027, 42039, 42002, 42009, 42010, 42070, + "Story": [42073, 42023, 42027, 42039, 42002, 42009, 42010, 42070, 42041, 42049, 42004, 42031, 42025, 42029, 42051, 42077], "Nether": [42017, 42044, 42069, 42058, 42034, 42060, 42066, 42076, 42064, 42071, 42021, 42062, 42008, 42061, 42033, 42011, 42006, 42019, 42000, 42040, 42001, 42015, 42104, 42014], @@ -656,7 +648,7 @@ def __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[in if base_name == "hookshot": display_data['hookshot_length'] = {0: '', 1: 'H', 2: 'L'}.get(level) - if base_name == "wallet": + if base_name == "wallet": display_data['wallet_size'] = {0: '99', 1: '200', 2: '500', 3: '999'}.get(level) # Determine display for bottles. Show letter if it's obtained, determine bottle count @@ -804,7 +796,7 @@ def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations: } timespinner_location_ids = { - "Present": [ + "Present": [ 1337000, 1337001, 1337002, 1337003, 1337004, 1337005, 1337006, 1337007, 1337008, 1337009, 1337010, 1337011, 1337012, 1337013, 1337014, 1337015, 1337016, 1337017, 1337018, 1337019, 1337020, 1337021, 1337022, 1337023, 1337024, 1337025, 1337026, 1337027, 1337028, 1337029, @@ -825,14 +817,14 @@ def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations: 1337150, 1337151, 1337152, 1337153, 1337154, 1337155, 1337171, 1337172, 1337173, 1337174, 1337175], "Ancient Pyramid": [ - 1337236, + 1337236, 1337246, 1337247, 1337248, 1337249] } if(slot_data["DownloadableItems"]): timespinner_location_ids["Present"] += [ 1337156, 1337157, 1337159, - 1337160, 1337161, 1337162, 1337163, 1337164, 1337165, 1337166, 1337167, 1337168, 1337169, + 1337160, 1337161, 1337162, 1337163, 1337164, 1337165, 1337166, 1337167, 1337168, 1337169, 1337170] if(slot_data["Cantoran"]): timespinner_location_ids["Past"].append(1337176) @@ -1323,9 +1315,28 @@ def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dic custom_items=custom_items, custom_locations=custom_locations) +def get_enabled_multiworld_trackers(room: Room, current: str): + enabled = [ + { + "name": "Generic", + "endpoint": "get_multiworld_tracker", + "current": current == "Generic" + } + ] + + if any(slot.game == "A Link to the Past" for slot in room.seed.slots) or current == "A Link to the Past": + enabled.append({ + "name": "A Link to the Past", + "endpoint": "get_LttP_multiworld_tracker", + "current": current == "A Link to the Past"} + ) + + return enabled + + @app.route('/tracker/') -@cache.memoize(timeout=1) # multisave is currently created at most every minute -def getTracker(tracker: UUID): +@cache.memoize(timeout=60) # multisave is currently created at most every minute +def get_multiworld_tracker(tracker: UUID): room: Room = Room.get(tracker=tracker) if not room: abort(404) @@ -1333,9 +1344,6 @@ def getTracker(tracker: UUID): precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \ get_static_room_data(room) - inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1) if playernumber not in groups} - for teamnumber, team in enumerate(names)} - checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations} for playernumber in range(1, len(team) + 1) if playernumber not in groups} for teamnumber, team in enumerate(names)} @@ -1344,7 +1352,6 @@ def getTracker(tracker: UUID): for playernumber in range(1, len(team) + 1) if playernumber not in groups} for teamnumber, team in enumerate(names)} - hints = {team: set() for team in range(len(names))} if room.multisave: multisave = restricted_loads(room.multisave) @@ -1354,6 +1361,78 @@ def getTracker(tracker: UUID): for (team, slot), slot_hints in multisave["hints"].items(): hints[team] |= set(slot_hints) + for (team, player), locations_checked in multisave.get("location_checks", {}).items(): + if player in groups: + continue + player_locations = locations[player] + checks_done[team][player]["Total"] = sum(1 for loc in locations_checked if loc in player_locations) + percent_total_checks_done[team][player] = int(checks_done[team][player]["Total"] / seed_checks_in_area[player]["Total"] * 100) if seed_checks_in_area[player]["Total"] else 100 + + activity_timers = {} + now = datetime.datetime.utcnow() + for (team, player), timestamp in multisave.get("client_activity_timers", []): + activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp) + + player_names = {} + for team, names in enumerate(names): + for player, name in enumerate(names, 1): + player_names[(team, player)] = name + long_player_names = player_names.copy() + for (team, player), alias in multisave.get("name_aliases", {}).items(): + player_names[(team, player)] = alias + long_player_names[(team, player)] = f"{alias} ({long_player_names[(team, player)]})" + + video = {} + for (team, player), data in multisave.get("video", []): + video[(team, player)] = data + + enabled_multiworld_trackers = get_enabled_multiworld_trackers(room, "Generic") + + return render_template("multiTracker.html", player_names=player_names, room=room, checks_done=checks_done, + percent_total_checks_done=percent_total_checks_done, checks_in_area=seed_checks_in_area, + activity_timers=activity_timers, video=video, hints=hints, + long_player_names=long_player_names, enabled_multiworld_trackers=enabled_multiworld_trackers) + + +@app.route('/tracker//lttp') +@cache.memoize(timeout=60) # multisave is currently created at most every minute +def get_LttP_multiworld_tracker(tracker: UUID): + room: Room = Room.get(tracker=tracker) + if not room: + abort(404) + locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \ + precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \ + get_static_room_data(room) + + inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1) if + playernumber not in groups} + for teamnumber, team in enumerate(names)} + + checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations} + for playernumber in range(1, len(team) + 1) if playernumber not in groups} + for teamnumber, team in enumerate(names)} + + percent_total_checks_done = {teamnumber: {playernumber: 0 + for playernumber in range(1, len(team) + 1) if playernumber not in groups} + for teamnumber, team in enumerate(names)} + + hints = {team: set() for team in range(len(names))} + if room.multisave: + multisave = restricted_loads(room.multisave) + else: + multisave = {} + if "hints" in multisave: + for (team, slot), slot_hints in multisave["hints"].items(): + hints[team] |= set(slot_hints) + + def attribute_item(team: int, recipient: int, item: int): + nonlocal inventory + target_item = links.get(item, item) + if item in levels: # non-progressive + inventory[team][recipient][target_item] = max(inventory[team][recipient][target_item], levels[item]) + else: + inventory[team][recipient][target_item] += 1 + for (team, player), locations_checked in multisave.get("location_checks", {}).items(): if player in groups: continue @@ -1361,18 +1440,19 @@ def getTracker(tracker: UUID): if precollected_items: precollected = precollected_items[player] for item_id in precollected: - attribute_item(inventory, team, player, item_id) + attribute_item(team, player, item_id) for location in locations_checked: if location not in player_locations or location not in player_location_to_area[player]: continue - item, recipient, flags = player_locations[location] - - if recipient in names: - attribute_item(inventory, team, recipient, item) - checks_done[team][player][player_location_to_area[player][location]] += 1 - checks_done[team][player]["Total"] += 1 - percent_total_checks_done[team][player] = int(checks_done[team][player]["Total"] / seed_checks_in_area[player]["Total"] * 100) if seed_checks_in_area[player]["Total"] else 100 + recipients = groups.get(recipient, [recipient]) + for recipient in recipients: + attribute_item(team, recipient, item) + checks_done[team][player][player_location_to_area[player][location]] += 1 + checks_done[team][player]["Total"] += 1 + percent_total_checks_done[team][player] = int( + checks_done[team][player]["Total"] / seed_checks_in_area[player]["Total"] * 100) if \ + seed_checks_in_area[player]["Total"] else 100 for (team, player), game_state in multisave.get("client_game_state", {}).items(): if player in groups: @@ -1414,14 +1494,19 @@ def getTracker(tracker: UUID): for (team, player), data in multisave.get("video", []): video[(team, player)] = data - return render_template("tracker.html", inventory=inventory, get_item_name_from_id=lookup_any_item_id_to_name, + enabled_multiworld_trackers = get_enabled_multiworld_trackers(room, "A Link to the Past") + + return render_template("lttpMultiTracker.html", inventory=inventory, get_item_name_from_id=lookup_any_item_id_to_name, lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names, tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=alttp_icons, - multi_items=multi_items, checks_done=checks_done, percent_total_checks_done=percent_total_checks_done, - ordered_areas=ordered_areas, checks_in_area=seed_checks_in_area, activity_timers=activity_timers, + multi_items=multi_items, checks_done=checks_done, + percent_total_checks_done=percent_total_checks_done, + ordered_areas=ordered_areas, checks_in_area=seed_checks_in_area, + activity_timers=activity_timers, key_locations=group_key_locations, small_key_ids=small_key_ids, big_key_ids=big_key_ids, video=video, big_key_locations=group_big_key_locations, - hints=hints, long_player_names=long_player_names) + hints=hints, long_player_names=long_player_names, + enabled_multiworld_trackers=enabled_multiworld_trackers) game_specific_trackers: typing.Dict[str, typing.Callable] = { From 5e1aa523732f40880cd66e8590b806b89fdc2470 Mon Sep 17 00:00:00 2001 From: espeon65536 <81029175+espeon65536@users.noreply.github.com> Date: Wed, 8 Mar 2023 21:13:52 -0700 Subject: [PATCH 037/172] Minecraft rewrite (#1493) * Minecraft: rewrite to modern AP standards * Fix gitignore to not try to ignore the entire minecraft world * minecraft: clean up MC-specific tests * minecraft: use pkgutil instead of open * minecraft: ship as apworld * mc: update region to new api * Increase upper limit on advancement and egg shard goals * mc: reduce egg shard count by 5 for structure compasses * Minecraft: add more tests Ensures data loading works; tests beatability with various options at their max setting; new tests for 1.19 advancements * test improvements * mc: typing and imports cleanup * parens * mc: condense filler item code and override get_filler_item_name --- .gitignore | 1 + setup.py | 1 + worlds/minecraft/Constants.py | 26 + worlds/minecraft/ItemPool.py | 52 ++ worlds/minecraft/Items.py | 108 --- worlds/minecraft/Locations.py | 192 ------ worlds/minecraft/Options.py | 20 +- worlds/minecraft/Regions.py | 93 --- worlds/minecraft/Rules.py | 616 +++++++++--------- worlds/minecraft/Structures.py | 57 ++ worlds/minecraft/__init__.py | 149 ++--- worlds/minecraft/data/excluded_locations.json | 40 ++ worlds/minecraft/data/items.json | 128 ++++ worlds/minecraft/data/locations.json | 250 +++++++ worlds/minecraft/data/regions.json | 28 + worlds/minecraft/test/TestAdvancements.py | 134 +++- worlds/minecraft/test/TestDataLoad.py | 60 ++ worlds/minecraft/test/TestEntrances.py | 8 +- worlds/minecraft/test/TestMinecraft.py | 68 -- worlds/minecraft/test/TestOptions.py | 49 ++ worlds/minecraft/test/__init__.py | 33 + 21 files changed, 1254 insertions(+), 859 deletions(-) create mode 100644 worlds/minecraft/Constants.py create mode 100644 worlds/minecraft/ItemPool.py delete mode 100644 worlds/minecraft/Items.py delete mode 100644 worlds/minecraft/Locations.py delete mode 100644 worlds/minecraft/Regions.py create mode 100644 worlds/minecraft/Structures.py create mode 100644 worlds/minecraft/data/excluded_locations.json create mode 100644 worlds/minecraft/data/items.json create mode 100644 worlds/minecraft/data/locations.json create mode 100644 worlds/minecraft/data/regions.json create mode 100644 worlds/minecraft/test/TestDataLoad.py delete mode 100644 worlds/minecraft/test/TestMinecraft.py create mode 100644 worlds/minecraft/test/TestOptions.py diff --git a/.gitignore b/.gitignore index 4b1cf4a0cd..4a9f3402a9 100644 --- a/.gitignore +++ b/.gitignore @@ -169,6 +169,7 @@ cython_debug/ jdk*/ minecraft*/ minecraft_versions.json +!worlds/minecraft/ # pyenv .python-version diff --git a/setup.py b/setup.py index 1d80377f08..7c55a4d256 100644 --- a/setup.py +++ b/setup.py @@ -53,6 +53,7 @@ apworlds: set = { "Super Mario World", "Stardew Valley", "Timespinner", + "Minecraft", } diff --git a/worlds/minecraft/Constants.py b/worlds/minecraft/Constants.py new file mode 100644 index 0000000000..0d1101e802 --- /dev/null +++ b/worlds/minecraft/Constants.py @@ -0,0 +1,26 @@ +import os +import json +import pkgutil + +def load_data_file(*args) -> dict: + fname = os.path.join("data", *args) + return json.loads(pkgutil.get_data(__name__, fname).decode()) + +# For historical reasons, these values are different. +# They remain different to ensure datapackage consistency. +# Do not separate other games' location and item IDs like this. +item_id_offset: int = 45000 +location_id_offset: int = 42000 + +item_info = load_data_file("items.json") +item_name_to_id = {name: item_id_offset + index \ + for index, name in enumerate(item_info["all_items"])} +item_name_to_id["Bee Trap"] = item_id_offset + 100 # historical reasons + +location_info = load_data_file("locations.json") +location_name_to_id = {name: location_id_offset + index \ + for index, name in enumerate(location_info["all_locations"])} + +exclusion_info = load_data_file("excluded_locations.json") + +region_info = load_data_file("regions.json") diff --git a/worlds/minecraft/ItemPool.py b/worlds/minecraft/ItemPool.py new file mode 100644 index 0000000000..78eeffca80 --- /dev/null +++ b/worlds/minecraft/ItemPool.py @@ -0,0 +1,52 @@ +from math import ceil +from typing import List + +from BaseClasses import MultiWorld, Item +from worlds.AutoWorld import World + +from . import Constants + +def get_junk_item_names(rand, k: int) -> str: + junk_weights = Constants.item_info["junk_weights"] + junk = rand.choices( + list(junk_weights.keys()), + weights=list(junk_weights.values()), + k=k) + return junk + +def build_item_pool(mc_world: World) -> List[Item]: + multiworld = mc_world.multiworld + player = mc_world.player + + itempool = [] + total_location_count = len(multiworld.get_unfilled_locations(player)) + + required_pool = Constants.item_info["required_pool"] + junk_weights = Constants.item_info["junk_weights"] + + # Add required progression items + for item_name, num in required_pool.items(): + itempool += [mc_world.create_item(item_name) for _ in range(num)] + + # Add structure compasses + if multiworld.structure_compasses[player]: + compasses = [name for name in mc_world.item_name_to_id if "Structure Compass" in name] + for item_name in compasses: + itempool.append(mc_world.create_item(item_name)) + + # Dragon egg shards + if multiworld.egg_shards_required[player] > 0: + num = multiworld.egg_shards_available[player] + itempool += [mc_world.create_item("Dragon Egg Shard") for _ in range(num)] + + # Bee traps + bee_trap_percentage = multiworld.bee_traps[player] * 0.01 + if bee_trap_percentage > 0: + bee_trap_qty = ceil(bee_trap_percentage * (total_location_count - len(itempool))) + itempool += [mc_world.create_item("Bee Trap") for _ in range(bee_trap_qty)] + + # Fill remaining itempool with randomly generated junk + junk = get_junk_item_names(multiworld.random, total_location_count - len(itempool)) + itempool += [mc_world.create_item(name) for name in junk] + + return itempool diff --git a/worlds/minecraft/Items.py b/worlds/minecraft/Items.py deleted file mode 100644 index 6cf8447c8f..0000000000 --- a/worlds/minecraft/Items.py +++ /dev/null @@ -1,108 +0,0 @@ -from BaseClasses import Item -import typing - - -class ItemData(typing.NamedTuple): - code: typing.Optional[int] - progression: bool - - -class MinecraftItem(Item): - game: str = "Minecraft" - - -item_table = { - "Archery": ItemData(45000, True), - "Progressive Resource Crafting": ItemData(45001, True), - # "Resource Blocks": ItemData(45002, True), - "Brewing": ItemData(45003, True), - "Enchanting": ItemData(45004, True), - "Bucket": ItemData(45005, True), - "Flint and Steel": ItemData(45006, True), - "Bed": ItemData(45007, True), - "Bottles": ItemData(45008, True), - "Shield": ItemData(45009, True), - "Fishing Rod": ItemData(45010, True), - "Campfire": ItemData(45011, True), - "Progressive Weapons": ItemData(45012, True), - "Progressive Tools": ItemData(45013, True), - "Progressive Armor": ItemData(45014, True), - "8 Netherite Scrap": ItemData(45015, True), - "8 Emeralds": ItemData(45016, False), - "4 Emeralds": ItemData(45017, False), - "Channeling Book": ItemData(45018, True), - "Silk Touch Book": ItemData(45019, True), - "Sharpness III Book": ItemData(45020, False), - "Piercing IV Book": ItemData(45021, True), - "Looting III Book": ItemData(45022, False), - "Infinity Book": ItemData(45023, False), - "4 Diamond Ore": ItemData(45024, False), - "16 Iron Ore": ItemData(45025, False), - "500 XP": ItemData(45026, False), - "100 XP": ItemData(45027, False), - "50 XP": ItemData(45028, False), - "3 Ender Pearls": ItemData(45029, True), - "4 Lapis Lazuli": ItemData(45030, False), - "16 Porkchops": ItemData(45031, False), - "8 Gold Ore": ItemData(45032, False), - "Rotten Flesh": ItemData(45033, False), - "Single Arrow": ItemData(45034, False), - "32 Arrows": ItemData(45035, False), - "Saddle": ItemData(45036, True), - "Structure Compass (Village)": ItemData(45037, True), - "Structure Compass (Pillager Outpost)": ItemData(45038, True), - "Structure Compass (Nether Fortress)": ItemData(45039, True), - "Structure Compass (Bastion Remnant)": ItemData(45040, True), - "Structure Compass (End City)": ItemData(45041, True), - "Shulker Box": ItemData(45042, False), - "Dragon Egg Shard": ItemData(45043, True), - "Spyglass": ItemData(45044, True), - "Lead": ItemData(45045, True), - - "Bee Trap": ItemData(45100, False), - "Blaze Rods": ItemData(None, True), - "Defeat Ender Dragon": ItemData(None, True), - "Defeat Wither": ItemData(None, True), -} - -# 33 required items -required_items = { - "Archery": 1, - "Progressive Resource Crafting": 2, - "Brewing": 1, - "Enchanting": 1, - "Bucket": 1, - "Flint and Steel": 1, - "Bed": 1, - "Bottles": 1, - "Shield": 1, - "Fishing Rod": 1, - "Campfire": 1, - "Progressive Weapons": 3, - "Progressive Tools": 3, - "Progressive Armor": 2, - "8 Netherite Scrap": 2, - "Channeling Book": 1, - "Silk Touch Book": 1, - "Sharpness III Book": 1, - "Piercing IV Book": 1, - "Looting III Book": 1, - "Infinity Book": 1, - "3 Ender Pearls": 4, - "Saddle": 1, - "Spyglass": 1, - "Lead": 1, -} - -junk_weights = { - "4 Emeralds": 2, - "4 Diamond Ore": 1, - "16 Iron Ore": 1, - "50 XP": 4, - "16 Porkchops": 2, - "8 Gold Ore": 1, - "Rotten Flesh": 1, - "32 Arrows": 1, -} - -lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code} diff --git a/worlds/minecraft/Locations.py b/worlds/minecraft/Locations.py deleted file mode 100644 index 46398ab11e..0000000000 --- a/worlds/minecraft/Locations.py +++ /dev/null @@ -1,192 +0,0 @@ -from BaseClasses import Location -import typing - - -class AdvData(typing.NamedTuple): - id: typing.Optional[int] - region: str - - -class MinecraftAdvancement(Location): - game: str = "Minecraft" - - def __init__(self, player: int, name: str, address: typing.Optional[int], parent): - super().__init__(player, name, address, parent) - self.event = not address - - -advancement_table = { - "Who is Cutting Onions?": AdvData(42000, 'Overworld'), - "Oh Shiny": AdvData(42001, 'Overworld'), - "Suit Up": AdvData(42002, 'Overworld'), - "Very Very Frightening": AdvData(42003, 'Overworld'), - "Hot Stuff": AdvData(42004, 'Overworld'), - "Free the End": AdvData(42005, 'The End'), - "A Furious Cocktail": AdvData(42006, 'Nether Fortress'), - "Best Friends Forever": AdvData(42007, 'Overworld'), - "Bring Home the Beacon": AdvData(42008, 'Nether Fortress'), - "Not Today, Thank You": AdvData(42009, 'Overworld'), - "Isn't It Iron Pick": AdvData(42010, 'Overworld'), - "Local Brewery": AdvData(42011, 'Nether Fortress'), - "The Next Generation": AdvData(42012, 'The End'), - "Fishy Business": AdvData(42013, 'Overworld'), - "Hot Tourist Destinations": AdvData(42014, 'The Nether'), - "This Boat Has Legs": AdvData(42015, 'The Nether'), - "Sniper Duel": AdvData(42016, 'Overworld'), - "Nether": AdvData(42017, 'The Nether'), - "Great View From Up Here": AdvData(42018, 'End City'), - "How Did We Get Here?": AdvData(42019, 'Nether Fortress'), - "Bullseye": AdvData(42020, 'Overworld'), - "Spooky Scary Skeleton": AdvData(42021, 'Nether Fortress'), - "Two by Two": AdvData(42022, 'The Nether'), - "Stone Age": AdvData(42023, 'Overworld'), - "Two Birds, One Arrow": AdvData(42024, 'Overworld'), - "We Need to Go Deeper": AdvData(42025, 'The Nether'), - "Who's the Pillager Now?": AdvData(42026, 'Pillager Outpost'), - "Getting an Upgrade": AdvData(42027, 'Overworld'), - "Tactical Fishing": AdvData(42028, 'Overworld'), - "Zombie Doctor": AdvData(42029, 'Overworld'), - "The City at the End of the Game": AdvData(42030, 'End City'), - "Ice Bucket Challenge": AdvData(42031, 'Overworld'), - "Remote Getaway": AdvData(42032, 'The End'), - "Into Fire": AdvData(42033, 'Nether Fortress'), - "War Pigs": AdvData(42034, 'Bastion Remnant'), - "Take Aim": AdvData(42035, 'Overworld'), - "Total Beelocation": AdvData(42036, 'Overworld'), - "Arbalistic": AdvData(42037, 'Overworld'), - "The End... Again...": AdvData(42038, 'The End'), - "Acquire Hardware": AdvData(42039, 'Overworld'), - "Not Quite \"Nine\" Lives": AdvData(42040, 'The Nether'), - "Cover Me With Diamonds": AdvData(42041, 'Overworld'), - "Sky's the Limit": AdvData(42042, 'End City'), - "Hired Help": AdvData(42043, 'Overworld'), - "Return to Sender": AdvData(42044, 'The Nether'), - "Sweet Dreams": AdvData(42045, 'Overworld'), - "You Need a Mint": AdvData(42046, 'The End'), - "Adventure": AdvData(42047, 'Overworld'), - "Monsters Hunted": AdvData(42048, 'Overworld'), - "Enchanter": AdvData(42049, 'Overworld'), - "Voluntary Exile": AdvData(42050, 'Pillager Outpost'), - "Eye Spy": AdvData(42051, 'Overworld'), - "The End": AdvData(42052, 'The End'), - "Serious Dedication": AdvData(42053, 'The Nether'), - "Postmortal": AdvData(42054, 'Village'), - "Monster Hunter": AdvData(42055, 'Overworld'), - "Adventuring Time": AdvData(42056, 'Overworld'), - "A Seedy Place": AdvData(42057, 'Overworld'), - "Those Were the Days": AdvData(42058, 'Bastion Remnant'), - "Hero of the Village": AdvData(42059, 'Village'), - "Hidden in the Depths": AdvData(42060, 'The Nether'), - "Beaconator": AdvData(42061, 'Nether Fortress'), - "Withering Heights": AdvData(42062, 'Nether Fortress'), - "A Balanced Diet": AdvData(42063, 'Village'), - "Subspace Bubble": AdvData(42064, 'The Nether'), - "Husbandry": AdvData(42065, 'Overworld'), - "Country Lode, Take Me Home": AdvData(42066, 'The Nether'), - "Bee Our Guest": AdvData(42067, 'Overworld'), - "What a Deal!": AdvData(42068, 'Village'), - "Uneasy Alliance": AdvData(42069, 'The Nether'), - "Diamonds!": AdvData(42070, 'Overworld'), - "A Terrible Fortress": AdvData(42071, 'Nether Fortress'), - "A Throwaway Joke": AdvData(42072, 'Overworld'), - "Minecraft": AdvData(42073, 'Overworld'), - "Sticky Situation": AdvData(42074, 'Overworld'), - "Ol' Betsy": AdvData(42075, 'Overworld'), - "Cover Me in Debris": AdvData(42076, 'The Nether'), - "The End?": AdvData(42077, 'The End'), - "The Parrots and the Bats": AdvData(42078, 'Overworld'), - "A Complete Catalogue": AdvData(42079, 'Village'), - "Getting Wood": AdvData(42080, 'Overworld'), - "Time to Mine!": AdvData(42081, 'Overworld'), - "Hot Topic": AdvData(42082, 'Overworld'), - "Bake Bread": AdvData(42083, 'Overworld'), - "The Lie": AdvData(42084, 'Overworld'), - "On a Rail": AdvData(42085, 'Overworld'), - "Time to Strike!": AdvData(42086, 'Overworld'), - "Cow Tipper": AdvData(42087, 'Overworld'), - "When Pigs Fly": AdvData(42088, 'Overworld'), - "Overkill": AdvData(42089, 'Nether Fortress'), - "Librarian": AdvData(42090, 'Overworld'), - "Overpowered": AdvData(42091, 'Bastion Remnant'), - "Wax On": AdvData(42092, 'Overworld'), - "Wax Off": AdvData(42093, 'Overworld'), - "The Cutest Predator": AdvData(42094, 'Overworld'), - "The Healing Power of Friendship": AdvData(42095, 'Overworld'), - "Is It a Bird?": AdvData(42096, 'Overworld'), - "Is It a Balloon?": AdvData(42097, 'The Nether'), - "Is It a Plane?": AdvData(42098, 'The End'), - "Surge Protector": AdvData(42099, 'Overworld'), - "Light as a Rabbit": AdvData(42100, 'Overworld'), - "Glow and Behold!": AdvData(42101, 'Overworld'), - "Whatever Floats Your Goat!": AdvData(42102, 'Overworld'), - "Caves & Cliffs": AdvData(42103, 'Overworld'), - "Feels like home": AdvData(42104, 'The Nether'), - "Sound of Music": AdvData(42105, 'Overworld'), - "Star Trader": AdvData(42106, 'Village'), - - # 1.19 advancements - "Birthday Song": AdvData(42107, 'Pillager Outpost'), - "Bukkit Bukkit": AdvData(42108, 'Overworld'), - "It Spreads": AdvData(42109, 'Overworld'), - "Sneak 100": AdvData(42110, 'Overworld'), - "When the Squad Hops into Town": AdvData(42111, 'Overworld'), - "With Our Powers Combined!": AdvData(42112, 'The Nether'), - "You've Got a Friend in Me": AdvData(42113, 'Pillager Outpost'), - - "Blaze Spawner": AdvData(None, 'Nether Fortress'), - "Ender Dragon": AdvData(None, 'The End'), - "Wither": AdvData(None, 'Nether Fortress'), -} - -exclusion_table = { - "hard": { - "Very Very Frightening", - "A Furious Cocktail", - "Two by Two", - "Two Birds, One Arrow", - "Arbalistic", - "Monsters Hunted", - "Beaconator", - "A Balanced Diet", - "Uneasy Alliance", - "Cover Me in Debris", - "A Complete Catalogue", - "Surge Protector", - "Sound of Music", - "Star Trader", - "When the Squad Hops into Town", - "With Our Powers Combined!", - }, - "unreasonable": { - "How Did We Get Here?", - "Adventuring Time", - }, -} - -def get_postgame_advancements(required_bosses): - - postgame_advancements = { - "ender_dragon": { - "Free the End", - "The Next Generation", - "The End... Again...", - "You Need a Mint", - "Monsters Hunted", - "Is It a Plane?", - }, - "wither": { - "Withering Heights", - "Bring Home the Beacon", - "Beaconator", - "A Furious Cocktail", - "How Did We Get Here?", - "Monsters Hunted", - } - } - - advancements = set() - if required_bosses in {"ender_dragon", "both"}: - advancements.update(postgame_advancements["ender_dragon"]) - if required_bosses in {"wither", "both"}: - advancements.update(postgame_advancements["wither"]) - return advancements diff --git a/worlds/minecraft/Options.py b/worlds/minecraft/Options.py index 161d44d9b8..084a611e44 100644 --- a/worlds/minecraft/Options.py +++ b/worlds/minecraft/Options.py @@ -6,7 +6,7 @@ class AdvancementGoal(Range): """Number of advancements required to spawn bosses.""" display_name = "Advancement Goal" range_start = 0 - range_end = 95 + range_end = 114 default = 40 @@ -14,7 +14,7 @@ class EggShardsRequired(Range): """Number of dragon egg shards to collect to spawn bosses.""" display_name = "Egg Shards Required" range_start = 0 - range_end = 40 + range_end = 74 default = 0 @@ -22,7 +22,7 @@ class EggShardsAvailable(Range): """Number of dragon egg shards available to collect.""" display_name = "Egg Shards Available" range_start = 0 - range_end = 40 + range_end = 74 default = 0 @@ -35,6 +35,14 @@ class BossGoal(Choice): option_both = 3 default = 1 + @property + def dragon(self): + return self.value % 2 == 1 + + @property + def wither(self): + return self.value > 1 + class ShuffleStructures(DefaultOnToggle): """Enables shuffling of villages, outposts, fortresses, bastions, and end cities.""" @@ -94,14 +102,16 @@ minecraft_options: typing.Dict[str, type(Option)] = { "egg_shards_required": EggShardsRequired, "egg_shards_available": EggShardsAvailable, "required_bosses": BossGoal, + "shuffle_structures": ShuffleStructures, "structure_compasses": StructureCompasses, - "bee_traps": BeeTraps, + "combat_difficulty": CombatDifficulty, "include_hard_advancements": HardAdvancements, "include_unreasonable_advancements": UnreasonableAdvancements, "include_postgame_advancements": PostgameAdvancements, + "bee_traps": BeeTraps, "send_defeated_mobs": SendDefeatedMobs, - "starting_items": StartingItems, "death_link": DeathLink, + "starting_items": StartingItems, } diff --git a/worlds/minecraft/Regions.py b/worlds/minecraft/Regions.py deleted file mode 100644 index d9f3f1b59e..0000000000 --- a/worlds/minecraft/Regions.py +++ /dev/null @@ -1,93 +0,0 @@ - -def link_minecraft_structures(world, player): - - # Link mandatory connections first - for (exit, region) in mandatory_connections: - world.get_entrance(exit, player).connect(world.get_region(region, player)) - - # Get all unpaired exits and all regions without entrances (except the Menu) - # This function is destructive on these lists. - exits = [exit.name for r in world.regions if r.player == player for exit in r.exits if exit.connected_region == None] - structs = [r.name for r in world.regions if r.player == player and r.entrances == [] and r.name != 'Menu'] - exits_spoiler = exits[:] # copy the original order for the spoiler log - try: - assert len(exits) == len(structs) - except AssertionError as e: # this should never happen - raise Exception(f"Could not obtain equal numbers of Minecraft exits and structures for player {player} ({world.player_name[player]})") - - pairs = {} - - def set_pair(exit, struct): - if (exit in exits) and (struct in structs) and (exit not in illegal_connections.get(struct, [])): - pairs[exit] = struct - exits.remove(exit) - structs.remove(struct) - else: - raise Exception(f"Invalid connection: {exit} => {struct} for player {player} ({world.player_name[player]})") - - # Connect plando structures first - if world.plando_connections[player]: - for conn in world.plando_connections[player]: - set_pair(conn.entrance, conn.exit) - - # The algorithm tries to place the most restrictive structures first. This algorithm always works on the - # relatively small set of restrictions here, but does not work on all possible inputs with valid configurations. - if world.shuffle_structures[player]: - structs.sort(reverse=True, key=lambda s: len(illegal_connections.get(s, []))) - for struct in structs[:]: - try: - exit = world.random.choice([e for e in exits if e not in illegal_connections.get(struct, [])]) - except IndexError: - raise Exception(f"No valid structure placements remaining for player {player} ({world.player_name[player]})") - set_pair(exit, struct) - else: # write remaining default connections - for (exit, struct) in default_connections: - if exit in exits: - set_pair(exit, struct) - - # Make sure we actually paired everything; might fail if plando - try: - assert len(exits) == len(structs) == 0 - except AssertionError: - raise Exception(f"Failed to connect all Minecraft structures for player {player} ({world.player_name[player]})") - - for exit in exits_spoiler: - world.get_entrance(exit, player).connect(world.get_region(pairs[exit], player)) - if world.shuffle_structures[player] or world.plando_connections[player]: - world.spoiler.set_entrance(exit, pairs[exit], 'entrance', player) - - - -# (Region name, list of exits) -mc_regions = [ - ('Menu', ['New World']), - ('Overworld', ['Nether Portal', 'End Portal', 'Overworld Structure 1', 'Overworld Structure 2']), - ('The Nether', ['Nether Structure 1', 'Nether Structure 2']), - ('The End', ['The End Structure']), - ('Village', []), - ('Pillager Outpost', []), - ('Nether Fortress', []), - ('Bastion Remnant', []), - ('End City', []) -] - -# (Entrance, region pointed to) -mandatory_connections = [ - ('New World', 'Overworld'), - ('Nether Portal', 'The Nether'), - ('End Portal', 'The End') -] - -default_connections = [ - ('Overworld Structure 1', 'Village'), - ('Overworld Structure 2', 'Pillager Outpost'), - ('Nether Structure 1', 'Nether Fortress'), - ('Nether Structure 2', 'Bastion Remnant'), - ('The End Structure', 'End City') -] - -# Structure: illegal locations -illegal_connections = { - 'Nether Fortress': ['The End Structure'] -} - diff --git a/worlds/minecraft/Rules.py b/worlds/minecraft/Rules.py index 2ec9523762..dae4241b99 100644 --- a/worlds/minecraft/Rules.py +++ b/worlds/minecraft/Rules.py @@ -1,317 +1,313 @@ -from ..generic.Rules import set_rule, add_rule -from .Locations import exclusion_table, get_postgame_advancements -from BaseClasses import MultiWorld -from ..AutoWorld import LogicMixin +import typing +from collections.abc import Callable + +from BaseClasses import CollectionState +from worlds.generic.Rules import exclusion_rules +from worlds.AutoWorld import World + +from . import Constants + +# Helper functions +# moved from logicmixin + +def has_iron_ingots(state: CollectionState, player: int) -> bool: + return state.has('Progressive Tools', player) and state.has('Progressive Resource Crafting', player) + +def has_copper_ingots(state: CollectionState, player: int) -> bool: + return state.has('Progressive Tools', player) and state.has('Progressive Resource Crafting', player) + +def has_gold_ingots(state: CollectionState, player: int) -> bool: + return state.has('Progressive Resource Crafting', player) and (state.has('Progressive Tools', player, 2) or state.can_reach('The Nether', 'Region', player)) + +def has_diamond_pickaxe(state: CollectionState, player: int) -> bool: + return state.has('Progressive Tools', player, 3) and has_iron_ingots(state, player) + +def craft_crossbow(state: CollectionState, player: int) -> bool: + return state.has('Archery', player) and has_iron_ingots(state, player) + +def has_bottle(state: CollectionState, player: int) -> bool: + return state.has('Bottles', player) and state.has('Progressive Resource Crafting', player) + +def has_spyglass(state: CollectionState, player: int) -> bool: + return has_copper_ingots(state, player) and state.has('Spyglass', player) and can_adventure(state, player) + +def can_enchant(state: CollectionState, player: int) -> bool: + return state.has('Enchanting', player) and has_diamond_pickaxe(state, player) # mine obsidian and lapis + +def can_use_anvil(state: CollectionState, player: int) -> bool: + return state.has('Enchanting', player) and state.has('Progressive Resource Crafting', player, 2) and has_iron_ingots(state, player) + +def fortress_loot(state: CollectionState, player: int) -> bool: # saddles, blaze rods, wither skulls + return state.can_reach('Nether Fortress', 'Region', player) and basic_combat(state, player) + +def can_brew_potions(state: CollectionState, player: int) -> bool: + return state.has('Blaze Rods', player) and state.has('Brewing', player) and has_bottle(state, player) + +def can_piglin_trade(state: CollectionState, player: int) -> bool: + return has_gold_ingots(state, player) and ( + state.can_reach('The Nether', 'Region', player) or + state.can_reach('Bastion Remnant', 'Region', player)) + +def overworld_villager(state: CollectionState, player: int) -> bool: + village_region = state.multiworld.get_region('Village', player).entrances[0].parent_region.name + if village_region == 'The Nether': # 2 options: cure zombie villager or build portal in village + return (state.can_reach('Zombie Doctor', 'Location', player) or + (has_diamond_pickaxe(state, player) and state.can_reach('Village', 'Region', player))) + elif village_region == 'The End': + return state.can_reach('Zombie Doctor', 'Location', player) + return state.can_reach('Village', 'Region', player) + +def enter_stronghold(state: CollectionState, player: int) -> bool: + return state.has('Blaze Rods', player) and state.has('Brewing', player) and state.has('3 Ender Pearls', player) + +# Difficulty-dependent functions +def combat_difficulty(state: CollectionState, player: int) -> bool: + return state.multiworld.combat_difficulty[player].current_key + +def can_adventure(state: CollectionState, player: int) -> bool: + death_link_check = not state.multiworld.death_link[player] or state.has('Bed', player) + if combat_difficulty(state, player) == 'easy': + return state.has('Progressive Weapons', player, 2) and has_iron_ingots(state, player) and death_link_check + elif combat_difficulty(state, player) == 'hard': + return True + return (state.has('Progressive Weapons', player) and death_link_check and + (state.has('Progressive Resource Crafting', player) or state.has('Campfire', player))) + +def basic_combat(state: CollectionState, player: int) -> bool: + if combat_difficulty(state, player) == 'easy': + return state.has('Progressive Weapons', player, 2) and state.has('Progressive Armor', player) and \ + state.has('Shield', player) and has_iron_ingots(state, player) + elif combat_difficulty(state, player) == 'hard': + return True + return state.has('Progressive Weapons', player) and (state.has('Progressive Armor', player) or state.has('Shield', player)) and has_iron_ingots(state, player) + +def complete_raid(state: CollectionState, player: int) -> bool: + reach_regions = state.can_reach('Village', 'Region', player) and state.can_reach('Pillager Outpost', 'Region', player) + if combat_difficulty(state, player) == 'easy': + return reach_regions and \ + state.has('Progressive Weapons', player, 3) and state.has('Progressive Armor', player, 2) and \ + state.has('Shield', player) and state.has('Archery', player) and \ + state.has('Progressive Tools', player, 2) and has_iron_ingots(state, player) + elif combat_difficulty(state, player) == 'hard': # might be too hard? + return reach_regions and state.has('Progressive Weapons', player, 2) and has_iron_ingots(state, player) and \ + (state.has('Progressive Armor', player) or state.has('Shield', player)) + return reach_regions and state.has('Progressive Weapons', player, 2) and has_iron_ingots(state, player) and \ + state.has('Progressive Armor', player) and state.has('Shield', player) + +def can_kill_wither(state: CollectionState, player: int) -> bool: + normal_kill = state.has("Progressive Weapons", player, 3) and state.has("Progressive Armor", player, 2) and can_brew_potions(state, player) and can_enchant(state, player) + if combat_difficulty(state, player) == 'easy': + return fortress_loot(state, player) and normal_kill and state.has('Archery', player) + elif combat_difficulty(state, player) == 'hard': # cheese kill using bedrock ceilings + return fortress_loot(state, player) and (normal_kill or state.can_reach('The Nether', 'Region', player) or state.can_reach('The End', 'Region', player)) + return fortress_loot(state, player) and normal_kill + +def can_respawn_ender_dragon(state: CollectionState, player: int) -> bool: + return state.can_reach('The Nether', 'Region', player) and state.can_reach('The End', 'Region', player) and \ + state.has('Progressive Resource Crafting', player) # smelt sand into glass + +def can_kill_ender_dragon(state: CollectionState, player: int) -> bool: + if combat_difficulty(state, player) == 'easy': + return state.has("Progressive Weapons", player, 3) and state.has("Progressive Armor", player, 2) and \ + state.has('Archery', player) and can_brew_potions(state, player) and can_enchant(state, player) + if combat_difficulty(state, player) == 'hard': + return (state.has('Progressive Weapons', player, 2) and state.has('Progressive Armor', player)) or \ + (state.has('Progressive Weapons', player, 1) and state.has('Bed', player)) + return state.has('Progressive Weapons', player, 2) and state.has('Progressive Armor', player) and state.has('Archery', player) + +def has_structure_compass(state: CollectionState, entrance_name: str, player: int) -> bool: + if not state.multiworld.structure_compasses[player]: + return True + return state.has(f"Structure Compass ({state.multiworld.get_entrance(entrance_name, player).connected_region.name})", player) -class MinecraftLogic(LogicMixin): +def get_rules_lookup(player: int): + rules_lookup: typing.Dict[str, typing.List[Callable[[CollectionState], bool]]] = { + "entrances": { + "Nether Portal": lambda state: (state.has('Flint and Steel', player) and + (state.has('Bucket', player) or state.has('Progressive Tools', player, 3)) and + has_iron_ingots(state, player)), + "End Portal": lambda state: enter_stronghold(state, player) and state.has('3 Ender Pearls', player, 4), + "Overworld Structure 1": lambda state: (can_adventure(state, player) and has_structure_compass(state, "Overworld Structure 1", player)), + "Overworld Structure 2": lambda state: (can_adventure(state, player) and has_structure_compass(state, "Overworld Structure 2", player)), + "Nether Structure 1": lambda state: (can_adventure(state, player) and has_structure_compass(state, "Nether Structure 1", player)), + "Nether Structure 2": lambda state: (can_adventure(state, player) and has_structure_compass(state, "Nether Structure 2", player)), + "The End Structure": lambda state: (can_adventure(state, player) and has_structure_compass(state, "The End Structure", player)), + }, + "locations": { + "Ender Dragon": lambda state: can_respawn_ender_dragon(state, player) and can_kill_ender_dragon(state, player), + "Wither": lambda state: can_kill_wither(state, player), + "Blaze Rods": lambda state: fortress_loot(state, player), - def _mc_has_iron_ingots(self, player: int): - return self.has('Progressive Tools', player) and self.has('Progressive Resource Crafting', player) - - def _mc_has_copper_ingots(self, player: int): - return self.has('Progressive Tools', player) and self.has('Progressive Resource Crafting', player) - - def _mc_has_gold_ingots(self, player: int): - return self.has('Progressive Resource Crafting', player) and (self.has('Progressive Tools', player, 2) or self.can_reach('The Nether', 'Region', player)) - - def _mc_has_diamond_pickaxe(self, player: int): - return self.has('Progressive Tools', player, 3) and self._mc_has_iron_ingots(player) - - def _mc_craft_crossbow(self, player: int): - return self.has('Archery', player) and self._mc_has_iron_ingots(player) - - def _mc_has_bottle(self, player: int): - return self.has('Bottles', player) and self.has('Progressive Resource Crafting', player) - - def _mc_has_spyglass(self, player: int): - return self._mc_has_copper_ingots(player) and self.has('Spyglass', player) and self._mc_can_adventure(player) - - def _mc_can_enchant(self, player: int): - return self.has('Enchanting', player) and self._mc_has_diamond_pickaxe(player) # mine obsidian and lapis - - def _mc_can_use_anvil(self, player: int): - return self.has('Enchanting', player) and self.has('Progressive Resource Crafting', player, 2) and self._mc_has_iron_ingots(player) - - def _mc_fortress_loot(self, player: int): # saddles, blaze rods, wither skulls - return self.can_reach('Nether Fortress', 'Region', player) and self._mc_basic_combat(player) - - def _mc_can_brew_potions(self, player: int): - return self.has('Blaze Rods', player) and self.has('Brewing', player) and self._mc_has_bottle(player) - - def _mc_can_piglin_trade(self, player: int): - return self._mc_has_gold_ingots(player) and ( - self.can_reach('The Nether', 'Region', player) or - self.can_reach('Bastion Remnant', 'Region', player)) - - def _mc_overworld_villager(self, player: int): - village_region = self.multiworld.get_region('Village', player).entrances[0].parent_region.name - if village_region == 'The Nether': # 2 options: cure zombie villager or build portal in village - return (self.can_reach('Zombie Doctor', 'Location', player) or - (self._mc_has_diamond_pickaxe(player) and self.can_reach('Village', 'Region', player))) - elif village_region == 'The End': - return self.can_reach('Zombie Doctor', 'Location', player) - return self.can_reach('Village', 'Region', player) - - def _mc_enter_stronghold(self, player: int): - return self.has('Blaze Rods', player) and self.has('Brewing', player) and self.has('3 Ender Pearls', player) - - # Difficulty-dependent functions - def _mc_combat_difficulty(self, player: int): - return self.multiworld.combat_difficulty[player].current_key - - def _mc_can_adventure(self, player: int): - death_link_check = not self.multiworld.death_link[player] or self.has('Bed', player) - if self._mc_combat_difficulty(player) == 'easy': - return self.has('Progressive Weapons', player, 2) and self._mc_has_iron_ingots(player) and death_link_check - elif self._mc_combat_difficulty(player) == 'hard': - return True - return (self.has('Progressive Weapons', player) and death_link_check and - (self.has('Progressive Resource Crafting', player) or self.has('Campfire', player))) - - def _mc_basic_combat(self, player: int): - if self._mc_combat_difficulty(player) == 'easy': - return self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player) and \ - self.has('Shield', player) and self._mc_has_iron_ingots(player) - elif self._mc_combat_difficulty(player) == 'hard': - return True - return self.has('Progressive Weapons', player) and (self.has('Progressive Armor', player) or self.has('Shield', player)) and self._mc_has_iron_ingots(player) - - def _mc_complete_raid(self, player: int): - reach_regions = self.can_reach('Village', 'Region', player) and self.can_reach('Pillager Outpost', 'Region', player) - if self._mc_combat_difficulty(player) == 'easy': - return reach_regions and \ - self.has('Progressive Weapons', player, 3) and self.has('Progressive Armor', player, 2) and \ - self.has('Shield', player) and self.has('Archery', player) and \ - self.has('Progressive Tools', player, 2) and self._mc_has_iron_ingots(player) - elif self._mc_combat_difficulty(player) == 'hard': # might be too hard? - return reach_regions and self.has('Progressive Weapons', player, 2) and self._mc_has_iron_ingots(player) and \ - (self.has('Progressive Armor', player) or self.has('Shield', player)) - return reach_regions and self.has('Progressive Weapons', player, 2) and self._mc_has_iron_ingots(player) and \ - self.has('Progressive Armor', player) and self.has('Shield', player) - - def _mc_can_kill_wither(self, player: int): - normal_kill = self.has("Progressive Weapons", player, 3) and self.has("Progressive Armor", player, 2) and self._mc_can_brew_potions(player) and self._mc_can_enchant(player) - if self._mc_combat_difficulty(player) == 'easy': - return self._mc_fortress_loot(player) and normal_kill and self.has('Archery', player) - elif self._mc_combat_difficulty(player) == 'hard': # cheese kill using bedrock ceilings - return self._mc_fortress_loot(player) and (normal_kill or self.can_reach('The Nether', 'Region', player) or self.can_reach('The End', 'Region', player)) - return self._mc_fortress_loot(player) and normal_kill - - def _mc_can_respawn_ender_dragon(self, player: int): - return self.can_reach('The Nether', 'Region', player) and self.can_reach('The End', 'Region', player) and \ - self.has('Progressive Resource Crafting', player) # smelt sand into glass - - def _mc_can_kill_ender_dragon(self, player: int): - if self._mc_combat_difficulty(player) == 'easy': - return self.has("Progressive Weapons", player, 3) and self.has("Progressive Armor", player, 2) and \ - self.has('Archery', player) and self._mc_can_brew_potions(player) and self._mc_can_enchant(player) - if self._mc_combat_difficulty(player) == 'hard': - return (self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player)) or \ - (self.has('Progressive Weapons', player, 1) and self.has('Bed', player)) - return self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player) and self.has('Archery', player) - - def _mc_has_structure_compass(self, entrance_name: str, player: int): - if not self.multiworld.structure_compasses[player]: - return True - return self.has(f"Structure Compass ({self.multiworld.get_entrance(entrance_name, player).connected_region.name})", player) - -# Sets rules on entrances and advancements that are always applied -def set_advancement_rules(world: MultiWorld, player: int): - - # Retrieves the appropriate structure compass for the given entrance - def get_struct_compass(entrance_name): - struct = world.get_entrance(entrance_name, player).connected_region.name - return f"Structure Compass ({struct})" - - set_rule(world.get_entrance("Nether Portal", player), lambda state: state.has('Flint and Steel', player) and - (state.has('Bucket', player) or state.has('Progressive Tools', player, 3)) and - state._mc_has_iron_ingots(player)) - set_rule(world.get_entrance("End Portal", player), lambda state: state._mc_enter_stronghold(player) and state.has('3 Ender Pearls', player, 4)) - set_rule(world.get_entrance("Overworld Structure 1", player), lambda state: state._mc_can_adventure(player) and state._mc_has_structure_compass("Overworld Structure 1", player)) - set_rule(world.get_entrance("Overworld Structure 2", player), lambda state: state._mc_can_adventure(player) and state._mc_has_structure_compass("Overworld Structure 2", player)) - set_rule(world.get_entrance("Nether Structure 1", player), lambda state: state._mc_can_adventure(player) and state._mc_has_structure_compass("Nether Structure 1", player)) - set_rule(world.get_entrance("Nether Structure 2", player), lambda state: state._mc_can_adventure(player) and state._mc_has_structure_compass("Nether Structure 2", player)) - set_rule(world.get_entrance("The End Structure", player), lambda state: state._mc_can_adventure(player) and state._mc_has_structure_compass("The End Structure", player)) - - set_rule(world.get_location("Ender Dragon", player), lambda state: state._mc_can_kill_ender_dragon(player)) - set_rule(world.get_location("Wither", player), lambda state: state._mc_can_kill_wither(player)) - set_rule(world.get_location("Blaze Spawner", player), lambda state: state._mc_fortress_loot(player)) - - set_rule(world.get_location("Who is Cutting Onions?", player), lambda state: state._mc_can_piglin_trade(player)) - set_rule(world.get_location("Oh Shiny", player), lambda state: state._mc_can_piglin_trade(player)) - set_rule(world.get_location("Suit Up", player), lambda state: state.has("Progressive Armor", player) and state._mc_has_iron_ingots(player)) - set_rule(world.get_location("Very Very Frightening", player), lambda state: state.has("Channeling Book", player) and - state._mc_can_use_anvil(player) and state._mc_can_enchant(player) and state._mc_overworld_villager(player)) - set_rule(world.get_location("Hot Stuff", player), lambda state: state.has("Bucket", player) and state._mc_has_iron_ingots(player)) - set_rule(world.get_location("Free the End", player), lambda state: state._mc_can_respawn_ender_dragon(player) and state._mc_can_kill_ender_dragon(player)) - set_rule(world.get_location("A Furious Cocktail", player), lambda state: state._mc_can_brew_potions(player) and - state.has("Fishing Rod", player) and # Water Breathing - state.can_reach('The Nether', 'Region', player) and # Regeneration, Fire Resistance, gold nuggets - state.can_reach('Village', 'Region', player) and # Night Vision, Invisibility - state.can_reach('Bring Home the Beacon', 'Location', player)) # Resistance - # set_rule(world.get_location("Best Friends Forever", player), lambda state: True) - set_rule(world.get_location("Bring Home the Beacon", player), lambda state: state._mc_can_kill_wither(player) and - state._mc_has_diamond_pickaxe(player) and state.has("Progressive Resource Crafting", player, 2)) - set_rule(world.get_location("Not Today, Thank You", player), lambda state: state.has("Shield", player) and state._mc_has_iron_ingots(player)) - set_rule(world.get_location("Isn't It Iron Pick", player), lambda state: state.has("Progressive Tools", player, 2) and state._mc_has_iron_ingots(player)) - set_rule(world.get_location("Local Brewery", player), lambda state: state._mc_can_brew_potions(player)) - set_rule(world.get_location("The Next Generation", player), lambda state: state._mc_can_kill_ender_dragon(player)) - set_rule(world.get_location("Fishy Business", player), lambda state: state.has("Fishing Rod", player)) - # set_rule(world.get_location("Hot Tourist Destinations", player), lambda state: True) - set_rule(world.get_location("This Boat Has Legs", player), lambda state: (state._mc_fortress_loot(player) or state._mc_complete_raid(player)) and - state.has("Saddle", player) and state.has("Fishing Rod", player)) - set_rule(world.get_location("Sniper Duel", player), lambda state: state.has("Archery", player)) - # set_rule(world.get_location("Nether", player), lambda state: True) - set_rule(world.get_location("Great View From Up Here", player), lambda state: state._mc_basic_combat(player)) - set_rule(world.get_location("How Did We Get Here?", player), lambda state: state._mc_can_brew_potions(player) and - state._mc_has_gold_ingots(player) and # Absorption - state.can_reach('End City', 'Region', player) and # Levitation - state.can_reach('The Nether', 'Region', player) and # potion ingredients - state.has("Fishing Rod", player) and state.has("Archery",player) and # Pufferfish, Nautilus Shells; spectral arrows - state.can_reach("Bring Home the Beacon", "Location", player) and # Haste - state.can_reach("Hero of the Village", "Location", player)) # Bad Omen, Hero of the Village - set_rule(world.get_location("Bullseye", player), lambda state: state.has("Archery", player) and state.has("Progressive Tools", player, 2) and state._mc_has_iron_ingots(player)) - set_rule(world.get_location("Spooky Scary Skeleton", player), lambda state: state._mc_basic_combat(player)) - set_rule(world.get_location("Two by Two", player), lambda state: state._mc_has_iron_ingots(player) and state.has("Bucket", player) and state._mc_can_adventure(player)) # shears > seagrass > turtles; buckets of tropical fish > axolotls; nether > striders; gold carrots > horses skips ingots - # set_rule(world.get_location("Stone Age", player), lambda state: True) - set_rule(world.get_location("Two Birds, One Arrow", player), lambda state: state._mc_craft_crossbow(player) and state._mc_can_enchant(player)) - # set_rule(world.get_location("We Need to Go Deeper", player), lambda state: True) - set_rule(world.get_location("Who's the Pillager Now?", player), lambda state: state._mc_craft_crossbow(player)) - set_rule(world.get_location("Getting an Upgrade", player), lambda state: state.has("Progressive Tools", player)) - set_rule(world.get_location("Tactical Fishing", player), lambda state: state.has("Bucket", player) and state._mc_has_iron_ingots(player)) - set_rule(world.get_location("Zombie Doctor", player), lambda state: state._mc_can_brew_potions(player) and state._mc_has_gold_ingots(player)) - # set_rule(world.get_location("The City at the End of the Game", player), lambda state: True) - set_rule(world.get_location("Ice Bucket Challenge", player), lambda state: state._mc_has_diamond_pickaxe(player)) - # set_rule(world.get_location("Remote Getaway", player), lambda state: True) - set_rule(world.get_location("Into Fire", player), lambda state: state._mc_basic_combat(player)) - set_rule(world.get_location("War Pigs", player), lambda state: state._mc_basic_combat(player)) - set_rule(world.get_location("Take Aim", player), lambda state: state.has("Archery", player)) - set_rule(world.get_location("Total Beelocation", player), lambda state: state.has("Silk Touch Book", player) and state._mc_can_use_anvil(player) and state._mc_can_enchant(player)) - set_rule(world.get_location("Arbalistic", player), lambda state: state._mc_craft_crossbow(player) and state.has("Piercing IV Book", player) and - state._mc_can_use_anvil(player) and state._mc_can_enchant(player)) - set_rule(world.get_location("The End... Again...", player), lambda state: state._mc_can_respawn_ender_dragon(player) and state._mc_can_kill_ender_dragon(player)) - set_rule(world.get_location("Acquire Hardware", player), lambda state: state._mc_has_iron_ingots(player)) - set_rule(world.get_location("Not Quite \"Nine\" Lives", player), lambda state: state._mc_can_piglin_trade(player) and state.has("Progressive Resource Crafting", player, 2)) - set_rule(world.get_location("Cover Me With Diamonds", player), lambda state: state.has("Progressive Armor", player, 2) and state.can_reach("Diamonds!", "Location", player)) - set_rule(world.get_location("Sky's the Limit", player), lambda state: state._mc_basic_combat(player)) - set_rule(world.get_location("Hired Help", player), lambda state: state.has("Progressive Resource Crafting", player, 2) and state._mc_has_iron_ingots(player)) - # set_rule(world.get_location("Return to Sender", player), lambda state: True) - set_rule(world.get_location("Sweet Dreams", player), lambda state: state.has("Bed", player) or state.can_reach('Village', 'Region', player)) - set_rule(world.get_location("You Need a Mint", player), lambda state: state._mc_can_respawn_ender_dragon(player) and state._mc_has_bottle(player)) - # set_rule(world.get_location("Adventure", player), lambda state: True) - set_rule(world.get_location("Monsters Hunted", player), lambda state: state._mc_can_respawn_ender_dragon(player) and state._mc_can_kill_ender_dragon(player) and - state._mc_can_kill_wither(player) and state.has("Fishing Rod", player)) # pufferfish for Water Breathing - set_rule(world.get_location("Enchanter", player), lambda state: state._mc_can_enchant(player)) - set_rule(world.get_location("Voluntary Exile", player), lambda state: state._mc_basic_combat(player)) - set_rule(world.get_location("Eye Spy", player), lambda state: state._mc_enter_stronghold(player)) - # set_rule(world.get_location("The End", player), lambda state: True) - set_rule(world.get_location("Serious Dedication", player), lambda state: state.can_reach("Hidden in the Depths", "Location", player) and state._mc_has_gold_ingots(player)) - set_rule(world.get_location("Postmortal", player), lambda state: state._mc_complete_raid(player)) - # set_rule(world.get_location("Monster Hunter", player), lambda state: True) - set_rule(world.get_location("Adventuring Time", player), lambda state: state._mc_can_adventure(player)) - # set_rule(world.get_location("A Seedy Place", player), lambda state: True) - # set_rule(world.get_location("Those Were the Days", player), lambda state: True) - set_rule(world.get_location("Hero of the Village", player), lambda state: state._mc_complete_raid(player)) - set_rule(world.get_location("Hidden in the Depths", player), lambda state: state._mc_can_brew_potions(player) and state.has("Bed", player) and state._mc_has_diamond_pickaxe(player)) # bed mining :) - set_rule(world.get_location("Beaconator", player), lambda state: state._mc_can_kill_wither(player) and state._mc_has_diamond_pickaxe(player) and - state.has("Progressive Resource Crafting", player, 2)) - set_rule(world.get_location("Withering Heights", player), lambda state: state._mc_can_kill_wither(player)) - set_rule(world.get_location("A Balanced Diet", player), lambda state: state._mc_has_bottle(player) and state._mc_has_gold_ingots(player) and # honey bottle; gapple - state.has("Progressive Resource Crafting", player, 2) and state.can_reach('The End', 'Region', player)) # notch apple, chorus fruit - set_rule(world.get_location("Subspace Bubble", player), lambda state: state._mc_has_diamond_pickaxe(player)) - # set_rule(world.get_location("Husbandry", player), lambda state: True) - set_rule(world.get_location("Country Lode, Take Me Home", player), lambda state: state.can_reach("Hidden in the Depths", "Location", player) and state._mc_has_gold_ingots(player)) - set_rule(world.get_location("Bee Our Guest", player), lambda state: state.has("Campfire", player) and state._mc_has_bottle(player)) - # set_rule(world.get_location("What a Deal!", player), lambda state: True) - set_rule(world.get_location("Uneasy Alliance", player), lambda state: state._mc_has_diamond_pickaxe(player) and state.has('Fishing Rod', player)) - set_rule(world.get_location("Diamonds!", player), lambda state: state.has("Progressive Tools", player, 2) and state._mc_has_iron_ingots(player)) - # set_rule(world.get_location("A Terrible Fortress", player), lambda state: True) # since you don't have to fight anything - set_rule(world.get_location("A Throwaway Joke", player), lambda state: state._mc_can_adventure(player)) # kill drowned - # set_rule(world.get_location("Minecraft", player), lambda state: True) - set_rule(world.get_location("Sticky Situation", player), lambda state: state.has("Campfire", player) and state._mc_has_bottle(player)) - set_rule(world.get_location("Ol' Betsy", player), lambda state: state._mc_craft_crossbow(player)) - set_rule(world.get_location("Cover Me in Debris", player), lambda state: state.has("Progressive Armor", player, 2) and - state.has("8 Netherite Scrap", player, 2) and state.has("Progressive Resource Crafting", player) and - state.can_reach("Diamonds!", "Location", player) and state.can_reach("Hidden in the Depths", "Location", player)) - # set_rule(world.get_location("The End?", player), lambda state: True) - # set_rule(world.get_location("The Parrots and the Bats", player), lambda state: True) - # set_rule(world.get_location("A Complete Catalogue", player), lambda state: True) # kill fish for raw - # set_rule(world.get_location("Getting Wood", player), lambda state: True) - # set_rule(world.get_location("Time to Mine!", player), lambda state: True) - set_rule(world.get_location("Hot Topic", player), lambda state: state.has("Progressive Resource Crafting", player)) - # set_rule(world.get_location("Bake Bread", player), lambda state: True) - set_rule(world.get_location("The Lie", player), lambda state: state._mc_has_iron_ingots(player) and state.has("Bucket", player)) - set_rule(world.get_location("On a Rail", player), lambda state: state._mc_has_iron_ingots(player) and state.has('Progressive Tools', player, 2)) # powered rails - # set_rule(world.get_location("Time to Strike!", player), lambda state: True) - # set_rule(world.get_location("Cow Tipper", player), lambda state: True) - set_rule(world.get_location("When Pigs Fly", player), lambda state: (state._mc_fortress_loot(player) or state._mc_complete_raid(player)) and - state.has("Saddle", player) and state.has("Fishing Rod", player) and state._mc_can_adventure(player)) - set_rule(world.get_location("Overkill", player), lambda state: state._mc_can_brew_potions(player) and - (state.has("Progressive Weapons", player) or state.can_reach('The Nether', 'Region', player))) # strength 1 + stone axe crit OR strength 2 + wood axe crit - set_rule(world.get_location("Librarian", player), lambda state: state.has("Enchanting", player)) - set_rule(world.get_location("Overpowered", player), lambda state: state._mc_has_iron_ingots(player) and - state.has('Progressive Tools', player, 2) and state._mc_basic_combat(player)) # mine gold blocks w/ iron pick - set_rule(world.get_location("Wax On", player), lambda state: state._mc_has_copper_ingots(player) and state.has('Campfire', player) and - state.has('Progressive Resource Crafting', player, 2)) - set_rule(world.get_location("Wax Off", player), lambda state: state._mc_has_copper_ingots(player) and state.has('Campfire', player) and - state.has('Progressive Resource Crafting', player, 2)) - set_rule(world.get_location("The Cutest Predator", player), lambda state: state._mc_has_iron_ingots(player) and state.has('Bucket', player)) - set_rule(world.get_location("The Healing Power of Friendship", player), lambda state: state._mc_has_iron_ingots(player) and state.has('Bucket', player)) - set_rule(world.get_location("Is It a Bird?", player), lambda state: state._mc_has_spyglass(player) and state._mc_can_adventure(player)) - set_rule(world.get_location("Is It a Balloon?", player), lambda state: state._mc_has_spyglass(player)) - set_rule(world.get_location("Is It a Plane?", player), lambda state: state._mc_has_spyglass(player) and state._mc_can_respawn_ender_dragon(player)) - set_rule(world.get_location("Surge Protector", player), lambda state: state.has("Channeling Book", player) and - state._mc_can_use_anvil(player) and state._mc_can_enchant(player) and state._mc_overworld_villager(player)) - set_rule(world.get_location("Light as a Rabbit", player), lambda state: state._mc_can_adventure(player) and state._mc_has_iron_ingots(player) and state.has('Bucket', player)) - set_rule(world.get_location("Glow and Behold!", player), lambda state: state._mc_can_adventure(player)) - set_rule(world.get_location("Whatever Floats Your Goat!", player), lambda state: state._mc_can_adventure(player)) - set_rule(world.get_location("Caves & Cliffs", player), lambda state: state._mc_has_iron_ingots(player) and state.has('Bucket', player) and state.has('Progressive Tools', player, 2)) - set_rule(world.get_location("Feels like home", player), lambda state: state._mc_has_iron_ingots(player) and state.has('Bucket', player) and state.has('Fishing Rod', player) and - (state._mc_fortress_loot(player) or state._mc_complete_raid(player)) and state.has("Saddle", player)) - set_rule(world.get_location("Sound of Music", player), lambda state: state.can_reach("Diamonds!", "Location", player) and state._mc_basic_combat(player)) - set_rule(world.get_location("Star Trader", player), lambda state: state._mc_has_iron_ingots(player) and state.has('Bucket', player) and - (state.can_reach("The Nether", 'Region', player) or state.can_reach("Nether Fortress", 'Region', player) or state._mc_can_piglin_trade(player)) and # soul sand for water elevator - state._mc_overworld_villager(player)) - - # 1.19 advancements - - # can make a cake, and a noteblock, and can reach a pillager outposts for allays - set_rule(world.get_location("Birthday Song", player), lambda state: state.can_reach("The Lie", "Location", player) and state.has("Progressive Tools", player, 2) and state._mc_has_iron_ingots(player)) - # can get to outposts. - # set_rule(world.get_location("You've Got a Friend in Me", player), lambda state: True) - # craft bucket and adventure to find frog spawning biome - set_rule(world.get_location("Bukkit Bukkit", player), lambda state: state.has("Bucket", player) and state._mc_has_iron_ingots(player) and state._mc_can_adventure(player)) - # I don't like this one its way to easy to get. just a pain to find. - set_rule(world.get_location("It Spreads", player), lambda state: state._mc_can_adventure(player) and state._mc_has_iron_ingots(player) and state.has("Progressive Tools", player, 2)) - # literally just a duplicate of It spreads. - set_rule(world.get_location("Sneak 100", player), lambda state: state._mc_can_adventure(player) and state._mc_has_iron_ingots(player) and state.has("Progressive Tools", player, 2)) - set_rule(world.get_location("When the Squad Hops into Town", player), lambda state: state._mc_can_adventure(player) and state.has("Lead", player)) - # lead frogs to the nether and a basalt delta's biomes to find magma cubes. - set_rule(world.get_location("With Our Powers Combined!", player), lambda state: state._mc_can_adventure(player) and state.has("Lead", player)) + "Who is Cutting Onions?": lambda state: can_piglin_trade(state, player), + "Oh Shiny": lambda state: can_piglin_trade(state, player), + "Suit Up": lambda state: state.has("Progressive Armor", player) and has_iron_ingots(state, player), + "Very Very Frightening": lambda state: (state.has("Channeling Book", player) and + can_use_anvil(state, player) and can_enchant(state, player) and overworld_villager(state, player)), + "Hot Stuff": lambda state: state.has("Bucket", player) and has_iron_ingots(state, player), + "Free the End": lambda state: can_respawn_ender_dragon(state, player) and can_kill_ender_dragon(state, player), + "A Furious Cocktail": lambda state: (can_brew_potions(state, player) and + state.has("Fishing Rod", player) and # Water Breathing + state.can_reach("The Nether", "Region", player) and # Regeneration, Fire Resistance, gold nuggets + state.can_reach("Village", "Region", player) and # Night Vision, Invisibility + state.can_reach("Bring Home the Beacon", "Location", player)), # Resistance + "Bring Home the Beacon": lambda state: (can_kill_wither(state, player) and + has_diamond_pickaxe(state, player) and state.has("Progressive Resource Crafting", player, 2)), + "Not Today, Thank You": lambda state: state.has("Shield", player) and has_iron_ingots(state, player), + "Isn't It Iron Pick": lambda state: state.has("Progressive Tools", player, 2) and has_iron_ingots(state, player), + "Local Brewery": lambda state: can_brew_potions(state, player), + "The Next Generation": lambda state: can_respawn_ender_dragon(state, player) and can_kill_ender_dragon(state, player), + "Fishy Business": lambda state: state.has("Fishing Rod", player), + "This Boat Has Legs": lambda state: ((fortress_loot(state, player) or complete_raid(state, player)) and + state.has("Saddle", player) and state.has("Fishing Rod", player)), + "Sniper Duel": lambda state: state.has("Archery", player), + "Great View From Up Here": lambda state: basic_combat(state, player), + "How Did We Get Here?": lambda state: (can_brew_potions(state, player) and + has_gold_ingots(state, player) and # Absorption + state.can_reach('End City', 'Region', player) and # Levitation + state.can_reach('The Nether', 'Region', player) and # potion ingredients + state.has("Fishing Rod", player) and state.has("Archery",player) and # Pufferfish, Nautilus Shells; spectral arrows + state.can_reach("Bring Home the Beacon", "Location", player) and # Haste + state.can_reach("Hero of the Village", "Location", player)), # Bad Omen, Hero of the Village + "Bullseye": lambda state: (state.has("Archery", player) and state.has("Progressive Tools", player, 2) and + has_iron_ingots(state, player)), + "Spooky Scary Skeleton": lambda state: basic_combat(state, player), + "Two by Two": lambda state: has_iron_ingots(state, player) and state.has("Bucket", player) and can_adventure(state, player), + "Two Birds, One Arrow": lambda state: craft_crossbow(state, player) and can_enchant(state, player), + "Who's the Pillager Now?": lambda state: craft_crossbow(state, player), + "Getting an Upgrade": lambda state: state.has("Progressive Tools", player), + "Tactical Fishing": lambda state: state.has("Bucket", player) and has_iron_ingots(state, player), + "Zombie Doctor": lambda state: can_brew_potions(state, player) and has_gold_ingots(state, player), + "Ice Bucket Challenge": lambda state: has_diamond_pickaxe(state, player), + "Into Fire": lambda state: basic_combat(state, player), + "War Pigs": lambda state: basic_combat(state, player), + "Take Aim": lambda state: state.has("Archery", player), + "Total Beelocation": lambda state: state.has("Silk Touch Book", player) and can_use_anvil(state, player) and can_enchant(state, player), + "Arbalistic": lambda state: (craft_crossbow(state, player) and state.has("Piercing IV Book", player) and + can_use_anvil(state, player) and can_enchant(state, player)), + "The End... Again...": lambda state: can_respawn_ender_dragon(state, player) and can_kill_ender_dragon(state, player), + "Acquire Hardware": lambda state: has_iron_ingots(state, player), + "Not Quite \"Nine\" Lives": lambda state: can_piglin_trade(state, player) and state.has("Progressive Resource Crafting", player, 2), + "Cover Me With Diamonds": lambda state: (state.has("Progressive Armor", player, 2) and + state.has("Progressive Tools", player, 2) and has_iron_ingots(state, player)), + "Sky's the Limit": lambda state: basic_combat(state, player), + "Hired Help": lambda state: state.has("Progressive Resource Crafting", player, 2) and has_iron_ingots(state, player), + "Sweet Dreams": lambda state: state.has("Bed", player) or state.can_reach('Village', 'Region', player), + "You Need a Mint": lambda state: can_respawn_ender_dragon(state, player) and has_bottle(state, player), + "Monsters Hunted": lambda state: (can_respawn_ender_dragon(state, player) and can_kill_ender_dragon(state, player) and + can_kill_wither(state, player) and state.has("Fishing Rod", player)), + "Enchanter": lambda state: can_enchant(state, player), + "Voluntary Exile": lambda state: basic_combat(state, player), + "Eye Spy": lambda state: enter_stronghold(state, player), + "Serious Dedication": lambda state: (can_brew_potions(state, player) and state.has("Bed", player) and + has_diamond_pickaxe(state, player) and has_gold_ingots(state, player)), + "Postmortal": lambda state: complete_raid(state, player), + "Adventuring Time": lambda state: can_adventure(state, player), + "Hero of the Village": lambda state: complete_raid(state, player), + "Hidden in the Depths": lambda state: can_brew_potions(state, player) and state.has("Bed", player) and has_diamond_pickaxe(state, player), + "Beaconator": lambda state: (can_kill_wither(state, player) and has_diamond_pickaxe(state, player) and + state.has("Progressive Resource Crafting", player, 2)), + "Withering Heights": lambda state: can_kill_wither(state, player), + "A Balanced Diet": lambda state: (has_bottle(state, player) and has_gold_ingots(state, player) and # honey bottle; gapple + state.has("Progressive Resource Crafting", player, 2) and state.can_reach('The End', 'Region', player)), # notch apple, chorus fruit + "Subspace Bubble": lambda state: has_diamond_pickaxe(state, player), + "Country Lode, Take Me Home": lambda state: state.can_reach("Hidden in the Depths", "Location", player) and has_gold_ingots(state, player), + "Bee Our Guest": lambda state: state.has("Campfire", player) and has_bottle(state, player), + "Uneasy Alliance": lambda state: has_diamond_pickaxe(state, player) and state.has('Fishing Rod', player), + "Diamonds!": lambda state: state.has("Progressive Tools", player, 2) and has_iron_ingots(state, player), + "A Throwaway Joke": lambda state: can_adventure(state, player), + "Sticky Situation": lambda state: state.has("Campfire", player) and has_bottle(state, player), + "Ol' Betsy": lambda state: craft_crossbow(state, player), + "Cover Me in Debris": lambda state: (state.has("Progressive Armor", player, 2) and + state.has("8 Netherite Scrap", player, 2) and state.has("Progressive Resource Crafting", player) and + has_diamond_pickaxe(state, player) and has_iron_ingots(state, player) and + can_brew_potions(state, player) and state.has("Bed", player)), + "Hot Topic": lambda state: state.has("Progressive Resource Crafting", player), + "The Lie": lambda state: has_iron_ingots(state, player) and state.has("Bucket", player), + "On a Rail": lambda state: has_iron_ingots(state, player) and state.has('Progressive Tools', player, 2), + "When Pigs Fly": lambda state: ((fortress_loot(state, player) or complete_raid(state, player)) and + state.has("Saddle", player) and state.has("Fishing Rod", player) and can_adventure(state, player)), + "Overkill": lambda state: (can_brew_potions(state, player) and + (state.has("Progressive Weapons", player) or state.can_reach('The Nether', 'Region', player))), + "Librarian": lambda state: state.has("Enchanting", player), + "Overpowered": lambda state: (has_iron_ingots(state, player) and + state.has('Progressive Tools', player, 2) and basic_combat(state, player)), + "Wax On": lambda state: (has_copper_ingots(state, player) and state.has('Campfire', player) and + state.has('Progressive Resource Crafting', player, 2)), + "Wax Off": lambda state: (has_copper_ingots(state, player) and state.has('Campfire', player) and + state.has('Progressive Resource Crafting', player, 2)), + "The Cutest Predator": lambda state: has_iron_ingots(state, player) and state.has('Bucket', player), + "The Healing Power of Friendship": lambda state: has_iron_ingots(state, player) and state.has('Bucket', player), + "Is It a Bird?": lambda state: has_spyglass(state, player) and can_adventure(state, player), + "Is It a Balloon?": lambda state: has_spyglass(state, player), + "Is It a Plane?": lambda state: has_spyglass(state, player) and can_respawn_ender_dragon(state, player), + "Surge Protector": lambda state: (state.has("Channeling Book", player) and + can_use_anvil(state, player) and can_enchant(state, player) and overworld_villager(state, player)), + "Light as a Rabbit": lambda state: can_adventure(state, player) and has_iron_ingots(state, player) and state.has('Bucket', player), + "Glow and Behold!": lambda state: can_adventure(state, player), + "Whatever Floats Your Goat!": lambda state: can_adventure(state, player), + "Caves & Cliffs": lambda state: has_iron_ingots(state, player) and state.has('Bucket', player) and state.has('Progressive Tools', player, 2), + "Feels like home": lambda state: (has_iron_ingots(state, player) and state.has('Bucket', player) and state.has('Fishing Rod', player) and + (fortress_loot(state, player) or complete_raid(state, player)) and state.has("Saddle", player)), + "Sound of Music": lambda state: state.has("Progressive Tools", player, 2) and has_iron_ingots(state, player) and basic_combat(state, player), + "Star Trader": lambda state: (has_iron_ingots(state, player) and state.has('Bucket', player) and + (state.can_reach("The Nether", 'Region', player) or + state.can_reach("Nether Fortress", 'Region', player) or # soul sand for water elevator + can_piglin_trade(state, player)) and + overworld_villager(state, player)), + "Birthday Song": lambda state: state.can_reach("The Lie", "Location", player) and state.has("Progressive Tools", player, 2) and has_iron_ingots(state, player), + "Bukkit Bukkit": lambda state: state.has("Bucket", player) and has_iron_ingots(state, player) and can_adventure(state, player), + "It Spreads": lambda state: can_adventure(state, player) and has_iron_ingots(state, player) and state.has("Progressive Tools", player, 2), + "Sneak 100": lambda state: can_adventure(state, player) and has_iron_ingots(state, player) and state.has("Progressive Tools", player, 2), + "When the Squad Hops into Town": lambda state: can_adventure(state, player) and state.has("Lead", player), + "With Our Powers Combined!": lambda state: can_adventure(state, player) and state.has("Lead", player), + } + } + return rules_lookup -# Sets rules on completion condition and postgame advancements -def set_completion_rules(world: MultiWorld, player: int): - def reachable_locations(state): - postgame_advancements = get_postgame_advancements(world.required_bosses[player].current_key) - return [location for location in world.get_locations() if - location.player == player and - location.name not in postgame_advancements and - location.address != None and - location.can_reach(state)] +def set_rules(mc_world: World) -> None: + multiworld = mc_world.multiworld + player = mc_world.player - def defeated_required_bosses(state): - return (world.required_bosses[player].current_key not in {"ender_dragon", "both"} or state.has("Defeat Ender Dragon", player)) and \ - (world.required_bosses[player].current_key not in {"wither", "both"} or state.has("Defeat Wither", player)) + rules_lookup = get_rules_lookup(player) - # 103 total advancements. Goal is to complete X advancements and then defeat the dragon. - # There are 11 possible postgame advancements; 5 for dragon, 5 for wither, 1 shared between them - # Hence the max for completion is 92 - egg_shards = min(world.egg_shards_required[player], world.egg_shards_available[player]) - completion_requirements = lambda state: len(reachable_locations(state)) >= world.advancement_goal[player] and \ - state.has("Dragon Egg Shard", player, egg_shards) - world.completion_condition[player] = lambda state: completion_requirements(state) and defeated_required_bosses(state) - # Set rules on postgame advancements - for adv_name in get_postgame_advancements(world.required_bosses[player].current_key): - add_rule(world.get_location(adv_name, player), completion_requirements) + # Set entrance rules + for entrance_name, rule in rules_lookup["entrances"].items(): + multiworld.get_entrance(entrance_name, player).access_rule = rule + + # Set location rules + for location_name, rule in rules_lookup["locations"].items(): + multiworld.get_location(location_name, player).access_rule = rule + + # Set rules surrounding completion + bosses = multiworld.required_bosses[player] + postgame_advancements = set() + if bosses.dragon: + postgame_advancements.update(Constants.exclusion_info["ender_dragon"]) + if bosses.wither: + postgame_advancements.update(Constants.exclusion_info["wither"]) + + def location_count(state: CollectionState) -> bool: + return len([location for location in multiworld.get_locations(player) if + location.address != None and + location.can_reach(state)]) + + def defeated_bosses(state: CollectionState) -> bool: + return ((not bosses.dragon or state.has("Ender Dragon", player)) + and (not bosses.wither or state.has("Wither", player))) + + egg_shards = min(multiworld.egg_shards_required[player], multiworld.egg_shards_available[player]) + completion_requirements = lambda state: (location_count(state) >= multiworld.advancement_goal[player] + and state.has("Dragon Egg Shard", player, egg_shards)) + multiworld.completion_condition[player] = lambda state: completion_requirements(state) and defeated_bosses(state) + + # Set exclusions on hard/unreasonable/postgame + excluded_advancements = set() + if not multiworld.include_hard_advancements[player]: + excluded_advancements.update(Constants.exclusion_info["hard"]) + if not multiworld.include_unreasonable_advancements[player]: + excluded_advancements.update(Constants.exclusion_info["unreasonable"]) + if not multiworld.include_postgame_advancements[player]: + excluded_advancements.update(postgame_advancements) + exclusion_rules(multiworld, player, excluded_advancements) diff --git a/worlds/minecraft/Structures.py b/worlds/minecraft/Structures.py new file mode 100644 index 0000000000..95bafc9efb --- /dev/null +++ b/worlds/minecraft/Structures.py @@ -0,0 +1,57 @@ +from worlds.AutoWorld import World + +from . import Constants + +def shuffle_structures(mc_world: World) -> None: + multiworld = mc_world.multiworld + player = mc_world.player + + default_connections = Constants.region_info["default_connections"] + illegal_connections = Constants.region_info["illegal_connections"] + + # Get all unpaired exits and all regions without entrances (except the Menu) + # This function is destructive on these lists. + exits = [exit.name for r in multiworld.regions if r.player == player for exit in r.exits if exit.connected_region == None] + structs = [r.name for r in multiworld.regions if r.player == player and r.entrances == [] and r.name != 'Menu'] + exits_spoiler = exits[:] # copy the original order for the spoiler log + + pairs = {} + + def set_pair(exit, struct): + if (exit in exits) and (struct in structs) and (exit not in illegal_connections.get(struct, [])): + pairs[exit] = struct + exits.remove(exit) + structs.remove(struct) + else: + raise Exception(f"Invalid connection: {exit} => {struct} for player {player} ({multiworld.player_name[player]})") + + # Connect plando structures first + if multiworld.plando_connections[player]: + for conn in multiworld.plando_connections[player]: + set_pair(conn.entrance, conn.exit) + + # The algorithm tries to place the most restrictive structures first. This algorithm always works on the + # relatively small set of restrictions here, but does not work on all possible inputs with valid configurations. + if multiworld.shuffle_structures[player]: + structs.sort(reverse=True, key=lambda s: len(illegal_connections.get(s, []))) + for struct in structs[:]: + try: + exit = multiworld.random.choice([e for e in exits if e not in illegal_connections.get(struct, [])]) + except IndexError: + raise Exception(f"No valid structure placements remaining for player {player} ({multiworld.player_name[player]})") + set_pair(exit, struct) + else: # write remaining default connections + for (exit, struct) in default_connections: + if exit in exits: + set_pair(exit, struct) + + # Make sure we actually paired everything; might fail if plando + try: + assert len(exits) == len(structs) == 0 + except AssertionError: + raise Exception(f"Failed to connect all Minecraft structures for player {player} ({multiworld.player_name[player]})") + + for exit in exits_spoiler: + multiworld.get_entrance(exit, player).connect(multiworld.get_region(pairs[exit], player)) + if multiworld.shuffle_structures[player] or multiworld.plando_connections[player]: + multiworld.spoiler.set_entrance(exit, pairs[exit], 'entrance', player) diff --git a/worlds/minecraft/__init__.py b/worlds/minecraft/__init__.py index 3d84f098d6..a685d1ab4b 100644 --- a/worlds/minecraft/__init__.py +++ b/worlds/minecraft/__init__.py @@ -1,17 +1,16 @@ import os import json from base64 import b64encode, b64decode -from math import ceil +from typing import Dict, Any -from .Items import MinecraftItem, item_table, required_items, junk_weights -from .Locations import MinecraftAdvancement, advancement_table, exclusion_table, get_postgame_advancements -from .Regions import mc_regions, link_minecraft_structures, default_connections -from .Rules import set_advancement_rules, set_completion_rules -from worlds.generic.Rules import exclusion_rules +from BaseClasses import Region, Entrance, Item, Tutorial, ItemClassification, Location +from worlds.AutoWorld import World, WebWorld -from BaseClasses import Region, Entrance, Item, Tutorial, ItemClassification +from . import Constants from .Options import minecraft_options -from ..AutoWorld import World, WebWorld +from .Structures import shuffle_structures +from .ItemPool import build_item_pool, get_junk_item_names +from .Rules import set_rules client_version = 9 @@ -71,13 +70,13 @@ class MinecraftWorld(World): topology_present = True web = MinecraftWebWorld() - 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()} + 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): - exits = [connection[0] for connection in default_connections] + def _get_mc_data(self) -> Dict[str, Any]: + exits = [connection[0] for connection in Constants.region_info["default_connections"]] return { 'world_seed': self.multiworld.per_slot_randoms[self.player].getrandbits(32), 'seed_name': self.multiworld.seed_name, @@ -96,74 +95,70 @@ class MinecraftWorld(World): 'race': self.multiworld.is_race, } - def generate_basic(self): + def create_item(self, name: str) -> Item: + item_class = ItemClassification.filler + if name in Constants.item_info["progression_items"]: + item_class = ItemClassification.progression + elif name in Constants.item_info["useful_items"]: + item_class = ItemClassification.useful + elif name in Constants.item_info["trap_items"]: + item_class = ItemClassification.trap - # Generate item pool - itempool = [] - junk_pool = junk_weights.copy() - # Add all required progression items - for (name, num) in required_items.items(): - itempool += [name] * num - # Add structure compasses if desired - if self.multiworld.structure_compasses[self.player]: - structures = [connection[1] for connection in default_connections] - for struct_name in structures: - itempool.append(f"Structure Compass ({struct_name})") - # Add dragon egg shards - if self.multiworld.egg_shards_required[self.player] > 0: - itempool += ["Dragon Egg Shard"] * self.multiworld.egg_shards_available[self.player] - # Add bee traps if desired - bee_trap_quantity = ceil(self.multiworld.bee_traps[self.player] * (len(self.location_names) - len(itempool)) * 0.01) - itempool += ["Bee Trap"] * bee_trap_quantity - # Fill remaining items with randomly generated junk - itempool += self.multiworld.random.choices(list(junk_pool.keys()), weights=list(junk_pool.values()), k=len(self.location_names) - len(itempool)) - # Convert itempool into real items - itempool = [item for item in map(lambda name: self.create_item(name), itempool)] + return MinecraftItem(name, item_class, self.item_name_to_id.get(name, None), self.player) - # Choose locations to automatically exclude based on settings - exclusion_pool = set() - exclusion_types = ['hard', 'unreasonable'] - for key in exclusion_types: - if not getattr(self.multiworld, f"include_{key}_advancements")[self.player]: - exclusion_pool.update(exclusion_table[key]) - # For postgame advancements, check with the boss goal - exclusion_pool.update(get_postgame_advancements(self.multiworld.required_bosses[self.player].current_key)) - exclusion_rules(self.multiworld, self.player, exclusion_pool) + def create_event(self, region_name: str, event_name: str) -> None: + region = self.multiworld.get_region(region_name, self.player) + loc = MinecraftLocation(self.player, event_name, None, region) + loc.place_locked_item(self.create_event_item(event_name)) + region.locations.append(loc) - # Prefill event locations with their events - self.multiworld.get_location("Blaze Spawner", self.player).place_locked_item(self.create_item("Blaze Rods")) - self.multiworld.get_location("Ender Dragon", self.player).place_locked_item(self.create_item("Defeat Ender Dragon")) - self.multiworld.get_location("Wither", self.player).place_locked_item(self.create_item("Defeat Wither")) + def create_event_item(self, name: str) -> None: + item = self.create_item(name) + item.classification = ItemClassification.progression + return item - self.multiworld.itempool += itempool + def create_regions(self) -> None: + # Create regions + for region_name, exits in Constants.region_info["regions"]: + r = Region(region_name, self.player, self.multiworld) + for exit_name in exits: + r.exits.append(Entrance(self.player, exit_name, r)) + self.multiworld.regions.append(r) - def get_filler_item_name(self) -> str: - return self.multiworld.random.choices(list(junk_weights.keys()), weights=list(junk_weights.values()))[0] + # Bind mandatory connections + for entr_name, region_name in Constants.region_info["mandatory_connections"]: + e = self.multiworld.get_entrance(entr_name, self.player) + r = self.multiworld.get_region(region_name, self.player) + e.connect(r) - def set_rules(self): - set_advancement_rules(self.multiworld, self.player) - set_completion_rules(self.multiworld, self.player) + # Add locations + for region_name, locations in Constants.location_info["locations_by_region"].items(): + region = self.multiworld.get_region(region_name, self.player) + for loc_name in locations: + loc = MinecraftLocation(self.player, loc_name, + self.location_name_to_id.get(loc_name, None), region) + region.locations.append(loc) - def create_regions(self): - def MCRegion(region_name: str, exits=[]): - ret = Region(region_name, self.player, self.multiworld) - ret.locations = [MinecraftAdvancement(self.player, loc_name, loc_data.id, ret) - for loc_name, loc_data in advancement_table.items() - if loc_data.region == region_name] - for exit in exits: - ret.exits.append(Entrance(self.player, exit, ret)) - return ret + # Add events + self.create_event("Nether Fortress", "Blaze Rods") + self.create_event("The End", "Ender Dragon") + self.create_event("Nether Fortress", "Wither") - self.multiworld.regions += [MCRegion(*r) for r in mc_regions] - link_minecraft_structures(self.multiworld, self.player) + # Shuffle the connections + shuffle_structures(self) - def generate_output(self, output_directory: str): + def create_items(self) -> None: + self.multiworld.itempool += build_item_pool(self) + + set_rules = set_rules + + def generate_output(self, output_directory: str) -> None: data = self._get_mc_data() filename = f"AP_{self.multiworld.get_out_file_name_base(self.player)}.apmc" with open(os.path.join(output_directory, filename), 'wb') as f: f.write(b64encode(bytes(json.dumps(data), 'utf-8'))) - def fill_slot_data(self): + def fill_slot_data(self) -> dict: slot_data = self._get_mc_data() for option_name in minecraft_options: option = getattr(self.multiworld, option_name)[self.player] @@ -171,20 +166,16 @@ class MinecraftWorld(World): slot_data[option_name] = int(option.value) return slot_data - def create_item(self, name: str) -> Item: - item_data = item_table[name] - if name == "Bee Trap": - classification = ItemClassification.trap - # prevent books from going on excluded locations - elif name in ("Sharpness III Book", "Infinity Book", "Looting III Book"): - classification = ItemClassification.useful - elif item_data.progression: - classification = ItemClassification.progression - else: - classification = ItemClassification.filler - item = MinecraftItem(name, classification, item_data.code, self.player) + def get_filler_item_name(self) -> str: + return get_junk_item_names(self.multiworld.random, 1)[0] + + +class MinecraftLocation(Location): + game = "Minecraft" + +class MinecraftItem(Item): + game = "Minecraft" - return item def mc_update_output(raw_data, server, port): data = json.loads(b64decode(raw_data)) diff --git a/worlds/minecraft/data/excluded_locations.json b/worlds/minecraft/data/excluded_locations.json new file mode 100644 index 0000000000..2f6fbbba6d --- /dev/null +++ b/worlds/minecraft/data/excluded_locations.json @@ -0,0 +1,40 @@ +{ + "hard": [ + "Very Very Frightening", + "A Furious Cocktail", + "Two by Two", + "Two Birds, One Arrow", + "Arbalistic", + "Monsters Hunted", + "Beaconator", + "A Balanced Diet", + "Uneasy Alliance", + "Cover Me in Debris", + "A Complete Catalogue", + "Surge Protector", + "Sound of Music", + "Star Trader", + "When the Squad Hops into Town", + "With Our Powers Combined!" + ], + "unreasonable": [ + "How Did We Get Here?", + "Adventuring Time" + ], + "ender_dragon": [ + "Free the End", + "The Next Generation", + "The End... Again...", + "You Need a Mint", + "Monsters Hunted", + "Is It a Plane?" + ], + "wither": [ + "Withering Heights", + "Bring Home the Beacon", + "Beaconator", + "A Furious Cocktail", + "How Did We Get Here?", + "Monsters Hunted" + ] +} \ No newline at end of file diff --git a/worlds/minecraft/data/items.json b/worlds/minecraft/data/items.json new file mode 100644 index 0000000000..7d35d18aeb --- /dev/null +++ b/worlds/minecraft/data/items.json @@ -0,0 +1,128 @@ +{ + "all_items": [ + "Archery", + "Progressive Resource Crafting", + "Resource Blocks", + "Brewing", + "Enchanting", + "Bucket", + "Flint and Steel", + "Bed", + "Bottles", + "Shield", + "Fishing Rod", + "Campfire", + "Progressive Weapons", + "Progressive Tools", + "Progressive Armor", + "8 Netherite Scrap", + "8 Emeralds", + "4 Emeralds", + "Channeling Book", + "Silk Touch Book", + "Sharpness III Book", + "Piercing IV Book", + "Looting III Book", + "Infinity Book", + "4 Diamond Ore", + "16 Iron Ore", + "500 XP", + "100 XP", + "50 XP", + "3 Ender Pearls", + "4 Lapis Lazuli", + "16 Porkchops", + "8 Gold Ore", + "Rotten Flesh", + "Single Arrow", + "32 Arrows", + "Saddle", + "Structure Compass (Village)", + "Structure Compass (Pillager Outpost)", + "Structure Compass (Nether Fortress)", + "Structure Compass (Bastion Remnant)", + "Structure Compass (End City)", + "Shulker Box", + "Dragon Egg Shard", + "Spyglass", + "Lead", + "Bee Trap" + ], + "progression_items": [ + "Archery", + "Progressive Resource Crafting", + "Resource Blocks", + "Brewing", + "Enchanting", + "Bucket", + "Flint and Steel", + "Bed", + "Bottles", + "Shield", + "Fishing Rod", + "Campfire", + "Progressive Weapons", + "Progressive Tools", + "Progressive Armor", + "8 Netherite Scrap", + "Channeling Book", + "Silk Touch Book", + "Piercing IV Book", + "3 Ender Pearls", + "Saddle", + "Structure Compass (Village)", + "Structure Compass (Pillager Outpost)", + "Structure Compass (Nether Fortress)", + "Structure Compass (Bastion Remnant)", + "Structure Compass (End City)", + "Dragon Egg Shard", + "Spyglass", + "Lead" + ], + "useful_items": [ + "Sharpness III Book", + "Looting III Book", + "Infinity Book" + ], + "trap_items": [ + "Bee Trap" + ], + + "required_pool": { + "Archery": 1, + "Progressive Resource Crafting": 2, + "Brewing": 1, + "Enchanting": 1, + "Bucket": 1, + "Flint and Steel": 1, + "Bed": 1, + "Bottles": 1, + "Shield": 1, + "Fishing Rod": 1, + "Campfire": 1, + "Progressive Weapons": 3, + "Progressive Tools": 3, + "Progressive Armor": 2, + "8 Netherite Scrap": 2, + "Channeling Book": 1, + "Silk Touch Book": 1, + "Sharpness III Book": 1, + "Piercing IV Book": 1, + "Looting III Book": 1, + "Infinity Book": 1, + "3 Ender Pearls": 4, + "Saddle": 1, + "Spyglass": 1, + "Lead": 1 + }, + "junk_weights": { + "4 Emeralds": 2, + "4 Diamond Ore": 1, + "16 Iron Ore": 1, + "50 XP": 4, + "16 Porkchops": 2, + "8 Gold Ore": 1, + "Rotten Flesh": 1, + "32 Arrows": 1 + } +} \ No newline at end of file diff --git a/worlds/minecraft/data/locations.json b/worlds/minecraft/data/locations.json new file mode 100644 index 0000000000..7cd00e5851 --- /dev/null +++ b/worlds/minecraft/data/locations.json @@ -0,0 +1,250 @@ +{ + "all_locations": [ + "Who is Cutting Onions?", + "Oh Shiny", + "Suit Up", + "Very Very Frightening", + "Hot Stuff", + "Free the End", + "A Furious Cocktail", + "Best Friends Forever", + "Bring Home the Beacon", + "Not Today, Thank You", + "Isn't It Iron Pick", + "Local Brewery", + "The Next Generation", + "Fishy Business", + "Hot Tourist Destinations", + "This Boat Has Legs", + "Sniper Duel", + "Nether", + "Great View From Up Here", + "How Did We Get Here?", + "Bullseye", + "Spooky Scary Skeleton", + "Two by Two", + "Stone Age", + "Two Birds, One Arrow", + "We Need to Go Deeper", + "Who's the Pillager Now?", + "Getting an Upgrade", + "Tactical Fishing", + "Zombie Doctor", + "The City at the End of the Game", + "Ice Bucket Challenge", + "Remote Getaway", + "Into Fire", + "War Pigs", + "Take Aim", + "Total Beelocation", + "Arbalistic", + "The End... Again...", + "Acquire Hardware", + "Not Quite \"Nine\" Lives", + "Cover Me With Diamonds", + "Sky's the Limit", + "Hired Help", + "Return to Sender", + "Sweet Dreams", + "You Need a Mint", + "Adventure", + "Monsters Hunted", + "Enchanter", + "Voluntary Exile", + "Eye Spy", + "The End", + "Serious Dedication", + "Postmortal", + "Monster Hunter", + "Adventuring Time", + "A Seedy Place", + "Those Were the Days", + "Hero of the Village", + "Hidden in the Depths", + "Beaconator", + "Withering Heights", + "A Balanced Diet", + "Subspace Bubble", + "Husbandry", + "Country Lode, Take Me Home", + "Bee Our Guest", + "What a Deal!", + "Uneasy Alliance", + "Diamonds!", + "A Terrible Fortress", + "A Throwaway Joke", + "Minecraft", + "Sticky Situation", + "Ol' Betsy", + "Cover Me in Debris", + "The End?", + "The Parrots and the Bats", + "A Complete Catalogue", + "Getting Wood", + "Time to Mine!", + "Hot Topic", + "Bake Bread", + "The Lie", + "On a Rail", + "Time to Strike!", + "Cow Tipper", + "When Pigs Fly", + "Overkill", + "Librarian", + "Overpowered", + "Wax On", + "Wax Off", + "The Cutest Predator", + "The Healing Power of Friendship", + "Is It a Bird?", + "Is It a Balloon?", + "Is It a Plane?", + "Surge Protector", + "Light as a Rabbit", + "Glow and Behold!", + "Whatever Floats Your Goat!", + "Caves & Cliffs", + "Feels like home", + "Sound of Music", + "Star Trader", + "Birthday Song", + "Bukkit Bukkit", + "It Spreads", + "Sneak 100", + "When the Squad Hops into Town", + "With Our Powers Combined!", + "You've Got a Friend in Me" + ], + "locations_by_region": { + "Overworld": [ + "Who is Cutting Onions?", + "Oh Shiny", + "Suit Up", + "Very Very Frightening", + "Hot Stuff", + "Best Friends Forever", + "Not Today, Thank You", + "Isn't It Iron Pick", + "Fishy Business", + "Sniper Duel", + "Bullseye", + "Stone Age", + "Two Birds, One Arrow", + "Getting an Upgrade", + "Tactical Fishing", + "Zombie Doctor", + "Ice Bucket Challenge", + "Take Aim", + "Total Beelocation", + "Arbalistic", + "Acquire Hardware", + "Cover Me With Diamonds", + "Hired Help", + "Sweet Dreams", + "Adventure", + "Monsters Hunted", + "Enchanter", + "Eye Spy", + "Monster Hunter", + "Adventuring Time", + "A Seedy Place", + "Husbandry", + "Bee Our Guest", + "Diamonds!", + "A Throwaway Joke", + "Minecraft", + "Sticky Situation", + "Ol' Betsy", + "The Parrots and the Bats", + "Getting Wood", + "Time to Mine!", + "Hot Topic", + "Bake Bread", + "The Lie", + "On a Rail", + "Time to Strike!", + "Cow Tipper", + "When Pigs Fly", + "Librarian", + "Wax On", + "Wax Off", + "The Cutest Predator", + "The Healing Power of Friendship", + "Is It a Bird?", + "Surge Protector", + "Light as a Rabbit", + "Glow and Behold!", + "Whatever Floats Your Goat!", + "Caves & Cliffs", + "Sound of Music", + "Bukkit Bukkit", + "It Spreads", + "Sneak 100", + "When the Squad Hops into Town" + ], + "The Nether": [ + "Hot Tourist Destinations", + "This Boat Has Legs", + "Nether", + "Two by Two", + "We Need to Go Deeper", + "Not Quite \"Nine\" Lives", + "Return to Sender", + "Serious Dedication", + "Hidden in the Depths", + "Subspace Bubble", + "Country Lode, Take Me Home", + "Uneasy Alliance", + "Cover Me in Debris", + "Is It a Balloon?", + "Feels like home", + "With Our Powers Combined!" + ], + "The End": [ + "Free the End", + "The Next Generation", + "Remote Getaway", + "The End... Again...", + "You Need a Mint", + "The End", + "The End?", + "Is It a Plane?" + ], + "Village": [ + "Postmortal", + "Hero of the Village", + "A Balanced Diet", + "What a Deal!", + "A Complete Catalogue", + "Star Trader" + ], + "Nether Fortress": [ + "A Furious Cocktail", + "Bring Home the Beacon", + "Local Brewery", + "How Did We Get Here?", + "Spooky Scary Skeleton", + "Into Fire", + "Beaconator", + "Withering Heights", + "A Terrible Fortress", + "Overkill" + ], + "Pillager Outpost": [ + "Who's the Pillager Now?", + "Voluntary Exile", + "Birthday Song", + "You've Got a Friend in Me" + ], + "Bastion Remnant": [ + "War Pigs", + "Those Were the Days", + "Overpowered" + ], + "End City": [ + "Great View From Up Here", + "The City at the End of the Game", + "Sky's the Limit" + ] + } +} \ No newline at end of file diff --git a/worlds/minecraft/data/regions.json b/worlds/minecraft/data/regions.json new file mode 100644 index 0000000000..c9e51e4829 --- /dev/null +++ b/worlds/minecraft/data/regions.json @@ -0,0 +1,28 @@ +{ + "regions": [ + ["Menu", ["New World"]], + ["Overworld", ["Nether Portal", "End Portal", "Overworld Structure 1", "Overworld Structure 2"]], + ["The Nether", ["Nether Structure 1", "Nether Structure 2"]], + ["The End", ["The End Structure"]], + ["Village", []], + ["Pillager Outpost", []], + ["Nether Fortress", []], + ["Bastion Remnant", []], + ["End City", []] + ], + "mandatory_connections": [ + ["New World", "Overworld"], + ["Nether Portal", "The Nether"], + ["End Portal", "The End"] + ], + "default_connections": [ + ["Overworld Structure 1", "Village"], + ["Overworld Structure 2", "Pillager Outpost"], + ["Nether Structure 1", "Nether Fortress"], + ["Nether Structure 2", "Bastion Remnant"], + ["The End Structure", "End City"] + ], + "illegal_connections": { + "Nether Fortress": ["The End Structure"] + } +} \ No newline at end of file diff --git a/worlds/minecraft/test/TestAdvancements.py b/worlds/minecraft/test/TestAdvancements.py index 5fc64f76bf..321aef1af9 100644 --- a/worlds/minecraft/test/TestAdvancements.py +++ b/worlds/minecraft/test/TestAdvancements.py @@ -1,10 +1,14 @@ -from .TestMinecraft import TestMinecraft +from . import MCTestBase # Format: # [location, expected_result, given_items, [excluded_items]] # Every advancement has its own test, named by its internal ID number. -class TestAdvancements(TestMinecraft): +class TestAdvancements(MCTestBase): + options = { + "shuffle_structures": False, + "structure_compasses": False + } def test_42000(self): self.run_location_tests([ @@ -1278,3 +1282,129 @@ class TestAdvancements(TestMinecraft): ["Whatever Floats Your Goat!", True, ["Progressive Weapons", "Progressive Resource Crafting"]], ["Whatever Floats Your Goat!", True, ["Progressive Weapons", "Campfire"]], ]) + + # bucket, iron pick + def test_42103(self): + self.run_location_tests([ + ["Caves & Cliffs", False, []], + ["Caves & Cliffs", False, [], ["Bucket"]], + ["Caves & Cliffs", False, [], ["Progressive Tools"]], + ["Caves & Cliffs", False, [], ["Progressive Resource Crafting"]], + ["Caves & Cliffs", True, ["Progressive Resource Crafting", "Progressive Tools", "Progressive Tools", "Bucket"]], + ]) + + # bucket, fishing rod, saddle, combat + def test_42104(self): + self.run_location_tests([ + ["Feels like home", False, []], + ["Feels like home", False, [], ['Progressive Resource Crafting']], + ["Feels like home", False, [], ['Progressive Tools']], + ["Feels like home", False, [], ['Progressive Weapons']], + ["Feels like home", False, [], ['Progressive Armor', 'Shield']], + ["Feels like home", False, [], ['Fishing Rod']], + ["Feels like home", False, [], ['Saddle']], + ["Feels like home", False, [], ['Bucket']], + ["Feels like home", False, [], ['Flint and Steel']], + ["Feels like home", True, ['Saddle', 'Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons', 'Progressive Armor', 'Fishing Rod']], + ["Feels like home", True, ['Saddle', 'Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons', 'Shield', 'Fishing Rod']], + ]) + + # iron pick, combat + def test_42105(self): + self.run_location_tests([ + ["Sound of Music", False, []], + ["Sound of Music", False, [], ["Progressive Tools"]], + ["Sound of Music", False, [], ["Progressive Resource Crafting"]], + ["Sound of Music", False, [], ["Progressive Weapons"]], + ["Sound of Music", False, [], ["Progressive Armor", "Shield"]], + ["Sound of Music", True, ["Progressive Tools", "Progressive Tools", "Progressive Resource Crafting", "Progressive Weapons", "Progressive Armor"]], + ["Sound of Music", True, ["Progressive Tools", "Progressive Tools", "Progressive Resource Crafting", "Progressive Weapons", "Shield"]], + ]) + + # bucket, nether, villager + def test_42106(self): + self.run_location_tests([ + ["Star Trader", False, []], + ["Star Trader", False, [], ["Bucket"]], + ["Star Trader", False, [], ["Flint and Steel"]], + ["Star Trader", False, [], ["Progressive Tools"]], + ["Star Trader", False, [], ["Progressive Resource Crafting"]], + ["Star Trader", False, [], ["Progressive Weapons"]], + ["Star Trader", True, ["Bucket", "Flint and Steel", "Progressive Tools", "Progressive Resource Crafting", "Progressive Weapons"]], + ]) + + # bucket, redstone -> iron pick, pillager outpost -> adventure + def test_42107(self): + self.run_location_tests([ + ["Birthday Song", False, []], + ["Birthday Song", False, [], ["Bucket"]], + ["Birthday Song", False, [], ["Progressive Tools"]], + ["Birthday Song", False, [], ["Progressive Weapons"]], + ["Birthday Song", False, [], ["Progressive Resource Crafting"]], + ["Birthday Song", True, ["Progressive Resource Crafting", "Progressive Tools", "Progressive Tools", "Progressive Weapons", "Bucket"]], + ]) + + # bucket, adventure + def test_42108(self): + self.run_location_tests([ + ["Bukkit Bukkit", False, []], + ["Bukkit Bukkit", False, [], ["Bucket"]], + ["Bukkit Bukkit", False, [], ["Progressive Tools"]], + ["Bukkit Bukkit", False, [], ["Progressive Weapons"]], + ["Bukkit Bukkit", False, [], ["Progressive Resource Crafting"]], + ["Bukkit Bukkit", True, ["Bucket", "Progressive Tools", "Progressive Weapons", "Progressive Resource Crafting"]], + ]) + + # iron pick, adventure + def test_42109(self): + self.run_location_tests([ + ["It Spreads", False, []], + ["It Spreads", False, [], ["Progressive Tools"]], + ["It Spreads", False, [], ["Progressive Weapons"]], + ["It Spreads", False, [], ["Progressive Resource Crafting"]], + ["It Spreads", True, ["Progressive Tools", "Progressive Tools", "Progressive Weapons", "Progressive Resource Crafting"]], + ]) + + # iron pick, adventure + def test_42110(self): + self.run_location_tests([ + ["Sneak 100", False, []], + ["Sneak 100", False, [], ["Progressive Tools"]], + ["Sneak 100", False, [], ["Progressive Weapons"]], + ["Sneak 100", False, [], ["Progressive Resource Crafting"]], + ["Sneak 100", True, ["Progressive Tools", "Progressive Tools", "Progressive Weapons", "Progressive Resource Crafting"]], + ]) + + # adventure, lead + def test_42111(self): + self.run_location_tests([ + ["When the Squad Hops into Town", False, []], + ["When the Squad Hops into Town", False, [], ["Progressive Weapons"]], + ["When the Squad Hops into Town", False, [], ["Campfire", "Progressive Resource Crafting"]], + ["When the Squad Hops into Town", False, [], ["Lead"]], + ["When the Squad Hops into Town", True, ["Progressive Weapons", "Lead", "Campfire"]], + ["When the Squad Hops into Town", True, ["Progressive Weapons", "Lead", "Progressive Resource Crafting"]], + ]) + + # adventure, lead, nether + def test_42112(self): + self.run_location_tests([ + ["With Our Powers Combined!", False, []], + ["With Our Powers Combined!", False, [], ["Lead"]], + ["With Our Powers Combined!", False, [], ["Bucket", "Progressive Tools"]], + ["With Our Powers Combined!", False, [], ["Flint and Steel"]], + ["With Our Powers Combined!", False, [], ["Progressive Weapons"]], + ["With Our Powers Combined!", False, [], ["Progressive Resource Crafting"]], + ["With Our Powers Combined!", True, ["Lead", "Progressive Weapons", "Progressive Resource Crafting", "Flint and Steel", "Progressive Tools", "Bucket"]], + ["With Our Powers Combined!", True, ["Lead", "Progressive Weapons", "Progressive Resource Crafting", "Flint and Steel", "Progressive Tools", "Progressive Tools", "Progressive Tools"]], + ]) + + # pillager outpost -> adventure + def test_42113(self): + self.run_location_tests([ + ["You've Got a Friend in Me", False, []], + ["You've Got a Friend in Me", False, [], ["Progressive Weapons"]], + ["You've Got a Friend in Me", False, [], ["Campfire", "Progressive Resource Crafting"]], + ["You've Got a Friend in Me", True, ["Progressive Weapons", "Campfire"]], + ["You've Got a Friend in Me", True, ["Progressive Weapons", "Progressive Resource Crafting"]], + ]) diff --git a/worlds/minecraft/test/TestDataLoad.py b/worlds/minecraft/test/TestDataLoad.py new file mode 100644 index 0000000000..c14eef071b --- /dev/null +++ b/worlds/minecraft/test/TestDataLoad.py @@ -0,0 +1,60 @@ +import unittest + +from .. import Constants + +class TestDataLoad(unittest.TestCase): + + def test_item_data(self): + item_info = Constants.item_info + + # All items in sub-tables are in all_items + all_items: set = set(item_info['all_items']) + assert set(item_info['progression_items']) <= all_items + assert set(item_info['useful_items']) <= all_items + assert set(item_info['trap_items']) <= all_items + assert set(item_info['required_pool'].keys()) <= all_items + assert set(item_info['junk_weights'].keys()) <= all_items + + # No overlapping ids (because of bee trap stuff) + all_ids: set = set(Constants.item_name_to_id.values()) + assert len(all_items) == len(all_ids) + + def test_location_data(self): + location_info = Constants.location_info + exclusion_info = Constants.exclusion_info + + # Every location has a region and every region's locations are in all_locations + all_locations: set = set(location_info['all_locations']) + all_locs_2: set = set() + for v in location_info['locations_by_region'].values(): + all_locs_2.update(v) + assert all_locations == all_locs_2 + + # All exclusions are locations + for v in exclusion_info.values(): + assert set(v) <= all_locations + + def test_region_data(self): + region_info = Constants.region_info + + # Every entrance and region in mandatory/default/illegal connections is a real entrance and region + all_regions = set() + all_entrances = set() + for v in region_info['regions']: + assert isinstance(v[0], str) + assert isinstance(v[1], list) + all_regions.add(v[0]) + all_entrances.update(v[1]) + + for v in region_info['mandatory_connections']: + assert v[0] in all_entrances + assert v[1] in all_regions + + for v in region_info['default_connections']: + assert v[0] in all_entrances + assert v[1] in all_regions + + for k, v in region_info['illegal_connections'].items(): + assert k in all_regions + assert set(v) <= all_entrances + diff --git a/worlds/minecraft/test/TestEntrances.py b/worlds/minecraft/test/TestEntrances.py index 8e80a1353a..946eb23d63 100644 --- a/worlds/minecraft/test/TestEntrances.py +++ b/worlds/minecraft/test/TestEntrances.py @@ -1,7 +1,11 @@ -from .TestMinecraft import TestMinecraft +from . import MCTestBase -class TestEntrances(TestMinecraft): +class TestEntrances(MCTestBase): + options = { + "shuffle_structures": False, + "structure_compasses": False + } def testPortals(self): self.run_entrance_tests([ diff --git a/worlds/minecraft/test/TestMinecraft.py b/worlds/minecraft/test/TestMinecraft.py deleted file mode 100644 index dc5c81c031..0000000000 --- a/worlds/minecraft/test/TestMinecraft.py +++ /dev/null @@ -1,68 +0,0 @@ -from test.TestBase import TestBase -from BaseClasses import MultiWorld, ItemClassification -from worlds import AutoWorld -from worlds.minecraft import MinecraftWorld -from worlds.minecraft.Items import MinecraftItem, item_table -from Options import Toggle -from worlds.minecraft.Options import AdvancementGoal, EggShardsRequired, EggShardsAvailable, BossGoal, BeeTraps, \ - ShuffleStructures, CombatDifficulty - - -# Converts the name of an item into an item object -def MCItemFactory(items, player: int): - ret = [] - singleton = False - if isinstance(items, str): - items = [items] - singleton = True - for item in items: - if item in item_table: - ret.append(MinecraftItem( - item, ItemClassification.progression if item_table[item].progression else ItemClassification.filler, - item_table[item].code, player - )) - else: - raise Exception(f"Unknown item {item}") - - if singleton: - return ret[0] - return ret - - -class TestMinecraft(TestBase): - - def setUp(self): - self.multiworld = MultiWorld(1) - self.multiworld.game[1] = "Minecraft" - self.multiworld.worlds[1] = MinecraftWorld(self.multiworld, 1) - exclusion_pools = ['hard', 'unreasonable', 'postgame'] - for pool in exclusion_pools: - setattr(self.multiworld, f"include_{pool}_advancements", {1: False}) - setattr(self.multiworld, "advancement_goal", {1: AdvancementGoal(30)}) - setattr(self.multiworld, "egg_shards_required", {1: EggShardsRequired(0)}) - setattr(self.multiworld, "egg_shards_available", {1: EggShardsAvailable(0)}) - setattr(self.multiworld, "required_bosses", {1: BossGoal(1)}) # ender dragon - setattr(self.multiworld, "shuffle_structures", {1: ShuffleStructures(False)}) - setattr(self.multiworld, "bee_traps", {1: BeeTraps(0)}) - setattr(self.multiworld, "combat_difficulty", {1: CombatDifficulty(1)}) # normal - setattr(self.multiworld, "structure_compasses", {1: Toggle(False)}) - setattr(self.multiworld, "death_link", {1: Toggle(False)}) - AutoWorld.call_single(self.multiworld, "create_regions", 1) - AutoWorld.call_single(self.multiworld, "generate_basic", 1) - AutoWorld.call_single(self.multiworld, "set_rules", 1) - - def _get_items(self, item_pool, all_except): - if all_except and len(all_except) > 0: - items = self.multiworld.itempool[:] - items = [item for item in items if - item.name not in all_except and not ("Bottle" in item.name and "AnyBottle" in all_except)] - items.extend(MCItemFactory(item_pool[0], 1)) - else: - items = MCItemFactory(item_pool[0], 1) - return self.get_state(items) - - def _get_items_partial(self, item_pool, missing_item): - new_items = item_pool[0].copy() - new_items.remove(missing_item) - items = MCItemFactory(new_items, 1) - return self.get_state(items) diff --git a/worlds/minecraft/test/TestOptions.py b/worlds/minecraft/test/TestOptions.py new file mode 100644 index 0000000000..668ed500e8 --- /dev/null +++ b/worlds/minecraft/test/TestOptions.py @@ -0,0 +1,49 @@ +from . import MCTestBase +from ..Constants import region_info +from ..Options import minecraft_options + +from BaseClasses import ItemClassification + +class AdvancementTestBase(MCTestBase): + options = { + "advancement_goal": minecraft_options["advancement_goal"].range_end + } + # beatability test implicit + +class ShardTestBase(MCTestBase): + options = { + "egg_shards_required": minecraft_options["egg_shards_required"].range_end, + "egg_shards_available": minecraft_options["egg_shards_available"].range_end + } + + # check that itempool is not overfilled with shards + def test_itempool(self): + assert len(self.multiworld.get_unfilled_locations()) == len(self.multiworld.itempool) + +class CompassTestBase(MCTestBase): + def test_compasses_in_pool(self): + structures = [x[1] for x in region_info["default_connections"]] + itempool_str = {item.name for item in self.multiworld.itempool} + for struct in structures: + assert f"Structure Compass ({struct})" in itempool_str + +class NoBeeTestBase(MCTestBase): + options = { + "bee_traps": 0 + } + + # With no bees, there are no traps in the pool + def test_bees(self): + for item in self.multiworld.itempool: + assert item.classification != ItemClassification.trap + + +class AllBeeTestBase(MCTestBase): + options = { + "bee_traps": 100 + } + + # With max bees, there are no filler items, only bee traps + def test_bees(self): + for item in self.multiworld.itempool: + assert item.classification != ItemClassification.filler diff --git a/worlds/minecraft/test/__init__.py b/worlds/minecraft/test/__init__.py index e69de29bb2..acf9b79491 100644 --- a/worlds/minecraft/test/__init__.py +++ b/worlds/minecraft/test/__init__.py @@ -0,0 +1,33 @@ +from test.TestBase import TestBase, WorldTestBase +from .. import MinecraftWorld + + +class MCTestBase(WorldTestBase, TestBase): + game = "Minecraft" + player: int = 1 + + def _create_items(self, items, player): + singleton = False + if isinstance(items, str): + items = [items] + singleton = True + ret = [self.multiworld.worlds[player].create_item(item) for item in items] + if singleton: + return ret[0] + return ret + + def _get_items(self, item_pool, all_except): + if all_except and len(all_except) > 0: + items = self.multiworld.itempool[:] + items = [item for item in items if item.name not in all_except] + items.extend(self._create_items(item_pool[0], 1)) + else: + items = self._create_items(item_pool[0], 1) + return self.get_state(items) + + def _get_items_partial(self, item_pool, missing_item): + new_items = item_pool[0].copy() + new_items.remove(missing_item) + items = self._create_items(new_items, 1) + return self.get_state(items) + From 942d689093ba59a3cdd4f70110471cd56a5b034b Mon Sep 17 00:00:00 2001 From: KonoTyran Date: Wed, 8 Mar 2023 20:14:54 -0800 Subject: [PATCH 038/172] [Slay the Spire] Enable support for modded characters, and add downfall support (#1368) * add ability to choose custom characters in STS * bump required protocol (client?) version. * fix slot data fill. * add downfall mode, as well as characters. * small change in documentation for character choice as it now uses internal ID's instead of visible titles... because other languages are a thing. --- worlds/spire/Options.py | 53 ++++++++++++++++++++++++++++++---------- worlds/spire/__init__.py | 16 ++++-------- 2 files changed, 45 insertions(+), 24 deletions(-) diff --git a/worlds/spire/Options.py b/worlds/spire/Options.py index 1711e12deb..76cbc4cf37 100644 --- a/worlds/spire/Options.py +++ b/worlds/spire/Options.py @@ -1,15 +1,34 @@ import typing -from Options import Choice, Option, Range, Toggle +from Options import TextChoice, Option, Range, Toggle -class Character(Choice): - """Pick What Character you wish to play with.""" +class Character(TextChoice): + """Enter the internal ID of the character to use. + + if you don't know the exact ID to enter with the mod installed go to + `Mods -> Archipelago Multi-world -> config` to view a list of installed modded character IDs. + + the downfall characters will only work if you have downfall installed. + + Spire Take the Wheel will have your client pick a random character from the list of all your installed characters + including custom ones. + + if the chosen character mod is not installed it will default back to 'The Ironclad' + """ display_name = "Character" - option_ironclad = 0 - option_silent = 1 - option_defect = 2 - option_watcher = 3 - default = 0 + option_The_Ironclad = 0 + option_The_Silent = 1 + option_The_Defect = 2 + option_The_Watcher = 3 + option_The_Hermit = 4 + option_The_Slime_Boss = 5 + option_The_Guardian = 6 + option_The_Hexaghost = 7 + option_The_Champ = 8 + option_The_Gremlins = 9 + option_The_Automaton = 10 + option_The_Snecko = 11 + option_spire_take_the_wheel = 12 class Ascension(Range): @@ -20,10 +39,17 @@ class Ascension(Range): default = 0 -class HeartRun(Toggle): - """Whether or not you will need to collect the 3 keys and enter the final act to - complete the game. The Heart does not need to be defeated.""" - display_name = "Heart Run" +class FinalAct(Toggle): + """Whether you will need to collect the 3 keys and beat the final act to complete the game.""" + display_name = "Final Act" + option_true = 1 + option_false = 0 + default = 0 + + +class Downfall(Toggle): + """When Downfall is Installed this will switch the played mode to Downfall""" + display_name = "Downfall" option_true = 1 option_false = 0 default = 0 @@ -32,5 +58,6 @@ class HeartRun(Toggle): spire_options: typing.Dict[str, type(Option)] = { "character": Character, "ascension": Ascension, - "heart_run": HeartRun + "final_act": FinalAct, + "downfall": Downfall, } diff --git a/worlds/spire/__init__.py b/worlds/spire/__init__.py index 5a7ed19ecf..a9f4d46d70 100644 --- a/worlds/spire/__init__.py +++ b/worlds/spire/__init__.py @@ -32,18 +32,11 @@ class SpireWorld(World): topology_present = False data_version = 1 web = SpireWeb() + required_client_version = (0, 3, 7) item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = location_table - def _get_slot_data(self): - return { - 'seed': "".join(self.multiworld.per_slot_randoms[self.player].choice(string.ascii_letters) for i in range(16)), - 'character': self.multiworld.character[self.player], - 'ascension': self.multiworld.ascension[self.player], - 'heart_run': self.multiworld.heart_run[self.player] - } - def generate_basic(self): # Fill out our pool with our items from item_pool, assuming 1 item if not present in item_pool pool = [] @@ -63,7 +56,6 @@ class SpireWorld(World): if self.multiworld.logic[self.player] != 'no logic': self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) - def set_rules(self): set_rules(self.multiworld, self.player) @@ -74,10 +66,12 @@ class SpireWorld(World): create_regions(self.multiworld, self.player) def fill_slot_data(self) -> dict: - slot_data = self._get_slot_data() + slot_data = { + 'seed': "".join(self.multiworld.slot_seeds[self.player].choice(string.ascii_letters) for i in range(16)) + } for option_name in spire_options: option = getattr(self.multiworld, option_name)[self.player] - slot_data[option_name] = int(option.value) + slot_data[option_name] = option.value return slot_data def get_filler_item_name(self) -> str: From c61f4672187f781aaff298fe6089bcc291231260 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 9 Mar 2023 12:31:35 +0100 Subject: [PATCH 039/172] WebHost: fix location_name_group related spinup crash --- WebHostLib/customserver.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index 9c21fca4f9..1c35d77884 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -131,6 +131,8 @@ def get_static_server_data() -> dict: "gamespackage": worlds.network_data_package["games"], "item_name_groups": {world_name: world.item_name_groups for world_name, world in worlds.AutoWorldRegister.world_types.items()}, + "location_name_groups": {world_name: world.location_name_groups for world_name, world in + worlds.AutoWorldRegister.world_types.items()}, } for world_name, world in worlds.AutoWorldRegister.world_types.items(): From 2e76085cf15e182a64d1ab1ab504d151894af9e0 Mon Sep 17 00:00:00 2001 From: Freya Arbjerg Date: Thu, 9 Mar 2023 19:24:38 +0100 Subject: [PATCH 040/172] WebHost: Fix generic tracker Datatables (#1519) --- .../static/assets/{multiTrackerCommon.js => trackerCommon.js} | 0 WebHostLib/templates/genericTracker.html | 2 +- WebHostLib/templates/lttpMultiTracker.html | 2 +- WebHostLib/templates/multiTracker.html | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename WebHostLib/static/assets/{multiTrackerCommon.js => trackerCommon.js} (100%) diff --git a/WebHostLib/static/assets/multiTrackerCommon.js b/WebHostLib/static/assets/trackerCommon.js similarity index 100% rename from WebHostLib/static/assets/multiTrackerCommon.js rename to WebHostLib/static/assets/trackerCommon.js diff --git a/WebHostLib/templates/genericTracker.html b/WebHostLib/templates/genericTracker.html index 508c084e7f..1c2fcd44c0 100644 --- a/WebHostLib/templates/genericTracker.html +++ b/WebHostLib/templates/genericTracker.html @@ -4,7 +4,7 @@ {{ player_name }}'s Tracker - + {% endblock %} {% block body %} diff --git a/WebHostLib/templates/lttpMultiTracker.html b/WebHostLib/templates/lttpMultiTracker.html index a86c6851cb..276e1de3ce 100644 --- a/WebHostLib/templates/lttpMultiTracker.html +++ b/WebHostLib/templates/lttpMultiTracker.html @@ -5,7 +5,7 @@ - + {% endblock %} {% block body %} diff --git a/WebHostLib/templates/multiTracker.html b/WebHostLib/templates/multiTracker.html index 25eeec8d78..3c6b09d173 100644 --- a/WebHostLib/templates/multiTracker.html +++ b/WebHostLib/templates/multiTracker.html @@ -3,7 +3,7 @@ {{ super() }} Multiworld Tracker - + {% endblock %} {% block body %} From 7fdf38b2ad76879a79a61242748f329b5e8bc7e5 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 9 Mar 2023 21:31:00 +0100 Subject: [PATCH 041/172] WebHost: automatically fill PATCH_TARGET -> HOST_ADDRESS and re-use it for rooms (#1518) --- Utils.py | 6 +++--- WebHost.py | 5 +++++ WebHostLib/__init__.py | 2 +- WebHostLib/autolauncher.py | 3 ++- WebHostLib/customserver.py | 8 +++++--- WebHostLib/downloads.py | 4 ++-- WebHostLib/templates/hostRoom.html | 4 ++-- WebHostLib/templates/macros.html | 2 +- docs/webhost configuration sample.yaml | 4 ++-- 9 files changed, 23 insertions(+), 15 deletions(-) diff --git a/Utils.py b/Utils.py index 3ec3d4ff56..059168c857 100644 --- a/Utils.py +++ b/Utils.py @@ -195,11 +195,11 @@ def get_public_ipv4() -> str: ip = socket.gethostbyname(socket.gethostname()) ctx = get_cert_none_ssl_context() try: - ip = urllib.request.urlopen("https://checkip.amazonaws.com/", context=ctx).read().decode("utf8").strip() + ip = urllib.request.urlopen("https://checkip.amazonaws.com/", context=ctx, timeout=10).read().decode("utf8").strip() except Exception as e: # noinspection PyBroadException try: - ip = urllib.request.urlopen("https://v4.ident.me", context=ctx).read().decode("utf8").strip() + ip = urllib.request.urlopen("https://v4.ident.me", context=ctx, timeout=10).read().decode("utf8").strip() except Exception: logging.exception(e) pass # we could be offline, in a local game, so no point in erroring out @@ -213,7 +213,7 @@ def get_public_ipv6() -> str: ip = socket.gethostbyname(socket.gethostname()) ctx = get_cert_none_ssl_context() try: - ip = urllib.request.urlopen("https://v6.ident.me", context=ctx).read().decode("utf8").strip() + ip = urllib.request.urlopen("https://v6.ident.me", context=ctx, timeout=10).read().decode("utf8").strip() except Exception as e: logging.exception(e) pass # we could be offline, in a local game, or ipv6 may not be available diff --git a/WebHost.py b/WebHost.py index d098f6e7fb..40d366a02f 100644 --- a/WebHost.py +++ b/WebHost.py @@ -33,6 +33,11 @@ def get_app(): import yaml app.config.from_file(configpath, yaml.safe_load) logging.info(f"Updated config from {configpath}") + if not app.config["HOST_ADDRESS"]: + logging.info("Getting public IP, as HOST_ADDRESS is empty.") + app.config["HOST_ADDRESS"] = Utils.get_public_ipv4() + logging.info(f"HOST_ADDRESS was set to {app.config['HOST_ADDRESS']}") + db.bind(**app.config["PONY"]) db.generate_mapping(create_tables=True) return app diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index e8e5b59d89..8bd3609c1d 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -51,7 +51,7 @@ app.config["PONY"] = { app.config["MAX_ROLL"] = 20 app.config["CACHE_TYPE"] = "flask_caching.backends.SimpleCache" app.config["JSON_AS_ASCII"] = False -app.config["PATCH_TARGET"] = "archipelago.gg" +app.config["HOST_ADDRESS"] = "" cache = Cache(app) Compress(app) diff --git a/WebHostLib/autolauncher.py b/WebHostLib/autolauncher.py index 4cf7243302..484755b3c3 100644 --- a/WebHostLib/autolauncher.py +++ b/WebHostLib/autolauncher.py @@ -179,6 +179,7 @@ class MultiworldInstance(): self.ponyconfig = config["PONY"] self.cert = config["SELFLAUNCHCERT"] self.key = config["SELFLAUNCHKEY"] + self.host = config["HOST_ADDRESS"] def start(self): if self.process and self.process.is_alive(): @@ -187,7 +188,7 @@ class MultiworldInstance(): logging.info(f"Spinning up {self.room_id}") process = multiprocessing.Process(group=None, target=run_server_process, args=(self.room_id, self.ponyconfig, get_static_server_data(), - self.cert, self.key), + self.cert, self.key, self.host), name="MultiHost") process.start() # bind after start to prevent thread sync issues with guardian. diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index 1c35d77884..584ca9feca 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -142,7 +142,8 @@ def get_static_server_data() -> dict: def run_server_process(room_id, ponyconfig: dict, static_server_data: dict, - cert_file: typing.Optional[str], cert_key_file: typing.Optional[str]): + cert_file: typing.Optional[str], cert_key_file: typing.Optional[str], + host: str): # establish DB connection for multidata and multisave db.bind(**ponyconfig) db.generate_mapping(check_tables=False) @@ -167,17 +168,18 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict, for wssocket in ctx.server.ws_server.sockets: socketname = wssocket.getsockname() if wssocket.family == socket.AF_INET6: - logging.info(f'Hosting game at [{get_public_ipv6()}]:{socketname[1]}') # Prefer IPv4, as most users seem to not have working ipv6 support if not port: port = socketname[1] elif wssocket.family == socket.AF_INET: - logging.info(f'Hosting game at {get_public_ipv4()}:{socketname[1]}') port = socketname[1] if port: + logging.info(f'Hosting game at {host}:{port}') with db_session: room = Room.get(id=ctx.room_id) room.last_port = port + else: + logging.exception("Could not determine port. Likely hosting failure.") with db_session: ctx.auto_shutdown = Room.get(id=room_id).timeout ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, [])) diff --git a/WebHostLib/downloads.py b/WebHostLib/downloads.py index d9600d2d16..02ea7320ef 100644 --- a/WebHostLib/downloads.py +++ b/WebHostLib/downloads.py @@ -26,7 +26,7 @@ def download_patch(room_id, patch_id): with zipfile.ZipFile(filelike, "a") as zf: with zf.open("archipelago.json", "r") as f: manifest = json.load(f) - manifest["server"] = f"{app.config['PATCH_TARGET']}:{last_port}" if last_port else None + manifest["server"] = f"{app.config['HOST_ADDRESS']}:{last_port}" if last_port else None with zipfile.ZipFile(new_file, "w") as new_zip: for file in zf.infolist(): if file.filename == "archipelago.json": @@ -64,7 +64,7 @@ def download_slot_file(room_id, player_id: int): if slot_data.game == "Minecraft": from worlds.minecraft import mc_update_output fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apmc" - data = mc_update_output(slot_data.data, server=app.config['PATCH_TARGET'], port=room.last_port) + data = mc_update_output(slot_data.data, server=app.config['HOST_ADDRESS'], port=room.last_port) return send_file(io.BytesIO(data), as_attachment=True, download_name=fname) elif slot_data.game == "Factorio": with zipfile.ZipFile(io.BytesIO(slot_data.data)) as zf: diff --git a/WebHostLib/templates/hostRoom.html b/WebHostLib/templates/hostRoom.html index 0a4ca70edd..6f02dc0944 100644 --- a/WebHostLib/templates/hostRoom.html +++ b/WebHostLib/templates/hostRoom.html @@ -25,8 +25,8 @@ The most likely failure reason is that the multiworld is too old to be loaded now. {% elif room.last_port %} You can connect to this room by using - '/connect {{ config['PATCH_TARGET'] }}:{{ room.last_port }}' + data-tooltip="This means address/ip is {{ config['HOST_ADDRESS'] }} and port is {{ room.last_port }}."> + '/connect {{ config['HOST_ADDRESS'] }}:{{ room.last_port }}' in the client.
{% endif %} diff --git a/WebHostLib/templates/macros.html b/WebHostLib/templates/macros.html index 927ec10ffe..11e333f05e 100644 --- a/WebHostLib/templates/macros.html +++ b/WebHostLib/templates/macros.html @@ -22,7 +22,7 @@ {% for patch in room.seed.slots|list|sort(attribute="player_id") %} {{ patch.player_id }} - {{ patch.player_name }} + {{ patch.player_name }} {{ patch.game }} {% if patch.game == "Minecraft" %} diff --git a/docs/webhost configuration sample.yaml b/docs/webhost configuration sample.yaml index f007805b9e..70050b0590 100644 --- a/docs/webhost configuration sample.yaml +++ b/docs/webhost configuration sample.yaml @@ -48,5 +48,5 @@ # TODO #JSON_AS_ASCII: false -# Patch target. This is the address encoded into the patch that will be used for client auto-connect. -#PATCH_TARGET: archipelago.gg \ No newline at end of file +# Host Address. This is the address encoded into the patch that will be used for client auto-connect. +#HOST_ADDRESS: archipelago.gg From b1599c557f21d347a21635007c6f1433971de8b2 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Fri, 10 Mar 2023 07:58:00 +0100 Subject: [PATCH 042/172] The Witness: Death Link + Small bug fixes (#1515) * Fully functional DeathLink implementation. But it's always on right now :D * Death Link options. Last commit: All entity names being sent through slot_data * Tutorial Gate Close logic fix * Improved option tooltip wording * Fixed shuffle_postgame: false not excluding some locations * Link to latest stable client rather than full releases page --- worlds/witness/Options.py | 7 +++++++ worlds/witness/WitnessLogic.txt | 2 +- worlds/witness/WitnessLogicExpert.txt | 2 +- worlds/witness/WitnessLogicVanilla.txt | 2 +- worlds/witness/__init__.py | 2 +- worlds/witness/docs/setup_en.md | 6 +++--- worlds/witness/locations.py | 3 +++ worlds/witness/static_logic.py | 10 +++++----- 8 files changed, 22 insertions(+), 12 deletions(-) diff --git a/worlds/witness/Options.py b/worlds/witness/Options.py index e4e7e33faa..2047eb9ca6 100644 --- a/worlds/witness/Options.py +++ b/worlds/witness/Options.py @@ -158,6 +158,12 @@ class HintAmount(Range): default = 10 +class DeathLink(Toggle): + """If on: Whenever you fail a puzzle (with some exceptions), everyone who is also on Death Link dies. + The effect of a "death" in The Witness is a Power Surge.""" + display_name = "Death Link" + + the_witness_options: Dict[str, type] = { "puzzle_randomization": PuzzleRandomization, "shuffle_symbols": ShuffleSymbols, @@ -176,6 +182,7 @@ the_witness_options: Dict[str, type] = { "trap_percentage": TrapPercentage, "puzzle_skip_amount": PuzzleSkipAmount, "hint_amount": HintAmount, + "death_link": DeathLink, } diff --git a/worlds/witness/WitnessLogic.txt b/worlds/witness/WitnessLogic.txt index 2b3c25f23b..329cdf3ce8 100644 --- a/worlds/witness/WitnessLogic.txt +++ b/worlds/witness/WitnessLogic.txt @@ -14,7 +14,7 @@ Tutorial (Tutorial) - Outside Tutorial - 0x03629: 158005 - 0x0A3B5 (Back Left) - True - True 158006 - 0x0A3B2 (Back Right) - True - True 158007 - 0x03629 (Gate Open) - 0x002C2 & 0x0A3B5 & 0x0A3B2 - True -158008 - 0x03505 (Gate Close) - 0x2FAF6 - True +158008 - 0x03505 (Gate Close) - 0x2FAF6 & 0x03629 - True 158009 - 0x0C335 (Pillar) - True - Triangles 158010 - 0x0C373 (Patio Floor) - 0x0C335 - Dots 159512 - 0x33530 (Cloud EP) - True - True diff --git a/worlds/witness/WitnessLogicExpert.txt b/worlds/witness/WitnessLogicExpert.txt index b396e5351e..d6b202895d 100644 --- a/worlds/witness/WitnessLogicExpert.txt +++ b/worlds/witness/WitnessLogicExpert.txt @@ -14,7 +14,7 @@ Tutorial (Tutorial) - Outside Tutorial - True: 158005 - 0x0A3B5 (Back Left) - True - Dots & Full Dots 158006 - 0x0A3B2 (Back Right) - True - Dots & Full Dots 158007 - 0x03629 (Gate Open) - 0x002C2 - Symmetry & Dots -158008 - 0x03505 (Gate Close) - 0x2FAF6 - False +158008 - 0x03505 (Gate Close) - 0x2FAF6 & 0x03629 - False 158009 - 0x0C335 (Pillar) - True - Triangles 158010 - 0x0C373 (Patio Floor) - 0x0C335 - Dots 159512 - 0x33530 (Cloud EP) - True - True diff --git a/worlds/witness/WitnessLogicVanilla.txt b/worlds/witness/WitnessLogicVanilla.txt index f0e84e3ebb..9c62cc98d8 100644 --- a/worlds/witness/WitnessLogicVanilla.txt +++ b/worlds/witness/WitnessLogicVanilla.txt @@ -14,7 +14,7 @@ Tutorial (Tutorial) - Outside Tutorial - 0x03629: 158005 - 0x0A3B5 (Back Left) - True - True 158006 - 0x0A3B2 (Back Right) - True - True 158007 - 0x03629 (Gate Open) - 0x002C2 & 0x0A3B5 & 0x0A3B2 - True -158008 - 0x03505 (Gate Close) - 0x2FAF6 - True +158008 - 0x03505 (Gate Close) - 0x2FAF6 & 0x03629 - True 158009 - 0x0C335 (Pillar) - True - Triangles - True 158010 - 0x0C373 (Patio Floor) - 0x0C335 - Dots 159512 - 0x33530 (Cloud EP) - True - True diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index f6fae6bddc..358e063403 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -67,7 +67,7 @@ class WitnessWorld(World): 'progressive_item_lists': self.items.MULTI_LISTS_BY_CODE, 'obelisk_side_id_to_EPs': self.static_logic.OBELISK_SIDE_ID_TO_EP_HEXES, 'precompleted_puzzles': {int(h, 16) for h in self.player_logic.PRECOMPLETED_LOCATIONS}, - 'ep_to_name': self.static_logic.EP_ID_TO_NAME, + 'entity_to_name': self.static_logic.ENTITY_ID_TO_NAME, } def generate_early(self): diff --git a/worlds/witness/docs/setup_en.md b/worlds/witness/docs/setup_en.md index 8e85090c10..94a50846f9 100644 --- a/worlds/witness/docs/setup_en.md +++ b/worlds/witness/docs/setup_en.md @@ -3,7 +3,7 @@ ## Required Software - [The Witness for 64-bit Windows (e.g. Steam version)](https://store.steampowered.com/app/210970/The_Witness/) -- [The Witness Archipelago Randomizer](https://github.com/NewSoupVi/The-Witness-Randomizer-for-Archipelago/releases) +- [The Witness Archipelago Randomizer](https://github.com/NewSoupVi/The-Witness-Randomizer-for-Archipelago/releases/latest) ## Optional Software @@ -14,7 +14,7 @@ 1. Launch The Witness 2. Start a fresh save -3. Launch [The Witness Archipelago Randomizer](https://github.com/NewSoupVi/The-Witness-Randomizer-for-Archipelago) +3. Launch [The Witness Archipelago Randomizer](https://github.com/NewSoupVi/The-Witness-Randomizer-for-Archipelago/releases/latest) 4. Enter the Archipelago address, slot name and password 5. Press "Connect" 6. Enjoy! @@ -23,7 +23,7 @@ To continue an earlier game: 1. Launch The Witness 2. Load the save you last played this world on, if it's not the one you loaded into automatically -3. Launch [The Witness Archipelago Randomizer](https://github.com/NewSoupVi/The-Witness-Randomizer-for-Archipelago) +3. Launch [The Witness Archipelago Randomizer](https://github.com/NewSoupVi/The-Witness-Randomizer-for-Archipelago/releases/latest) 4. Press "Load Credentials" (or type them in manually) 5. Press "Connect" diff --git a/worlds/witness/locations.py b/worlds/witness/locations.py index ccdeda1b56..f9d1012cb4 100644 --- a/worlds/witness/locations.py +++ b/worlds/witness/locations.py @@ -387,6 +387,9 @@ class StaticWitnessLocations: "Mountain Floor 2 Near Row 5", "Mountain Floor 2 Far Row 6", + "Mountain Floor 2 Light Bridge Controller Near", + "Mountain Floor 2 Light Bridge Controller Far", + "Mountain Bottom Floor Yellow Bridge EP", "Mountain Bottom Floor Blue Bridge EP", "Mountain Floor 2 Pink Bridge EP", diff --git a/worlds/witness/static_logic.py b/worlds/witness/static_logic.py index 4311a84fa1..f395613b91 100644 --- a/worlds/witness/static_logic.py +++ b/worlds/witness/static_logic.py @@ -81,8 +81,6 @@ class StaticWitnessLogicObj: full_check_name = check_name elif "EP" in check_name: location_type = "EP" - - self.EP_ID_TO_NAME[check_hex] = full_check_name else: location_type = "General" @@ -114,6 +112,8 @@ class StaticWitnessLogicObj: "panelType": location_type } + self.ENTITY_ID_TO_NAME[check_hex] = full_check_name + self.CHECKS_BY_NAME[self.CHECKS_BY_HEX[check_hex]["checkName"]] = self.CHECKS_BY_HEX[check_hex] self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[check_hex] = requirement @@ -132,7 +132,7 @@ class StaticWitnessLogicObj: self.EP_TO_OBELISK_SIDE = dict() - self.EP_ID_TO_NAME = dict() + self.ENTITY_ID_TO_NAME = dict() self.read_logic_file(file_path) @@ -159,7 +159,7 @@ class StaticWitnessLogic: EP_TO_OBELISK_SIDE = dict() - EP_ID_TO_NAME = dict() + ENTITY_ID_TO_NAME = dict() def parse_items(self): """ @@ -235,4 +235,4 @@ class StaticWitnessLogic: self.EP_TO_OBELISK_SIDE.update(self.sigma_normal.EP_TO_OBELISK_SIDE) - self.EP_ID_TO_NAME.update(self.sigma_normal.EP_ID_TO_NAME) \ No newline at end of file + self.ENTITY_ID_TO_NAME.update(self.sigma_normal.ENTITY_ID_TO_NAME) \ No newline at end of file From 4068ba2f15903fe3aa62cbd6ef89858351dfd8d0 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Fri, 10 Mar 2023 00:59:47 -0600 Subject: [PATCH 043/172] LTTP: add option `__doc__`s (#1521) * LTTP: add option `__doc__`s * review comments --- worlds/alttp/Options.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index b1cfbc674e..dd007954e1 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -107,10 +107,14 @@ class Crystals(Range): class CrystalsTower(Crystals): + """Number of crystals needed to open Ganon's Tower""" + display_name = "Crystals for GT" default = 7 class CrystalsGanon(Crystals): + """Number of crystals needed to damage Ganon""" + display_name = "Crystals for Ganon" default = 7 @@ -121,12 +125,15 @@ class TriforcePieces(Range): class ShopItemSlots(Range): + """Number of slots in all shops available to have items from the multiworld""" + display_name = "Available Shop Slots" range_start = 0 range_end = 30 class ShopPriceModifier(Range): """Percentage modifier for shuffled item prices in shops""" + display_name = "Shop Price Cost Percent" range_start = 0 default = 100 range_end = 400 @@ -144,7 +151,7 @@ class LTTPBosses(PlandoBosses): Full chooses 3 bosses at random to be placed twice instead of Lanmolas, Moldorm, and Helmasaur. Chaos allows any boss to appear any number of times. Singularity places a single boss in as many places as possible, and a second boss in any remaining locations. - Supports plando placement. Formatting here: https://archipelago.gg/tutorial/A%20Link%20to%20the%20Past/plando/en""" + Supports plando placement.""" display_name = "Boss Shuffle" option_none = 0 option_basic = 1 @@ -202,6 +209,7 @@ class Enemies(Choice): class Progressive(Choice): + """How item types that have multiple tiers (armor, bows, gloves, shields, and swords) should be rewarded""" display_name = "Progressive Items" option_off = 0 option_grouped_random = 1 @@ -305,22 +313,27 @@ class Palette(Choice): class OWPalette(Palette): + """The type of palette shuffle to use for the overworld""" display_name = "Overworld Palette" class UWPalette(Palette): + """The type of palette shuffle to use for the underworld (caves, dungeons, etc.)""" display_name = "Underworld Palette" class HUDPalette(Palette): + """The type of palette shuffle to use for the HUD""" display_name = "Menu Palette" class SwordPalette(Palette): + """The type of palette shuffle to use for the sword""" display_name = "Sword Palette" class ShieldPalette(Palette): + """The type of palette shuffle to use for the shield""" display_name = "Shield Palette" @@ -329,6 +342,7 @@ class ShieldPalette(Palette): class HeartBeep(Choice): + """How quickly the heart beep sound effect will play""" display_name = "Heart Beep Rate" option_normal = 0 option_double = 1 @@ -338,6 +352,7 @@ class HeartBeep(Choice): class HeartColor(Choice): + """The color of hearts in the HUD""" display_name = "Heart Color" option_red = 0 option_blue = 1 @@ -346,10 +361,12 @@ class HeartColor(Choice): class QuickSwap(DefaultOnToggle): + """Allows you to quickly swap items while playing with L/R""" display_name = "L/R Quickswapping" class MenuSpeed(Choice): + """How quickly the menu appears/disappears""" display_name = "Menu Speed" option_normal = 0 option_instant = 1, @@ -360,14 +377,17 @@ class MenuSpeed(Choice): class Music(DefaultOnToggle): + """Whether background music will play in game""" display_name = "Play music" class ReduceFlashing(DefaultOnToggle): + """Reduces flashing for certain scenes such as the Misery Mire and Ganon's Tower opening cutscenes""" display_name = "Reduce Screen Flashes" class TriforceHud(Choice): + """When and how the triforce hunt HUD should display""" display_name = "Display Method for Triforce Hunt" option_normal = 0 option_hide_goal = 1 @@ -375,6 +395,11 @@ class TriforceHud(Choice): option_hide_both = 3 +class GlitchBoots(DefaultOnToggle): + """If this is enabled, the player will start with Pegasus Boots when playing with overworld glitches or harder logic.""" + display_name = "Glitched Starting Boots" + + class BeemizerRange(Range): value: int range_start = 0 @@ -437,7 +462,7 @@ alttp_options: typing.Dict[str, type(Option)] = { "music": Music, "reduceflashing": ReduceFlashing, "triforcehud": TriforceHud, - "glitch_boots": DefaultOnToggle, + "glitch_boots": GlitchBoots, "beemizer_total_chance": BeemizerTotalChance, "beemizer_trap_chance": BeemizerTrapChance, "death_link": DeathLink, From 5fef41eb976f36fccbe5c0c486a6c490be8b55aa Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Fri, 10 Mar 2023 17:21:17 -0500 Subject: [PATCH 044/172] [WebHost] Make site header mobile-friendly (#1523) --- WebHostLib/static/assets/baseHeader.js | 18 +++++ .../button-images/hamburger-menu-icon.png | Bin 0 -> 5659 bytes WebHostLib/static/styles/themes/base.css | 63 +++++++++++++++++- WebHostLib/templates/header/baseHeader.html | 13 ++++ 4 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 WebHostLib/static/assets/baseHeader.js create mode 100644 WebHostLib/static/static/button-images/hamburger-menu-icon.png diff --git a/WebHostLib/static/assets/baseHeader.js b/WebHostLib/static/assets/baseHeader.js new file mode 100644 index 0000000000..b8ee82dd63 --- /dev/null +++ b/WebHostLib/static/assets/baseHeader.js @@ -0,0 +1,18 @@ +window.addEventListener('load', () => { + const menuButton = document.getElementById('base-header-mobile-menu-button'); + const mobileMenu = document.getElementById('base-header-mobile-menu'); + + menuButton.addEventListener('click', (evt) => { + evt.preventDefault(); + + if (!mobileMenu.style.display || mobileMenu.style.display === 'none') { + return mobileMenu.style.display = 'flex'; + } + + mobileMenu.style.display = 'none'; + }); + + window.addEventListener('resize', () => { + mobileMenu.style.display = 'none'; + }); +}); diff --git a/WebHostLib/static/static/button-images/hamburger-menu-icon.png b/WebHostLib/static/static/button-images/hamburger-menu-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f1c96316358da657e3b81f484f0ebe58f212a5c3 GIT binary patch literal 5659 zcmdT{X;@QNx86vF3KeeERuohS2T&OjP|$=Sr67PZ!l zSvx4GAXE?)q@rRf2o)JZ7_1G0AfO0DK*+Zf>d@b*V^k{?_N3N z=wQ84ZoM1;U?s`M(g^^mDfoQ*u?$>st3>YtuzVeDj~mO)-j3i;57YCb(DzgI*kKV+ z4uA=f9pUF6LS?D$rv}o(O)(SKZ(>ww6jO|=p*_w%!h#w^vx#F+o#Pz#_{W9#<0%-T znVbom03C!;S$?YQu+VTOfo+PB_$9#K;$?F+nU=1ObbUiHXsR*``Nl1Y-5^csv$ofHg4Cg&Mld*l?B~TQ{7k zE_Six!;;GMXV4;8GD{P-oEs{zhFa`9K|>wY~j6XAu_mmqeJXUD5C$ z{yfyb1oS3v=APIHD%Oe0q(?FQsk@@7;VgB@S-{;077VH%i)slUQ;Y%5V4E%ue(-zr z@dSMwL0?}NXNbeSRko*7XaTYRr?L@YJ02<%-q(ikp!l);{y+L}51b&@UJ43A^B*I6 zXJbJRr88h~Fo$iDVBd*I78Z^SdH^jH%9u{pU#gOJSs3Z#jf`{+^z4k! znMNJ?Zv8)09XhyF0_?f?F)P6xWtO3iAu3y3#1Dpm*Nh3GArh zfnv!UPZEi^Mxjw+knatr!KxZa0+fi{-pOE+I2`u9o!mc^i5o}NL;@#*4UNg7Gh!u6 zOWov5ja+(LdJUyX+DKJZ0w#i=zc>O@Oay~YiSnmXBtbxTOHw91fEDA%pqd9l9GPOw z0|IC;)>u{8p|rqoDnr#k4+oq7FCJrpsL5Wa~-;XN*Z zc1vrJV<1^!-?9~adCPayaEK%ZWV2P>$0{IW9-u)?e1HW2HlK(((V}g+drc!T;G4Ta zYvkoKN%xk`?q02&ne%$9PC&tA$hMbFiiY$Xeaf_oy;aWNb-lXs80%Yl;UvbXf@&X{ zUBEoLp4-E(9b(=T=C#!f9kjI_P2b0pw$cnqvrb*i;o*~~SBIN?VYp#;uaTCz`g&^P zyzXbeddHmi8dzA|a3wR+Gvo03-_?45uDIoYYjvr+^NUjvXA2JN$rXMaluFl)My8q<{;+=}yBNe%VayJa=uulG&OExoCyS^Td%%L$#M9K5P zT66PWusJ3Jc_F_IKu*w<03dBO{8w&Q?%D z&wT$%c`nTKD|z8h1N@`}6Lg}!w_2D;C)bIpsrlPI^Ycjx8oXRe2|ZSydpj;n7E~2o zCxvU|jCo$ts_7Z6nz+y?Q+4!ZW3$W94@d^bPAgWOdn_t@1z6M`*Q!AU`%(GY@cF@W z#O+S4xfdVYCpl>1(eL#~?n;e(j79N@3Kf$x<}Sfi1T8cnS1_wDBMoGPUTBP#a7;8h zb0w#g>@j;eUTm9#1fn;p9U7!SgK?q2t&Cq+-60nuQ+s%}h2-6z@(W1$-OHZgd+{7` zTyA>Xr%ceL<4vCI_3*hAr%+1>T>Q<-{v&XN(jur^5V{!uw0KWVI1)S~ADN5?p;n5h zo$%V2?ZPIrHB%#IFZ-W#YzJLUgXoR!RctIqAarX>aL zR#cy$l#FiyDVOb55I|_+K?SfpI~a+*ytHPB+C4H|4sfSdOqj@~Ec=)M)_l<_RRqLp z|HA^CQ;MX&kosr?kW(E7j==oir?MUGxfuU3@YRV?CWW_4Qsr5pylX!Ja2N9?rzRuR zt1Mm)YIQ1;^t5Vxgb1j&S#)63{00$@)HuDwf%>3JupGp!jXKQB9lJrw?tONPiB3If zi2QW(GH}FKZNYpZiob)CSIWcy-=M(KbmEkbkj!c7+@=U{-_Ih2x(ojJ;l00;JM}*W zxSz|iW*%^?`>Ue2a(mkHWr5N|FBCgZWN#jB*A2Din6M99NP*P&Zv_h_y!%yUUSe1) zbLMA6`GWek)1LsPE-lzt^Cvf%t(rn=62k_wxQX*ITD;2N{h{4lac5ZajqbJDEyY6P zQ-YDZP14|#r@^RV#~h=%2!b}=-Q-zPl63kqAQNN8Jy6nI+HG{N3^x29q`G^52#2Lo zrl(aihnjB2Jv=PE+>~_s;sa0815KXcoENB18LVI%&i`QU;yr!lYWp77o5>tOAyTHn z)9+3I_5Mv-vp#4{T<=U->f&$p@xu$y*jCc`YKzVQT%f6CTHhj$CNJwLvXYxaw?1oY zpA2|*MYnY}@K^^I)G{@N;O;Mv-XRHIEZx}6-a@hE;whrcw8 zmp5|bJ3$2c6rm%$%L?11>XYv*Ag-vF9Rrr5Uu8PON-nOlJr>X9BG%}3tKOZUJXDQa zgA^WHHB)@hdxHz{grEUwQMj%T9v>Ry4hsLKsgIb-)QdA zXLj!^$Z?h^onu10>kH~17uZgYL#j&dtutayleCG^iqO`CBd|k-9kiXmmp#N$8I9r# zJ3RcFi(K_ZcI)uvv6 zIX4p2|5*O5;3Ra;q$w^$+*Scqr^nnA(q<>sgiq%JAi3f45q^nh`=U+7*}hni@@Wrw z@=JAY@i&m0_Zp)rp$N}Y+{tb;MyTCV?{9$=c9AS*IOn4XwnM@P=dI8~5Xa10%>tHiY(TOQ|AsK!qI|{fD*T# zq_+chaz5f3yio=yHN$c&3_VPx>U9w00InpHdy{gvyd5Hv``YMf8S`VLWf&wmej?&f5J z^}zz9x~%!NK@D9rj>H=!=m1|wA|&kNuJii`@KZ^Rs8??`#E9BH`^jJ6?Y?db;tHFY z(c40=cZ7}eKnw49C3Y`gX7*%2Dbs|zHum*KQ&3*Fk@6yM)|KoLSyZG&g1xKFDam8@_KyD|{YhM>Q222RjuIwz6f^PMskI!t3!Txx zSN>YAzy;;i4aA}MVc8$Vh5BA7+Tod>g{n9h#vOi|;8oexLcv#PCP_~`&YWFr zwK1nx5wKeL9Dxmb_nfjUIHil=vjlCsZ<+0BEo~TqGhMcT=eI)!nEaN2wvilGGIu6D z7CWxyBotCQpI1tWfx`H?GfcGe-miRF_w&4wJe$cdbf{4YUW`3VQeQr#sxp3Oz$G)< zK;w8Zv<*b_NRxepm9pTIi3HRi#@pwiePP|y) zv9)x*DLK=#4ft-$wVezWC5~tdZ;>Vb_$LKyk?egq=KbMCzrFe~@YT*HSHwTnzq}AX zTxcyG-PlH%V-8MZ#jbSwc~faKyaMQ2^ErCs;b;|IZ>)IJv25vjmrB#jGMtZ@z-(W? z4yaf9kwfu}D(&7xihg7aCyj&iB<~;uzeXPtU9dHpdK8J5bn^W^EA;$Q?`sT$@@Y5? zuJJ87A-J=&)^204TfzsLH(sjzd&j-MC&E@6M)kNFoP&G9kea=cR0EVr0iy-WfKuk0 zDHq4LVbn?{V=p>VlP5^(lhay zti$cb`Jxz9njB&Pe0>XHV@^JK zLraw<(`YHl%x(0(VQL>7Z>cZJ=Vw(;zW=2CS(c1@YC=WRzuwQ>962TQ~qO`&c+p@3!)Vfsq424+m@7*UGL{! z?oy?Dq-K}EKs1#!qnYDz`7QZSqVF(Fxn%9aS4Jfw>28 + {% endblock %} {% block header %} @@ -16,5 +17,17 @@ f.a.q. discord
+
+ + Menu + +
+ {% endblock %} From f3ca0a21c95cac1a4ae532f9511d7250dbca207b Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Fri, 10 Mar 2023 18:14:44 -0600 Subject: [PATCH 045/172] Docs: Add an option api doc (#1181) * write up an option api doc * address reviews * some clarification * add note about using schema * Add ItemSet and formatting * bulletpoint option defining Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> * split random description to new sentence Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> * use inclusive and parallel language for example Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> * changes from review * commas Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> * capitalize Toggle Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> * the sliver conventions --------- Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> --- docs/options api.md | 188 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 docs/options api.md diff --git a/docs/options api.md b/docs/options api.md new file mode 100644 index 0000000000..a1407f2ceb --- /dev/null +++ b/docs/options api.md @@ -0,0 +1,188 @@ +# Archipelago Options API + +This document covers some of the generic options available using Archipelago's options handling system. + +For more information on where these options go in your world please refer to: + - [world api.md](/docs/world%20api.md) + +Archipelago will be abbreviated as "AP" from now on. + +## Option Definitions +Option parsing in AP is done using different Option classes. For each option you would like to have in your game, you +need to create: +- A new option class with a docstring detailing what the option will do to your user. +- A `display_name` to be displayed on the webhost. +- A new entry in the `option_definitions` dict for your World. +By style and convention, the internal names should be snake_case. If the option supports having multiple sub_options +such as Choice options, these can be defined with `option_my_sub_option`, where the preceding `option_` is required and +stripped for users, so will show as `my_sub_option` in yaml files and if `auto_display_name` is True `My Sub Option` +on the webhost. All options support `random` as a generic option. `random` chooses from any of the available +values for that option, and is reserved by AP. You can set this as your default value but you cannot define your own +new `option_random`. + +### Option Creation +As an example, suppose we want an option that lets the user start their game with a sword in their inventory. Let's +create our option class (with a docstring), give it a `display_name`, and add it to a dictionary that keeps track of our +options: + +```python +# Options.py +class StartingSword(Toggle): + """Adds a sword to your starting inventory.""" + display_name = "Start With Sword" + + +example_options = { + "starting_sword": StartingSword +} +``` + +This will create a `Toggle` option, internally called `starting_sword`. To then submit this to the multiworld, we add it +to our world's `__init__.py`: + +```python +from worlds.AutoWorld import World +from .Options import options + + +class ExampleWorld(World): + option_definitions = options +``` + +### Option Checking +Options are parsed by `Generate.py` before the worlds are created, and then the option classes are created shortly after +world instantiation. These are created as attributes on the MultiWorld and can be accessed with +`self.multiworld.my_option_name[self.player]`. This is the option class, which supports direct comparison methods to +relevant objects (like comparing a Toggle class to a `bool`). If you need to access the option result directly, this is +the option class's `value` attribute. For our example above we can do a simple check: +```python +if self.multiworld.starting_sword[self.player]: + do_some_things() +``` + +or if I need a boolean object, such as in my slot_data I can access it as: +```python +start_with_sword = bool(self.multiworld.starting_sword[self.player].value) +``` + +## Generic Option Classes +These options are generically available to every game automatically, but can be overridden for slightly different +behavior, if desired. See `worlds/soe/Options.py` for an example. + +### Accessibility +Sets rules for availability of locations for the player. `Items` is for all items available but not necessarily all +locations, such as self-locking keys, but needs to be set by the world for this to be different from locations access. + +### ProgressionBalancing +Algorithm for moving progression items into earlier spheres to make the gameplay experience a bit smoother. Can be +overridden if you want a different default value. + +### LocalItems +Forces the players' items local to their world. + +### NonLocalItems +Forces the players' items outside their world. + +### StartInventory +Allows the player to define a dictionary of starting items with item name and quantity. + +### StartHints +Gives the player starting hints for where the items defined here are. + +### StartLocationHints +Gives the player starting hints for the items on locations defined here. + +### ExcludeLocations +Marks locations given here as `LocationProgressType.Excluded` so that progression items can't be placed on them. + +### PriorityLocations +Marks locations given here as `LocationProgressType.Priority` forcing progression items on them. + +### ItemLinks +Allows users to share their item pool with other players. Currently item links are per game. A link of one game between +two players will combine their items in the link into a single item, which then gets replaced with `World.create_filler()`. + +## Basic Option Classes +### Toggle +The example above. This simply has 0 and 1 as its available results with 0 (false) being the default value. Cannot be +compared to strings but can be directly compared to True and False. + +### DefaultOnToggle +Like Toggle, but 1 (true) is the default value. + +### Choice +A numeric option allowing you to define different sub options. Values are stored as integers, but you can also do +comparison methods with the class and strings, so if you have an `option_early_sword`, this can be compared with: +```python +if self.multiworld.sword_availability[self.player] == "early_sword": + do_early_sword_things() +``` + +or: +```python +from .Options import SwordAvailability + +if self.multiworld.sword_availability[self.player] == SwordAvailability.option_early_sword: + do_early_sword_things() +``` + +### Range +A numeric option allowing a variety of integers including the endpoints. Has a default `range_start` of 0 and default +`range_end` of 1. Allows for negative values as well. This will always be an integer and has no methods for string +comparisons. + +### SpecialRange +Like range but also allows you to define a dictionary of special names the user can use to equate to a specific value. +For example: +```python +special_range_names: { + "normal": 20, + "extreme": 99, +} +``` + +will let users use the names "normal" or "extreme" in their options selections, but will still return those as integers +to you. Useful if you want special handling regarding those specified values. + +## More Advanced Options +### FreeText +This is an option that allows the user to enter any possible string value. Can only be compared with strings, and has +no validation step, so if this needs to be validated, you can either add a validation step to the option class or +within the world. + +### TextChoice +Like choice allows you to predetermine options and has all of the same comparison methods and handling. Also accepts any +user defined string as a valid option, so will either need to be validated by adding a validation step to the option +class or within world, if necessary. Value for this class is `Union[str, int]` so if you need the value at a specified +point, `self.multiworld.my_option[self.player].current_key` will always return a string. + +### PlandoBosses +An option specifically built for handling boss rando, if your game can use it. Is a subclass of TextChoice so supports +everything it does, as well as having multiple validation steps to automatically support boss plando from users. If +using this class, you must define `bosses`, a set of valid boss names, and `locations`, a set of valid boss location +names, and `def can_place_boss`, which passes a boss and location, allowing you to check if that placement is valid for +your game. When this function is called, `bosses`, `locations`, and the passed strings will all be lowercase. There is +also a `duplicate_bosses` attribute allowing you to define if a boss can be placed multiple times in your world. False +by default, and will reject duplicate boss names from the user. For an example of using this class, refer to +`worlds.alttp.options.py` + +### OptionDict +This option returns a dictionary. Setting a default here is recommended as it will output the dictionary to the +template. If you set a [Schema](https://pypi.org/project/schema/) on the class with `schema = Schema()`, then the +options system will automatically validate the user supplied data against the schema to ensure it's in the correct +format. + +### ItemDict +Like OptionDict, except this will verify that every key in the dictionary is a valid name for an item for your world. + +### OptionList +This option defines a List, where the user can add any number of strings to said list, allowing duplicate values. You +can define a set of keys in `valid_keys`, and a default list if you want certain options to be available without editing +for this. If `valid_keys_casefold` is true, the verification will be case-insensitive; `verify_item_name` will check +that each value is a valid item name; and`verify_location_name` will check that each value is a valid location name. + +### OptionSet +Like OptionList, but returns a set, preventing duplicates. + +### ItemSet +Like OptionSet, but will verify that all the items in the set are a valid name for an item for your world. From 0cfdc973f66010b051db56bbebb9c70b649ae497 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sat, 11 Mar 2023 10:09:09 +0100 Subject: [PATCH 046/172] The Witness - Expert logic bug (could lead to broken seeds) (#1525) --- worlds/witness/WitnessLogicExpert.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/witness/WitnessLogicExpert.txt b/worlds/witness/WitnessLogicExpert.txt index d6b202895d..c6cc59605a 100644 --- a/worlds/witness/WitnessLogicExpert.txt +++ b/worlds/witness/WitnessLogicExpert.txt @@ -270,7 +270,7 @@ Door - 0x0368A (Stairs) - 0x03677 159413 - 0x00614 (Lift EP) - 0x275FF & 0x03675 - True Quarry Boathouse (Quarry Boathouse) - Quarry - True - Quarry Boathouse Upper Front - 0x03852 - Quarry Boathouse Behind Staircase - 0x2769B: -158146 - 0x034D4 (Intro Left) - True - Stars & Stars + Same Colored Symbol + Eraser +158146 - 0x034D4 (Intro Left) - True - Stars & Stars + Same Colored Symbol & Eraser 158147 - 0x021D5 (Intro Right) - True - Shapers & Eraser 158148 - 0x03852 (Ramp Height Control) - 0x034D4 & 0x021D5 - Rotated Shapers 158166 - 0x17CA6 (Boat Spawn) - True - Boat @@ -1121,4 +1121,4 @@ Obelisks (EPs) - Entry - True: 159752 - 0xFFE52 (Town Obelisk Side 3) - 0x33857 & 0x33879 & 0x03C19 - True 159753 - 0xFFE53 (Town Obelisk Side 4) - 0x28B30 & 0x035C9 - True 159754 - 0xFFE54 (Town Obelisk Side 5) - 0x03335 & 0x03412 & 0x038A6 & 0x038AA & 0x03E3F & 0x03E40 & 0x28B8E - True -159755 - 0xFFE55 (Town Obelisk Side 6) - 0x28B91 & 0x03BCE & 0x03BCF & 0x03BD1 & 0x339B6 & 0x33A20 & 0x33A29 & 0x33A2A & 0x33B06 - True \ No newline at end of file +159755 - 0xFFE55 (Town Obelisk Side 6) - 0x28B91 & 0x03BCE & 0x03BCF & 0x03BD1 & 0x339B6 & 0x33A20 & 0x33A29 & 0x33A2A & 0x33B06 - True From 04e6a8eae87b9301f913cdd7a3f2e9d13f133d9b Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Thu, 9 Mar 2023 18:15:58 -0600 Subject: [PATCH 047/172] FFR: add option `__doc__`s --- worlds/ff1/Options.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/worlds/ff1/Options.py b/worlds/ff1/Options.py index 2ab4b33622..0993d103d5 100644 --- a/worlds/ff1/Options.py +++ b/worlds/ff1/Options.py @@ -4,14 +4,17 @@ from Options import OptionDict class Locations(OptionDict): + """to roll settings go to https://finalfantasyrandomizer.com/""" display_name = "locations" class Items(OptionDict): + """to roll settings go to https://finalfantasyrandomizer.com/""" display_name = "items" class Rules(OptionDict): + """to roll settings go to https://finalfantasyrandomizer.com/""" display_name = "rules" From 426a81a065e3401d6be15d7f8b1abf25cfec9549 Mon Sep 17 00:00:00 2001 From: el-u <109771707+el-u@users.noreply.github.com> Date: Sat, 11 Mar 2023 20:15:30 +0100 Subject: [PATCH 048/172] oot/alttp: fix bugs found through MMBN3 testing (#1527) --- worlds/alttp/__init__.py | 16 ++++++---------- worlds/oot/HintList.py | 2 +- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 78f12e5119..8ca82d43d5 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -20,7 +20,7 @@ from .Client import ALTTPSNIClient from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \ get_hash_string, get_base_rom_path, LttPDeltaPatch from .Rules import set_rules -from .Shops import create_shops, ShopSlotFill, ShopType, price_rate_display, price_type_display_name +from .Shops import create_shops, Shop, ShopSlotFill, ShopType, price_rate_display, price_type_display_name from .SubClasses import ALttPItem, LTTPRegionType from worlds.AutoWorld import World, WebWorld, LogicMixin from .StateHelpers import can_buy_unlimited @@ -674,11 +674,7 @@ class ALTTPWorld(World): f'\n\nBosses{(f" ({self.multiworld.get_player_name(self.player)})" if self.multiworld.players > 1 else "")}:\n') spoiler_handle.write(' ' + '\n '.join([f'{x}: {y}' for x, y in bossmap.items()])) - def build_shop_info() -> typing.Dict: - shop = self.multiworld.shops[self.player] - if not shop.custom: - return None - + def build_shop_info(shop: Shop) -> typing.Dict[str, str]: shop_data = { "location": str(shop.region), "type": "Take Any" if shop.type == ShopType.TakeAny else "Shop" @@ -704,12 +700,12 @@ class ALTTPWorld(World): return shop_data - shop_data = build_shop_info() - if shop_data is not None: + if shop_info := [build_shop_info(shop) for shop in self.multiworld.shops if shop.custom]: spoiler_handle.write('\n\nShops:\n\n') - spoiler_handle.write(''.join("{} [{}]\n {}".format(shop_data['location'], shop_data['type'], "\n ".join( + for shop_data in shop_info: + spoiler_handle.write("{} [{}]\n {}\n".format(shop_data['location'], shop_data['type'], "\n ".join( item for item in [shop_data.get('item_0', None), shop_data.get('item_1', None), shop_data.get('item_2', None)] if - item)))) + item))) def get_filler_item_name(self) -> str: if self.multiworld.goal[self.player] == "icerodhunt": diff --git a/worlds/oot/HintList.py b/worlds/oot/HintList.py index 556e165184..b0f20858e7 100644 --- a/worlds/oot/HintList.py +++ b/worlds/oot/HintList.py @@ -50,7 +50,7 @@ def getHint(item, clearer_hint=False): return Hint(item, clearText, hintType) else: return Hint(item, textOptions, hintType) - elif type(item) is str: + elif isinstance(item, str): return Hint(item, item, 'generic') else: # is an Item return Hint(item.name, item.hint_text, 'item') From 54cce4c392992a90719d9cf3477e14cc61305f98 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sun, 12 Mar 2023 05:03:48 -0500 Subject: [PATCH 049/172] LTTP: fix ice rod hunt boss shuffle (#1529) --- worlds/alttp/ItemPool.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index 80028c3fb3..7fd93ab93e 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -9,7 +9,7 @@ from worlds.alttp.Dungeons import get_dungeon_item_pool_player from worlds.alttp.EntranceShuffle import connect_entrance from Fill import FillError from worlds.alttp.Items import ItemFactory, GetBeemizerItem -from worlds.alttp.Options import smallkey_shuffle, compass_shuffle, bigkey_shuffle, map_shuffle +from worlds.alttp.Options import smallkey_shuffle, compass_shuffle, bigkey_shuffle, map_shuffle, LTTPBosses from .StateHelpers import has_triforce_pieces, has_melee_weapon # This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space. @@ -250,8 +250,10 @@ def generate_itempool(world): world.push_item(loc, ItemFactory('Triforce Piece', player), False) world.treasure_hunt_count[player] = 1 if world.boss_shuffle[player] != 'none': - if 'turtle rock-' not in world.boss_shuffle[player]: - world.boss_shuffle[player] = f'Turtle Rock-Trinexx;{world.boss_shuffle[player]}' + if isinstance(world.boss_shuffle[player].value, str) and 'turtle rock-' not in world.boss_shuffle[player].value: + world.boss_shuffle[player] = LTTPBosses.from_text(f'Turtle Rock-Trinexx;{world.boss_shuffle[player].current_key}') + elif isinstance(world.boss_shuffle[player].value, int): + world.boss_shuffle[player] = LTTPBosses.from_text(f'Turtle Rock-Trinexx;{world.boss_shuffle[player].current_key}') else: logging.warning(f'Cannot guarantee that Trinexx is the boss of Turtle Rock for player {player}') loc.event = True From 39563cc347c5b89b445eb6916b0f18d962813f9e Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 12 Mar 2023 12:38:13 +0100 Subject: [PATCH 050/172] WebHost: add game and Factorio to multitracker (#1526) Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- .../templates/multiFactorioTracker.html | 44 +++++++++ WebHostLib/templates/multiTracker.html | 8 ++ .../templates/multiTrackerNavigation.html | 7 +- WebHostLib/tracker.py | 94 ++++++++++++++----- 4 files changed, 126 insertions(+), 27 deletions(-) create mode 100644 WebHostLib/templates/multiFactorioTracker.html diff --git a/WebHostLib/templates/multiFactorioTracker.html b/WebHostLib/templates/multiFactorioTracker.html new file mode 100644 index 0000000000..bc0a977ab6 --- /dev/null +++ b/WebHostLib/templates/multiFactorioTracker.html @@ -0,0 +1,44 @@ +{% extends "multiTracker.html" %} +{% block custom_table_headers %} + + Logistic Science Pack + + + Military Science Pack + + + Chemical Science Pack + + + Production Science Pack + + + Utility Science Pack + + + Space Science Pack + +{% endblock %} +{% block custom_table_row scoped %} +{% if games[player] == "Factorio" %} +{% if inventory[team][player][131161] or inventory[team][player][131281] %}✔{% endif %} +{% if inventory[team][player][131172] or inventory[team][player][131281] > 1%}✔{% endif %} +{% if inventory[team][player][131195] or inventory[team][player][131281] > 2%}✔{% endif %} +{% if inventory[team][player][131240] or inventory[team][player][131281] > 3%}✔{% endif %} +{% if inventory[team][player][131240] or inventory[team][player][131281] > 4%}✔{% endif %} +{% if inventory[team][player][131220] or inventory[team][player][131281] > 5%}✔{% endif %} +{% else %} +❌ +❌ +❌ +❌ +❌ +❌ +{% endif %} +{% endblock%} diff --git a/WebHostLib/templates/multiTracker.html b/WebHostLib/templates/multiTracker.html index 3c6b09d173..c6defff00b 100644 --- a/WebHostLib/templates/multiTracker.html +++ b/WebHostLib/templates/multiTracker.html @@ -30,6 +30,10 @@ # Name + Game + {% block custom_table_headers %} + {# implement this block in game-specific multi trackers #} + {% endblock %} Checks % Last
Activity @@ -41,6 +45,10 @@ {{ loop.index }} {{ player_names[(team, loop.index)]|e }} + {{ games[player] }} + {% block custom_table_row scoped %} + {# implement this block in game-specific multi trackers #} + {% endblock %} {{ checks["Total"] }}/{{ checks_in_area[player]["Total"] }} {{ percent_total_checks_done[team][player] }} {%- if activity_timers[(team, player)] -%} diff --git a/WebHostLib/templates/multiTrackerNavigation.html b/WebHostLib/templates/multiTrackerNavigation.html index c6498fc68d..f712f33679 100644 --- a/WebHostLib/templates/multiTrackerNavigation.html +++ b/WebHostLib/templates/multiTrackerNavigation.html @@ -2,11 +2,8 @@
{% for enabled_tracker in enabled_multiworld_trackers %} {% set tracker_url = url_for(enabled_tracker.endpoint, tracker=room.tracker) %} - {%- if enabled_tracker.current -%} - {{ enabled_tracker.name }} - {%- else -%} - {{ enabled_tracker.name }} - {%- endif -%} + {{ enabled_tracker.name }} {% endfor %}
{%- endif -%} diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index c2a44324b6..8d9311fc8f 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -666,7 +666,6 @@ def __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[in } for item_name, item_id in multi_items.items(): base_name = item_name.split()[-1].lower() - count = inventory[item_id] display_data[base_name+"_count"] = inventory[item_id] # Gather dungeon locations @@ -1323,24 +1322,22 @@ def get_enabled_multiworld_trackers(room: Room, current: str): "current": current == "Generic" } ] - - if any(slot.game == "A Link to the Past" for slot in room.seed.slots) or current == "A Link to the Past": - enabled.append({ - "name": "A Link to the Past", - "endpoint": "get_LttP_multiworld_tracker", - "current": current == "A Link to the Past"} - ) - + for game_name, endpoint in multi_trackers.items(): + if any(slot.game == game_name for slot in room.seed.slots) or current == game_name: + enabled.append({ + "name": game_name, + "endpoint": endpoint.__name__, + "current": current == game_name} + ) return enabled -@app.route('/tracker/') -@cache.memoize(timeout=60) # multisave is currently created at most every minute -def get_multiworld_tracker(tracker: UUID): +def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[str, typing.Any]]: room: Room = Room.get(tracker=tracker) if not room: - abort(404) - locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \ + return None + + locations, names, use_door_tracker, checks_in_area, player_location_to_area, \ precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \ get_static_room_data(room) @@ -1366,7 +1363,9 @@ def get_multiworld_tracker(tracker: UUID): continue player_locations = locations[player] checks_done[team][player]["Total"] = sum(1 for loc in locations_checked if loc in player_locations) - percent_total_checks_done[team][player] = int(checks_done[team][player]["Total"] / seed_checks_in_area[player]["Total"] * 100) if seed_checks_in_area[player]["Total"] else 100 + percent_total_checks_done[team][player] = int(checks_done[team][player]["Total"] / + checks_in_area[player]["Total"] * 100) \ + if checks_in_area[player]["Total"] else 100 activity_timers = {} now = datetime.datetime.utcnow() @@ -1386,15 +1385,61 @@ def get_multiworld_tracker(tracker: UUID): for (team, player), data in multisave.get("video", []): video[(team, player)] = data - enabled_multiworld_trackers = get_enabled_multiworld_trackers(room, "Generic") - - return render_template("multiTracker.html", player_names=player_names, room=room, checks_done=checks_done, - percent_total_checks_done=percent_total_checks_done, checks_in_area=seed_checks_in_area, - activity_timers=activity_timers, video=video, hints=hints, - long_player_names=long_player_names, enabled_multiworld_trackers=enabled_multiworld_trackers) + return dict(player_names=player_names, room=room, checks_done=checks_done, + percent_total_checks_done=percent_total_checks_done, checks_in_area=checks_in_area, + activity_timers=activity_timers, video=video, hints=hints, + long_player_names=long_player_names, + multisave=multisave, precollected_items=precollected_items, groups=groups, + locations=locations, games=games) -@app.route('/tracker//lttp') +def _get_inventory_data(data: typing.Dict[str, typing.Any]) -> typing.Dict[int, typing.Dict[int, int]]: + inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in team_data} + for teamnumber, team_data in data["checks_done"].items()} + + groups = data["groups"] + + for (team, player), locations_checked in data["multisave"].get("location_checks", {}).items(): + if player in data["groups"]: + continue + player_locations = data["locations"][player] + precollected = data["precollected_items"][player] + for item_id in precollected: + inventory[team][player][item_id] += 1 + for location in locations_checked: + item_id, recipient, flags = player_locations[location] + recipients = groups.get(recipient, [recipient]) + for recipient in recipients: + inventory[team][recipient][item_id] += 1 + return inventory + + +@app.route('/tracker/') +@cache.memoize(timeout=60) # multisave is currently created at most every minute +def get_multiworld_tracker(tracker: UUID): + data = _get_multiworld_tracker_data(tracker) + if not data: + abort(404) + + data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Generic") + + return render_template("multiTracker.html", **data) + + +@app.route('/tracker//Factorio') +@cache.memoize(timeout=60) # multisave is currently created at most every minute +def get_Factorio_multiworld_tracker(tracker: UUID): + data = _get_multiworld_tracker_data(tracker) + if not data: + abort(404) + + data["inventory"] = _get_inventory_data(data) + data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Factorio") + + return render_template("multiFactorioTracker.html", **data) + + +@app.route('/tracker//A Link to the Past') @cache.memoize(timeout=60) # multisave is currently created at most every minute def get_LttP_multiworld_tracker(tracker: UUID): room: Room = Room.get(tracker=tracker) @@ -1518,3 +1563,8 @@ game_specific_trackers: typing.Dict[str, typing.Callable] = { "Super Metroid": __renderSuperMetroidTracker, "Starcraft 2 Wings of Liberty": __renderSC2WoLTracker } + +multi_trackers: typing.Dict[str, typing.Callable] = { + "A Link to the Past": get_LttP_multiworld_tracker, + "Factorio": get_Factorio_multiworld_tracker, +} From 070a92e76c945acddcea3a7c1c8ca180b17f35a7 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sun, 12 Mar 2023 09:05:50 -0500 Subject: [PATCH 051/172] The Messenger: implement new game (#1494) * initial commit of messenger integration * setup no_logic and needed slot_data * fix some typos and determinism * make all of it deterministic * add documentation * swapped to non local items so change the fed data * ~~deathlink~~ * satisfy the docs test * update doc test to show expected name * split custom classes into a separate file and fix an errant rule * make access dependency test give more useful errors * implement tests * remove some unneccessary back entrances and make names clearer * fix some big dumbs * successful unit tests are good also some slight reorganizing * add astral tea quest line, and potentially power seals as items * if TYPE_CHECKING... aahhhhhh * oop forgot to remove legacy code * having the seed and leaves as actual items doesn't seem to do anything so remove them. locations still work though * update setup guide with some changes * Tower HQ was creating duplicate locations * allow self locking items * cleanup * move self_locking_items function to core * docstring * implement choice of notes needed for music box * test the default value * don't create any starting inventory items * make item creation faster * change default accessibility and power seals options * improve documentation * precollected_items is a dict of Items... * implement shop chest goal * tests * always assign total and required seals * add new goals and set music box as requiring shop chest on shop chest goals instead of just setting it as the completion * fix dumb test quirk * implement music box skip as an option * world rewrite/cleanup * default to apworld and add game to readme * revert bleeding commits from other PRs * more bleeds * fix some errors in options docstrings * ??? * make my set rules method not have an awful name * test cleanup * add a test for item accessibility * fix issues with tests * make the self locking item behavior work correctly * misc cleanup * more general cleanup to be a good example * quick rules rewrite * more general cleanup and typing * more speed, more clean * bump data version * make sure the locked item belongs to current player * fix bad name and indent. call MessengerItem directly for events * add poptracker pack to docs * doc cleanup and "known issues" section that I probably won't be able to fix any time soon. * missed some spots * add another bug i forgot about * be consistently wrong --- README.md | 1 + setup.py | 1 + worlds/messenger/Constants.py | 153 +++++++++++++++++++++ worlds/messenger/Options.py | 66 +++++++++ worlds/messenger/Regions.py | 52 +++++++ worlds/messenger/Rules.py | 158 ++++++++++++++++++++++ worlds/messenger/SubClasses.py | 58 ++++++++ worlds/messenger/__init__.py | 125 +++++++++++++++++ worlds/messenger/docs/en_The Messenger.md | 75 ++++++++++ worlds/messenger/docs/setup_en.md | 52 +++++++ worlds/messenger/test/TestAccess.py | 149 ++++++++++++++++++++ worlds/messenger/test/TestNotes.py | 30 ++++ worlds/messenger/test/TestShopChest.py | 79 +++++++++++ worlds/messenger/test/__init__.py | 6 + 14 files changed, 1005 insertions(+) create mode 100644 worlds/messenger/Constants.py create mode 100644 worlds/messenger/Options.py create mode 100644 worlds/messenger/Regions.py create mode 100644 worlds/messenger/Rules.py create mode 100644 worlds/messenger/SubClasses.py create mode 100644 worlds/messenger/__init__.py create mode 100644 worlds/messenger/docs/en_The Messenger.md create mode 100644 worlds/messenger/docs/setup_en.md create mode 100644 worlds/messenger/test/TestAccess.py create mode 100644 worlds/messenger/test/TestNotes.py create mode 100644 worlds/messenger/test/TestShopChest.py create mode 100644 worlds/messenger/test/__init__.py diff --git a/README.md b/README.md index 9e6ed2b1eb..b99182f496 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ Currently, the following games are supported: * Wargroove * Stardew Valley * The Legend of Zelda +* The Messenger For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/setup.py b/setup.py index 7c55a4d256..8ad4f32efa 100644 --- a/setup.py +++ b/setup.py @@ -54,6 +54,7 @@ apworlds: set = { "Stardew Valley", "Timespinner", "Minecraft", + "The Messenger", } diff --git a/worlds/messenger/Constants.py b/worlds/messenger/Constants.py new file mode 100644 index 0000000000..d57081edac --- /dev/null +++ b/worlds/messenger/Constants.py @@ -0,0 +1,153 @@ +# items +# listing individual groups first for easy lookup +NOTES = [ + "Key of Hope", + "Key of Chaos", + "Key of Courage", + "Key of Love", + "Key of Strength", + "Key of Symbiosis" +] + +PROG_ITEMS = [ + "Wingsuit", + "Rope Dart", + "Ninja Tabi", + "Power Thistle", + "Demon King Crown", + "Ruxxtin's Amulet", + "Fairy Bottle", + "Sun Crest", + "Moon Crest", + # "Astral Seed", + # "Astral Tea Leaves" +] + +PHOBEKINS = [ + "Necro", + "Pyro", + "Claustro", + "Acro" +] + +USEFUL_ITEMS = [ + "Windmill Shuriken" +] + +# item_name_to_id needs to be deterministic and match upstream +ALL_ITEMS = [ + *NOTES, + "Windmill Shuriken", + "Wingsuit", + "Rope Dart", + "Ninja Tabi", + # "Astral Seed", + # "Astral Tea Leaves", + "Candle", + "Seashell", + "Power Thistle", + "Demon King Crown", + "Ruxxtin's Amulet", + "Fairy Bottle", + "Sun Crest", + "Moon Crest", + *PHOBEKINS, + "Power Seal", + "Time Shard" # there's 45 separate instances of this in the client lookup, but hopefully we don't care? +] + +# locations +# the names of these don't actually matter, but using the upstream's names for now +# order must be exactly the same as upstream +ALWAYS_LOCATIONS = [ + # notes + "Key of Love", + "Key of Courage", + "Key of Chaos", + "Key of Symbiosis", + "Key of Strength", + "Key of Hope", + # upgrades + "Wingsuit", + "Rope Dart", + "Ninja Tabi", + "Climbing Claws", + # quest items + "Astral Seed", + "Astral Tea Leaves", + "Candle", + "Seashell", + "Power Thistle", + "Demon King Crown", + "Ruxxtin's Amulet", + "Fairy Bottle", + "Sun Crest", + "Moon Crest", + # phobekins + "Necro", + "Pyro", + "Claustro", + "Acro" +] + +SEALS = [ + "Ninja Village Seal - Tree House", + + "Autumn Hills Seal - Trip Saws", + "Autumn Hills Seal - Double Swing Saws", + "Autumn Hills Seal - Spike Ball Swing", + "Autumn Hills Seal - Spike Ball Darts", + + "Catacombs Seal - Triple Spike Crushers", + "Catacombs Seal - Crusher Gauntlet", + "Catacombs Seal - Dirty Pond", + + "Bamboo Creek Seal - Spike Crushers and Doors", + "Bamboo Creek Seal - Spike Ball Pits", + "Bamboo Creek Seal - Spike Crushers and Doors v2", + + "Howling Grotto Seal - Windy Saws and Balls", + "Howling Grotto Seal - Crushing Pits", + "Howling Grotto Seal - Breezy Crushers", + + "Quillshroom Marsh Seal - Spikey Window", + "Quillshroom Marsh Seal - Sand Trap", + "Quillshroom Marsh Seal - Do the Spike Wave", + + "Searing Crags Seal - Triple Ball Spinner", + "Searing Crags Seal - Raining Rocks", + "Searing Crags Seal - Rhythm Rocks", + + "Glacial Peak Seal - Ice Climbers", + "Glacial Peak Seal - Projectile Spike Pit", + "Glacial Peak Seal - Glacial Air Swag", + + "Tower of Time Seal - Time Waster Seal", + "Tower of Time Seal - Lantern Climb", + "Tower of Time Seal - Arcane Orbs", + + "Cloud Ruins Seal - Ghost Pit", + "Cloud Ruins Seal - Toothbrush Alley", + "Cloud Ruins Seal - Saw Pit", + "Cloud Ruins Seal - Money Farm Room", + + "Underworld Seal - Sharp and Windy Climb", + "Underworld Seal - Spike Wall", + "Underworld Seal - Fireball Wave", + "Underworld Seal - Rising Fanta", + + "Forlorn Temple Seal - Rocket Maze", + "Forlorn Temple Seal - Rocket Sunset", + + "Sunken Shrine Seal - Ultra Lifeguard", + "Sunken Shrine Seal - Waterfall Paradise", + "Sunken Shrine Seal - Tabi Gauntlet", + + "Riviere Turquoise Seal - Bounces and Balls", + "Riviere Turquoise Seal - Launch of Faith", + "Riviere Turquoise Seal - Flower Power", + + "Elemental Skylands Seal - Air", + "Elemental Skylands Seal - Water", + "Elemental Skylands Seal - Fire" +] diff --git a/worlds/messenger/Options.py b/worlds/messenger/Options.py new file mode 100644 index 0000000000..1baca12e3a --- /dev/null +++ b/worlds/messenger/Options.py @@ -0,0 +1,66 @@ +from Options import DefaultOnToggle, DeathLink, Range, Accessibility, Choice + + +class MessengerAccessibility(Accessibility): + default = Accessibility.option_locations + # defaulting to locations accessibility since items makes certain items self-locking + __doc__ = Accessibility.__doc__.replace(f"default {Accessibility.default}", f"default {default}") + + +class Logic(DefaultOnToggle): + """Whether the seed should be guaranteed completable.""" + display_name = "Use Logic" + + +class PowerSeals(DefaultOnToggle): + """Whether power seal locations should be randomized.""" + display_name = "Shuffle Seals" + + +class Goal(Choice): + """Requirement to finish the game. Power Seal Hunt will force power seal locations to be shuffled.""" + display_name = "Goal" + option_open_music_box = 0 + option_power_seal_hunt = 1 + + +class MusicBox(DefaultOnToggle): + """Whether the music box gauntlet needs to be done.""" + display_name = "Music Box Gauntlet" + + +class NotesNeeded(Range): + """How many notes are needed to access the Music Box.""" + display_name = "Notes Needed" + range_start = 1 + range_end = 6 + default = range_end + + +class AmountSeals(Range): + """Number of power seals that exist in the item pool when power seal hunt is the goal.""" + display_name = "Total Power Seals" + range_start = 1 + range_end = 45 + default = range_end + + +class RequiredSeals(Range): + """Percentage of total seals required to open the shop chest.""" + display_name = "Percent Seals Required" + range_start = 10 + range_end = 100 + default = range_end + + +messenger_options = { + "accessibility": MessengerAccessibility, + "enable_logic": Logic, + "shuffle_seals": PowerSeals, + "goal": Goal, + "music_box": MusicBox, + "notes_needed": NotesNeeded, + "total_seals": AmountSeals, + "percent_seals_required": RequiredSeals, + "death_link": DeathLink, +} diff --git a/worlds/messenger/Regions.py b/worlds/messenger/Regions.py new file mode 100644 index 0000000000..468c69cfd8 --- /dev/null +++ b/worlds/messenger/Regions.py @@ -0,0 +1,52 @@ +from typing import Dict, Set, List + +REGIONS: Dict[str, List[str]] = { + "Menu": [], + "Tower HQ": [], + "The Shop": [], + "Tower of Time": [], + "Ninja Village": ["Candle", "Astral Seed"], + "Autumn Hills": ["Climbing Claws", "Key of Hope"], + "Forlorn Temple": ["Demon King Crown"], + "Catacombs": ["Necro", "Ruxxtin's Amulet"], + "Bamboo Creek": ["Claustro"], + "Howling Grotto": ["Wingsuit"], + "Quillshroom Marsh": ["Seashell"], + "Searing Crags": ["Rope Dart"], + "Searing Crags Upper": ["Power Thistle", "Key of Strength", "Astral Tea Leaves"], + "Glacial Peak": [], + "Cloud Ruins": ["Acro"], + "Underworld": ["Pyro", "Key of Chaos"], + "Dark Cave": [], + "Riviere Turquoise": ["Fairy Bottle"], + "Sunken Shrine": ["Ninja Tabi", "Sun Crest", "Moon Crest", "Key of Love"], + "Elemental Skylands": ["Key of Symbiosis"], + "Corrupted Future": ["Key of Courage"], + "Music Box": ["Rescue Phantom"] +} +"""seal locations have the region in their name and may not need to be created so skip them here""" + + +REGION_CONNECTIONS: Dict[str, Set[str]] = { + "Menu": {"Tower HQ"}, + "Tower HQ": {"Autumn Hills", "Howling Grotto", "Searing Crags", "Glacial Peak", "Tower of Time", "Riviere Turquoise", + "Sunken Shrine", "Corrupted Future", "The Shop", "Music Box"}, + "Tower of Time": set(), + "Ninja Village": set(), + "Autumn Hills": {"Ninja Village", "Forlorn Temple", "Catacombs"}, + "Forlorn Temple": {"Catacombs", "Bamboo Creek"}, + "Catacombs": {"Autumn Hills", "Bamboo Creek", "Dark Cave"}, + "Bamboo Creek": {"Catacombs", "Howling Grotto"}, + "Howling Grotto": {"Bamboo Creek", "Quillshroom Marsh", "Sunken Shrine"}, + "Quillshroom Marsh": {"Howling Grotto", "Searing Crags"}, + "Searing Crags": {"Searing Crags Upper", "Quillshroom Marsh", "Underworld"}, + "Searing Crags Upper": {"Searing Crags", "Glacial Peak"}, + "Glacial Peak": {"Searing Crags Upper", "Tower HQ", "Cloud Ruins", "Elemental Skylands"}, + "Cloud Ruins": {"Underworld"}, + "Underworld": set(), + "Dark Cave": {"Catacombs", "Riviere Turquoise"}, + "Riviere Turquoise": set(), + "Sunken Shrine": {"Howling Grotto"}, + "Elemental Skylands": set() +} +"""Vanilla layout mapping with all Tower HQ portals open. from -> to""" diff --git a/worlds/messenger/Rules.py b/worlds/messenger/Rules.py new file mode 100644 index 0000000000..c273167802 --- /dev/null +++ b/worlds/messenger/Rules.py @@ -0,0 +1,158 @@ +from typing import Dict, Callable, Optional, Tuple, Union, TYPE_CHECKING, List, Iterable + +from BaseClasses import CollectionState, MultiWorld, Location, Region, Entrance, Item +from .Options import MessengerAccessibility, Goal +from .Constants import NOTES, PHOBEKINS +from ..generic.Rules import add_rule, set_rule + +if TYPE_CHECKING: + from . import MessengerWorld +else: + MessengerWorld = object + + +class MessengerRules: + player: int + world: MessengerWorld + + def __init__(self, world: MessengerWorld): + self.player = world.player + self.world = world + + self.region_rules: Dict[str, Callable[[CollectionState], bool]] = { + "Ninja Village": self.has_wingsuit, + "Autumn Hills": self.has_wingsuit, + "Catacombs": self.has_wingsuit, + "Bamboo Creek": self.has_wingsuit, + "Searing Crags Upper": self.has_vertical, + "Cloud Ruins": lambda state: self.has_wingsuit(state) and state.has("Ruxxtin's Amulet", self.player), + "Underworld": self.has_tabi, + "Forlorn Temple": lambda state: state.has_all(PHOBEKINS, self.player) and self.has_wingsuit(state), + "Glacial Peak": self.has_vertical, + "Elemental Skylands": lambda state: state.has("Fairy Bottle", self.player), + "Music Box": lambda state: state.has_all(NOTES, self.player) + } + + self.location_rules: Dict[str, Callable[[CollectionState], bool]] = { + # ninja village + "Ninja Village Seal - Tree House": self.has_dart, + # autumn hills + "Key of Hope": self.has_dart, + # howling grotto + "Howling Grotto Seal - Windy Saws and Balls": self.has_wingsuit, + "Howling Grotto Seal - Crushing Pits": lambda state: self.has_wingsuit(state) and self.has_dart(state), + # searing crags + "Key of Strength": lambda state: state.has("Power Thistle", self.player), + # glacial peak + "Glacial Peak Seal - Ice Climbers": self.has_dart, + "Glacial Peak Seal - Projectile Spike Pit": self.has_vertical, + "Glacial Peak Seal - Glacial Air Swag": self.has_vertical, + # tower of time + "Tower of Time Seal - Time Waster Seal": self.has_dart, + "Tower of Time Seal - Lantern Climb": self.has_wingsuit, + "Tower of Time Seal - Arcane Orbs": lambda state: self.has_wingsuit(state) and self.has_dart(state), + # underworld + "Underworld Seal - Sharp and Windy Climb": self.has_wingsuit, + "Underworld Seal - Fireball Wave": self.has_wingsuit, + "Underworld Seal - Rising Fanta": self.has_dart, + # sunken shrine + "Sun Crest": self.has_tabi, + "Moon Crest": self.has_tabi, + "Key of Love": lambda state: state.has_all({"Sun Crest", "Moon Crest"}, self.player), + "Sunken Shrine Seal - Waterfall Paradise": self.has_tabi, + "Sunken Shrine Seal - Tabi Gauntlet": self.has_tabi, + # riviere turquoise + "Fairy Bottle": self.has_vertical, + "Riviere Turquoise Seal - Flower Power": self.has_vertical, + # elemental skylands + "Key of Symbiosis": self.has_dart, + "Elemental Skylands Seal - Air": self.has_wingsuit, + "Elemental Skylands Seal - Water": self.has_dart, + "Elemental Skylands Seal - Fire": self.has_dart, + # corrupted future + "Key of Courage": lambda state: state.has_all({"Demon King Crown", "Fairy Bottle"}, self.player), + # the shop + "Shop Chest": self.has_enough_seals + } + + def has_wingsuit(self, state: CollectionState) -> bool: + return state.has("Wingsuit", self.player) + + def has_dart(self, state: CollectionState) -> bool: + return state.has("Rope Dart", self.player) + + def has_tabi(self, state: CollectionState) -> bool: + return state.has("Ninja Tabi", self.player) + + def has_vertical(self, state: CollectionState) -> bool: + return self.has_wingsuit(state) or self.has_dart(state) + + def has_enough_seals(self, state: CollectionState) -> bool: + required_seals = state.multiworld.worlds[self.player].required_seals + return state.has("Power Seal", self.player, required_seals) + + def set_messenger_rules(self) -> None: + multiworld = self.world.multiworld + + for region in multiworld.get_regions(self.player): + if region.name in self.region_rules: + for entrance in region.entrances: + entrance.access_rule = self.region_rules[region.name] + for loc in region.locations: + if loc.name in self.location_rules: + loc.access_rule = self.location_rules[loc.name] + if multiworld.goal[self.player] == Goal.option_power_seal_hunt: + set_rule(multiworld.get_entrance("Tower HQ -> Music Box", self.player), + lambda state: state.has("Shop Chest", self.player)) + + if multiworld.enable_logic[self.player]: + multiworld.completion_condition[self.player] = lambda state: state.has("Rescue Phantom", self.player) + else: + multiworld.accessibility[self.player].value = MessengerAccessibility.option_minimal + if multiworld.accessibility[self.player] > MessengerAccessibility.option_locations: + set_self_locking_items(multiworld, self.player) + + +def location_item_name(state: CollectionState, location_name: str, player: int) -> Optional[Tuple[str, int]]: + location = state.multiworld.get_location(location_name, player) + if location.item is None: + return None + return location.item.name, location.item.player + + +def allow_self_locking_items(spot: Union[Location, Region], *item_names: str) -> None: + """ + Sets rules on the supplied spot, such that the supplied item_name(s) can possibly be placed there. + :param spot: Location or Region that the item(s) are allowed to be placed in + :param item_names: item name or names that are allowed to be placed in the Location or Region + """ + player = spot.player + + def set_always_allow(location: Location, rule: Callable[[CollectionState, Item], bool]) -> None: + location.always_allow = rule + + def add_allowed_rules(area: Union[Location, Entrance], location: Location) -> None: + for item_name in item_names: + add_rule(area, lambda state, item_name=item_name: + location_item_name(state, location.name, player) == (item_name, player), "or") + set_always_allow(location, lambda state, item: + item.player == player and item.name in [item_name for item_name in item_names]) + + if isinstance(spot, Region): + for entrance in spot.entrances: + for location in spot.locations: + add_allowed_rules(entrance, location) + else: + add_allowed_rules(spot, spot) + + +def set_self_locking_items(multiworld: MultiWorld, player: int) -> None: + # do the ones for seal shuffle on and off first + allow_self_locking_items(multiworld.get_location("Key of Strength", player), "Power Thistle") + allow_self_locking_items(multiworld.get_location("Key of Love", player), "Sun Crest", "Moon Crest") + allow_self_locking_items(multiworld.get_location("Key of Courage", player), "Demon King Crown") + + # add these locations when seals aren't shuffled + if not multiworld.shuffle_seals[player]: + allow_self_locking_items(multiworld.get_region("Cloud Ruins", player), "Ruxxtin's Amulet") + allow_self_locking_items(multiworld.get_region("Forlorn Temple", player), *PHOBEKINS) diff --git a/worlds/messenger/SubClasses.py b/worlds/messenger/SubClasses.py new file mode 100644 index 0000000000..32803f5e0d --- /dev/null +++ b/worlds/messenger/SubClasses.py @@ -0,0 +1,58 @@ +from typing import Set, TYPE_CHECKING, Optional, Dict + +from BaseClasses import Region, Location, Item, ItemClassification, Entrance +from .Constants import SEALS, NOTES, PROG_ITEMS, PHOBEKINS, USEFUL_ITEMS +from .Options import Goal +from .Regions import REGIONS + +if TYPE_CHECKING: + from . import MessengerWorld +else: + MessengerWorld = object + + +class MessengerRegion(Region): + def __init__(self, name: str, world: MessengerWorld): + super().__init__(name, world.player, world.multiworld) + self.add_locations(self.multiworld.worlds[self.player].location_name_to_id) + world.multiworld.regions.append(self) + + def add_locations(self, name_to_id: Dict[str, int]) -> None: + for loc in REGIONS[self.name]: + self.locations.append(MessengerLocation(loc, self, name_to_id.get(loc, None))) + if self.name == "The Shop" and self.multiworld.goal[self.player] > Goal.option_open_music_box: + self.locations.append(MessengerLocation("Shop Chest", self, name_to_id.get("Shop Chest", None))) + # putting some dumb special case for searing crags and ToT so i can split them into 2 regions + if self.multiworld.shuffle_seals[self.player] and self.name not in {"Searing Crags", "Tower HQ"}: + for seal_loc in SEALS: + if seal_loc.startswith(self.name.split(" ")[0]): + self.locations.append(MessengerLocation(seal_loc, self, name_to_id.get(seal_loc, None))) + + def add_exits(self, exits: Set[str]) -> None: + for exit in exits: + ret = Entrance(self.player, f"{self.name} -> {exit}", self) + self.exits.append(ret) + ret.connect(self.multiworld.get_region(exit, self.player)) + + +class MessengerLocation(Location): + game = "The Messenger" + + def __init__(self, name: str, parent: MessengerRegion, loc_id: Optional[int]): + super().__init__(parent.player, name, loc_id, parent) + if loc_id is None: + self.place_locked_item(MessengerItem(name, parent.player, None)) + + +class MessengerItem(Item): + game = "The Messenger" + + def __init__(self, name: str, player: int, item_id: Optional[int] = None): + if name in {*NOTES, *PROG_ITEMS, *PHOBEKINS} or item_id is None: + item_class = ItemClassification.progression + elif name in USEFUL_ITEMS: + item_class = ItemClassification.useful + else: + item_class = ItemClassification.filler + super().__init__(name, item_class, item_id, player) + diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py new file mode 100644 index 0000000000..1c42b30494 --- /dev/null +++ b/worlds/messenger/__init__.py @@ -0,0 +1,125 @@ +from typing import Dict, Any, List, Optional + +from BaseClasses import Tutorial, ItemClassification +from worlds.AutoWorld import World, WebWorld +from .Constants import NOTES, PROG_ITEMS, PHOBEKINS, USEFUL_ITEMS, ALWAYS_LOCATIONS, SEALS, ALL_ITEMS +from .Options import messenger_options, NotesNeeded, Goal, PowerSeals +from .Regions import REGIONS, REGION_CONNECTIONS +from .Rules import MessengerRules +from .SubClasses import MessengerRegion, MessengerItem + + +class MessengerWeb(WebWorld): + theme = "ocean" + + bug_report_page = "https://github.com/minous27/TheMessengerRandomizerMod/issues" + + tut_en = Tutorial( + "Multiworld Setup Tutorial", + "A guide to setting up The Messenger randomizer on your computer.", + "English", + "setup_en.md", + "setup/en", + ["alwaysintreble"] + ) + + tutorials = [tut_en] + + +class MessengerWorld(World): + """ + As a demon army besieges his village, a young ninja ventures through a cursed world, to deliver a scroll paramount + to his clan’s survival. What begins as a classic action platformer soon unravels into an expansive time-traveling + adventure full of thrills, surprises, and humor. + """ + game = "The Messenger" + + item_name_groups = { + "Notes": set(NOTES), + "Keys": set(NOTES), + "Crest": {"Sun Crest", "Moon Crest"}, + "Phobe": set(PHOBEKINS), + "Phobekin": set(PHOBEKINS), + "Shuriken": {"Windmill Shuriken"}, + } + + option_definitions = messenger_options + + base_offset = 0xADD_000 + item_name_to_id = {item: item_id + for item_id, item in enumerate(ALL_ITEMS, base_offset)} + location_name_to_id = {location: location_id + for location_id, location in enumerate([*ALWAYS_LOCATIONS, *SEALS], base_offset)} + + data_version = 1 + + web = MessengerWeb() + + total_seals: Optional[int] = None + required_seals: Optional[int] = None + + def generate_early(self) -> None: + if self.multiworld.goal[self.player] == Goal.option_power_seal_hunt: + self.multiworld.shuffle_seals[self.player].value = PowerSeals.option_true + self.total_seals = self.multiworld.total_seals[self.player].value + self.required_seals = int(self.multiworld.percent_seals_required[self.player].value / 100 * self.total_seals) + + def create_regions(self) -> None: + for region in [MessengerRegion(reg_name, self) for reg_name in REGIONS]: + if region.name in REGION_CONNECTIONS: + region.add_exits(REGION_CONNECTIONS[region.name]) + + def create_items(self) -> None: + itempool: List[MessengerItem] = [] + if self.multiworld.goal[self.player] == Goal.option_power_seal_hunt: + seals = [self.create_item("Power Seal") for _ in range(self.total_seals)] + for i in range(self.required_seals): + seals[i].classification = ItemClassification.progression_skip_balancing + itempool += seals + else: + notes = self.multiworld.random.sample(NOTES, k=len(NOTES)) + precollected_notes_amount = NotesNeeded.range_end - self.multiworld.notes_needed[self.player] + if precollected_notes_amount: + for note in notes[:precollected_notes_amount]: + self.multiworld.push_precollected(self.create_item(note)) + itempool += [self.create_item(note) for note in notes[precollected_notes_amount:]] + + itempool += [self.create_item(item) + for item in self.item_name_to_id + if item not in + { + "Power Seal", "Time Shard", *NOTES, + *{collected_item.name for collected_item in self.multiworld.precollected_items[self.player]} + # this is a set and currently won't create items for anything that appears in here at all + # if we get in a position where this can have duplicates of items that aren't Power Seals + # or Time shards, this will need to be redone. + }] + itempool += [self.create_filler() + for _ in range(len(self.multiworld.get_unfilled_locations(self.player)) - len(itempool))] + + self.multiworld.itempool += itempool + + def set_rules(self) -> None: + MessengerRules(self).set_messenger_rules() + + def fill_slot_data(self) -> Dict[str, Any]: + locations: Dict[int, List[str]] = {} + for loc in self.multiworld.get_filled_locations(self.player): + if loc.item.code: + locations[loc.address] = [loc.item.name, self.multiworld.player_name[loc.item.player]] + + return { + "deathlink": self.multiworld.death_link[self.player].value, + "goal": self.multiworld.goal[self.player].current_key, + "music_box": self.multiworld.music_box[self.player].value, + "required_seals": self.required_seals, + "locations": locations, + "settings": {"Difficulty": "Basic" if not self.multiworld.shuffle_seals[self.player] else "Advanced"} + } + + def get_filler_item_name(self) -> str: + return "Time Shard" + + def create_item(self, name: str) -> MessengerItem: + item_id: Optional[int] = self.item_name_to_id.get(name, None) + return MessengerItem(name, self.player, item_id) diff --git a/worlds/messenger/docs/en_The Messenger.md b/worlds/messenger/docs/en_The Messenger.md new file mode 100644 index 0000000000..f1b53cd18c --- /dev/null +++ b/worlds/messenger/docs/en_The Messenger.md @@ -0,0 +1,75 @@ +# The Messenger + +## Quick Links +- [Setup](../../../../games/The%20Messenger/setup/en) +- [Settings Page](../../../../games/The%20Messenger/player-settings) +- [Courier Github](https://github.com/Brokemia/Courier) +- [The Messenger Randomizer Github](https://github.com/minous27/TheMessengerRandomizerMod) +- [Jacksonbird8237's Item Tracker](https://github.com/Jacksonbird8237/TheMessengerItemTracker) +- [PopTracker Pack](https://github.com/alwaysintreble/TheMessengerTrackPack) + +## What does randomization do in this game? + +All items and upgrades that can be picked up by the player in the game are randomized. The player starts in the Tower of +Time HQ with the past section finished, all area portals open, and with the cloud step, and climbing claws already +obtained. You'll be forced to do sections of the game in different ways with your current abilities. Currently, logic +assumes you already have all shop upgrades. + +## What items can appear in other players' worlds? + +* The player's movement items +* Quest and pedestal items +* Music Box notes +* The Phobekins +* Time shards +* Power Seals + +## Where can I find items? + +You can find items wherever items can be picked up in the original game. This includes: +* Shopkeeper dialog where the player originally gains movement items +* Quest Item pickups +* Music Box notes +* Phobekins +* Power seals + +## What are the item name groups? + +When you attempt to hint for items in Archipelago you can use either the name for the specific item, or the name of a +group of items. Hinting for a group will choose a random item from the group that you do not currently have and hint +for it. The groups you can use for The Messenger are: +* Notes - This covers the music notes +* Keys - An alternative name for the music notes +* Crest - The Sun and Moon Crests +* Phobekin - Any of the Phobekins +* Phobe - An alternative name for the Phobekins +* Shuriken - The windmill shuriken + +## Other changes + +* The player can return to the Tower of Time HQ at any point by selecting the button from the options menu + * This can cause issues if used at specific times. Current known: + * During Boss fights + * After Courage Note collection (Corrupted Future chase) + * This is currently an expected action in logic. If you do need to teleport during this chase sequence, it +is recommended to quit to title and reload the save +* After reaching ninja village a teleport option is added to the menu to reach it quickly +* Toggle Windmill Shuriken button is added to option menu once the item is received + +## Currently known issues +* Necro cutscene will sometimes not play correctly, but will still reward the item +* Ruxxtin Coffin cutscene will sometimes not play correctly, but will still reward the item +* If you receive the Fairy Bottle while in Quillshroom Marsh, The Decurse Queen cutscene will not play +* If you defeat Barma'thazël, the cutscene afterward will not play correctly since that is what normally transitions +you to 2nd quest. The game will not kill you if you fall here, so you can teleport to HQ at any point after defeating him. +* Sometimes upon teleporting back to HQ, Ninja will run left and enter a different portal than the one entered by the +player. +* If playing the game in non-english, sometimes the text entry menus will say "What is your name?" in local language +instead of the correct text. This can be fixed by going into the game options and selecting your language in the menu. +It does not need to be changed to something else and back. +* Text entry menus don't accept controller input + +## What do I do if I have a problem? + +If you believe something happened that isn't intended, please get the `log.txt`from the folder of your game installation +and send a bug report either on github or the [Archipelago Discord Server](http://archipelago.gg/discord) diff --git a/worlds/messenger/docs/setup_en.md b/worlds/messenger/docs/setup_en.md new file mode 100644 index 0000000000..59341e74e0 --- /dev/null +++ b/worlds/messenger/docs/setup_en.md @@ -0,0 +1,52 @@ +# The Messenger Randomizer Setup Guide + +## Quick Links +- [Main Page](../../../../games/The%20Messenger/info/en) +- [Settings Page](../../../../games/The%20Messenger/player-settings) +- [Courier Github](https://github.com/Brokemia/Courier) +- [The Messenger Randomizer Github](https://github.com/minous27/TheMessengerRandomizerMod) +- [Jacksonbird8237's Item Tracker](https://github.com/Jacksonbird8237/TheMessengerItemTracker) +- [PopTracker Pack](https://github.com/alwaysintreble/TheMessengerTrackPack) + +## Required Software + +- [The Messenger](https://store.steampowered.com/app/764790/The_Messenger/) + - Only Steam version is currently supported. +- [Courier Mod Loader](https://github.com/Brokemia/Courier/releases) +- [The Messenger Randomizer Mod](https://github.com/minous27/TheMessengerRandomizerMod/releases) + +## Installation + +1. Download and install Courier Mod Loader using the instructions on the release page +2. Download and install the randomizer mod + * Download the latest `TheMessengerRandomizer.zip` + * Extract the zip file to `TheMessenger/Mods/` of your game's install location + * Optionally, Backup your save game + 1. Press `Windows Key + R` to open run + 2. Type `%appdata%` to access AppData + 3. Navigate to `AppData/locallow/SabotageStudios/The Messenger` + 4. Rename `SaveGame.txt` to any name of your choice + +## Joining a MultiWorld Game + +1. Launch the game +2. Navigate to `Options > Third Party Mod Options` +3. Select `Reset Randomizer File Slots` + * This will set up all of your save slots with new randomizer save files. You can have up to 3 randomizer files at a +time, but must do this step again to start new runs afterwards. +4. Enter connection info using the relevant option buttons + * **The game is limited to alphanumerical characters and `-` so when entering the host name replace `.` with ` ` and +ensure that your player name when generating a settings file follows these constrictions** + * This defaults to `archipelago.gg` and does not need to be manually changed if connecting to a game hosted on the +website. +5. Select the `Connect to Archipelago` button +6. Navigate to save file selection +7. Select a new valid randomizer save + +## Troubleshooting + +If you launch the game, and it hangs on the splash screen for more than 30 seconds try these steps: +1. Close the game and remove `TheMessengerRandomizer` from the `Mods` folder. +2. Launch The Messenger +3. Delete any save slot +4. Reinstall the randomizer mod following step 2 of the installation. \ No newline at end of file diff --git a/worlds/messenger/test/TestAccess.py b/worlds/messenger/test/TestAccess.py new file mode 100644 index 0000000000..eba4ad9bbf --- /dev/null +++ b/worlds/messenger/test/TestAccess.py @@ -0,0 +1,149 @@ +from . import MessengerTestBase +from ..Constants import NOTES, PHOBEKINS +from ..Options import MessengerAccessibility + + +class AccessTest(MessengerTestBase): + + def testTabi(self) -> None: + """locations that hard require the Ninja Tabi""" + locations = ["Pyro", "Key of Chaos", "Underworld Seal - Sharp and Windy Climb", "Underworld Seal - Spike Wall", + "Underworld Seal - Fireball Wave", "Underworld Seal - Rising Fanta", "Sun Crest", "Moon Crest", + "Sunken Shrine Seal - Waterfall Paradise", "Sunken Shrine Seal - Tabi Gauntlet"] + items = [["Ninja Tabi"]] + self.assertAccessDependency(locations, items) + + def testDart(self) -> None: + """locations that hard require the Rope Dart""" + locations = ["Ninja Village Seal - Tree House", "Key of Hope", "Howling Grotto Seal - Crushing Pits", + "Glacial Peak Seal - Ice Climbers", "Tower of Time Seal - Time Waster Seal", + "Tower of Time Seal - Arcane Orbs", "Underworld Seal - Rising Fanta", "Key of Symbiosis", + "Elemental Skylands Seal - Water", "Elemental Skylands Seal - Fire"] + items = [["Rope Dart"]] + self.assertAccessDependency(locations, items) + + def testWingsuit(self) -> None: + """locations that hard require the Wingsuit""" + locations = ["Candle", "Ninja Village Seal - Tree House", "Climbing Claws", "Key of Hope", + "Autumn Hills Seal - Trip Saws", "Autumn Hills Seal - Double Swing Saws", + "Autumn Hills Seal - Spike Ball Swing", "Autumn Hills Seal - Spike Ball Darts", "Necro", + "Ruxxtin's Amulet", "Catacombs Seal - Triple Spike Crushers", "Catacombs Seal - Crusher Gauntlet", + "Catacombs Seal - Dirty Pond", "Claustro", "Acro", "Bamboo Creek Seal - Spike Crushers and Doors", + "Bamboo Creek Seal - Spike Ball Pits", "Bamboo Creek Seal - Spike Crushers and Doors v2", + "Howling Grotto Seal - Crushing Pits", "Howling Grotto Seal - Windy Saws and Balls", + "Tower of Time Seal - Lantern Climb", "Demon King Crown", "Cloud Ruins Seal - Ghost Pit", + "Cloud Ruins Seal - Toothbrush Alley", "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room", + "Tower of Time Seal - Lantern Climb", "Tower of Time Seal - Arcane Orbs", + "Underworld Seal - Sharp and Windy Climb", "Underworld Seal - Fireball Wave", + "Elemental Skylands Seal - Air", "Forlorn Temple Seal - Rocket Maze", + "Forlorn Temple Seal - Rocket Sunset", "Astral Seed"] + items = [["Wingsuit"]] + self.assertAccessDependency(locations, items) + + def testVertical(self) -> None: + """locations that require either the Rope Dart or the Wingsuit""" + locations = ["Ninja Village Seal - Tree House", "Key of Hope", "Howling Grotto Seal - Crushing Pits", + "Glacial Peak Seal - Ice Climbers", "Tower of Time Seal - Time Waster Seal", + "Underworld Seal - Rising Fanta", "Key of Symbiosis", + "Elemental Skylands Seal - Water", "Elemental Skylands Seal - Fire", "Candle", + "Ninja Village Seal - Tree House", "Climbing Claws", "Key of Hope", + "Autumn Hills Seal - Trip Saws", "Autumn Hills Seal - Double Swing Saws", + "Autumn Hills Seal - Spike Ball Swing", "Autumn Hills Seal - Spike Ball Darts", "Necro", + "Ruxxtin's Amulet", "Catacombs Seal - Triple Spike Crushers", "Catacombs Seal - Crusher Gauntlet", + "Catacombs Seal - Dirty Pond", "Claustro", "Acro", "Bamboo Creek Seal - Spike Crushers and Doors", + "Bamboo Creek Seal - Spike Ball Pits", "Bamboo Creek Seal - Spike Crushers and Doors v2", + "Howling Grotto Seal - Crushing Pits", "Howling Grotto Seal - Windy Saws and Balls", + "Tower of Time Seal - Lantern Climb", "Demon King Crown", "Cloud Ruins Seal - Ghost Pit", + "Cloud Ruins Seal - Toothbrush Alley", "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room", + "Tower of Time Seal - Lantern Climb", "Tower of Time Seal - Arcane Orbs", + "Underworld Seal - Sharp and Windy Climb", "Underworld Seal - Fireball Wave", + "Elemental Skylands Seal - Air", "Forlorn Temple Seal - Rocket Maze", "Forlorn Temple Seal - Rocket Sunset", + "Power Thistle", "Key of Strength", "Glacial Peak Seal - Projectile Spike Pit", + "Glacial Peak Seal - Glacial Air Swag", "Fairy Bottle", "Riviere Turquoise Seal - Flower Power", + "Searing Crags Seal - Triple Ball Spinner", "Searing Crags Seal - Raining Rocks", + "Searing Crags Seal - Rhythm Rocks", "Astral Seed", "Astral Tea Leaves"] + items = [["Wingsuit", "Rope Dart"]] + self.assertAccessDependency(locations, items) + + def testAmulet(self) -> None: + """Locations that require Ruxxtin's Amulet""" + locations = ["Acro", "Cloud Ruins Seal - Ghost Pit", "Cloud Ruins Seal - Toothbrush Alley", + "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room"] + # Cloud Ruins requires Ruxxtin's Amulet + items = [["Ruxxtin's Amulet"]] + self.assertAccessDependency(locations, items) + + def testBottle(self) -> None: + """Elemental Skylands and Corrupted Future require the Fairy Bottle""" + locations = ["Key of Symbiosis", "Elemental Skylands Seal - Air", "Elemental Skylands Seal - Fire", + "Elemental Skylands Seal - Water", "Key of Courage"] + items = [["Fairy Bottle"]] + self.assertAccessDependency(locations, items) + + def testCrests(self) -> None: + """Test Key of Love nonsense""" + locations = ["Key of Love"] + items = [["Sun Crest", "Moon Crest"]] + self.assertAccessDependency(locations, items) + self.collect_all_but("Sun Crest") + self.assertEqual(self.can_reach_location("Key of Love"), False) + self.remove(self.get_item_by_name("Moon Crest")) + self.collect_by_name("Sun Crest") + self.assertEqual(self.can_reach_location("Key of Love"), False) + + def testThistle(self) -> None: + """I'm a chuckster!""" + locations = ["Key of Strength"] + items = [["Power Thistle"]] + self.assertAccessDependency(locations, items) + + def testCrown(self) -> None: + """Crocomire but not""" + locations = ["Key of Courage"] + items = [["Demon King Crown"]] + self.assertAccessDependency(locations, items) + + def testGoal(self) -> None: + """Test some different states to verify goal requires the correct items""" + self.collect_all_but([*NOTES, "Rescue Phantom"]) + self.assertEqual(self.can_reach_location("Rescue Phantom"), False) + self.collect_all_but(["Key of Love", "Rescue Phantom"]) + self.assertBeatable(False) + self.collect_by_name(["Key of Love"]) + self.assertEqual(self.can_reach_location("Rescue Phantom"), True) + self.assertBeatable(True) + + +class ItemsAccessTest(MessengerTestBase): + options = { + "shuffle_seals": False, + "accessibility": MessengerAccessibility.option_items + } + + def testSelfLockingItems(self) -> None: + """Force items that can be self locked to ensure it's valid placement.""" + location_lock_pairs = { + "Key of Strength": ["Power Thistle"], + "Key of Love": ["Sun Crest", "Moon Crest"], + "Key of Courage": ["Demon King Crown"], + "Acro": ["Ruxxtin's Amulet"], + "Demon King Crown": PHOBEKINS + } + + for loc in location_lock_pairs: + for item_name in location_lock_pairs[loc]: + item = self.get_item_by_name(item_name) + with self.subTest("Fulfills Accessibility", location=loc, item=item_name): + self.assertTrue(self.multiworld.get_location(loc, self.player).can_fill(self.multiworld.state, item, True)) + + +class NoLogicTest(MessengerTestBase): + options = { + "enable_logic": "false" + } + + def testNoLogic(self) -> None: + """Test some funny locations to make sure they aren't reachable but we can still win""" + self.assertEqual(self.can_reach_location("Pyro"), False) + self.assertEqual(self.can_reach_location("Rescue Phantom"), False) + self.assertBeatable(True) diff --git a/worlds/messenger/test/TestNotes.py b/worlds/messenger/test/TestNotes.py new file mode 100644 index 0000000000..07745e3397 --- /dev/null +++ b/worlds/messenger/test/TestNotes.py @@ -0,0 +1,30 @@ +from . import MessengerTestBase +from ..Constants import NOTES + + +class TwoNoteGoalTest(MessengerTestBase): + options = { + "notes_needed": 2, + } + + def testPrecollectedNotes(self) -> None: + self.assertEqual(self.multiworld.state.count_group("Notes", self.player), 4) + + +class FourNoteGoalTest(MessengerTestBase): + options = { + "notes_needed": 4, + } + + def testPrecollectedNotes(self) -> None: + self.assertEqual(self.multiworld.state.count_group("Notes", self.player), 2) + + +class DefaultGoalTest(MessengerTestBase): + def testPrecollectedNotes(self) -> None: + self.assertEqual(self.multiworld.state.count_group("Notes", self.player), 0) + + def testGoal(self) -> None: + self.assertBeatable(False) + self.collect_by_name(NOTES) + self.assertBeatable(True) diff --git a/worlds/messenger/test/TestShopChest.py b/worlds/messenger/test/TestShopChest.py new file mode 100644 index 0000000000..c3f2c4dd55 --- /dev/null +++ b/worlds/messenger/test/TestShopChest.py @@ -0,0 +1,79 @@ +from BaseClasses import ItemClassification, CollectionState +from . import MessengerTestBase + + +class NoLogicTest(MessengerTestBase): + options = { + "enable_logic": "false", + "goal": "power_seal_hunt", + } + + def testChestAccess(self): + """Test to make sure we can win even though we can't reach the chest.""" + self.assertEqual(self.can_reach_location("Shop Chest"), False) + self.assertBeatable(True) + + +class AllSealsRequired(MessengerTestBase): + options = { + "shuffle_seals": "false", + "goal": "power_seal_hunt", + } + + def testSealsShuffled(self) -> None: + """Shuffle seals should be forced on when shop chest is the goal so test it.""" + self.assertTrue(self.multiworld.shuffle_seals[self.player]) + + def testChestAccess(self) -> None: + """Defaults to a total of 45 power seals in the pool and required.""" + with self.subTest("Access Dependency"): + self.assertEqual(len([seal for seal in self.multiworld.itempool if seal.name == "Power Seal"]), + self.multiworld.total_seals[self.player]) + locations = ["Shop Chest"] + items = [["Power Seal"]] + self.assertAccessDependency(locations, items) + self.multiworld.state = CollectionState(self.multiworld) + + self.assertEqual(self.can_reach_location("Shop Chest"), False) + self.assertBeatable(False) + self.collect_all_but(["Power Seal", "Shop Chest", "Rescue Phantom"]) + self.assertEqual(self.can_reach_location("Shop Chest"), False) + self.assertBeatable(False) + self.collect_by_name("Power Seal") + self.assertEqual(self.can_reach_location("Shop Chest"), True) + self.assertBeatable(True) + + +class HalfSealsRequired(MessengerTestBase): + options = { + "goal": "power_seal_hunt", + "percent_seals_required": 50, + } + + def testSealsAmount(self) -> None: + """Should have 45 power seals in the item pool and half that required""" + self.assertEqual(self.multiworld.total_seals[self.player], 45) + self.assertEqual(self.multiworld.worlds[self.player].total_seals, 45) + self.assertEqual(self.multiworld.worlds[self.player].required_seals, 22) + total_seals = [seal for seal in self.multiworld.itempool if seal.name == "Power Seal"] + required_seals = [seal for seal in total_seals if seal.classification == ItemClassification.progression_skip_balancing] + self.assertEqual(len(total_seals), 45) + self.assertEqual(len(required_seals), 22) + + +class ThirtyThirtySeals(MessengerTestBase): + options = { + "goal": "power_seal_hunt", + "total_seals": 30, + "percent_seals_required": 34, + } + + def testSealsAmount(self) -> None: + """Should have 30 power seals in the pool and 33 percent of that required.""" + self.assertEqual(self.multiworld.total_seals[self.player], 30) + self.assertEqual(self.multiworld.worlds[self.player].total_seals, 30) + self.assertEqual(self.multiworld.worlds[self.player].required_seals, 10) + total_seals = [seal for seal in self.multiworld.itempool if seal.name == "Power Seal"] + required_seals = [seal for seal in total_seals if seal.classification == ItemClassification.progression_skip_balancing] + self.assertEqual(len(total_seals), 30) + self.assertEqual(len(required_seals), 10) diff --git a/worlds/messenger/test/__init__.py b/worlds/messenger/test/__init__.py new file mode 100644 index 0000000000..7ab1e11781 --- /dev/null +++ b/worlds/messenger/test/__init__.py @@ -0,0 +1,6 @@ +from test.TestBase import WorldTestBase + + +class MessengerTestBase(WorldTestBase): + game = "The Messenger" + player: int = 1 From 4bc0e84a7f052f2daf5aacc3fd7317587c47c826 Mon Sep 17 00:00:00 2001 From: Qwazzy Date: Sun, 12 Mar 2023 18:58:17 -0500 Subject: [PATCH 052/172] Docs: SM64 Guide update to explain how to launch the game with batch files (#768) * Update setup_en.md Added several sections in regards to opening the completed SM64 build with batch files instead of SM64PCLauncher. * Update setup_en.md * Update setup_en.md * Update setup_en.md * Update setup_en.md * Apply suggestions from code review Co-authored-by: Yussur Mustafa Oraji Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> * Apply more suggestions from code review matches the original suggestion from SoldierofOrder --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: Yussur Mustafa Oraji Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> --- worlds/sm64ex/docs/setup_en.md | 56 ++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/worlds/sm64ex/docs/setup_en.md b/worlds/sm64ex/docs/setup_en.md index acf9432fe5..1ff8b5e938 100644 --- a/worlds/sm64ex/docs/setup_en.md +++ b/worlds/sm64ex/docs/setup_en.md @@ -67,6 +67,62 @@ Failing to use a new file may make some locations unavailable. However, this can To play offline, first generate a seed on the game's settings page. Create a room and download the `.apsm64ex` file, and start the game with the `--sm64ap_file "path/to/FileName"` launch argument. +# Optional: Using Batch Files to play offline and MultiWorld games + +As an alternative to launching the game with sm64pclauncher, it is also possible to launch the completed build with the use of Windows batch files. This has the added benefit of streamlining the join process so that manual editing of connection info is not needed for each new game. However, you'll need to be somewhat comfortable with creating and using batch files. + +IMPORTANT NOTE: The remainder of this section uses copy-and-paste code that assumes you're using the US version. If you instead use the Japanese version, you'll need to edit the EXE name accordingly by changing "sm64.us.f3dex2e.exe" to "sm64.jp.f3dex2e.exe". + +### Making an offline.bat for launching offline patch files + +Open Notepad. Paste in the following text: `start sm64.us.f3dex2e.exe --sm64ap_file %1` + +Go to File > Save As... + +Navigate to the folder you selected for your SM64 build when you followed the Build guide for SM64PCLauncher earlier. Once there, navigate further into `build` and then `us_pc`. This folder should be the same folder that `sm64.us.f3dex2e.exe` resides in. + +Make the file name `"offline.bat"` . THE QUOTE MARKS ARE IMPORTANT! Otherwise, it will create a text file instead ("offline.bat.txt"), which won't work as a batch file. + +Now you should have a file called `offline.bat` with a gear icon in the same folder as your "sm64.us.f3dex2e.exe". Right click `offline.bat` and choose `Send To > Desktop (Create Shortcut)`. +- If the icon for this file is a notepad rather than a gear, you saved it as a .txt file on accident. To fix this, change the file extension to .bat. + +From now on, whenever you start an offline, single-player game, just download the `.apsm64ex` patch file from the Generator, then drag-and-drop that onto `offline.bat` to open the game and start playing. + +NOTE: When playing offline patch files, a `.save` file is created in the same directory as your patch file, which contains your save data for that seed. Don't delete it until you're done with that seed. + +### Making an online.bat for launching online Multiworld games + +These steps are very similar. You will be making a batch file in the same location as before. However, the text you put into this batch file is different, and you will not drag patch files onto it. + +Use the same steps as before to open Notepad and paste in the following: + +`set /p port="Enter port number of room - "` + +`set /p slot="Enter slot name - "` + +`start sm64.us.f3dex2e.exe --sm64ap_name "%slot%" --sm64ap_ip archipelago.gg:%port%` + +Save this file as `"online.bat"`, then create a shortcut by following the same steps as before. + +To use this batch file, double-click it. A window will open. Type the five-digit port number of the room you wish to join, then type your slot name. +- The port number is provided on the room page. The game host should share this page with all players. +- The slot name is whatever you typed in the "Name" field when creating a config file. All slot names are visible on the room page. + +Once you provide those two bits of information, the game will open. If the info is correct, when the game starts, you will see "Connected to Archipelago" on the bottom of your screen, and you will be able to enter the castle. +- If you don't see this text and crash upon entering the castle, try again. Double-check the port number and slot name; even a single typo will cause your connection to fail. + +### Addendum - Deleting old saves + +Loading an old Mario save alongside a new seed is a bad idea, as it can cause locked doors and castle secret stars to already be unlocked / obtained. You should avoid opening a save that says "Stars x 0" as opposed to one that simply says "New". + +You can manually delete these old saves in-game before starting a new game, but that can be tedious. With a small edit to the batch files, you can delete these old saves automatically. Just add the line `del %AppData%\sm64ex\*.bin` to the batch file, above the `start` command. For example, here is `offline.bat` with the additional line: + +`del %AppData%\sm64ex\*.bin` + +`start sm64.us.f3dex2e.exe --sm64ap_file %1` + +This extra line deletes any previous save data before opening the game. Don't worry about lost stars or checks - the AP server (or in the case of offline, the `.save` file) keeps track of your star count, unlocked keys/caps/cannons, and which locations have already been checked, so you won't have to redo them. At worst you'll have to rewatch the door unlocking animations, and catch the rabbit Mips twice for his first star again if you haven't yet collected the second one. + ## Installation Troubleshooting Start the game from the command line to view helpful messages regarding SM64EX. From 030e41363a4c8b98a9895eaceedfdb4420efdb19 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 13 Mar 2023 16:30:39 +0100 Subject: [PATCH 053/172] Setup: update cx-Freeze --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8ad4f32efa..5f109d7a05 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ from pathlib import Path # This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it try: - requirement = 'cx-Freeze>=6.14.1' + requirement = 'cx-Freeze>=6.14.7' pkg_resources.require(requirement) import cx_Freeze except pkg_resources.ResolutionError: From 4d7bd929bc4a8c5ec284f7f697fe50daaf010ad4 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 13 Mar 2023 16:18:52 +0100 Subject: [PATCH 054/172] WebHost: update modules --- WebHostLib/requirements.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt index 2397bf91b4..d5c1719863 100644 --- a/WebHostLib/requirements.txt +++ b/WebHostLib/requirements.txt @@ -1,7 +1,7 @@ -flask>=2.2.2 +flask>=2.2.3 pony>=0.7.16 waitress>=2.1.2 -Flask-Caching>=2.0.1 +Flask-Caching>=2.0.2 Flask-Compress>=1.13 -Flask-Limiter>=2.8.1 -bokeh>=3.0.2 +Flask-Limiter>=3.3.0 +bokeh>=3.1.0 From df55455fc0507fe4d4535b062d34c3acfba567f8 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Mon, 13 Mar 2023 18:40:55 -0400 Subject: [PATCH 055/172] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20Version=203=20(#1?= =?UTF-8?q?520)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Coin items received or found in the Game Corner are now shuffled, locations require Coin Case * Prizesanity option (shuffle Game Corner Prizes) * DexSanity option: location checks for marking Pokémon as caught in your Pokédex. Also an option to set all Pokémon in your Pokédex as seen from the start, to aid in locating them. * Option to randomize the layout of the Rock Tunnel. * Area 1-to-1 mapping: When one instance of a Wild Pokémon in a given area is randomized, all instances of that Pokémon will be the same. So that if a route had 3 different Pokémon before, it will have 3 after randomization. * Option to randomize the moves taught by TMs. * Exact controls for TM/HM compatibility chances. * Option to randomize Pokémon's pallets or set them based on primary type. * Added Cinnabar Gym trainers to Trainersanity and randomized the quiz questions and answers. Getting a correct answer will flag the trainer as defeated so that you can obtain the Trainersanity check without defeating the trainer if you answer correctly. --- PokemonClient.py | 28 +- data/lua/PKMN_RB/pkmn_rb.lua | 79 +- worlds/pokemon_rb/__init__.py | 152 ++- worlds/pokemon_rb/basepatch_blue.bsdiff4 | Bin 36601 -> 38871 bytes worlds/pokemon_rb/basepatch_red.bsdiff4 | Bin 36602 -> 38878 bytes .../docs/en_Pokemon Red and Blue.md | 2 + worlds/pokemon_rb/items.py | 10 +- worlds/pokemon_rb/locations.py | 88 +- worlds/pokemon_rb/logic.py | 14 +- worlds/pokemon_rb/options.py | 169 ++- worlds/pokemon_rb/poke_data.py | 355 ++--- worlds/pokemon_rb/regions.py | 6 +- worlds/pokemon_rb/rock_tunnel.py | 294 ++++ worlds/pokemon_rb/rom.py | 487 ++++++- worlds/pokemon_rb/rom_addresses.py | 1177 +++++++++-------- worlds/pokemon_rb/rules.py | 72 +- worlds/pokemon_rb/text.py | 6 +- worlds/smz3/__init__.py | 4 +- 18 files changed, 2025 insertions(+), 918 deletions(-) create mode 100644 worlds/pokemon_rb/rock_tunnel.py diff --git a/PokemonClient.py b/PokemonClient.py index eb1f124391..e78e76fa00 100644 --- a/PokemonClient.py +++ b/PokemonClient.py @@ -17,7 +17,7 @@ from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandP from worlds.pokemon_rb.locations import location_data from worlds.pokemon_rb.rom import RedDeltaPatch, BlueDeltaPatch -location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}} +location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}, "DexSanityFlag": {}} location_bytes_bits = {} for location in location_data: if location.ram_address is not None: @@ -40,7 +40,7 @@ CONNECTION_INITIAL_STATUS = "Connection has not been initiated" DISPLAY_MSGS = True -SCRIPT_VERSION = 1 +SCRIPT_VERSION = 3 class GBCommandProcessor(ClientCommandProcessor): @@ -70,6 +70,8 @@ class GBContext(CommonContext): self.set_deathlink = False self.client_compatibility_mode = 0 self.items_handling = 0b001 + self.sent_release = False + self.sent_collect = False async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: @@ -124,7 +126,8 @@ def get_payload(ctx: GBContext): "items": [item.item for item in ctx.items_received], "messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items() if key[0] > current_time - 10}, - "deathlink": ctx.deathlink_pending + "deathlink": ctx.deathlink_pending, + "options": ((ctx.permissions['release'] in ('goal', 'enabled')) * 2) + (ctx.permissions['collect'] in ('goal', 'enabled')) } ) ctx.deathlink_pending = False @@ -134,10 +137,13 @@ def get_payload(ctx: GBContext): async def parse_locations(data: List, ctx: GBContext): locations = [] flags = {"EventFlag": data[:0x140], "Missable": data[0x140:0x140 + 0x20], - "Hidden": data[0x140 + 0x20: 0x140 + 0x20 + 0x0E], "Rod": data[0x140 + 0x20 + 0x0E:]} + "Hidden": data[0x140 + 0x20: 0x140 + 0x20 + 0x0E], + "Rod": data[0x140 + 0x20 + 0x0E:0x140 + 0x20 + 0x0E + 0x01]} - if len(flags['Rod']) > 1: - return + if len(data) > 0x140 + 0x20 + 0x0E + 0x01: + flags["DexSanityFlag"] = data[0x140 + 0x20 + 0x0E + 0x01:] + else: + flags["DexSanityFlag"] = [0] * 19 for flag_type, loc_map in location_map.items(): for flag, loc_id in loc_map.items(): @@ -207,6 +213,16 @@ async def gb_sync_task(ctx: GBContext): async_start(parse_locations(data_decoded['locations'], ctx)) if 'deathLink' in data_decoded and data_decoded['deathLink'] and 'DeathLink' in ctx.tags: await ctx.send_death(ctx.auth + " is out of usable Pokémon! " + ctx.auth + " blacked out!") + if 'options' in data_decoded: + msgs = [] + if data_decoded['options'] & 4 and not ctx.sent_release: + ctx.sent_release = True + msgs.append({"cmd": "Say", "text": "!release"}) + if data_decoded['options'] & 8 and not ctx.sent_collect: + ctx.sent_collect = True + msgs.append({"cmd": "Say", "text": "!collect"}) + if msgs: + await ctx.send_msgs(msgs) if ctx.set_deathlink: await ctx.update_death_link(True) except asyncio.TimeoutError: diff --git a/data/lua/PKMN_RB/pkmn_rb.lua b/data/lua/PKMN_RB/pkmn_rb.lua index eaf7516547..036f7a6255 100644 --- a/data/lua/PKMN_RB/pkmn_rb.lua +++ b/data/lua/PKMN_RB/pkmn_rb.lua @@ -7,7 +7,7 @@ local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected" local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made" local STATE_UNINITIALIZED = "Uninitialized" -local SCRIPT_VERSION = 1 +local SCRIPT_VERSION = 3 local APIndex = 0x1A6E local APDeathLinkAddress = 0x00FD @@ -16,7 +16,8 @@ local EventFlagAddress = 0x1735 local MissableAddress = 0x161A local HiddenItemsAddress = 0x16DE local RodAddress = 0x1716 -local InGame = 0x1A71 +local DexSanityAddress = 0x1A71 +local InGameAddress = 0x1A84 local ClientCompatibilityAddress = 0xFF00 local ItemsReceived = nil @@ -34,6 +35,7 @@ local frame = 0 local u8 = nil local wU8 = nil local u16 +local compat = nil local function defineMemoryFunctions() local memDomain = {} @@ -70,18 +72,6 @@ function slice (tbl, s, e) return new end -function processBlock(block) - if block == nil then - return - end - local itemsBlock = block["items"] - memDomain.wram() - if itemsBlock ~= nil then - ItemsReceived = itemsBlock - end - deathlink_rec = block["deathlink"] -end - function difference(a, b) local aa = {} for k,v in pairs(a) do aa[v]=true end @@ -99,6 +89,7 @@ function generateLocationsChecked() events = uRange(EventFlagAddress, 0x140) missables = uRange(MissableAddress, 0x20) hiddenitems = uRange(HiddenItemsAddress, 0x0E) + dexsanity = uRange(DexSanityAddress, 19) rod = u8(RodAddress) data = {} @@ -108,6 +99,9 @@ function generateLocationsChecked() table.foreach(hiddenitems, function(k, v) table.insert(data, v) end) table.insert(data, rod) + if compat > 1 then + table.foreach(dexsanity, function(k, v) table.insert(data, v) end) + end return data end @@ -141,7 +135,15 @@ function receive() return end if l ~= nil then - processBlock(json.decode(l)) + block = json.decode(l) + if block ~= nil then + local itemsBlock = block["items"] + if itemsBlock ~= nil then + ItemsReceived = itemsBlock + end + deathlink_rec = block["deathlink"] + + end end -- Determine Message to send back memDomain.rom() @@ -156,15 +158,31 @@ function receive() seedName = newSeedName local retTable = {} retTable["scriptVersion"] = SCRIPT_VERSION - retTable["clientCompatibilityVersion"] = u8(ClientCompatibilityAddress) + + if compat == nil then + compat = u8(ClientCompatibilityAddress) + if compat < 2 then + InGameAddress = 0x1A71 + end + end + + retTable["clientCompatibilityVersion"] = compat retTable["playerName"] = playerName retTable["seedName"] = seedName memDomain.wram() - if u8(InGame) == 0xAC then + + in_game = u8(InGameAddress) + if in_game == 0x2A or in_game == 0xAC then retTable["locations"] = generateLocationsChecked() + elseif in_game ~= 0 then + print("Game may have crashed") + curstate = STATE_UNINITIALIZED + return end + retTable["deathLink"] = deathlink_send deathlink_send = false + msg = json.encode(retTable).."\n" local ret, error = gbSocket:send(msg) if ret == nil then @@ -193,16 +211,23 @@ function main() if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then if (frame % 5 == 0) then receive() - if u8(InGame) == 0xAC and u8(APItemAddress) == 0x00 then - ItemIndex = u16(APIndex) - if deathlink_rec == true then - wU8(APDeathLinkAddress, 1) - elseif u8(APDeathLinkAddress) == 3 then - wU8(APDeathLinkAddress, 0) - deathlink_send = true - end - if ItemsReceived[ItemIndex + 1] ~= nil then - wU8(APItemAddress, ItemsReceived[ItemIndex + 1] - 172000000) + in_game = u8(InGameAddress) + if in_game == 0x2A or in_game == 0xAC then + if u8(APItemAddress) == 0x00 then + ItemIndex = u16(APIndex) + if deathlink_rec == true then + wU8(APDeathLinkAddress, 1) + elseif u8(APDeathLinkAddress) == 3 then + wU8(APDeathLinkAddress, 0) + deathlink_send = true + end + if ItemsReceived[ItemIndex + 1] ~= nil then + item_id = ItemsReceived[ItemIndex + 1] - 172000000 + if item_id > 255 then + item_id = item_id - 256 + end + wU8(APItemAddress, item_id) + end end end end diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index 88813ad940..cea26d4639 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -15,7 +15,7 @@ from .options import pokemon_rb_options from .rom_addresses import rom_addresses from .text import encode_text from .rom import generate_output, get_base_rom_bytes, get_base_rom_path, process_pokemon_data, process_wild_pokemon,\ - process_static_pokemon + process_static_pokemon, process_move_data from .rules import set_rules import worlds.pokemon_rb.poke_data as poke_data @@ -40,13 +40,14 @@ class PokemonRedBlueWorld(World): game = "Pokemon Red and Blue" option_definitions = pokemon_rb_options - data_version = 5 - required_client_version = (0, 3, 7) + data_version = 7 + required_client_version = (0, 3, 9) topology_present = False item_name_to_id = {name: data.id for name, data in item_table.items()} - location_name_to_id = {location.name: location.address for location in location_data if location.type == "Item"} + location_name_to_id = {location.name: location.address for location in location_data if location.type == "Item" + and location.address is not None} item_name_groups = item_groups web = PokemonWebWorld() @@ -58,11 +59,14 @@ class PokemonRedBlueWorld(World): self.extra_badges = {} self.type_chart = None self.local_poke_data = None + self.local_move_data = None + self.local_tms = None self.learnsets = None self.trainer_name = None self.rival_name = None self.type_chart = None self.traps = None + self.trade_mons = {} @classmethod def stage_assert_generate(cls, multiworld: MultiWorld): @@ -94,6 +98,12 @@ class PokemonRedBlueWorld(World): if len(self.multiworld.player_name[self.player].encode()) > 16: raise Exception(f"Player name too long for {self.multiworld.get_player_name(self.player)}. Player name cannot exceed 16 bytes for Pokémon Red and Blue.") + if (self.multiworld.dexsanity[self.player] and self.multiworld.accessibility[self.player] == "locations" + and (self.multiworld.catch_em_all[self.player] != "all_pokemon" + or self.multiworld.randomize_wild_pokemon[self.player] == "vanilla" + or self.multiworld.randomize_legendary_pokemon[self.player] != "any")): + self.multiworld.accessibility[self.player] = self.multiworld.accessibility[self.player].from_text("items") + if self.multiworld.badges_needed_for_hm_moves[self.player].value >= 2: badges_to_add = ["Marsh Badge", "Volcano Badge", "Earth Badge"] if self.multiworld.badges_needed_for_hm_moves[self.player].value == 3: @@ -107,6 +117,7 @@ class PokemonRedBlueWorld(World): for badge in badges_to_add: self.extra_badges[hm_moves.pop()] = badge + process_move_data(self) process_pokemon_data(self) if self.multiworld.randomize_type_chart[self.player] == "vanilla": @@ -178,8 +189,13 @@ class PokemonRedBlueWorld(World): if self.multiworld.randomize_pokedex[self.player] == "start_with": start_inventory["Pokedex"] = 1 self.multiworld.push_precollected(self.create_item("Pokedex")) + locations = [location for location in location_data if location.type == "Item"] item_pool = [] + combined_traps = (self.multiworld.poison_trap_weight[self.player].value + + self.multiworld.fire_trap_weight[self.player].value + + self.multiworld.paralyze_trap_weight[self.player].value + + self.multiworld.ice_trap_weight[self.player].value) for location in locations: if not location.inclusion(self.multiworld, self.player): continue @@ -189,9 +205,18 @@ class PokemonRedBlueWorld(World): item = self.create_filler() elif location.original_item is None: item = self.create_filler() + elif location.original_item == "Pokedex": + if self.multiworld.randomize_pokedex[self.player] == "vanilla": + self.multiworld.get_location(location.name, self.player).event = True + location.event = True + item = self.create_item("Pokedex") + elif location.original_item.startswith("TM"): + if self.multiworld.randomize_tm_moves[self.player]: + item = self.create_item(location.original_item.split(" ")[0]) + else: + item = self.create_item(location.original_item) else: item = self.create_item(location.original_item) - combined_traps = self.multiworld.poison_trap_weight[self.player].value + self.multiworld.fire_trap_weight[self.player].value + self.multiworld.paralyze_trap_weight[self.player].value + self.multiworld.ice_trap_weight[self.player].value if (item.classification == ItemClassification.filler and self.multiworld.random.randint(1, 100) <= self.multiworld.trap_percentage[self.player].value and combined_traps != 0): item = self.create_item(self.select_trap()) @@ -205,9 +230,62 @@ class PokemonRedBlueWorld(World): self.multiworld.itempool += item_pool def pre_fill(self) -> None: - process_wild_pokemon(self) process_static_pokemon(self) + pokemon_locs = [location.name for location in location_data if location.type != "Item"] + for location in self.multiworld.get_locations(self.player): + if location.name in pokemon_locs: + location.show_in_spoiler = False + + def intervene(move): + accessible_slots = [loc for loc in self.multiworld.get_reachable_locations(test_state, self.player) if loc.type == "Wild Encounter"] + move_bit = pow(2, poke_data.hm_moves.index(move) + 2) + viable_mons = [mon for mon in self.local_poke_data if self.local_poke_data[mon]["tms"][6] & move_bit] + placed_mons = [slot.item.name for slot in accessible_slots] + # this sort method doesn't seem to work if you reference the same list being sorted in the lambda + placed_mons_copy = placed_mons.copy() + placed_mons.sort(key=lambda i: placed_mons_copy.count(i)) + placed_mon = placed_mons.pop() + if self.multiworld.area_1_to_1_mapping[self.player]: + zone = " - ".join(placed_mon.split(" - ")[:-1]) + replace_slots = [slot for slot in accessible_slots if slot.name.startswith(zone) and slot.item.name == + placed_mon] + else: + replace_slots = [self.multiworld.random.choice([slot for slot in accessible_slots if slot.item.name == + placed_mon])] + replace_mon = self.multiworld.random.choice(viable_mons) + for replace_slot in replace_slots: + replace_slot.item = self.create_item(replace_mon) + last_intervene = None + while True: + intervene_move = None + test_state = self.multiworld.get_all_state(False) + if not self.multiworld.badgesanity[self.player]: + for badge in ["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Soul Badge", + "Marsh Badge", "Volcano Badge", "Earth Badge"]: + test_state.collect(self.create_item(badge)) + if not test_state.pokemon_rb_can_surf(self.player): + intervene_move = "Surf" + if not test_state.pokemon_rb_can_strength(self.player): + intervene_move = "Strength" + # cut may not be needed if accessibility is minimal, unless you need all 8 badges and badgesanity is off, + # as you will require cut to access celadon gyn + if (self.multiworld.accessibility[self.player] != "minimal" or ((not + self.multiworld.badgesanity[self.player]) and max(self.multiworld.elite_four_condition[self.player], + self.multiworld.victory_road_condition[self.player]) > 7)): + if not test_state.pokemon_rb_can_cut(self.player): + intervene_move = "Cut" + if (self.multiworld.accessibility[self.player].current_key != "minimal" and + (self.multiworld.trainersanity[self.player] or self.multiworld.extra_key_items[self.player])): + if not test_state.pokemon_rb_can_flash(self.player): + intervene_move = "Flash" + if intervene_move: + if intervene_move == last_intervene: + raise Exception(f"Caught in infinite loop attempting to ensure {intervene_move} is available to player {self.player}") + intervene(intervene_move) + last_intervene = intervene_move + else: + break if self.multiworld.old_man[self.player].value == 1: self.multiworld.local_early_items[self.player]["Oak's Parcel"] = 1 @@ -237,17 +315,26 @@ class PokemonRedBlueWorld(World): else: raise FillError(f"Failed to place badges for player {self.player}") - locs = [self.multiworld.get_location("Fossil - Choice A", self.player), - self.multiworld.get_location("Fossil - Choice B", self.player)] - for loc in locs: - add_item_rule(loc, lambda i: i.advancement or i.name in self.item_name_groups["Unique"] - or i.name == "Master Ball") + # Place local items in some locations to prevent save-scumming. Also Oak's PC to prevent an "AP Item" from + # entering the player's inventory. + + locs = {self.multiworld.get_location("Fossil - Choice A", self.player), + self.multiworld.get_location("Fossil - Choice B", self.player)} + + if self.multiworld.dexsanity[self.player]: + for mon in ([" ".join(self.multiworld.get_location( + f"Pallet Town - Starter {i}", self.player).item.name.split(" ")[1:]) for i in range(1, 4)] + + [" ".join(self.multiworld.get_location( + f"Fighting Dojo - Gift {i}", self.player).item.name.split(" ")[1:]) for i in range(1, 3)]): + loc = self.multiworld.get_location(f"Pokedex - {mon}", self.player) + if loc.item is None: + locs.add(loc) loc = self.multiworld.get_location("Pallet Town - Player's PC", self.player) if loc.item is None: - locs.append(loc) + locs.add(loc) - for loc in locs: + for loc in sorted(locs): unplaced_items = [] if loc.name in self.multiworld.priority_locations[self.player].value: add_item_rule(loc, lambda i: i.advancement) @@ -262,21 +349,6 @@ class PokemonRedBlueWorld(World): unplaced_items.append(item) self.multiworld.itempool += unplaced_items - intervene = False - test_state = self.multiworld.get_all_state(False) - if not test_state.pokemon_rb_can_surf(self.player) or not test_state.pokemon_rb_can_strength(self.player): - intervene = True - elif self.multiworld.accessibility[self.player].current_key != "minimal": - if not test_state.pokemon_rb_can_cut(self.player) or not test_state.pokemon_rb_can_flash(self.player): - intervene = True - if intervene: - # the way this is handled will be improved significantly in the future when I add options to - # let you choose the exact weights for HM compatibility - logging.warning( - f"HM-compatible Pokémon possibly missing, placing Mew on Route 1 for player {self.player}") - loc = self.multiworld.get_location("Route 1 - Wild Pokemon - 1", self.player) - loc.item = self.create_item("Mew") - def create_regions(self): if self.multiworld.free_fly_location[self.player].value: if self.multiworld.old_man[self.player].value == 0: @@ -317,6 +389,12 @@ class PokemonRedBlueWorld(World): spoiler_handle.write(f"\n\nType matchups ({self.multiworld.player_name[self.player]}):\n\n") for matchup in self.type_chart: spoiler_handle.write(f"{matchup[0]} deals {matchup[2] * 10}% damage to {matchup[1]}\n") + spoiler_handle.write(f"\n\nPokémon locations ({self.multiworld.player_name[self.player]}):\n\n") + pokemon_locs = [location.name for location in location_data if location.type != "Item"] + for location in self.multiworld.get_locations(self.player): + if location.name in pokemon_locs: + spoiler_handle.write(location.name + ": " + location.item.name + "\n") + def get_filler_item_name(self) -> str: combined_traps = self.multiworld.poison_trap_weight[self.player].value + self.multiworld.fire_trap_weight[self.player].value + self.multiworld.paralyze_trap_weight[self.player].value + self.multiworld.ice_trap_weight[self.player].value @@ -336,6 +414,21 @@ class PokemonRedBlueWorld(World): self.traps += ["Ice Trap"] * self.multiworld.ice_trap_weight[self.player].value return self.multiworld.random.choice(self.traps) + def extend_hint_information(self, hint_data): + if self.multiworld.dexsanity[self.player]: + hint_data[self.player] = {} + mon_locations = {mon: set() for mon in poke_data.pokemon_data.keys()} + for loc in location_data: #self.multiworld.get_locations(self.player): + if loc.type in ["Wild Encounter", "Static Pokemon", "Legendary Pokemon", "Missable Pokemon"]: + mon = self.multiworld.get_location(loc.name, self.player).item.name + if mon.startswith("Static ") or mon.startswith("Missable "): + mon = " ".join(mon.split(" ")[1:]) + mon_locations[mon].add(loc.name.split(" -")[0]) + for mon in mon_locations: + if mon_locations[mon]: + hint_data[self.player][self.multiworld.get_location(f"Pokedex - {mon}", self.player).address] = \ + ", ".join(mon_locations[mon]) + def fill_slot_data(self) -> dict: return { "second_fossil_check_condition": self.multiworld.second_fossil_check_condition[self.player].value, @@ -358,7 +451,8 @@ class PokemonRedBlueWorld(World): "type_chart": self.type_chart, "randomize_pokedex": self.multiworld.randomize_pokedex[self.player].value, "trainersanity": self.multiworld.trainersanity[self.player].value, - "death_link": self.multiworld.death_link[self.player].value + "death_link": self.multiworld.death_link[self.player].value, + "prizesanity": self.multiworld.prizesanity[self.player].value } diff --git a/worlds/pokemon_rb/basepatch_blue.bsdiff4 b/worlds/pokemon_rb/basepatch_blue.bsdiff4 index 6932762f4d5f7374a19b00060fb3614defc9d4cc..6123d714151c7543e02cb834abeb42db32b7f87a 100644 GIT binary patch literal 38871 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 literal 36601 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

D8=XTmlIrNPH?6tWHiJPSn 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 0 @@ -20,14 +26,13 @@ def extra_key_items(multiworld, player): return multiworld.extra_key_items[player] -def pokedex(multiworld, player): - return multiworld.randomize_pokedex[player].value > 0 - - def always_on(multiworld, player): return True +def prizesanity(multiworld, player): + return multiworld.prizesanity[player] + class LocationData: @@ -72,6 +77,13 @@ class Rod: self.flag = flag +class DexSanityFlag: + def __init__(self, flag): + self.byte = int(flag / 8) + self.bit = flag % 8 + self.flag = flag + + location_data = [ LocationData("Vermilion City", "Fishing Guru", "Old Rod", rom_addresses["Rod_Vermilion_City_Fishing_Guru"], Rod(3)), @@ -119,7 +131,7 @@ location_data = [ LocationData("Celadon City", "Gambling Addict", "Coin Case", rom_addresses["Event_Gambling_Addict"], EventFlag(480)), LocationData("Celadon Gym", "Erika 2", "TM21 Mega Drain", rom_addresses["Event_Celadon_Gym"], EventFlag(424)), - LocationData("Silph Co 11F", "Silph Co President", "Master Ball", rom_addresses["Event_Silph_Co_President"], + LocationData("Silph Co 11F", "Silph Co President (Card Key)", "Master Ball", rom_addresses["Event_Silph_Co_President"], EventFlag(1933)), LocationData("Silph Co 2F", "Woman", "TM36 Self-Destruct", rom_addresses["Event_Scared_Woman"], EventFlag(1791)), @@ -374,7 +386,7 @@ location_data = [ LocationData("Seafoam Islands B4F", "Hidden Item Corner Island", "Ultra Ball", rom_addresses['Hidden_Item_Seafoam_Islands_B4F'], Hidden(26), inclusion=hidden_items), LocationData("Pokemon Mansion 1F", "Hidden Item Block Near Entrance Carpet", "Moon Stone", rom_addresses['Hidden_Item_Pokemon_Mansion_1F'], Hidden(27), inclusion=hidden_items), LocationData("Pokemon Mansion 3F", "Hidden Item Behind Burglar", "Max Revive", rom_addresses['Hidden_Item_Pokemon_Mansion_3F'], Hidden(28), inclusion=hidden_items), - LocationData("Route 23", "Hidden Item Rocks Before Final Guard", "Full Restore", rom_addresses['Hidden_Item_Route_23_1'], Hidden(29), inclusion=hidden_items), + LocationData("Route 23", "Hidden Item Rocks Before Victory Road", "Full Restore", rom_addresses['Hidden_Item_Route_23_1'], Hidden(29), inclusion=hidden_items), LocationData("Route 23", "Hidden Item East Bush After Water", "Ultra Ball", rom_addresses['Hidden_Item_Route_23_2'], Hidden(30), inclusion=hidden_items), LocationData("Route 23", "Hidden Item On Island", "Max Ether", rom_addresses['Hidden_Item_Route_23_3'], Hidden(31), inclusion=hidden_items), LocationData("Victory Road 2F", "Hidden Item Rock Before Moltres", "Ultra Ball", rom_addresses['Hidden_Item_Victory_Road_2F_1'], Hidden(32), inclusion=hidden_items), @@ -400,7 +412,8 @@ location_data = [ LocationData("Cerulean City", "Hidden Item Gym Badge Guy's Backyard", "Rare Candy", rom_addresses['Hidden_Item_Cerulean_City'], Hidden(52), inclusion=hidden_items), LocationData("Route 4", "Hidden Item Plateau East Of Mt Moon", "Great Ball", rom_addresses['Hidden_Item_Route_4'], Hidden(53), inclusion=hidden_items), - LocationData("Pallet Town", "Oak's Parcel Reward", "Pokedex", rom_addresses["Event_Pokedex"], EventFlag(0x38), inclusion=pokedex), + + LocationData("Pallet Town", "Oak's Parcel Reward", "Pokedex", rom_addresses["Event_Pokedex"], EventFlag(0x38)), LocationData("Pokemon Mansion 1F", "Scientist", None, rom_addresses["Trainersanity_EVENT_BEAT_MANSION_1_TRAINER_0_ITEM"], EventFlag(376), inclusion=trainersanity), LocationData("Pokemon Mansion 2F", "Burglar", None, rom_addresses["Trainersanity_EVENT_BEAT_MANSION_2_TRAINER_0_ITEM"], EventFlag(43), inclusion=trainersanity), @@ -712,6 +725,36 @@ location_data = [ LocationData("Indigo Plateau", "Bruno", None, rom_addresses["Trainersanity_EVENT_BEAT_BRUNOS_ROOM_TRAINER_0_ITEM"], EventFlag(20), inclusion=trainersanity), LocationData("Indigo Plateau", "Agatha", None, rom_addresses["Trainersanity_EVENT_BEAT_AGATHAS_ROOM_TRAINER_0_ITEM"], EventFlag(19), inclusion=trainersanity), LocationData("Indigo Plateau", "Lance", None, rom_addresses["Trainersanity_EVENT_BEAT_LANCES_ROOM_TRAINER_0_ITEM"], EventFlag(18), inclusion=trainersanity), + LocationData("Cinnabar Gym", "Burglar 1", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_A_ITEM"], EventFlag(374), inclusion=trainersanity), + LocationData("Cinnabar Gym", "Super Nerd 1", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_B_ITEM"], EventFlag(373), inclusion=trainersanity), + LocationData("Cinnabar Gym", "Super Nerd 2", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_2_ITEM"], EventFlag(372), inclusion=trainersanity), + LocationData("Cinnabar Gym", "Burglar 2", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_3_ITEM"], EventFlag(371), inclusion=trainersanity), + LocationData("Cinnabar Gym", "Super Nerd 3", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_4_ITEM"], EventFlag(370), inclusion=trainersanity), + LocationData("Cinnabar Gym", "Super Nerd 4", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_5_ITEM"], EventFlag(369), inclusion=trainersanity), + LocationData("Cinnabar Gym", "Super Nerd 5", None, rom_addresses["Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_6_ITEM"], EventFlag(368), inclusion=trainersanity), + + LocationData("Celadon Prize Corner", "Item Prize 1", "TM23 Dragon Rage", rom_addresses["Prize_Item_A"], EventFlag(0x69a), inclusion=prizesanity), + LocationData("Celadon Prize Corner", "Item Prize 2", "TM15 Hyper Beam", rom_addresses["Prize_Item_B"], EventFlag(0x69B), inclusion=prizesanity), + LocationData("Celadon Prize Corner", "Item Prize 3", "TM50 Substitute", rom_addresses["Prize_Item_C"], EventFlag(0x69C), inclusion=prizesanity), + + LocationData("Celadon Game Corner", "West Gambler's Gift (Coin Case)", "10 Coins", rom_addresses["Event_Game_Corner_Gift_A"], EventFlag(0x1ba)), + LocationData("Celadon Game Corner", "Center Gambler's Gift (Coin Case)", "20 Coins", rom_addresses["Event_Game_Corner_Gift_C"], EventFlag(0x1bc)), + LocationData("Celadon Game Corner", "East Gambler's Gift (Coin Case)", "20 Coins", rom_addresses["Event_Game_Corner_Gift_B"], EventFlag(0x1bb)), + + LocationData("Celadon Game Corner", "Hidden Item Northwest By Counter (Coin Case)", "10 Coins", rom_addresses["Hidden_Item_Game_Corner_1"], Hidden(54), inclusion=hidden_items), + LocationData("Celadon Game Corner", "Hidden Item Southwest Corner (Coin Case)", "10 Coins", rom_addresses["Hidden_Item_Game_Corner_2"], Hidden(55), inclusion=hidden_items), + LocationData("Celadon Game Corner", "Hidden Item Near Rumor Man (Coin Case)", "20 Coins", rom_addresses["Hidden_Item_Game_Corner_3"], Hidden(56), inclusion=hidden_items), + LocationData("Celadon Game Corner", "Hidden Item Near Speculating Woman (Coin Case)", "10 Coins", rom_addresses["Hidden_Item_Game_Corner_4"], Hidden(57), inclusion=hidden_items), + LocationData("Celadon Game Corner", "Hidden Item Near West Gifting Gambler (Coin Case)", "10 Coins", rom_addresses["Hidden_Item_Game_Corner_5"], Hidden(58), inclusion=hidden_items), + LocationData("Celadon Game Corner", "Hidden Item Near Wonderful Time Woman (Coin Case)", "20 Coins", rom_addresses["Hidden_Item_Game_Corner_6"], Hidden(59), inclusion=hidden_items), + LocationData("Celadon Game Corner", "Hidden Item Near Failing Gym Information Guy (Coin Case)", "10 Coins", rom_addresses["Hidden_Item_Game_Corner_7"], Hidden(60), inclusion=hidden_items), + LocationData("Celadon Game Corner", "Hidden Item Near East Gifting Gambler (Coin Case)", "10 Coins", rom_addresses["Hidden_Item_Game_Corner_8"], Hidden(61), inclusion=hidden_items), + LocationData("Celadon Game Corner", "Hidden Item Near Hooked Guy (Coin Case)", "10 Coins", rom_addresses["Hidden_Item_Game_Corner_9"], Hidden(62), inclusion=hidden_items), + LocationData("Celadon Game Corner", "Hidden Item at End of Horizontal Machine Row (Coin Case)", "20 Coins", rom_addresses["Hidden_Item_Game_Corner_10"], Hidden(63), inclusion=hidden_items), + LocationData("Celadon Game Corner", "Hidden Item in Front of Horizontal Machine Row (Coin Case)", "100 Coins", rom_addresses["Hidden_Item_Game_Corner_11"], Hidden(64), inclusion=hidden_items), + + *[LocationData("Pokedex", mon, None, rom_addresses["Dexsanity_Items"] + i, DexSanityFlag(i), + type="Item", inclusion=dexsanity) for (mon, i) in zip(pokemon_data.keys(), range(0, 152))], LocationData("Indigo Plateau", "Become Champion", "Become Champion", event=True), LocationData("Pokemon Tower 7F", "Fuji Saved", "Fuji Saved", event=True), @@ -1965,6 +2008,25 @@ location_data = [ LocationData("Cinnabar Island", "Dome Fossil Pokemon", "Kabuto", rom_addresses["Gift_Kabuto"], None, event=True, type="Static Pokemon"), + LocationData("Route 2 East", "Marcel Trade", "Mr Mime", rom_addresses["Trade_Marcel"] + 1, None, event=True, + type="Static Pokemon"), + LocationData("Underground Tunnel North-South", "Spot Trade", "Nidoran F", rom_addresses["Trade_Spot"] + 1, None, + event=True, type="Static Pokemon"), + LocationData("Route 11", "Terry Trade", "Nidorina", rom_addresses["Trade_Terry"] + 1, None, event=True, + type="Static Pokemon"), + LocationData("Route 18", "Marc Trade", "Lickitung", rom_addresses["Trade_Marc"] + 1, None, event=True, + type="Static Pokemon"), + LocationData("Cinnabar Island", "Sailor Trade", "Seel", rom_addresses["Trade_Sailor"] + 1, None, event=True, + type="Static Pokemon"), + LocationData("Cinnabar Island", "Crinkles Trade", "Tangela", rom_addresses["Trade_Crinkles"] + 1, None, + event=True, type="Static Pokemon"), + LocationData("Cinnabar Island", "Doris Trade", "Electrode", rom_addresses["Trade_Doris"] + 1, None, + event=True, type="Static Pokemon"), + LocationData("Vermilion City", "Dux Trade", "Farfetchd", rom_addresses["Trade_Dux"] + 1, None, event=True, + type="Static Pokemon"), + LocationData("Cerulean City", "Lola Trade", "Jynx", rom_addresses["Trade_Lola"] + 1, None, event=True, + type="Static Pokemon"), + # not counted for logic currently. Could perhaps make static encounters resettable in the future? LocationData("Power Plant", "Fake Pokeball Battle 1", "Voltorb", rom_addresses["Static_Encounter_Voltorb_A"], None, event=True, type="Missable Pokemon"), @@ -2043,20 +2105,24 @@ location_data = [ ] -for i, location in enumerate(location_data): + + +i = 0 +for location in location_data: if location.event or location.rom_address is None: location.address = None else: location.address = loc_id_start + i - + i += 1 class PokemonRBLocation(Location): game = "Pokemon Red and Blue" - def __init__(self, player, name, address, rom_address): + def __init__(self, player, name, address, rom_address, type): super(PokemonRBLocation, self).__init__( player, name, address ) - self.rom_address = rom_address \ No newline at end of file + self.rom_address = rom_address + self.type = type diff --git a/worlds/pokemon_rb/logic.py b/worlds/pokemon_rb/logic.py index 70e825c2b5..8425bcdb4b 100644 --- a/worlds/pokemon_rb/logic.py +++ b/worlds/pokemon_rb/logic.py @@ -45,14 +45,13 @@ class PokemonLogic(LogicMixin): ["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Soul Badge", "Marsh Badge", "Volcano Badge", "Earth Badge", "Bicycle", "Silph Scope", "Item Finder", "Super Rod", "Good Rod", "Old Rod", "Lift Key", "Card Key", "Town Map", "Coin Case", "S.S. Ticket", "Secret Key", - "Mansion Key", "Safari Pass", "Plant Key", "Hideout Key", "HM01 Cut", "HM02 Fly", "HM03 Surf", - "HM04 Strength", "HM05 Flash"] if self.has(item, player)]) >= count + "Poke Flute", "Mansion Key", "Safari Pass", "Plant Key", "Hideout Key", "HM01 Cut", "HM02 Fly", + "HM03 Surf", "HM04 Strength", "HM05 Flash"] if self.has(item, player)]) >= count def pokemon_rb_can_pass_guards(self, player): if self.multiworld.tea[player].value: return self.has("Tea", player) else: - # this could just be "True", but you never know what weird options I might introduce later ;) return self.can_reach("Celadon City - Counter Man", "Location", player) def pokemon_rb_has_badges(self, count, player): @@ -60,13 +59,8 @@ class PokemonLogic(LogicMixin): "Soul Badge", "Volcano Badge", "Earth Badge"] if self.has(item, player)]) >= count def pokemon_rb_oaks_aide(self, count, player): - if self.multiworld.randomize_pokedex[player].value > 0: - if not self.has("Pokedex", player): - return False - else: - if not self.has("Oak's Parcel", player): - return False - return self.pokemon_rb_has_pokemon(count, player) + return ((not self.multiworld.require_pokedex[player] or self.has("Pokedex", player)) + and self.pokemon_rb_has_pokemon(count, player)) def pokemon_rb_has_pokemon(self, count, player): obtained_pokemon = set() diff --git a/worlds/pokemon_rb/options.py b/worlds/pokemon_rb/options.py index ae51c47b32..b705e6f2f9 100644 --- a/worlds/pokemon_rb/options.py +++ b/worlds/pokemon_rb/options.py @@ -65,7 +65,7 @@ class CeruleanCaveCondition(Range): If extra_key_items is turned on, the number chosen will be increased by 4.""" display_name = "Cerulean Cave Condition" range_start = 0 - range_end = 25 + range_end = 26 default = 20 @@ -155,6 +155,12 @@ class RandomizeHiddenItems(Choice): default = 0 +class PrizeSanity(Toggle): + """Shuffles the TM prizes at the Celadon Prize Corner into the item pool.""" + display_name = "Prizesanity" + default = 0 + + class TrainerSanity(Toggle): """Add a location check to every trainer in the game, which can be obtained by talking to a trainer after defeating them. Does not affect gym leaders and some scripted event battles (including all Rival, Giovanni, and @@ -163,12 +169,44 @@ class TrainerSanity(Toggle): default = 0 +class RequirePokedex(Toggle): + """Require the Pokedex to obtain items from Oak's Aides or from Dexsanity checks.""" + display_name = "Require Pokedex" + default = 1 + + +class AllPokemonSeen(Toggle): + """Start with all Pokemon "seen" in your Pokedex. This allows you to see where Pokemon can be encountered in the + wild. Pokemon found by fishing or in the Cerulean Cave are not displayed.""" + default = 0 + + +class DexSanity(Toggle): + """Adds a location check for each Pokemon flagged "Owned" on your Pokedex. If accessibility is set to `locations` + and randomize_wild_pokemon is off, catch_em_all is not `all_pokemon` or randomize_legendary_pokemon is not `any`, + accessibility will be forced to `items` instead, as not all Dexsanity locations can be guaranteed to be considered + reachable in logic. + If Pokedex is required, the items for Pokemon acquired before acquiring the Pokedex can be found by talking to + Professor Oak or evaluating the Pokedex via Oak's PC.""" + display_name = "Dexsanity" + default = 0 + + class FreeFlyLocation(Toggle): """One random fly destination will be unlocked by default.""" display_name = "Free Fly Location" default = 1 +class RandomizeRockTunnel(Toggle): + """Randomize the layout of Rock Tunnel. This is highly experimental, if you encounter any issues (items or trainers + unreachable, trainers walking over walls, inability to reach end of tunnel, anything looking strange) to + Alchav#8826 in the Archipelago Discord (directly or in #pkmn-red-blue) along with the seed number found on the + signs outside the tunnel.""" + display_name = "Randomize Rock Tunnel" + default = 0 + + class OaksAidRt2(Range): """Number of Pokemon registered in the Pokedex required to receive the item from Oak's Aide on Route 2. Vanilla is 10.""" @@ -229,6 +267,12 @@ class RandomizeWildPokemon(Choice): option_completely_random = 4 +class Area1To1Mapping(Toggle): + """When randomizing wild Pokemon, for each zone, all instances of a particular Pokemon will be replaced with the + same Pokemon, resulting in fewer Pokemon in each area.""" + default = 1 + + class RandomizeStarterPokemon(Choice): """Randomize the starter Pokemon choices.""" display_name = "Randomize Starter Pokemon" @@ -334,6 +378,13 @@ class MinimumCatchRate(Range): default = 3 +class MoveBalancing(Toggle): + """All one-hit-KO moves and fixed-damage moves become normal damaging moves. + Blizzard, and moves that cause sleep have their accuracy reduced.""" + display_name = "Move Balancing" + default = 0 + + class RandomizePokemonMovesets(Choice): """Randomize the moves learned by Pokemon. prefer_types will prefer moves that match the type of the Pokemon.""" display_name = "Randomize Pokemon Movesets" @@ -343,6 +394,12 @@ class RandomizePokemonMovesets(Choice): default = 0 +class ConfineTranstormToDitto(Toggle): + """Regardless of moveset randomization, will keep Ditto's first move as Transform no others will learn it. + If an enemy Pokemon uses transform before you catch it, it will permanently change to Ditto after capture.""" + display_name = "Confine Transform to Ditto" + default = 1 + class StartWithFourMoves(Toggle): """If movesets are randomized, this will give all Pokemon 4 starting moves.""" display_name = "Start With Four Moves" @@ -356,30 +413,62 @@ class SameTypeAttackBonus(Toggle): default = 1 -class TMCompatibility(Choice): - """Randomize which Pokemon can learn each TM. prefer_types: 90% chance if Pokemon's type matches the move, - 50% chance if move is Normal type and the Pokemon is not, and 25% chance otherwise. Pokemon will retain the same - TM compatibility when they evolve if the evolved form has the same type(s). Mew will always be able to learn - every TM.""" - display_name = "TM Compatibility" - default = 0 - option_vanilla = 0 - option_prefer_types = 1 - option_completely_random = 2 - option_full_compatibility = 3 +class RandomizeTMMoves(Toggle): + """Randomize the moves taught by TMs. + All TM items will be flagged as 'filler' items regardless of how good the move they teach are.""" + display_name = "Randomize TM Moves" -class HMCompatibility(Choice): - """Randomize which Pokemon can learn each HM. prefer_types: 100% chance if Pokemon's type matches the move, - 75% chance if move is Normal type and the Pokemon is not, and 25% chance otherwise. Pokemon will retain the same - HM compatibility when they evolve if the evolved form has the same type(s). Mew will always be able to learn - every HM.""" - display_name = "HM Compatibility" - default = 0 - option_vanilla = 0 - option_prefer_types = 1 - option_completely_random = 2 - option_full_compatibility = 3 +class TMHMCompatibility(SpecialRange): + range_start = -1 + range_end = 100 + special_range_names = { + "vanilla": -1, + "none": 0, + "full": 100 + } + default = -1 + + +class TMSameTypeCompatibility(TMHMCompatibility): + """Chance of each TM being usable on each Pokemon whose type matches the move.""" + display_name = "TM Same-Type Compatibility" + + +class TMNormalTypeCompatibility(TMHMCompatibility): + """Chance of each TM being usable on each Pokemon if the move is Normal type and the Pokemon is not.""" + display_name = "TM Normal-Type Compatibility" + + +class TMOtherTypeCompatibility(TMHMCompatibility): + """Chance of each TM being usable on each Pokemon if the move a type other than Normal or one of the Pokemon's types.""" + display_name = "TM Other-Type Compatibility" + + +class HMSameTypeCompatibility(TMHMCompatibility): + """Chance of each HM being usable on each Pokemon whose type matches the move. + At least one Pokemon will always be able to learn the moves needed to meet your accessibility requirements.""" + display_name = "HM Same-Type Compatibility" + + +class HMNormalTypeCompatibility(TMHMCompatibility): + """Chance of each HM being usable on each Pokemon if the move is Normal type and the Pokemon is not. + At least one Pokemon will always be able to learn the moves needed to meet your accessibility requirements.""" + display_name = "HM Normal-Type Compatibility" + + +class HMOtherTypeCompatibility(TMHMCompatibility): + """Chance of each HM being usable on each Pokemon if the move a type other than Normal or one of the Pokemon's types. + At least one Pokemon will always be able to learn the moves needed to meet your accessibility requirements.""" + display_name = "HM Other-Type Compatibility" + + +class InheritTMHMCompatibility(Toggle): + """If on, evolved Pokemon will inherit their pre-evolved form's TM and HM compatibilities. + They will then roll the above set chances again at a 50% lower rate for all TMs and HMs their predecessor could not + learn, unless the evolved form has additional or different types, then moves of those new types will be rolled + at the full set chance.""" + display_name = "Inherit TM/HM Compatibility" class RandomizePokemonTypes(Choice): @@ -543,6 +632,17 @@ class IceTrapWeight(TrapWeight): default = 0 +class RandomizePokemonPalettes(Choice): + """Modify palettes of Pokemon. Primary Type will set Pokemons' palettes based on their primary type, Follow + Evolutions will randomize palettes but palettes will remain the same through evolutions (except Eeveelutions), + Completely Random will randomize all Pokemons' palettes individually""" + display_name = "Randomize Pokemon Palettes" + option_vanilla = 0 + option_primary_type = 1 + option_follow_evolutions = 2 + option_completely_random = 3 + + pokemon_rb_options = { "game_version": GameVersion, "trainer_name": TrainerName, @@ -561,16 +661,22 @@ pokemon_rb_options = { "extra_strength_boulders": ExtraStrengthBoulders, "require_item_finder": RequireItemFinder, "randomize_hidden_items": RandomizeHiddenItems, + "prizesanity": PrizeSanity, "trainersanity": TrainerSanity, - "badges_needed_for_hm_moves": BadgesNeededForHMMoves, - "free_fly_location": FreeFlyLocation, + "require_pokedex": RequirePokedex, + "all_pokemon_seen": AllPokemonSeen, + "dexsanity": DexSanity, "oaks_aide_rt_2": OaksAidRt2, "oaks_aide_rt_11": OaksAidRt11, "oaks_aide_rt_15": OaksAidRt15, + "badges_needed_for_hm_moves": BadgesNeededForHMMoves, + "free_fly_location": FreeFlyLocation, + "randomize_rock_tunnel": RandomizeRockTunnel, "blind_trainers": BlindTrainers, "minimum_steps_between_encounters": MinimumStepsBetweenEncounters, "exp_modifier": ExpModifier, "randomize_wild_pokemon": RandomizeWildPokemon, + "area_1_to_1_mapping": Area1To1Mapping, "randomize_starter_pokemon": RandomizeStarterPokemon, "randomize_static_pokemon": RandomizeStaticPokemon, "randomize_legendary_pokemon": RandomizeLegendaryPokemon, @@ -580,11 +686,19 @@ pokemon_rb_options = { "minimum_catch_rate": MinimumCatchRate, "randomize_trainer_parties": RandomizeTrainerParties, "trainer_legendaries": TrainerLegendaries, + "move_balancing": MoveBalancing, "randomize_pokemon_movesets": RandomizePokemonMovesets, + "confine_transform_to_ditto": ConfineTranstormToDitto, "start_with_four_moves": StartWithFourMoves, "same_type_attack_bonus": SameTypeAttackBonus, - "tm_compatibility": TMCompatibility, - "hm_compatibility": HMCompatibility, + "randomize_tm_moves": RandomizeTMMoves, + "tm_same_type_compatibility": TMSameTypeCompatibility, + "tm_normal_type_compatibility": TMNormalTypeCompatibility, + "tm_other_type_compatibility": TMOtherTypeCompatibility, + "hm_same_type_compatibility": HMSameTypeCompatibility, + "hm_normal_type_compatibility": HMNormalTypeCompatibility, + "hm_other_type_compatibility": HMOtherTypeCompatibility, + "inherit_tm_hm_compatibility": InheritTMHMCompatibility, "randomize_pokemon_types": RandomizePokemonTypes, "secondary_type_chance": SecondaryTypeChance, "randomize_type_chart": RandomizeTypeChart, @@ -604,5 +718,6 @@ pokemon_rb_options = { "fire_trap_weight": FireTrapWeight, "paralyze_trap_weight": ParalyzeTrapWeight, "ice_trap_weight": IceTrapWeight, + "randomize_pokemon_palettes": RandomizePokemonPalettes, "death_link": DeathLink } diff --git a/worlds/pokemon_rb/poke_data.py b/worlds/pokemon_rb/poke_data.py index 691db1c46e..6218c70aa6 100644 --- a/worlds/pokemon_rb/poke_data.py +++ b/worlds/pokemon_rb/poke_data.py @@ -1006,172 +1006,172 @@ learnsets = { } moves = { - 'No Move': {'id': 0, 'power': 0, 'type': 'Typeless', 'accuracy': 0, 'pp': 0}, - 'Pound': {'id': 1, 'power': 40, 'type': 'Normal', 'accuracy': 100, 'pp': 35}, - 'Karate Chop': {'id': 2, 'power': 50, 'type': 'Normal', 'accuracy': 100, 'pp': 25}, - 'Doubleslap': {'id': 3, 'power': 15, 'type': 'Normal', 'accuracy': 85, 'pp': 10}, - 'Comet Punch': {'id': 4, 'power': 18, 'type': 'Normal', 'accuracy': 85, 'pp': 15}, - 'Mega Punch': {'id': 5, 'power': 80, 'type': 'Normal', 'accuracy': 85, 'pp': 20}, - 'Pay Day': {'id': 6, 'power': 40, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, - 'Fire Punch': {'id': 7, 'power': 75, 'type': 'Fire', 'accuracy': 100, 'pp': 15}, - 'Ice Punch': {'id': 8, 'power': 75, 'type': 'Ice', 'accuracy': 100, 'pp': 15}, - 'Thunderpunch': {'id': 9, 'power': 75, 'type': 'Electric', 'accuracy': 100, 'pp': 15}, - 'Scratch': {'id': 10, 'power': 40, 'type': 'Normal', 'accuracy': 100, 'pp': 35}, - 'Vicegrip': {'id': 11, 'power': 55, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, - 'Guillotine': {'id': 12, 'power': 1, 'type': 'Normal', 'accuracy': 30, 'pp': 5}, - 'Razor Wind': {'id': 13, 'power': 80, 'type': 'Normal', 'accuracy': 75, 'pp': 10}, - 'Swords Dance': {'id': 14, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, - 'Cut': {'id': 15, 'power': 50, 'type': 'Normal', 'accuracy': 95, 'pp': 30}, - 'Gust': {'id': 16, 'power': 40, 'type': 'Normal', 'accuracy': 100, 'pp': 35}, - 'Wing Attack': {'id': 17, 'power': 35, 'type': 'Flying', 'accuracy': 100, 'pp': 35}, - 'Whirlwind': {'id': 18, 'power': 0, 'type': 'Normal', 'accuracy': 85, 'pp': 20}, - 'Fly': {'id': 19, 'power': 70, 'type': 'Flying', 'accuracy': 95, 'pp': 15}, - 'Bind': {'id': 20, 'power': 15, 'type': 'Normal', 'accuracy': 75, 'pp': 20}, - 'Slam': {'id': 21, 'power': 80, 'type': 'Normal', 'accuracy': 75, 'pp': 20}, - 'Vine Whip': {'id': 22, 'power': 35, 'type': 'Grass', 'accuracy': 100, 'pp': 10}, - 'Stomp': {'id': 23, 'power': 65, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, - 'Double Kick': {'id': 24, 'power': 30, 'type': 'Fighting', 'accuracy': 100, 'pp': 30}, - 'Mega Kick': {'id': 25, 'power': 120, 'type': 'Normal', 'accuracy': 75, 'pp': 5}, - 'Jump Kick': {'id': 26, 'power': 70, 'type': 'Fighting', 'accuracy': 95, 'pp': 25}, - 'Rolling Kick': {'id': 27, 'power': 60, 'type': 'Fighting', 'accuracy': 85, 'pp': 15}, - 'Sand Attack': {'id': 28, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 15}, - 'Headbutt': {'id': 29, 'power': 70, 'type': 'Normal', 'accuracy': 100, 'pp': 15}, - 'Horn Attack': {'id': 30, 'power': 65, 'type': 'Normal', 'accuracy': 100, 'pp': 25}, - 'Fury Attack': {'id': 31, 'power': 15, 'type': 'Normal', 'accuracy': 85, 'pp': 20}, - 'Horn Drill': {'id': 32, 'power': 1, 'type': 'Normal', 'accuracy': 30, 'pp': 5}, - 'Tackle': {'id': 33, 'power': 35, 'type': 'Normal', 'accuracy': 95, 'pp': 35}, - 'Body Slam': {'id': 34, 'power': 85, 'type': 'Normal', 'accuracy': 100, 'pp': 15}, - 'Wrap': {'id': 35, 'power': 15, 'type': 'Normal', 'accuracy': 85, 'pp': 20}, - 'Take Down': {'id': 36, 'power': 90, 'type': 'Normal', 'accuracy': 85, 'pp': 20}, - 'Thrash': {'id': 37, 'power': 90, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, - 'Double Edge': {'id': 38, 'power': 100, 'type': 'Normal', 'accuracy': 100, 'pp': 15}, - 'Tail Whip': {'id': 39, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, - 'Poison Sting': {'id': 40, 'power': 15, 'type': 'Poison', 'accuracy': 100, 'pp': 35}, - 'Twineedle': {'id': 41, 'power': 25, 'type': 'Bug', 'accuracy': 100, 'pp': 20}, - 'Pin Missile': {'id': 42, 'power': 14, 'type': 'Bug', 'accuracy': 85, 'pp': 20}, - 'Leer': {'id': 43, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, - 'Bite': {'id': 44, 'power': 60, 'type': 'Normal', 'accuracy': 100, 'pp': 25}, - 'Growl': {'id': 45, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 40}, - 'Roar': {'id': 46, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, - 'Sing': {'id': 47, 'power': 0, 'type': 'Normal', 'accuracy': 55, 'pp': 15}, - 'Supersonic': {'id': 48, 'power': 0, 'type': 'Normal', 'accuracy': 55, 'pp': 20}, - 'Sonicboom': {'id': 49, 'power': 1, 'type': 'Normal', 'accuracy': 90, 'pp': 20}, - 'Disable': {'id': 50, 'power': 0, 'type': 'Normal', 'accuracy': 55, 'pp': 20}, - 'Acid': {'id': 51, 'power': 40, 'type': 'Poison', 'accuracy': 100, 'pp': 30}, - 'Ember': {'id': 52, 'power': 40, 'type': 'Fire', 'accuracy': 100, 'pp': 25}, - 'Flamethrower': {'id': 53, 'power': 95, 'type': 'Fire', 'accuracy': 100, 'pp': 15}, - 'Mist': {'id': 54, 'power': 0, 'type': 'Ice', 'accuracy': 100, 'pp': 30}, - 'Water Gun': {'id': 55, 'power': 40, 'type': 'Water', 'accuracy': 100, 'pp': 25}, - 'Hydro Pump': {'id': 56, 'power': 120, 'type': 'Water', 'accuracy': 80, 'pp': 5}, - 'Surf': {'id': 57, 'power': 95, 'type': 'Water', 'accuracy': 100, 'pp': 15}, - 'Ice Beam': {'id': 58, 'power': 95, 'type': 'Ice', 'accuracy': 100, 'pp': 10}, - 'Blizzard': {'id': 59, 'power': 120, 'type': 'Ice', 'accuracy': 90, 'pp': 5}, - 'Psybeam': {'id': 60, 'power': 65, 'type': 'Psychic', 'accuracy': 100, 'pp': 20}, - 'Bubblebeam': {'id': 61, 'power': 65, 'type': 'Water', 'accuracy': 100, 'pp': 20}, - 'Aurora Beam': {'id': 62, 'power': 65, 'type': 'Ice', 'accuracy': 100, 'pp': 20}, - 'Hyper Beam': {'id': 63, 'power': 150, 'type': 'Normal', 'accuracy': 90, 'pp': 5}, - 'Peck': {'id': 64, 'power': 35, 'type': 'Flying', 'accuracy': 100, 'pp': 35}, - 'Drill Peck': {'id': 65, 'power': 80, 'type': 'Flying', 'accuracy': 100, 'pp': 20}, - 'Submission': {'id': 66, 'power': 80, 'type': 'Fighting', 'accuracy': 80, 'pp': 25}, - 'Low Kick': {'id': 67, 'power': 50, 'type': 'Fighting', 'accuracy': 90, 'pp': 20}, - 'Counter': {'id': 68, 'power': 1, 'type': 'Fighting', 'accuracy': 100, 'pp': 20}, - 'Seismic Toss': {'id': 69, 'power': 1, 'type': 'Fighting', 'accuracy': 100, 'pp': 20}, - 'Strength': {'id': 70, 'power': 80, 'type': 'Normal', 'accuracy': 100, 'pp': 15}, - 'Absorb': {'id': 71, 'power': 20, 'type': 'Grass', 'accuracy': 100, 'pp': 20}, - 'Mega Drain': {'id': 72, 'power': 40, 'type': 'Grass', 'accuracy': 100, 'pp': 10}, - 'Leech Seed': {'id': 73, 'power': 0, 'type': 'Grass', 'accuracy': 90, 'pp': 10}, - 'Growth': {'id': 74, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 40}, - 'Razor Leaf': {'id': 75, 'power': 55, 'type': 'Grass', 'accuracy': 95, 'pp': 25}, - 'Solarbeam': {'id': 76, 'power': 120, 'type': 'Grass', 'accuracy': 100, 'pp': 10}, - 'Poisonpowder': {'id': 77, 'power': 0, 'type': 'Poison', 'accuracy': 75, 'pp': 35}, - 'Stun Spore': {'id': 78, 'power': 0, 'type': 'Grass', 'accuracy': 75, 'pp': 30}, - 'Sleep Powder': {'id': 79, 'power': 0, 'type': 'Grass', 'accuracy': 75, 'pp': 15}, - 'Petal Dance': {'id': 80, 'power': 70, 'type': 'Grass', 'accuracy': 100, 'pp': 20}, - 'String Shot': {'id': 81, 'power': 0, 'type': 'Bug', 'accuracy': 95, 'pp': 40}, - 'Dragon Rage': {'id': 82, 'power': 1, 'type': 'Dragon', 'accuracy': 100, 'pp': 10}, - 'Fire Spin': {'id': 83, 'power': 15, 'type': 'Fire', 'accuracy': 70, 'pp': 15}, - 'Thundershock': {'id': 84, 'power': 40, 'type': 'Electric', 'accuracy': 100, 'pp': 30}, - 'Thunderbolt': {'id': 85, 'power': 95, 'type': 'Electric', 'accuracy': 100, 'pp': 15}, - 'Thunder Wave': {'id': 86, 'power': 0, 'type': 'Electric', 'accuracy': 100, 'pp': 20}, - 'Thunder': {'id': 87, 'power': 120, 'type': 'Electric', 'accuracy': 70, 'pp': 10}, - 'Rock Throw': {'id': 88, 'power': 50, 'type': 'Rock', 'accuracy': 65, 'pp': 15}, - 'Earthquake': {'id': 89, 'power': 100, 'type': 'Ground', 'accuracy': 100, 'pp': 10}, - 'Fissure': {'id': 90, 'power': 1, 'type': 'Ground', 'accuracy': 30, 'pp': 5}, - 'Dig': {'id': 91, 'power': 100, 'type': 'Ground', 'accuracy': 100, 'pp': 10}, - 'Toxic': {'id': 92, 'power': 0, 'type': 'Poison', 'accuracy': 85, 'pp': 10}, - 'Confusion': {'id': 93, 'power': 50, 'type': 'Psychic', 'accuracy': 100, 'pp': 25}, - 'Psychic': {'id': 94, 'power': 90, 'type': 'Psychic', 'accuracy': 100, 'pp': 10}, - 'Hypnosis': {'id': 95, 'power': 0, 'type': 'Psychic', 'accuracy': 60, 'pp': 20}, - 'Meditate': {'id': 96, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 40}, - 'Agility': {'id': 97, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 30}, - 'Quick Attack': {'id': 98, 'power': 40, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, - 'Rage': {'id': 99, 'power': 20, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, - 'Teleport': {'id': 100, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 20}, - 'Night Shade': {'id': 101, 'power': 0, 'type': 'Ghost', 'accuracy': 100, 'pp': 15}, - 'Mimic': {'id': 102, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10}, - 'Screech': {'id': 103, 'power': 0, 'type': 'Normal', 'accuracy': 85, 'pp': 40}, - 'Double Team': {'id': 104, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 15}, - 'Recover': {'id': 105, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, - 'Harden': {'id': 106, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, - 'Minimize': {'id': 107, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, - 'Smokescreen': {'id': 108, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, - 'Confuse Ray': {'id': 109, 'power': 0, 'type': 'Ghost', 'accuracy': 100, 'pp': 10}, - 'Withdraw': {'id': 110, 'power': 0, 'type': 'Water', 'accuracy': 100, 'pp': 40}, - 'Defense Curl': {'id': 111, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 40}, - 'Barrier': {'id': 112, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 30}, - 'Light Screen': {'id': 113, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 30}, - 'Haze': {'id': 114, 'power': 0, 'type': 'Ice', 'accuracy': 100, 'pp': 30}, - 'Reflect': {'id': 115, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 20}, - 'Focus Energy': {'id': 116, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, - 'Bide': {'id': 117, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10}, - 'Metronome': {'id': 118, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10}, - 'Mirror Move': {'id': 119, 'power': 0, 'type': 'Flying', 'accuracy': 100, 'pp': 20}, - 'Selfdestruct': {'id': 120, 'power': 130, 'type': 'Normal', 'accuracy': 100, 'pp': 5}, - 'Egg Bomb': {'id': 121, 'power': 100, 'type': 'Normal', 'accuracy': 75, 'pp': 10}, - 'Lick': {'id': 122, 'power': 20, 'type': 'Ghost', 'accuracy': 100, 'pp': 30}, - 'Smog': {'id': 123, 'power': 20, 'type': 'Poison', 'accuracy': 70, 'pp': 20}, - 'Sludge': {'id': 124, 'power': 65, 'type': 'Poison', 'accuracy': 100, 'pp': 20}, - 'Bone Club': {'id': 125, 'power': 65, 'type': 'Ground', 'accuracy': 85, 'pp': 20}, - 'Fire Blast': {'id': 126, 'power': 120, 'type': 'Fire', 'accuracy': 85, 'pp': 5}, - 'Waterfall': {'id': 127, 'power': 80, 'type': 'Water', 'accuracy': 100, 'pp': 15}, - 'Clamp': {'id': 128, 'power': 35, 'type': 'Water', 'accuracy': 75, 'pp': 10}, - 'Swift': {'id': 129, 'power': 60, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, - 'Skull Bash': {'id': 130, 'power': 100, 'type': 'Normal', 'accuracy': 100, 'pp': 15}, - 'Spike Cannon': {'id': 131, 'power': 20, 'type': 'Normal', 'accuracy': 100, 'pp': 15}, - 'Constrict': {'id': 132, 'power': 10, 'type': 'Normal', 'accuracy': 100, 'pp': 35}, - 'Amnesia': {'id': 133, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 20}, - 'Kinesis': {'id': 134, 'power': 0, 'type': 'Psychic', 'accuracy': 80, 'pp': 15}, - 'Softboiled': {'id': 135, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10}, - 'Hi Jump Kick': {'id': 136, 'power': 85, 'type': 'Fighting', 'accuracy': 90, 'pp': 20}, - 'Glare': {'id': 137, 'power': 0, 'type': 'Normal', 'accuracy': 75, 'pp': 30}, - 'Dream Eater': {'id': 138, 'power': 100, 'type': 'Psychic', 'accuracy': 100, 'pp': 15}, - 'Poison Gas': {'id': 139, 'power': 0, 'type': 'Poison', 'accuracy': 55, 'pp': 40}, - 'Barrage': {'id': 140, 'power': 15, 'type': 'Normal', 'accuracy': 85, 'pp': 20}, - 'Leech Life': {'id': 141, 'power': 20, 'type': 'Bug', 'accuracy': 100, 'pp': 15}, - 'Lovely Kiss': {'id': 142, 'power': 0, 'type': 'Normal', 'accuracy': 75, 'pp': 10}, - 'Sky Attack': {'id': 143, 'power': 140, 'type': 'Flying', 'accuracy': 90, 'pp': 5}, - 'Transform': {'id': 144, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10}, - 'Bubble': {'id': 145, 'power': 20, 'type': 'Water', 'accuracy': 100, 'pp': 30}, - 'Dizzy Punch': {'id': 146, 'power': 70, 'type': 'Normal', 'accuracy': 100, 'pp': 10}, - 'Spore': {'id': 147, 'power': 0, 'type': 'Grass', 'accuracy': 100, 'pp': 15}, - 'Flash': {'id': 148, 'power': 0, 'type': 'Normal', 'accuracy': 70, 'pp': 20}, - 'Psywave': {'id': 149, 'power': 1, 'type': 'Psychic', 'accuracy': 80, 'pp': 15}, - 'Splash': {'id': 150, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 40}, - 'Acid Armor': {'id': 151, 'power': 0, 'type': 'Poison', 'accuracy': 100, 'pp': 40}, - 'Crabhammer': {'id': 152, 'power': 90, 'type': 'Water', 'accuracy': 85, 'pp': 10}, - 'Explosion': {'id': 153, 'power': 170, 'type': 'Normal', 'accuracy': 100, 'pp': 5}, - 'Fury Swipes': {'id': 154, 'power': 18, 'type': 'Normal', 'accuracy': 80, 'pp': 15}, - 'Bonemerang': {'id': 155, 'power': 50, 'type': 'Ground', 'accuracy': 90, 'pp': 10}, - 'Rest': {'id': 156, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 10}, - 'Rock Slide': {'id': 157, 'power': 75, 'type': 'Rock', 'accuracy': 90, 'pp': 10}, - 'Hyper Fang': {'id': 158, 'power': 80, 'type': 'Normal', 'accuracy': 90, 'pp': 15}, - 'Sharpen': {'id': 159, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, - 'Conversion': {'id': 160, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, - 'Tri Attack': {'id': 161, 'power': 80, 'type': 'Normal', 'accuracy': 100, 'pp': 10}, - 'Super Fang': {'id': 162, 'power': 1, 'type': 'Normal', 'accuracy': 90, 'pp': 10}, - 'Slash': {'id': 163, 'power': 70, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, - 'Substitute': {'id': 164, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10}, - #'Struggle': {'id': 165, 'power': 50, 'type': 'Struggle_Type', 'accuracy': 100, 'pp': 10} + 'No Move': {'id': 0, 'power': 0, 'type': 'Typeless', 'accuracy': 0, 'pp': 0, 'effect': 0}, + 'Pound': {'id': 1, 'power': 40, 'type': 'Normal', 'accuracy': 100, 'pp': 35, 'effect': 0}, + 'Karate Chop': {'id': 2, 'power': 50, 'type': 'Normal', 'accuracy': 100, 'pp': 25, 'effect': 0}, + 'Doubleslap': {'id': 3, 'power': 15, 'type': 'Normal', 'accuracy': 85, 'pp': 10, 'effect': 29}, + 'Comet Punch': {'id': 4, 'power': 18, 'type': 'Normal', 'accuracy': 85, 'pp': 15, 'effect': 29}, + 'Mega Punch': {'id': 5, 'power': 80, 'type': 'Normal', 'accuracy': 85, 'pp': 20, 'effect': 0}, + 'Pay Day': {'id': 6, 'power': 40, 'type': 'Normal', 'accuracy': 100, 'pp': 20, 'effect': 16}, + 'Fire Punch': {'id': 7, 'power': 75, 'type': 'Fire', 'accuracy': 100, 'pp': 15, 'effect': 4}, + 'Ice Punch': {'id': 8, 'power': 75, 'type': 'Ice', 'accuracy': 100, 'pp': 15, 'effect': 5}, + 'Thunderpunch': {'id': 9, 'power': 75, 'type': 'Electric', 'accuracy': 100, 'pp': 15, 'effect': 6}, + 'Scratch': {'id': 10, 'power': 40, 'type': 'Normal', 'accuracy': 100, 'pp': 35, 'effect': 0}, + 'Vicegrip': {'id': 11, 'power': 55, 'type': 'Normal', 'accuracy': 100, 'pp': 30, 'effect': 0}, + 'Guillotine': {'id': 12, 'power': 1, 'type': 'Normal', 'accuracy': 30, 'pp': 5, 'effect': 38}, + 'Razor Wind': {'id': 13, 'power': 80, 'type': 'Normal', 'accuracy': 75, 'pp': 10, 'effect': 39}, + 'Swords Dance': {'id': 14, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30, 'effect': 50}, + 'Cut': {'id': 15, 'power': 50, 'type': 'Normal', 'accuracy': 95, 'pp': 30, 'effect': 0}, + 'Gust': {'id': 16, 'power': 40, 'type': 'Normal', 'accuracy': 100, 'pp': 35, 'effect': 0}, + 'Wing Attack': {'id': 17, 'power': 35, 'type': 'Flying', 'accuracy': 100, 'pp': 35, 'effect': 0}, + 'Whirlwind': {'id': 18, 'power': 0, 'type': 'Normal', 'accuracy': 85, 'pp': 20, 'effect': 28}, + 'Fly': {'id': 19, 'power': 70, 'type': 'Flying', 'accuracy': 95, 'pp': 15, 'effect': 43}, + 'Bind': {'id': 20, 'power': 15, 'type': 'Normal', 'accuracy': 75, 'pp': 20, 'effect': 42}, + 'Slam': {'id': 21, 'power': 80, 'type': 'Normal', 'accuracy': 75, 'pp': 20, 'effect': 0}, + 'Vine Whip': {'id': 22, 'power': 35, 'type': 'Grass', 'accuracy': 100, 'pp': 10, 'effect': 0}, + 'Stomp': {'id': 23, 'power': 65, 'type': 'Normal', 'accuracy': 100, 'pp': 20, 'effect': 37}, + 'Double Kick': {'id': 24, 'power': 30, 'type': 'Fighting', 'accuracy': 100, 'pp': 30, 'effect': 44}, + 'Mega Kick': {'id': 25, 'power': 120, 'type': 'Normal', 'accuracy': 75, 'pp': 5, 'effect': 0}, + 'Jump Kick': {'id': 26, 'power': 70, 'type': 'Fighting', 'accuracy': 95, 'pp': 25, 'effect': 45}, + 'Rolling Kick': {'id': 27, 'power': 60, 'type': 'Fighting', 'accuracy': 85, 'pp': 15, 'effect': 37}, + 'Sand Attack': {'id': 28, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 15, 'effect': 22}, + 'Headbutt': {'id': 29, 'power': 70, 'type': 'Normal', 'accuracy': 100, 'pp': 15, 'effect': 37}, + 'Horn Attack': {'id': 30, 'power': 65, 'type': 'Normal', 'accuracy': 100, 'pp': 25, 'effect': 0}, + 'Fury Attack': {'id': 31, 'power': 15, 'type': 'Normal', 'accuracy': 85, 'pp': 20, 'effect': 29}, + 'Horn Drill': {'id': 32, 'power': 1, 'type': 'Normal', 'accuracy': 30, 'pp': 5, 'effect': 38}, + 'Tackle': {'id': 33, 'power': 35, 'type': 'Normal', 'accuracy': 95, 'pp': 35, 'effect': 0}, + 'Body Slam': {'id': 34, 'power': 85, 'type': 'Normal', 'accuracy': 100, 'pp': 15, 'effect': 36}, + 'Wrap': {'id': 35, 'power': 15, 'type': 'Normal', 'accuracy': 85, 'pp': 20, 'effect': 42}, + 'Take Down': {'id': 36, 'power': 90, 'type': 'Normal', 'accuracy': 85, 'pp': 20, 'effect': 48}, + 'Thrash': {'id': 37, 'power': 90, 'type': 'Normal', 'accuracy': 100, 'pp': 20, 'effect': 27}, + 'Double Edge': {'id': 38, 'power': 100, 'type': 'Normal', 'accuracy': 100, 'pp': 15, 'effect': 48}, + 'Tail Whip': {'id': 39, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30, 'effect': 19}, + 'Poison Sting': {'id': 40, 'power': 15, 'type': 'Poison', 'accuracy': 100, 'pp': 35, 'effect': 2}, + 'Twineedle': {'id': 41, 'power': 25, 'type': 'Bug', 'accuracy': 100, 'pp': 20, 'effect': 77}, + 'Pin Missile': {'id': 42, 'power': 14, 'type': 'Bug', 'accuracy': 85, 'pp': 20, 'effect': 29}, + 'Leer': {'id': 43, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30, 'effect': 19}, + 'Bite': {'id': 44, 'power': 60, 'type': 'Normal', 'accuracy': 100, 'pp': 25, 'effect': 31}, + 'Growl': {'id': 45, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 40, 'effect': 18}, + 'Roar': {'id': 46, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 20, 'effect': 28}, + 'Sing': {'id': 47, 'power': 0, 'type': 'Normal', 'accuracy': 55, 'pp': 15, 'effect': 32}, + 'Supersonic': {'id': 48, 'power': 0, 'type': 'Normal', 'accuracy': 55, 'pp': 20, 'effect': 49}, + 'Sonicboom': {'id': 49, 'power': 1, 'type': 'Normal', 'accuracy': 90, 'pp': 20, 'effect': 41}, + 'Disable': {'id': 50, 'power': 0, 'type': 'Normal', 'accuracy': 55, 'pp': 20, 'effect': 86}, + 'Acid': {'id': 51, 'power': 40, 'type': 'Poison', 'accuracy': 100, 'pp': 30, 'effect': 69}, + 'Ember': {'id': 52, 'power': 40, 'type': 'Fire', 'accuracy': 100, 'pp': 25, 'effect': 4}, + 'Flamethrower': {'id': 53, 'power': 95, 'type': 'Fire', 'accuracy': 100, 'pp': 15, 'effect': 4}, + 'Mist': {'id': 54, 'power': 0, 'type': 'Ice', 'accuracy': 100, 'pp': 30, 'effect': 46}, + 'Water Gun': {'id': 55, 'power': 40, 'type': 'Water', 'accuracy': 100, 'pp': 25, 'effect': 0}, + 'Hydro Pump': {'id': 56, 'power': 120, 'type': 'Water', 'accuracy': 80, 'pp': 5, 'effect': 0}, + 'Surf': {'id': 57, 'power': 95, 'type': 'Water', 'accuracy': 100, 'pp': 15, 'effect': 0}, + 'Ice Beam': {'id': 58, 'power': 95, 'type': 'Ice', 'accuracy': 100, 'pp': 10, 'effect': 5}, + 'Blizzard': {'id': 59, 'power': 120, 'type': 'Ice', 'accuracy': 90, 'pp': 5, 'effect': 5}, + 'Psybeam': {'id': 60, 'power': 65, 'type': 'Psychic', 'accuracy': 100, 'pp': 20, 'effect': 76}, + 'Bubblebeam': {'id': 61, 'power': 65, 'type': 'Water', 'accuracy': 100, 'pp': 20, 'effect': 70}, + 'Aurora Beam': {'id': 62, 'power': 65, 'type': 'Ice', 'accuracy': 100, 'pp': 20, 'effect': 68}, + 'Hyper Beam': {'id': 63, 'power': 150, 'type': 'Normal', 'accuracy': 90, 'pp': 5, 'effect': 80}, + 'Peck': {'id': 64, 'power': 35, 'type': 'Flying', 'accuracy': 100, 'pp': 35, 'effect': 0}, + 'Drill Peck': {'id': 65, 'power': 80, 'type': 'Flying', 'accuracy': 100, 'pp': 20, 'effect': 0}, + 'Submission': {'id': 66, 'power': 80, 'type': 'Fighting', 'accuracy': 80, 'pp': 25, 'effect': 48}, + 'Low Kick': {'id': 67, 'power': 50, 'type': 'Fighting', 'accuracy': 90, 'pp': 20, 'effect': 37}, + 'Counter': {'id': 68, 'power': 1, 'type': 'Fighting', 'accuracy': 100, 'pp': 20, 'effect': 0}, + 'Seismic Toss': {'id': 69, 'power': 1, 'type': 'Fighting', 'accuracy': 100, 'pp': 20, 'effect': 41}, + 'Strength': {'id': 70, 'power': 80, 'type': 'Normal', 'accuracy': 100, 'pp': 15, 'effect': 0}, + 'Absorb': {'id': 71, 'power': 20, 'type': 'Grass', 'accuracy': 100, 'pp': 20, 'effect': 3}, + 'Mega Drain': {'id': 72, 'power': 40, 'type': 'Grass', 'accuracy': 100, 'pp': 10, 'effect': 3}, + 'Leech Seed': {'id': 73, 'power': 0, 'type': 'Grass', 'accuracy': 90, 'pp': 10, 'effect': 84}, + 'Growth': {'id': 74, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 40, 'effect': 13}, + 'Razor Leaf': {'id': 75, 'power': 55, 'type': 'Grass', 'accuracy': 95, 'pp': 25, 'effect': 0}, + 'Solarbeam': {'id': 76, 'power': 120, 'type': 'Grass', 'accuracy': 100, 'pp': 10, 'effect': 39}, + 'Poisonpowder': {'id': 77, 'power': 0, 'type': 'Poison', 'accuracy': 75, 'pp': 35, 'effect': 66}, + 'Stun Spore': {'id': 78, 'power': 0, 'type': 'Grass', 'accuracy': 75, 'pp': 30, 'effect': 67}, + 'Sleep Powder': {'id': 79, 'power': 0, 'type': 'Grass', 'accuracy': 75, 'pp': 15, 'effect': 32}, + 'Petal Dance': {'id': 80, 'power': 70, 'type': 'Grass', 'accuracy': 100, 'pp': 20, 'effect': 27}, + 'String Shot': {'id': 81, 'power': 0, 'type': 'Bug', 'accuracy': 95, 'pp': 40, 'effect': 20}, + 'Dragon Rage': {'id': 82, 'power': 1, 'type': 'Dragon', 'accuracy': 100, 'pp': 10, 'effect': 41}, + 'Fire Spin': {'id': 83, 'power': 15, 'type': 'Fire', 'accuracy': 70, 'pp': 15, 'effect': 42}, + 'Thundershock': {'id': 84, 'power': 40, 'type': 'Electric', 'accuracy': 100, 'pp': 30, 'effect': 6}, + 'Thunderbolt': {'id': 85, 'power': 95, 'type': 'Electric', 'accuracy': 100, 'pp': 15, 'effect': 6}, + 'Thunder Wave': {'id': 86, 'power': 0, 'type': 'Electric', 'accuracy': 100, 'pp': 20, 'effect': 67}, + 'Thunder': {'id': 87, 'power': 120, 'type': 'Electric', 'accuracy': 70, 'pp': 10, 'effect': 6}, + 'Rock Throw': {'id': 88, 'power': 50, 'type': 'Rock', 'accuracy': 65, 'pp': 15, 'effect': 0}, + 'Earthquake': {'id': 89, 'power': 100, 'type': 'Ground', 'accuracy': 100, 'pp': 10, 'effect': 0}, + 'Fissure': {'id': 90, 'power': 1, 'type': 'Ground', 'accuracy': 30, 'pp': 5, 'effect': 38}, + 'Dig': {'id': 91, 'power': 100, 'type': 'Ground', 'accuracy': 100, 'pp': 10, 'effect': 39}, + 'Toxic': {'id': 92, 'power': 0, 'type': 'Poison', 'accuracy': 85, 'pp': 10, 'effect': 66}, + 'Confusion': {'id': 93, 'power': 50, 'type': 'Psychic', 'accuracy': 100, 'pp': 25, 'effect': 76}, + 'Psychic': {'id': 94, 'power': 90, 'type': 'Psychic', 'accuracy': 100, 'pp': 10, 'effect': 71}, + 'Hypnosis': {'id': 95, 'power': 0, 'type': 'Psychic', 'accuracy': 60, 'pp': 20, 'effect': 32}, + 'Meditate': {'id': 96, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 40, 'effect': 10}, + 'Agility': {'id': 97, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 30, 'effect': 52}, + 'Quick Attack': {'id': 98, 'power': 40, 'type': 'Normal', 'accuracy': 100, 'pp': 30, 'effect': 0}, + 'Rage': {'id': 99, 'power': 20, 'type': 'Normal', 'accuracy': 100, 'pp': 20, 'effect': 81}, + 'Teleport': {'id': 100, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 20, 'effect': 28}, + 'Night Shade': {'id': 101, 'power': 0, 'type': 'Ghost', 'accuracy': 100, 'pp': 15, 'effect': 41}, + 'Mimic': {'id': 102, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10, 'effect': 82}, + 'Screech': {'id': 103, 'power': 0, 'type': 'Normal', 'accuracy': 85, 'pp': 40, 'effect': 59}, + 'Double Team': {'id': 104, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 15, 'effect': 15}, + 'Recover': {'id': 105, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 20, 'effect': 56}, + 'Harden': {'id': 106, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30, 'effect': 11}, + 'Minimize': {'id': 107, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 20, 'effect': 15}, + 'Smokescreen': {'id': 108, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 20, 'effect': 22}, + 'Confuse Ray': {'id': 109, 'power': 0, 'type': 'Ghost', 'accuracy': 100, 'pp': 10, 'effect': 49}, + 'Withdraw': {'id': 110, 'power': 0, 'type': 'Water', 'accuracy': 100, 'pp': 40, 'effect': 11}, + 'Defense Curl': {'id': 111, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 40, 'effect': 11}, + 'Barrier': {'id': 112, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 30, 'effect': 51}, + 'Light Screen': {'id': 113, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 30, 'effect': 64}, + 'Haze': {'id': 114, 'power': 0, 'type': 'Ice', 'accuracy': 100, 'pp': 30, 'effect': 25}, + 'Reflect': {'id': 115, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 20, 'effect': 65}, + 'Focus Energy': {'id': 116, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30, 'effect': 47}, + 'Bide': {'id': 117, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10, 'effect': 26}, + 'Metronome': {'id': 118, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10, 'effect': 83}, + 'Mirror Move': {'id': 119, 'power': 0, 'type': 'Flying', 'accuracy': 100, 'pp': 20, 'effect': 9}, + 'Selfdestruct': {'id': 120, 'power': 130, 'type': 'Normal', 'accuracy': 100, 'pp': 5, 'effect': 7}, + 'Egg Bomb': {'id': 121, 'power': 100, 'type': 'Normal', 'accuracy': 75, 'pp': 10, 'effect': 0}, + 'Lick': {'id': 122, 'power': 20, 'type': 'Ghost', 'accuracy': 100, 'pp': 30, 'effect': 36}, + 'Smog': {'id': 123, 'power': 20, 'type': 'Poison', 'accuracy': 70, 'pp': 20, 'effect': 33}, + 'Sludge': {'id': 124, 'power': 65, 'type': 'Poison', 'accuracy': 100, 'pp': 20, 'effect': 33}, + 'Bone Club': {'id': 125, 'power': 65, 'type': 'Ground', 'accuracy': 85, 'pp': 20, 'effect': 31}, + 'Fire Blast': {'id': 126, 'power': 120, 'type': 'Fire', 'accuracy': 85, 'pp': 5, 'effect': 34}, + 'Waterfall': {'id': 127, 'power': 80, 'type': 'Water', 'accuracy': 100, 'pp': 15, 'effect': 0}, + 'Clamp': {'id': 128, 'power': 35, 'type': 'Water', 'accuracy': 75, 'pp': 10, 'effect': 42}, + 'Swift': {'id': 129, 'power': 60, 'type': 'Normal', 'accuracy': 100, 'pp': 20, 'effect': 17}, + 'Skull Bash': {'id': 130, 'power': 100, 'type': 'Normal', 'accuracy': 100, 'pp': 15, 'effect': 39}, + 'Spike Cannon': {'id': 131, 'power': 20, 'type': 'Normal', 'accuracy': 100, 'pp': 15, 'effect': 29}, + 'Constrict': {'id': 132, 'power': 10, 'type': 'Normal', 'accuracy': 100, 'pp': 35, 'effect': 70}, + 'Amnesia': {'id': 133, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 20, 'effect': 53}, + 'Kinesis': {'id': 134, 'power': 0, 'type': 'Psychic', 'accuracy': 80, 'pp': 15, 'effect': 22}, + 'Softboiled': {'id': 135, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10, 'effect': 56}, + 'Hi Jump Kick': {'id': 136, 'power': 85, 'type': 'Fighting', 'accuracy': 90, 'pp': 20, 'effect': 45}, + 'Glare': {'id': 137, 'power': 0, 'type': 'Normal', 'accuracy': 75, 'pp': 30, 'effect': 67}, + 'Dream Eater': {'id': 138, 'power': 100, 'type': 'Psychic', 'accuracy': 100, 'pp': 15, 'effect': 8}, + 'Poison Gas': {'id': 139, 'power': 0, 'type': 'Poison', 'accuracy': 55, 'pp': 40, 'effect': 66}, + 'Barrage': {'id': 140, 'power': 15, 'type': 'Normal', 'accuracy': 85, 'pp': 20, 'effect': 29}, + 'Leech Life': {'id': 141, 'power': 20, 'type': 'Bug', 'accuracy': 100, 'pp': 15, 'effect': 3}, + 'Lovely Kiss': {'id': 142, 'power': 0, 'type': 'Normal', 'accuracy': 75, 'pp': 10, 'effect': 32}, + 'Sky Attack': {'id': 143, 'power': 140, 'type': 'Flying', 'accuracy': 90, 'pp': 5, 'effect': 39}, + 'Transform': {'id': 144, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10, 'effect': 57}, + 'Bubble': {'id': 145, 'power': 20, 'type': 'Water', 'accuracy': 100, 'pp': 30, 'effect': 70}, + 'Dizzy Punch': {'id': 146, 'power': 70, 'type': 'Normal', 'accuracy': 100, 'pp': 10, 'effect': 0}, + 'Spore': {'id': 147, 'power': 0, 'type': 'Grass', 'accuracy': 100, 'pp': 15, 'effect': 32}, + 'Flash': {'id': 148, 'power': 0, 'type': 'Normal', 'accuracy': 70, 'pp': 20, 'effect': 22}, + 'Psywave': {'id': 149, 'power': 1, 'type': 'Psychic', 'accuracy': 80, 'pp': 15, 'effect': 41}, + 'Splash': {'id': 150, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 40, 'effect': 85}, + 'Acid Armor': {'id': 151, 'power': 0, 'type': 'Poison', 'accuracy': 100, 'pp': 40, 'effect': 51}, + 'Crabhammer': {'id': 152, 'power': 90, 'type': 'Water', 'accuracy': 85, 'pp': 10, 'effect': 0}, + 'Explosion': {'id': 153, 'power': 170, 'type': 'Normal', 'accuracy': 100, 'pp': 5, 'effect': 7}, + 'Fury Swipes': {'id': 154, 'power': 18, 'type': 'Normal', 'accuracy': 80, 'pp': 15, 'effect': 29}, + 'Bonemerang': {'id': 155, 'power': 50, 'type': 'Ground', 'accuracy': 90, 'pp': 10, 'effect': 44}, + 'Rest': {'id': 156, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 10, 'effect': 56}, + 'Rock Slide': {'id': 157, 'power': 75, 'type': 'Rock', 'accuracy': 90, 'pp': 10, 'effect': 0}, + 'Hyper Fang': {'id': 158, 'power': 80, 'type': 'Normal', 'accuracy': 90, 'pp': 15, 'effect': 31}, + 'Sharpen': {'id': 159, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30, 'effect': 10}, + 'Conversion': {'id': 160, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30, 'effect': 24}, + 'Tri Attack': {'id': 161, 'power': 80, 'type': 'Normal', 'accuracy': 100, 'pp': 10, 'effect': 0}, + 'Super Fang': {'id': 162, 'power': 1, 'type': 'Normal', 'accuracy': 90, 'pp': 10, 'effect': 40}, + 'Slash': {'id': 163, 'power': 70, 'type': 'Normal', 'accuracy': 100, 'pp': 20, 'effect': 0}, + 'Substitute': {'id': 164, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10, 'effect': 79} + #'Struggle': {'id': 165, 'power': 50, 'type': 'Struggle_Type', 'accuracy': 100, 'pp': 10, 'effect': 48} } encounter_tables = {'Wild_Super_Rod_A': 2, 'Wild_Super_Rod_B': 2, 'Wild_Super_Rod_C': 3, 'Wild_Super_Rod_D': 2, @@ -1204,6 +1204,29 @@ tm_moves = [ 'Selfdestruct', 'Egg Bomb', 'Fire Blast', 'Swift', 'Skull Bash', 'Softboiled', 'Dream Eater', 'Sky Attack', 'Rest', 'Thunder Wave', 'Psywave', 'Explosion', 'Rock Slide', 'Tri Attack', 'Substitute' ] +#['No Move', 'Pound', 'Karate Chop', 'Doubleslap', 'Comet Punch', 'Fire Punch', 'Ice Punch', 'Thunderpunch', 'Scratch', +# 'Vicegrip', 'Guillotine', 'Cut', 'Gust', 'Wing Attack', 'Fly', 'Bind', 'Slam', 'Vine Whip', 'Stomp', 'Double Kick', 'Jump Kick', +# 'Rolling Kick', 'Sand Attack', 'Headbutt', 'Horn Attack', 'Fury Attack', 'Tackle', 'Wrap', 'Thrash', 'Tail Whip', 'Poison Sting', +# 'Twineedle', 'Pin Missile', 'Leer', 'Bite', 'Growl', 'Roar', 'Sing', 'Supersonic', 'Sonicboom', 'Disable', 'Acid', 'Ember', 'Flamethrower', +# 'Mist', 'Hydro Pump', 'Surf', 'Psybeam', 'Aurora Beam', 'Peck', 'Drill Peck', 'Low Kick', 'Strength', 'Absorb', 'Leech Seed', 'Growth', +# 'Razor Leaf', 'Poisonpowder', 'Stun Spore', 'Sleep Powder', 'Petal Dance', 'String Shot', 'Fire Spin', 'Thundershock', 'Rock Throw', 'Confusion', +# 'Hypnosis', 'Meditate', 'Agility', 'Quick Attack', 'Night Shade', 'Screech', 'Recover', 'Harden', 'Minimize', 'Smokescreen', 'Confuse Ray', 'Withdraw', +# 'Defense Curl', 'Barrier', 'Light Screen', 'Haze', 'Focus Energy', 'Mirror Move', 'Lick', 'Smog', 'Sludge', 'Bone Club', 'Waterfall', 'Clamp', 'Spike Cannon', +# 'Constrict', 'Amnesia', 'Kinesis', 'Hi Jump Kick', 'Glare', 'Poison Gas', 'Barrage', 'Leech Life', 'Lovely Kiss', 'Transform', 'Bubble', 'Dizzy Punch', 'Spore', 'Flash', +# 'Splash', 'Acid Armor', 'Crabhammer', 'Fury Swipes', 'Bonemerang', 'Hyper Fang', 'Sharpen', 'Conversion', 'Super Fang', 'Slash'] + +# print([i for i in list(moves.keys()) if i not in tm_moves]) +# filler_moves = [ +# "Razor Wind", "Whirlwind", "Counter", "Teleport", "Bide", "Skull Bash", "Sky Attack", "Psywave", +# "Pound", "Karate Chop", "Doubleslap", "Comet Punch", "Scratch", "Vicegrip", "Gust", "Wing Attack", "Bind", +# "Vine Whip", "Sand Attack", "Fury Attack", "Tackle", "Wrap", "Tail Whip", "Poison Sting", "Twineedle", +# "Leer", "Growl", "Roar", "Sing", "Supersonic", "Sonicboom", "Disable", "Acid", "Ember", "Mist", "Peck", "Absorb", +# "Growth", "Poisonpowder", "String Shot", "Meditate", "Agility", "Screech", "Double Team", "Harden", "Minimize", +# "Smokescreen", "Confuse Ray", "Withdraw", "Defense Curl", "Barrier", "Light Screen", "Haze", "Reflect", +# "Focus Energy", "Lick", "Smog", "Clamp", "Spike Cannon", "Constrict" +# +# ] + first_stage_pokemon = [pokemon for pokemon in pokemon_data.keys() if pokemon not in evolves_from] legendary_pokemon = ["Articuno", "Zapdos", "Moltres", "Mewtwo", "Mew"] diff --git a/worlds/pokemon_rb/regions.py b/worlds/pokemon_rb/regions.py index 674d24d148..98dbb3af8f 100644 --- a/worlds/pokemon_rb/regions.py +++ b/worlds/pokemon_rb/regions.py @@ -23,11 +23,12 @@ def create_regions(multiworld: MultiWorld, player: int): locations_per_region.setdefault(location.region, []) if location.inclusion(multiworld, player): locations_per_region[location.region].append(PokemonRBLocation(player, location.name, location.address, - location.rom_address)) + location.rom_address, location.type)) regions = [ create_region(multiworld, player, "Menu", locations_per_region), create_region(multiworld, player, "Anywhere", locations_per_region), create_region(multiworld, player, "Fossil", locations_per_region), + create_region(multiworld, player, "Pokedex", locations_per_region), create_region(multiworld, player, "Pallet Town", locations_per_region), create_region(multiworld, player, "Route 1", locations_per_region), create_region(multiworld, player, "Viridian City", locations_per_region), @@ -88,6 +89,7 @@ def create_regions(multiworld: MultiWorld, player: int): create_region(multiworld, player, "Route 8", locations_per_region), create_region(multiworld, player, "Route 8 Grass", locations_per_region), create_region(multiworld, player, "Celadon City", locations_per_region), + create_region(multiworld, player, "Celadon Game Corner", locations_per_region), create_region(multiworld, player, "Celadon Prize Corner", locations_per_region), create_region(multiworld, player, "Celadon Gym", locations_per_region), create_region(multiworld, player, "Route 16", locations_per_region), @@ -148,6 +150,7 @@ def create_regions(multiworld: MultiWorld, player: int): multiworld.regions += regions connect(multiworld, player, "Menu", "Anywhere", one_way=True) connect(multiworld, player, "Menu", "Pallet Town", one_way=True) + connect(multiworld, player, "Menu", "Pokedex", one_way=True) connect(multiworld, player, "Menu", "Fossil", lambda state: state.pokemon_rb_fossil_checks( state.multiworld.second_fossil_check_condition[player].value, player), one_way=True) connect(multiworld, player, "Pallet Town", "Route 1") @@ -220,6 +223,7 @@ def create_regions(multiworld: MultiWorld, player: int): connect(multiworld, player, "Route 8", "Route 8 Grass", lambda state: state.pokemon_rb_can_cut(player), one_way=True) connect(multiworld, player, "Route 7", "Celadon City") connect(multiworld, player, "Celadon City", "Celadon Gym", lambda state: state.pokemon_rb_can_cut(player), one_way=True) + connect(multiworld, player, "Celadon City", "Celadon Game Corner") connect(multiworld, player, "Celadon City", "Celadon Prize Corner") connect(multiworld, player, "Celadon City", "Route 16") connect(multiworld, player, "Route 16", "Route 16 North", lambda state: state.pokemon_rb_can_cut(player), one_way=True) diff --git a/worlds/pokemon_rb/rock_tunnel.py b/worlds/pokemon_rb/rock_tunnel.py new file mode 100644 index 0000000000..3a70709eb0 --- /dev/null +++ b/worlds/pokemon_rb/rock_tunnel.py @@ -0,0 +1,294 @@ +from .rom_addresses import rom_addresses + +disallowed1F = [[2, 2], [3, 2], [1, 8], [2, 8], [7, 7], [8, 7], [10, 4], [11, 4], [11, 12], + [11, 13], [16, 10], [17, 10], [18, 10], [16, 12], [17, 12], [18, 12]] +disallowed2F = [[16, 2], [17, 2], [18, 2], [15, 5], [15, 6], [10, 10], [11, 10], [12, 10], [7, 14], [8, 14], [1, 15], + [13, 15], [13, 16], [1, 12], [1, 10], [3, 5], [3, 6], [5, 6], [5, 7], [5, 8], [1, 2], [1, 3], [1, 4], + [11, 1]] + + +def randomize_rock_tunnel(data, random): + + seed = random.randint(0, 999999999999999999) + random.seed(seed) + + map1f = [] + map2f = [] + + address = rom_addresses["Map_Rock_Tunnel1F"] + for y in range(0, 18): + row = [] + for x in range(0, 20): + row.append(data[address]) + address += 1 + map1f.append(row) + + address = rom_addresses["Map_Rock_TunnelB1F"] + for y in range(0, 18): + row = [] + for x in range(0, 20): + row.append(data[address]) + address += 1 + map2f.append(row) + + current_map = map1f + + def floor(x, y): + current_map[y][x] = 1 + + def wide(x, y): + current_map[y][x] = 32 + current_map[y][x + 1] = 34 + + def tall(x, y): + current_map[y][x] = 23 + current_map[y + 1][x] = 31 + + def single(x, y): + current_map[y][x] = 2 + + # 0 = top left, 1 = middle, 2 = top right, 3 = bottom right + entrance_c = random.choice([0, 1, 2]) + exit_c = [0, 1, 3] + if entrance_c == 2: + exit_c.remove(1) + else: + exit_c.remove(entrance_c) + exit_c = random.choice(exit_c) + remaining = [i for i in [0, 1, 2, 3] if i not in [entrance_c, exit_c]] + + if entrance_c == 0: + floor(6, 3) + floor(6, 4) + tall(random.randint(8, 10), 2) + wide(4, random.randint(5, 7)) + wide(1, random.choice([5, 6, 7, 9])) + elif entrance_c == 1: + if remaining == [0, 2] or random.randint(0, 1): + tall(random.randint(8, 10), 2) + floor(7, 4) + floor(8, 4) + else: + tall(random.randint(11, 12), 5) + floor(9, 5) + floor(9, 6) + elif entrance_c == 2: + floor(16, 2) + floor(16, 3) + if remaining == [1, 3]: + wide(17, 4) + else: + tall(random.randint(11, 17), random.choice([2, 5])) + + if exit_c == 0: + r = random.sample([0, 1, 2], 2) + if 0 in r: + floor(1, 11) + floor(2, 11) + if 1 in r: + floor(3, 11) + floor(4, 11) + if 2 in r: + floor(5, 11) + floor(6, 11) + elif exit_c == 1 or (exit_c == 3 and entrance_c == 0): + r = random.sample([1, 3, 5, 7], random.randint(1, 2)) + for i in r: + floor(i, 11) + floor(i + 1, 11) + if exit_c != 3: + tall(random.choice([9, 10, 12]), 12) + + # 0 = top left, 1 = middle, 2 = top right, 3 = bottom right + # [0, 1] [0, 2] [1, 2] [1, 3], [2, 3] + if remaining[0] == 1: + floor(9, 5) + floor(9, 6) + + if remaining == [0, 2]: + if random.randint(0, 1): + tall(9, 4) + floor(9, 6) + floor(9, 7) + else: + floor(10, 7) + floor(11, 7) + + if remaining == [1, 2]: + floor(16, 2) + floor(16, 3) + tall(random.randint(11, 17), random.choice([2, 5])) + if remaining in [[1, 3], [2, 3]]: + r = round(random.triangular(0, 3, 0)) + floor(12 + (r * 2), 7) + if r < 3: + floor(13 + (r * 2), 7) + if remaining == [1, 3]: + wide(10, random.choice([3, 5])) + + if remaining != [0, 1] and exit_c != 1: + wide(7, 6) + + if entrance_c != 0: + if random.randint(0, 1): + wide(4, random.randint(4, 7)) + else: + wide(1, random.choice([5, 6, 7, 9])) + + current_map = map2f + + if 3 in remaining: + c = random.choice([entrance_c, exit_c]) + else: + c = random.choice(remaining) + + # 0 = top right, 1 = middle, 2 = bottom right, 3 = top left + if c in [0, 1]: + if random.randint(0, 2): + tall(random.choice([2, 4]), 5) + r = random.choice([1, 3, 7, 9, 11]) + floor(3 if r < 11 else random.randint(1, 2), r) + floor(3 if r < 11 else random.randint(1, 2), r + 1) + if random.randint(0, 2): + tall(random.randint(6, 7), 7) + r = random.choice([1, 3, 5, 9]) + floor(6, r) + floor(6, r + 1) + if random.randint(0, 2): + wide(7, 15) + r = random.randint(0, 4) + if r == 0: + floor(9, 14) + floor(10, 14) + elif r == 1: + floor(11, 14) + floor(12, 14) + elif r == 2: + floor(13, 13) + floor(13, 14) + elif r == 3: + floor(13, 11) + floor(13, 12) + elif r == 4: + floor(13, 10) + floor(14, 10) + if c == 0: + tall(random.randint(9, 10), 5) + if random.randint(0, 1): + floor(10, 7) + floor(11, 7) + tall(random.randint(12, 17), 8) + else: + floor(12, 5) + floor(12, 6) + wide(13, random.randint(4, 5)) + wide(17, random.randint(3, 5)) + r = random.choice([1, 3]) + floor(12, r) + floor(12, + 1) + + elif c == 2: + r = random.randint(0, 6) + if r == 0: + floor(12, 1) + floor(12, 2) + elif r == 1: + floor(12, 3) + floor(12, 4) + elif r == 2: + floor(12, 5) + floor(12, 6) + elif r == 3: + floor(10, 7) + floor(11, 7) + elif r == 4: + floor(9, 7) + floor(9, 8) + elif r == 5: + floor(9, 9) + floor(9, 10) + elif r == 6: + floor(8, 11) + floor(9, 11) + if r < 2 or (r in [2, 3] and random.randint(0, 1)): + wide(7, random.randint(6, 7)) + elif r in [2, 3]: + tall(random.randint(9, 10), 5) + else: + tall(random.randint(6, 7), 7) + r = random.randint(r, 6) + if r == 0: + #early block + wide(13, random.randint(2, 5)) + tall(random.randint(14, 15), 1) + elif r == 1: + if random.randint(0, 1): + tall(16, 5) + tall(random.choice([14, 15, 17]), 1) + else: + wide(16, random.randint(6,8)) + single(18, 7) + elif r == 2: + tall(random.randint(12, 16), 8) + elif r == 3: + wide(10, 9) + single(12, 9) + elif r == 4: + wide(10, random.randint(11, 12)) + single(12, random.randint(11, 12)) + elif r == 5: + tall(random.randint(8, 10), 12) + elif r == 6: + wide(7, 15) + r = random.randint(r, 6) + if r == 6: + #late open + r2 = random.randint(0, 2) + floor(1 + (r2 * 2), 14) + floor(2 + (r2 * 2), 14) + elif r == 5: + floor(6, 12) + floor(6, 13) + elif r == 4: + if random.randint(0, 1): + floor(6, 11) + floor(7, 11) + else: + floor(8, 11) + floor(9, 11) + elif r == 3: + floor(9, 9) + floor(9, 10) + elif r < 3: + single(9, 7) + floor(9, 8) + + def check_addable_block(check_map, disallowed): + if check_map[y][x] == 1 and [x, y] not in disallowed: + i = 0 + for xx in range(x-1, x+2): + for yy in range(y-1, y+2): + if check_map[yy][xx] == 1: + i += 1 + if i >= 8: + single(x, y) + + for _ in range(100): + y = random.randint(1, 16) + x = random.randint(1, 18) + current_map = map1f + check_addable_block(map1f, disallowed1F) + current_map = map2f + check_addable_block(map2f, disallowed2F) + + address = rom_addresses["Map_Rock_Tunnel1F"] + for y in map1f: + for x in y: + data[address] = x + address += 1 + address = rom_addresses["Map_Rock_TunnelB1F"] + for y in map2f: + for x in y: + data[address] = x + address += 1 + return seed \ No newline at end of file diff --git a/worlds/pokemon_rb/rom.py b/worlds/pokemon_rb/rom.py index 9dbc3a8b83..1a5f3250aa 100644 --- a/worlds/pokemon_rb/rom.py +++ b/worlds/pokemon_rb/rom.py @@ -8,6 +8,7 @@ from .text import encode_text from .rom_addresses import rom_addresses from .locations import location_data from .items import item_table +from .rock_tunnel import randomize_rock_tunnel import worlds.pokemon_rb.poke_data as poke_data @@ -28,15 +29,15 @@ def filter_moves(moves, type, random): return ret -def get_move(moves, chances, random, starting_move=False): +def get_move(local_move_data, moves, chances, random, starting_move=False): type = choose_forced_type(chances, random) filtered_moves = filter_moves(moves, type, random) for move in filtered_moves: - if poke_data.moves[move]["accuracy"] > 80 and poke_data.moves[move]["power"] > 0 or not starting_move: + if local_move_data[move]["accuracy"] > 80 and local_move_data[move]["power"] > 0 or not starting_move: moves.remove(move) return move else: - return get_move(moves, [], random, starting_move) + return get_move(local_move_data, moves, [], random, starting_move) def get_encounter_slots(self): @@ -75,6 +76,42 @@ def randomize_pokemon(self, mon, mons_list, randomize_type, random): return mon +def set_mon_palettes(self, random, data): + if self.multiworld.randomize_pokemon_palettes[self.player] == "vanilla": + return + pallet_map = { + "Poison": 0x0F, + "Normal": 0x10, + "Ice": 0x11, + "Fire": 0x12, + "Water": 0x13, + "Ghost": 0x14, + "Ground": 0x15, + "Grass": 0x16, + "Psychic": 0x17, + "Electric": 0x18, + "Rock": 0x19, + "Dragon": 0x1F, + "Flying": 0x20, + "Fighting": 0x21, + "Bug": 0x22 + } + palettes = [] + for mon in poke_data.pokemon_data: + if self.multiworld.randomize_pokemon_palettes[self.player] == "primary_type": + pallet = pallet_map[self.local_poke_data[mon]["type1"]] + elif (self.multiworld.randomize_pokemon_palettes[self.player] == "follow_evolutions" and mon in + poke_data.evolves_from and poke_data.evolves_from[mon] != "Eevee"): + pallet = palettes[-1] + else: # completely_random or follow_evolutions and it is not an evolved form (except eeveelutions) + pallet = random.choice(list(pallet_map.values())) + palettes.append(pallet) + address = rom_addresses["Mon_Palettes"] + for pallet in palettes: + data[address] = pallet + address += 1 + + def process_trainer_data(self, data, random): mons_list = [pokemon for pokemon in poke_data.pokemon_data.keys() if pokemon not in poke_data.legendary_pokemon or self.multiworld.trainer_legendaries[self.player].value] @@ -163,6 +200,7 @@ def process_static_pokemon(self): randomize_type, self.multiworld.random)) location.place_locked_item(mon) + chosen_mons = set() for slot in starter_slots: location = self.multiworld.get_location(slot.name, self.player) randomize_type = self.multiworld.randomize_starter_pokemon[self.player].value @@ -170,9 +208,13 @@ def process_static_pokemon(self): if not randomize_type: location.place_locked_item(self.create_item(slot_type + " " + slot.original_item)) else: - location.place_locked_item(self.create_item(slot_type + " " + - randomize_pokemon(self, slot.original_item, mons_list, randomize_type, - self.multiworld.random))) + mon = self.create_item(slot_type + " " + randomize_pokemon(self, slot.original_item, mons_list, + randomize_type, self.multiworld.random)) + while mon.name in chosen_mons: + mon = self.create_item(slot_type + " " + randomize_pokemon(self, slot.original_item, mons_list, + randomize_type, self.multiworld.random)) + chosen_mons.add(mon.name) + location.place_locked_item(mon) def process_wild_pokemon(self): @@ -180,27 +222,36 @@ def process_wild_pokemon(self): encounter_slots = get_encounter_slots(self) placed_mons = {pokemon: 0 for pokemon in poke_data.pokemon_data.keys()} + zone_mapping = {} if self.multiworld.randomize_wild_pokemon[self.player].value: mons_list = [pokemon for pokemon in poke_data.pokemon_data.keys() if pokemon not in poke_data.legendary_pokemon or self.multiworld.randomize_legendary_pokemon[self.player].value == 3] self.multiworld.random.shuffle(encounter_slots) locations = [] for slot in encounter_slots: - mon = randomize_pokemon(self, slot.original_item, mons_list, - self.multiworld.randomize_wild_pokemon[self.player].value, self.multiworld.random) + location = self.multiworld.get_location(slot.name, self.player) + zone = " - ".join(location.name.split(" - ")[:-1]) + if zone not in zone_mapping: + zone_mapping[zone] = {} + original_mon = slot.original_item + if self.multiworld.area_1_to_1_mapping[self.player] and original_mon in zone_mapping[zone]: + mon = zone_mapping[zone][original_mon] + else: + mon = randomize_pokemon(self, original_mon, mons_list, + self.multiworld.randomize_wild_pokemon[self.player].value, self.multiworld.random) # if static Pokemon are not randomized, we make sure nothing on Pokemon Tower 6F is a Marowak # if static Pokemon are randomized we deal with that during static encounter randomization while (self.multiworld.randomize_static_pokemon[self.player].value == 0 and mon == "Marowak" and "Pokemon Tower 6F" in slot.name): # to account for the possibility that only one ground type Pokemon exists, match only stats for this fix - mon = randomize_pokemon(self, slot.original_item, mons_list, 2, self.multiworld.random) + mon = randomize_pokemon(self, original_mon, mons_list, 2, self.multiworld.random) placed_mons[mon] += 1 - location = self.multiworld.get_location(slot.name, self.player) location.item = self.create_item(mon) location.event = True location.locked = True location.item.location = location locations.append(location) + zone_mapping[zone][original_mon] = mon mons_to_add = [] remaining_pokemon = [pokemon for pokemon in poke_data.pokemon_data.keys() if placed_mons[pokemon] == 0 and @@ -223,22 +274,46 @@ def process_wild_pokemon(self): for mon in mons_to_add: stat_base = get_base_stat_total(mon) candidate_locations = get_encounter_slots(self) - if self.multiworld.randomize_wild_pokemon[self.player].value in [1, 3]: - candidate_locations = [slot for slot in candidate_locations if any([poke_data.pokemon_data[slot.original_item][ - "type1"] in [self.local_poke_data[mon]["type1"], self.local_poke_data[mon]["type2"]], - poke_data.pokemon_data[slot.original_item]["type2"] in [self.local_poke_data[mon]["type1"], - self.local_poke_data[mon]["type2"]]])] - if not candidate_locations: - candidate_locations = get_encounter_slots(self) candidate_locations = [self.multiworld.get_location(location.name, self.player) for location in candidate_locations] - candidate_locations.sort(key=lambda slot: abs(get_base_stat_total(slot.item.name) - stat_base)) + if self.multiworld.randomize_wild_pokemon[self.player].current_key in ["match_base_stats", "match_types_and_base_stats"]: + candidate_locations.sort(key=lambda slot: abs(get_base_stat_total(slot.item.name) - stat_base)) + if self.multiworld.randomize_wild_pokemon[self.player].current_key in ["match_types", "match_types_and_base_stats"]: + candidate_locations.sort(key=lambda slot: not any([poke_data.pokemon_data[slot.original_item]["type1"] in + [self.local_poke_data[mon]["type1"], self.local_poke_data[mon]["type2"]], + poke_data.pokemon_data[slot.original_item]["type2"] in + [self.local_poke_data[mon]["type1"], self.local_poke_data[mon]["type2"]]])) for location in candidate_locations: - if placed_mons[location.item.name] > 1 or location.item.name not in poke_data.first_stage_pokemon: - placed_mons[location.item.name] -= 1 - location.item = self.create_item(mon) - location.item.location = location + zone = " - ".join(location.name.split(" - ")[:-1]) + if self.multiworld.catch_em_all[self.player] == "all_pokemon" and self.multiworld.area_1_to_1_mapping[self.player]: + if not [self.multiworld.get_location(l.name, self.player) for l in get_encounter_slots(self) + if (not l.name.startswith(zone)) and + self.multiworld.get_location(l.name, self.player).item.name == location.item.name]: + continue + if self.multiworld.catch_em_all[self.player] == "first_stage" and self.multiworld.area_1_to_1_mapping[self.player]: + if not [self.multiworld.get_location(l.name, self.player) for l in get_encounter_slots(self) + if (not l.name.startswith(zone)) and + self.multiworld.get_location(l.name, self.player).item.name == location.item.name and l.name + not in poke_data.evolves_from]: + continue + + if placed_mons[location.item.name] < 2 and (location.item.name in poke_data.first_stage_pokemon + or self.multiworld.catch_em_all[self.player]): + continue + + if self.multiworld.area_1_to_1_mapping[self.player]: + place_locations = [place_location for place_location in candidate_locations if + place_location.name.startswith(zone) and + place_location.item.name == location.item.name] + else: + place_locations = [location] + for place_location in place_locations: + placed_mons[place_location.item.name] -= 1 + place_location.item = self.create_item(mon) + place_location.item.location = place_location placed_mons[mon] += 1 - break + break + else: + raise Exception else: for slot in encounter_slots: @@ -250,10 +325,41 @@ def process_wild_pokemon(self): placed_mons[location.item.name] += 1 +def process_move_data(self): + self.local_move_data = deepcopy(poke_data.moves) + if self.multiworld.move_balancing[self.player]: + self.local_move_data["Sing"]["accuracy"] = 30 + self.local_move_data["Sleep Powder"]["accuracy"] = 40 + self.local_move_data["Spore"]["accuracy"] = 50 + self.local_move_data["Sonicboom"]["effect"] = 0 + self.local_move_data["Sonicboom"]["power"] = 50 + self.local_move_data["Dragon Rage"]["effect"] = 0 + self.local_move_data["Dragon Rage"]["power"] = 80 + self.local_move_data["Horn Drill"]["effect"] = 0 + self.local_move_data["Horn Drill"]["power"] = 70 + self.local_move_data["Horn Drill"]["accuracy"] = 90 + self.local_move_data["Guillotine"]["effect"] = 0 + self.local_move_data["Guillotine"]["power"] = 70 + self.local_move_data["Guillotine"]["accuracy"] = 90 + self.local_move_data["Fissure"]["effect"] = 0 + self.local_move_data["Fissure"]["power"] = 70 + self.local_move_data["Fissure"]["accuracy"] = 90 + self.local_move_data["Blizzard"]["accuracy"] = 70 + if self.multiworld.randomize_tm_moves[self.player]: + self.local_tms = self.multiworld.random.sample([move for move in poke_data.moves.keys() if move not in + ["No Move"] + poke_data.hm_moves], 50) + else: + self.local_tms = poke_data.tm_moves.copy() + + def process_pokemon_data(self): local_poke_data = deepcopy(poke_data.pokemon_data) learnsets = deepcopy(poke_data.learnsets) + tms_hms = self.local_tms + poke_data.hm_moves + + + compat_hms = set() for mon, mon_data in local_poke_data.items(): if self.multiworld.randomize_pokemon_stats[self.player].value == 1: @@ -265,18 +371,21 @@ def process_pokemon_data(self): mon_data["spd"] = stats[3] mon_data["spc"] = stats[4] elif self.multiworld.randomize_pokemon_stats[self.player].value == 2: - old_stats = mon_data["hp"] + mon_data["atk"] + mon_data["def"] + mon_data["spd"] + mon_data["spc"] - 5 - stats = [1, 1, 1, 1, 1] - while old_stats > 0: - stat = self.multiworld.random.randint(0, 4) - if stats[stat] < 255: - old_stats -= 1 - stats[stat] += 1 - mon_data["hp"] = stats[0] - mon_data["atk"] = stats[1] - mon_data["def"] = stats[2] - mon_data["spd"] = stats[3] - mon_data["spc"] = stats[4] + first_run = True + while (mon_data["hp"] > 255 or mon_data["atk"] > 255 or mon_data["def"] > 255 or mon_data["spd"] > 255 + or mon_data["spc"] > 255 or first_run): + first_run = False + total_stats = mon_data["hp"] + mon_data["atk"] + mon_data["def"] + mon_data["spd"] + mon_data["spc"] - 60 + dist = [self.multiworld.random.randint(1, 101) / 100, self.multiworld.random.randint(1, 101) / 100, + self.multiworld.random.randint(1, 101) / 100, self.multiworld.random.randint(1, 101) / 100, + self.multiworld.random.randint(1, 101) / 100] + total_dist = sum(dist) + + mon_data["hp"] = int(round(dist[0] / total_dist * total_stats) + 20) + mon_data["atk"] = int(round(dist[1] / total_dist * total_stats) + 10) + mon_data["def"] = int(round(dist[2] / total_dist * total_stats) + 10) + mon_data["spd"] = int(round(dist[3] / total_dist * total_stats) + 10) + mon_data["spc"] = int(round(dist[4] / total_dist * total_stats) + 10) if self.multiworld.randomize_pokemon_types[self.player].value: if self.multiworld.randomize_pokemon_types[self.player].value == 1 and mon in poke_data.evolves_from: type1 = local_poke_data[poke_data.evolves_from[mon]]["type1"] @@ -318,46 +427,237 @@ def process_pokemon_data(self): moves = list(poke_data.moves.keys()) for move in ["No Move"] + poke_data.hm_moves: moves.remove(move) - mon_data["start move 1"] = get_move(moves, chances, self.multiworld.random, True) - for i in range(2, 5): - if mon_data[f"start move {i}"] != "No Move" or self.multiworld.start_with_four_moves[ - self.player].value == 1: - mon_data[f"start move {i}"] = get_move(moves, chances, self.multiworld.random) + if self.multiworld.confine_transform_to_ditto[self.player]: + moves.remove("Transform") + if self.multiworld.start_with_four_moves[self.player]: + num_moves = 4 + else: + num_moves = len([i for i in [mon_data["start move 1"], mon_data["start move 2"], + mon_data["start move 3"], mon_data["start move 4"]] if i != "No Move"]) if mon in learnsets: - for move_num in range(0, len(learnsets[mon])): - learnsets[mon][move_num] = get_move(moves, chances, self.multiworld.random) + num_moves += len(learnsets[mon]) + non_power_moves = [] + learnsets[mon] = [] + for i in range(num_moves): + if i == 0 and mon == "Ditto" and self.multiworld.confine_transform_to_ditto[self.player]: + move = "Transform" + else: + move = get_move(self.local_move_data, moves, chances, self.multiworld.random) + while move == "Transform" and self.multiworld.confine_transform_to_ditto[self.player]: + move = get_move(self.local_move_data, moves, chances, self.multiworld.random) + if self.local_move_data[move]["power"] < 5: + non_power_moves.append(move) + else: + learnsets[mon].append(move) + learnsets[mon].sort(key=lambda move: self.local_move_data[move]["power"]) + if learnsets[mon]: + for move in non_power_moves: + learnsets[mon].insert(self.multiworld.random.randint(1, len(learnsets[mon])), move) + else: + learnsets[mon] = non_power_moves + for i in range(1, 5): + if mon_data[f"start move {i}"] != "No Move" or self.multiworld.start_with_four_moves[self.player]: + mon_data[f"start move {i}"] = learnsets[mon].pop(0) + if self.multiworld.randomize_pokemon_catch_rates[self.player].value: mon_data["catch rate"] = self.multiworld.random.randint(self.multiworld.minimum_catch_rate[self.player], 255) else: mon_data["catch rate"] = max(self.multiworld.minimum_catch_rate[self.player], mon_data["catch rate"]) - if mon != "Mew": - tms_hms = poke_data.tm_moves + poke_data.hm_moves - for flag, tm_move in enumerate(tms_hms): - if ((mon in poke_data.evolves_from.keys() and mon_data["type1"] == - local_poke_data[poke_data.evolves_from[mon]]["type1"] and mon_data["type2"] == - local_poke_data[poke_data.evolves_from[mon]]["type2"]) and ( - (flag < 50 and self.multiworld.tm_compatibility[self.player].value in [1, 2]) or ( - flag >= 51 and self.multiworld.hm_compatibility[self.player].value in [1, 2]))): - bit = 1 if local_poke_data[poke_data.evolves_from[mon]]["tms"][int(flag / 8)] & 1 << (flag % 8) else 0 - elif (flag < 50 and self.multiworld.tm_compatibility[self.player].value == 1) or (flag >= 50 and self.multiworld.hm_compatibility[self.player].value == 1): - type_match = poke_data.moves[tm_move]["type"] in [mon_data["type1"], mon_data["type2"]] - bit = int(self.multiworld.random.randint(1, 100) < [[90, 50, 25], [100, 75, 25]][flag >= 50][0 if type_match else 1 if poke_data.moves[tm_move]["type"] == "Normal" else 2]) - elif (flag < 50 and self.multiworld.tm_compatibility[self.player].value == 2) or (flag >= 50 and self.multiworld.hm_compatibility[self.player].value == 2): - bit = self.multiworld.random.randint(0, 1) - elif (flag < 50 and self.multiworld.tm_compatibility[self.player].value == 3) or (flag >= 50 and self.multiworld.hm_compatibility[self.player].value == 3): + def roll_tm_compat(roll_move): + if self.local_move_data[roll_move]["type"] in [mon_data["type1"], mon_data["type2"]]: + if roll_move in poke_data.hm_moves: + if self.multiworld.hm_same_type_compatibility[self.player].value == -1: + return mon_data["tms"][int(flag / 8)] + r = self.multiworld.random.randint(1, 100) <= self.multiworld.hm_same_type_compatibility[self.player].value + if r and mon not in poke_data.legendary_pokemon: + compat_hms.add(roll_move) + return r + else: + if self.multiworld.tm_same_type_compatibility[self.player].value == -1: + return mon_data["tms"][int(flag / 8)] + return self.multiworld.random.randint(1, 100) <= self.multiworld.tm_same_type_compatibility[self.player].value + elif self.local_move_data[roll_move]["type"] == "Normal" and "Normal" not in [mon_data["type1"], mon_data["type2"]]: + if roll_move in poke_data.hm_moves: + if self.multiworld.hm_normal_type_compatibility[self.player].value == -1: + return mon_data["tms"][int(flag / 8)] + r = self.multiworld.random.randint(1, 100) <= self.multiworld.hm_normal_type_compatibility[self.player].value + if r and mon not in poke_data.legendary_pokemon: + compat_hms.add(roll_move) + return r + else: + if self.multiworld.tm_normal_type_compatibility[self.player].value == -1: + return mon_data["tms"][int(flag / 8)] + return self.multiworld.random.randint(1, 100) <= self.multiworld.tm_normal_type_compatibility[self.player].value + else: + if roll_move in poke_data.hm_moves: + if self.multiworld.hm_other_type_compatibility[self.player].value == -1: + return mon_data["tms"][int(flag / 8)] + r = self.multiworld.random.randint(1, 100) <= self.multiworld.hm_other_type_compatibility[self.player].value + if r and mon not in poke_data.legendary_pokemon: + compat_hms.add(roll_move) + return r + else: + if self.multiworld.tm_other_type_compatibility[self.player].value == -1: + return mon_data["tms"][int(flag / 8)] + return self.multiworld.random.randint(1, 100) <= self.multiworld.tm_other_type_compatibility[self.player].value + + + for flag, tm_move in enumerate(tms_hms): + if mon in poke_data.evolves_from.keys() and self.multiworld.inherit_tm_hm_compatibility[self.player]: + + if local_poke_data[poke_data.evolves_from[mon]]["tms"][int(flag / 8)] & 1 << (flag % 8): + # always inherit learnable tms/hms bit = 1 else: - continue - if bit: - mon_data["tms"][int(flag / 8)] |= 1 << (flag % 8) - else: - mon_data["tms"][int(flag / 8)] &= ~(1 << (flag % 8)) + if self.local_move_data[tm_move]["type"] in [mon_data["type1"], mon_data["type2"]] and \ + self.local_move_data[tm_move]["type"] not in [ + local_poke_data[poke_data.evolves_from[mon]]["type1"], + local_poke_data[poke_data.evolves_from[mon]]["type2"]]: + # the tm/hm is for a move whose type matches current mon, but not pre-evolved form + # so this gets full chance roll + bit = roll_tm_compat(tm_move) + # otherwise 50% reduced chance to add compatibility over pre-evolved form + elif self.multiworld.random.randint(1, 100) > 50 and roll_tm_compat(tm_move): + bit = 1 + else: + bit = 0 + else: + bit = roll_tm_compat(tm_move) + if bit: + mon_data["tms"][int(flag / 8)] |= 1 << (flag % 8) + else: + mon_data["tms"][int(flag / 8)] &= ~(1 << (flag % 8)) + + hm_verify = ["Surf", "Strength"] + if self.multiworld.accessibility[self.player] != "minimal" or ((not + self.multiworld.badgesanity[self.player]) and max(self.multiworld.elite_four_condition[self.player], + self.multiworld.victory_road_condition[self.player]) > 7): + hm_verify += ["Cut"] + if self.multiworld.accessibility[self.player] != "minimal" and (self.multiworld.trainersanity[self.player] or + self.multiworld.extra_key_items[self.player]): + hm_verify += ["Flash"] + + for hm_move in hm_verify: + if hm_move not in compat_hms: + mon = self.multiworld.random.choice([mon for mon in poke_data.pokemon_data if mon not in + poke_data.legendary_pokemon]) + flag = tms_hms.index(hm_move) + local_poke_data[mon]["tms"][int(flag / 8)] |= 1 << (flag % 8) self.local_poke_data = local_poke_data self.learnsets = learnsets +def write_quizzes(self, data, random): + + def get_quiz(q, a): + if q == 0: + r = random.randint(0, 3) + if r == 0: + mon = self.trade_mons["Trade_Dux"] + text = "A woman inVermilion City" + elif r == 1: + mon = self.trade_mons["Trade_Lola"] + text = "A man inCerulean City" + elif r == 2: + mon = self.trade_mons["Trade_Marcel"] + text = "Someone on Route 2" + elif r == 3: + mon = self.trade_mons["Trade_Spot"] + text = "Someone on Route 5" + if not a: + answers.append(0) + old_mon = mon + while old_mon == mon: + mon = random.choice(list(poke_data.pokemon_data.keys())) + + return encode_text(f"{text}was looking for{mon}?") + elif q == 1: + for location in self.multiworld.get_filled_locations(): + if location.item.name == "Secret Key" and location.item.player == self.player: + break + if location.player == self.player: + player_name = "yourself" + else: + player_name = self.multiworld.player_names[location.player] + if not a: + if len(self.multiworld.player_name) > 1: + old_name = player_name + while old_name == player_name: + player_name = random.choice(list(self.multiworld.player_name.values())) + else: + return encode_text("You're playingin a multiworldwith otherplayers?") + return encode_text(f"The Secret Key wasfound by{player_name[:17]}?") + elif q == 2: + if a: + return encode_text(f"#mon ispronouncedPo-kay-mon?") + else: + if random.randint(0, 1): + return encode_text(f"#mon ispronouncedPo-key-mon?") + else: + return encode_text(f"#mon ispronouncedPo-kuh-mon?") + elif q == 3: + starters = [" ".join(self.multiworld.get_location( + f"Pallet Town - Starter {i}", self.player).item.name.split(" ")[1:]) for i in range(1, 4)] + mon = random.choice(starters) + nots = random.choice(range(8, 16, 2)) + if random.randint(0, 1): + while mon in starters: + mon = random.choice(list(poke_data.pokemon_data.keys())) + if a: + nots += 1 + elif not a: + nots += 1 + text = f"{mon} was" + while nots > 0: + i = random.randint(1, min(4, nots)) + text += ("not " * i) + "" + nots -= i + text += "a starter choice?" + return encode_text(text) + elif q == 4: + if a: + tm_text = self.local_tms[27] + else: + if self.multiworld.randomize_tm_moves[self.player]: + wrong_tms = self.local_tms.copy() + wrong_tms.pop(27) + tm_text = random.choice(wrong_tms) + else: + tm_text = "TOMBSTONER" + return encode_text(f"TM28 contains{tm_text.upper()}?") + elif q == 5: + i = 8 + while not a and i in [1, 8]: + i = random.randint(0, 99999999) + return encode_text(f"There are {i}certified #MONLEAGUE BADGEs?") + elif q == 6: + i = 2 + while not a and i in [1, 2]: + i = random.randint(0, 99) + return encode_text(f"POLIWAG evolves {i}times?") + elif q == 7: + entity = "Motor Carrier" + if not a: + entity = random.choice(["Driver", "Shipper"]) + return encode_text("Title 49 of theU.S. Code ofFederalRegulations part397.67 states" + f"that the{entity}is responsiblefor planningroutes when" + "hazardousmaterials aretransported?") + + answers = [random.randint(0, 1), random.randint(0, 1), random.randint(0, 1), + random.randint(0, 1), random.randint(0, 1), random.randint(0, 1)] + + questions = random.sample((range(0, 8)), 6) + question_texts = [] + for i, question in enumerate(questions): + question_texts.append(get_quiz(question, answers[i])) + + for i, quiz in enumerate(["A", "B", "C", "D", "E", "F"]): + data[rom_addresses[f"Quiz_Answer_{quiz}"]] = int(not answers[i]) << 4 | (i + 1) + write_bytes(data, question_texts[i], rom_addresses[f"Text_Quiz_{quiz}"]) + + def generate_output(self, output_directory: str): random = self.multiworld.per_slot_randoms[self.player] game_version = self.multiworld.game_version[self.player].current_key @@ -384,10 +684,33 @@ def generate_output(self, output_directory: str): elif " ".join(location.item.name.split()[1:]) in poke_data.pokemon_data.keys(): data[address] = poke_data.pokemon_data[" ".join(location.item.name.split()[1:])]["id"] else: - data[address] = self.item_name_to_id[location.item.name] - 172000000 + item_id = self.item_name_to_id[location.item.name] - 172000000 + if item_id > 255: + item_id -= 256 + data[address] = item_id else: data[location.rom_address] = 0x2C # AP Item + + def set_trade_mon(address, loc): + mon = self.multiworld.get_location(loc, self.player).item.name + data[rom_addresses[address]] = poke_data.pokemon_data[mon]["id"] + self.trade_mons[address] = mon + + if game_version == "red": + set_trade_mon("Trade_Terry", "Safari Zone Center - Wild Pokemon - 5") + set_trade_mon("Trade_Spot", "Safari Zone East - Wild Pokemon - 1") + else: + set_trade_mon("Trade_Terry", "Safari Zone Center - Wild Pokemon - 7") + set_trade_mon("Trade_Spot", "Safari Zone East - Wild Pokemon - 7") + set_trade_mon("Trade_Marcel", "Route 24 - Wild Pokemon - 6") + set_trade_mon("Trade_Sailor", "Pokemon Mansion 1F - Wild Pokemon - 3") + set_trade_mon("Trade_Dux", "Route 3 - Wild Pokemon - 2") + set_trade_mon("Trade_Marc", "Route 23 - Super Rod Pokemon - 1") + set_trade_mon("Trade_Lola", "Route 10 - Super Rod Pokemon - 1") + set_trade_mon("Trade_Doris", "Cerulean Cave 1F - Wild Pokemon - 9") + set_trade_mon("Trade_Crinkles", "Route 12 - Wild Pokemon - 4") + data[rom_addresses['Fly_Location']] = self.fly_map_code if self.multiworld.tea[self.player].value: @@ -421,6 +744,14 @@ def generate_output(self, output_directory: str): if self.multiworld.old_man[self.player].value == 2: data[rom_addresses['Option_Old_Man']] = 0x11 data[rom_addresses['Option_Old_Man_Lying']] = 0x15 + if self.multiworld.require_pokedex[self.player]: + data[rom_addresses["Require_Pokedex_A"]] = 1 + data[rom_addresses["Require_Pokedex_B"]] = 1 + if self.multiworld.dexsanity[self.player]: + data[rom_addresses["Option_Dexsanity_A"]] = 1 + data[rom_addresses["Option_Dexsanity_B"]] = 1 + if self.multiworld.all_pokemon_seen[self.player]: + data[rom_addresses["Option_Pokedex_Seen"]] = 1 money = str(self.multiworld.starting_money[self.player].value).zfill(6) data[rom_addresses["Starting_Money_High"]] = int(money[:2], 16) data[rom_addresses["Starting_Money_Middle"]] = int(money[2:4], 16) @@ -433,6 +764,7 @@ def generate_output(self, output_directory: str): write_bytes(data, encode_text( " ".join(self.multiworld.get_location("Route 3 - Pokemon For Sale", self.player).item.name.upper().split()[1:])), rom_addresses["Text_Magikarp_Salesman"]) + write_quizzes(self, data, random) if self.multiworld.badges_needed_for_hm_moves[self.player].value == 0: for hm_move in poke_data.hm_moves: @@ -492,10 +824,10 @@ def generate_output(self, output_directory: str): data[address + 17] = poke_data.moves[self.local_poke_data[mon]["start move 3"]]["id"] data[address + 18] = poke_data.moves[self.local_poke_data[mon]["start move 4"]]["id"] write_bytes(data, self.local_poke_data[mon]["tms"], address + 20) - if mon in self.learnsets: - address = rom_addresses["Learnset_" + mon.replace(" ", "")] - for i, move in enumerate(self.learnsets[mon]): - data[(address + 1) + i * 2] = poke_data.moves[move]["id"] + if mon in self.learnsets and self.learnsets[mon]: + address = rom_addresses["Learnset_" + mon.replace(" ", "")] + for i, move in enumerate(self.learnsets[mon]): + data[(address + 1) + i * 2] = poke_data.moves[move]["id"] data[rom_addresses["Option_Aide_Rt2"]] = self.multiworld.oaks_aide_rt_2[self.player].value data[rom_addresses["Option_Aide_Rt11"]] = self.multiworld.oaks_aide_rt_11[self.player].value @@ -507,8 +839,8 @@ def generate_output(self, output_directory: str): if self.multiworld.reusable_tms[self.player].value: data[rom_addresses["Option_Reusable_TMs"]] = 0xC9 - data[rom_addresses["Option_Trainersanity"]] = self.multiworld.trainersanity[self.player].value - data[rom_addresses["Option_Trainersanity2"]] = self.multiworld.trainersanity[self.player].value + for i in range(1, 10): + data[rom_addresses[f"Option_Trainersanity{i}"]] = self.multiworld.trainersanity[self.player].value data[rom_addresses["Option_Always_Half_STAB"]] = int(not self.multiworld.same_type_attack_bonus[self.player].value) @@ -532,8 +864,23 @@ def generate_output(self, output_directory: str): if data[rom_addresses["Start_Inventory"] + item.code - 172000000] < 255: data[rom_addresses["Start_Inventory"] + item.code - 172000000] += 1 + set_mon_palettes(self, random, data) process_trainer_data(self, data, random) + for move_data in self.local_move_data.values(): + if move_data["id"] == 0: + continue + address = rom_addresses["Move_Data"] + ((move_data["id"] - 1) * 6) + write_bytes(data, bytearray([move_data["id"], move_data["effect"], move_data["power"], + poke_data.type_ids[move_data["type"]], round(move_data["accuracy"] * 2.55), move_data["pp"]]), address) + + TM_IDs = bytearray([poke_data.moves[move]["id"] for move in self.local_tms]) + write_bytes(data, TM_IDs, rom_addresses["TM_Moves"]) + + if self.multiworld.randomize_rock_tunnel[self.player]: + seed = randomize_rock_tunnel(data, random) + write_bytes(data, encode_text(f"SEED: {seed}"), rom_addresses["Text_Rock_Tunnel_Sign"]) + mons = [mon["id"] for mon in poke_data.pokemon_data.values()] random.shuffle(mons) data[rom_addresses['Title_Mon_First']] = mons.pop() @@ -564,7 +911,7 @@ def generate_output(self, output_directory: str): else: write_bytes(data, self.rival_name, rom_addresses['Rival_Name']) - data[0xFF00] = 1 # client compatibility version + data[0xFF00] = 2 # client compatibility version write_bytes(data, self.multiworld.seed_name.encode(), 0xFFDB) write_bytes(data, self.multiworld.player_name[self.player].encode(), 0xFFF0) diff --git a/worlds/pokemon_rb/rom_addresses.py b/worlds/pokemon_rb/rom_addresses.py index 30c38fa240..11b6e1463d 100644 --- a/worlds/pokemon_rb/rom_addresses.py +++ b/worlds/pokemon_rb/rom_addresses.py @@ -1,7 +1,7 @@ rom_addresses = { "Option_Encounter_Minimum_Steps": 0x3c3, - "Option_Blind_Trainers": 0x30fc, - "Option_Trainersanity": 0x318c, + "Option_Blind_Trainers": 0x30e2, + "Option_Trainersanity1": 0x3172, "Option_Lose_Money": 0x40d4, "Base_Stats_Mew": 0x4260, "Title_Mon_First": 0x4373, @@ -9,94 +9,95 @@ rom_addresses = { "Player_Name": 0x456e, "Rival_Name": 0x4576, "Price_Master_Ball": 0x45d0, - "Title_Seed": 0x5e3a, - "Title_Slot_Name": 0x5e5a, - "PC_Item": 0x6228, - "PC_Item_Quantity": 0x622d, - "Options": 0x623d, - "Fly_Location": 0x6242, - "Skip_Player_Name": 0x625b, - "Skip_Rival_Name": 0x6269, - "Option_Old_Man": 0xcafc, - "Option_Old_Man_Lying": 0xcaff, - "Option_Boulders": 0xcda5, - "Option_Rock_Tunnel_Extra_Items": 0xcdae, - "Wild_Route1": 0xd108, - "Wild_Route2": 0xd11e, - "Wild_Route22": 0xd134, - "Wild_ViridianForest": 0xd14a, - "Wild_Route3": 0xd160, - "Wild_MtMoon1F": 0xd176, - "Wild_MtMoonB1F": 0xd18c, - "Wild_MtMoonB2F": 0xd1a2, - "Wild_Route4": 0xd1b8, - "Wild_Route24": 0xd1ce, - "Wild_Route25": 0xd1e4, - "Wild_Route9": 0xd1fa, - "Wild_Route5": 0xd210, - "Wild_Route6": 0xd226, - "Wild_Route11": 0xd23c, - "Wild_RockTunnel1F": 0xd252, - "Wild_RockTunnelB1F": 0xd268, - "Wild_Route10": 0xd27e, - "Wild_Route12": 0xd294, - "Wild_Route8": 0xd2aa, - "Wild_Route7": 0xd2c0, - "Wild_PokemonTower3F": 0xd2da, - "Wild_PokemonTower4F": 0xd2f0, - "Wild_PokemonTower5F": 0xd306, - "Wild_PokemonTower6F": 0xd31c, - "Wild_PokemonTower7F": 0xd332, - "Wild_Route13": 0xd348, - "Wild_Route14": 0xd35e, - "Wild_Route15": 0xd374, - "Wild_Route16": 0xd38a, - "Wild_Route17": 0xd3a0, - "Wild_Route18": 0xd3b6, - "Wild_SafariZoneCenter": 0xd3cc, - "Wild_SafariZoneEast": 0xd3e2, - "Wild_SafariZoneNorth": 0xd3f8, - "Wild_SafariZoneWest": 0xd40e, - "Wild_SeaRoutes": 0xd425, - "Wild_SeafoamIslands1F": 0xd43a, - "Wild_SeafoamIslandsB1F": 0xd450, - "Wild_SeafoamIslandsB2F": 0xd466, - "Wild_SeafoamIslandsB3F": 0xd47c, - "Wild_SeafoamIslandsB4F": 0xd492, - "Wild_PokemonMansion1F": 0xd4a8, - "Wild_PokemonMansion2F": 0xd4be, - "Wild_PokemonMansion3F": 0xd4d4, - "Wild_PokemonMansionB1F": 0xd4ea, - "Wild_Route21": 0xd500, - "Wild_Surf_Route21": 0xd515, - "Wild_CeruleanCave1F": 0xd52a, - "Wild_CeruleanCave2F": 0xd540, - "Wild_CeruleanCaveB1F": 0xd556, - "Wild_PowerPlant": 0xd56c, - "Wild_Route23": 0xd582, - "Wild_VictoryRoad2F": 0xd598, - "Wild_VictoryRoad3F": 0xd5ae, - "Wild_VictoryRoad1F": 0xd5c4, - "Wild_DiglettsCave": 0xd5da, - "Ghost_Battle5": 0xd730, - "HM_Surf_Badge_a": 0xda1e, - "HM_Surf_Badge_b": 0xda23, - "Wild_Old_Rod": 0xe320, - "Wild_Good_Rod": 0xe34d, - "Option_Reusable_TMs": 0xe619, - "Wild_Super_Rod_A": 0xea4e, - "Wild_Super_Rod_B": 0xea53, - "Wild_Super_Rod_C": 0xea58, - "Wild_Super_Rod_D": 0xea5f, - "Wild_Super_Rod_E": 0xea64, - "Wild_Super_Rod_F": 0xea69, - "Wild_Super_Rod_G": 0xea72, - "Wild_Super_Rod_H": 0xea7b, - "Wild_Super_Rod_I": 0xea84, - "Wild_Super_Rod_J": 0xea8d, - "Starting_Money_High": 0xf957, - "Starting_Money_Middle": 0xf95a, - "Starting_Money_Low": 0xf95d, + "Title_Seed": 0x5e57, + "Title_Slot_Name": 0x5e77, + "PC_Item": 0x6245, + "PC_Item_Quantity": 0x624a, + "Options": 0x625a, + "Fly_Location": 0x625f, + "Skip_Player_Name": 0x6278, + "Skip_Rival_Name": 0x6286, + "Option_Old_Man": 0xcb05, + "Option_Old_Man_Lying": 0xcb08, + "Option_Boulders": 0xcdae, + "Option_Rock_Tunnel_Extra_Items": 0xcdb7, + "Wild_Route1": 0xd111, + "Wild_Route2": 0xd127, + "Wild_Route22": 0xd13d, + "Wild_ViridianForest": 0xd153, + "Wild_Route3": 0xd169, + "Wild_MtMoon1F": 0xd17f, + "Wild_MtMoonB1F": 0xd195, + "Wild_MtMoonB2F": 0xd1ab, + "Wild_Route4": 0xd1c1, + "Wild_Route24": 0xd1d7, + "Wild_Route25": 0xd1ed, + "Wild_Route9": 0xd203, + "Wild_Route5": 0xd219, + "Wild_Route6": 0xd22f, + "Wild_Route11": 0xd245, + "Wild_RockTunnel1F": 0xd25b, + "Wild_RockTunnelB1F": 0xd271, + "Wild_Route10": 0xd287, + "Wild_Route12": 0xd29d, + "Wild_Route8": 0xd2b3, + "Wild_Route7": 0xd2c9, + "Wild_PokemonTower3F": 0xd2e3, + "Wild_PokemonTower4F": 0xd2f9, + "Wild_PokemonTower5F": 0xd30f, + "Wild_PokemonTower6F": 0xd325, + "Wild_PokemonTower7F": 0xd33b, + "Wild_Route13": 0xd351, + "Wild_Route14": 0xd367, + "Wild_Route15": 0xd37d, + "Wild_Route16": 0xd393, + "Wild_Route17": 0xd3a9, + "Wild_Route18": 0xd3bf, + "Wild_SafariZoneCenter": 0xd3d5, + "Wild_SafariZoneEast": 0xd3eb, + "Wild_SafariZoneNorth": 0xd401, + "Wild_SafariZoneWest": 0xd417, + "Wild_SeaRoutes": 0xd42e, + "Wild_SeafoamIslands1F": 0xd443, + "Wild_SeafoamIslandsB1F": 0xd459, + "Wild_SeafoamIslandsB2F": 0xd46f, + "Wild_SeafoamIslandsB3F": 0xd485, + "Wild_SeafoamIslandsB4F": 0xd49b, + "Wild_PokemonMansion1F": 0xd4b1, + "Wild_PokemonMansion2F": 0xd4c7, + "Wild_PokemonMansion3F": 0xd4dd, + "Wild_PokemonMansionB1F": 0xd4f3, + "Wild_Route21": 0xd509, + "Wild_Surf_Route21": 0xd51e, + "Wild_CeruleanCave1F": 0xd533, + "Wild_CeruleanCave2F": 0xd549, + "Wild_CeruleanCaveB1F": 0xd55f, + "Wild_PowerPlant": 0xd575, + "Wild_Route23": 0xd58b, + "Wild_VictoryRoad2F": 0xd5a1, + "Wild_VictoryRoad3F": 0xd5b7, + "Wild_VictoryRoad1F": 0xd5cd, + "Wild_DiglettsCave": 0xd5e3, + "Ghost_Battle5": 0xd739, + "HM_Surf_Badge_a": 0xda2f, + "HM_Surf_Badge_b": 0xda34, + "Wild_Old_Rod": 0xe331, + "Wild_Good_Rod": 0xe35e, + "Option_Reusable_TMs": 0xe62a, + "Wild_Super_Rod_A": 0xea5f, + "Wild_Super_Rod_B": 0xea64, + "Wild_Super_Rod_C": 0xea69, + "Wild_Super_Rod_D": 0xea70, + "Wild_Super_Rod_E": 0xea75, + "Wild_Super_Rod_F": 0xea7a, + "Wild_Super_Rod_G": 0xea83, + "Wild_Super_Rod_H": 0xea8c, + "Wild_Super_Rod_I": 0xea95, + "Wild_Super_Rod_J": 0xea9e, + "Starting_Money_High": 0xf968, + "Starting_Money_Middle": 0xf96b, + "Starting_Money_Low": 0xf96e, + "Option_Pokedex_Seen": 0xf989, "HM_Fly_Badge_a": 0x1318e, "HM_Fly_Badge_b": 0x13193, "HM_Cut_Badge_a": 0x131c4, @@ -105,35 +106,36 @@ rom_addresses = { "HM_Strength_Badge_b": 0x131f9, "HM_Flash_Badge_a": 0x13208, "HM_Flash_Badge_b": 0x1320d, + "TM_Moves": 0x1376c, "Encounter_Chances": 0x13911, "Option_Viridian_Gym_Badges": 0x1901d, "Event_Sleepy_Guy": 0x191bc, "Starter2_K": 0x195a8, "Starter3_K": 0x195b0, "Event_Rocket_Thief": 0x196cc, - "Option_Cerulean_Cave_Condition": 0x1986c, - "Event_Stranded_Man": 0x19b1f, - "Event_Rivals_Sister": 0x19cf2, - "Option_Pokemon_League_Badges": 0x19e0f, - "Shop10": 0x19ee6, - "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_0_ITEM": 0x1a03a, - "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_1_ITEM": 0x1a048, - "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_2_ITEM": 0x1a056, - "Missable_Silph_Co_4F_Item_1": 0x1a0fe, - "Missable_Silph_Co_4F_Item_2": 0x1a105, - "Missable_Silph_Co_4F_Item_3": 0x1a10c, - "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_0_ITEM": 0x1a264, - "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_1_ITEM": 0x1a272, - "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_2_ITEM": 0x1a280, - "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_3_ITEM": 0x1a28e, - "Missable_Silph_Co_5F_Item_1": 0x1a366, - "Missable_Silph_Co_5F_Item_2": 0x1a36d, - "Missable_Silph_Co_5F_Item_3": 0x1a374, - "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_0_ITEM": 0x1a4a4, - "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_1_ITEM": 0x1a4b2, - "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_2_ITEM": 0x1a4c0, - "Missable_Silph_Co_6F_Item_1": 0x1a5e2, - "Missable_Silph_Co_6F_Item_2": 0x1a5e9, + "Option_Cerulean_Cave_Condition": 0x19875, + "Event_Stranded_Man": 0x19b28, + "Event_Rivals_Sister": 0x19cfb, + "Option_Pokemon_League_Badges": 0x19e18, + "Shop10": 0x19eef, + "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_0_ITEM": 0x1a043, + "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_1_ITEM": 0x1a051, + "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_2_ITEM": 0x1a05f, + "Missable_Silph_Co_4F_Item_1": 0x1a107, + "Missable_Silph_Co_4F_Item_2": 0x1a10e, + "Missable_Silph_Co_4F_Item_3": 0x1a115, + "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_0_ITEM": 0x1a26d, + "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_1_ITEM": 0x1a27b, + "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_2_ITEM": 0x1a289, + "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_3_ITEM": 0x1a297, + "Missable_Silph_Co_5F_Item_1": 0x1a36f, + "Missable_Silph_Co_5F_Item_2": 0x1a376, + "Missable_Silph_Co_5F_Item_3": 0x1a37d, + "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_0_ITEM": 0x1a4ad, + "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_1_ITEM": 0x1a4bb, + "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_2_ITEM": 0x1a4c9, + "Missable_Silph_Co_6F_Item_1": 0x1a5eb, + "Missable_Silph_Co_6F_Item_2": 0x1a5f2, "Event_Free_Sample": 0x1cad6, "Starter1_F": 0x1cca2, "Starter2_F": 0x1cca6, @@ -145,49 +147,50 @@ rom_addresses = { "Starter2_I": 0x1d0fa, "Starter1_D": 0x1d101, "Starter3_D": 0x1d10b, - "Starter2_E": 0x1d2e5, - "Starter3_E": 0x1d2ed, - "Event_Pokedex": 0x1d351, - "Event_Oaks_Gift": 0x1d381, - "Event_Pokemart_Quest": 0x1d579, - "Shop1": 0x1d5a3, - "Event_Bicycle_Shop": 0x1d83d, - "Text_Bicycle": 0x1d8d0, - "Event_Fuji": 0x1da05, - "Trainersanity_EVENT_BEAT_MEW_ITEM": 0x1dc58, - "Static_Encounter_Mew": 0x1dc88, - "Gift_Eevee": 0x1dd01, - "Shop7": 0x1dd53, - "Event_Mr_Psychic": 0x1de30, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_0_ITEM": 0x1e32b, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_1_ITEM": 0x1e339, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_2_ITEM": 0x1e347, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_3_ITEM": 0x1e355, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_4_ITEM": 0x1e363, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_5_ITEM": 0x1e371, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_6_ITEM": 0x1e37f, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_7_ITEM": 0x1e38d, - "Trainersanity_EVENT_BEAT_ZAPDOS_ITEM": 0x1e39b, - "Static_Encounter_Voltorb_A": 0x1e40a, - "Static_Encounter_Voltorb_B": 0x1e412, - "Static_Encounter_Voltorb_C": 0x1e41a, - "Static_Encounter_Electrode_A": 0x1e422, - "Static_Encounter_Voltorb_D": 0x1e42a, - "Static_Encounter_Voltorb_E": 0x1e432, - "Static_Encounter_Electrode_B": 0x1e43a, - "Static_Encounter_Voltorb_F": 0x1e442, - "Static_Encounter_Zapdos": 0x1e44a, - "Missable_Power_Plant_Item_1": 0x1e452, - "Missable_Power_Plant_Item_2": 0x1e459, - "Missable_Power_Plant_Item_3": 0x1e460, - "Missable_Power_Plant_Item_4": 0x1e467, - "Missable_Power_Plant_Item_5": 0x1e46e, - "Event_Rt16_House_Woman": 0x1e647, - "Option_Victory_Road_Badges": 0x1e718, - "Event_Bill": 0x1e949, + "Starter2_E": 0x1d300, + "Starter3_E": 0x1d308, + "Event_Pokedex": 0x1d36c, + "Event_Oaks_Gift": 0x1d39c, + "Event_Pokemart_Quest": 0x1d594, + "Shop1": 0x1d5be, + "Event_Bicycle_Shop": 0x1d858, + "Text_Bicycle": 0x1d8eb, + "Event_Fuji": 0x1da20, + "Trainersanity_EVENT_BEAT_MEW_ITEM": 0x1dc73, + "Static_Encounter_Mew": 0x1dca3, + "Gift_Eevee": 0x1dd1c, + "Shop7": 0x1dd6e, + "Event_Mr_Psychic": 0x1de4b, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_0_ITEM": 0x1e346, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_1_ITEM": 0x1e354, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_2_ITEM": 0x1e362, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_3_ITEM": 0x1e370, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_4_ITEM": 0x1e37e, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_5_ITEM": 0x1e38c, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_6_ITEM": 0x1e39a, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_7_ITEM": 0x1e3a8, + "Trainersanity_EVENT_BEAT_ZAPDOS_ITEM": 0x1e3b6, + "Static_Encounter_Voltorb_A": 0x1e425, + "Static_Encounter_Voltorb_B": 0x1e42d, + "Static_Encounter_Voltorb_C": 0x1e435, + "Static_Encounter_Electrode_A": 0x1e43d, + "Static_Encounter_Voltorb_D": 0x1e445, + "Static_Encounter_Voltorb_E": 0x1e44d, + "Static_Encounter_Electrode_B": 0x1e455, + "Static_Encounter_Voltorb_F": 0x1e45d, + "Static_Encounter_Zapdos": 0x1e465, + "Missable_Power_Plant_Item_1": 0x1e46d, + "Missable_Power_Plant_Item_2": 0x1e474, + "Missable_Power_Plant_Item_3": 0x1e47b, + "Missable_Power_Plant_Item_4": 0x1e482, + "Missable_Power_Plant_Item_5": 0x1e489, + "Event_Rt16_House_Woman": 0x1e662, + "Option_Victory_Road_Badges": 0x1e733, + "Event_Bill": 0x1e964, "Starter1_O": 0x372b0, "Starter2_O": 0x372b4, "Starter3_O": 0x372b8, + "Move_Data": 0x38000, "Base_Stats": 0x383de, "Starter3_C": 0x39cf2, "Starter1_C": 0x39cf8, @@ -217,320 +220,345 @@ rom_addresses = { "Rival_Starter3_H": 0x3a4ab, "Rival_Starter1_H": 0x3a4b9, "Trainer_Data_End": 0x3a52e, - "Learnset_Rhydon": 0x3b1d9, - "Learnset_Kangaskhan": 0x3b1e7, - "Learnset_NidoranM": 0x3b1f6, - "Learnset_Clefairy": 0x3b208, - "Learnset_Spearow": 0x3b219, - "Learnset_Voltorb": 0x3b228, - "Learnset_Nidoking": 0x3b234, - "Learnset_Slowbro": 0x3b23c, - "Learnset_Ivysaur": 0x3b24f, - "Learnset_Exeggutor": 0x3b25f, - "Learnset_Lickitung": 0x3b263, - "Learnset_Exeggcute": 0x3b273, - "Learnset_Grimer": 0x3b284, - "Learnset_Gengar": 0x3b292, - "Learnset_NidoranF": 0x3b29b, - "Learnset_Nidoqueen": 0x3b2a9, - "Learnset_Cubone": 0x3b2b4, - "Learnset_Rhyhorn": 0x3b2c3, - "Learnset_Lapras": 0x3b2d1, - "Learnset_Mew": 0x3b2e1, - "Learnset_Gyarados": 0x3b2eb, - "Learnset_Shellder": 0x3b2fb, - "Learnset_Tentacool": 0x3b30a, - "Learnset_Gastly": 0x3b31f, - "Learnset_Scyther": 0x3b325, - "Learnset_Staryu": 0x3b337, - "Learnset_Blastoise": 0x3b347, - "Learnset_Pinsir": 0x3b355, - "Learnset_Tangela": 0x3b363, - "Learnset_Growlithe": 0x3b379, - "Learnset_Onix": 0x3b385, - "Learnset_Fearow": 0x3b391, - "Learnset_Pidgey": 0x3b3a0, - "Learnset_Slowpoke": 0x3b3b1, - "Learnset_Kadabra": 0x3b3c9, - "Learnset_Graveler": 0x3b3e1, - "Learnset_Chansey": 0x3b3ef, - "Learnset_Machoke": 0x3b407, - "Learnset_MrMime": 0x3b413, - "Learnset_Hitmonlee": 0x3b41f, - "Learnset_Hitmonchan": 0x3b42b, - "Learnset_Arbok": 0x3b437, - "Learnset_Parasect": 0x3b443, - "Learnset_Psyduck": 0x3b452, - "Learnset_Drowzee": 0x3b461, - "Learnset_Golem": 0x3b46f, - "Learnset_Magmar": 0x3b47f, - "Learnset_Electabuzz": 0x3b48f, - "Learnset_Magneton": 0x3b49b, - "Learnset_Koffing": 0x3b4ac, - "Learnset_Mankey": 0x3b4bd, - "Learnset_Seel": 0x3b4cc, - "Learnset_Diglett": 0x3b4db, - "Learnset_Tauros": 0x3b4e7, - "Learnset_Farfetchd": 0x3b4f9, - "Learnset_Venonat": 0x3b508, - "Learnset_Dragonite": 0x3b516, - "Learnset_Doduo": 0x3b52b, - "Learnset_Poliwag": 0x3b53c, - "Learnset_Jynx": 0x3b54a, - "Learnset_Moltres": 0x3b558, - "Learnset_Articuno": 0x3b560, - "Learnset_Zapdos": 0x3b568, - "Learnset_Meowth": 0x3b575, - "Learnset_Krabby": 0x3b584, - "Learnset_Vulpix": 0x3b59a, - "Learnset_Pikachu": 0x3b5ac, - "Learnset_Dratini": 0x3b5c1, - "Learnset_Dragonair": 0x3b5d0, - "Learnset_Kabuto": 0x3b5df, - "Learnset_Kabutops": 0x3b5e9, - "Learnset_Horsea": 0x3b5f6, - "Learnset_Seadra": 0x3b602, - "Learnset_Sandshrew": 0x3b615, - "Learnset_Sandslash": 0x3b621, - "Learnset_Omanyte": 0x3b630, - "Learnset_Omastar": 0x3b63a, - "Learnset_Jigglypuff": 0x3b648, - "Learnset_Eevee": 0x3b666, - "Learnset_Flareon": 0x3b670, - "Learnset_Jolteon": 0x3b682, - "Learnset_Vaporeon": 0x3b694, - "Learnset_Machop": 0x3b6a9, - "Learnset_Zubat": 0x3b6b8, - "Learnset_Ekans": 0x3b6c7, - "Learnset_Paras": 0x3b6d6, - "Learnset_Poliwhirl": 0x3b6e6, - "Learnset_Poliwrath": 0x3b6f4, - "Learnset_Beedrill": 0x3b704, - "Learnset_Dodrio": 0x3b714, - "Learnset_Primeape": 0x3b722, - "Learnset_Dugtrio": 0x3b72e, - "Learnset_Venomoth": 0x3b73a, - "Learnset_Dewgong": 0x3b748, - "Learnset_Butterfree": 0x3b762, - "Learnset_Machamp": 0x3b772, - "Learnset_Golduck": 0x3b780, - "Learnset_Hypno": 0x3b78c, - "Learnset_Golbat": 0x3b79a, - "Learnset_Mewtwo": 0x3b7a6, - "Learnset_Snorlax": 0x3b7b2, - "Learnset_Magikarp": 0x3b7bf, - "Learnset_Muk": 0x3b7c7, - "Learnset_Kingler": 0x3b7d7, - "Learnset_Cloyster": 0x3b7e3, - "Learnset_Electrode": 0x3b7e9, - "Learnset_Weezing": 0x3b7f7, - "Learnset_Persian": 0x3b803, - "Learnset_Marowak": 0x3b80f, - "Learnset_Haunter": 0x3b827, - "Learnset_Alakazam": 0x3b832, - "Learnset_Pidgeotto": 0x3b843, - "Learnset_Pidgeot": 0x3b851, - "Learnset_Bulbasaur": 0x3b864, - "Learnset_Venusaur": 0x3b874, - "Learnset_Tentacruel": 0x3b884, - "Learnset_Goldeen": 0x3b89b, - "Learnset_Seaking": 0x3b8a9, - "Learnset_Ponyta": 0x3b8c2, - "Learnset_Rapidash": 0x3b8d0, - "Learnset_Rattata": 0x3b8e1, - "Learnset_Raticate": 0x3b8eb, - "Learnset_Nidorino": 0x3b8f9, - "Learnset_Nidorina": 0x3b90b, - "Learnset_Geodude": 0x3b91c, - "Learnset_Porygon": 0x3b92a, - "Learnset_Aerodactyl": 0x3b934, - "Learnset_Magnemite": 0x3b942, - "Learnset_Charmander": 0x3b957, - "Learnset_Squirtle": 0x3b968, - "Learnset_Charmeleon": 0x3b979, - "Learnset_Wartortle": 0x3b98a, - "Learnset_Charizard": 0x3b998, - "Learnset_Oddish": 0x3b9b1, - "Learnset_Gloom": 0x3b9c3, - "Learnset_Vileplume": 0x3b9d1, - "Learnset_Bellsprout": 0x3b9dc, - "Learnset_Weepinbell": 0x3b9f0, - "Learnset_Victreebel": 0x3ba00, + "Learnset_Rhydon": 0x3b1e1, + "Learnset_Kangaskhan": 0x3b1ef, + "Learnset_NidoranM": 0x3b1fe, + "Learnset_Clefairy": 0x3b210, + "Learnset_Spearow": 0x3b221, + "Learnset_Voltorb": 0x3b230, + "Learnset_Nidoking": 0x3b23c, + "Learnset_Slowbro": 0x3b244, + "Learnset_Ivysaur": 0x3b257, + "Learnset_Exeggutor": 0x3b267, + "Learnset_Lickitung": 0x3b26b, + "Learnset_Exeggcute": 0x3b27b, + "Learnset_Grimer": 0x3b28c, + "Learnset_Gengar": 0x3b29a, + "Learnset_NidoranF": 0x3b2a3, + "Learnset_Nidoqueen": 0x3b2b1, + "Learnset_Cubone": 0x3b2bc, + "Learnset_Rhyhorn": 0x3b2cb, + "Learnset_Lapras": 0x3b2d9, + "Learnset_Mew": 0x3b2e9, + "Learnset_Gyarados": 0x3b2f3, + "Learnset_Shellder": 0x3b303, + "Learnset_Tentacool": 0x3b312, + "Learnset_Gastly": 0x3b327, + "Learnset_Scyther": 0x3b32d, + "Learnset_Staryu": 0x3b33f, + "Learnset_Blastoise": 0x3b34f, + "Learnset_Pinsir": 0x3b35d, + "Learnset_Tangela": 0x3b36b, + "Learnset_Growlithe": 0x3b381, + "Learnset_Onix": 0x3b38d, + "Learnset_Fearow": 0x3b399, + "Learnset_Pidgey": 0x3b3a8, + "Learnset_Slowpoke": 0x3b3b9, + "Learnset_Kadabra": 0x3b3d1, + "Learnset_Graveler": 0x3b3e9, + "Learnset_Chansey": 0x3b3f7, + "Learnset_Machoke": 0x3b40f, + "Learnset_MrMime": 0x3b41b, + "Learnset_Hitmonlee": 0x3b427, + "Learnset_Hitmonchan": 0x3b433, + "Learnset_Arbok": 0x3b43f, + "Learnset_Parasect": 0x3b44b, + "Learnset_Psyduck": 0x3b45a, + "Learnset_Drowzee": 0x3b469, + "Learnset_Golem": 0x3b477, + "Learnset_Magmar": 0x3b487, + "Learnset_Electabuzz": 0x3b497, + "Learnset_Magneton": 0x3b4a3, + "Learnset_Koffing": 0x3b4b4, + "Learnset_Mankey": 0x3b4c5, + "Learnset_Seel": 0x3b4d4, + "Learnset_Diglett": 0x3b4e3, + "Learnset_Tauros": 0x3b4ef, + "Learnset_Farfetchd": 0x3b501, + "Learnset_Venonat": 0x3b510, + "Learnset_Dragonite": 0x3b51e, + "Learnset_Doduo": 0x3b533, + "Learnset_Poliwag": 0x3b544, + "Learnset_Jynx": 0x3b552, + "Learnset_Moltres": 0x3b560, + "Learnset_Articuno": 0x3b568, + "Learnset_Zapdos": 0x3b570, + "Learnset_Meowth": 0x3b57d, + "Learnset_Krabby": 0x3b58c, + "Learnset_Vulpix": 0x3b5a2, + "Learnset_Pikachu": 0x3b5b4, + "Learnset_Dratini": 0x3b5c9, + "Learnset_Dragonair": 0x3b5d8, + "Learnset_Kabuto": 0x3b5e7, + "Learnset_Kabutops": 0x3b5f1, + "Learnset_Horsea": 0x3b5fe, + "Learnset_Seadra": 0x3b60a, + "Learnset_Sandshrew": 0x3b61d, + "Learnset_Sandslash": 0x3b629, + "Learnset_Omanyte": 0x3b638, + "Learnset_Omastar": 0x3b642, + "Learnset_Jigglypuff": 0x3b650, + "Learnset_Eevee": 0x3b66e, + "Learnset_Flareon": 0x3b678, + "Learnset_Jolteon": 0x3b68a, + "Learnset_Vaporeon": 0x3b69c, + "Learnset_Machop": 0x3b6b1, + "Learnset_Zubat": 0x3b6c0, + "Learnset_Ekans": 0x3b6cf, + "Learnset_Paras": 0x3b6de, + "Learnset_Poliwhirl": 0x3b6ee, + "Learnset_Poliwrath": 0x3b6fc, + "Learnset_Beedrill": 0x3b70c, + "Learnset_Dodrio": 0x3b71c, + "Learnset_Primeape": 0x3b72a, + "Learnset_Dugtrio": 0x3b736, + "Learnset_Venomoth": 0x3b742, + "Learnset_Dewgong": 0x3b750, + "Learnset_Butterfree": 0x3b76a, + "Learnset_Machamp": 0x3b77a, + "Learnset_Golduck": 0x3b788, + "Learnset_Hypno": 0x3b794, + "Learnset_Golbat": 0x3b7a2, + "Learnset_Mewtwo": 0x3b7ae, + "Learnset_Snorlax": 0x3b7ba, + "Learnset_Magikarp": 0x3b7c7, + "Learnset_Muk": 0x3b7cf, + "Learnset_Kingler": 0x3b7df, + "Learnset_Cloyster": 0x3b7eb, + "Learnset_Electrode": 0x3b7f1, + "Learnset_Weezing": 0x3b7ff, + "Learnset_Persian": 0x3b80b, + "Learnset_Marowak": 0x3b817, + "Learnset_Haunter": 0x3b82f, + "Learnset_Alakazam": 0x3b83a, + "Learnset_Pidgeotto": 0x3b84b, + "Learnset_Pidgeot": 0x3b859, + "Learnset_Bulbasaur": 0x3b86c, + "Learnset_Venusaur": 0x3b87c, + "Learnset_Tentacruel": 0x3b88c, + "Learnset_Goldeen": 0x3b8a3, + "Learnset_Seaking": 0x3b8b1, + "Learnset_Ponyta": 0x3b8ca, + "Learnset_Rapidash": 0x3b8d8, + "Learnset_Rattata": 0x3b8e9, + "Learnset_Raticate": 0x3b8f3, + "Learnset_Nidorino": 0x3b901, + "Learnset_Nidorina": 0x3b913, + "Learnset_Geodude": 0x3b924, + "Learnset_Porygon": 0x3b932, + "Learnset_Aerodactyl": 0x3b93c, + "Learnset_Magnemite": 0x3b94b, + "Learnset_Charmander": 0x3b960, + "Learnset_Squirtle": 0x3b971, + "Learnset_Charmeleon": 0x3b982, + "Learnset_Wartortle": 0x3b993, + "Learnset_Charizard": 0x3b9a1, + "Learnset_Oddish": 0x3b9ba, + "Learnset_Gloom": 0x3b9cc, + "Learnset_Vileplume": 0x3b9da, + "Learnset_Bellsprout": 0x3b9e5, + "Learnset_Weepinbell": 0x3b9f9, + "Learnset_Victreebel": 0x3ba09, "Option_Always_Half_STAB": 0x3e3fb, "Type_Chart": 0x3e4ee, "Ghost_Battle3": 0x3f1be, - "Trainersanity_EVENT_BEAT_MANSION_1_TRAINER_0_ITEM": 0x44341, - "Missable_Pokemon_Mansion_1F_Item_1": 0x443d8, - "Missable_Pokemon_Mansion_1F_Item_2": 0x443df, - "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_1_TRAINER_0_ITEM": 0x44514, - "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_1_TRAINER_1_ITEM": 0x44522, - "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_1_TRAINER_2_ITEM": 0x44530, - "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_1_TRAINER_3_ITEM": 0x4453e, - "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_1_TRAINER_4_ITEM": 0x4454c, - "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_1_TRAINER_5_ITEM": 0x4455a, - "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_1_TRAINER_6_ITEM": 0x44568, - "Map_Rock_TunnelF": 0x44686, - "Trainersanity_EVENT_BEAT_VICTORY_ROAD_3_TRAINER_0_ITEM": 0x44a55, - "Trainersanity_EVENT_BEAT_VICTORY_ROAD_3_TRAINER_1_ITEM": 0x44a63, - "Trainersanity_EVENT_BEAT_VICTORY_ROAD_3_TRAINER_2_ITEM": 0x44a71, - "Trainersanity_EVENT_BEAT_VICTORY_ROAD_3_TRAINER_3_ITEM": 0x44a7f, - "Missable_Victory_Road_3F_Item_1": 0x44b1f, - "Missable_Victory_Road_3F_Item_2": 0x44b26, - "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_1_TRAINER_0_ITEM": 0x44c47, - "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_1_TRAINER_1_ITEM": 0x44c55, - "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_1_TRAINER_2_ITEM": 0x44c63, - "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_1_TRAINER_3_ITEM": 0x44c71, - "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_1_TRAINER_4_ITEM": 0x44c7f, - "Missable_Rocket_Hideout_B1F_Item_1": 0x44d4f, - "Missable_Rocket_Hideout_B1F_Item_2": 0x44d56, - "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_2_TRAINER_0_ITEM": 0x45100, - "Missable_Rocket_Hideout_B2F_Item_1": 0x45141, - "Missable_Rocket_Hideout_B2F_Item_2": 0x45148, - "Missable_Rocket_Hideout_B2F_Item_3": 0x4514f, - "Missable_Rocket_Hideout_B2F_Item_4": 0x45156, - "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_3_TRAINER_0_ITEM": 0x45333, - "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_3_TRAINER_1_ITEM": 0x45341, - "Missable_Rocket_Hideout_B3F_Item_1": 0x45397, - "Missable_Rocket_Hideout_B3F_Item_2": 0x4539e, - "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_4_TRAINER_0_ITEM": 0x4554a, - "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_4_TRAINER_1_ITEM": 0x45558, - "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_4_TRAINER_2_ITEM": 0x45566, - "Missable_Rocket_Hideout_B4F_Item_1": 0x45655, - "Missable_Rocket_Hideout_B4F_Item_2": 0x4565c, - "Missable_Rocket_Hideout_B4F_Item_3": 0x45663, - "Missable_Rocket_Hideout_B4F_Item_4": 0x4566a, - "Missable_Rocket_Hideout_B4F_Item_5": 0x45671, - "Missable_Safari_Zone_East_Item_1": 0x458e0, - "Missable_Safari_Zone_East_Item_2": 0x458e7, - "Missable_Safari_Zone_East_Item_3": 0x458ee, - "Missable_Safari_Zone_East_Item_4": 0x458f5, - "Missable_Safari_Zone_North_Item_1": 0x45a40, - "Missable_Safari_Zone_North_Item_2": 0x45a47, - "Missable_Safari_Zone_Center_Item": 0x45c27, - "Missable_Cerulean_Cave_2F_Item_1": 0x45e64, - "Missable_Cerulean_Cave_2F_Item_2": 0x45e6b, - "Missable_Cerulean_Cave_2F_Item_3": 0x45e72, - "Trainersanity_EVENT_BEAT_MEWTWO_ITEM": 0x45f4a, - "Static_Encounter_Mewtwo": 0x45f74, - "Missable_Cerulean_Cave_B1F_Item_1": 0x45f7c, - "Missable_Cerulean_Cave_B1F_Item_2": 0x45f83, - "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_0_ITEM": 0x46059, - "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_1_ITEM": 0x46067, - "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_2_ITEM": 0x46075, - "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_3_ITEM": 0x46083, - "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_4_ITEM": 0x46091, - "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_5_ITEM": 0x4609f, - "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_6_ITEM": 0x460ad, - "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_7_ITEM": 0x460bb, - "Missable_Rock_Tunnel_B1F_Item_1": 0x461df, - "Missable_Rock_Tunnel_B1F_Item_2": 0x461e6, - "Missable_Rock_Tunnel_B1F_Item_3": 0x461ed, - "Missable_Rock_Tunnel_B1F_Item_4": 0x461f4, - "Trainersanity_EVENT_BEAT_ARTICUNO_ITEM": 0x468f7, - "Static_Encounter_Articuno": 0x4694e, - "Hidden_Item_Viridian_Forest_1": 0x46eaf, - "Hidden_Item_Viridian_Forest_2": 0x46eb5, - "Hidden_Item_MtMoonB2F_1": 0x46ebc, - "Hidden_Item_MtMoonB2F_2": 0x46ec2, - "Hidden_Item_Route_25_1": 0x46ed6, - "Hidden_Item_Route_25_2": 0x46edc, - "Hidden_Item_Route_9": 0x46ee3, - "Hidden_Item_SS_Anne_Kitchen": 0x46ef6, - "Hidden_Item_SS_Anne_B1F": 0x46efd, - "Hidden_Item_Route_10_1": 0x46f04, - "Hidden_Item_Route_10_2": 0x46f0a, - "Hidden_Item_Rocket_Hideout_B1F": 0x46f11, - "Hidden_Item_Rocket_Hideout_B3F": 0x46f18, - "Hidden_Item_Rocket_Hideout_B4F": 0x46f1f, - "Hidden_Item_Pokemon_Tower_5F": 0x46f33, - "Hidden_Item_Route_13_1": 0x46f3a, - "Hidden_Item_Route_13_2": 0x46f40, - "Hidden_Item_Safari_Zone_West": 0x46f4e, - "Hidden_Item_Silph_Co_5F": 0x46f55, - "Hidden_Item_Silph_Co_9F": 0x46f5c, - "Hidden_Item_Copycats_House": 0x46f63, - "Hidden_Item_Cerulean_Cave_1F": 0x46f6a, - "Hidden_Item_Cerulean_Cave_B1F": 0x46f71, - "Hidden_Item_Power_Plant_1": 0x46f78, - "Hidden_Item_Power_Plant_2": 0x46f7e, - "Hidden_Item_Seafoam_Islands_B2F": 0x46f85, - "Hidden_Item_Seafoam_Islands_B4F": 0x46f8c, - "Hidden_Item_Pokemon_Mansion_1F": 0x46f93, - "Hidden_Item_Pokemon_Mansion_3F": 0x46fa7, - "Hidden_Item_Pokemon_Mansion_B1F": 0x46fb4, - "Hidden_Item_Route_23_1": 0x46fc7, - "Hidden_Item_Route_23_2": 0x46fcd, - "Hidden_Item_Route_23_3": 0x46fd3, - "Hidden_Item_Victory_Road_2F_1": 0x46fda, - "Hidden_Item_Victory_Road_2F_2": 0x46fe0, - "Hidden_Item_Unused_6F": 0x46fe7, - "Hidden_Item_Viridian_City": 0x46ff5, - "Hidden_Item_Route_11": 0x470a2, - "Hidden_Item_Route_12": 0x470a9, - "Hidden_Item_Route_17_1": 0x470b7, - "Hidden_Item_Route_17_2": 0x470bd, - "Hidden_Item_Route_17_3": 0x470c3, - "Hidden_Item_Route_17_4": 0x470c9, - "Hidden_Item_Route_17_5": 0x470cf, - "Hidden_Item_Underground_Path_NS_1": 0x470d6, - "Hidden_Item_Underground_Path_NS_2": 0x470dc, - "Hidden_Item_Underground_Path_WE_1": 0x470e3, - "Hidden_Item_Underground_Path_WE_2": 0x470e9, - "Hidden_Item_Celadon_City": 0x470f0, - "Hidden_Item_Seafoam_Islands_B3F": 0x470f7, - "Hidden_Item_Vermilion_City": 0x470fe, - "Hidden_Item_Cerulean_City": 0x47105, - "Hidden_Item_Route_4": 0x4710c, + "Dexsanity_Items": 0x44254, + "Option_Dexsanity_A": 0x44301, + "Require_Pokedex_B": 0x44305, + "Option_Dexsanity_B": 0x44362, + "Trainersanity_EVENT_BEAT_MANSION_1_TRAINER_0_ITEM": 0x44540, + "Missable_Pokemon_Mansion_1F_Item_1": 0x445d7, + "Missable_Pokemon_Mansion_1F_Item_2": 0x445de, + "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_1_TRAINER_0_ITEM": 0x44713, + "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_1_TRAINER_1_ITEM": 0x44721, + "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_1_TRAINER_2_ITEM": 0x4472f, + "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_1_TRAINER_3_ITEM": 0x4473d, + "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_1_TRAINER_4_ITEM": 0x4474b, + "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_1_TRAINER_5_ITEM": 0x44759, + "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_1_TRAINER_6_ITEM": 0x44767, + "Map_Rock_Tunnel1F": 0x44884, + "Trainersanity_EVENT_BEAT_VICTORY_ROAD_3_TRAINER_0_ITEM": 0x44c54, + "Trainersanity_EVENT_BEAT_VICTORY_ROAD_3_TRAINER_1_ITEM": 0x44c62, + "Trainersanity_EVENT_BEAT_VICTORY_ROAD_3_TRAINER_2_ITEM": 0x44c70, + "Trainersanity_EVENT_BEAT_VICTORY_ROAD_3_TRAINER_3_ITEM": 0x44c7e, + "Missable_Victory_Road_3F_Item_1": 0x44d1e, + "Missable_Victory_Road_3F_Item_2": 0x44d25, + "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_1_TRAINER_0_ITEM": 0x44e46, + "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_1_TRAINER_1_ITEM": 0x44e54, + "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_1_TRAINER_2_ITEM": 0x44e62, + "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_1_TRAINER_3_ITEM": 0x44e70, + "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_1_TRAINER_4_ITEM": 0x44e7e, + "Missable_Rocket_Hideout_B1F_Item_1": 0x44f4e, + "Missable_Rocket_Hideout_B1F_Item_2": 0x44f55, + "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_2_TRAINER_0_ITEM": 0x452ff, + "Missable_Rocket_Hideout_B2F_Item_1": 0x45340, + "Missable_Rocket_Hideout_B2F_Item_2": 0x45347, + "Missable_Rocket_Hideout_B2F_Item_3": 0x4534e, + "Missable_Rocket_Hideout_B2F_Item_4": 0x45355, + "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_3_TRAINER_0_ITEM": 0x45532, + "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_3_TRAINER_1_ITEM": 0x45540, + "Missable_Rocket_Hideout_B3F_Item_1": 0x45596, + "Missable_Rocket_Hideout_B3F_Item_2": 0x4559d, + "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_4_TRAINER_0_ITEM": 0x45749, + "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_4_TRAINER_1_ITEM": 0x45757, + "Trainersanity_EVENT_BEAT_ROCKET_HIDEOUT_4_TRAINER_2_ITEM": 0x45765, + "Missable_Rocket_Hideout_B4F_Item_1": 0x45854, + "Missable_Rocket_Hideout_B4F_Item_2": 0x4585b, + "Missable_Rocket_Hideout_B4F_Item_3": 0x45862, + "Missable_Rocket_Hideout_B4F_Item_4": 0x45869, + "Missable_Rocket_Hideout_B4F_Item_5": 0x45870, + "Missable_Safari_Zone_East_Item_1": 0x45adf, + "Missable_Safari_Zone_East_Item_2": 0x45ae6, + "Missable_Safari_Zone_East_Item_3": 0x45aed, + "Missable_Safari_Zone_East_Item_4": 0x45af4, + "Missable_Safari_Zone_North_Item_1": 0x45c3f, + "Missable_Safari_Zone_North_Item_2": 0x45c46, + "Missable_Safari_Zone_Center_Item": 0x45e26, + "Missable_Cerulean_Cave_2F_Item_1": 0x46063, + "Missable_Cerulean_Cave_2F_Item_2": 0x4606a, + "Missable_Cerulean_Cave_2F_Item_3": 0x46071, + "Trainersanity_EVENT_BEAT_MEWTWO_ITEM": 0x46149, + "Static_Encounter_Mewtwo": 0x46173, + "Missable_Cerulean_Cave_B1F_Item_1": 0x4617b, + "Missable_Cerulean_Cave_B1F_Item_2": 0x46182, + "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_0_ITEM": 0x46258, + "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_1_ITEM": 0x46266, + "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_2_ITEM": 0x46274, + "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_3_ITEM": 0x46282, + "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_4_ITEM": 0x46290, + "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_5_ITEM": 0x4629e, + "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_6_ITEM": 0x462ac, + "Trainersanity_EVENT_BEAT_ROCK_TUNNEL_2_TRAINER_7_ITEM": 0x462ba, + "Missable_Rock_Tunnel_B1F_Item_1": 0x463de, + "Missable_Rock_Tunnel_B1F_Item_2": 0x463e5, + "Missable_Rock_Tunnel_B1F_Item_3": 0x463ec, + "Missable_Rock_Tunnel_B1F_Item_4": 0x463f3, + "Map_Rock_TunnelB1F": 0x46404, + "Trainersanity_EVENT_BEAT_ARTICUNO_ITEM": 0x46af6, + "Static_Encounter_Articuno": 0x46b4d, + "Hidden_Item_Game_Corner_1": 0x46fe5, + "Hidden_Item_Game_Corner_2": 0x46feb, + "Hidden_Item_Game_Corner_3": 0x46ff1, + "Hidden_Item_Game_Corner_4": 0x46ff7, + "Hidden_Item_Game_Corner_5": 0x46ffd, + "Hidden_Item_Game_Corner_6": 0x47003, + "Hidden_Item_Game_Corner_7": 0x47009, + "Hidden_Item_Game_Corner_8": 0x4700f, + "Hidden_Item_Game_Corner_9": 0x47015, + "Hidden_Item_Game_Corner_10": 0x4701b, + "Hidden_Item_Game_Corner_11": 0x47021, + "Quiz_Answer_A": 0x47055, + "Quiz_Answer_B": 0x4705b, + "Quiz_Answer_C": 0x47061, + "Quiz_Answer_D": 0x47067, + "Quiz_Answer_E": 0x4706d, + "Quiz_Answer_F": 0x47073, + "Hidden_Item_Viridian_Forest_1": 0x470a8, + "Hidden_Item_Viridian_Forest_2": 0x470ae, + "Hidden_Item_MtMoonB2F_1": 0x470b5, + "Hidden_Item_MtMoonB2F_2": 0x470bb, + "Hidden_Item_Route_25_1": 0x470cf, + "Hidden_Item_Route_25_2": 0x470d5, + "Hidden_Item_Route_9": 0x470dc, + "Hidden_Item_SS_Anne_Kitchen": 0x470ef, + "Hidden_Item_SS_Anne_B1F": 0x470f6, + "Hidden_Item_Route_10_1": 0x470fd, + "Hidden_Item_Route_10_2": 0x47103, + "Hidden_Item_Rocket_Hideout_B1F": 0x4710a, + "Hidden_Item_Rocket_Hideout_B3F": 0x47111, + "Hidden_Item_Rocket_Hideout_B4F": 0x47118, + "Hidden_Item_Pokemon_Tower_5F": 0x4712c, + "Hidden_Item_Route_13_1": 0x47133, + "Hidden_Item_Route_13_2": 0x47139, + "Hidden_Item_Safari_Zone_West": 0x47147, + "Hidden_Item_Silph_Co_5F": 0x4714e, + "Hidden_Item_Silph_Co_9F": 0x47155, + "Hidden_Item_Copycats_House": 0x4715c, + "Hidden_Item_Cerulean_Cave_1F": 0x47163, + "Hidden_Item_Cerulean_Cave_B1F": 0x4716a, + "Hidden_Item_Power_Plant_1": 0x47171, + "Hidden_Item_Power_Plant_2": 0x47177, + "Hidden_Item_Seafoam_Islands_B2F": 0x4717e, + "Hidden_Item_Seafoam_Islands_B4F": 0x47185, + "Hidden_Item_Pokemon_Mansion_1F": 0x4718c, + "Hidden_Item_Pokemon_Mansion_3F": 0x471a0, + "Hidden_Item_Pokemon_Mansion_B1F": 0x471ad, + "Hidden_Item_Route_23_1": 0x471c0, + "Hidden_Item_Route_23_2": 0x471c6, + "Hidden_Item_Route_23_3": 0x471cc, + "Hidden_Item_Victory_Road_2F_1": 0x471d3, + "Hidden_Item_Victory_Road_2F_2": 0x471d9, + "Hidden_Item_Unused_6F": 0x471e0, + "Hidden_Item_Viridian_City": 0x471ee, + "Hidden_Item_Route_11": 0x4729b, + "Hidden_Item_Route_12": 0x472a2, + "Hidden_Item_Route_17_1": 0x472b0, + "Hidden_Item_Route_17_2": 0x472b6, + "Hidden_Item_Route_17_3": 0x472bc, + "Hidden_Item_Route_17_4": 0x472c2, + "Hidden_Item_Route_17_5": 0x472c8, + "Hidden_Item_Underground_Path_NS_1": 0x472cf, + "Hidden_Item_Underground_Path_NS_2": 0x472d5, + "Hidden_Item_Underground_Path_WE_1": 0x472dc, + "Hidden_Item_Underground_Path_WE_2": 0x472e2, + "Hidden_Item_Celadon_City": 0x472e9, + "Hidden_Item_Seafoam_Islands_B3F": 0x472f0, + "Hidden_Item_Vermilion_City": 0x472f7, + "Hidden_Item_Cerulean_City": 0x472fe, + "Hidden_Item_Route_4": 0x47305, "Event_Counter": 0x482d3, - "Event_Thirsty_Girl_Lemonade": 0x484f9, - "Event_Thirsty_Girl_Soda": 0x4851d, - "Event_Thirsty_Girl_Water": 0x48541, - "Option_Tea": 0x4871d, - "Event_Mansion_Lady": 0x4872a, - "Badge_Celadon_Gym": 0x48a1b, - "Event_Celadon_Gym": 0x48a2f, - "Trainersanity_EVENT_BEAT_CELADON_GYM_TRAINER_0_ITEM": 0x48a75, - "Trainersanity_EVENT_BEAT_CELADON_GYM_TRAINER_1_ITEM": 0x48a83, - "Trainersanity_EVENT_BEAT_CELADON_GYM_TRAINER_2_ITEM": 0x48a91, - "Trainersanity_EVENT_BEAT_CELADON_GYM_TRAINER_3_ITEM": 0x48a9f, - "Trainersanity_EVENT_BEAT_CELADON_GYM_TRAINER_4_ITEM": 0x48aad, - "Trainersanity_EVENT_BEAT_CELADON_GYM_TRAINER_5_ITEM": 0x48abb, - "Trainersanity_EVENT_BEAT_CELADON_GYM_TRAINER_6_ITEM": 0x48ac9, - "Event_Gambling_Addict": 0x492a1, - "Gift_Magikarp": 0x4943e, - "Option_Aide_Rt11": 0x4959b, - "Event_Rt11_Oaks_Aide": 0x4959f, - "Event_Mourning_Girl": 0x49699, - "Option_Aide_Rt15": 0x49784, - "Event_Rt_15_Oaks_Aide": 0x49788, - "Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_0_ITEM": 0x49b2e, - "Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_1_ITEM": 0x49b3c, - "Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_2_ITEM": 0x49b4a, - "Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_3_ITEM": 0x49b58, - "Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_4_ITEM": 0x49b66, - "Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_5_ITEM": 0x49b74, - "Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_6_ITEM": 0x49b82, - "Missable_Mt_Moon_1F_Item_1": 0x49c91, - "Missable_Mt_Moon_1F_Item_2": 0x49c98, - "Missable_Mt_Moon_1F_Item_3": 0x49c9f, - "Missable_Mt_Moon_1F_Item_4": 0x49ca6, - "Missable_Mt_Moon_1F_Item_5": 0x49cad, - "Missable_Mt_Moon_1F_Item_6": 0x49cb4, - "Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_0_ITEM": 0x49f87, - "Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_1_ITEM": 0x49f95, - "Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_2_ITEM": 0x49fa3, - "Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_3_ITEM": 0x49fb1, - "Dome_Fossil_Text": 0x4a025, - "Event_Dome_Fossil": 0x4a045, - "Helix_Fossil_Text": 0x4a081, - "Event_Helix_Fossil": 0x4a0a1, - "Missable_Mt_Moon_B2F_Item_1": 0x4a18a, - "Missable_Mt_Moon_B2F_Item_2": 0x4a191, - "Missable_Safari_Zone_West_Item_1": 0x4a373, - "Missable_Safari_Zone_West_Item_2": 0x4a37a, - "Missable_Safari_Zone_West_Item_3": 0x4a381, - "Missable_Safari_Zone_West_Item_4": 0x4a388, - "Event_Safari_Zone_Secret_House": 0x4a48d, + "Event_Thirsty_Girl_Lemonade": 0x48501, + "Event_Thirsty_Girl_Soda": 0x48525, + "Event_Thirsty_Girl_Water": 0x48549, + "Option_Tea": 0x48725, + "Event_Mansion_Lady": 0x48732, + "Badge_Celadon_Gym": 0x48a23, + "Event_Celadon_Gym": 0x48a37, + "Trainersanity_EVENT_BEAT_CELADON_GYM_TRAINER_0_ITEM": 0x48a7d, + "Trainersanity_EVENT_BEAT_CELADON_GYM_TRAINER_1_ITEM": 0x48a8b, + "Trainersanity_EVENT_BEAT_CELADON_GYM_TRAINER_2_ITEM": 0x48a99, + "Trainersanity_EVENT_BEAT_CELADON_GYM_TRAINER_3_ITEM": 0x48aa7, + "Trainersanity_EVENT_BEAT_CELADON_GYM_TRAINER_4_ITEM": 0x48ab5, + "Trainersanity_EVENT_BEAT_CELADON_GYM_TRAINER_5_ITEM": 0x48ac3, + "Trainersanity_EVENT_BEAT_CELADON_GYM_TRAINER_6_ITEM": 0x48ad1, + "Event_Game_Corner_Gift_A": 0x48e98, + "Event_Game_Corner_Gift_C": 0x48f14, + "Event_Game_Corner_Gift_B": 0x48f63, + "Event_Gambling_Addict": 0x49306, + "Gift_Magikarp": 0x494a3, + "Option_Aide_Rt11": 0x49600, + "Event_Rt11_Oaks_Aide": 0x49604, + "Event_Mourning_Girl": 0x496fe, + "Option_Aide_Rt15": 0x497e9, + "Event_Rt_15_Oaks_Aide": 0x497ed, + "Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_0_ITEM": 0x49b93, + "Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_1_ITEM": 0x49ba1, + "Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_2_ITEM": 0x49baf, + "Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_3_ITEM": 0x49bbd, + "Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_4_ITEM": 0x49bcb, + "Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_5_ITEM": 0x49bd9, + "Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_6_ITEM": 0x49be7, + "Missable_Mt_Moon_1F_Item_1": 0x49cf6, + "Missable_Mt_Moon_1F_Item_2": 0x49cfd, + "Missable_Mt_Moon_1F_Item_3": 0x49d04, + "Missable_Mt_Moon_1F_Item_4": 0x49d0b, + "Missable_Mt_Moon_1F_Item_5": 0x49d12, + "Missable_Mt_Moon_1F_Item_6": 0x49d19, + "Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_0_ITEM": 0x49fec, + "Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_1_ITEM": 0x49ffa, + "Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_2_ITEM": 0x4a008, + "Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_3_ITEM": 0x4a016, + "Dome_Fossil_Text": 0x4a08a, + "Event_Dome_Fossil": 0x4a0aa, + "Helix_Fossil_Text": 0x4a0e6, + "Event_Helix_Fossil": 0x4a106, + "Missable_Mt_Moon_B2F_Item_1": 0x4a1ef, + "Missable_Mt_Moon_B2F_Item_2": 0x4a1f6, + "Missable_Safari_Zone_West_Item_1": 0x4a3d8, + "Missable_Safari_Zone_West_Item_2": 0x4a3df, + "Missable_Safari_Zone_West_Item_3": 0x4a3e6, + "Missable_Safari_Zone_West_Item_4": 0x4a3ed, + "Event_Safari_Zone_Secret_House": 0x4a4f2, "Missable_Route_24_Item": 0x506e6, "Missable_Route_25_Item": 0x5080b, "Trainersanity_EVENT_BEAT_ROUTE_20_TRAINER_0_ITEM": 0x50d47, @@ -606,13 +634,16 @@ rom_addresses = { "Prize_Mon_D2": 0x5288a, "Prize_Mon_E2": 0x5288b, "Prize_Mon_F2": 0x5288c, - "Prize_Mon_A": 0x529b0, - "Prize_Mon_B": 0x529b2, - "Prize_Mon_C": 0x529b4, - "Prize_Mon_D": 0x529b6, - "Prize_Mon_E": 0x529b8, - "Prize_Mon_F": 0x529ba, - "Start_Inventory": 0x52add, + "Prize_Item_A": 0x52895, + "Prize_Item_B": 0x52896, + "Prize_Item_C": 0x52897, + "Prize_Mon_A": 0x529cc, + "Prize_Mon_B": 0x529ce, + "Prize_Mon_C": 0x529d0, + "Prize_Mon_D": 0x529d2, + "Prize_Mon_E": 0x529d4, + "Prize_Mon_F": 0x529d6, + "Start_Inventory": 0x52af9, "Missable_Route_2_Item_1": 0x5404a, "Missable_Route_2_Item_2": 0x54051, "Missable_Route_4_Item": 0x543df, @@ -696,81 +727,82 @@ rom_addresses = { "Missable_Route_12_Item_2": 0x5870b, "Missable_Route_15_Item": 0x589c7, "Ghost_Battle6": 0x58df0, - "Trainersanity_EVENT_BEAT_ROUTE_6_TRAINER_0_ITEM": 0x59106, - "Trainersanity_EVENT_BEAT_ROUTE_6_TRAINER_1_ITEM": 0x59114, - "Trainersanity_EVENT_BEAT_ROUTE_6_TRAINER_2_ITEM": 0x59122, - "Trainersanity_EVENT_BEAT_ROUTE_6_TRAINER_3_ITEM": 0x59130, - "Trainersanity_EVENT_BEAT_ROUTE_6_TRAINER_4_ITEM": 0x5913e, - "Trainersanity_EVENT_BEAT_ROUTE_6_TRAINER_5_ITEM": 0x5914c, - "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_0_ITEM": 0x5921e, - "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_1_ITEM": 0x5922c, - "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_2_ITEM": 0x5923a, - "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_3_ITEM": 0x59248, - "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_4_ITEM": 0x59256, - "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_5_ITEM": 0x59264, - "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_6_ITEM": 0x59272, - "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_7_ITEM": 0x59280, - "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_8_ITEM": 0x5928e, - "Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_0_ITEM": 0x59406, - "Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_1_ITEM": 0x59414, - "Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_2_ITEM": 0x59422, - "Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_3_ITEM": 0x59430, - "Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_4_ITEM": 0x5943e, - "Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_5_ITEM": 0x5944c, - "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_0_ITEM": 0x59533, - "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_1_ITEM": 0x59541, - "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_2_ITEM": 0x5954f, - "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_3_ITEM": 0x5955d, - "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_4_ITEM": 0x5956b, - "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_5_ITEM": 0x59579, - "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_6_ITEM": 0x59587, - "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_7_ITEM": 0x59595, - "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_8_ITEM": 0x595a3, - "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_9_ITEM": 0x595b1, - "Static_Encounter_Snorlax_A": 0x596ef, - "Trainersanity_EVENT_BEAT_ROUTE_12_TRAINER_0_ITEM": 0x5975d, - "Trainersanity_EVENT_BEAT_ROUTE_12_TRAINER_1_ITEM": 0x5976b, - "Trainersanity_EVENT_BEAT_ROUTE_12_TRAINER_2_ITEM": 0x59779, - "Trainersanity_EVENT_BEAT_ROUTE_12_TRAINER_3_ITEM": 0x59787, - "Trainersanity_EVENT_BEAT_ROUTE_12_TRAINER_4_ITEM": 0x59795, - "Trainersanity_EVENT_BEAT_ROUTE_12_TRAINER_5_ITEM": 0x597a3, - "Trainersanity_EVENT_BEAT_ROUTE_12_TRAINER_6_ITEM": 0x597b1, - "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_0_ITEM": 0x598b9, - "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_1_ITEM": 0x598c7, - "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_2_ITEM": 0x598d5, - "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_3_ITEM": 0x598e3, - "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_4_ITEM": 0x598f1, - "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_5_ITEM": 0x598ff, - "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_6_ITEM": 0x5990d, - "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_7_ITEM": 0x5991b, - "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_8_ITEM": 0x59929, - "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_9_ITEM": 0x59937, - "Static_Encounter_Snorlax_B": 0x59a51, - "Trainersanity_EVENT_BEAT_ROUTE_16_TRAINER_0_ITEM": 0x59abd, - "Trainersanity_EVENT_BEAT_ROUTE_16_TRAINER_1_ITEM": 0x59acb, - "Trainersanity_EVENT_BEAT_ROUTE_16_TRAINER_2_ITEM": 0x59ad9, - "Trainersanity_EVENT_BEAT_ROUTE_16_TRAINER_3_ITEM": 0x59ae7, - "Trainersanity_EVENT_BEAT_ROUTE_16_TRAINER_4_ITEM": 0x59af5, - "Trainersanity_EVENT_BEAT_ROUTE_16_TRAINER_5_ITEM": 0x59b03, - "Trainersanity_EVENT_BEAT_ROUTE_18_TRAINER_0_ITEM": 0x59be4, - "Trainersanity_EVENT_BEAT_ROUTE_18_TRAINER_1_ITEM": 0x59bf2, - "Trainersanity_EVENT_BEAT_ROUTE_18_TRAINER_2_ITEM": 0x59c00, - "Event_Pokemon_Fan_Club": 0x59d13, - "Trainersanity_EVENT_BEAT_SILPH_CO_2F_TRAINER_0_ITEM": 0x59e73, - "Trainersanity_EVENT_BEAT_SILPH_CO_2F_TRAINER_1_ITEM": 0x59e81, - "Trainersanity_EVENT_BEAT_SILPH_CO_2F_TRAINER_2_ITEM": 0x59e8f, - "Trainersanity_EVENT_BEAT_SILPH_CO_2F_TRAINER_3_ITEM": 0x59e9d, - "Event_Scared_Woman": 0x59eaf, - "Trainersanity_EVENT_BEAT_SILPH_CO_3F_TRAINER_0_ITEM": 0x5a0b7, - "Trainersanity_EVENT_BEAT_SILPH_CO_3F_TRAINER_1_ITEM": 0x5a0c5, - "Missable_Silph_Co_3F_Item": 0x5a15f, - "Trainersanity_EVENT_BEAT_SILPH_CO_10F_TRAINER_0_ITEM": 0x5a281, - "Trainersanity_EVENT_BEAT_SILPH_CO_10F_TRAINER_1_ITEM": 0x5a28f, - "Missable_Silph_Co_10F_Item_1": 0x5a319, - "Missable_Silph_Co_10F_Item_2": 0x5a320, - "Missable_Silph_Co_10F_Item_3": 0x5a327, - "Trainersanity_EVENT_BEAT_LANCES_ROOM_TRAINER_0_ITEM": 0x5a48a, - "Guard_Drink_List": 0x5a69f, + "Require_Pokedex_A": 0x59051, + "Trainersanity_EVENT_BEAT_ROUTE_6_TRAINER_0_ITEM": 0x5910b, + "Trainersanity_EVENT_BEAT_ROUTE_6_TRAINER_1_ITEM": 0x59119, + "Trainersanity_EVENT_BEAT_ROUTE_6_TRAINER_2_ITEM": 0x59127, + "Trainersanity_EVENT_BEAT_ROUTE_6_TRAINER_3_ITEM": 0x59135, + "Trainersanity_EVENT_BEAT_ROUTE_6_TRAINER_4_ITEM": 0x59143, + "Trainersanity_EVENT_BEAT_ROUTE_6_TRAINER_5_ITEM": 0x59151, + "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_0_ITEM": 0x59223, + "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_1_ITEM": 0x59231, + "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_2_ITEM": 0x5923f, + "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_3_ITEM": 0x5924d, + "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_4_ITEM": 0x5925b, + "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_5_ITEM": 0x59269, + "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_6_ITEM": 0x59277, + "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_7_ITEM": 0x59285, + "Trainersanity_EVENT_BEAT_ROUTE_8_TRAINER_8_ITEM": 0x59293, + "Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_0_ITEM": 0x5940d, + "Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_1_ITEM": 0x5941b, + "Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_2_ITEM": 0x59429, + "Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_3_ITEM": 0x59437, + "Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_4_ITEM": 0x59445, + "Trainersanity_EVENT_BEAT_ROUTE_10_TRAINER_5_ITEM": 0x59453, + "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_0_ITEM": 0x5953a, + "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_1_ITEM": 0x59548, + "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_2_ITEM": 0x59556, + "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_3_ITEM": 0x59564, + "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_4_ITEM": 0x59572, + "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_5_ITEM": 0x59580, + "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_6_ITEM": 0x5958e, + "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_7_ITEM": 0x5959c, + "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_8_ITEM": 0x595aa, + "Trainersanity_EVENT_BEAT_ROUTE_11_TRAINER_9_ITEM": 0x595b8, + "Static_Encounter_Snorlax_A": 0x596f6, + "Trainersanity_EVENT_BEAT_ROUTE_12_TRAINER_0_ITEM": 0x59764, + "Trainersanity_EVENT_BEAT_ROUTE_12_TRAINER_1_ITEM": 0x59772, + "Trainersanity_EVENT_BEAT_ROUTE_12_TRAINER_2_ITEM": 0x59780, + "Trainersanity_EVENT_BEAT_ROUTE_12_TRAINER_3_ITEM": 0x5978e, + "Trainersanity_EVENT_BEAT_ROUTE_12_TRAINER_4_ITEM": 0x5979c, + "Trainersanity_EVENT_BEAT_ROUTE_12_TRAINER_5_ITEM": 0x597aa, + "Trainersanity_EVENT_BEAT_ROUTE_12_TRAINER_6_ITEM": 0x597b8, + "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_0_ITEM": 0x598c0, + "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_1_ITEM": 0x598ce, + "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_2_ITEM": 0x598dc, + "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_3_ITEM": 0x598ea, + "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_4_ITEM": 0x598f8, + "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_5_ITEM": 0x59906, + "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_6_ITEM": 0x59914, + "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_7_ITEM": 0x59922, + "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_8_ITEM": 0x59930, + "Trainersanity_EVENT_BEAT_ROUTE_15_TRAINER_9_ITEM": 0x5993e, + "Static_Encounter_Snorlax_B": 0x59a58, + "Trainersanity_EVENT_BEAT_ROUTE_16_TRAINER_0_ITEM": 0x59ac4, + "Trainersanity_EVENT_BEAT_ROUTE_16_TRAINER_1_ITEM": 0x59ad2, + "Trainersanity_EVENT_BEAT_ROUTE_16_TRAINER_2_ITEM": 0x59ae0, + "Trainersanity_EVENT_BEAT_ROUTE_16_TRAINER_3_ITEM": 0x59aee, + "Trainersanity_EVENT_BEAT_ROUTE_16_TRAINER_4_ITEM": 0x59afc, + "Trainersanity_EVENT_BEAT_ROUTE_16_TRAINER_5_ITEM": 0x59b0a, + "Trainersanity_EVENT_BEAT_ROUTE_18_TRAINER_0_ITEM": 0x59beb, + "Trainersanity_EVENT_BEAT_ROUTE_18_TRAINER_1_ITEM": 0x59bf9, + "Trainersanity_EVENT_BEAT_ROUTE_18_TRAINER_2_ITEM": 0x59c07, + "Event_Pokemon_Fan_Club": 0x59d1a, + "Trainersanity_EVENT_BEAT_SILPH_CO_2F_TRAINER_0_ITEM": 0x59e7a, + "Trainersanity_EVENT_BEAT_SILPH_CO_2F_TRAINER_1_ITEM": 0x59e88, + "Trainersanity_EVENT_BEAT_SILPH_CO_2F_TRAINER_2_ITEM": 0x59e96, + "Trainersanity_EVENT_BEAT_SILPH_CO_2F_TRAINER_3_ITEM": 0x59ea4, + "Event_Scared_Woman": 0x59eb6, + "Trainersanity_EVENT_BEAT_SILPH_CO_3F_TRAINER_0_ITEM": 0x5a0be, + "Trainersanity_EVENT_BEAT_SILPH_CO_3F_TRAINER_1_ITEM": 0x5a0cc, + "Missable_Silph_Co_3F_Item": 0x5a166, + "Trainersanity_EVENT_BEAT_SILPH_CO_10F_TRAINER_0_ITEM": 0x5a288, + "Trainersanity_EVENT_BEAT_SILPH_CO_10F_TRAINER_1_ITEM": 0x5a296, + "Missable_Silph_Co_10F_Item_1": 0x5a320, + "Missable_Silph_Co_10F_Item_2": 0x5a327, + "Missable_Silph_Co_10F_Item_3": 0x5a32e, + "Trainersanity_EVENT_BEAT_LANCES_ROOM_TRAINER_0_ITEM": 0x5a491, + "Guard_Drink_List": 0x5a6a6, "Event_Museum": 0x5c266, "Badge_Pewter_Gym": 0x5c3ed, "Event_Pewter_Gym": 0x5c401, @@ -879,6 +911,16 @@ rom_addresses = { "Trainersanity_EVENT_BEAT_SILPH_CO_11F_TRAINER_1_ITEM": 0x6234a, "Event_Silph_Co_President": 0x6235d, "Ghost_Battle4": 0x708e1, + "Trade_Terry": 0x71b7b, + "Trade_Marcel": 0x71b89, + "Trade_Sailor": 0x71ba5, + "Trade_Dux": 0x71bb3, + "Trade_Marc": 0x71bc1, + "Trade_Lola": 0x71bcf, + "Trade_Doris": 0x71bdd, + "Trade_Crinkles": 0x71beb, + "Trade_Spot": 0x71bf9, + "Mon_Palettes": 0x725dd, "Badge_Viridian_Gym": 0x749f7, "Event_Viridian_Gym": 0x74a0b, "Trainersanity_EVENT_BEAT_VIRIDIAN_GYM_TRAINER_0_ITEM": 0x74a66, @@ -906,18 +948,39 @@ rom_addresses = { "Trainersanity_EVENT_BEAT_FUCHSIA_GYM_TRAINER_5_ITEM": 0x756d2, "Badge_Cinnabar_Gym": 0x75a06, "Event_Cinnabar_Gym": 0x75a1a, - "Event_Lab_Scientist": 0x75e43, - "Fossils_Needed_For_Second_Item": 0x75f10, - "Event_Dome_Fossil_B": 0x75f8d, - "Event_Helix_Fossil_B": 0x75fad, - "Shop8": 0x760cb, - "Starter2_N": 0x761fe, - "Starter3_N": 0x76206, - "Trainersanity_EVENT_BEAT_LORELEIS_ROOM_TRAINER_0_ITEM": 0x764ce, - "Trainersanity_EVENT_BEAT_BRUNOS_ROOM_TRAINER_0_ITEM": 0x76627, - "Trainersanity_EVENT_BEAT_AGATHAS_ROOM_TRAINER_0_ITEM": 0x76786, - "Option_Itemfinder": 0x768ff, - "Text_Magikarp_Salesman": 0x8a7fe, + "Option_Trainersanity4": 0x75af6, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_B_ITEM": 0x75b02, + "Option_Trainersanity3": 0x75b46, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_A_ITEM": 0x75b52, + "Option_Trainersanity5": 0x75bad, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_2_ITEM": 0x75bb9, + "Option_Trainersanity6": 0x75bfd, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_3_ITEM": 0x75c09, + "Option_Trainersanity7": 0x75c4d, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_4_ITEM": 0x75c59, + "Option_Trainersanity8": 0x75c9d, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_5_ITEM": 0x75ca9, + "Option_Trainersanity9": 0x75ced, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_6_ITEM": 0x75cf9, + "Event_Lab_Scientist": 0x75f17, + "Fossils_Needed_For_Second_Item": 0x75fe4, + "Event_Dome_Fossil_B": 0x76061, + "Event_Helix_Fossil_B": 0x76081, + "Shop8": 0x7619f, + "Starter2_N": 0x762d2, + "Starter3_N": 0x762da, + "Trainersanity_EVENT_BEAT_LORELEIS_ROOM_TRAINER_0_ITEM": 0x7663d, + "Trainersanity_EVENT_BEAT_BRUNOS_ROOM_TRAINER_0_ITEM": 0x76796, + "Trainersanity_EVENT_BEAT_AGATHAS_ROOM_TRAINER_0_ITEM": 0x768f5, + "Option_Itemfinder": 0x76a6e, + "Text_Quiz_A": 0x88806, + "Text_Quiz_B": 0x8893a, + "Text_Quiz_C": 0x88a6e, + "Text_Quiz_D": 0x88ba2, + "Text_Quiz_E": 0x88cd6, + "Text_Quiz_F": 0x88e0a, + "Text_Magikarp_Salesman": 0x8ae3f, + "Text_Rock_Tunnel_Sign": 0x8e82a, "Text_Badges_Needed": 0x92304, "Badge_Text_Boulder_Badge": 0x99010, "Badge_Text_Cascade_Badge": 0x99028, diff --git a/worlds/pokemon_rb/rules.py b/worlds/pokemon_rb/rules.py index 493a58e594..5d117dc50e 100644 --- a/worlds/pokemon_rb/rules.py +++ b/worlds/pokemon_rb/rules.py @@ -1,26 +1,45 @@ -from ..generic.Rules import add_item_rule, add_rule +from ..generic.Rules import add_item_rule, add_rule, item_name +from .items import item_groups + def set_rules(world, player): - add_item_rule(world.get_location("Pallet Town - Player's PC", player), - lambda i: i.player == player and "Badge" not in i.name and "Trap" not in i.name and - i.name != "Pokedex") + item_rules = { + "Pallet Town - Player's PC": (lambda i: i.player == player and "Badge" not in i.name and "Trap" not in i.name + and i.name != "Pokedex" and "Coins" not in i.name) + } + + if world.prizesanity[player]: + def prize_rule(i): + return i.player != player or i.name in item_groups["Unique"] + item_rules["Celadon Prize Corner - Item Prize 1"] = prize_rule + item_rules["Celadon Prize Corner - Item Prize 2"] = prize_rule + item_rules["Celadon Prize Corner - Item Prize 3"] = prize_rule + + if world.accessibility[player] != "locations": + world.get_location("Cerulean City - Bicycle Shop", player).always_allow = (lambda state, item: + item.name == "Bike Voucher" + and item.player == player) + world.get_location("Fuchsia City - Safari Zone Warden", player).always_allow = (lambda state, item: + item.name == "Gold Teeth" and + item.player == player) access_rules = { - "Pallet Town - Rival's Sister": lambda state: state.has("Oak's Parcel", player), "Pallet Town - Oak's Post-Route-22-Rival Gift": lambda state: state.has("Oak's Parcel", player), "Viridian City - Sleepy Guy": lambda state: state.pokemon_rb_can_cut(player) or state.pokemon_rb_can_surf(player), "Route 2 - Oak's Aide": lambda state: state.pokemon_rb_oaks_aide(state.multiworld.oaks_aide_rt_2[player].value + 5, player), "Pewter City - Museum": lambda state: state.pokemon_rb_can_cut(player), - "Cerulean City - Bicycle Shop": lambda state: state.has("Bike Voucher", player), + "Cerulean City - Bicycle Shop": lambda state: state.has("Bike Voucher", player) + or item_name(state, "Cerulean City - Bicycle Shop", player) == ("Bike Voucher", player), "Lavender Town - Mr. Fuji": lambda state: state.has("Fuji Saved", player), "Vermilion Gym - Lt. Surge 1": lambda state: state.pokemon_rb_can_cut(player or state.pokemon_rb_can_surf(player)), "Vermilion Gym - Lt. Surge 2": lambda state: state.pokemon_rb_can_cut(player or state.pokemon_rb_can_surf(player)), "Route 11 - Oak's Aide": lambda state: state.pokemon_rb_oaks_aide(state.multiworld.oaks_aide_rt_11[player].value + 5, player), "Celadon City - Stranded Man": lambda state: state.pokemon_rb_can_surf(player), - "Silph Co 11F - Silph Co President": lambda state: state.has("Card Key", player), - "Fuchsia City - Safari Zone Warden": lambda state: state.has("Gold Teeth", player), + "Silph Co 11F - Silph Co President (Card Key)": lambda state: state.has("Card Key", player), + "Fuchsia City - Safari Zone Warden": lambda state: state.has("Gold Teeth", player) + or item_name(state, "Fuchsia City - Safari Zone Warden", player) == ("Gold Teeth", player), "Route 12 - Island Item": lambda state: state.pokemon_rb_can_surf(player), "Route 12 - Item Behind Cuttable Tree": lambda state: state.pokemon_rb_can_cut(player), "Route 15 - Oak's Aide": lambda state: state.pokemon_rb_oaks_aide(state.multiworld.oaks_aide_rt_15[player].value + 5, player), @@ -38,6 +57,23 @@ def set_rules(world, player): "Silph Co 6F - Southwest Item (Card Key)": lambda state: state.has("Card Key", player), "Silph Co 7F - East Item (Card Key)": lambda state: state.has("Card Key", player), "Safari Zone Center - Island Item": lambda state: state.pokemon_rb_can_surf(player), + "Celadon Prize Corner - Item Prize 1": lambda state: state.has("Coin Case", player), + "Celadon Prize Corner - Item Prize 2": lambda state: state.has("Coin Case", player), + "Celadon Prize Corner - Item Prize 3": lambda state: state.has("Coin Case", player), + "Celadon Game Corner - West Gambler's Gift (Coin Case)": lambda state: state.has("Coin Case", player), + "Celadon Game Corner - Center Gambler's Gift (Coin Case)": lambda state: state.has("Coin Case", player), + "Celadon Game Corner - East Gambler's Gift (Coin Case)": lambda state: state.has("Coin Case", player), + "Celadon Game Corner - Hidden Item Northwest By Counter (Coin Case)": lambda state: state.has("Coin Case", player), + "Celadon Game Corner - Hidden Item Southwest Corner (Coin Case)": lambda state: state.has("Coin Case", player), + "Celadon Game Corner - Hidden Item Near Rumor Man (Coin Case)": lambda state: state.has("Coin Case", player), + "Celadon Game Corner - Hidden Item Near Speculating Woman (Coin Case)": lambda state: state.has("Coin Case", player), + "Celadon Game Corner - Hidden Item Near West Gifting Gambler (Coin Case)": lambda state: state.has("Coin Case", player), + "Celadon Game Corner - Hidden Item Near Wonderful Time Woman (Coin Case)": lambda state: state.has("Coin Case", player), + "Celadon Game Corner - Hidden Item Near Failing Gym Information Guy (Coin Case)": lambda state: state.has( "Coin Case", player), + "Celadon Game Corner - Hidden Item Near East Gifting Gambler (Coin Case)": lambda state: state.has("Coin Case", player), + "Celadon Game Corner - Hidden Item Near Hooked Guy (Coin Case)": lambda state: state.has("Coin Case", player), + "Celadon Game Corner - Hidden Item at End of Horizontal Machine Row (Coin Case)": lambda state: state.has("Coin Case", player), + "Celadon Game Corner - Hidden Item in Front of Horizontal Machine Row (Coin Case)": lambda state: state.has("Coin Case", player), "Silph Co 11F - Silph Co Liberated": lambda state: state.has("Card Key", player), @@ -89,6 +125,16 @@ def set_rules(world, player): "Seafoam Islands B4F - Legendary Pokemon": lambda state: state.pokemon_rb_can_strength(player), "Vermilion City - Legendary Pokemon": lambda state: state.pokemon_rb_can_surf(player) and state.has("S.S. Ticket", player), + "Route 2 - Marcel Trade": lambda state: state.can_reach("Route 24 - Wild Pokemon - 6", "Location", player), + "Underground Tunnel West-East - Spot Trade": lambda state: state.can_reach("Route 24 - Wild Pokemon - 6", "Location", player), + "Route 11 - Terry Trade": lambda state: state.can_reach("Safari Zone Center - Wild Pokemon - 5", "Location", player), + "Route 18 - Marc Trade": lambda state: state.can_reach("Route 23 - Super Rod Pokemon - 1", "Location", player), + "Cinnabar Island - Sailor Trade": lambda state: state.can_reach("Pokemon Mansion 1F - Wild Pokemon - 3", "Location", player), + "Cinnabar Island - Crinkles Trade": lambda state: state.can_reach("Route 12 - Wild Pokemon - 4", "Location", player), + "Cinnabar Island - Doris Trade": lambda state: state.can_reach("Cerulean Cave 1F - Wild Pokemon - 9", "Location", player), + "Vermilion City - Dux Trade": lambda state: state.can_reach("Route 3 - Wild Pokemon - 2", "Location", player), + "Cerulean City - Lola Trade": lambda state: state.can_reach("Route 10 - Super Rod Pokemon - 1", "Location", player), + # Pokédex check "Pallet Town - Oak's Parcel Reward": lambda state: state.has("Oak's Parcel", player), @@ -142,7 +188,7 @@ def set_rules(world, player): "Pokemon Mansion 1F - Hidden Item Block Near Entrance Carpet": lambda state: state.pokemon_rb_can_get_hidden_items(player), "Pokemon Mansion 3F - Hidden Item Behind Burglar": lambda state: state.pokemon_rb_can_get_hidden_items(player), - "Route 23 - Hidden Item Rocks Before Final Guard": lambda state: state.pokemon_rb_can_get_hidden_items( + "Route 23 - Hidden Item Rocks Before Victory Road": lambda state: state.pokemon_rb_can_get_hidden_items( player), "Route 23 - Hidden Item East Bush After Water": lambda state: state.pokemon_rb_can_get_hidden_items( player), @@ -178,3 +224,11 @@ def set_rules(world, player): for loc in world.get_locations(player): if loc.name in access_rules: add_rule(loc, access_rules[loc.name]) + if loc.name in item_rules: + add_item_rule(loc, item_rules[loc.name]) + if loc.name.startswith("Pokedex"): + mon = loc.name.split(" - ")[1] + add_rule(loc, lambda state, i=mon: (state.has("Pokedex", player) or not + state.multiworld.require_pokedex[player]) and (state.has(i, player) + or state.has(f"Static {i}", player))) + diff --git a/worlds/pokemon_rb/text.py b/worlds/pokemon_rb/text.py index e15623d4b8..feb54e656a 100644 --- a/worlds/pokemon_rb/text.py +++ b/worlds/pokemon_rb/text.py @@ -1,5 +1,9 @@ special_chars = { "PKMN": 0x4A, + "LINE": 0x4F, + "CONT": 0x55, + "DONE": 0x57, + "PROMPT": 0x58, "'d": 0xBB, "'l": 0xBC, "'t": 0xBE, @@ -105,7 +109,7 @@ char_map = { "9": 0xFF, } -unsafe_chars = ["@", "#", "PKMN"] +unsafe_chars = ["@", "#", "PKMN", "LINE", "DONE", "CONT", "PROMPT"] def encode_text(text: str, length: int=0, whitespace=False, force=False, safety=False): diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 93ac7fbddf..5e73f5db0c 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -509,7 +509,7 @@ class SMZ3World(World): return self.smz3DungeonItems else: return [] - + def post_fill(self): # some small or big keys (those always_allow) can be unreachable in-game # while logic still collects some of them (probably to simulate the player collecting pot keys in the logic), some others don't @@ -524,7 +524,7 @@ class SMZ3World(World): loc.item.classification = ItemClassification.filler loc.item.item.Progression = False loc.item.location.event = False - self.unreachable.append(loc) + self.unreachable.append(loc) def get_filler_item_name(self) -> str: return self.multiworld.random.choice(self.junkItemsNames) From 060ee926e7cfa9aa086645750e3335f06f9d1523 Mon Sep 17 00:00:00 2001 From: Jarno Date: Mon, 13 Mar 2023 23:45:56 +0100 Subject: [PATCH 056/172] Core: type specified missing per_game_common_options (#1509) --- BaseClasses.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/BaseClasses.py b/BaseClasses.py index a375699fb9..0ff9e10470 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -71,6 +71,11 @@ class MultiWorld(): completion_condition: Dict[int, Callable[[CollectionState], bool]] indirect_connections: Dict[Region, Set[Entrance]] exclude_locations: Dict[int, Options.ExcludeLocations] + priority_locations: Dict[int, Options.PriorityLocations] + start_inventory: Dict[int, Options.StartInventory] + start_hints: Dict[int, Options.StartHints] + start_location_hints: Dict[int, Options.StartLocationHints] + item_links: Dict[int, Options.ItemLinks] game: Dict[int, str] From 573a1a8402caa3833a62d589d1169d6cae862ea6 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Mon, 13 Mar 2023 18:55:34 -0500 Subject: [PATCH 057/172] Core: Add a function to allow worlds to easily allow self-locking items (#1383) * implement function to allow self locking items for items accessibility * swap some lttp locations to use new functionality * lambda capture `item_name` and `location` * don't lambda capture location * Revert weird visual indent * make location.always_allow additive * fix always_allow rule for multiple items * don't need to lambda capture item_names * oop * move player assignment to the beginning * always_allow should only be for that player so prevent non_local_items * messenger got merged so have it use this * Core: fix doc string indentation for allow_self_locking_items * Core: fix doc string indentation for allow_self_locking_items, number two --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- BaseClasses.py | 2 +- worlds/alttp/Rules.py | 32 ++++++++++++++++---------------- worlds/generic/Rules.py | 33 ++++++++++++++++++++++++++++++--- worlds/messenger/Rules.py | 39 +++------------------------------------ 4 files changed, 50 insertions(+), 56 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 0ff9e10470..221675bfd4 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -966,7 +966,7 @@ class Location: self.parent_region = parent def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool: - return (self.always_allow(state, item) + return ((self.always_allow(state, item) and item.name not in state.multiworld.non_local_items[item.player]) or ((self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful)) and self.item_rule(item) and (not check_access or self.can_reach(state)))) diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index ff35c30554..e6c5f15a2f 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -4,7 +4,7 @@ from typing import Iterator, Set from BaseClasses import Entrance, MultiWorld from worlds.generic.Rules import (add_item_rule, add_rule, forbid_item, - item_in_locations, item_name, set_rule) + item_in_locations, location_item_name, set_rule, allow_self_locking_items) from . import OverworldGlitchRules from .Bosses import GanonDefeatRule @@ -268,7 +268,7 @@ def global_rules(world, player): if not (world.smallkey_shuffle[player] and world.bigkey_shuffle[player]): add_rule(world.get_location('Desert Palace - Prize', player), lambda state: state.multiworld.get_region('Desert Palace Main (Outer)', player).can_reach(state)) - set_rule(world.get_entrance('Tower of Hera Small Key Door', player), lambda state: state._lttp_has_key('Small Key (Tower of Hera)', player) or item_name(state, 'Tower of Hera - Big Key Chest', player) == ('Small Key (Tower of Hera)', player)) + set_rule(world.get_entrance('Tower of Hera Small Key Door', player), lambda state: state._lttp_has_key('Small Key (Tower of Hera)', player) or location_item_name(state, 'Tower of Hera - Big Key Chest', player) == ('Small Key (Tower of Hera)', player)) set_rule(world.get_entrance('Tower of Hera Big Key Door', player), lambda state: state.has('Big Key (Tower of Hera)', player)) set_rule(world.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player)) set_rule(world.get_location('Tower of Hera - Big Key Chest', player), lambda state: has_fire_source(state, player)) @@ -278,27 +278,27 @@ def global_rules(world, player): set_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Flippers', player) and state.has('Open Floodgate', player)) set_rule(world.get_entrance('Swamp Palace Small Key Door', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player)) set_rule(world.get_entrance('Swamp Palace (Center)', player), lambda state: state.has('Hammer', player)) - set_rule(world.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player) or item_name(state, 'Swamp Palace - Big Chest', player) == ('Big Key (Swamp Palace)', player)) + set_rule(world.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player)) if world.accessibility[player] != 'locations': - set_always_allow(world.get_location('Swamp Palace - Big Chest', player), lambda state, item: item.name == 'Big Key (Swamp Palace)' and item.player == player) + allow_self_locking_items(world.get_location('Swamp Palace - Big Chest', player), 'Big Key (Swamp Palace)') set_rule(world.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player)) if not world.smallkey_shuffle[player] and world.logic[player] not in ['hybridglitches', 'nologic']: forbid_item(world.get_location('Swamp Palace - Entrance', player), 'Big Key (Swamp Palace)', player) set_rule(world.get_entrance('Thieves Town Big Key Door', player), lambda state: state.has('Big Key (Thieves Town)', player)) set_rule(world.get_entrance('Blind Fight', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player)) - set_rule(world.get_location('Thieves\' Town - Big Chest', player), lambda state: (state._lttp_has_key('Small Key (Thieves Town)', player) or item_name(state, 'Thieves\' Town - Big Chest', player) == ('Small Key (Thieves Town)', player)) and state.has('Hammer', player)) + set_rule(world.get_location('Thieves\' Town - Big Chest', player), lambda state: (state._lttp_has_key('Small Key (Thieves Town)', player)) and state.has('Hammer', player)) if world.accessibility[player] != 'locations': - set_always_allow(world.get_location('Thieves\' Town - Big Chest', player), lambda state, item: item.name == 'Small Key (Thieves Town)' and item.player == player and state.has('Hammer', player)) + allow_self_locking_items(world.get_location('Thieves\' Town - Big Chest', player), 'Small Key (Thieves Town)') set_rule(world.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player)) set_rule(world.get_entrance('Skull Woods First Section South Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player)) set_rule(world.get_entrance('Skull Woods First Section (Right) North Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player)) set_rule(world.get_entrance('Skull Woods First Section West Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 2)) # ideally would only be one key, but we may have spent thst key already on escaping the right section set_rule(world.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 2)) - set_rule(world.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player) or item_name(state, 'Skull Woods - Big Chest', player) == ('Big Key (Skull Woods)', player)) + set_rule(world.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player)) if world.accessibility[player] != 'locations': - set_always_allow(world.get_location('Skull Woods - Big Chest', player), lambda state, item: item.name == 'Big Key (Skull Woods)' and item.player == player) + allow_self_locking_items(world.get_location('Skull Woods - Big Chest', player), 'Big Key (Skull Woods)') set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player) and has_sword(state, player)) # sword required for curtain set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: can_melt_things(state, player)) @@ -318,9 +318,9 @@ def global_rules(world, player): set_rule(world.get_location('Misery Mire - Main Lobby', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 1) or state._lttp_has_key('Big Key (Misery Mire)', player)) # we can place a small key in the West wing iff it also contains/blocks the Big Key, as we cannot reach and softlock with the basement key door yet set_rule(world.get_entrance('Misery Mire (West)', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 2) if (( - item_name(state, 'Misery Mire - Compass Chest', player) in [('Big Key (Misery Mire)', player)]) or + location_item_name(state, 'Misery Mire - Compass Chest', player) in [('Big Key (Misery Mire)', player)]) or ( - item_name(state, 'Misery Mire - Big Key Chest', player) in [('Big Key (Misery Mire)', player)])) else state._lttp_has_key('Small Key (Misery Mire)', player, 3)) + location_item_name(state, 'Misery Mire - Big Key Chest', player) in [('Big Key (Misery Mire)', player)])) else state._lttp_has_key('Small Key (Misery Mire)', player, 3)) set_rule(world.get_location('Misery Mire - Compass Chest', player), lambda state: has_fire_source(state, player)) set_rule(world.get_location('Misery Mire - Big Key Chest', player), lambda state: has_fire_source(state, player)) set_rule(world.get_entrance('Misery Mire (Vitreous)', player), lambda state: state.has('Cane of Somaria', player)) @@ -350,12 +350,12 @@ def global_rules(world, player): set_rule(world.get_location('Palace of Darkness - Big Chest', player), lambda state: state.has('Big Key (Palace of Darkness)', player)) set_rule(world.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or ( - item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 3))) + location_item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 3))) if world.accessibility[player] != 'locations': set_always_allow(world.get_location('Palace of Darkness - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5)) set_rule(world.get_entrance('Palace of Darkness Spike Statue Room Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or ( - item_name(state, 'Palace of Darkness - Harmless Hellway', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 4))) + location_item_name(state, 'Palace of Darkness - Harmless Hellway', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 4))) if world.accessibility[player] != 'locations': set_always_allow(world.get_location('Palace of Darkness - Harmless Hellway', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5)) @@ -369,7 +369,7 @@ def global_rules(world, player): set_rule(world.get_entrance('Ganons Tower (Tile Room)', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('Ganons Tower (Hookshot Room)', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player))) set_rule(world.get_entrance('Ganons Tower (Map Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or ( - item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player), ('Small Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 3))) + location_item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player), ('Small Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 3))) if world.accessibility[player] != 'locations': set_always_allow(world.get_location('Ganons Tower - Map Chest', player), lambda state, item: item.name == 'Small Key (Ganons Tower)' and item.player == player and state._lttp_has_key('Small Key (Ganons Tower)', player, 3) and state.can_reach('Ganons Tower (Hookshot Room)', 'region', player)) @@ -911,7 +911,7 @@ def set_trock_key_rules(world, player): # Now we need to set rules based on which entrances we have access to. The most important point is whether we have back access. If we have back access, we # might open all the locked doors in any order so we need maximally restrictive rules. if can_reach_back: - set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: (state._lttp_has_key('Small Key (Turtle Rock)', player, 4) or item_name(state, 'Turtle Rock - Big Key Chest', player) == ('Small Key (Turtle Rock)', player))) + set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: (state._lttp_has_key('Small Key (Turtle Rock)', player, 4) or location_item_name(state, 'Turtle Rock - Big Key Chest', player) == ('Small Key (Turtle Rock)', player))) set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4)) # Only consider wasting the key on the Trinexx door for going from the front entrance to middle section. If other key doors are accessible, then these doors can be avoided set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3)) @@ -931,7 +931,7 @@ def set_trock_key_rules(world, player): def tr_big_key_chest_keys_needed(state): # This function handles the key requirements for the TR Big Chest in the situations it having the Big Key should logically require 2 keys, small key # should logically require no keys, and anything else should logically require 4 keys. - item = item_name(state, 'Turtle Rock - Big Key Chest', player) + item = location_item_name(state, 'Turtle Rock - Big Key Chest', player) if item in [('Small Key (Turtle Rock)', player)]: return 0 if item in [('Big Key (Turtle Rock)', player)]: @@ -1514,4 +1514,4 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool): continue if location.name in bunny_accessible_locations: continue - add_rule(location, get_rule_to_add(entrance.connected_region, location)) \ No newline at end of file + add_rule(location, get_rule_to_add(entrance.connected_region, location)) diff --git a/worlds/generic/Rules.py b/worlds/generic/Rules.py index 98fb560aee..fb783edb67 100644 --- a/worlds/generic/Rules.py +++ b/worlds/generic/Rules.py @@ -1,7 +1,7 @@ import collections import typing -from BaseClasses import LocationProgressType, MultiWorld +from BaseClasses import LocationProgressType, MultiWorld, Location, Region, Entrance if typing.TYPE_CHECKING: import BaseClasses @@ -143,14 +143,41 @@ def add_item_rule(location: "BaseClasses.Location", rule: ItemRule, combine: str def item_in_locations(state: "BaseClasses.CollectionState", item: str, player: int, locations: typing.Sequence["BaseClasses.Location"]) -> bool: for location in locations: - if item_name(state, location[0], location[1]) == (item, player): + if location_item_name(state, location[0], location[1]) == (item, player): return True return False -def item_name(state: "BaseClasses.CollectionState", location: str, player: int) -> \ +def location_item_name(state: "BaseClasses.CollectionState", location: str, player: int) -> \ typing.Optional[typing.Tuple[str, int]]: location = state.multiworld.get_location(location, player) if location.item is None: return None return location.item.name, location.item.player + + +def allow_self_locking_items(spot: typing.Union[Location, Region], *item_names: str) -> None: + """ + This function sets rules on the supplied spot, such that the supplied item_name(s) can possibly be placed there. + + spot: Location or Region that the item(s) are allowed to be placed in + item_names: item name or names that are allowed to be placed in the Location or Region + """ + player = spot.player + + def add_allowed_rules(area: typing.Union[Location, Entrance], location: Location) -> None: + def set_always_allow(location: Location, rule: typing.Callable) -> None: + location.always_allow = rule + + for item_name in item_names: + add_rule(area, lambda state, item_name=item_name: + location_item_name(state, location.name, player) == (item_name, player), "or") + set_always_allow(location, lambda state, item: + item.player == player and item.name in [item_name for item_name in item_names]) + + if isinstance(spot, Region): + for entrance in spot.entrances: + for location in spot.locations: + add_allowed_rules(entrance, location) + else: + add_allowed_rules(spot, spot) diff --git a/worlds/messenger/Rules.py b/worlds/messenger/Rules.py index c273167802..a7e0a1a76b 100644 --- a/worlds/messenger/Rules.py +++ b/worlds/messenger/Rules.py @@ -1,9 +1,9 @@ -from typing import Dict, Callable, Optional, Tuple, Union, TYPE_CHECKING, List, Iterable +from typing import Dict, Callable, TYPE_CHECKING -from BaseClasses import CollectionState, MultiWorld, Location, Region, Entrance, Item +from BaseClasses import CollectionState, MultiWorld +from worlds.generic.Rules import set_rule, allow_self_locking_items from .Options import MessengerAccessibility, Goal from .Constants import NOTES, PHOBEKINS -from ..generic.Rules import add_rule, set_rule if TYPE_CHECKING: from . import MessengerWorld @@ -113,39 +113,6 @@ class MessengerRules: set_self_locking_items(multiworld, self.player) -def location_item_name(state: CollectionState, location_name: str, player: int) -> Optional[Tuple[str, int]]: - location = state.multiworld.get_location(location_name, player) - if location.item is None: - return None - return location.item.name, location.item.player - - -def allow_self_locking_items(spot: Union[Location, Region], *item_names: str) -> None: - """ - Sets rules on the supplied spot, such that the supplied item_name(s) can possibly be placed there. - :param spot: Location or Region that the item(s) are allowed to be placed in - :param item_names: item name or names that are allowed to be placed in the Location or Region - """ - player = spot.player - - def set_always_allow(location: Location, rule: Callable[[CollectionState, Item], bool]) -> None: - location.always_allow = rule - - def add_allowed_rules(area: Union[Location, Entrance], location: Location) -> None: - for item_name in item_names: - add_rule(area, lambda state, item_name=item_name: - location_item_name(state, location.name, player) == (item_name, player), "or") - set_always_allow(location, lambda state, item: - item.player == player and item.name in [item_name for item_name in item_names]) - - if isinstance(spot, Region): - for entrance in spot.entrances: - for location in spot.locations: - add_allowed_rules(entrance, location) - else: - add_allowed_rules(spot, spot) - - def set_self_locking_items(multiworld: MultiWorld, player: int) -> None: # do the ones for seal shuffle on and off first allow_self_locking_items(multiworld.get_location("Key of Strength", player), "Power Thistle") From 2a28a6de2827f3ff1700c6b412cae815d380b48c Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 14 Mar 2023 01:25:59 +0100 Subject: [PATCH 058/172] Pokemon: apply rename of location_item_name --- worlds/pokemon_rb/rules.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/worlds/pokemon_rb/rules.py b/worlds/pokemon_rb/rules.py index 5d117dc50e..704446d66d 100644 --- a/worlds/pokemon_rb/rules.py +++ b/worlds/pokemon_rb/rules.py @@ -1,4 +1,4 @@ -from ..generic.Rules import add_item_rule, add_rule, item_name +from ..generic.Rules import add_item_rule, add_rule, location_item_name from .items import item_groups @@ -31,7 +31,7 @@ def set_rules(world, player): "Route 2 - Oak's Aide": lambda state: state.pokemon_rb_oaks_aide(state.multiworld.oaks_aide_rt_2[player].value + 5, player), "Pewter City - Museum": lambda state: state.pokemon_rb_can_cut(player), "Cerulean City - Bicycle Shop": lambda state: state.has("Bike Voucher", player) - or item_name(state, "Cerulean City - Bicycle Shop", player) == ("Bike Voucher", player), + or location_item_name(state, "Cerulean City - Bicycle Shop", player) == ("Bike Voucher", player), "Lavender Town - Mr. Fuji": lambda state: state.has("Fuji Saved", player), "Vermilion Gym - Lt. Surge 1": lambda state: state.pokemon_rb_can_cut(player or state.pokemon_rb_can_surf(player)), "Vermilion Gym - Lt. Surge 2": lambda state: state.pokemon_rb_can_cut(player or state.pokemon_rb_can_surf(player)), @@ -39,7 +39,7 @@ def set_rules(world, player): "Celadon City - Stranded Man": lambda state: state.pokemon_rb_can_surf(player), "Silph Co 11F - Silph Co President (Card Key)": lambda state: state.has("Card Key", player), "Fuchsia City - Safari Zone Warden": lambda state: state.has("Gold Teeth", player) - or item_name(state, "Fuchsia City - Safari Zone Warden", player) == ("Gold Teeth", player), + or location_item_name(state, "Fuchsia City - Safari Zone Warden", player) == ("Gold Teeth", player), "Route 12 - Island Item": lambda state: state.pokemon_rb_can_surf(player), "Route 12 - Item Behind Cuttable Tree": lambda state: state.pokemon_rb_can_cut(player), "Route 15 - Oak's Aide": lambda state: state.pokemon_rb_oaks_aide(state.multiworld.oaks_aide_rt_15[player].value + 5, player), From ca2c0e6ce2255c44f4dbb364bbb964b6d95faa2a Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 14 Mar 2023 01:32:00 +0100 Subject: [PATCH 059/172] CI: update stuff (#1534) * CI: skip SNI, skip unittests if not needed, run build for setup.py * CI: update actions * CI: update upload-artifact Fixes more warnings --- .github/workflows/build.yml | 34 +++++++++++++++------------ .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/lint.yml | 4 ++-- .github/workflows/release.yml | 9 ++----- .github/workflows/unittests.yml | 18 +++++++++++--- 5 files changed, 39 insertions(+), 28 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c51f155049..378dc87bf9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,10 +2,20 @@ name: Build -on: workflow_dispatch +on: + push: + paths: + - '.github/workflows/build.yaml' + - 'setup.py' + - 'requirements.txt' + pull_request: + paths: + - '.github/workflows/build.yaml' + - 'setup.py' + - 'requirements.txt' + workflow_dispatch: env: - SNI_VERSION: v0.0.88 ENEMIZER_VERSION: 7.1 APPIMAGETOOL_VERSION: 13 @@ -15,15 +25,13 @@ jobs: build-win-py38: # RCs will still be built and signed by hand runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: '3.8' - name: Download run-time dependencies run: | - Invoke-WebRequest -Uri https://github.com/alttpo/sni/releases/download/${Env:SNI_VERSION}/sni-${Env:SNI_VERSION}-windows-amd64.zip -OutFile sni.zip - Expand-Archive -Path sni.zip -DestinationPath SNI -Force Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force - name: Build @@ -39,7 +47,7 @@ jobs: Rename-Item exe.$NAME Archipelago 7z a -mx=9 -mhe=on -ms "../dist/$ZIP_NAME" Archipelago - name: Store 7z - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: ${{ env.ZIP_NAME }} path: dist/${{ env.ZIP_NAME }} @@ -49,14 +57,14 @@ jobs: runs-on: ubuntu-18.04 steps: # - copy code below to release.yml - - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install base dependencies run: | sudo apt update sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0 sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below - name: Get a recent python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: '3.9' - name: Install build-time dependencies @@ -69,10 +77,6 @@ jobs: chmod a+rx appimagetool - name: Download run-time dependencies run: | - wget -nv https://github.com/alttpo/sni/releases/download/$SNI_VERSION/sni-$SNI_VERSION-manylinux2014-amd64.tar.xz - tar xf sni-*.tar.xz - rm sni-*.tar.xz - mv sni-* SNI wget -nv https://github.com/Ijwu/Enemizer/releases/download/$ENEMIZER_VERSION/ubuntu.16.04-x64.7z 7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z - name: Build @@ -93,13 +97,13 @@ jobs: echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV # - copy code above to release.yml - - name: Store AppImage - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: ${{ env.APPIMAGE_NAME }} path: dist/${{ env.APPIMAGE_NAME }} retention-days: 7 - name: Store .tar.gz - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: ${{ env.TAR_NAME }} path: dist/${{ env.TAR_NAME }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index b331c25506..d42ad493e2 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -35,7 +35,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 28adb50026..b2aa9a8463 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,9 +11,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.9 - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: 3.9 - name: Install dependencies diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e9559f7856..fa3dd32100 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,7 +8,6 @@ on: - '*.*.*' env: - SNI_VERSION: v0.0.88 ENEMIZER_VERSION: 7.1 APPIMAGETOOL_VERSION: 13 @@ -36,14 +35,14 @@ jobs: - name: Set env run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV # - code below copied from build.yml - - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install base dependencies run: | sudo apt update sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0 sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below - name: Get a recent python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: '3.9' - name: Install build-time dependencies @@ -56,10 +55,6 @@ jobs: chmod a+rx appimagetool - name: Download run-time dependencies run: | - wget -nv https://github.com/alttpo/sni/releases/download/$SNI_VERSION/sni-$SNI_VERSION-manylinux2014-amd64.tar.xz - tar xf sni-*.tar.xz - rm sni-*.tar.xz - mv sni-* SNI wget -nv https://github.com/Ijwu/Enemizer/releases/download/$ENEMIZER_VERSION/ubuntu.16.04-x64.7z 7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z - name: Build diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index fe61c8b0f0..c3969ee2b1 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -3,7 +3,19 @@ name: unittests -on: [push, pull_request] +on: + push: + paths-ignore: + - 'docs/**' + - 'setup.py' + - '*.iss' + - '.gitignore' + pull_request: + paths-ignore: + - 'docs/**' + - 'setup.py' + - '*.iss' + - '.gitignore' jobs: build: @@ -27,9 +39,9 @@ jobs: os: macos-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python.version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python.version }} - name: Install dependencies From 37499b40a1e292b4c96da69fbbdc941bedba616c Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Tue, 14 Mar 2023 13:31:51 -0400 Subject: [PATCH 060/172] SMZ3: shop check fix 2 (#1538) --- worlds/smz3/data/zsm.ips | Bin 1470839 -> 1470841 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/worlds/smz3/data/zsm.ips b/worlds/smz3/data/zsm.ips index f2ecadf8ac66588974f4620d6165a214e20af67c..fff36d95d15c96a9cc756c4fd0aa10ccda3e1751 100644 GIT binary patch delta 1691 zcmZWoYfMvT7(QPwr?;;+KrGV%9;wj94Ai=bxapX5l8N_eG_{!`g01MbACux1Y6gL! zt}eQK8t8BtDusYwpkz>TC5 zdnt;%#DX|OYePEJ?*m><~p7#v6FTh^U{6KtljYn*3Y z3llh`s$>LAwtKOKZ7#w;C!iRKZp5=l``g%vGRT!;Y**jb@z(Ip;HqaPM1dr>JeElk34n8BvE`d~=idO)eBd|QQK@>?-3E1l;L!9PSU^pqUvox;; zc7vpC2D_3NOR=5!H-r5}5vx@+uLHvxiFGS^49q96JQS4dRi$A6Nis}S^2YG&;A3eL zFU6BkK)Tc23LZi9B3W7n(P2>}p)!bC$ows^d5>J&{56R+$ay8$-2%%)`$dr)-vahd z$%rbZ(mY`Ak?ejC*eUY) zR)}_ca4ND%ICU%ND@3fWpm-SscS>v?1y}!mu$hI-TQanUjcvnT_4DODT>E#5D^sWi z(WD^S8%<%}J)^g&cvaNGR1yCUJdqaFD)y@#_yvS=$&CtlT$ntrz_Xxbd+`Kl1zubT z&F{rsU@5A^DbPwQvE7t=Er|TovZ_W0-Iz}=&Sl#w@dXXaB-^X871sW&8ry({s&Piu z`3ToC?mQGT*o;)mcqHeD#wNes+DnDy2Yrgrf79!rKY7|3%Dltn(BoYn{B804W?M>y zRpWxk;(_#dFGWSgh{dC(o-WdAMLT?-VQrbVRa;_krjHoPcE?t(Y-jY=d{A0 z`H;x70ubvWu7JK4-yPtFhVuVdPSMQW+WP@LxgRjZr_;Z31vlN4VhDZ(H^(ShkSe%k zhw$JpT){2j)V^*2C6tmWBuPz%)rcmZCW8ywr6$AI&^|nDSe6)uV`5rxPwxa^1JFo% z4;$ubh4aTofYa=>FAS@92?54v6LiNZi=dMej4_`P_%}`p$sDIIO4~S2Uxjgd9p~s; z2w;#TO*O7Sd)U&c#%dq(v%i07e0di-MKY|WRcL_qT1}c=vcLLC^-s(^mkDm|>%eE&!82`5u(fRsi=&pu?PsZq#;m zn4uD&5}}gV&W;Jz4TxqV#^m;NG)VU4B)c==hFybNcpd6Gcr&iSPT^k)+7kf&I?grJ f1Arz|W+YEeg!^#~_ewY8ioPKX*kv=4GZ*{=O7@X2 delta 1718 zcmZWoe@qi+7{0Gp+T;4Q*A`_|7grBZYs?0;ZpQA1fuYlhI$2yatfKr7sLq)z3TQ^n zU?7_#Ta2Hm9WGO8F%Y9>cN(#CZqtiy_8&#a0tkyD+n}+OUz@V`?u;?9$(#3ip7(j* zzTbCu6MgH)`qrN=h_d~b@vgjr+IYVI?6&A)GwQ4j&h&5&?MwduM%<#Kmal*J`ii%8Y2^}VDM-8tStj`Ev zjv7{?!$Rk%A=8SS!i(`Z&!)5686SUJ_>{+E2wf9)reLeXtmzQF8v?w)QD4c+nCz&) zNy6zIe9MX+lOH!@Cpn*wIrK7lsRX;oqkODKp67ICvq5r(cLz<+SNd5g11fFLRoFt- z5s^L)FH*Z)%myU+fGN}6NNI49v;zEek(~v29$H133a}e}pzgyJ0{&K(T%@WHcDt%z zCYg~mFe{?6`+A86W+PLwJyT9Dvb_lGUIjBwy`%%f0hNvEBt5WpGF$}qd#Zh7G1xiM zQ;c0qmQG^9ut;Tkon!#ENoHb$`QbP%}PAHg3&?=krge!&r?Vu$s40=~)ZuE&FlB!a$`&C0St!)0Q%I<3<2JFLR zO14iU!)5ZZ%fSAIf*Geq*}Pa~V@SeaZ;+WAiK>007wmUcLl#mtXURufVNiz`&q7WG zGv_D?L2I7M$|>dYYh)%rNZBrQZ^g4UXwwhdaRu}Ct`4#FXYI||)RKrTNbi4W3!?oI z8^7a%(V>_0ktBW}yoy0*&9zoq>sS(If)td4dhavdeZtvYcs@c|q`Mpz=^~HHu@h2y z1)dIRSp{AW>FWyI4wluG*aoSz61&Vv5=gwp%PG~HzT zo<~XEzZ|QnuzphZBzd8GvX`PFN=uTLp{CwpS;Y>YaF{J&Ti7B~o;_wN`C=qD_tdG7 zvtJCHSQr)xYjCfY`}@9Fqp67t4M9es2+6I->4+ya^*93`960fCpFM2!W!QhJy;6^b zEA_??F`6OFCdM3uE(_Za8DFB|JPk*T3(*$x%Ml}wej@`%jD-nv2E@#|WJ=2uAAscm zY-Hn6<7HMZ9B)+kd_S~ZCHa11Egl*W2M06%d5_ZZL$y)AkwpEbu?6N%F}u5r(hkBe zyL+6{1gY#E7c@ujh}k{z)O*(f6j5<}E=g=MtwUzAt;ytpbfd|122#~A)0%`X7&F3x zd%;Bj4ge=f-!aqctUP}_2HY&n`_8m(uiU_M4q0D%E=ktO8J^1oImUB#6eWqt+*)){ zC{5<7eW+0w`ks4jFA9)3spj|5Lt%HSIexE3H$Xl=X+D5*$joMQifKiMxIAX3tbFrz zk8%#Ke#M3Jls3;VT%2ohf2Psg5hhwJ6B@STuDEKb4l0QJ*J^na zbrW}+B?Ugk{5H#sRDYkiYUC@ZE&~v=z@Fvp0A_-66O_>w0RNJvHp?t@ul7=#1;PrE z05MIt)HXeJ6EX{ba*3_-k-kYwEL#Wn>3P7y4|ff{12HPr7Hjc From 4b7033fce7a5f0cbf31957f5e7d6afe82be62e1f Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Tue, 14 Mar 2023 13:36:17 -0400 Subject: [PATCH 061/172] Pokemon R/B: Version 3 final touches (#1542) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Pokémon R/B: Dexsanity balls * Pokémon R/B: Early Parcel improvement * Pokémon R/B: Early Parcel dexsanity stuff only when dexsanity --- worlds/pokemon_rb/__init__.py | 6 +++++- worlds/pokemon_rb/locations.py | 5 +++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index cea26d4639..344b96f2b9 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -287,8 +287,12 @@ class PokemonRedBlueWorld(World): else: break - if self.multiworld.old_man[self.player].value == 1: + if self.multiworld.old_man[self.player] == "early_parcel": self.multiworld.local_early_items[self.player]["Oak's Parcel"] = 1 + if self.multiworld.dexsanity[self.player]: + for location in [self.multiworld.get_location(f"Pokedex - {mon}", self.player) + for mon in poke_data.pokemon_data.keys()]: + add_item_rule(location, lambda item: item.name != "Oak's Parcel" or item.player != self.player) if not self.multiworld.badgesanity[self.player].value: self.multiworld.non_local_items[self.player].value -= self.item_name_groups["Badges"] diff --git a/worlds/pokemon_rb/locations.py b/worlds/pokemon_rb/locations.py index d3af9a881a..a1b64e12e5 100644 --- a/worlds/pokemon_rb/locations.py +++ b/worlds/pokemon_rb/locations.py @@ -753,8 +753,9 @@ location_data = [ LocationData("Celadon Game Corner", "Hidden Item at End of Horizontal Machine Row (Coin Case)", "20 Coins", rom_addresses["Hidden_Item_Game_Corner_10"], Hidden(63), inclusion=hidden_items), LocationData("Celadon Game Corner", "Hidden Item in Front of Horizontal Machine Row (Coin Case)", "100 Coins", rom_addresses["Hidden_Item_Game_Corner_11"], Hidden(64), inclusion=hidden_items), - *[LocationData("Pokedex", mon, None, rom_addresses["Dexsanity_Items"] + i, DexSanityFlag(i), - type="Item", inclusion=dexsanity) for (mon, i) in zip(pokemon_data.keys(), range(0, 152))], + *[LocationData("Pokedex", mon, ball, rom_addresses["Dexsanity_Items"] + i, DexSanityFlag(i), type="Item", + inclusion=dexsanity) for (mon, i, ball) in zip(pokemon_data.keys(), range(0, 152), + ["Poke Ball", "Great Ball", "Ultra Ball"]* 51)], LocationData("Indigo Plateau", "Become Champion", "Become Champion", event=True), LocationData("Pokemon Tower 7F", "Fuji Saved", "Fuji Saved", event=True), From 3a190a8fb250d2f0c608e16109108510ce6abe67 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 14 Mar 2023 19:29:20 +0100 Subject: [PATCH 062/172] CI: more filters, update CodeQL (#1540) * CI: fix and more greedy filtering * CI: only run lint if *.py changed * CI: only run CodeQL if supported file changed * CI: fix unittests still triggering for build.yml * CI: update CodeQL action * CI: trigger codeql when changing the workflow --- .github/workflows/build.yml | 4 ++-- .github/workflows/codeql-analysis.yml | 14 +++++++++++--- .github/workflows/lint.yml | 8 +++++++- .github/workflows/unittests.yml | 26 ++++++++++++++++---------- 4 files changed, 36 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 378dc87bf9..d4e1efd466 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,12 +5,12 @@ name: Build on: push: paths: - - '.github/workflows/build.yaml' + - '.github/workflows/build.yml' - 'setup.py' - 'requirements.txt' pull_request: paths: - - '.github/workflows/build.yaml' + - '.github/workflows/build.yml' - 'setup.py' - 'requirements.txt' workflow_dispatch: diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index d42ad493e2..6aeb477a22 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -14,9 +14,17 @@ name: "CodeQL" on: push: branches: [ main ] + paths: + - '**.py' + - '**.js' + - '.github/workflows/codeql-analysis.yml' pull_request: # The branches below must be a subset of the branches above branches: [ main ] + paths: + - '**.py' + - '**.js' + - '.github/workflows/codeql-analysis.yml' schedule: - cron: '44 8 * * 1' @@ -39,7 +47,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -50,7 +58,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -64,4 +72,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b2aa9a8463..7ecda45eda 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -3,7 +3,13 @@ name: lint -on: [push, pull_request] +on: + push: + paths: + - '**.py' + pull_request: + paths: + - '**.py' jobs: build: diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index c3969ee2b1..93be745a8c 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -5,17 +5,23 @@ name: unittests on: push: - paths-ignore: - - 'docs/**' - - 'setup.py' - - '*.iss' - - '.gitignore' + paths: + - '**' + - '!docs/**' + - '!setup.py' + - '!*.iss' + - '!.gitignore' + - '!.github/workflows/**' + - '.github/workflows/unittests.yml' pull_request: - paths-ignore: - - 'docs/**' - - 'setup.py' - - '*.iss' - - '.gitignore' + paths: + - '**' + - '!docs/**' + - '!setup.py' + - '!*.iss' + - '!.gitignore' + - '!.github/workflows/**' + - '.github/workflows/unittests.yml' jobs: build: From e433246f0c28c8b36666270eadb1adc920eee134 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 14 Mar 2023 15:18:55 -0500 Subject: [PATCH 063/172] The Messenger: docs improvement (#1545) * The Messenger: docs improvement * more wordy mod link Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * indent * revert accidental indent oop Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- worlds/messenger/docs/en_The Messenger.md | 22 +++++++++---------- worlds/messenger/docs/setup_en.md | 26 +++++++++++------------ 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/worlds/messenger/docs/en_The Messenger.md b/worlds/messenger/docs/en_The Messenger.md index f1b53cd18c..16faa97cd9 100644 --- a/worlds/messenger/docs/en_The Messenger.md +++ b/worlds/messenger/docs/en_The Messenger.md @@ -1,7 +1,7 @@ # The Messenger ## Quick Links -- [Setup](../../../../games/The%20Messenger/setup/en) +- [Setup](../../../../tutorial/The%20Messenger/setup/en) - [Settings Page](../../../../games/The%20Messenger/player-settings) - [Courier Github](https://github.com/Brokemia/Courier) - [The Messenger Randomizer Github](https://github.com/minous27/TheMessengerRandomizerMod) @@ -48,25 +48,23 @@ for it. The groups you can use for The Messenger are: ## Other changes * The player can return to the Tower of Time HQ at any point by selecting the button from the options menu - * This can cause issues if used at specific times. Current known: - * During Boss fights - * After Courage Note collection (Corrupted Future chase) - * This is currently an expected action in logic. If you do need to teleport during this chase sequence, it -is recommended to quit to title and reload the save + * This can cause issues if used at specific times. Current known: + * During Boss fights + * After Courage Note collection (Corrupted Future chase) + * This is currently an expected action in logic. If you do need to teleport during this chase sequence, it + is recommended to quit to title and reload the save * After reaching ninja village a teleport option is added to the menu to reach it quickly * Toggle Windmill Shuriken button is added to option menu once the item is received ## Currently known issues * Necro cutscene will sometimes not play correctly, but will still reward the item * Ruxxtin Coffin cutscene will sometimes not play correctly, but will still reward the item -* If you receive the Fairy Bottle while in Quillshroom Marsh, The Decurse Queen cutscene will not play +* If you receive the Fairy Bottle while in Quillshroom Marsh, The Decurse Queen cutscene will not play. You can exit + to Searing Crags and re-enter to get it to play correctly. * If you defeat Barma'thazël, the cutscene afterward will not play correctly since that is what normally transitions -you to 2nd quest. The game will not kill you if you fall here, so you can teleport to HQ at any point after defeating him. + you to 2nd quest. The game will not kill you if you fall here, so you can teleport to HQ at any point after defeating him. * Sometimes upon teleporting back to HQ, Ninja will run left and enter a different portal than the one entered by the -player. -* If playing the game in non-english, sometimes the text entry menus will say "What is your name?" in local language -instead of the correct text. This can be fixed by going into the game options and selecting your language in the menu. -It does not need to be changed to something else and back. + player. * Text entry menus don't accept controller input ## What do I do if I have a problem? diff --git a/worlds/messenger/docs/setup_en.md b/worlds/messenger/docs/setup_en.md index 59341e74e0..3b88503362 100644 --- a/worlds/messenger/docs/setup_en.md +++ b/worlds/messenger/docs/setup_en.md @@ -1,31 +1,29 @@ # The Messenger Randomizer Setup Guide ## Quick Links -- [Main Page](../../../../games/The%20Messenger/info/en) +- [Game Info](../../../../games/The%20Messenger/info/en) - [Settings Page](../../../../games/The%20Messenger/player-settings) - [Courier Github](https://github.com/Brokemia/Courier) - [The Messenger Randomizer Github](https://github.com/minous27/TheMessengerRandomizerMod) - [Jacksonbird8237's Item Tracker](https://github.com/Jacksonbird8237/TheMessengerItemTracker) - [PopTracker Pack](https://github.com/alwaysintreble/TheMessengerTrackPack) -## Required Software - -- [The Messenger](https://store.steampowered.com/app/764790/The_Messenger/) - - Only Steam version is currently supported. -- [Courier Mod Loader](https://github.com/Brokemia/Courier/releases) -- [The Messenger Randomizer Mod](https://github.com/minous27/TheMessengerRandomizerMod/releases) - ## Installation 1. Download and install Courier Mod Loader using the instructions on the release page + * [Latest release is currently 0.7.1](https://github.com/Brokemia/Courier/releases) 2. Download and install the randomizer mod - * Download the latest `TheMessengerRandomizer.zip` - * Extract the zip file to `TheMessenger/Mods/` of your game's install location - * Optionally, Backup your save game + 1. Download the latest TheMessengerRandomizer.zip from the [The Messenger Randomizer Mod releases page](https://github.com/minous27/TheMessengerRandomizerMod/releases) + 2. Extract the zip file to `TheMessenger/Mods/` of your game's install location + 3. Optionally, Backup your save game + * On Windows 1. Press `Windows Key + R` to open run 2. Type `%appdata%` to access AppData 3. Navigate to `AppData/locallow/SabotageStudios/The Messenger` 4. Rename `SaveGame.txt` to any name of your choice + * On Linux + 1. Navigate to `steamapps/compatdata/764790/pfx/drive_c/users/steamuser/AppData/LocalLow/Sabotage Studio/The Messenger` + 2. Rename `SaveGame.txt` to any name of your choice ## Joining a MultiWorld Game @@ -33,12 +31,12 @@ 2. Navigate to `Options > Third Party Mod Options` 3. Select `Reset Randomizer File Slots` * This will set up all of your save slots with new randomizer save files. You can have up to 3 randomizer files at a -time, but must do this step again to start new runs afterwards. + time, but must do this step again to start new runs afterwards. 4. Enter connection info using the relevant option buttons * **The game is limited to alphanumerical characters and `-` so when entering the host name replace `.` with ` ` and -ensure that your player name when generating a settings file follows these constrictions** + ensure that your player name when generating a settings file follows these constrictions** * This defaults to `archipelago.gg` and does not need to be manually changed if connecting to a game hosted on the -website. + website. 5. Select the `Connect to Archipelago` button 6. Navigate to save file selection 7. Select a new valid randomizer save From 5d6184f1fdae2063ad92d362f12c386abc7e2c4b Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Tue, 14 Mar 2023 23:36:39 -0400 Subject: [PATCH 064/172] STS: update slot_seeds to per_slot_randoms --- worlds/spire/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/spire/__init__.py b/worlds/spire/__init__.py index a9f4d46d70..0695d18700 100644 --- a/worlds/spire/__init__.py +++ b/worlds/spire/__init__.py @@ -67,7 +67,7 @@ class SpireWorld(World): def fill_slot_data(self) -> dict: slot_data = { - 'seed': "".join(self.multiworld.slot_seeds[self.player].choice(string.ascii_letters) for i in range(16)) + 'seed': "".join(self.multiworld.per_slot_randoms[self.player].choice(string.ascii_letters) for i in range(16)) } for option_name in spire_options: option = getattr(self.multiworld, option_name)[self.player] From d825576f12fa95759a36991a989b57a565d2a402 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 15 Mar 2023 17:03:33 +0100 Subject: [PATCH 065/172] Factorio: content update Energy Link: * Transfer and Storage increased by 10X * Cost of building increased by roughly 10X * This is a way to address their effect on performance, as users now need a tenth of them to get the same throughput, it also differentiates them from Accumulators 5 new Traps: * Teleport Trap * Grenade Trap * Cluster Grenade Trap * Artillery Trap * Atomic Rocket Trap When max science is lower than min science, the two are now swapped. Max Evolution Trap count was changed from 25 -> 10. New option: Ingredients Offset * When creating random recipes, use this many more or less ingredients in the new recipe. --- worlds/factorio/Mod.py | 1 + worlds/factorio/Options.py | 59 +++++- worlds/factorio/__init__.py | 82 +++++--- worlds/factorio/data/mod/LICENSE.md | 3 +- worlds/factorio/data/mod/lib.lua | 44 ++--- worlds/factorio/data/mod_template/control.lua | 179 ++++++++++++++++-- worlds/factorio/data/mod_template/data.lua | 6 +- 7 files changed, 292 insertions(+), 82 deletions(-) diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index cda1ca1f66..bce4bb2d16 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -136,6 +136,7 @@ def generate_mod(world: "Factorio", output_directory: str): "goal": multiworld.goal[player].value, "energy_link": multiworld.energy_link[player].value, "useless_technologies": useless_technologies, + "chunk_shuffle": multiworld.chunk_shuffle[player].value if hasattr(multiworld, "chunk_shuffle") else 0, } for factorio_option in Options.factorio_options: diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index eda5eec701..85394cae44 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -1,5 +1,6 @@ from __future__ import annotations import typing +import datetime from Options import Choice, OptionDict, OptionSet, ItemDict, Option, DefaultOnToggle, Range, DeathLink, Toggle from schema import Schema, Optional, And, Or @@ -197,6 +198,14 @@ class RecipeIngredients(Choice): option_science_pack = 1 +class RecipeIngredientsOffset(Range): + """When randomizing ingredients, remove or add this many "slots" of items. + For example, at -1 a randomized Automation Science Pack will only require 1 ingredient, instead of 2.""" + display_name = "Randomized Recipe Ingredients Offset" + range_start = -1 + range_end = 5 + + class FactorioStartItems(ItemDict): """Mapping of Factorio internal item-name to amount granted on start.""" display_name = "Starting Items" @@ -223,9 +232,36 @@ class AttackTrapCount(TrapCount): display_name = "Attack Traps" +class TeleportTrapCount(TrapCount): + """Trap items that when received trigger a random teleport.""" + display_name = "Teleport Traps" + + +class GrenadeTrapCount(TrapCount): + """Trap items that when received trigger a grenade explosion on each player.""" + display_name = "Grenade Traps" + + +class ClusterGrenadeTrapCount(TrapCount): + """Trap items that when received trigger a cluster grenade explosion on each player.""" + display_name = "Cluster Grenade Traps" + + +class ArtilleryTrapCount(TrapCount): + """Trap items that when received trigger an artillery shell on each player.""" + display_name = "Artillery Traps" + + +class AtomicRocketTrapCount(TrapCount): + """Trap items that when received trigger an atomic rocket explosion on each player. + Warning: there is no warning. The launch is instantaneous.""" + display_name = "Atomic Rocket Traps" + + class EvolutionTrapCount(TrapCount): """Trap items that when received increase the enemy evolution.""" display_name = "Evolution Traps" + range_end = 10 class EvolutionTrapIncrease(Range): @@ -404,12 +440,31 @@ factorio_options: typing.Dict[str, type(Option)] = { "free_sample_whitelist": FactorioFreeSampleWhitelist, "recipe_time": RecipeTime, "recipe_ingredients": RecipeIngredients, + "recipe_ingredients_offset": RecipeIngredientsOffset, "imported_blueprints": ImportedBlueprint, "world_gen": FactorioWorldGen, "progressive": Progressive, - "evolution_traps": EvolutionTrapCount, + "teleport_traps": TeleportTrapCount, + "grenade_traps": GrenadeTrapCount, + "cluster_grenade_traps": ClusterGrenadeTrapCount, + "artillery_traps": ArtilleryTrapCount, + "atomic_rocket_traps": AtomicRocketTrapCount, "attack_traps": AttackTrapCount, + "evolution_traps": EvolutionTrapCount, "evolution_trap_increase": EvolutionTrapIncrease, "death_link": DeathLink, - "energy_link": EnergyLink + "energy_link": EnergyLink, } + +# spoilers below. If you spoil it for yourself, please at least don't spoil it for anyone else. +if datetime.datetime.today().month == 4: + + class ChunkShuffle(Toggle): + """Entrance Randomizer.""" + display_name = "Chunk Shuffle" + + + if datetime.datetime.today().day > 1: + ChunkShuffle.__doc__ += """ + 2023 April Fool's option. Shuffles chunk border transitions.""" + factorio_options["chunk_shuffle"] = ChunkShuffle diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index e691ac61c9..6391701d7b 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -35,6 +35,11 @@ class FactorioItem(Item): all_items = tech_table.copy() all_items["Attack Trap"] = factorio_base_id - 1 all_items["Evolution Trap"] = factorio_base_id - 2 +all_items["Teleport Trap"] = factorio_base_id - 3 +all_items["Grenade Trap"] = factorio_base_id - 4 +all_items["Cluster Grenade Trap"] = factorio_base_id - 5 +all_items["Artillery Trap"] = factorio_base_id - 6 +all_items["Atomic Rocket Trap"] = factorio_base_id - 7 class Factorio(World): @@ -43,7 +48,7 @@ class Factorio(World): Nauvis, an inhospitable world filled with dangerous creatures called biters. Build a factory, research new technologies, and become more efficient in your quest to build a rocket and return home. """ - game: str = "Factorio" + game = "Factorio" special_nodes = {"automation", "logistics", "rocket-silo"} custom_recipes: typing.Dict[str, Recipe] location_pool: typing.List[FactorioScienceLocation] @@ -52,12 +57,11 @@ class Factorio(World): web = FactorioWeb() item_name_to_id = all_items - # TODO: remove base_tech_table ~ 0.3.7 - location_name_to_id = {**base_tech_table, **location_table} + location_name_to_id = location_table item_name_groups = { "Progressive": set(progressive_tech_table.keys()), } - data_version = 6 + data_version = 7 required_client_version = (0, 3, 6) ordered_science_packs: typing.List[str] = MaxSciencePack.get_ordered_science_packs() @@ -73,8 +77,10 @@ class Factorio(World): generate_output = generate_mod def generate_early(self) -> None: - self.multiworld.max_tech_cost[self.player] = max(self.multiworld.max_tech_cost[self.player], - self.multiworld.min_tech_cost[self.player]) + # if max < min, then swap max and min + if self.multiworld.max_tech_cost[self.player] < self.multiworld.min_tech_cost[self.player]: + self.multiworld.min_tech_cost[self.player].value, self.multiworld.max_tech_cost[self.player].value = \ + self.multiworld.max_tech_cost[self.player].value, self.multiworld.min_tech_cost[self.player].value self.tech_mix = self.multiworld.tech_cost_mix[self.player] self.skip_silo = self.multiworld.silo[self.player].value == Silo.option_spawn @@ -87,14 +93,25 @@ class Factorio(World): nauvis = Region("Nauvis", player, self.multiworld) location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \ - self.multiworld.evolution_traps[player].value + self.multiworld.attack_traps[player].value + self.multiworld.evolution_traps[player] + \ + self.multiworld.attack_traps[player] + \ + self.multiworld.teleport_traps[player] + \ + self.multiworld.grenade_traps[player] + \ + self.multiworld.cluster_grenade_traps[player] + \ + self.multiworld.atomic_rocket_traps[player] + \ + self.multiworld.artillery_traps[player] location_pool = [] for pack in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()): location_pool.extend(location_pools[pack]) + try: + location_names = self.multiworld.random.sample(location_pool, location_count) + except ValueError as e: + # should be "ValueError: Sample larger than population or is negative" + raise Exception("Too many traps for too few locations. Either decrease the trap count, " + f"or increase the location count (higher max science pack). (Player {self.player})") from e - location_names = self.multiworld.random.sample(location_pool, location_count) self.locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis) for loc_name in location_names] distribution: TechCostDistribution = self.multiworld.tech_cost_distribution[self.player] @@ -132,6 +149,14 @@ class Factorio(World): crash.connect(nauvis) self.multiworld.regions += [menu, nauvis] + def create_items(self) -> None: + player = self.player + traps = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", "Atomic Rocket") + for trap_name in traps: + self.multiworld.itempool.extend(self.create_item(f"{trap_name} Trap") for _ in + range(getattr(self.multiworld, + f"{trap_name.lower().replace(' ', '_')}_traps")[player])) + def set_rules(self): world = self.multiworld player = self.player @@ -184,10 +209,6 @@ class Factorio(World): player = self.player want_progressives = collections.defaultdict(lambda: self.multiworld.progressive[player]. want_progressives(self.multiworld.random)) - self.multiworld.itempool.extend(self.create_item("Evolution Trap") for _ in - range(self.multiworld.evolution_traps[player].value)) - self.multiworld.itempool.extend(self.create_item("Attack Trap") for _ in - range(self.multiworld.attack_traps[player].value)) cost_sorted_locations = sorted(self.locations, key=lambda location: location.name) special_index = {"automation": 0, @@ -265,10 +286,11 @@ class Factorio(World): 2: "chemistry"} return categories.get(liquids, category) - def make_quick_recipe(self, original: Recipe, pool: list, allow_liquids: int = 2) -> Recipe: + def make_quick_recipe(self, original: Recipe, pool: list, allow_liquids: int = 2, + ingredients_offset: int = 0) -> Recipe: new_ingredients = {} liquids_used = 0 - for _ in original.ingredients: + for _ in range(len(original.ingredients) + ingredients_offset): new_ingredient = pool.pop() if new_ingredient in fluids: while liquids_used == allow_liquids and new_ingredient in fluids: @@ -282,7 +304,7 @@ class Factorio(World): original.products, original.energy) def make_balanced_recipe(self, original: Recipe, pool: typing.Set[str], factor: float = 1, - allow_liquids: int = 2) -> Recipe: + allow_liquids: int = 2, ingredients_offset: int = 0) -> Recipe: """Generate a recipe from pool with time and cost similar to original * factor""" new_ingredients = {} # have to first sort for determinism, while filtering out non-stacking items @@ -291,7 +313,7 @@ class Factorio(World): self.multiworld.random.shuffle(pool) target_raw = int(sum((count for ingredient, count in original.base_cost.items())) * factor) target_energy = original.total_energy * factor - target_num_ingredients = len(original.ingredients) + target_num_ingredients = len(original.ingredients) + ingredients_offset remaining_raw = target_raw remaining_energy = target_energy remaining_num_ingredients = target_num_ingredients @@ -382,12 +404,13 @@ class Factorio(World): return custom_technologies def set_custom_recipes(self): + ingredients_offset = self.multiworld.recipe_ingredients_offset[self.player] original_rocket_part = recipes["rocket-part"] science_pack_pools = get_science_pack_pools() valid_pool = sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_max_pack()] & valid_ingredients) self.multiworld.random.shuffle(valid_pool) self.custom_recipes = {"rocket-part": Recipe("rocket-part", original_rocket_part.category, - {valid_pool[x]: 10 for x in range(3)}, + {valid_pool[x]: 10 for x in range(3 + ingredients_offset)}, original_rocket_part.products, original_rocket_part.energy)} @@ -397,7 +420,8 @@ class Factorio(World): valid_pool += sorted(science_pack_pools[pack]) self.multiworld.random.shuffle(valid_pool) if pack in recipes: # skips over space science pack - new_recipe = self.make_quick_recipe(recipes[pack], valid_pool) + new_recipe = self.make_quick_recipe(recipes[pack], valid_pool, ingredients_offset= + ingredients_offset) self.custom_recipes[pack] = new_recipe if self.multiworld.silo[self.player].value == Silo.option_randomize_recipe \ @@ -407,21 +431,27 @@ class Factorio(World): valid_pool |= science_pack_pools[pack] if self.multiworld.silo[self.player].value == Silo.option_randomize_recipe: - new_recipe = self.make_balanced_recipe(recipes["rocket-silo"], valid_pool, - factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7) + new_recipe = self.make_balanced_recipe( + recipes["rocket-silo"], valid_pool, + factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7, + ingredients_offset=ingredients_offset) self.custom_recipes["rocket-silo"] = new_recipe if self.multiworld.satellite[self.player].value == Satellite.option_randomize_recipe: - new_recipe = self.make_balanced_recipe(recipes["satellite"], valid_pool, - factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7) + new_recipe = self.make_balanced_recipe( + recipes["satellite"], valid_pool, + factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7, + ingredients_offset=ingredients_offset) self.custom_recipes["satellite"] = new_recipe bridge = "ap-energy-bridge" new_recipe = self.make_quick_recipe( - Recipe(bridge, "crafting", {"replace_1": 1, "replace_2": 1, "replace_3": 1}, + Recipe(bridge, "crafting", {"replace_1": 1, "replace_2": 1, "replace_3": 1, + "replace_4": 1, "replace_5": 1, "replace_6": 1}, {bridge: 1}, 10), - sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_ordered_science_packs()[0]])) + sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_ordered_science_packs()[0]]), + ingredients_offset=ingredients_offset) for ingredient_name in new_recipe.ingredients: - new_recipe.ingredients[ingredient_name] = self.multiworld.random.randint(10, 100) + new_recipe.ingredients[ingredient_name] = self.multiworld.random.randint(50, 500) self.custom_recipes[bridge] = new_recipe needed_recipes = self.multiworld.max_science_pack[self.player].get_allowed_packs() | {"rocket-part"} @@ -452,7 +482,7 @@ class Factorio(World): tech_table[name], self.player) item = FactorioItem(name, - ItemClassification.trap if "Trap" in name else ItemClassification.filler, + ItemClassification.trap if name.endswith("Trap") else ItemClassification.filler, all_items[name], self.player) return item diff --git a/worlds/factorio/data/mod/LICENSE.md b/worlds/factorio/data/mod/LICENSE.md index 1299d90b46..5cf699c413 100644 --- a/worlds/factorio/data/mod/LICENSE.md +++ b/worlds/factorio/data/mod/LICENSE.md @@ -1,7 +1,8 @@ The MIT License (MIT) -Copyright (c) 2021 Berserker55 and Dewiniaid +Copyright (c) 2023 Berserker55 +Copyright (c) 2021 Dewiniaid Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/worlds/factorio/data/mod/lib.lua b/worlds/factorio/data/mod/lib.lua index 8bcd7325ee..2b18f119a4 100644 --- a/worlds/factorio/data/mod/lib.lua +++ b/worlds/factorio/data/mod/lib.lua @@ -1,32 +1,3 @@ -function filter_ingredients(ingredients, ingredient_filter) - local new_ingredient_list = {} - for _, ingredient_table in pairs(ingredients) do - if ingredient_filter[ingredient_table[1]] then -- name of ingredient_table - table.insert(new_ingredient_list, ingredient_table) - end - end - - return new_ingredient_list -end - -function add_ingredients(ingredients, added_ingredients) - local new_ingredient_list = table.deepcopy(ingredients) - for new_ingredient, count in pairs(added_ingredients) do - local found = false - for _, old_ingredient in pairs(ingredients) do - if old_ingredient[1] == new_ingredient then - found = true - break - end - end - if not found then - table.insert(new_ingredient_list, {new_ingredient, count}) - end - end - - return new_ingredient_list -end - function get_any_stack_size(name) local item = game.item_prototypes[name] if item ~= nil then @@ -50,4 +21,19 @@ function split(s, sep) string.gsub(s, pattern, function(c) fields[#fields + 1] = c end) return fields +end + +function random_offset_position(position, offset) + return {x=position.x+math.random(-offset, offset), y=position.y+math.random(-1024, 1024)} +end + +function fire_entity_at_players(entity_name, speed) + for _, player in ipairs(game.forces["player"].players) do + current_character = player.character + if current_character ~= nil then + current_character.surface.create_entity{name=entity_name, + position=random_offset_position(current_character.position, 128), + target=current_character, speed=speed} + end + end end \ No newline at end of file diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua index 077c7b03a9..4ecfdb4630 100644 --- a/worlds/factorio/data/mod_template/control.lua +++ b/worlds/factorio/data/mod_template/control.lua @@ -11,7 +11,7 @@ TRAP_EVO_FACTOR = {{ evolution_trap_increase }} / 100 MAX_SCIENCE_PACK = {{ max_science_pack }} GOAL = {{ goal }} ARCHIPELAGO_DEATH_LINK_SETTING = "archipelago-death-link-{{ slot_player }}-{{ seed_name }}" -ENERGY_INCREMENT = {{ energy_link * 1000000 }} +ENERGY_INCREMENT = {{ energy_link * 10000000 }} ENERGY_LINK_EFFICIENCY = 0.75 if settings.global[ARCHIPELAGO_DEATH_LINK_SETTING].value then @@ -22,6 +22,119 @@ end CURRENTLY_DEATH_LOCK = 0 +{% if chunk_shuffle %} +LAST_POSITIONS = {} +GENERATOR = nil +NORTH = 1 +EAST = 2 +SOUTH = 3 +WEST = 4 +ER_COLOR = {1, 1, 1, 0.2} +ER_SEED = {{ random.randint(4294967295, 2*4294967295)}} +CURRENTLY_MOVING = false +ER_FRAMES = {} +CHUNK_OFFSET = { +[NORTH] = {0, 1}, +[EAST] = {1, 0}, +[SOUTH] = {0, -1}, +[WEST] = {-1, 0} +} + + +function on_player_changed_position(event) + if CURRENTLY_MOVING == true then + return + end + local player_id = event.player_index + local player = game.get_player(player_id) + local character = player.character -- can be nil, such as spectators + + if character == nil then + return + end + local last_position = LAST_POSITIONS[player_id] + if last_position == nil then + LAST_POSITIONS[player_id] = character.position + return + end + + last_x_chunk = math.floor(last_position.x / 32) + current_x_chunk = math.floor(character.position.x / 32) + last_y_chunk = math.floor(last_position.y / 32) + current_y_chunk = math.floor(character.position.y / 32) + if (ER_FRAMES[player_id] ~= nil and rendering.is_valid(ER_FRAMES[player_id])) then + rendering.destroy(ER_FRAMES[player_id]) + end + ER_FRAMES[player_id] = rendering.draw_rectangle{ + color=ER_COLOR, width=1, filled=false, left_top = {current_x_chunk*32, current_y_chunk*32}, + right_bottom={current_x_chunk*32+32, current_y_chunk*32+32}, players={player}, time_to_live=60, + draw_on_ground= true, only_in_alt_mode = true, surface=character.surface} + if current_x_chunk == last_x_chunk and current_y_chunk == last_y_chunk then -- nothing needs doing + return + end + if ((last_position.x - character.position.x) ^ 2 + (last_position.y - character.position.y) ^ 2) > 4000 then + -- distance too high, death or other teleport took place + LAST_POSITIONS[player_id] = character.position + return + end + -- we'll need a deterministic random state + if GENERATOR == nil or not GENERATOR.valid then + GENERATOR = game.create_random_generator() + end + + -- sufficiently random pattern + GENERATOR.re_seed((ER_SEED + (last_x_chunk * 1730000000) + (last_y_chunk * 97000)) % 4294967295) + -- we now need all 4 exit directions deterministically shuffled to the 4 outgoing directions. + local exit_table = { + [1] = 1, + [2] = 2, + [3] = 3, + [4] = 4 + } + exit_table = fisher_yates_shuffle(exit_table) + if current_x_chunk > last_x_chunk then -- going right/east + outbound_direction = EAST + elseif current_x_chunk < last_x_chunk then -- going left/west + outbound_direction = WEST + end + + if current_y_chunk > last_y_chunk then -- going down/south + outbound_direction = SOUTH + elseif current_y_chunk < last_y_chunk then -- going up/north + outbound_direction = NORTH + end + local target_direction = exit_table[outbound_direction] + + local target_position = {(CHUNK_OFFSET[target_direction][1] + last_x_chunk) * 32 + 16, + (CHUNK_OFFSET[target_direction][2] + last_y_chunk) * 32 + 16} + target_position = character.surface.find_non_colliding_position(character.prototype.name, + target_position, 32, 0.5) + if target_position ~= nil then + rendering.draw_circle{color = ER_COLOR, radius = 1, filled = true, + target = {character.position.x, character.position.y}, surface = character.surface, + time_to_live = 300, draw_on_ground = true} + rendering.draw_line{color = ER_COLOR, width = 3, gap_length = 0.5, dash_length = 0.5, + from = {character.position.x, character.position.y}, to = target_position, + surface = character.surface, + time_to_live = 300, draw_on_ground = true} + CURRENTLY_MOVING = true -- prevent recursive event + character.teleport(target_position) + CURRENTLY_MOVING = false + end + LAST_POSITIONS[player_id] = character.position +end + +function fisher_yates_shuffle(tbl) + for i = #tbl, 2, -1 do + local j = GENERATOR(i) + tbl[i], tbl[j] = tbl[j], tbl[i] + end + return tbl +end + +script.on_event(defines.events.on_player_changed_position, on_player_changed_position) +{% endif %} + function on_check_energy_link(event) --- assuming 1 MJ increment and 5MJ battery: --- first 2 MJ request fill, last 2 MJ push energy, middle 1 MJ does nothing @@ -180,8 +293,8 @@ script.on_event(defines.events.on_player_removed, on_player_removed) function on_rocket_launched(event) if event.rocket and event.rocket.valid and global.forcedata[event.rocket.force.name]['victory'] == 0 then - if event.rocket.get_item_count("satellite") > 0 or GOAL == 0 then - global.forcedata[event.rocket.force.name]['victory'] = 1 + if event.rocket.get_item_count("satellite") > 0 or GOAL == 0 then + global.forcedata[event.rocket.force.name]['victory'] = 1 dumpInfo(event.rocket.force) game.set_game_state { @@ -190,8 +303,8 @@ function on_rocket_launched(event) can_continue = true, victorious_force = event.rocket.force } - end - end + end + end end script.on_event(defines.events.on_rocket_launched, on_rocket_launched) @@ -236,7 +349,7 @@ function update_player(index) end else player.print("Unable to receive " .. count .. "x [item=" .. name .. "] as this item does not exist.") - samples[name] = nil + samples[name] = nil end end @@ -254,9 +367,9 @@ script.on_event(defines.events.on_player_main_inventory_changed, update_player_e function add_samples(force, name, count) local function add_to_table(t) if count <= 0 then - -- Fixes a bug with single craft, if a recipe gives 0 of a given item. - return - end + -- Fixes a bug with single craft, if a recipe gives 0 of a given item. + return + end t[name] = (t[name] or 0) + count end -- Add to global table of earned samples for future new players @@ -298,8 +411,8 @@ script.on_event(defines.events.on_research_finished, function(event) --Don't acknowledge AP research as an Editor Extensions test force --Also no need for free samples in the Editor extensions testing surfaces, as these testing surfaces --are worked on exclusively in editor mode. - return - end + return + end if technology.researched and string.find(technology.name, "ap%-") == 1 then -- check if it came from the server anyway, then we don't need to double send. dumpInfo(technology.force) --is sendable @@ -510,6 +623,37 @@ commands.add_command("ap-print", "Used by the Archipelago client to print messag game.print(call.parameter) end) +TRAP_TABLE = { +["Attack Trap"] = function () + game.surfaces["nauvis"].build_enemy_base(game.forces["player"].get_spawn_position(game.get_surface(1)), 25) +end, +["Evolution Trap"] = function () + game.forces["enemy"].evolution_factor = game.forces["enemy"].evolution_factor + (TRAP_EVO_FACTOR * (1 - game.forces["enemy"].evolution_factor)) + game.print({"", "New evolution factor:", game.forces["enemy"].evolution_factor}) +end, +["Teleport Trap"] = function () + for _, player in ipairs(game.forces["player"].players) do + current_character = player.character + if current_character ~= nil then + current_character.teleport(current_character.surface.find_non_colliding_position( + current_character.prototype.name, random_offset_position(current_character.position, 1024), 0, 1)) + end + end +end, +["Grenade Trap"] = function () + fire_entity_at_players("grenade", 0.1) +end, +["Cluster Grenade Trap"] = function () + fire_entity_at_players("cluster-grenade", 0.1) +end, +["Artillery Trap"] = function () + fire_entity_at_players("artillery-projectile", 1) +end, +["Atomic Rocket Trap"] = function () + fire_entity_at_players("atomic-rocket", 0.1) +end, +} + commands.add_command("ap-get-technology", "Grant a technology, used by the Archipelago Client.", function(call) if global.index_sync == nil then global.index_sync = {} @@ -552,18 +696,11 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi tech.researched = true end end - elseif item_name == "Attack Trap" then - if global.index_sync[index] == nil then -- not yet received trap - game.print({"", "Received Attack Trap from ", source}) - global.index_sync[index] = item_name - local spawn_position = force.get_spawn_position(game.get_surface(1)) - game.surfaces["nauvis"].build_enemy_base(spawn_position, 25) - end - elseif item_name == "Evolution Trap" then + elseif TRAP_TABLE[item_name] ~= nil then if global.index_sync[index] == nil then -- not yet received trap global.index_sync[index] = item_name - game.forces["enemy"].evolution_factor = game.forces["enemy"].evolution_factor + (TRAP_EVO_FACTOR * (1 - game.forces["enemy"].evolution_factor)) - game.print({"", "Received Evolution Trap from ", source, ". New factor:", game.forces["enemy"].evolution_factor}) + game.print({"", "Received ", item_name, " from ", source}) + TRAP_TABLE[item_name]() end else game.print("Unknown Item " .. item_name) diff --git a/worlds/factorio/data/mod_template/data.lua b/worlds/factorio/data/mod_template/data.lua index d790831478..82053453ea 100644 --- a/worlds/factorio/data/mod_template/data.lua +++ b/worlds/factorio/data/mod_template/data.lua @@ -14,9 +14,9 @@ local energy_bridge = table.deepcopy(data.raw["accumulator"]["accumulator"]) energy_bridge.name = "ap-energy-bridge" energy_bridge.minable.result = "ap-energy-bridge" energy_bridge.localised_name = "Archipelago EnergyLink Bridge" -energy_bridge.energy_source.buffer_capacity = "5MJ" -energy_bridge.energy_source.input_flow_limit = "1MW" -energy_bridge.energy_source.output_flow_limit = "1MW" +energy_bridge.energy_source.buffer_capacity = "50MJ" +energy_bridge.energy_source.input_flow_limit = "10MW" +energy_bridge.energy_source.output_flow_limit = "10MW" tint_icon(energy_bridge, energy_bridge_tint()) energy_bridge.picture.layers[1].tint = energy_bridge_tint() energy_bridge.picture.layers[1].hr_version.tint = energy_bridge_tint() From ff9f563d4a772072418bc2e09bb4901fd039dc76 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Mon, 20 Mar 2023 11:01:08 -0500 Subject: [PATCH 066/172] Deprecate `data_version` and introduce `checksum` for DataPackages. (#684) Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- .gitignore | 1 + BaseClasses.py | 2 +- CommonClient.py | 52 ++++++++++++++++++++++-------------- Main.py | 14 +++++----- MultiServer.py | 47 ++++++++++++++++++++------------ Utils.py | 51 +++++++++++++++++++++++++++++++++++ WebHostLib/api/__init__.py | 13 +++++++-- WebHostLib/customserver.py | 17 ++++++++++-- WebHostLib/models.py | 5 ++++ WebHostLib/tracker.py | 29 +++++++++++++------- WebHostLib/upload.py | 33 ++++++++++++++++++++--- docs/network protocol.md | 40 ++++++++++++++------------- docs/world api.md | 9 ++----- requirements.txt | 3 ++- worlds/AutoWorld.py | 50 +++++++++++++++++++++++++++++----- worlds/__init__.py | 18 ++++++------- worlds/bk_sudoku/__init__.py | 6 +++-- worlds/generic/__init__.py | 1 + worlds/hylics2/__init__.py | 24 ++++++++--------- worlds/oribf/__init__.py | 1 + 20 files changed, 297 insertions(+), 119 deletions(-) diff --git a/.gitignore b/.gitignore index 4a9f3402a9..f0df6e6f56 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,7 @@ Output Logs/ /setup.ini /installdelete.iss /data/user.kv +/datapackage # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/BaseClasses.py b/BaseClasses.py index 221675bfd4..bfc1c69d27 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -336,7 +336,7 @@ class MultiWorld(): return self.player_name[player] def get_file_safe_player_name(self, player: int) -> str: - return ''.join(c for c in self.get_player_name(player) if c not in '<>:"/\\|?*') + return Utils.get_file_safe_name(self.get_player_name(player)) def get_out_file_name_base(self, player: int) -> str: """ the base name (without file extension) for each player's output file for a seed """ diff --git a/CommonClient.py b/CommonClient.py index 02dd55da98..afce98d8ca 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -136,7 +136,7 @@ class CommonContext: items_handling: typing.Optional[int] = None want_slot_data: bool = True # should slot_data be retrieved via Connect - # datapackage + # 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})') @@ -223,7 +223,7 @@ class CommonContext: self.watcher_event = asyncio.Event() self.jsontotextparser = JSONtoTextParser(self) - self.update_datapackage(network_data_package) + self.update_data_package(network_data_package) # execution self.keep_alive_task = asyncio.create_task(keep_alive(self), name="Bouncy") @@ -399,32 +399,40 @@ class CommonContext: self.input_task.cancel() # DataPackage - async def prepare_datapackage(self, relevant_games: typing.Set[str], - remote_datepackage_versions: typing.Dict[str, int]): + async def prepare_data_package(self, relevant_games: typing.Set[str], + remote_date_package_versions: typing.Dict[str, int], + remote_data_package_checksums: typing.Dict[str, str]): """Validate that all data is present for the current multiworld. Download, assimilate and cache missing data from the server.""" # by documentation any game can use Archipelago locations/items -> always relevant relevant_games.add("Archipelago") - cache_package = Utils.persistent_load().get("datapackage", {}).get("games", {}) needed_updates: typing.Set[str] = set() for game in relevant_games: - if game not in remote_datepackage_versions: + if game not in remote_date_package_versions and game not in remote_data_package_checksums: continue - remote_version: int = remote_datepackage_versions[game] - if remote_version == 0: # custom datapackage for this game + remote_version: int = remote_date_package_versions.get(game, 0) + remote_checksum: typing.Optional[str] = remote_data_package_checksums.get(game) + + if remote_version == 0 and not remote_checksum: # custom data package and no checksum for this game needed_updates.add(game) continue + local_version: int = network_data_package["games"].get(game, {}).get("version", 0) + local_checksum: typing.Optional[str] = network_data_package["games"].get(game, {}).get("checksum") # no action required if local version is new enough - if remote_version > local_version: - cache_version: int = cache_package.get(game, {}).get("version", 0) + if (not remote_checksum and (remote_version > local_version or remote_version == 0)) \ + or remote_checksum != local_checksum: + cached_game = Utils.load_data_package_for_checksum(game, remote_checksum) + cache_version: int = cached_game.get("version", 0) + cache_checksum: typing.Optional[str] = cached_game.get("checksum") # download remote version if cache is not new enough - if remote_version > cache_version: + if (not remote_checksum and (remote_version > cache_version or remote_version == 0)) \ + or remote_checksum != cache_checksum: needed_updates.add(game) else: - self.update_game(cache_package[game]) + self.update_game(cached_game) if needed_updates: await self.send_msgs([{"cmd": "GetDataPackage", "games": list(needed_updates)}]) @@ -434,15 +442,17 @@ class CommonContext: for location_name, location_id in game_package["location_name_to_id"].items(): self.location_names[location_id] = location_name - def update_datapackage(self, data_package: dict): - for game, gamedata in data_package["games"].items(): - self.update_game(gamedata) + def update_data_package(self, data_package: dict): + for game, game_data in data_package["games"].items(): + self.update_game(game_data) - def consume_network_datapackage(self, data_package: dict): - self.update_datapackage(data_package) + def consume_network_data_package(self, data_package: dict): + self.update_data_package(data_package) current_cache = Utils.persistent_load().get("datapackage", {}).get("games", {}) current_cache.update(data_package["games"]) Utils.persistent_store("datapackage", "games", current_cache) + for game, game_data in data_package["games"].items(): + Utils.store_data_package_for_checksum(game, game_data) # DeathLink hooks @@ -661,14 +671,16 @@ async def process_server_cmd(ctx: CommonContext, args: dict): current_team = network_player.team logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot)) - # update datapackage - await ctx.prepare_datapackage(set(args["games"]), args["datapackage_versions"]) + # update data package + data_package_versions = args.get("datapackage_versions", {}) + data_package_checksums = args.get("datapackage_checksums", {}) + await ctx.prepare_data_package(set(args["games"]), data_package_versions, data_package_checksums) await ctx.server_auth(args['password']) elif cmd == 'DataPackage': logger.info("Got new ID/Name DataPackage") - ctx.consume_network_datapackage(args['data']) + ctx.consume_network_data_package(args['data']) elif cmd == 'ConnectionRefused': errors = args["errors"] diff --git a/Main.py b/Main.py index 372cadc5fd..03b2e1b155 100644 --- a/Main.py +++ b/Main.py @@ -355,13 +355,11 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No for player in world.groups.get(location.item.player, {}).get("players", [])]): precollect_hint(location) - # custom datapackage - datapackage = {} - for game_world in world.worlds.values(): - if game_world.data_version == 0 and game_world.game not in datapackage: - datapackage[game_world.game] = worlds.network_data_package["games"][game_world.game] - datapackage[game_world.game]["item_name_groups"] = game_world.item_name_groups - datapackage[game_world.game]["location_name_groups"] = game_world.location_name_groups + # embedded data package + data_package = { + game_world.game: worlds.network_data_package["games"][game_world.game] + for game_world in world.worlds.values() + } multidata = { "slot_data": slot_data, @@ -378,7 +376,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No "tags": ["AP"], "minimum_versions": minimum_versions, "seed_name": world.seed_name, - "datapackage": datapackage, + "datapackage": data_package, } AutoWorld.call_all(world, "modify_multidata", multidata) diff --git a/MultiServer.py b/MultiServer.py index 6c3106a93d..4eadbb7998 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -7,17 +7,20 @@ import functools import logging import zlib import collections -import typing -import inspect -import weakref import datetime -import threading -import random -import pickle -import itertools -import time -import operator +import functools import hashlib +import inspect +import itertools +import logging +import operator +import pickle +import random +import threading +import time +import typing +import weakref +import zlib import ModuleUpdate @@ -160,6 +163,7 @@ class Context: stored_data_notification_clients: typing.Dict[str, typing.Set[Client]] slot_info: typing.Dict[int, NetworkSlot] + checksums: typing.Dict[str, str] item_names: typing.Dict[int, str] = 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})') @@ -233,6 +237,7 @@ class Context: # init empty to satisfy linter, I suppose self.gamespackage = {} + self.checksums = {} self.item_name_groups = {} self.location_name_groups = {} self.all_item_and_group_names = {} @@ -241,7 +246,7 @@ class Context: self._load_game_data() - # Datapackage retrieval + # Data package retrieval def _load_game_data(self): import worlds self.gamespackage = worlds.network_data_package["games"] @@ -255,6 +260,7 @@ class Context: def _init_game_data(self): for game_name, game_package in self.gamespackage.items(): + 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 for location_name, location_id in game_package["location_name_to_id"].items(): @@ -350,6 +356,7 @@ class Context: [{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments} for text in texts])) + # loading def load(self, multidatapath: str, use_embedded_server_options: bool = False): @@ -366,7 +373,7 @@ class Context: with open(multidatapath, 'rb') as f: data = f.read() - self._load(self.decompress(data), use_embedded_server_options) + self._load(self.decompress(data), {}, use_embedded_server_options) self.data_filename = multidatapath @staticmethod @@ -376,7 +383,8 @@ class Context: raise Utils.VersionException("Incompatible multidata.") return restricted_loads(zlib.decompress(data[1:])) - def _load(self, decoded_obj: dict, use_embedded_server_options: bool): + def _load(self, decoded_obj: dict, game_data_packages: typing.Dict[str, typing.Any], + use_embedded_server_options: bool): self.read_data = {} mdata_ver = decoded_obj["minimum_versions"]["server"] if mdata_ver > Utils.version_tuple: @@ -431,13 +439,15 @@ class Context: server_options = decoded_obj.get("server_options", {}) self._set_options(server_options) - # custom datapackage + # embedded data package for game_name, data in decoded_obj.get("datapackage", {}).items(): - logging.info(f"Loading custom datapackage for game {game_name}") + if game_name in game_data_packages: + data = game_data_packages[game_name] + logging.info(f"Loading embedded data package for game {game_name}") self.gamespackage[game_name] = data self.item_name_groups[game_name] = data["item_name_groups"] self.location_name_groups[game_name] = data["location_name_groups"] - del data["item_name_groups"] # remove from datapackage, but keep in self.item_name_groups + del data["item_name_groups"] # remove from data package, but keep in self.item_name_groups del data["location_name_groups"] self._init_game_data() for game_name, data in self.item_name_groups.items(): @@ -735,10 +745,11 @@ async def on_client_connected(ctx: Context, client: Client): NetworkPlayer(team, slot, ctx.name_aliases.get((team, slot), name), name) ) + games = {ctx.games[x] for x in range(1, len(ctx.games) + 1)} await ctx.send_msgs(client, [{ 'cmd': 'RoomInfo', 'password': bool(ctx.password), - 'games': {ctx.games[x] for x in range(1, len(ctx.games) + 1)}, + 'games': games, # tags are for additional features in the communication. # Name them by feature or fork, as you feel is appropriate. 'tags': ctx.tags, @@ -747,7 +758,9 @@ async def on_client_connected(ctx: Context, client: Client): '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()}, + 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}, 'seed_name': ctx.seed_name, 'time': time.time(), }]) diff --git a/Utils.py b/Utils.py index 059168c857..f7305a1e2a 100644 --- a/Utils.py +++ b/Utils.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import json import typing import builtins import os @@ -142,6 +143,17 @@ def user_path(*path: str) -> str: return os.path.join(user_path.cached_path, *path) +def cache_path(*path: str) -> str: + """Returns path to a file in the user's Archipelago cache directory.""" + if hasattr(cache_path, "cached_path"): + pass + else: + import appdirs + cache_path.cached_path = appdirs.user_cache_dir("Archipelago", False) + + return os.path.join(cache_path.cached_path, *path) + + def output_path(*path: str) -> str: if hasattr(output_path, 'cached_path'): return os.path.join(output_path.cached_path, *path) @@ -385,6 +397,45 @@ def persistent_load() -> typing.Dict[str, dict]: return storage +def get_file_safe_name(name: str) -> str: + return "".join(c for c in name if c not in '<>:"/\\|?*') + + +def load_data_package_for_checksum(game: str, checksum: typing.Optional[str]) -> Dict[str, Any]: + if checksum and game: + if checksum != get_file_safe_name(checksum): + raise ValueError(f"Bad symbols in checksum: {checksum}") + path = cache_path("datapackage", get_file_safe_name(game), f"{checksum}.json") + if os.path.exists(path): + try: + with open(path, "r", encoding="utf-8-sig") as f: + return json.load(f) + except Exception as e: + logging.debug(f"Could not load data package: {e}") + + # fall back to old cache + cache = persistent_load().get("datapackage", {}).get("games", {}).get(game, {}) + if cache.get("checksum") == checksum: + return cache + + # cache does not match + return {} + + +def store_data_package_for_checksum(game: str, data: typing.Dict[str, Any]) -> None: + checksum = data.get("checksum") + if checksum and game: + if checksum != get_file_safe_name(checksum): + raise ValueError(f"Bad symbols in checksum: {checksum}") + game_folder = cache_path("datapackage", get_file_safe_name(game)) + os.makedirs(game_folder, exist_ok=True) + try: + with open(os.path.join(game_folder, f"{checksum}.json"), "w", encoding="utf-8-sig") as f: + json.dump(data, f, ensure_ascii=False, separators=(",", ":")) + except Exception as e: + logging.debug(f"Could not store data package: {e}") + + def get_adjuster_settings(game_name: str) -> typing.Dict[str, typing.Any]: adjuster_settings = persistent_load().get("adjuster", {}).get(game_name, {}) return adjuster_settings diff --git a/WebHostLib/api/__init__.py b/WebHostLib/api/__init__.py index eac19d8456..102c3a49f6 100644 --- a/WebHostLib/api/__init__.py +++ b/WebHostLib/api/__init__.py @@ -39,12 +39,21 @@ def get_datapackage(): @api_endpoints.route('/datapackage_version') @cache.cached() - def get_datapackage_versions(): - from worlds import network_data_package, AutoWorldRegister + 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(): + from worlds import network_data_package + version_package = { + game: game_data["checksum"] for game, game_data in network_data_package["games"].items() + } + return version_package + + from . import generate, user # trigger registration diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index 584ca9feca..6e7da40737 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -19,7 +19,7 @@ import Utils from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor, load_server_cert from Utils import get_public_ipv4, get_public_ipv6, restricted_loads, cache_argsless -from .models import Room, Command, db +from .models import Command, GameDataPackage, Room, db class CustomClientMessageProcessor(ClientMessageProcessor): @@ -92,7 +92,20 @@ class WebHostContext(Context): else: self.port = get_random_port() - return self._load(self.decompress(room.seed.multidata), True) + multidata = self.decompress(room.seed.multidata) + game_data_packages = {} + for game in list(multidata["datapackage"]): + game_data = multidata["datapackage"][game] + if "checksum" in game_data: + if self.gamespackage.get(game, {}).get("checksum") == game_data["checksum"]: + # non-custom. remove from multidata + # games package could be dropped from static data once all rooms embed data package + del multidata["datapackage"][game] + else: + data = Utils.restricted_loads(GameDataPackage.get(checksum=game_data["checksum"]).data) + game_data_packages[game] = data + + return self._load(multidata, game_data_packages, True) @db_session def init_save(self, enabled: bool = True): diff --git a/WebHostLib/models.py b/WebHostLib/models.py index dbd03b166c..eba5c4eb4d 100644 --- a/WebHostLib/models.py +++ b/WebHostLib/models.py @@ -56,3 +56,8 @@ class Generation(db.Entity): options = Required(buffer, lazy=True) meta = Required(LongStr, default=lambda: "{\"race\": false}") state = Required(int, default=0, index=True) + + +class GameDataPackage(db.Entity): + checksum = PrimaryKey(str) + data = Required(bytes) diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 8d9311fc8f..d4260bc85a 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -11,10 +11,10 @@ from werkzeug.exceptions import abort from MultiServer import Context, get_saving_second from NetUtils import SlotType from Utils import restricted_loads -from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name +from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name, network_data_package from worlds.alttp import Items from . import app, cache -from .models import Room +from .models import GameDataPackage, Room alttp_icons = { "Blue Shield": r"https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png", @@ -229,14 +229,15 @@ def render_timedelta(delta: datetime.timedelta): @pass_context def get_location_name(context: runtime.Context, loc: int) -> str: + # once all rooms embed data package, the chain lookup can be dropped context_locations = context.get("custom_locations", {}) - return collections.ChainMap(lookup_any_location_id_to_name, context_locations).get(loc, loc) + return collections.ChainMap(context_locations, lookup_any_location_id_to_name).get(loc, loc) @pass_context def get_item_name(context: runtime.Context, item: int) -> str: context_items = context.get("custom_items", {}) - return collections.ChainMap(lookup_any_item_id_to_name, context_items).get(item, item) + return collections.ChainMap(context_items, lookup_any_item_id_to_name).get(item, item) app.jinja_env.filters["location_name"] = get_location_name @@ -274,11 +275,21 @@ def get_static_room_data(room: Room): if slot_info.type == SlotType.group} for game in games.values(): - if game in multidata["datapackage"]: - custom_locations.update( - {id: name for name, id in multidata["datapackage"][game]["location_name_to_id"].items()}) - custom_items.update( - {id: name for name, id in multidata["datapackage"][game]["item_name_to_id"].items()}) + if game not in multidata["datapackage"]: + continue + game_data = multidata["datapackage"][game] + if "checksum" in game_data: + if network_data_package["games"].get(game, {}).get("checksum") == game_data["checksum"]: + # non-custom. remove from multidata + # network_data_package import could be skipped once all rooms embed data package + del multidata["datapackage"][game] + continue + else: + game_data = restricted_loads(GameDataPackage.get(checksum=game_data["checksum"]).data) + custom_locations.update( + {id_: name for name, id_ in game_data["location_name_to_id"].items()}) + custom_items.update( + {id_: name for name, id_ in game_data["item_name_to_id"].items()}) elif "games" in multidata: games = multidata["games"] seed_checks_in_area = checks_in_area.copy() diff --git a/WebHostLib/upload.py b/WebHostLib/upload.py index dd0d218ed2..0314d64ab1 100644 --- a/WebHostLib/upload.py +++ b/WebHostLib/upload.py @@ -1,19 +1,22 @@ import base64 import json +import pickle import typing import uuid import zipfile -from io import BytesIO +import zlib +from io import BytesIO from flask import request, flash, redirect, url_for, session, render_template, Markup -from pony.orm import flush, select +from pony.orm import commit, flush, select, rollback +from pony.orm.core import TransactionIntegrityError import MultiServer from NetUtils import NetworkSlot, SlotType from Utils import VersionException, __version__ from worlds.Files import AutoPatchRegister from . import app -from .models import Seed, Room, Slot +from .models import Seed, Room, Slot, GameDataPackage banned_zip_contents = (".sfc", ".z64", ".n64", ".sms", ".gb") @@ -78,6 +81,27 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s # Load multi data. if multidata: decompressed_multidata = MultiServer.Context.decompress(multidata) + recompress = False + + if "datapackage" in decompressed_multidata: + # strip datapackage from multidata, leaving only the checksums + game_data_packages: typing.List[GameDataPackage] = [] + for game, game_data in decompressed_multidata["datapackage"].items(): + if game_data.get("checksum"): + game_data_package = GameDataPackage(checksum=game_data["checksum"], + data=pickle.dumps(game_data)) + decompressed_multidata["datapackage"][game] = { + "version": game_data.get("version", 0), + "checksum": game_data["checksum"] + } + recompress = True + try: + commit() # commit game data package + game_data_packages.append(game_data_package) + except TransactionIntegrityError: + del game_data_package + rollback() + if "slot_info" in decompressed_multidata: for slot, slot_info in decompressed_multidata["slot_info"].items(): # Ignore Player Groups (e.g. item links) @@ -90,6 +114,9 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s flush() # commit slots + if recompress: + multidata = multidata[0:1] + zlib.compress(pickle.dumps(decompressed_multidata), 9) + seed = Seed(multidata=multidata, spoiler=spoiler, slots=slots, owner=owner, meta=json.dumps(meta), id=sid if sid else uuid.uuid4()) flush() # create seed diff --git a/docs/network protocol.md b/docs/network protocol.md index bfffcc580a..c320934bd1 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -64,18 +64,19 @@ These packets are are sent from the multiworld server to the client. They are no ### RoomInfo Sent to clients when they connect to an Archipelago server. #### Arguments -| Name | Type | Notes | -| ---- | ---- | ----- | -| version | [NetworkVersion](#NetworkVersion) | Object denoting the version of Archipelago which the server is running. | -| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. Example: `WebHost` | -| password | bool | Denoted whether a password is required to join this room.| -| permissions | dict\[str, [Permission](#Permission)\[int\]\] | Mapping of permission name to [Permission](#Permission), keys are: "release", "collect" and "remaining". | -| hint_cost | int | The amount of points it costs 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). | -| 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. | +| Name | Type | Notes | +|-----------------------|-----------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| version | [NetworkVersion](#NetworkVersion) | Object denoting the version of Archipelago which the server is running. | +| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. Example: `WebHost` | +| password | bool | Denoted whether a password is required to join this room. | +| permissions | dict\[str, [Permission](#Permission)\[int\]\] | Mapping of permission name to [Permission](#Permission), keys are: "release", "collect" and "remaining". | +| hint_cost | int | The amount of points it costs 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. | #### release Dictates what is allowed when it comes to a player releasing their run. A release is an action which distributes the rest of the items in a player's run to those other players awaiting them. @@ -106,8 +107,8 @@ Dictates what is allowed when it comes to a player querying the items remaining ### ConnectionRefused Sent to clients when the server refuses connection. This is sent during the initial connection handshake. #### Arguments -| Name | Type | Notes | -| ---- | ---- | ----- | +| Name | Type | Notes | +|--------|-------------|--------------------------------------------------------------------------------------------------------------------------------------------------------| | errors | list\[str\] | Optional. When provided, should contain any one of: `InvalidSlot`, `InvalidGame`, `IncompatibleVersion`, `InvalidPassword`, or `InvalidItemsHandling`. | InvalidSlot indicates that the sent 'name' field did not match any auth entry on the server. @@ -644,11 +645,12 @@ Note: #### GameData GameData is a **dict** but contains these keys and values. It's broken out into another "type" for ease of documentation. -| Name | Type | Notes | -| ---- | ---- | ----- | -| 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 | +| Name | Type | Notes | +|---------------------|----------------|-------------------------------------------------------------------------------------------------------------------------------| +| 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 Tags are represented as a list of strings, the common Client tags follow: diff --git a/docs/world api.md b/docs/world api.md index 922674fd29..66a639f1b8 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -364,14 +364,9 @@ class MyGameLocation(Location): # or from Locations import MyGameLocation class MyGameWorld(World): """Insert description of the world/game here.""" - game: str = "My Game" # name of the game/world + game = "My Game" # name of the game/world option_definitions = mygame_options # options the player can set - topology_present: bool = 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 = 0 + topology_present = True # show path to required location checks in spoiler # ID of first item and location, could be hard-coded but code may be easier # to read with this as a propery. diff --git a/requirements.txt b/requirements.txt index 6c9e3b9d2d..8c73949dd4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,5 @@ jellyfish>=0.9.0 jinja2>=3.1.2 schema>=0.7.5 kivy>=2.1.0 -bsdiff4>=1.2.2 \ No newline at end of file +bsdiff4>=1.2.2 +appdirs>=1.4.4 diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 3fb705bdf3..55a6b9219a 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -1,20 +1,24 @@ from __future__ import annotations +import hashlib import logging -import sys import pathlib -from typing import Dict, FrozenSet, Set, Tuple, List, Optional, TextIO, Any, Callable, Type, Union, TYPE_CHECKING, \ - ClassVar +import sys +from typing import Any, Callable, ClassVar, Dict, FrozenSet, List, Optional, Set, TYPE_CHECKING, TextIO, Tuple, Type, \ + Union -from Options import AssembleOptions from BaseClasses import CollectionState +from Options import AssembleOptions if TYPE_CHECKING: from BaseClasses import MultiWorld, Item, Location, Tutorial + from . import GamesPackage class AutoWorldRegister(type): world_types: Dict[str, Type[World]] = {} + __file__: str + zip_path: Optional[str] def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoWorldRegister: if "web" in dct: @@ -154,9 +158,14 @@ class World(metaclass=AutoWorldRegister): data_version: ClassVar[int] = 1 """ - increment this every time something in your world's names/id mappings changes. - While this is set to 0, this world's DataPackage is considered in testing mode and will be inserted to the multidata - and retrieved by clients on every connection. + 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) @@ -343,8 +352,35 @@ class World(metaclass=AutoWorldRegister): def create_filler(self) -> "Item": return self.create_item(self.get_filler_item_name()) + @classmethod + def get_data_package_data(cls) -> "GamesPackage": + sorted_item_name_groups = { + name: sorted(cls.item_name_groups[name]) for name in sorted(cls.item_name_groups) + } + sorted_location_name_groups = { + name: sorted(cls.location_name_groups[name]) for name in sorted(cls.location_name_groups) + } + res: "GamesPackage" = { + # sorted alphabetically + "item_name_groups": sorted_item_name_groups, + "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 + # any methods attached to this can be used as part of CollectionState, # please use a prefix as all of them get clobbered together class LogicMixin(metaclass=AutoLogicRegister): pass + + +def data_package_checksum(data: "GamesPackage") -> str: + """Calculates the data package checksum for a game from a dict""" + assert "checksum" not in data, "Checksum already in data" + assert sorted(data) == list(data), "Data not ordered" + from NetUtils import encode + return hashlib.sha1(encode(data).encode()).hexdigest() diff --git a/worlds/__init__.py b/worlds/__init__.py index 34dece0e40..3470c1a353 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -20,14 +20,19 @@ if typing.TYPE_CHECKING: from .AutoWorld import World -class GamesPackage(typing.TypedDict): +class GamesData(typing.TypedDict): + item_name_groups: typing.Dict[str, typing.List[str]] item_name_to_id: typing.Dict[str, int] + location_name_groups: typing.Dict[str, typing.List[str]] location_name_to_id: typing.Dict[str, int] version: int +class GamesPackage(GamesData, total=False): + checksum: str + + class DataPackage(typing.TypedDict): - version: int games: typing.Dict[str, GamesPackage] @@ -75,14 +80,9 @@ games: typing.Dict[str, GamesPackage] = {} from .AutoWorld import AutoWorldRegister +# Build the data package for each game. for world_name, world in AutoWorldRegister.world_types.items(): - games[world_name] = { - "item_name_to_id": world.item_name_to_id, - "location_name_to_id": world.location_name_to_id, - "version": world.data_version, - # seems clients don't actually want this. Keeping it here in case someone changes their mind. - # "item_name_groups": {name: tuple(items) for name, items in world.item_name_groups.items()} - } + games[world_name] = world.get_data_package_data() lookup_any_item_id_to_name.update(world.item_id_to_name) lookup_any_location_id_to_name.update(world.location_id_to_name) diff --git a/worlds/bk_sudoku/__init__.py b/worlds/bk_sudoku/__init__.py index 4b0f0fb408..f914baf066 100644 --- a/worlds/bk_sudoku/__init__.py +++ b/worlds/bk_sudoku/__init__.py @@ -1,7 +1,8 @@ -from BaseClasses import Tutorial -from ..AutoWorld import World, WebWorld from typing import Dict +from BaseClasses import Tutorial +from ..AutoWorld import WebWorld, World + class Bk_SudokuWebWorld(WebWorld): settings_page = "games/Sudoku/info/en" @@ -24,6 +25,7 @@ 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/generic/__init__.py b/worlds/generic/__init__.py index 4c7c14c48f..732dc51196 100644 --- a/worlds/generic/__init__.py +++ b/worlds/generic/__init__.py @@ -42,6 +42,7 @@ 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/hylics2/__init__.py b/worlds/hylics2/__init__.py index b5fede32c3..b12beaaa3a 100644 --- a/worlds/hylics2/__init__.py +++ b/worlds/hylics2/__init__.py @@ -2,8 +2,8 @@ import random from typing import Dict, Any from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification from worlds.generic.Rules import set_rule -from ..AutoWorld import World, WebWorld -from . import Items, Locations, Options, Rules, Exits +from . import Exits, Items, Locations, Options, Rules +from ..AutoWorld import WebWorld, World class Hylics2Web(WebWorld): @@ -20,13 +20,13 @@ class Hylics2Web(WebWorld): class Hylics2World(World): """ - Hylics 2 is a surreal and unusual RPG, with a bizarre yet unique visual style. Play as Wayne, + Hylics 2 is a surreal and unusual RPG, with a bizarre yet unique visual style. Play as Wayne, travel the world, and gather your allies to defeat the nefarious Gibby in his Hylemxylem! """ game: str = "Hylics 2" web = Hylics2Web() - all_items = {**Items.item_table, **Items.gesture_item_table, **Items.party_item_table, + all_items = {**Items.item_table, **Items.gesture_item_table, **Items.party_item_table, **Items.medallion_item_table} all_locations = {**Locations.location_table, **Locations.tv_location_table, **Locations.party_location_table, **Locations.medallion_location_table} @@ -37,7 +37,7 @@ class Hylics2World(World): topology_present: bool = True - data_version: 1 + data_version = 1 start_location = "Waynehouse" @@ -59,7 +59,7 @@ class Hylics2World(World): def create_event(self, event: str): return Hylics2Item(event, ItemClassification.progression_skip_balancing, None, self.player) - + # set random starting location if option is enabled def generate_early(self): if self.multiworld.random_start[self.player]: @@ -76,7 +76,7 @@ class Hylics2World(World): def generate_basic(self): # create item pool pool = [] - + # add regular items for i, data in Items.item_table.items(): if data["count"] > 0: @@ -114,7 +114,7 @@ class Hylics2World(World): gestures = list(Items.gesture_item_table.items()) tvs = list(Locations.tv_location_table.items()) - # if Extra Items in Logic is enabled place CHARGE UP first and make sure it doesn't get + # if Extra Items in Logic is enabled place CHARGE UP first and make sure it doesn't get # placed at Sage Airship: TV if self.multiworld.extra_items_in_logic[self.player]: tv = self.multiworld.random.choice(tvs) @@ -122,7 +122,7 @@ class Hylics2World(World): while tv[1]["name"] == "Sage Airship: TV": tv = self.multiworld.random.choice(tvs) self.multiworld.get_location(tv[1]["name"], self.player)\ - .place_locked_item(self.add_item(gestures[gest][1]["name"], gestures[gest][1]["classification"], + .place_locked_item(self.add_item(gestures[gest][1]["name"], gestures[gest][1]["classification"], gestures[gest])) gestures.remove(gestures[gest]) tvs.remove(tv) @@ -182,7 +182,7 @@ class Hylics2World(World): 16: Region("Sage Airship", self.player, self.multiworld), 17: Region("Hylemxylem", self.player, self.multiworld) } - + # create regions from table for i, reg in region_table.items(): self.multiworld.regions.append(reg) @@ -214,7 +214,7 @@ class Hylics2World(World): for i, data in Locations.tv_location_table.items(): region_table[data["region"]].locations\ .append(Hylics2Location(self.player, data["name"], i, region_table[data["region"]])) - + # add party member locations if option is enabled if self.multiworld.party_shuffle[self.player]: for i, data in Locations.party_location_table.items(): @@ -241,4 +241,4 @@ class Hylics2Location(Location): class Hylics2Item(Item): - game: str = "Hylics 2" \ No newline at end of file + game: str = "Hylics 2" diff --git a/worlds/oribf/__init__.py b/worlds/oribf/__init__.py index 02350917a3..854025a8ed 100644 --- a/worlds/oribf/__init__.py +++ b/worlds/oribf/__init__.py @@ -13,6 +13,7 @@ class OriBlindForest(World): game: str = "Ori and the Blind Forest" topology_present = True + data_version = 1 item_name_to_id = item_table location_name_to_id = lookup_name_to_id From 6d13dc494455bef97e0f1afc8928853f71d5b5ad Mon Sep 17 00:00:00 2001 From: el-u <109771707+el-u@users.noreply.github.com> Date: Mon, 20 Mar 2023 17:04:57 +0100 Subject: [PATCH 067/172] lufia2ac: new features, bug fixes, and more (#1549) ### New features - ***Architect mode*** Usually the cave is randomized by the game, meaning that each attempt will produce a different dungeon. However, with this new feature the player can, between runs, opt into keeping the same cave. If activated, they will then encounter the same floor layouts, same enemy spawns, and same red chest contents as on their previous attempt. - ***Custom item pool*** Previously, the multiworld item pool consisted entirely of random blue chest items because, well, the permanent checks are blue chests and that's what one would normally get from these. While blue chest items often greatly increase your odds against regular enemies, being able to defeat the Master can be contingent on having an appropriate equipment setup of red chest items (such as Dekar blade) or even enemy drops (such as Hidora rock), most of which cannot normally be obtained from blue chests. With the custom item pool option, players now have the freedom to place any cave item into the multiworld itempool for their world. - ***Enemy floor number, enemy sprite, and enemy movement pattern randomization*** Experienced players can deduce a lot of information about the opposition they will be facing, for example: Given the current floor number, one can know in advance which of the enemy types will have a chance to spawn on that floor. And when seeing a particular enemy sprite, one can already know which enemy types one might have to face in battle if one were to come in contact with it, and also how that enemy group will move through the dungeon. Three new randomization options are added for players who want to spice up their game: one can shuffle which enemy types appear on which floor, one can shuffle which sprite is used by which enemy type, and one can shuffle which movement pattern is used by which sprite. - ***EXP modifier*** Just a simple multiplier option to allow people to level up faster. (For technical reasons, the maximum amount of EXP that can be awarded for a single enemy is limited to 65535, but even with the maximum allowed modifier of 500% there are only 6 enemy types in the cave that can reach this cap.) ### Balance change - ***proportionally adjust chest type distribution to accommodate increased blue chest chance*** One of the main problems that became apparent in the current version has to do with the distribution of chest contents. The game considers 6 categories, namely: consumable (mostly non-restorative), consumable (restorative), blue chest item, spell, gear, and weapon. Since only blue chests count as multiworld locations, we want to have a mechanism to customize the blue chest chance. Given how the chest types are detetermined in game, a naive implementation of an increased blue chest chance causes only the consumable chance to be decreased in return. In practice, this has resulted in some players of worlds with a high blue chest chance struggling (more than usual) to keep their party alive because they were always low on comsumables that restore HP and MP. The new algorithm tries to avoid this one-sided effect by having an increase in blue chest chance resulting in a decrease of all other types, calculated in such a way that the relative distribution of the other 5 categories stays (approximately) the same. ### Bug fixes - ***prevent using party member items if character is already in party*** This should have been changed at the same time that 6eb00621e39c930f5746f5f3c69a6bc19cd0e84a was made, but oh well... - ***fix glitched sprite when opening a chest immediately after receiving an item*** When opening a chest right after receiving a multiworld item (such that there were two item get animations in the exact same iteration of the game main loop), the item from the chest would display an incorrect sprite in the wrong place. Fixed by cleaning up some relevant memory addresses after getting the multiworld item. - ***fix death link*** There was a condition in `deathlink_kill_player` that looked kinda smart (it checked the time against `last_death_link`), but actually wasn't smart at all because `deathlink_kill_player` is executed as an async task and the main thread will update `last_death_link` after creating the task, meaning that whether or not the incoming death link would actually be passed to the game seems to have been up to a race condition. Fixed by simply removing that check. ### Other - ***add Lufia II Ancient Cave (and SMW) to the network diagram*** These two games were missing from the SNES sector. - ***implement get_filler_item_name*** Place a restorative consumable instead of a completely random item. (Now the only known problem with item links in lufia2ac is... that noone has ever tested item links. But this should be an improvement at least. Anyway, now #1172 can come ;) And btw., if you think that the implementation of random selection in this method looks weird, that's because it is indeed weird. (It tries to recreate the algorithm that the game itself uses when it generates a replacement item for a chest that would contain a spell that the party already knows.) - ***store all options in a dataclass*** This is basically like using #993 (but without actual support from core). It makes the lufia2ac world code much nicer to maintain because one doesn't have to change 5 different places anymore when adding or renaming an option. - ***remove master_hp.scale*** I have to admit: `scale` was a mistake. Never have I seen a single option value cause so many user misconceptions. Some people assume it affects enemies other than the Master; some people assume it affects stats other than HP; and many people will just assume it is a magic option that will somehow counterbalance whatever settings combination they are currently trying to shoot themselves in the foot with. On top of that, the `scale` mechanism probably doesn't provide a good user experience even when used for its intended purpose (since having reached floor XY in general doesn't mean you will have the power to deplete XY% of the Masters usual HP; especially given that, due to the randomness of loot, you are never guaranteed to be able to defeat the vanilla Master even when you have cleared 100% of the floors). The intended target audience of the `master_hp` option are people who want to fight the Master (and know how to fight it), but also want to lessen (to a degree of their choosing) the harsh dependence on the specific equipment setups that are usually required to win this fight even when having done all 99 floors. They can achieve this by setting the `master_hp` option to a numeric value appropriate for the level of challenge they are seeking. Therefore, nothing of value should be lost by removing the special `scale` value from the `master_hp` option, while at the same time a major source of user confusion will be eliminated. - ***typing*** This (combined with the switch to the option dataclass) greatly reduces the typing problems in the lufia2ac world. The remaining typing errors mostly fall into 4 categories: 1. Lambdas with defaults (which seem to be incorrectly reported as an error due to a mypy bug) 1. Classmethods that return instances (which could probably be improved using PEP 673 "Self" types, but that would require Python 3.11 as the minimum supported version) 1. Everything that inherits from TextChoice (which is a typing mess in core) 1. Everything related to asar.py (which does not have proper typing and lies outside of this project) ## How was this tested? https://discord.com/channels/731205301247803413/1080852357442707476 and others --- SNIClient.py | 4 +- docs/network diagram/network diagram.jpg | Bin 538820 -> 548086 bytes docs/network diagram/network diagram.md | 12 + docs/network diagram/network diagram.svg | 2 +- worlds/lufia2ac/Client.py | 252 +----------- worlds/lufia2ac/Enemies.py | 382 ++++++++++++++++++ worlds/lufia2ac/Options.py | 239 ++++++++--- worlds/lufia2ac/Rom.py | 6 +- worlds/lufia2ac/Utils.py | 21 + worlds/lufia2ac/__init__.py | 279 +++++++------ worlds/lufia2ac/basepatch/basepatch.asm | 167 +++++++- worlds/lufia2ac/basepatch/basepatch.bsdiff4 | Bin 7083 -> 7638 bytes .../lufia2ac/docs/en_Lufia II Ancient Cave.md | 20 +- worlds/lufia2ac/test/TestCustomItemPool.py | 57 +++ worlds/lufia2ac/test/TestGoal.py | 33 +- 15 files changed, 1032 insertions(+), 442 deletions(-) create mode 100644 worlds/lufia2ac/Enemies.py create mode 100644 worlds/lufia2ac/Utils.py create mode 100644 worlds/lufia2ac/test/TestCustomItemPool.py diff --git a/SNIClient.py b/SNIClient.py index 8d402b1d5f..f4ad53c617 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -115,8 +115,8 @@ class SNIClientCommandProcessor(ClientCommandProcessor): class SNIContext(CommonContext): command_processor: typing.Type[SNIClientCommandProcessor] = SNIClientCommandProcessor - game = None # set in validate_rom - items_handling = None # set in game_watcher + game: typing.Optional[str] = None # set in validate_rom + items_handling: typing.Optional[int] = None # set in game_watcher snes_connect_task: "typing.Optional[asyncio.Task[None]]" = None snes_autoreconnect_task: typing.Optional["asyncio.Task[None]"] = None diff --git a/docs/network diagram/network diagram.jpg b/docs/network diagram/network diagram.jpg index 15495e27246af113f335977137042555fb723716..0027db57ba5af8cf44794c57d4aa22105d540233 100644 GIT binary patch literal 548086 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

O+y|K4m-Z|L&+ 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°S%hxrkbu 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%0D<69a)b##+U^hMQr`% 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 z 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 literal 538820 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 z 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ξ)2 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

Qae&f;CSa29vpI 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%0As)sSa~r<88ynUgoxil%b9`XJ8pCe?`XdY-f6V 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 Q*>7%3_n&glLcetX1>p1z?*IS* diff --git a/docs/network diagram/network diagram.md b/docs/network diagram/network diagram.md index 2bffd9f295..926c8723a0 100644 --- a/docs/network diagram/network diagram.md +++ b/docs/network diagram/network diagram.md @@ -75,6 +75,18 @@ flowchart LR end SNI <-- Various, depending on SNES device --> DK3 + %% Super Mario World + subgraph Super Mario World + SMW[SNES] + end + SNI <-- Various, depending on SNES device --> SMW + + %% Lufia II Ancient Cave + subgraph Lufia II Ancient Cave + L2AC[SNES] + end + SNI <-- Various, depending on SNES device --> L2AC + %% Native Clients or Games %% Games or clients which compile to native or which the client is integrated in the game. subgraph "Native" diff --git a/docs/network diagram/network diagram.svg b/docs/network diagram/network diagram.svg index 38d3cc0713..ba29b744d5 100644 --- a/docs/network diagram/network diagram.svg +++ b/docs/network diagram/network diagram.svg @@ -1 +1 @@ -
Factorio
Secret of Evermore
WebHost (archipelago.gg)
.NET
Java
Native
Donkey Kong Country 3
Super Metroid/A Link to the Past Combo Randomizer
Super Metroid
Ocarina of Time
Final Fantasy 1
A Link to the Past
ChecksFinder
Starcraft 2
FNA/XNA
Unity
Minecraft
Secret of Evermore
WebSockets
WebSockets
Integrated
Integrated
Various, depending on SNES device
LuaSockets
Integrated
LuaSockets
Integrated
Integrated
WebSockets
Various, depending on SNES device
Various, depending on SNES device
Various, depending on SNES device
The Witness Randomizer
Various, depending on SNES device
WebSockets
WebSockets
Mod the Spire
TCP
Forge Mod Loader
WebSockets
TsRandomizer
RogueLegacyRandomizer
BepInEx
QModLoader (BepInEx)
HK Modding API
WebSockets
SQL
Subprocesses
SQL
Deposit Generated Worlds
Provide Generation Instructions
Subprocesses
Subprocesses
RCON
UDP
Integrated
Factorio Server
FactorioClient
Factorio Games
Factorio Mod Generated by AP
Factorio Modding API
SNES
Configurable (waitress, gunicorn, flask)
AutoHoster
PonyORM DB
WebHost
Flask WebContent
AutoGenerator
Mod with Archipelago.MultiClient.Net
Risk of Rain 2
Subnautica
Hollow Knight
Raft
Timespinner
Rogue Legacy
Mod with Archipelago.MultiClient.Java
Slay the Spire
Minecraft Forge Server
Any Java Minecraft Clients
Game using apclientpp Client Library
Game using Apcpp Client Library
Super Mario 64 Ex
VVVVVV
Meritous
The Witness
Sonic Adventure 2: Battle
Dark Souls 3
ap-soeclient
SNES
SNES
SNES
OoTClient
Lua Connector
BizHawk with Ocarina of Time Loaded
FF1Client
Lua Connector
BizHawk with Final Fantasy Loaded
SNES
ChecksFinderClient
ChecksFinder
Starcraft 2 Game Client
Starcraft2Client.py
apsc2 Python Package
Archipelago Server
CommonClient.py
Super Nintendo Interface (SNI)
SNIClient
\ No newline at end of file +
Factorio
Secret of Evermore
WebHost (archipelago.gg)
.NET
Java
Native
Lufia II Ancient Cave
Super Mario World
Donkey Kong Country 3
SMZ3
Super Metroid
Ocarina of Time
Final Fantasy 1
A Link to the Past
ChecksFinder
Starcraft 2
FNA/XNA
Unity
Minecraft
Secret of Evermore
WebSockets
WebSockets
Integrated
Integrated
Various, depending on SNES device
LuaSockets
Integrated
LuaSockets
Integrated
Integrated
WebSockets
Various, depending on SNES device
Various, depending on SNES device
Various, depending on SNES device
Various, depending on SNES device
Various, depending on SNES device
The Witness Randomizer
Various, depending on SNES device
WebSockets
WebSockets
Mod the Spire
TCP
Forge Mod Loader
WebSockets
TsRandomizer
RogueLegacyRandomizer
BepInEx
QModLoader (BepInEx)
HK Modding API
WebSockets
SQL
Subprocesses
SQL
Deposit Generated Worlds
Provide Generation Instructions
Subprocesses
Subprocesses
RCON
UDP
Integrated
Factorio Server
FactorioClient
Factorio Games
Factorio Mod Generated by AP
Factorio Modding API
SNES
Configurable (waitress, gunicorn, flask)
AutoHoster
PonyORM DB
WebHost
Flask WebContent
AutoGenerator
Mod with Archipelago.MultiClient.Net
Risk of Rain 2
Subnautica
Hollow Knight
Raft
Timespinner
Rogue Legacy
Mod with Archipelago.MultiClient.Java
Slay the Spire
Minecraft Forge Server
Any Java Minecraft Clients
Game using apclientpp Client Library
Game using Apcpp Client Library
Super Mario 64 Ex
VVVVVV
Meritous
The Witness
Sonic Adventure 2: Battle
Dark Souls 3
ap-soeclient
SNES
SNES
SNES
SNES
SNES
OoTClient
Lua Connector
BizHawk with Ocarina of Time Loaded
FF1Client
Lua Connector
BizHawk with Final Fantasy Loaded
SNES
ChecksFinderClient
ChecksFinder
Starcraft 2 Game Client
Starcraft2Client.py
apsc2 Python Package
Archipelago Server
CommonClient.py
Super Nintendo Interface (SNI)
SNIClient
\ No newline at end of file diff --git a/worlds/lufia2ac/Client.py b/worlds/lufia2ac/Client.py index 002b257dbd..58ee7f87f9 100644 --- a/worlds/lufia2ac/Client.py +++ b/worlds/lufia2ac/Client.py @@ -2,10 +2,11 @@ import logging import time import typing from logging import Logger -from typing import Dict +from typing import Optional from NetUtils import ClientStatus, NetworkItem from worlds.AutoSNIClient import SNIClient +from .Enemies import enemy_id_to_name from .Items import start_id as items_start_id from .Locations import start_id as locations_start_id @@ -24,233 +25,6 @@ L2AC_DEATH_ADDR: int = SRAM_START + 0x203D L2AC_TX_ADDR: int = SRAM_START + 0x2040 L2AC_RX_ADDR: int = SRAM_START + 0x2800 -enemy_names: Dict[int, str] = { - 0x00: "a Goblin", - 0x01: "an Armor goblin", - 0x02: "a Regal Goblin", - 0x03: "a Goblin Mage", - 0x04: "a Troll", - 0x05: "an Ork", - 0x06: "a Fighter ork", - 0x07: "an Ork Mage", - 0x08: "a Lizardman", - 0x09: "a Skull Lizard", - 0x0A: "an Armour Dait", - 0x0B: "a Dragonian", - 0x0C: "a Cyclops", - 0x0D: "a Mega Cyclops", - 0x0E: "a Flame genie", - 0x0F: "a Well Genie", - 0x10: "a Wind Genie", - 0x11: "an Earth Genie", - 0x12: "a Cobalt", - 0x13: "a Merman", - 0x14: "an Aqualoi", - 0x15: "an Imp", - 0x16: "a Fiend", - 0x17: "an Archfiend", - 0x18: "a Hound", - 0x19: "a Doben", - 0x1A: "a Winger", - 0x1B: "a Serfaco", - 0x1C: "a Pug", - 0x1D: "a Salamander", - 0x1E: "a Brinz Lizard", - 0x1F: "a Seahorse", - 0x20: "a Seirein", - 0x21: "an Earth Viper", - 0x22: "a Gnome", - 0x23: "a Wispy", - 0x24: "a Thunderbeast", - 0x25: "a Lunar bear", - 0x26: "a Shadowfly", - 0x27: "a Shadow", - 0x28: "a Lion", - 0x29: "a Sphinx", - 0x2A: "a Mad horse", - 0x2B: "an Armor horse", - 0x2C: "a Buffalo", - 0x2D: "a Bruse", - 0x2E: "a Bat", - 0x2F: "a Big Bat", - 0x30: "a Red Bat", - 0x31: "an Eagle", - 0x32: "a Hawk", - 0x33: "a Crow", - 0x34: "a Baby Frog", - 0x35: "a King Frog", - 0x36: "a Lizard", - 0x37: "a Newt", - 0x38: "a Needle Lizard", - 0x39: "a Poison Lizard", - 0x3A: "a Medusa", - 0x3B: "a Ramia", - 0x3C: "a Basilisk", - 0x3D: "a Cokatoris", - 0x3E: "a Scorpion", - 0x3F: "an Antares", - 0x40: "a Small Crab", - 0x41: "a Big Crab", - 0x42: "a Red Lobster", - 0x43: "a Spider", - 0x44: "a Web Spider", - 0x45: "a Beetle", - 0x46: "a Poison Beetle", - 0x47: "a Mosquito", - 0x48: "a Coridras", - 0x49: "a Spinner", - 0x4A: "a Tartona", - 0x4B: "an Armour Nail", - 0x4C: "a Moth", - 0x4D: "a Mega Moth", - 0x4E: "a Big Bee", - 0x4F: "a Dark Fly", - 0x50: "a Stinger", - 0x51: "an Armor Bee", - 0x52: "a Sentopez", - 0x53: "a Cancer", - 0x54: "a Garbost", - 0x55: "a Bolt Fish", - 0x56: "a Moray", - 0x57: "a She Viper", - 0x58: "an Angler fish", - 0x59: "a Unicorn", - 0x5A: "an Evil Shell", - 0x5B: "a Drill Shell", - 0x5C: "a Snell", - 0x5D: "an Ammonite", - 0x5E: "an Evil Fish", - 0x5F: "a Squid", - 0x60: "a Kraken", - 0x61: "a Killer Whale", - 0x62: "a White Whale", - 0x63: "a Grianos", - 0x64: "a Behemoth", - 0x65: "a Perch", - 0x66: "a Current", - 0x67: "a Vampire Rose", - 0x68: "a Desert Rose", - 0x69: "a Venus Fly", - 0x6A: "a Moray Vine", - 0x6B: "a Torrent", - 0x6C: "a Mad Ent", - 0x6D: "a Crow Kelp", - 0x6E: "a Red Plant", - 0x6F: "La Fleshia", - 0x70: "a Wheel Eel", - 0x71: "a Skeleton", - 0x72: "a Ghoul", - 0x73: "a Zombie", - 0x74: "a Specter", - 0x75: "a Dark Spirit", - 0x76: "a Snatcher", - 0x77: "a Jurahan", - 0x78: "a Demise", - 0x79: "a Leech", - 0x7A: "a Necromancer", - 0x7B: "a Hade Chariot", - 0x7C: "a Hades", - 0x7D: "a Dark Skull", - 0x7E: "a Hades Skull", - 0x7F: "a Mummy", - 0x80: "a Vampire", - 0x81: "a Nosferato", - 0x82: "a Ghost Ship", - 0x83: "a Deadly Sword", - 0x84: "a Deadly Armor", - 0x85: "a T Rex", - 0x86: "a Brokion", - 0x87: "a Pumpkin Head", - 0x88: "a Mad Head", - 0x89: "a Snow Gas", - 0x8A: "a Great Coca", - 0x8B: "a Gargoyle", - 0x8C: "a Rogue Shape", - 0x8D: "a Bone Gorem", - 0x8E: "a Nuborg", - 0x8F: "a Wood Gorem", - 0x90: "a Mad Gorem", - 0x91: "a Green Clay", - 0x92: "a Sand Gorem", - 0x93: "a Magma Gorem", - 0x94: "an Iron Gorem", - 0x95: "a Gold Gorem", - 0x96: "a Hidora", - 0x97: "a Sea Hidora", - 0x98: "a High Hidora", - 0x99: "a King Hidora", - 0x9A: "an Orky", - 0x9B: "a Waiban", - 0x9C: "a White Dragon", - 0x9D: "a Red Dragon", - 0x9E: "a Blue Dragon", - 0x9F: "a Green Dragon", - 0xA0: "a Black Dragon", - 0xA1: "a Copper Dragon", - 0xA2: "a Silver Dragon", - 0xA3: "a Gold Dragon", - 0xA4: "a Red Jelly", - 0xA5: "a Blue Jelly", - 0xA6: "a Bili Jelly", - 0xA7: "a Red Core", - 0xA8: "a Blue Core", - 0xA9: "a Green Core", - 0xAA: "a No Core", - 0xAB: "a Mimic", - 0xAC: "a Blue Mimic", - 0xAD: "an Ice Roge", - 0xAE: "a Mushroom", - 0xAF: "a Big Mushr'm", - 0xB0: "a Minataurus", - 0xB1: "a Gorgon", - 0xB2: "a Ninja", - 0xB3: "an Asashin", - 0xB4: "a Samurai", - 0xB5: "a Dark Warrior", - 0xB6: "an Ochi Warrior", - 0xB7: "a Sly Fox", - 0xB8: "a Tengu", - 0xB9: "a Warm Eye", - 0xBA: "a Wizard", - 0xBB: "a Dark Sum'ner", - 0xBC: "the Big Catfish", - 0xBD: "a Follower", - 0xBE: "the Tarantula", - 0xBF: "Pierre", - 0xC0: "Daniele", - 0xC1: "the Venge Ghost", - 0xC2: "the Fire Dragon", - 0xC3: "the Tank", - 0xC4: "Idura", - 0xC5: "Camu", - 0xC6: "Gades", - 0xC7: "Amon", - 0xC8: "Erim", - 0xC9: "Daos", - 0xCA: "a Lizard Man", - 0xCB: "a Goblin", - 0xCC: "a Skeleton", - 0xCD: "a Regal Goblin", - 0xCE: "a Goblin", - 0xCF: "a Goblin Mage", - 0xD0: "a Slave", - 0xD1: "a Follower", - 0xD2: "a Groupie", - 0xD3: "the Egg Dragon", - 0xD4: "a Mummy", - 0xD5: "a Troll", - 0xD6: "Gades", - 0xD7: "Idura", - 0xD8: "a Lion", - 0xD9: "the Rogue Flower", - 0xDA: "a Gargoyle", - 0xDB: "a Ghost Ship", - 0xDC: "Idura", - 0xDD: "a Soldier", - 0xDE: "Gades", - 0xDF: "the Master", -} - class L2ACSNIClient(SNIClient): game: str = "Lufia II Ancient Cave" @@ -258,7 +32,7 @@ class L2ACSNIClient(SNIClient): async def validate_rom(self, ctx: SNIContext) -> bool: from SNIClient import snes_read - rom_name: bytes = await snes_read(ctx, L2AC_ROMNAME_START, 0x15) + rom_name: Optional[bytes] = await snes_read(ctx, L2AC_ROMNAME_START, 0x15) if rom_name is None or rom_name[:4] != b"L2AC": return False @@ -272,7 +46,7 @@ class L2ACSNIClient(SNIClient): async def game_watcher(self, ctx: SNIContext) -> None: from SNIClient import snes_buffered_write, snes_flush_writes, snes_read - rom: bytes = await snes_read(ctx, L2AC_ROMNAME_START, 0x15) + rom: Optional[bytes] = await snes_read(ctx, L2AC_ROMNAME_START, 0x15) if rom != ctx.rom: ctx.rom = None return @@ -281,30 +55,30 @@ class L2ACSNIClient(SNIClient): # not successfully connected to a multiworld server, cannot process the game sending items return - signature: bytes = await snes_read(ctx, L2AC_SIGN_ADDR, 16) + signature: Optional[bytes] = await snes_read(ctx, L2AC_SIGN_ADDR, 16) if signature != b"ArchipelagoLufia": return # Goal if not ctx.finished_game: - goal_data: bytes = await snes_read(ctx, L2AC_GOAL_ADDR, 10) + goal_data: Optional[bytes] = await snes_read(ctx, L2AC_GOAL_ADDR, 10) if goal_data is not None and goal_data[goal_data[0]] == 0x01: await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) ctx.finished_game = True # DeathLink TX - death_data: bytes = await snes_read(ctx, L2AC_DEATH_ADDR, 3) + death_data: Optional[bytes] = await snes_read(ctx, L2AC_DEATH_ADDR, 3) if death_data is not None: await ctx.update_death_link(bool(death_data[0])) if death_data[1] != 0x00: snes_buffered_write(ctx, L2AC_DEATH_ADDR + 1, b"\x00") if "DeathLink" in ctx.tags and ctx.last_death_link + 1 < time.time(): player_name: str = ctx.player_names.get(ctx.slot, str(ctx.slot)) - enemy_name: str = enemy_names.get(death_data[1] - 1, hex(death_data[1] - 1)) + enemy_name: str = enemy_id_to_name.get(death_data[1] - 1, hex(death_data[1] - 1)) await ctx.send_death(f"{player_name} was totally defeated by {enemy_name}.") # TX - tx_data: bytes = await snes_read(ctx, L2AC_TX_ADDR, 8) + tx_data: Optional[bytes] = await snes_read(ctx, L2AC_TX_ADDR, 8) if tx_data is not None: snes_items_sent = int.from_bytes(tx_data[:2], "little") client_items_sent = int.from_bytes(tx_data[2:4], "little") @@ -316,7 +90,7 @@ class L2ACSNIClient(SNIClient): client_items_sent += 1 ctx.locations_checked.add(location_id) - await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}]) + await ctx.send_msgs([{"cmd": "LocationChecks", "locations": [location_id]}]) snes_logger.info("New Check: %s (%d/%d)" % ( location, @@ -329,7 +103,7 @@ class L2ACSNIClient(SNIClient): snes_buffered_write(ctx, L2AC_TX_ADDR + 4, ap_items_found.to_bytes(2, "little")) # RX - rx_data: bytes = await snes_read(ctx, L2AC_RX_ADDR, 4) + rx_data: Optional[bytes] = await snes_read(ctx, L2AC_RX_ADDR, 4) if rx_data is not None: snes_items_received = int.from_bytes(rx_data[:2], "little") @@ -343,7 +117,7 @@ class L2ACSNIClient(SNIClient): ctx.player_names[item.player], ctx.location_names[item.location], 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 + 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")) await snes_flush_writes(ctx) @@ -352,7 +126,7 @@ class L2ACSNIClient(SNIClient): from SNIClient import DeathState, snes_buffered_write, snes_flush_writes # DeathLink RX - if "DeathLink" in ctx.tags and ctx.last_death_link + 1 < time.time(): + if "DeathLink" in ctx.tags: snes_buffered_write(ctx, L2AC_DEATH_ADDR + 2, b"\x01") else: snes_buffered_write(ctx, L2AC_DEATH_ADDR + 2, b"\x00") diff --git a/worlds/lufia2ac/Enemies.py b/worlds/lufia2ac/Enemies.py new file mode 100644 index 0000000000..5d82966c0c --- /dev/null +++ b/worlds/lufia2ac/Enemies.py @@ -0,0 +1,382 @@ +from typing import Dict + +enemy_id_to_name: Dict[int, str] = { + 0x00: "a Goblin", + 0x01: "an Armor goblin", + 0x02: "a Regal Goblin", + 0x03: "a Goblin Mage", + 0x04: "a Troll", + 0x05: "an Ork", + 0x06: "a Fighter ork", + 0x07: "an Ork Mage", + 0x08: "a Lizardman", + 0x09: "a Skull Lizard", + 0x0A: "an Armour Dait", + 0x0B: "a Dragonian", + 0x0C: "a Cyclops", + 0x0D: "a Mega Cyclops", + 0x0E: "a Flame genie", + 0x0F: "a Well Genie", + 0x10: "a Wind Genie", + 0x11: "an Earth Genie", + 0x12: "a Cobalt", + 0x13: "a Merman", + 0x14: "an Aqualoi", + 0x15: "an Imp", + 0x16: "a Fiend", + 0x17: "an Archfiend", + 0x18: "a Hound", + 0x19: "a Doben", + 0x1A: "a Winger", + 0x1B: "a Serfaco", + 0x1C: "a Pug", + 0x1D: "a Salamander", + 0x1E: "a Brinz Lizard", + 0x1F: "a Seahorse", + 0x20: "a Seirein", + 0x21: "an Earth Viper", + 0x22: "a Gnome", + 0x23: "a Wispy", + 0x24: "a Thunderbeast", + 0x25: "a Lunar bear", + 0x26: "a Shadowfly", + 0x27: "a Shadow", + 0x28: "a Lion", + 0x29: "a Sphinx", + 0x2A: "a Mad horse", + 0x2B: "an Armor horse", + 0x2C: "a Buffalo", + 0x2D: "a Bruse", + 0x2E: "a Bat", + 0x2F: "a Big Bat", + 0x30: "a Red Bat", + 0x31: "an Eagle", + 0x32: "a Hawk", + 0x33: "a Crow", + 0x34: "a Baby Frog", + 0x35: "a King Frog", + 0x36: "a Lizard", + 0x37: "a Newt", + 0x38: "a Needle Lizard", + 0x39: "a Poison Lizard", + 0x3A: "a Medusa", + 0x3B: "a Ramia", + 0x3C: "a Basilisk", + 0x3D: "a Cokatoris", + 0x3E: "a Scorpion", + 0x3F: "an Antares", + 0x40: "a Small Crab", + 0x41: "a Big Crab", + 0x42: "a Red Lobster", + 0x43: "a Spider", + 0x44: "a Web Spider", + 0x45: "a Beetle", + 0x46: "a Poison Beetle", + 0x47: "a Mosquito", + 0x48: "a Coridras", + 0x49: "a Spinner", + 0x4A: "a Tartona", + 0x4B: "an Armour Nail", + 0x4C: "a Moth", + 0x4D: "a Mega Moth", + 0x4E: "a Big Bee", + 0x4F: "a Dark Fly", + 0x50: "a Stinger", + 0x51: "an Armor Bee", + 0x52: "a Sentopez", + 0x53: "a Cancer", + 0x54: "a Garbost", + 0x55: "a Bolt Fish", + 0x56: "a Moray", + 0x57: "a She Viper", + 0x58: "an Angler fish", + 0x59: "a Unicorn", + 0x5A: "an Evil Shell", + 0x5B: "a Drill Shell", + 0x5C: "a Snell", + 0x5D: "an Ammonite", + 0x5E: "an Evil Fish", + 0x5F: "a Squid", + 0x60: "a Kraken", + 0x61: "a Killer Whale", + 0x62: "a White Whale", + 0x63: "a Grianos", + 0x64: "a Behemoth", + 0x65: "a Perch", + 0x66: "a Current", + 0x67: "a Vampire Rose", + 0x68: "a Desert Rose", + 0x69: "a Venus Fly", + 0x6A: "a Moray Vine", + 0x6B: "a Torrent", + 0x6C: "a Mad Ent", + 0x6D: "a Crow Kelp", + 0x6E: "a Red Plant", + 0x6F: "La Fleshia", + 0x70: "a Wheel Eel", + 0x71: "a Skeleton", + 0x72: "a Ghoul", + 0x73: "a Zombie", + 0x74: "a Specter", + 0x75: "a Dark Spirit", + 0x76: "a Snatcher", + 0x77: "a Jurahan", + 0x78: "a Demise", + 0x79: "a Leech", + 0x7A: "a Necromancer", + 0x7B: "a Hade Chariot", + 0x7C: "a Hades", + 0x7D: "a Dark Skull", + 0x7E: "a Hades Skull", + 0x7F: "a Mummy", + 0x80: "a Vampire", + 0x81: "a Nosferato", + 0x82: "a Ghost Ship", + 0x83: "a Deadly Sword", + 0x84: "a Deadly Armor", + 0x85: "a T Rex", + 0x86: "a Brokion", + 0x87: "a Pumpkin Head", + 0x88: "a Mad Head", + 0x89: "a Snow Gas", + 0x8A: "a Great Coca", + 0x8B: "a Gargoyle", + 0x8C: "a Rogue Shape", + 0x8D: "a Bone Gorem", + 0x8E: "a Nuborg", + 0x8F: "a Wood Gorem", + 0x90: "a Mad Gorem", + 0x91: "a Green Clay", + 0x92: "a Sand Gorem", + 0x93: "a Magma Gorem", + 0x94: "an Iron Gorem", + 0x95: "a Gold Gorem", + 0x96: "a Hidora", + 0x97: "a Sea Hidora", + 0x98: "a High Hidora", + 0x99: "a King Hidora", + 0x9A: "an Orky", + 0x9B: "a Waiban", + 0x9C: "a White Dragon", + 0x9D: "a Red Dragon", + 0x9E: "a Blue Dragon", + 0x9F: "a Green Dragon", + 0xA0: "a Black Dragon", + 0xA1: "a Copper Dragon", + 0xA2: "a Silver Dragon", + 0xA3: "a Gold Dragon", + 0xA4: "a Red Jelly", + 0xA5: "a Blue Jelly", + 0xA6: "a Bili Jelly", + 0xA7: "a Red Core", + 0xA8: "a Blue Core", + 0xA9: "a Green Core", + 0xAA: "a No Core", + 0xAB: "a Mimic", + 0xAC: "a Blue Mimic", + 0xAD: "an Ice Roge", + 0xAE: "a Mushroom", + 0xAF: "a Big Mushr'm", + 0xB0: "a Minataurus", + 0xB1: "a Gorgon", + 0xB2: "a Ninja", + 0xB3: "an Asashin", + 0xB4: "a Samurai", + 0xB5: "a Dark Warrior", + 0xB6: "an Ochi Warrior", + 0xB7: "a Sly Fox", + 0xB8: "a Tengu", + 0xB9: "a Warm Eye", + 0xBA: "a Wizard", + 0xBB: "a Dark Sum'ner", + 0xBC: "the Big Catfish", + 0xBD: "a Follower", + 0xBE: "the Tarantula", + 0xBF: "Pierre", + 0xC0: "Daniele", + 0xC1: "the Venge Ghost", + 0xC2: "the Fire Dragon", + 0xC3: "the Tank", + 0xC4: "Idura", + 0xC5: "Camu", + 0xC6: "Gades", + 0xC7: "Amon", + 0xC8: "Erim", + 0xC9: "Daos", + 0xCA: "a Lizard Man", + 0xCB: "a Goblin", + 0xCC: "a Skeleton", + 0xCD: "a Regal Goblin", + 0xCE: "a Goblin", + 0xCF: "a Goblin Mage", + 0xD0: "a Slave", + 0xD1: "a Follower", + 0xD2: "a Groupie", + 0xD3: "the Egg Dragon", + 0xD4: "a Mummy", + 0xD5: "a Troll", + 0xD6: "Gades", + 0xD7: "Idura", + 0xD8: "a Lion", + 0xD9: "the Rogue Flower", + 0xDA: "a Gargoyle", + 0xDB: "a Ghost Ship", + 0xDC: "Idura", + 0xDD: "a Soldier", + 0xDE: "Gades", + 0xDF: "the Master", +} + +enemy_name_to_sprite: Dict[str, int] = { + "Ammonite": 0x81, + "Antares": 0x8B, + "Archfiend": 0xBD, + "Armor Bee": 0x98, + "Armor goblin": 0x9D, + "Armour Dait": 0xEF, + "Armour Nail": 0xEB, + "Asashin": 0x82, + "Baby Frog": 0xBE, + "Basilisk": 0xB6, + "Bat": 0x8F, + "Beetle": 0x86, + "Behemoth": 0xB6, + "Big Bat": 0x8F, + "Big Mushr'm": 0xDB, + "Bili Jelly": 0xDE, + "Black Dragon": 0xC0, + "Blue Core": 0x95, + "Blue Dragon": 0xC0, + "Blue Jelly": 0xDD, + "Blue Mimic": 0xF0, + "Bone Gorem": 0xA0, + "Brinz Lizard": 0xEE, + "Brokion": 0xD3, + "Buffalo": 0x84, + "Cobalt": 0xA6, + "Cokatoris": 0xD2, + "Copper Dragon": 0xC0, + "Coridras": 0xEA, + "Crow": 0xB4, + "Crow Kelp": 0xBC, + "Cyclops": 0xB9, + "Dark Skull": 0xB5, + "Dark Spirit": 0xE7, + "Dark Sum'ner": 0xAB, + "Dark Warrior": 0xB0, + "Deadly Armor": 0x99, + "Deadly Sword": 0x90, + "Demise": 0xAD, + "Desert Rose": 0x96, + "Dragonian": 0xEF, + "Drill Shell": 0x81, + "Eagle": 0xB4, + "Earth Genie": 0xB9, + "Earth Viper": 0xB3, + "Evil Fish": 0x80, + "Fiend": 0xBD, + "Fighter ork": 0xA5, + "Flame genie": 0xB9, + "Garbost": 0xD8, + "Ghost Ship": 0xD1, + "Ghoul": 0xE1, + "Gnome": 0xA5, + "Goblin": 0x9D, + "Gold Dragon": 0xC0, + "Gold Gorem": 0xE2, + "Gorgon": 0xAA, + "Great Coca": 0xD2, + "Green Core": 0x95, + "Green Dragon": 0xC0, + "Grianos": 0xB6, + "Hade Chariot": 0xBA, + "Hades": 0xBA, + "Hades Skull": 0xB5, + "Hidora": 0xBF, + "High Hidora": 0xBF, + "Hound": 0x8A, + "Ice Roge": 0xBD, + "Imp": 0xAC, + "Iron Gorem": 0xA1, + "Jurahan": 0xD5, + "Leech": 0xAD, + "Lion": 0xB7, + "Lizard": 0x83, + "Lizardman": 0x9E, + "Lunar bear": 0x9B, + "Mad Ent": 0x8E, + "Mad Gorem": 0xA3, + "Mad Head": 0xAF, + "Mad horse": 0x85, + "Magma Gorem": 0xE3, + "Medusa": 0x9C, + "Mega Moth": 0xDC, + "Mega Cyclops": 0xB9, + "Mimic": 0xA4, + "Minataurus": 0xAA, + "Moray Vine": 0x9A, + "Mosquito": 0x92, + "Moth": 0x93, + "Mummy": 0xA8, + "Mushroom": 0x8C, + "Necromancer": 0xAB, + "Needle Lizard": 0xD6, + "Newt": 0x83, + "Ninja": 0x82, + "No Core": 0x95, + "Nosferato": 0x9F, + "Nuborg": 0xE5, + "Ochi Warrior": 0xB0, + "Ork": 0xA5, + "Orky": 0xBF, + "Poison Beetle": 0xD7, + "Pug": 0x8D, + "Pumpkin Head": 0xAF, + "Ramia": 0xAE, + "Red Bat": 0x8F, + "Red Core": 0x95, + "Red Dragon": 0xC0, + "Red Jelly": 0x94, + "Red Plant": 0xEC, + "Regal Goblin": 0x9D, + "Rogue Shape": 0xC4, + "Salamander": 0xC1, + "Samurai": 0xB0, + "Sand Gorem": 0xE4, + "Scorpion": 0x8B, + "Sea Hidora": 0xBF, + "Seirein": 0xAE, + "Sentopez": 0xDA, + "Serfaco": 0xE8, + "Shadow": 0xB2, + "Silver Dragon": 0xC0, + "Skeleton": 0xA0, + "Skull Lizard": 0x9E, + "Sly Fox": 0xED, + "Snow Gas": 0xD2, + "Specter": 0xE7, + "Sphinx": 0xB7, + "Spider": 0xD9, + "Spinner": 0xE9, + "Squid": 0x80, + "Stinger": 0x98, + "T Rex": 0xD3, + "Tartona": 0xB8, + "Tengu": 0xD4, + "Thunderbeast": 0x9B, + "Troll": 0xA9, + "Vampire": 0x9F, + "Vampire Rose": 0x96, + "Venus Fly": 0xE0, + "Waiban": 0xC3, + "Warm Eye": 0x88, + "Well Genie": 0xB9, + "Wheel Eel": 0x97, + "White Dragon": 0xC3, + "Wind Genie": 0xB9, + "Winger": 0xB1, + "Wispy": 0x91, + "Wizard": 0xAB, + "Wood Gorem": 0xA2, + "Zombie": 0xA7, +} diff --git a/worlds/lufia2ac/Options.py b/worlds/lufia2ac/Options.py index 47b183c8b1..df71ef44a9 100644 --- a/worlds/lufia2ac/Options.py +++ b/worlds/lufia2ac/Options.py @@ -1,10 +1,16 @@ from __future__ import annotations import random -from itertools import chain, combinations -from typing import Any, cast, Dict, List, Optional, Set, Tuple +from dataclasses import dataclass +from itertools import accumulate, chain, combinations +from typing import Any, cast, Dict, Iterator, List, Mapping, Optional, Set, Tuple, Type, TYPE_CHECKING, Union -from Options import AssembleOptions, Choice, DeathLink, Option, Range, SpecialRange, TextChoice, Toggle +from Options import AssembleOptions, Choice, DeathLink, ItemDict, Range, SpecialRange, TextChoice, Toggle +from .Enemies import enemy_name_to_sprite + +if TYPE_CHECKING: + from BaseClasses import PlandoOptions + from worlds.AutoWorld import World class AssembleCustomizableChoices(AssembleOptions): @@ -37,6 +43,22 @@ class RandomGroupsChoice(Choice, metaclass=AssembleCustomizableChoices): return super().from_text(text) +class EnemyChoice(TextChoice): + _valid_sprites: Dict[str, int] = {enemy_name.lower(): sprite for enemy_name, sprite in enemy_name_to_sprite.items()} + + def verify(self, world: Type[World], player_name: str, plando_options: PlandoOptions) -> None: + if isinstance(self.value, int): + return + if str(self.value).lower() in self._valid_sprites: + return + raise ValueError(f"Could not find option '{self.value}' for '{self.__class__.__name__}', known options are:\n" + f"{', '.join(self.options)}, {', '.join(enemy_name_to_sprite)}.") + + @property + def sprite(self) -> Optional[int]: + return self._valid_sprites.get(str(self.value).lower()) + + class LevelMixin: xp_coefficients: List[int] = sorted([191, 65, 50, 32, 18, 14, 6, 3, 3, 2, 2, 2, 2] * 8, reverse=True) @@ -61,8 +83,7 @@ class BlueChestChance(Range): """The chance of a chest being a blue chest. It is given in units of 1/256, i.e., a value of 25 corresponds to 25/256 ~ 9.77%. - If you increase the blue chest chance, then the chance of finding consumables is decreased in return. - The chance of finding red chest equipment or spells is unaffected. + If you increase the blue chest chance, then the red chest chance is decreased in return. Supported values: 5 – 75 Default value: 25 (five times as much as in an unmodified game) """ @@ -72,6 +93,14 @@ class BlueChestChance(Range): range_end = 75 default = 25 + @property + def chest_type_thresholds(self) -> bytes: + ratio: float = (256 - self.value) / (256 - 5) + # unmodified chances are: consumable (mostly non-restorative) = 36/256, consumable (restorative) = 58/256, + # blue chest = 5/256, spell = 30/256, gear = 45/256 (and the remaining part, weapon = 82/256) + chest_type_chances: List[float] = [36 * ratio, 58 * ratio, float(self.value), 30 * ratio, 45 * ratio] + return bytes(round(threshold) for threshold in reversed(tuple(accumulate(chest_type_chances)))) + class BlueChestCount(Range): """The number of blue chest items that will be in your item pool. @@ -152,7 +181,7 @@ class Boss(RandomGroupsChoice): "random-high": ["venge_ghost", "white_dragon_x3", "fire_dragon", "ghost_ship", "tank"], "random-sinistral": ["gades_c", "amon", "erim", "daos"], } - extra_options = frozenset(random_groups) + extra_options = set(random_groups) @property def flag(self) -> int: @@ -242,6 +271,34 @@ class CrowdedFloorChance(Range): default = 16 +class CustomItemPool(ItemDict, Mapping[str, int]): + """Customize your multiworld item pool. + + Using this option you can place any cave item in your multiworld item pool. (By default, the pool is filled with + blue chest items.) Here you can add any valid item from the Lufia II Ancient Cave section of the datapackage + (see https://archipelago.gg/datapackage). The value of this option has to be a mapping of item name to count, + e.g., to add two Deadly rods and one Dekar Blade: {Deadly rod: 2, Dekar blade: 1} + The maximum total amount of custom items you can place is limited by the chosen blue_chest_count; any remaining, + non-customized space in the pool will be occupied by random blue chest items. + """ + + display_name = "Custom item pool" + value: Dict[str, int] + + @property + def count(self) -> int: + return sum(self.values()) + + def __getitem__(self, key: str) -> int: + return self.value.__getitem__(key) + + def __iter__(self) -> Iterator[str]: + return self.value.__iter__() + + def __len__(self) -> int: + return self.value.__len__() + + class DefaultCapsule(Choice): """Preselect the active capsule monster. @@ -277,7 +334,8 @@ class DefaultParty(RandomGroupsChoice, TextChoice): """ display_name = "Default party lineup" - default = "M" + default: Union[str, int] = "M" + value: Union[str, int] random_groups = { "random-2p": ["M" + "".join(p) for p in combinations("ADGLST", 1)], @@ -288,7 +346,7 @@ class DefaultParty(RandomGroupsChoice, TextChoice): _valid_sorted_parties: List[List[str]] = [sorted(party) for party in ("M", *chain(*random_groups.values()))] _members_to_bytes: bytes = bytes.maketrans(b"MSGATDL", bytes(range(7))) - def verify(self, *args, **kwargs) -> None: + def verify(self, world: Type[World], player_name: str, plando_options: PlandoOptions) -> None: if str(self.value).lower() in self.random_groups: return if sorted(str(self.value).upper()) in self._valid_sorted_parties: @@ -317,6 +375,97 @@ class DefaultParty(RandomGroupsChoice, TextChoice): return len(str(self.value)) +class EnemyFloorNumbers(Choice): + """Change which enemy types are encountered at which floor numbers. + + Supported values: + vanilla + Ninja, e.g., is allowed to appear on the 3 floors B44-B46 + shuffle — The existing enemy types are redistributed among nearby floors. Shifts by up to 6 floors are possible. + Ninja, e.g., will be allowed to appear on exactly 3 consecutive floors somewhere from B38-B40 to B50-B52 + randomize — For each floor, new enemy types are chosen randomly from the set usually possible on floors [-6, +6]. + Ninja, e.g., is among the various possible selections for any enemy slot affecting the floors from B38 to B52 + Default value: vanilla (same as in an unmodified game) + """ + + display_name = "Enemy floor numbers" + option_vanilla = 0 + option_shuffle = 1 + option_randomize = 2 + default = option_vanilla + + +class EnemyMovementPatterns(EnemyChoice): + """Change the movement patterns of enemies. + + Supported values: + vanilla + shuffle_by_pattern — The existing movement patterns are redistributed among each other. + Sprites that usually share a movement pattern will still share movement patterns after shuffling + randomize_by_pattern — For each movement pattern, a new one is chosen randomly from the set of existing patterns. + Sprites that usually share a movement pattern will still share movement patterns after randomizing + shuffle_by_sprite — The existing movement patterns of sprites are redistributed among the enemy sprites. + Sprites that usually share a movement pattern can end up with different movement patterns after shuffling + randomize_by_sprite — For each sprite, a new movement is chosen randomly from the set of existing patterns. + Sprites that usually share a movement pattern can end up with different movement patterns after randomizing + singularity — All enemy sprites use the same, randomly selected movement pattern + Alternatively, you can directly specify an enemy name such as "Red Jelly" as the value of this option. + In that case, the movement pattern usually associated with this sprite will be used by all enemy sprites + Default value: vanilla (same as in an unmodified game) + """ + + display_name = "Enemy movement patterns" + option_vanilla = 0 + option_shuffle_by_pattern = 1 + option_randomize_by_pattern = 2 + option_shuffle_by_sprite = 3 + option_randomize_by_sprite = 4 + option_singularity = 5 + default = option_vanilla + + +class EnemySprites(EnemyChoice): + """Change the appearance of enemies. + + Supported values: + vanilla + shuffle — The existing sprites are redistributed among the enemy types. + This means that, after shuffling, exactly 1 enemy type will be dressing up as the "Red Jelly" sprite + randomize — For each enemy type, a new sprite is chosen randomly from the set of existing sprites. + This means that, after randomizing, any number of enemy types could end up using the "Red Jelly" sprite + singularity — All enemies use the same, randomly selected sprite + Alternatively, you can directly specify an enemy name such as "Red Jelly" as the value of this option. + In this case, the sprite usually associated with that enemy will be used by all enemies + Default value: vanilla (same as in an unmodified game) + """ + + display_name = "Enemy sprites" + option_vanilla = 0 + option_shuffle = 1 + option_randomize = 2 + option_singularity = 3 + default = option_vanilla + + +class ExpModifier(Range): + """Percentage modifier for EXP gained from enemies. + + Supported values: 100 – 500 + Default value: 100 (same as in an unmodified game) + """ + + display_name = "EXP modifier" + range_start = 100 + range_end = 500 + default = 100 + + def __call__(self, exp: bytes) -> bytes: + try: + return (int.from_bytes(exp, "little") * self.value // 100).to_bytes(2, "little") + except OverflowError: + return b"\xFF\xFF" + + class FinalFloor(Range): """The final floor, where the boss resides. @@ -424,28 +573,18 @@ class IrisTreasuresRequired(Range): default = 9 -class MasterHp(SpecialRange): +class MasterHp(Range): """The number of hit points of the Master - Supported values: - 1 – 9980, - scale — scales the HP depending on the value of final_floor + (Only has an effect if boss is set to master.) + Supported values: 1 – 9980 Default value: 9980 (same as in an unmodified game) """ display_name = "Master HP" - range_start = 0 + range_start = 1 range_end = 9980 default = 9980 - special_range_cutoff = 1 - special_range_names = { - "default": 9980, - "scale": 0, - } - - @staticmethod - def scale(final_floor: int) -> int: - return final_floor * 100 + 80 class PartyStartingLevel(LevelMixin, Range): @@ -503,7 +642,8 @@ class ShufflePartyMembers(Toggle): Supported values: false — all 6 optional party members are present in the cafe and can be recruited right away true — only Maxim is available from the start; 6 new "items" are added to your pool and shuffled into the - multiworld; when one of these items is found, the corresponding party member is unlocked for you to use + multiworld; when one of these items is found, the corresponding party member is unlocked for you to use. + While cave diving, you can add newly unlocked ones to your party by using the character items from the inventory Default value: false (same as in an unmodified game) """ @@ -514,27 +654,32 @@ class ShufflePartyMembers(Toggle): return 0b00000000 if self.value else 0b11111100 -l2ac_option_definitions: Dict[str, type(Option)] = { - "blue_chest_chance": BlueChestChance, - "blue_chest_count": BlueChestCount, - "boss": Boss, - "capsule_cravings_jp_style": CapsuleCravingsJPStyle, - "capsule_starting_form": CapsuleStartingForm, - "capsule_starting_level": CapsuleStartingLevel, - "crowded_floor_chance": CrowdedFloorChance, - "death_link": DeathLink, - "default_capsule": DefaultCapsule, - "default_party": DefaultParty, - "final_floor": FinalFloor, - "gear_variety_after_b9": GearVarietyAfterB9, - "goal": Goal, - "healing_floor_chance": HealingFloorChance, - "initial_floor": InitialFloor, - "iris_floor_chance": IrisFloorChance, - "iris_treasures_required": IrisTreasuresRequired, - "master_hp": MasterHp, - "party_starting_level": PartyStartingLevel, - "run_speed": RunSpeed, - "shuffle_capsule_monsters": ShuffleCapsuleMonsters, - "shuffle_party_members": ShufflePartyMembers, -} +@dataclass +class L2ACOptions: + blue_chest_chance: BlueChestChance + blue_chest_count: BlueChestCount + boss: Boss + capsule_cravings_jp_style: CapsuleCravingsJPStyle + capsule_starting_form: CapsuleStartingForm + capsule_starting_level: CapsuleStartingLevel + crowded_floor_chance: CrowdedFloorChance + custom_item_pool: CustomItemPool + death_link: DeathLink + default_capsule: DefaultCapsule + default_party: DefaultParty + enemy_floor_numbers: EnemyFloorNumbers + enemy_movement_patterns: EnemyMovementPatterns + enemy_sprites: EnemySprites + exp_modifier: ExpModifier + final_floor: FinalFloor + gear_variety_after_b9: GearVarietyAfterB9 + goal: Goal + healing_floor_chance: HealingFloorChance + initial_floor: InitialFloor + iris_floor_chance: IrisFloorChance + iris_treasures_required: IrisTreasuresRequired + master_hp: MasterHp + party_starting_level: PartyStartingLevel + run_speed: RunSpeed + shuffle_capsule_monsters: ShuffleCapsuleMonsters + shuffle_party_members: ShufflePartyMembers diff --git a/worlds/lufia2ac/Rom.py b/worlds/lufia2ac/Rom.py index ac168c8864..1da8d235a6 100644 --- a/worlds/lufia2ac/Rom.py +++ b/worlds/lufia2ac/Rom.py @@ -22,15 +22,15 @@ class L2ACDeltaPatch(APDeltaPatch): def get_base_rom_bytes(file_name: str = "") -> bytes: base_rom_bytes: Optional[bytes] = getattr(get_base_rom_bytes, "base_rom_bytes", None) if not base_rom_bytes: - file_name: str = get_base_rom_path(file_name) - base_rom_bytes = bytes(Utils.read_snes_rom(open(file_name, "rb"))) + file_path: str = get_base_rom_path(file_name) + base_rom_bytes = bytes(Utils.read_snes_rom(open(file_path, "rb"))) basemd5 = hashlib.md5() basemd5.update(base_rom_bytes) if L2USHASH != basemd5.hexdigest(): raise Exception("Supplied Base Rom does not match known MD5 for US release. " "Get the correct game and version, then dump it") - get_base_rom_bytes.base_rom_bytes = base_rom_bytes + setattr(get_base_rom_bytes, "base_rom_bytes", base_rom_bytes) return base_rom_bytes diff --git a/worlds/lufia2ac/Utils.py b/worlds/lufia2ac/Utils.py new file mode 100644 index 0000000000..6c2e28d137 --- /dev/null +++ b/worlds/lufia2ac/Utils.py @@ -0,0 +1,21 @@ +from random import Random +from typing import Dict, List, MutableSequence, Sequence, Set, Tuple + + +def constrained_choices(population: Sequence[int], d: int, *, k: int, random: Random) -> List[int]: + n: int = len(population) + constraints: Dict[int, Tuple[int, ...]] = { + i: tuple(dict.fromkeys(population[j] for j in range(max(0, i - d), min(i + d + 1, n)))) for i in range(n) + } + + return [random.choice(constraints[i]) for i in range(k)] + + +def constrained_shuffle(x: MutableSequence[int], d: int, random: Random) -> None: + n: int = len(x) + constraints: Dict[int, Set[int]] = {i: set(x[j] for j in range(max(0, i - d), min(i + d + 1, n))) for i in range(n)} + + for _ in range(d * n * n): + i, j = random.randrange(n), random.randrange(n) + if x[i] in constraints[j] and x[j] in constraints[i]: + x[i], x[j] = x[j], x[i] diff --git a/worlds/lufia2ac/__init__.py b/worlds/lufia2ac/__init__.py index e54f84283d..587792a58d 100644 --- a/worlds/lufia2ac/__init__.py +++ b/worlds/lufia2ac/__init__.py @@ -2,19 +2,21 @@ import base64 import itertools import os from enum import IntFlag -from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple +from random import Random +from typing import Any, ClassVar, Dict, get_type_hints, Iterator, List, Set, Tuple from BaseClasses import Entrance, Item, ItemClassification, MultiWorld, Region, Tutorial -from Main import __version__ from Options import AssembleOptions +from Utils import __version__ from worlds.AutoWorld import WebWorld, World from worlds.generic.Rules import add_rule, set_rule from .Client import L2ACSNIClient # noqa: F401 from .Items import ItemData, ItemType, l2ac_item_name_to_id, l2ac_item_table, L2ACItem, start_id as items_start_id from .Locations import l2ac_location_name_to_id, L2ACLocation -from .Options import Boss, CapsuleStartingForm, CapsuleStartingLevel, DefaultParty, Goal, l2ac_option_definitions, \ - MasterHp, PartyStartingLevel, ShuffleCapsuleMonsters, ShufflePartyMembers +from .Options import CapsuleStartingLevel, DefaultParty, EnemyFloorNumbers, EnemyMovementPatterns, EnemySprites, \ + ExpModifier, Goal, L2ACOptions from .Rom import get_base_rom_bytes, get_base_rom_path, L2ACDeltaPatch +from .Utils import constrained_choices, constrained_shuffle from .basepatch import apply_basepatch CHESTS_PER_SPHERE: int = 5 @@ -42,7 +44,7 @@ class L2ACWorld(World): game: ClassVar[str] = "Lufia II Ancient Cave" web: ClassVar[WebWorld] = L2ACWeb() - option_definitions: ClassVar[Dict[str, AssembleOptions]] = l2ac_option_definitions + option_definitions: ClassVar[Dict[str, AssembleOptions]] = get_type_hints(L2ACOptions) item_name_to_id: ClassVar[Dict[str, int]] = l2ac_item_name_to_id location_name_to_id: ClassVar[Dict[str, int]] = l2ac_location_name_to_id item_name_groups: ClassVar[Dict[str, Set[str]]] = { @@ -54,30 +56,8 @@ class L2ACWorld(World): required_client_version: Tuple[int, int, int] = (0, 3, 6) # L2ACWorld specific properties - rom_name: Optional[bytearray] - - blue_chest_chance: Optional[int] - blue_chest_count: Optional[int] - boss: Optional[Boss] - capsule_cravings_jp_style: Optional[int] - capsule_starting_form: Optional[CapsuleStartingForm] - capsule_starting_level: Optional[CapsuleStartingLevel] - crowded_floor_chance: Optional[int] - death_link: Optional[int] - default_capsule: Optional[int] - default_party: Optional[DefaultParty] - final_floor: Optional[int] - gear_variety_after_b9: Optional[int] - goal: Optional[int] - healing_floor_chance: Optional[int] - initial_floor: Optional[int] - iris_floor_chance: Optional[int] - iris_treasures_required: Optional[int] - master_hp: Optional[int] - party_starting_level: Optional[PartyStartingLevel] - run_speed: Optional[int] - shuffle_capsule_monsters: Optional[ShuffleCapsuleMonsters] - shuffle_party_members: Optional[ShufflePartyMembers] + rom_name: bytearray + o: L2ACOptions @classmethod def stage_assert_generate(cls, multiworld: MultiWorld) -> None: @@ -95,37 +75,17 @@ class L2ACWorld(World): bytearray(f"L2AC{__version__.replace('.', '')[:3]}_{self.player}_{self.multiworld.seed}", "utf8")[:21] self.rom_name.extend([0] * (21 - len(self.rom_name))) - self.blue_chest_chance = self.multiworld.blue_chest_chance[self.player].value - self.blue_chest_count = self.multiworld.blue_chest_count[self.player].value - self.boss = self.multiworld.boss[self.player] - self.capsule_cravings_jp_style = self.multiworld.capsule_cravings_jp_style[self.player].value - self.capsule_starting_form = self.multiworld.capsule_starting_form[self.player] - self.capsule_starting_level = self.multiworld.capsule_starting_level[self.player] - self.crowded_floor_chance = self.multiworld.crowded_floor_chance[self.player].value - self.death_link = self.multiworld.death_link[self.player].value - self.default_capsule = self.multiworld.default_capsule[self.player].value - self.default_party = self.multiworld.default_party[self.player] - self.final_floor = self.multiworld.final_floor[self.player].value - self.gear_variety_after_b9 = self.multiworld.gear_variety_after_b9[self.player].value - self.goal = self.multiworld.goal[self.player].value - self.healing_floor_chance = self.multiworld.healing_floor_chance[self.player].value - self.initial_floor = self.multiworld.initial_floor[self.player].value - self.iris_floor_chance = self.multiworld.iris_floor_chance[self.player].value - self.iris_treasures_required = self.multiworld.iris_treasures_required[self.player].value - self.master_hp = self.multiworld.master_hp[self.player].value - self.party_starting_level = self.multiworld.party_starting_level[self.player] - self.run_speed = self.multiworld.run_speed[self.player].value - self.shuffle_capsule_monsters = self.multiworld.shuffle_capsule_monsters[self.player] - self.shuffle_party_members = self.multiworld.shuffle_party_members[self.player] + self.o = L2ACOptions(**{opt: getattr(self.multiworld, opt)[self.player] for opt in self.option_definitions}) - if self.capsule_starting_level.value == CapsuleStartingLevel.special_range_names["party_starting_level"]: - self.capsule_starting_level.value = self.party_starting_level.value - if self.initial_floor >= self.final_floor: - self.initial_floor = self.final_floor - 1 - if self.master_hp == MasterHp.special_range_names["scale"]: - self.master_hp = MasterHp.scale(self.final_floor) - if self.shuffle_party_members: - self.default_party.value = DefaultParty.default + if self.o.blue_chest_count < self.o.custom_item_pool.count: + raise ValueError(f"Number of items in custom_item_pool ({self.o.custom_item_pool.count}) is " + f"greater than blue_chest_count ({self.o.blue_chest_count}).") + if self.o.capsule_starting_level == CapsuleStartingLevel.special_range_names["party_starting_level"]: + self.o.capsule_starting_level.value = int(self.o.party_starting_level) + if self.o.initial_floor >= self.o.final_floor: + self.o.initial_floor.value = self.o.final_floor - 1 + if self.o.shuffle_party_members: + self.o.default_party.value = DefaultParty.default def create_regions(self) -> None: menu = Region("Menu", self.player, self.multiworld) @@ -134,10 +94,10 @@ class L2ACWorld(World): ancient_dungeon = Region("AncientDungeon", self.player, self.multiworld, "Ancient Dungeon") ancient_dungeon.exits.append(Entrance(self.player, "FinalFloorEntrance", ancient_dungeon)) - item_count: int = self.blue_chest_count - if self.shuffle_capsule_monsters: + item_count: int = int(self.o.blue_chest_count) + if self.o.shuffle_capsule_monsters: item_count += len(self.item_name_groups["Capsule monsters"]) - if self.shuffle_party_members: + if self.o.shuffle_party_members: item_count += len(self.item_name_groups["Party members"]) for location_name, location_id in itertools.islice(l2ac_location_name_to_id.items(), item_count): ancient_dungeon.locations.append(L2ACLocation(self.player, location_name, location_id, ancient_dungeon)) @@ -167,21 +127,23 @@ class L2ACWorld(World): .connect(self.multiworld.get_region("FinalFloor", self.player)) def create_items(self) -> None: - item_pool: List[str] = \ - self.multiworld.random.choices(sorted(self.item_name_groups["Blue chest items"]), k=self.blue_chest_count) - if self.shuffle_capsule_monsters: + item_pool: List[str] = self.multiworld.random.choices(sorted(self.item_name_groups["Blue chest items"]), + k=self.o.blue_chest_count - self.o.custom_item_pool.count) + item_pool += [item_name for item_name, count in self.o.custom_item_pool.items() for _ in range(count)] + + if self.o.shuffle_capsule_monsters: item_pool += self.item_name_groups["Capsule monsters"] - self.blue_chest_count += len(self.item_name_groups["Capsule monsters"]) - if self.shuffle_party_members: + self.o.blue_chest_count.value += len(self.item_name_groups["Capsule monsters"]) + if self.o.shuffle_party_members: item_pool += self.item_name_groups["Party members"] - self.blue_chest_count += len(self.item_name_groups["Party members"]) + self.o.blue_chest_count.value += len(self.item_name_groups["Party members"]) for item_name in item_pool: item_data: ItemData = l2ac_item_table[item_name] item_id: int = items_start_id + item_data.code self.multiworld.itempool.append(L2ACItem(item_name, item_data.classification, item_id, self.player)) def set_rules(self) -> None: - for i in range(1, self.blue_chest_count): + for i in range(1, self.o.blue_chest_count): if i % CHESTS_PER_SPHERE == 0: set_rule(self.multiworld.get_location(f"Blue chest {i + 1}", self.player), lambda state, j=i: state.has("Progressive chest access", self.player, j // CHESTS_PER_SPHERE)) @@ -192,27 +154,27 @@ class L2ACWorld(World): lambda state, j=i: state.can_reach(f"Blue chest {j}", "Location", self.player)) set_rule(self.multiworld.get_entrance("FinalFloorEntrance", self.player), - lambda state: state.can_reach(f"Blue chest {self.blue_chest_count}", "Location", self.player)) + lambda state: state.can_reach(f"Blue chest {self.o.blue_chest_count}", "Location", self.player)) set_rule(self.multiworld.get_location("Iris Treasures", self.player), - lambda state: state.can_reach(f"Blue chest {self.blue_chest_count}", "Location", self.player)) + lambda state: state.can_reach(f"Blue chest {self.o.blue_chest_count}", "Location", self.player)) set_rule(self.multiworld.get_location("Boss", self.player), - lambda state: state.can_reach(f"Blue chest {self.blue_chest_count}", "Location", self.player)) - if self.shuffle_capsule_monsters: + lambda state: state.can_reach(f"Blue chest {self.o.blue_chest_count}", "Location", self.player)) + if self.o.shuffle_capsule_monsters: add_rule(self.multiworld.get_location("Boss", self.player), lambda state: state.has("DARBI", self.player)) - if self.shuffle_party_members: + if self.o.shuffle_party_members: add_rule(self.multiworld.get_location("Boss", self.player), lambda state: state.has("Dekar", self.player) and state.has("Guy", self.player) and state.has("Arty", self.player)) - if self.goal == Goal.option_final_floor: + if self.o.goal == Goal.option_final_floor: self.multiworld.completion_condition[self.player] = \ lambda state: state.has("Final Floor access", self.player) - elif self.goal == Goal.option_iris_treasure_hunt: + elif self.o.goal == Goal.option_iris_treasure_hunt: self.multiworld.completion_condition[self.player] = \ lambda state: state.has("Treasures collected", self.player) - elif self.goal == Goal.option_boss: + elif self.o.goal == Goal.option_boss: self.multiworld.completion_condition[self.player] = \ lambda state: state.has("Boss victory", self.player) - elif self.goal == Goal.option_boss_iris_treasure_hunt: + elif self.o.goal == Goal.option_boss_iris_treasure_hunt: self.multiworld.completion_condition[self.player] = \ lambda state: state.has("Boss victory", self.player) and state.has("Treasures collected", self.player) @@ -223,39 +185,45 @@ class L2ACWorld(World): rom_bytearray = bytearray(apply_basepatch(get_base_rom_bytes())) # start and stop indices are offsets in the ROM file, not LoROM mapped SNES addresses rom_bytearray[0x007FC0:0x007FC0 + 21] = self.rom_name - rom_bytearray[0x014308:0x014308 + 1] = self.capsule_starting_level.value.to_bytes(1, "little") - rom_bytearray[0x01432F:0x01432F + 1] = self.capsule_starting_form.unlock.to_bytes(1, "little") - rom_bytearray[0x01433C:0x01433C + 1] = self.capsule_starting_form.value.to_bytes(1, "little") - rom_bytearray[0x0190D5:0x0190D5 + 1] = self.iris_floor_chance.to_bytes(1, "little") - rom_bytearray[0x019153:0x019153 + 1] = (0x63 - self.blue_chest_chance).to_bytes(1, "little") - rom_bytearray[0x019176] = 0x38 if self.gear_variety_after_b9 else 0x18 - rom_bytearray[0x019477:0x019477 + 1] = self.healing_floor_chance.to_bytes(1, "little") - rom_bytearray[0x0194A2:0x0194A2 + 1] = self.crowded_floor_chance.to_bytes(1, "little") - rom_bytearray[0x019E82:0x019E82 + 1] = self.final_floor.to_bytes(1, "little") - rom_bytearray[0x01FC75:0x01FC75 + 1] = self.run_speed.to_bytes(1, "little") - rom_bytearray[0x01FC81:0x01FC81 + 1] = self.run_speed.to_bytes(1, "little") - rom_bytearray[0x02B2A1:0x02B2A1 + 5] = self.default_party.roster + rom_bytearray[0x014308:0x014308 + 1] = self.o.capsule_starting_level.value.to_bytes(1, "little") + rom_bytearray[0x01432F:0x01432F + 1] = self.o.capsule_starting_form.unlock.to_bytes(1, "little") + rom_bytearray[0x01433C:0x01433C + 1] = self.o.capsule_starting_form.value.to_bytes(1, "little") + rom_bytearray[0x0190D5:0x0190D5 + 1] = self.o.iris_floor_chance.value.to_bytes(1, "little") + rom_bytearray[0x019147:0x019157 + 1:4] = self.o.blue_chest_chance.chest_type_thresholds + rom_bytearray[0x019176] = 0x38 if self.o.gear_variety_after_b9 else 0x18 + rom_bytearray[0x019477:0x019477 + 1] = self.o.healing_floor_chance.value.to_bytes(1, "little") + rom_bytearray[0x0194A2:0x0194A2 + 1] = self.o.crowded_floor_chance.value.to_bytes(1, "little") + rom_bytearray[0x019E82:0x019E82 + 1] = self.o.final_floor.value.to_bytes(1, "little") + rom_bytearray[0x01FC75:0x01FC75 + 1] = self.o.run_speed.value.to_bytes(1, "little") + rom_bytearray[0x01FC81:0x01FC81 + 1] = self.o.run_speed.value.to_bytes(1, "little") + rom_bytearray[0x02B2A1:0x02B2A1 + 5] = self.o.default_party.roster for offset in range(0x02B395, 0x02B452, 0x1B): - rom_bytearray[offset:offset + 1] = self.party_starting_level.value.to_bytes(1, "little") + rom_bytearray[offset:offset + 1] = self.o.party_starting_level.value.to_bytes(1, "little") for offset in range(0x02B39A, 0x02B457, 0x1B): - rom_bytearray[offset:offset + 3] = self.party_starting_level.xp.to_bytes(3, "little") + rom_bytearray[offset:offset + 3] = self.o.party_starting_level.xp.to_bytes(3, "little") rom_bytearray[0x05699E:0x05699E + 147] = self.get_goal_text_bytes() - rom_bytearray[0x056AA3:0x056AA3 + 24] = self.default_party.event_script - rom_bytearray[0x072742:0x072742 + 1] = self.boss.value.to_bytes(1, "little") - rom_bytearray[0x072748:0x072748 + 1] = self.boss.flag.to_bytes(1, "little") + rom_bytearray[0x056AA3:0x056AA3 + 24] = self.o.default_party.event_script + rom_bytearray[0x072742:0x072742 + 1] = self.o.boss.value.to_bytes(1, "little") + rom_bytearray[0x072748:0x072748 + 1] = self.o.boss.flag.to_bytes(1, "little") rom_bytearray[0x09D59B:0x09D59B + 256] = self.get_node_connection_table() - rom_bytearray[0x0B4F02:0x0B4F02 + 2] = self.master_hp.to_bytes(2, "little") - rom_bytearray[0x280010:0x280010 + 2] = self.blue_chest_count.to_bytes(2, "little") - rom_bytearray[0x280012:0x280012 + 3] = self.capsule_starting_level.xp.to_bytes(3, "little") - rom_bytearray[0x280015:0x280015 + 1] = self.initial_floor.to_bytes(1, "little") - rom_bytearray[0x280016:0x280016 + 1] = self.default_capsule.to_bytes(1, "little") - rom_bytearray[0x280017:0x280017 + 1] = self.iris_treasures_required.to_bytes(1, "little") - rom_bytearray[0x280018:0x280018 + 1] = self.shuffle_party_members.unlock.to_bytes(1, "little") - rom_bytearray[0x280019:0x280019 + 1] = self.shuffle_capsule_monsters.unlock.to_bytes(1, "little") - rom_bytearray[0x280030:0x280030 + 1] = self.goal.to_bytes(1, "little") - rom_bytearray[0x28003D:0x28003D + 1] = self.death_link.to_bytes(1, "little") + rom_bytearray[0x0B05C0:0x0B05C0 + 18843] = self.get_enemy_stats() + rom_bytearray[0x0B4F02:0x0B4F02 + 2] = self.o.master_hp.value.to_bytes(2, "little") + rom_bytearray[0x280010:0x280010 + 2] = self.o.blue_chest_count.value.to_bytes(2, "little") + rom_bytearray[0x280012:0x280012 + 3] = self.o.capsule_starting_level.xp.to_bytes(3, "little") + rom_bytearray[0x280015:0x280015 + 1] = self.o.initial_floor.value.to_bytes(1, "little") + rom_bytearray[0x280016:0x280016 + 1] = self.o.default_capsule.value.to_bytes(1, "little") + rom_bytearray[0x280017:0x280017 + 1] = self.o.iris_treasures_required.value.to_bytes(1, "little") + rom_bytearray[0x280018:0x280018 + 1] = self.o.shuffle_party_members.unlock.to_bytes(1, "little") + rom_bytearray[0x280019:0x280019 + 1] = self.o.shuffle_capsule_monsters.unlock.to_bytes(1, "little") + rom_bytearray[0x280030:0x280030 + 1] = self.o.goal.value.to_bytes(1, "little") + rom_bytearray[0x28003D:0x28003D + 1] = self.o.death_link.value.to_bytes(1, "little") rom_bytearray[0x281200:0x281200 + 470] = self.get_capsule_cravings_table() + (rom_bytearray[0x08A1D4:0x08A1D4 + 128], + rom_bytearray[0x0A595C:0x0A595C + 200], + rom_bytearray[0x0A5DF6:0x0A5DF6 + 192], + rom_bytearray[0x27F6B5:0x27F6B5 + 113]) = self.get_enemy_floors_sprites_and_movement_patterns() + with open(rom_path, "wb") as f: f.write(rom_bytearray) except Exception as e: @@ -276,13 +244,19 @@ class L2ACWorld(World): # end of ordered Main.py calls def create_item(self, name: str) -> Item: - item_data: ItemData = l2ac_item_table.get(name) + item_data: ItemData = l2ac_item_table[name] return L2ACItem(name, item_data.classification, items_start_id + item_data.code, self.player) + def get_filler_item_name(self) -> str: + return ["Potion", "Hi-Magic", "Miracle", "Hi-Potion", "Potion", "Ex-Potion", "Regain", "Ex-Magic", "Hi-Magic"][ + (self.multiworld.random.randrange(9) + self.multiworld.random.randrange(9)) // 2] + + # end of overridden AutoWorld.py methods + def get_capsule_cravings_table(self) -> bytes: rom: bytes = get_base_rom_bytes() - if self.capsule_cravings_jp_style: + if self.o.capsule_cravings_jp_style: number_of_items: int = 467 items_offset: int = 0x0B4F69 value_thresholds: List[int] = \ @@ -307,17 +281,92 @@ class L2ACWorld(World): else: return rom[0x0AFF16:0x0AFF16 + 470] + def get_enemy_floors_sprites_and_movement_patterns(self) -> Tuple[bytes, bytes, bytes, bytes]: + rom: bytes = get_base_rom_bytes() + + if self.o.enemy_floor_numbers == EnemyFloorNumbers.default \ + and self.o.enemy_sprites == EnemySprites.default \ + and self.o.enemy_movement_patterns == EnemyMovementPatterns.default: + return rom[0x08A1D4:0x08A1D4 + 128], rom[0x0A595C:0x0A595C + 200], \ + rom[0x0A5DF6:0x0A5DF6 + 192], rom[0x27F6B5:0x27F6B5 + 113] + + formations: bytes = rom[0x0A595C:0x0A595C + 200] + sprites: bytes = rom[0x0A5DF6:0x0A5DF6 + 192] + indices: bytes = rom[0x27F6B5:0x27F6B5 + 113] + pointers: List[bytes] = [rom[0x08A1D4 + 2 * index:0x08A1D4 + 2 * index + 2] for index in range(64)] + + used_formations: List[int] = list(formations) + formation_set: Set[int] = set(used_formations) + used_sprites: List[int] = [sprite for formation, sprite in enumerate(sprites) if formation in formation_set] + sprite_set: Set[int] = set(used_sprites) + used_indices: List[int] = [index for sprite, index in enumerate(indices, 128) if sprite in sprite_set] + index_set: Set[int] = set(used_indices) + used_pointers: List[bytes] = [pointer for index, pointer in enumerate(pointers) if index in index_set] + + slot_random: Random = self.multiworld.per_slot_randoms[self.player] + + d: int = 2 * 6 + if self.o.enemy_floor_numbers == EnemyFloorNumbers.option_shuffle: + constrained_shuffle(used_formations, d, random=slot_random) + elif self.o.enemy_floor_numbers == EnemyFloorNumbers.option_randomize: + used_formations = constrained_choices(used_formations, d, k=len(used_formations), random=slot_random) + + if self.o.enemy_sprites == EnemySprites.option_shuffle: + slot_random.shuffle(used_sprites) + elif self.o.enemy_sprites == EnemySprites.option_randomize: + used_sprites = slot_random.choices(tuple(dict.fromkeys(used_sprites)), k=len(used_sprites)) + elif self.o.enemy_sprites == EnemySprites.option_singularity: + used_sprites = [slot_random.choice(tuple(dict.fromkeys(used_sprites)))] * len(used_sprites) + elif self.o.enemy_sprites.sprite: + used_sprites = [self.o.enemy_sprites.sprite] * len(used_sprites) + + if self.o.enemy_movement_patterns == EnemyMovementPatterns.option_shuffle_by_pattern: + slot_random.shuffle(used_pointers) + elif self.o.enemy_movement_patterns == EnemyMovementPatterns.option_randomize_by_pattern: + used_pointers = slot_random.choices(tuple(dict.fromkeys(used_pointers)), k=len(used_pointers)) + elif self.o.enemy_movement_patterns == EnemyMovementPatterns.option_shuffle_by_sprite: + slot_random.shuffle(used_indices) + elif self.o.enemy_movement_patterns == EnemyMovementPatterns.option_randomize_by_sprite: + used_indices = slot_random.choices(tuple(dict.fromkeys(used_indices)), k=len(used_indices)) + elif self.o.enemy_movement_patterns == EnemyMovementPatterns.option_singularity: + used_indices = [slot_random.choice(tuple(dict.fromkeys(used_indices)))] * len(used_indices) + elif self.o.enemy_movement_patterns.sprite: + used_indices = [indices[self.o.enemy_movement_patterns.sprite - 128]] * len(used_indices) + + sprite_iter: Iterator[int] = iter(used_sprites) + index_iter: Iterator[int] = iter(used_indices) + pointer_iter: Iterator[bytes] = iter(used_pointers) + formations = bytes(used_formations) + sprites = bytes(next(sprite_iter) if form in formation_set else sprite for form, sprite in enumerate(sprites)) + indices = bytes(next(index_iter) if sprite in sprite_set else idx for sprite, idx in enumerate(indices, 128)) + pointers = [next(pointer_iter) if idx in index_set else pointer for idx, pointer in enumerate(pointers)] + return b"".join(pointers), formations, sprites, indices + + def get_enemy_stats(self) -> bytes: + rom: bytes = get_base_rom_bytes() + + if self.o.exp_modifier == ExpModifier.default: + return rom[0x0B05C0:0x0B05C0 + 18843] + + number_of_enemies: int = 224 + enemy_stats = bytearray(rom[0x0B05C0:0x0B05C0 + 18843]) + + for enemy_id in range(number_of_enemies): + pointer: int = int.from_bytes(enemy_stats[2 * enemy_id:2 * enemy_id + 2], "little") + enemy_stats[pointer + 29:pointer + 31] = self.o.exp_modifier(enemy_stats[pointer + 29:pointer + 31]) + return enemy_stats + def get_goal_text_bytes(self) -> bytes: goal_text: List[str] = [] - iris: str = f"{self.iris_treasures_required} Iris treasure{'s' if self.iris_treasures_required > 1 else ''}" - if self.goal == Goal.option_boss: - goal_text = ["You have to defeat", f"the boss on B{self.final_floor}."] - elif self.goal == Goal.option_iris_treasure_hunt: + iris: str = f"{self.o.iris_treasures_required} Iris treasure{'s' if self.o.iris_treasures_required > 1 else ''}" + if self.o.goal == Goal.option_boss: + goal_text = ["You have to defeat", f"the boss on B{self.o.final_floor}."] + elif self.o.goal == Goal.option_iris_treasure_hunt: goal_text = ["You have to find", f"{iris}."] - elif self.goal == Goal.option_boss_iris_treasure_hunt: - goal_text = ["You have to retrieve", f"{iris} and", f"defeat the boss on B{self.final_floor}."] - elif self.goal == Goal.option_final_floor: - goal_text = [f"You need to get to B{self.final_floor}."] + elif self.o.goal == Goal.option_boss_iris_treasure_hunt: + goal_text = ["You have to retrieve", f"{iris} and", f"defeat the boss on B{self.o.final_floor}."] + elif self.o.goal == Goal.option_final_floor: + goal_text = [f"You need to get to B{self.o.final_floor}."] assert len(goal_text) <= 4 and all(len(line) <= 28 for line in goal_text), goal_text goal_text_bytes = bytes((0x08, *b"\x03".join(line.encode("ascii") for line in goal_text), 0x00)) return goal_text_bytes + b"\x00" * (147 - len(goal_text_bytes)) diff --git a/worlds/lufia2ac/basepatch/basepatch.asm b/worlds/lufia2ac/basepatch/basepatch.asm index d263b3d4f0..a2ea539fd4 100644 --- a/worlds/lufia2ac/basepatch/basepatch.asm +++ b/worlds/lufia2ac/basepatch/basepatch.asm @@ -3,13 +3,13 @@ lorom org $DFFFFD ; expand ROM to 3MB DB "EOF" -org $80FFD8 ; expand SRAM to 16KB - DB $04 ; overwrites DB $03 +org $80FFD8 ; expand SRAM to 32KB + DB $05 ; overwrites DB $03 org $80809A ; patch copy protection - CMP $704000 ; overwrites CMP $702000 + CMP $710000 ; overwrites CMP $702000 org $8080A6 ; patch copy protection - CMP $704000 ; overwrites CMP $702000 + CMP $710000 ; overwrites CMP $702000 @@ -34,8 +34,8 @@ org $8AF681 ; skip gruberik lexis dialogue org $8EA349 ; skip ancient cave entrance dialogue DB $1C,$B0,$01 ; L2SASM JMP $8EA1AD+$01B0 -org $8EA384 ; skip ancient cave exit dialogue - DB $1C,$2B,$02 ; L2SASM JMP $8EA1AD+$022B +org $8EA384 ; reset architect mode, skip ancient cave exit dialogue + DB $1B,$E1,$1C,$2B,$02 ; clear flag $E1, L2SASM JMP $8EA1AD+$022B org $8EA565 ; skip ancient cave leaving dialogue DB $1C,$E9,$03 ; L2SASM JMP $8EA1AD+$03E9 @@ -108,11 +108,13 @@ Init: STX $4302 ; A-bus destination address $F02000 (SRAM) LDA.b #$F0 STA $4304 - STX $4305 ; transfer 8kB + LDX.w #$6000 + STX $4305 ; transfer 24kB LDA.b #$01 STA $420B ; start DMA channel 1 ; sign expanded SRAM PHB + TDC LDA.b #$3F LDX.w #$8000 LDY.w #$2000 @@ -213,6 +215,8 @@ RX: JSR SpecialItemGet SEP #$20 JSL $8EC1EF ; call chest opening routine (but without chest opening animation) + STZ $A7 ; cleanup + JSL $83AB4F ; cleanup +: SEP #$20 RTS @@ -268,11 +272,15 @@ SpecialItemUse: SBC.w #$01B1 ; party member items range from $01B2 to $01B7 BMI + ASL + TAX ASL ASL ADC.w #$FD2E STA $09B7 ; set pointer to L2SASM join script SEP #$20 + LDA $8ED8C7,X ; load predefined bitmask with a single bit set + BIT $077E ; check against EV flags $02 to $07 (party member flags) + BNE + ; abort if character already present LDA $07A9 ; load EV register $11 (party counter) CMP.b #$03 BPL + ; abort if party full @@ -593,18 +601,16 @@ FinalFloor: pushpc org $8488BB ; DB=$84, x=0, m=0 - SEC ; {carry clear = disable this feature, carry set = enable this feature} JSL Providence ; overwrites LDX.w #$1402 : STX $0A8D - NOP ; + NOP #2 pullpc Providence: LDX.w #$1402 ; (overwritten instruction) STX $0A8D ; (overwritten instruction) add Potion x10 - BCC + - LDX.w #$022D ; + LDX.w #$022D STX $0A8F ; add Providence -+: RTL + RTL @@ -646,6 +652,142 @@ StartInventory: +; architect mode +pushpc +org $8EA1E7 +base = $8EA1AD ; ancient cave entrance script base + DB $15,$E1 : DW .locked-base ; L2SASM JMP .locked if flag $E1 set + DB $08,"Did you like the layout",$03, \ + "of the last cave? I can",$03, \ + "lock it down and prevent",$03, \ + "the cave from changing.",$01 + DB $08,"Do you want to lock",$03, \ + "the cave layout?",$01 + DB $10,$02 : DW .cancel-base,.lock-base ; setup 2 choices: .cancel and .lock + DB $08,"Cancel",$0F,"LOCK IT DOWN!",$0B +.cancel: + DB $4C,$54,$00 ; play sound $54, END +.lock: + DB $5A,$05,$03,$7F,$37,$28,$56,$4C,$6B,$1A,$E1 ; shake, delay $28 f, stop shake, play sound $6B, set flag $E1 +.locked: + DB $08,"It's locked down.",$00 + warnpc $8EA344 +org $839018 + ; DB=$83, x=0, m=1 + JSL ArchitectMode ; overwrites LDA.b #$7E : PHA : PLB +pullpc + +ArchitectMode: +; check current mode + LDA $079A + BIT.b #$02 + BEQ + ; go to write mode if flag $E1 (i.e., bit $02 in $079A) not set +; read mode (replaying the locked down layout) + JSR ArchitectBlockAddress + LDA $F00000,X ; check if current block is marked as filled + BEQ + ; go to write mode if block unused + TDC + LDA.b #$36 + LDY.w #$0521 + INX + MVN $7E,$F0 ; restore 55 RNG values from $F00000,X to $7E0521 + INX + LDA $F00000,X + STA $0559 ; restore current RNG index from $F00000,X to $7E0559 + BRA ++ +; write mode (recording the layout) ++: JSR ArchitectClearBlocks + JSR ArchitectBlockAddress + LDA $7FE696 + STA $F00000,X ; mark block as used + TDC + LDA.b #$36 + LDX.w #$0521 + INY + MVN $F0,$7E ; backup 55 RNG values from $7E0521 to $F00000,Y + INY + LDA $7E0559 + STA $0000,Y ; backup current RNG index from $7E0559 to $F00000,Y + LDA.b #$7E ; (overwritten instruction) set DB=$7E + PHA ; (overwritten instruction) + PLB ; (overwritten instruction) +++: RTL + +ArchitectClearBlocks: + LDA $7FE696 ; read next floor number + CMP $D08015 ; compare initial floor number + BEQ + + BRL ++ ; skip if not initial floor ++: LDA.b #$F0 + PHA + PLB + !floor = 1 + while !floor < 99 ; mark all blocks as unused + STZ !floor*$40+$6000 + !floor #= !floor+1 + endwhile +++: RTS + +ArchitectBlockAddress: +; calculate target SRAM address + TDC + LDA $7FE696 ; read next floor number + REP #$20 + ASL #6 + ADC.w #$6000 ; target SRAM address = next_floor * $40 + $6000 + TAX + TAY + SEP #$20 + RTS + + + +; for architect mode: make red chest behavior for iris treasure replacements independent of current inventory +; by ensuring the same number of RNG calls, no matter if you have the iris item already or not +; (done by prefilling *all* chests first and potentially overwriting one of them with an iris item afterwards, +; instead of checking the iris item first and then potentially filling *one fewer* regular chest) +pushpc +org $8390C9 + ; DB=$96, x=0, m=1 + NOP ; overwrites LDY.w #$0000 + BRA + ; go to regular red chest generation +-: ; iris treasure handling happens below +org $839114 + ; DB=$7F, x=0, m=1 + NOP #36 ; overwrites all of providence handling + LDA.b #$83 ; (overwritten instruction from org $8391E9) set DB=$83 for floor layout generation + PHA ; (overwritten instruction from org $8391E9) + PLB ; (overwritten instruction from org $8391E9) + BRL ++ ; go to end ++: LDY.w #$0000 ; (overwritten instruction from org $8390C9) initialize chest index + ; red chests are filled below +org $8391E9 + ; DB=$7F, x=0, m=1 + NOP ; overwrites LDA.b #$83 : PHA : PLB + BRL - ; go to iris treasure handling +++: ; floor layout generation happens below +pullpc + + + +; for architect mode: make red chest behavior for spell replacements independent of currently learned spells +; by ensuring the same number of RNG calls, no matter if you have the spell already or not +pushpc +org $8391A6 + ; DB=$7F, x=0, m=1 + JSL SpellRNG ; overwrites LDA.b #$80 : STA $E747,Y + NOP +pullpc + +SpellRNG: + LDA.b #$80 ; (overwritten instruction) mark chest item as spell + STA $E747,Y ; (overwritten instruction) + JSL $8082C7 ; + JSL $8082C7 ; advance RNG twice + RTL + + + ; increase variety of red chest gear after B9 pushpc org $839176 @@ -891,3 +1033,4 @@ pullpc ; $F02800 2 received counter ; $F02802 2 processed counter ; $F02804 inf list of received items +; $F06000 inf architect mode RNG state backups diff --git a/worlds/lufia2ac/basepatch/basepatch.bsdiff4 b/worlds/lufia2ac/basepatch/basepatch.bsdiff4 index e31ac74a52ea681061368489051d3d063039e1b7..7d622537c1c2a1c839c29df044c5be023aa6c542 100644 GIT binary patch literal 7638 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±4CH$%dPr8(f9at-l=R=;>a1jTS2;}J)6dLBR{ek@k#u(cI31+S~Gu&t`c>v zj=St|zeYawa=&tYcv+O&RkxO*>B|0WJr10!LCTP&ta-C_n=0mB=Ha9M&<@A{uqqxL zE*cko^GnRbjj@#F6+Je-dc}|mqN`iXY))8(2Pje1`4p``()Kjul^o79D}I)KF<66; z620hi!y(&}{BZCzXY0U`1icD#cz3exqw{5jo>_cxOc-gfzfqQ22&F~9 z#Em$xrF_}~roco(B-EmyQJ9ex6Ty}4k3#&o{M_HR6|Yd6&dwfis0oEC(IsFZHercT zRx%c`hR6)hC#a25J|w%qun4@FessJaw|bad%IlcBAi1L;che9}=v11k zMO9f@kZGLR!5Ubg1}VciT>^tKwyC9j&u14RBWu~}JQ*C)ksRH_9vqG(Cu1x0rn$e2 z?9sVz<{vUwhl4d1fkTm~zF&9Pu_Z0jmV9Nhq z|5+%Nz^7O+9slhvlU1BqtX&9?=RTZD4fc2UT?+l4&|50PA;poql+*zSkcfuB7k+ZD zGTIaa;pc;JL{4A(@8@U+>T${-X??9XCCmF*N9~{v9g@Yl8`b2y!MT}cw`o(G?cVi` zFa=6Mz25n9*n^msaqC?>@rnCDisx?u)Cv~@{ySXA%^xD-;di@Q%9?dy+W`bBW=^F> ze4d}BzQ3%ma=%FNNnF+84%l-%B9*%9q9!a} Tqct5E)6Sm5tHH=Dc?9@h+M?oG literal 7083 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 None: + self.assertEqual(0, len(self.get_items_by_name("Dekar blade"))) + + +class TestINeedDekarBlade(L2ACTestBase): + options = { + "custom_item_pool": { + "Dekar blade": 2, + }, + } + + def test_i_need_dekar_blade(self) -> None: + self.assertEqual(2, len(self.get_items_by_name("Dekar blade"))) + + +class TestVerifyCount(L2ACTestBase): + auto_construct = False + options = { + "custom_item_pool": { + "Dekar blade": 26, + }, + } + + def test_verify_count(self) -> None: + self.assertRaisesRegex(ValueError, + "Number of items in custom_item_pool \\(26\\) is greater than blue_chest_count \\(25\\)", + lambda: self.world_setup()) + + +class TestVerifyItemName(L2ACTestBase): + auto_construct = False + options = { + "custom_item_pool": { + "The car blade": 2, + }, + } + + def test_verify_item_name(self) -> None: + self.assertRaisesRegex(Exception, + "Item The car blade from option CustomItemPool\\(The car blade: 2\\) is not a " + "valid item name from Lufia II Ancient Cave\\. Did you mean 'Dekar blade'", + lambda: handle_option(Namespace(game="Lufia II Ancient Cave", name="Player"), + self.options, "custom_item_pool", CustomItemPool, + PlandoOptions(0))) diff --git a/worlds/lufia2ac/test/TestGoal.py b/worlds/lufia2ac/test/TestGoal.py index 06393ff1ef..6dc78e66d2 100644 --- a/worlds/lufia2ac/test/TestGoal.py +++ b/worlds/lufia2ac/test/TestGoal.py @@ -2,13 +2,12 @@ from . import L2ACTestBase class TestDefault(L2ACTestBase): - options = {} - def testEverything(self): + def test_everything(self) -> None: self.collect_all_but(["Boss victory"]) self.assertBeatable(True) - def testNothing(self): + def test_nothing(self) -> None: self.assertBeatable(True) @@ -17,15 +16,15 @@ class TestShuffleCapsuleMonsters(L2ACTestBase): "shuffle_capsule_monsters": True, } - def testEverything(self): + def test_everything(self) -> None: self.collect_all_but(["Boss victory"]) self.assertBeatable(True) - def testBestParty(self): + def test_best_party(self) -> None: self.collect_by_name("DARBI") self.assertBeatable(True) - def testNoDarbi(self): + def test_no_darbi(self) -> None: self.collect_all_but(["Boss victory", "DARBI"]) self.assertBeatable(False) @@ -35,23 +34,23 @@ class TestShufflePartyMembers(L2ACTestBase): "shuffle_party_members": True, } - def testEverything(self): + def test_everything(self) -> None: self.collect_all_but(["Boss victory"]) self.assertBeatable(True) - def testBestParty(self): + def test_best_party(self) -> None: self.collect_by_name(["Dekar", "Guy", "Arty"]) self.assertBeatable(True) - def testNoDekar(self): + def test_no_dekar(self) -> None: self.collect_all_but(["Boss victory", "Dekar"]) self.assertBeatable(False) - def testNoGuy(self): + def test_no_guy(self) -> None: self.collect_all_but(["Boss victory", "Guy"]) self.assertBeatable(False) - def testNoArty(self): + def test_no_arty(self) -> None: self.collect_all_but(["Boss victory", "Arty"]) self.assertBeatable(False) @@ -62,26 +61,26 @@ class TestShuffleBoth(L2ACTestBase): "shuffle_party_members": True, } - def testEverything(self): + def test_everything(self) -> None: self.collect_all_but(["Boss victory"]) self.assertBeatable(True) - def testBestParty(self): + def test_best_party(self) -> None: self.collect_by_name(["Dekar", "Guy", "Arty", "DARBI"]) self.assertBeatable(True) - def testNoDekar(self): + def test_no_dekar(self) -> None: self.collect_all_but(["Boss victory", "Dekar"]) self.assertBeatable(False) - def testNoGuy(self): + def test_no_guy(self) -> None: self.collect_all_but(["Boss victory", "Guy"]) self.assertBeatable(False) - def testNoArty(self): + def test_no_arty(self) -> None: self.collect_all_but(["Boss victory", "Arty"]) self.assertBeatable(False) - def testNoDarbi(self): + def test_no_darbi(self) -> None: self.collect_all_but(["Boss victory", "DARBI"]) self.assertBeatable(False) From 6671b21a869ffbc6697108ee19c5a6915044d0c2 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 20 Mar 2023 17:10:12 +0100 Subject: [PATCH 068/172] Core: Generic excluded fill (#1511) --- Fill.py | 41 +++++++++++++++++++++++++++++++++------- worlds/alttp/Dungeons.py | 11 +++++++---- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/Fill.py b/Fill.py index 92b57af58b..ef84a23b01 100644 --- a/Fill.py +++ b/Fill.py @@ -23,15 +23,27 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item] def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: typing.List[Location], - itempool: typing.List[Item], single_player_placement: bool = False, lock: bool = False, + item_pool: typing.List[Item], single_player_placement: bool = False, lock: bool = False, swap: bool = True, on_place: typing.Optional[typing.Callable[[Location], None]] = None, - allow_partial: bool = False) -> None: + allow_partial: bool = False, allow_excluded: bool = False) -> None: + """ + :param world: Multiworld to be filled. + :param base_state: State assumed before fill. + :param locations: Locations to be filled with item_pool + :param item_pool: Items to fill into the locations + :param single_player_placement: if true, can speed up placement if everything belongs to a single player + :param lock: locations are set to locked as they are filled + :param swap: if true, swaps of already place items are done in the event of a dead end + :param on_place: callback that is called when a placement happens + :param allow_partial: only place what is possible. Remaining items will be in the item_pool list. + :param allow_excluded: if true and placement fails, it is re-attempted while ignoring excluded on Locations + """ unplaced_items: typing.List[Item] = [] placements: typing.List[Location] = [] swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter() reachable_items: typing.Dict[int, typing.Deque[Item]] = {} - for item in itempool: + for item in item_pool: reachable_items.setdefault(item.player, deque()).append(item) while any(reachable_items.values()) and locations: @@ -39,9 +51,9 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: items_to_place = [items.pop() for items in reachable_items.values() if items] for item in items_to_place: - itempool.remove(item) + item_pool.remove(item) maximum_exploration_state = sweep_from_pool( - base_state, itempool + unplaced_items) + base_state, item_pool + unplaced_items) has_beaten_game = world.has_beaten_game(maximum_exploration_state) @@ -111,7 +123,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: reachable_items[placed_item.player].appendleft( placed_item) - itempool.append(placed_item) + item_pool.append(placed_item) break @@ -133,6 +145,21 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: if on_place: on_place(spot_to_fill) + if allow_excluded: + # check if partial fill is the result of excluded locations, in which case retry + excluded_locations = [ + location for location in locations + if location.progress_type == location.progress_type.EXCLUDED and not location.item + ] + if excluded_locations: + for location in excluded_locations: + location.progress_type = location.progress_type.DEFAULT + fill_restrictive(world, base_state, excluded_locations, unplaced_items, single_player_placement, lock, + swap, on_place, allow_partial, False) + for location in excluded_locations: + if not location.item: + location.progress_type = location.progress_type.EXCLUDED + if not allow_partial and len(unplaced_items) > 0 and len(locations) > 0: # There are leftover unplaceable items and locations that won't accept them if world.can_beat_game(): @@ -142,7 +169,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: raise FillError(f'No more spots to place {unplaced_items}, locations {locations} are invalid. ' f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}') - itempool.extend(unplaced_items) + item_pool.extend(unplaced_items) def remaining_fill(world: MultiWorld, diff --git a/worlds/alttp/Dungeons.py b/worlds/alttp/Dungeons.py index a37ded8d10..ec6862b9d0 100644 --- a/worlds/alttp/Dungeons.py +++ b/worlds/alttp/Dungeons.py @@ -7,6 +7,8 @@ from worlds.alttp.Items import ItemFactory from worlds.alttp.Regions import lookup_boss_drops from worlds.alttp.Options import smallkey_shuffle +if typing.TYPE_CHECKING: + from .SubClasses import ALttPLocation def create_dungeons(world, player): def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dungeon_items): @@ -138,9 +140,10 @@ def fill_dungeons_restrictive(world): if in_dungeon_items: restricted_players = {player for player, restricted in world.restrict_dungeon_item_on_boss.items() if restricted} - locations = [location for location in get_unfilled_dungeon_locations(world) - # filter boss - if not (location.player in restricted_players and location.name in lookup_boss_drops)] + locations: typing.List["ALttPLocation"] = [ + location for location in get_unfilled_dungeon_locations(world) + # filter boss + if not (location.player in restricted_players and location.name in lookup_boss_drops)] if dungeon_specific: for location in locations: dungeon = location.parent_region.dungeon @@ -159,7 +162,7 @@ def fill_dungeons_restrictive(world): (5 if (item.player, item.name) in dungeon_specific else 0)) for item in in_dungeon_items: all_state_base.remove(item) - fill_restrictive(world, all_state_base, locations, in_dungeon_items, True, True) + fill_restrictive(world, all_state_base, locations, in_dungeon_items, True, True, allow_excluded=True) dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A], From d4b793902f5ca1adb1d1e14997751cb5e28968cf Mon Sep 17 00:00:00 2001 From: toasterparty Date: Mon, 20 Mar 2023 09:16:19 -0700 Subject: [PATCH 069/172] [OC2] Overworld Logic (#1530) --- worlds/overcooked2/Logic.py | 168 ++++++++++++++++----- worlds/overcooked2/Options.py | 15 +- worlds/overcooked2/Overcooked2Levels.py | 59 ++++++++ worlds/overcooked2/__init__.py | 2 +- worlds/overcooked2/docs/setup_en.md | 10 ++ worlds/overcooked2/test/TestOvercooked2.py | 48 +++++- 6 files changed, 260 insertions(+), 42 deletions(-) diff --git a/worlds/overcooked2/Logic.py b/worlds/overcooked2/Logic.py index 52bff89d21..b86dc86d3f 100644 --- a/worlds/overcooked2/Logic.py +++ b/worlds/overcooked2/Logic.py @@ -1,22 +1,17 @@ from BaseClasses import CollectionState -from .Overcooked2Levels import Overcooked2GenericLevel, Overcooked2Dlc, Overcooked2Level +from .Overcooked2Levels import Overcooked2GenericLevel, Overcooked2Dlc, Overcooked2Level, OverworldRegion, overworld_region_by_level from typing import Dict from random import Random - def has_requirements_for_level_access(state: CollectionState, level_name: str, previous_level_completed_event_name: str, - required_star_count: int, player: int) -> bool: - # Check if the ramps in the overworld are set correctly - if level_name in ramp_logic: - (ramp_reqs, level_reqs) = ramp_logic[level_name] + required_star_count: int, allow_ramp_tricks: bool, player: int) -> bool: - for req in level_reqs: - if not state.has(req + " Level Complete", player): - return False # This level needs another to be beaten first - - for req in ramp_reqs: - if not state.has(req + " Ramp", player): - return False # The player doesn't have the pre-requisite ramp button + # Must have correct ramp buttons and pre-requisite levels, or tricks to sequence break + overworld_region = overworld_region_by_level[level_name] + overworld_logic = overworld_region_logic[overworld_region] + visited = list() + if not overworld_logic(state, player, allow_ramp_tricks, visited): + return False # Kevin Levels Need to have the corresponding items if level_name.startswith("K"): @@ -81,8 +76,9 @@ def is_item_progression(item_name, level_mapping, include_kevin): if item_name.endswith("Emote"): return False - if "Kevin" in item_name or "Ramp" in item_name: - return True # always progression + for item_identifier in ["Kevin", "Ramp", "Dash"]: + if item_identifier in item_name: + return True # These things are always progression because they can have overworld implications def item_in_logic(shortname, _item_name): for star in range(0, 3): @@ -214,28 +210,128 @@ def is_completable_no_items(level: Overcooked2GenericLevel) -> bool: return len(exclusive) == 0 and len(additive) == 0 +def can_reach_main(state: CollectionState, player: int, allow_tricks: bool, visited: list) -> bool: + if OverworldRegion.main in visited: + return False + visited.append(OverworldRegion.main) -# If key missing, doesn't require a ramp to access (or the logic is handled by a preceeding level) -# -# If empty, a ramp is required to access, but the ramp button is garunteed accessible -# -# If populated, ramp(s) are required to access and the button requires all levels in the -# list to be compelted before it can be pressed -# -ramp_logic = { - "1-5": (["Yellow"], []), - "2-2": (["Green"], []), - "3-1": (["Blue"], []), - "5-2": (["Purple"], []), - "6-1": (["Pink"], []), - "6-2": (["Red", "Purple"], ["5-1"]), # 5-1 spawns blue button, blue button gets you to red button - "Kevin-1": (["Dark Green"], []), - "Kevin-7": (["Purple"], ["5-1"]), # 5-1 spawns blue button, - # press blue button, - # climb blue ramp, - # jump the gap, - # climb wood ramps - "Kevin-8": (["Red", "Blue"], ["5-1", "6-2"]), # Same as above, but 6-2 spawns the ramp to K8 + return True + +def can_reach_yellow_island(state: CollectionState, player: int, allow_tricks: bool, visited: list) -> bool: + if OverworldRegion.yellow_island in visited: + return False + visited.append(OverworldRegion.yellow_island) + + return state.has("Yellow Ramp", player) + +def can_reach_dark_green_mountain(state: CollectionState, player: int, allow_tricks: bool, visited: list) -> bool: + if OverworldRegion.dark_green_mountain in visited: + return False + visited.append(OverworldRegion.dark_green_mountain) + + return state.has_all({"Dark Green Ramp", "Kevin-1"}, player) + +def can_reach_out_of_bounds(state: CollectionState, player: int, allow_tricks: bool, visited: list) -> bool: + if OverworldRegion.out_of_bounds in visited: + return False + visited.append(OverworldRegion.out_of_bounds) + + return allow_tricks and state.has("Progressive Dash", player) and can_reach_dark_green_mountain(state, player, allow_tricks, visited) + +def can_reach_stonehenge_mountain(state: CollectionState, player: int, allow_tricks: bool, visited: list) -> bool: + if OverworldRegion.stonehenge_mountain in visited: + return False + visited.append(OverworldRegion.stonehenge_mountain) + + if state.has("Blue Ramp", player): + return True + + if can_reach_out_of_bounds(state, player, allow_tricks, visited): + return True + + return False + +def can_reach_sky_shelf(state: CollectionState, player: int, allow_tricks: bool, visited: list) -> bool: + if OverworldRegion.sky_shelf in visited: + return False + visited.append(OverworldRegion.sky_shelf) + + if state.has("Green Ramp", player): + return True + + if state.has_all({"5-1 Level Complete", "Purple Ramp"}, player): + return True + + if allow_tricks and can_reach_pink_island(state, player, allow_tricks, visited) and state.has("Progressive Dash", player): + return True + + if can_reach_tip_of_the_map(state, player, allow_tricks, visited): + return True + + return False + +def can_reach_pink_island(state: CollectionState, player: int, allow_tricks: bool, visited: list) -> bool: + if OverworldRegion.pink_island in visited: + return False + visited.append(OverworldRegion.pink_island) + + if state.has("Pink Ramp", player): + return True + + if allow_tricks and state.has("Progressive Dash", player) and can_reach_sky_shelf(state, player, allow_tricks, visited): + return True + + return False + +def can_reach_tip_of_the_map(state: CollectionState, player: int, allow_tricks: bool, visited: list) -> bool: + if OverworldRegion.tip_of_the_map in visited: + return False + visited.append(OverworldRegion.tip_of_the_map) + + if state.has_all({"5-1 Level Complete", "Purple Ramp"}, player): + return True + + if can_reach_out_of_bounds(state, player, allow_tricks, visited): + return True + + if allow_tricks and can_reach_sky_shelf(state, player, allow_tricks, visited): + return True + + return False + +def can_reach_mars_shelf(state: CollectionState, player: int, allow_tricks: bool, visited: list) -> bool: + if OverworldRegion.mars_shelf in visited: + return False + visited.append(OverworldRegion.mars_shelf) + + tip_of_the_map = can_reach_tip_of_the_map(state, player, allow_tricks, visited) + + if tip_of_the_map and allow_tricks: + return True + + if tip_of_the_map and state.has_all({"6-1 Level Complete", "Red Ramp"}, player): + return True + + return False + +def can_reach_kevin_eight_island(state: CollectionState, player: int, allow_tricks: bool, visited: list) -> bool: + if OverworldRegion.kevin_eight_island in visited: + return False + visited.append(OverworldRegion.kevin_eight_island) + + return can_reach_mars_shelf(state, player, allow_tricks, visited) + + +overworld_region_logic = { + OverworldRegion.main : can_reach_main , + OverworldRegion.yellow_island : can_reach_yellow_island , + OverworldRegion.sky_shelf : can_reach_sky_shelf , + OverworldRegion.stonehenge_mountain: can_reach_stonehenge_mountain, + OverworldRegion.tip_of_the_map : can_reach_tip_of_the_map , + OverworldRegion.pink_island : can_reach_pink_island , + OverworldRegion.mars_shelf : can_reach_mars_shelf , + OverworldRegion.dark_green_mountain: can_reach_dark_green_mountain, + OverworldRegion.kevin_eight_island : can_reach_kevin_eight_island , } horde_logic = { # Additive diff --git a/worlds/overcooked2/Options.py b/worlds/overcooked2/Options.py index 400796af59..c859634092 100644 --- a/worlds/overcooked2/Options.py +++ b/worlds/overcooked2/Options.py @@ -1,6 +1,6 @@ from enum import IntEnum from typing import TypedDict -from Options import DefaultOnToggle, Range, Choice +from Options import Toggle, DefaultOnToggle, Range, Choice class LocationBalancingMode(IntEnum): @@ -21,6 +21,12 @@ class OC2OnToggle(DefaultOnToggle): return bool(self.value) +class OC2Toggle(Toggle): + @property + def result(self) -> bool: + return bool(self.value) + + class LocationBalancing(Choice): """Location balancing affects the density of progression items found in your world relative to other wordlds. This setting changes nothing for solo games. @@ -36,6 +42,10 @@ class LocationBalancing(Choice): option_full = LocationBalancingMode.full.value default = LocationBalancingMode.compromise.value +class RampTricks(OC2Toggle): + """If enabled, generated games may require sequence breaks on the overworld map. This includes crossing small gaps and escaping out of bounds.""" + display_name = "Overworld Tricks" + class DeathLink(Choice): """DeathLink is an opt-in feature for Multiworlds where individual death events are propogated to all games with DeathLink enabled. @@ -66,7 +76,7 @@ class AlwaysPreserveCookingProgress(OC2OnToggle): display_name = "Preserve Cooking/Mixing Progress" -class DisplayLeaderboardScores(OC2OnToggle): +class DisplayLeaderboardScores(OC2Toggle): """Modifies the Overworld map to fetch and display the current world records for each level. Press number keys 1-4 to view leaderboard scores for that number of players.""" display_name = "Display Leaderboard Scores" @@ -153,6 +163,7 @@ class StarThresholdScale(Range): overcooked_options = { # generator options "location_balancing": LocationBalancing, + "ramp_tricks": RampTricks, # deathlink "deathlink": DeathLink, diff --git a/worlds/overcooked2/Overcooked2Levels.py b/worlds/overcooked2/Overcooked2Levels.py index 007be13c9e..e0d23eb6c3 100644 --- a/worlds/overcooked2/Overcooked2Levels.py +++ b/worlds/overcooked2/Overcooked2Levels.py @@ -372,3 +372,62 @@ level_id_to_shortname = { (Overcooked2Dlc.SEASONAL , 30 ): "Moon 1-4" , (Overcooked2Dlc.SEASONAL , 31 ): "Moon 1-5" , } + +class OverworldRegion(IntEnum): + main = 0 + yellow_island = 1 + sky_shelf = 2 + stonehenge_mountain = 3 + tip_of_the_map = 4 + pink_island = 5 + mars_shelf = 6 + dark_green_mountain = 7 + kevin_eight_island = 8 + out_of_bounds = 9 + +overworld_region_by_level = { + "1-1": OverworldRegion.main, + "1-2": OverworldRegion.main, + "1-3": OverworldRegion.main, + "1-4": OverworldRegion.main, + "1-5": OverworldRegion.yellow_island, + "1-6": OverworldRegion.yellow_island, + "2-1": OverworldRegion.main, + "2-2": OverworldRegion.sky_shelf, + "2-3": OverworldRegion.sky_shelf, + "2-4": OverworldRegion.main, + "2-5": OverworldRegion.main, + "2-6": OverworldRegion.main, + "3-1": OverworldRegion.main, + "3-2": OverworldRegion.main, + "3-3": OverworldRegion.main, + "3-4": OverworldRegion.main, + "3-5": OverworldRegion.main, + "3-6": OverworldRegion.main, + "4-1": OverworldRegion.main, + "4-2": OverworldRegion.main, + "4-3": OverworldRegion.main, + "4-4": OverworldRegion.main, + "4-5": OverworldRegion.main, + "4-6": OverworldRegion.main, + "5-1": OverworldRegion.main, + "5-2": OverworldRegion.sky_shelf, + "5-3": OverworldRegion.main, + "5-4": OverworldRegion.tip_of_the_map, + "5-5": OverworldRegion.tip_of_the_map, + "5-6": OverworldRegion.tip_of_the_map, + "6-1": OverworldRegion.pink_island, + "6-2": OverworldRegion.tip_of_the_map, + "6-3": OverworldRegion.tip_of_the_map, + "6-4": OverworldRegion.sky_shelf, + "6-5": OverworldRegion.mars_shelf, + "6-6": OverworldRegion.mars_shelf, + "Kevin-1": OverworldRegion.dark_green_mountain, + "Kevin-2": OverworldRegion.main, + "Kevin-3": OverworldRegion.main, + "Kevin-4": OverworldRegion.main, + "Kevin-5": OverworldRegion.main, + "Kevin-6": OverworldRegion.main, + "Kevin-7": OverworldRegion.tip_of_the_map, + "Kevin-8": OverworldRegion.kevin_eight_island, +} diff --git a/worlds/overcooked2/__init__.py b/worlds/overcooked2/__init__.py index 63d87648e1..f481b3e556 100644 --- a/worlds/overcooked2/__init__.py +++ b/worlds/overcooked2/__init__.py @@ -322,7 +322,7 @@ class Overcooked2World(World): level_access_rule: Callable[[CollectionState], bool] = \ lambda state, level_name=level.level_name, previous_level_completed_event_name=previous_level_completed_event_name, required_star_count=required_star_count: \ - has_requirements_for_level_access(state, level_name, previous_level_completed_event_name, required_star_count, self.player) + has_requirements_for_level_access(state, level_name, previous_level_completed_event_name, required_star_count, self.options["RampTricks"], self.player) self.connect_regions("Overworld", level.level_name, level_access_rule) # Level --> Overworld diff --git a/worlds/overcooked2/docs/setup_en.md b/worlds/overcooked2/docs/setup_en.md index d724f02f7f..8738e3ed58 100644 --- a/worlds/overcooked2/docs/setup_en.md +++ b/worlds/overcooked2/docs/setup_en.md @@ -82,3 +82,13 @@ To completely remove *OC2-Modding*, navigate to your game's installation folder Since the goal of randomizer isn't necessarily to achieve new personal high scores, players may find themselves waiting for a level timer to expire once they've met their objective. A new feature called *Auto-Complete* has been added to automatically complete levels once a target star count has been achieved. To enable *Auto-Complete*, press the **Show** button near the top of your screen to expand the modding controls. Then, repeatedly press the **Auto-Complete** button until it shows the desired setting. + +## Overworld Sequence Breaking + +In the world's settings, there is an option called "Overworld Tricks" which allows the generator to make games which require doing tricks with the food truck to complete. This includes: + +- Dashing across gaps + +- "Wiggling" up ledges + +- Going out of bounds [See Video](https://youtu.be/VdOGhi6XPu4) diff --git a/worlds/overcooked2/test/TestOvercooked2.py b/worlds/overcooked2/test/TestOvercooked2.py index a6b5a4dcde..4cb12d9d9b 100644 --- a/worlds/overcooked2/test/TestOvercooked2.py +++ b/worlds/overcooked2/test/TestOvercooked2.py @@ -1,10 +1,12 @@ import unittest - from random import Random +from worlds.AutoWorld import AutoWorldRegister +from test.general import setup_solo_multiworld + from worlds.overcooked2.Items import * -from worlds.overcooked2.Overcooked2Levels import Overcooked2Dlc, Overcooked2Level, level_id_to_shortname, ITEMS_TO_EXCLUDE_IF_NO_DLC -from worlds.overcooked2.Logic import level_logic, level_shuffle_factory +from worlds.overcooked2.Overcooked2Levels import Overcooked2Dlc, Overcooked2Level, OverworldRegion, overworld_region_by_level, level_id_to_shortname, ITEMS_TO_EXCLUDE_IF_NO_DLC +from worlds.overcooked2.Logic import level_logic, overworld_region_logic, level_shuffle_factory from worlds.overcooked2.Locations import oc2_location_name_to_id @@ -170,3 +172,43 @@ class Overcooked2Test(unittest.TestCase): count += 1 self.assertEqual(count, len(level_id_range), f"Number of levels in {dlc.name} has discrepancy between level_id range and directory") + + def testOverworldRegion(self): + # OverworldRegion + # overworld_region_by_level + # overworld_region_logic + + # Test for duplicates + regions_list = [x for x in OverworldRegion] + regions_set = set(regions_list) + self.assertEqual(len(regions_list), len(regions_set), f"Duplicate values in OverworldRegion") + + # Test all levels represented + shortnames = [level.as_generic_level.shortname for level in Overcooked2Level()] + for shortname in shortnames: + if " " in shortname: + shortname = shortname.split(" ")[1] + shortname = shortname.replace("K-", "Kevin-") + self.assertIn(shortname, overworld_region_by_level) + + for region in overworld_region_by_level.values(): + # Test all regions valid + self.assertIn(region, regions_list) + + # Test Region Coverage + self.assertIn(region, overworld_region_logic) + + # Test all regions valid + for region in overworld_region_logic: + self.assertIn(region, regions_set) + + self.assertIn("Overcooked! 2", AutoWorldRegister.world_types.keys()) + world_type = AutoWorldRegister.world_types["Overcooked! 2"] + world = setup_solo_multiworld(world_type) + state = world.get_all_state(False) + + # Test region logic + for logic in overworld_region_logic.values(): + for allow_tricks in [False, True]: + result = logic(state, 1, allow_tricks, list()) + self.assertIn(result, [False, True]) From 67bf12369a2e2e7a4ec0963052bcf4f37f0d4c75 Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Mon, 20 Mar 2023 12:19:55 -0400 Subject: [PATCH 070/172] KH2 game implementation (#1438) --- KH2Client.py | 874 ++++++++++++ Launcher.py | 2 + WebHostLib/downloads.py | 2 + WebHostLib/templates/macros.html | 3 + worlds/kh2/Items.py | 1021 ++++++++++++++ worlds/kh2/Locations.py | 1780 ++++++++++++++++++++++++ worlds/kh2/Names/ItemName.py | 404 ++++++ worlds/kh2/Names/LocationName.py | 763 ++++++++++ worlds/kh2/Names/RegionName.py | 90 ++ worlds/kh2/OpenKH.py | 246 ++++ worlds/kh2/Options.py | 253 ++++ worlds/kh2/Regions.py | 1242 +++++++++++++++++ worlds/kh2/Rules.py | 96 ++ worlds/kh2/WorldLocations.py | 845 +++++++++++ worlds/kh2/XPValues.py | 119 ++ worlds/kh2/__init__.py | 315 +++++ worlds/kh2/docs/en_Kingdom Hearts 2.md | 105 ++ worlds/kh2/docs/setup_en.md | 48 + worlds/kh2/logic.py | 312 +++++ worlds/kh2/mod_template/mod.yml | 50 + worlds/kh2/requirements.txt | 1 + worlds/kh2/test/TestGoal.py | 27 + worlds/kh2/test/__init__.py | 5 + 23 files changed, 8603 insertions(+) create mode 100644 KH2Client.py create mode 100644 worlds/kh2/Items.py create mode 100644 worlds/kh2/Locations.py create mode 100644 worlds/kh2/Names/ItemName.py create mode 100644 worlds/kh2/Names/LocationName.py create mode 100644 worlds/kh2/Names/RegionName.py create mode 100644 worlds/kh2/OpenKH.py create mode 100644 worlds/kh2/Options.py create mode 100644 worlds/kh2/Regions.py create mode 100644 worlds/kh2/Rules.py create mode 100644 worlds/kh2/WorldLocations.py create mode 100644 worlds/kh2/XPValues.py create mode 100644 worlds/kh2/__init__.py create mode 100644 worlds/kh2/docs/en_Kingdom Hearts 2.md create mode 100644 worlds/kh2/docs/setup_en.md create mode 100644 worlds/kh2/logic.py create mode 100644 worlds/kh2/mod_template/mod.yml create mode 100644 worlds/kh2/requirements.txt create mode 100644 worlds/kh2/test/TestGoal.py create mode 100644 worlds/kh2/test/__init__.py diff --git a/KH2Client.py b/KH2Client.py new file mode 100644 index 0000000000..7c3f69117a --- /dev/null +++ b/KH2Client.py @@ -0,0 +1,874 @@ +import os +import asyncio +import ModuleUpdate +import typing +import json +import Utils +from pymem import pymem +from worlds.kh2.Items import DonaldAbility_Table, GoofyAbility_Table, exclusionItem_table, CheckDupingItems +from worlds.kh2 import all_locations, item_dictionary_table, exclusion_table + +from worlds.kh2.WorldLocations import * + +from worlds import network_data_package + +if __name__ == "__main__": + Utils.init_logging("KH2Client", exception_logger="Client") + +from NetUtils import ClientStatus +from CommonClient import gui_enabled, logger, get_base_parser, ClientCommandProcessor, \ + CommonContext, server_loop + +ModuleUpdate.update() + +kh2_loc_name_to_id = network_data_package["games"]["Kingdom Hearts 2"]["location_name_to_id"] + + +# class KH2CommandProcessor(ClientCommandProcessor): + + +class KH2Context(CommonContext): + # command_processor: int = KH2CommandProcessor + game = "Kingdom Hearts 2" + items_handling = 0b101 # Indicates you get items sent from other worlds. + + def __init__(self, server_address, password): + super(KH2Context, self).__init__(server_address, password) + self.kh2LocalItems = None + self.ability = None + self.growthlevel = None + self.KH2_sync_task = None + self.syncing = False + self.kh2connected = False + self.serverconneced = False + self.item_name_to_data = {name: data for name, data, in item_dictionary_table.items()} + self.location_name_to_data = {name: data for name, data, in all_locations.items()} + self.lookup_id_to_item: typing.Dict[int, str] = {data.code: item_name for item_name, data in + item_dictionary_table.items() if data.code} + self.lookup_id_to_Location: typing.Dict[int, str] = {data.code: item_name for item_name, data in + all_locations.items() if data.code} + self.location_name_to_worlddata = {name: data for name, data, in all_world_locations.items()} + + self.location_table = {} + self.collectible_table = {} + self.collectible_override_flags_address = 0 + self.collectible_offsets = {} + self.sending = [] + # flag for if the player has gotten their starting inventory from the server + self.hasStartingInvo = False + # list used to keep track of locations+items player has. Used for disoneccting + self.kh2seedsave = {"checked_locations": {"0": []}, + "starting_inventory": self.hasStartingInvo, + + # Character: [back of invo, front of invo] + "SoraInvo": [0x25CC, 0x2546], + "DonaldInvo": [0x2678, 0x2658], + "GoofyInvo": [0x278E, 0x276C], + "AmountInvo": { + "ServerItems": { + "Ability": {}, + "Amount": {}, + "Growth": {"High Jump": 0, "Quick Run": 0, "Dodge Roll": 0, "Aerial Dodge": 0, + "Glide": 0}, + "Bitmask": [], + "Weapon": {"Sora": [], "Donald": [], "Goofy": []}, + "Equipment": [], + "Magic": {}, + "StatIncrease": {}, + "Boost": {}, + }, + "LocalItems": { + "Ability": {}, + "Amount": {}, + "Growth": {"High Jump": 0, "Quick Run": 0, "Dodge Roll": 0, + "Aerial Dodge": 0, "Glide": 0}, + "Bitmask": [], + "Weapon": {"Sora": [], "Donald": [], "Goofy": []}, + "Equipment": [], + "Magic": {}, + "StatIncrease": {}, + "Boost": {}, + }}, + "worldIdChecks": { + "1": [], # world of darkness (story cutscenes) + "2": [], + "3": [], # destiny island doesn't have checks to ima put tt checks here + "4": [], + "5": [], + "6": [], + "7": [], + "8": [], + "9": [], + "10": [], + "11": [], + # atlantica isn't a supported world. if you go in atlantica it will check dc + "12": [], + "13": [], + "14": [], + "15": [], + # world map, but you only go to the world map while on the way to goa so checking hb + "16": [], + "17": [], + "18": [], + "255": [], # starting screen + }, + "Levels": { + "SoraLevel": 0, + "ValorLevel": 0, + "WisdomLevel": 0, + "LimitLevel": 0, + "MasterLevel": 0, + "FinalLevel": 0, + } + } + self.slotDataProgressionNames = {} + self.kh2seedname = None + self.kh2slotdata = None + self.itemamount = {} + # sora equipped, valor equipped, master equipped, final equipped + self.keybladeAnchorList = (0x24F0, 0x32F4, 0x339C, 0x33D4) + if "localappdata" in os.environ: + self.game_communication_path = os.path.expandvars(r"%localappdata%\KH2AP") + self.amountOfPieces = 0 + # hooked object + self.kh2 = None + self.ItemIsSafe = False + self.game_connected = False + self.finalxemnas = False + self.worldid = { + 1: TWTNW_Checks, # world of darkness (story cutscenes) + 2: TT_Checks, + 3: TT_Checks, # destiny island doesn't have checks to ima put tt checks here + 4: HB_Checks, + 5: BC_Checks, + 6: Oc_Checks, + 7: AG_Checks, + 8: LoD_Checks, + 9: HundredAcreChecks, + 10: PL_Checks, + 11: DC_Checks, # atlantica isn't a supported world. if you go in atlantica it will check dc + 12: DC_Checks, + 13: TR_Checks, + 14: HT_Checks, + 15: HB_Checks, # world map, but you only go to the world map while on the way to goa so checking hb + 16: PR_Checks, + 17: SP_Checks, + 18: TWTNW_Checks, + 255: HB_Checks, # starting screen + } + # 0x2A09C00+0x40 is the sve anchor. +1 is the last saved room + self.sveroom = 0x2A09C00 + 0x41 + # 0 not in battle 1 in yellow battle 2 red battle #short + self.inBattle = 0x2A0EAC4 + 0x40 + self.onDeath = 0xAB9078 + # PC Address anchors + self.Now = 0x0714DB8 + self.Save = 0x09A70B0 + self.Sys3 = 0x2A59DF0 + self.Bt10 = 0x2A74880 + self.BtlEnd = 0x2A0D3E0 + self.Slot1 = 0x2A20C98 + + self.chest_set = set(exclusion_table["Chests"]) + + self.keyblade_set = set(CheckDupingItems["Weapons"]["Keyblades"]) + self.staff_set = set(CheckDupingItems["Weapons"]["Staffs"]) + self.shield_set = set(CheckDupingItems["Weapons"]["Shields"]) + + self.all_weapons = self.keyblade_set.union(self.staff_set).union(self.shield_set) + + self.equipment_categories = CheckDupingItems["Equipment"] + self.armor_set = set(self.equipment_categories["Armor"]) + self.accessories_set = set(self.equipment_categories["Accessories"]) + self.all_equipment = self.armor_set.union(self.accessories_set) + + self.Equipment_Anchor_Dict = { + "Armor": [0x2504, 0x2506, 0x2508, 0x250A], + "Accessories": [0x2514, 0x2516, 0x2518, 0x251A]} + + self.AbilityQuantityDict = {} + self.ability_categories = CheckDupingItems["Abilities"] + + self.sora_ability_set = set(self.ability_categories["Sora"]) + self.donald_ability_set = set(self.ability_categories["Donald"]) + self.goofy_ability_set = set(self.ability_categories["Goofy"]) + + self.all_abilities = self.sora_ability_set.union(self.donald_ability_set).union(self.goofy_ability_set) + + self.boost_set = set(CheckDupingItems["Boosts"]) + self.stat_increase_set = set(CheckDupingItems["Stat Increases"]) + + self.AbilityQuantityDict = {item: self.item_name_to_data[item].quantity for item in self.all_abilities} + # Growth:[level 1,level 4,slot] + self.growth_values_dict = {"High Jump": [0x05E, 0x061, 0x25CE], + "Quick Run": [0x62, 0x65, 0x25D0], + "Dodge Roll": [0x234, 0x237, 0x25D2], + "Aerial Dodge": [0x066, 0x069, 0x25D4], + "Glide": [0x6A, 0x6D, 0x25D6]} + self.boost_to_anchor_dict = { + "Power Boost": 0x24F9, + "Magic Boost": 0x24FA, + "Defense Boost": 0x24FB, + "AP Boost": 0x24F8} + + self.AbilityCodeList = [self.item_name_to_data[item].code for item in exclusionItem_table["Ability"]] + self.master_growth = {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"} + + self.bitmask_item_code = [ + 0x130000, 0x130001, 0x130002, 0x130003, 0x130004, 0x130005, 0x130006, 0x130007 + , 0x130008, 0x130009, 0x13000A, 0x13000B, 0x13000C + , 0x13001F, 0x130020, 0x130021, 0x130022, 0x130023 + , 0x13002A, 0x13002B, 0x13002C, 0x13002D] + + async def server_auth(self, password_requested: bool = False): + if password_requested and not self.password: + await super(KH2Context, self).server_auth(password_requested) + await self.get_username() + await self.send_connect() + + async def connection_closed(self): + self.kh2connected = False + self.serverconneced = False + if self.kh2seedname is not None and self.auth is not None: + with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"), + 'w') as f: + f.write(json.dumps(self.kh2seedsave, indent=4)) + await super(KH2Context, self).connection_closed() + + async def disconnect(self, allow_autoreconnect: bool = False): + self.kh2connected = False + self.serverconneced = False + if self.kh2seedname not in {None} and self.auth not in {None}: + with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"), + 'w') as f: + f.write(json.dumps(self.kh2seedsave, indent=4)) + await super(KH2Context, self).disconnect() + + @property + def endpoints(self): + if self.server: + return [self.server] + else: + return [] + + async def shutdown(self): + if self.kh2seedname not in {None} and self.auth not in {None}: + with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"), + 'w') as f: + f.write(json.dumps(self.kh2seedsave, indent=4)) + await super(KH2Context, self).shutdown() + + def on_package(self, cmd: str, args: dict): + if cmd in {"RoomInfo"}: + self.kh2seedname = args['seed_name'] + if not os.path.exists(self.game_communication_path): + os.makedirs(self.game_communication_path) + if not os.path.exists(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json"): + with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"), + 'wt') as f: + pass + elif os.path.exists(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json"): + with open(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json", 'r') as f: + self.kh2seedsave = json.load(f) + + if cmd in {"Connected"}: + for player in args['players']: + if str(player.slot) not in self.kh2seedsave["checked_locations"]: + self.kh2seedsave["checked_locations"].update({str(player.slot): []}) + self.kh2slotdata = args['slot_data'] + self.serverconneced = True + self.kh2LocalItems = {int(location): item for location, item in self.kh2slotdata["LocalItems"].items()} + try: + self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX") + logger.info("You are now auto-tracking") + self.kh2connected = True + except Exception as e: + logger.info("Line 247") + if self.kh2connected: + logger.info("Connection Lost") + self.kh2connected = False + logger.info(e) + + if cmd in {"ReceivedItems"}: + start_index = args["index"] + if start_index != len(self.items_received): + for item in args['items']: + # starting invo from server + if item.location in {-2}: + if not self.kh2seedsave["starting_inventory"]: + asyncio.create_task(self.give_item(item.item)) + # if location is not already given or is !getitem + elif item.location not in self.kh2seedsave["checked_locations"][str(item.player)] \ + or item.location in {-1}: + asyncio.create_task(self.give_item(item.item)) + if item.location not in self.kh2seedsave["checked_locations"][str(item.player)] \ + and item.location not in {-1, -2}: + self.kh2seedsave["checked_locations"][str(item.player)].append(item.location) + if not self.kh2seedsave["starting_inventory"] and self.kh2connected: + self.kh2seedsave["starting_inventory"] = True + + if cmd in {"RoomUpdate"}: + if "checked_locations" in args: + new_locations = set(args["checked_locations"]) + # TODO: make this take locations from other players on the same slot so proper coop happens + #items_to_give = [self.kh2slotdata["LocalItems"][str(location_id)] for location_id in new_locations if + # location_id in self.kh2LocalItems.keys()] + self.checked_locations |= new_locations + + async def checkWorldLocations(self): + try: + currentworldint = int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x0714DB8, 1), "big") + curworldid = self.worldid[currentworldint] + if currentworldint != 1: + for location, data in curworldid.items(): + if location not in self.locations_checked \ + and (int.from_bytes( + self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1), + "big") & 0x1 << data.bitIndex) > 0: + self.locations_checked.add(location) + self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))] + except Exception as e: + logger.info("Line 285") + if self.kh2connected: + logger.info("Connection Lost.") + self.kh2connected = False + logger.info(e) + + async def checkLevels(self): + try: + for location, data in SoraLevels.items(): + currentLevel = int.from_bytes( + self.kh2.read_bytes(self.kh2.base_address + self.Save + 0x24FF, 1), "big") + if location not in self.locations_checked \ + and currentLevel >= data.bitIndex: + if self.kh2seedsave["Levels"]["SoraLevel"] < currentLevel: + self.kh2seedsave["Levels"]["SoraLevel"] = currentLevel + self.locations_checked.add(location) + self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))] + formDict = { + 0: ["ValorLevel", ValorLevels], 1: ["WisdomLevel", WisdomLevels], 2: ["LimitLevel", LimitLevels], + 3: ["MasterLevel", MasterLevels], 4: ["FinalLevel", FinalLevels]} + for i in range(5): + for location, data in formDict[i][1].items(): + formlevel = int.from_bytes( + self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1), "big") + if location not in self.locations_checked \ + and formlevel >= data.bitIndex: + if formlevel > self.kh2seedsave["Levels"][formDict[i][0]]: + self.kh2seedsave["Levels"][formDict[i][0]] = formlevel + self.locations_checked.add(location) + self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))] + except Exception as e: + logger.info("Line 312") + if self.kh2connected: + logger.info("Connection Lost.") + self.kh2connected = False + logger.info(e) + + async def checkSlots(self): + try: + for location, data in weaponSlots.items(): + if location not in self.locations_checked: + if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1), + "big") > 0: + self.locations_checked.add(location) + self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))] + + for location, data in formSlots.items(): + if location not in self.locations_checked: + if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1), + "big") & 0x1 << data.bitIndex > 0: + self.locations_checked.add(location) + self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))] + except Exception as e: + if self.kh2connected: + logger.info("Line 333") + logger.info("Connection Lost.") + self.kh2connected = False + logger.info(e) + + async def verifyChests(self): + try: + currentworld = str(int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x0714DB8, 1), "big")) + for location in self.kh2seedsave["worldIdChecks"][currentworld]: + locationName = self.lookup_id_to_Location[location] + if locationName in self.chest_set: + if locationName in self.location_name_to_worlddata.keys(): + locationData = self.location_name_to_worlddata[locationName] + if int.from_bytes( + self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained, 1), + "big") & 0x1 << locationData.bitIndex == 0: + roomData = int.from_bytes( + self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained, + 1), "big") + self.kh2.write_bytes(self.kh2.base_address + self.Save + locationData.addrObtained, + (roomData | 0x01 << locationData.bitIndex).to_bytes(1, 'big'), 1) + + except Exception as e: + if self.kh2connected: + logger.info("Line 350") + logger.info("Connection Lost.") + self.kh2connected = False + logger.info(e) + + async def verifyLevel(self): + for leveltype, anchor in {"SoraLevel": 0x24FF, + "ValorLevel": 0x32F6, + "WisdomLevel": 0x332E, + "LimitLevel": 0x3366, + "MasterLevel": 0x339E, + "FinalLevel": 0x33D6}.items(): + if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + anchor, 1), "big") < \ + self.kh2seedsave["Levels"][leveltype]: + self.kh2.write_bytes(self.kh2.base_address + self.Save + anchor, + (self.kh2seedsave["Levels"][leveltype]).to_bytes(1, 'big'), 1) + + def verifyLocation(self, location): + locationData = self.location_name_to_worlddata[location] + locationName = self.lookup_id_to_Location[location] + isChecked = True + + if locationName not in levels_locations: + if (int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained, 1), + "big") & 0x1 << locationData.bitIndex) == 0: + isChecked = False + elif locationName in SoraLevels: + if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + 0x24FF, 1), + "big") < locationData.bitIndex: + isChecked = False + elif int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained, 1), + "big") < locationData.bitIndex: + isChecked = False + return isChecked + + async def give_item(self, item, ItemType="ServerItems"): + while not self.kh2connected: + await asyncio.sleep(1) + try: + itemname = self.lookup_id_to_item[item] + itemcode = self.item_name_to_data[itemname] + # cannot give items during loading screens + # 0x8E9DA3=load 0xAB8BC7=black 0x2A148E8=controllable 0x715568=room transition + while int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Now, 1), "big") in {255, 1}: + await asyncio.sleep(1) + + if itemcode.ability: + abilityInvoType = 0 + TwilightZone = 2 + if ItemType == "LocalItems": + abilityInvoType = 1 + TwilightZone = -2 + if itemname in {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"}: + + self.kh2seedsave["AmountInvo"][ItemType]["Growth"][itemname] += 1 + return + + if itemname not in self.kh2seedsave["AmountInvo"][ItemType]["Ability"]: + self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname] = [] + # appending the slot that the ability should be in + + if len(self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname]) < \ + self.AbilityQuantityDict[itemname]: + if itemname in self.sora_ability_set: + self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname].append( + self.kh2seedsave["SoraInvo"][abilityInvoType]) + self.kh2seedsave["SoraInvo"][abilityInvoType] -= TwilightZone + elif itemname in self.donald_ability_set: + self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname].append( + self.kh2seedsave["DonaldInvo"][abilityInvoType]) + self.kh2seedsave["DonaldInvo"][abilityInvoType] -= TwilightZone + else: + self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname].append( + self.kh2seedsave["GoofyInvo"][abilityInvoType]) + self.kh2seedsave["GoofyInvo"][abilityInvoType] -= TwilightZone + + elif itemcode.code in self.bitmask_item_code: + + if itemname not in self.kh2seedsave["AmountInvo"][ItemType]["Bitmask"]: + self.kh2seedsave["AmountInvo"][ItemType]["Bitmask"].append(itemname) + + elif itemcode.memaddr in {0x3594, 0x3595, 0x3596, 0x3597, 0x35CF, 0x35D0}: + + if itemname in self.kh2seedsave["AmountInvo"][ItemType]["Magic"]: + self.kh2seedsave["AmountInvo"][ItemType]["Magic"][itemname] += 1 + else: + self.kh2seedsave["AmountInvo"][ItemType]["Magic"][itemname] = 1 + elif itemname in self.all_equipment: + + self.kh2seedsave["AmountInvo"][ItemType]["Equipment"].append(itemname) + + elif itemname in self.all_weapons: + if itemname in self.keyblade_set: + self.kh2seedsave["AmountInvo"][ItemType]["Weapon"]["Sora"].append(itemname) + elif itemname in self.staff_set: + self.kh2seedsave["AmountInvo"][ItemType]["Weapon"]["Donald"].append(itemname) + else: + self.kh2seedsave["AmountInvo"][ItemType]["Weapon"]["Goofy"].append(itemname) + + elif itemname in self.boost_set: + if itemname in self.kh2seedsave["AmountInvo"][ItemType]["Boost"]: + self.kh2seedsave["AmountInvo"][ItemType]["Boost"][itemname] += 1 + else: + self.kh2seedsave["AmountInvo"][ItemType]["Boost"][itemname] = 1 + + elif itemname in self.stat_increase_set: + + if itemname in self.kh2seedsave["AmountInvo"][ItemType]["StatIncrease"]: + self.kh2seedsave["AmountInvo"][ItemType]["StatIncrease"][itemname] += 1 + else: + self.kh2seedsave["AmountInvo"][ItemType]["StatIncrease"][itemname] = 1 + + else: + if itemname in self.kh2seedsave["AmountInvo"][ItemType]["Amount"]: + self.kh2seedsave["AmountInvo"][ItemType]["Amount"][itemname] += 1 + else: + self.kh2seedsave["AmountInvo"][ItemType]["Amount"][itemname] = 1 + + except Exception as e: + if self.kh2connected: + logger.info("Line 398") + logger.info("Connection Lost.") + self.kh2connected = False + logger.info(e) + + def run_gui(self): + """Import kivy UI system and start running it as self.ui_task.""" + from kvui import GameManager + + class KH2Manager(GameManager): + logging_pairs = [ + ("Client", "Archipelago") + ] + base_title = "Archipelago KH2 Client" + + self.ui = KH2Manager(self) + self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + + async def verifyItems(self): + try: + local_amount = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Amount"].keys()) + server_amount = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Amount"].keys()) + master_amount = local_amount | server_amount + + local_ability = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Ability"].keys()) + server_ability = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Ability"].keys()) + master_ability = local_ability | server_ability + + local_bitmask = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Bitmask"]) + server_bitmask = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Bitmask"]) + master_bitmask = local_bitmask | server_bitmask + + local_keyblade = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Weapon"]["Sora"]) + local_staff = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Weapon"]["Donald"]) + local_shield = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Weapon"]["Goofy"]) + + server_keyblade = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Weapon"]["Sora"]) + server_staff = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Weapon"]["Donald"]) + server_shield = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Weapon"]["Goofy"]) + + master_keyblade = local_keyblade | server_keyblade + master_staff = local_staff | server_staff + master_shield = local_shield | server_shield + + local_equipment = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Equipment"]) + server_equipment = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Equipment"]) + master_equipment = local_equipment | server_equipment + + local_magic = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Magic"].keys()) + server_magic = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Magic"].keys()) + master_magic = local_magic | server_magic + + local_stat = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["StatIncrease"].keys()) + server_stat = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["StatIncrease"].keys()) + master_stat = local_stat | server_stat + + local_boost = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Boost"].keys()) + server_boost = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Boost"].keys()) + master_boost = local_boost | server_boost + + for itemName in master_amount: + itemData = self.item_name_to_data[itemName] + amountOfItems = 0 + if itemName in local_amount: + amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["Amount"][itemName] + if itemName in server_amount: + amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["Amount"][itemName] + + if itemName == "Torn Page": + # Torn Pages are handled differently because they can be consumed. + # Will check the progression in 100 acre and - the amount of visits + # amountofitems-amount of visits done + for location, data in tornPageLocks.items(): + if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1), + "big") & 0x1 << data.bitIndex > 0: + amountOfItems -= 1 + if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), + "big") != amountOfItems and amountOfItems >= 0: + self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, + amountOfItems.to_bytes(1, 'big'), 1) + + for itemName in master_keyblade: + itemData = self.item_name_to_data[itemName] + # if isChecked: + # if the inventory slot for that keyblade is less than the amount they should have + if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), + "big") <= 0: + # Checking form anchors for the keyblade + if self.kh2.read_short(self.kh2.base_address + self.Save + 0x24F0) == itemData.kh2id \ + or self.kh2.read_short(self.kh2.base_address + self.Save + 0x32F4) == itemData.kh2id \ + or self.kh2.read_short(self.kh2.base_address + self.Save + 0x339C) == itemData.kh2id \ + or self.kh2.read_short(self.kh2.base_address + self.Save + 0x33D4) == itemData.kh2id: + self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, + (0).to_bytes(1, 'big'), 1) + else: + self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, + (1).to_bytes(1, 'big'), 1) + for itemName in master_staff: + itemData = self.item_name_to_data[itemName] + if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), + "big") != 1 \ + and self.kh2.read_short(self.kh2.base_address + self.Save + 0x2604) != itemData.kh2id: + self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, + (1).to_bytes(1, 'big'), 1) + + for itemName in master_shield: + itemData = self.item_name_to_data[itemName] + if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), + "big") != 1 \ + and self.kh2.read_short(self.kh2.base_address + self.Save + 0x2718) != itemData.kh2id: + self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, + (1).to_bytes(1, 'big'), 1) + + for itemName in master_ability: + itemData = self.item_name_to_data[itemName] + ability_slot = [] + if itemName in local_ability: + ability_slot += self.kh2seedsave["AmountInvo"]["LocalItems"]["Ability"][itemName] + if itemName in server_ability: + ability_slot += self.kh2seedsave["AmountInvo"]["ServerItems"]["Ability"][itemName] + for slot in ability_slot: + current = self.kh2.read_short(self.kh2.base_address + self.Save + slot) + ability = current & 0x0FFF + if ability | 0x8000 != (0x8000 + itemData.memaddr): + self.kh2.write_short(self.kh2.base_address + self.Save + slot, itemData.memaddr) + + for itemName in self.master_growth: + growthLevel = self.kh2seedsave["AmountInvo"]["ServerItems"]["Growth"][itemName] \ + + self.kh2seedsave["AmountInvo"]["LocalItems"]["Growth"][itemName] + if growthLevel > 0: + slot = self.growth_values_dict[itemName][2] + min_growth = self.growth_values_dict[itemName][0] + max_growth = self.growth_values_dict[itemName][1] + if growthLevel > 4: + growthLevel = 4 + current_growth_level = self.kh2.read_short(self.kh2.base_address + self.Save + slot) + ability = current_growth_level & 0x0FFF + # if the player should be getting a growth ability + if ability | 0x8000 != 0x8000 + min_growth - 1 + growthLevel: + # if it should be level one of that growth + if 0x8000 + min_growth - 1 + growthLevel <= 0x8000 + min_growth or ability < min_growth: + self.kh2.write_short(self.kh2.base_address + self.Save + slot, min_growth) + # if it is already in the inventory + elif ability | 0x8000 < (0x8000 + max_growth): + self.kh2.write_short(self.kh2.base_address + self.Save + slot, current_growth_level + 1) + + for itemName in master_bitmask: + itemData = self.item_name_to_data[itemName] + itemMemory = int.from_bytes( + self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), "big") + if (int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), + "big") & 0x1 << itemData.bitmask) == 0: + self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, + (itemMemory | 0x01 << itemData.bitmask).to_bytes(1, 'big'), 1) + + for itemName in master_equipment: + itemData = self.item_name_to_data[itemName] + isThere = False + if itemName in self.accessories_set: + Equipment_Anchor_List = self.Equipment_Anchor_Dict["Accessories"] + else: + Equipment_Anchor_List = self.Equipment_Anchor_Dict["Armor"] + if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), + "big") != 1: + # Checking form anchors for the equipment + for slot in Equipment_Anchor_List: + if self.kh2.read_short(self.kh2.base_address + self.Save + slot) == itemData.kh2id: + isThere = True + self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, + (0).to_bytes(1, 'big'), 1) + break + if not isThere: + self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, + (1).to_bytes(1, 'big'), 1) + + for itemName in master_magic: + itemData = self.item_name_to_data[itemName] + amountOfItems = 0 + if itemName in local_magic: + amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["Magic"][itemName] + if itemName in server_magic: + amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["Magic"][itemName] + if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), + "big") != amountOfItems \ + and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x741320, 1), "big") in {10, 8}: + self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, + amountOfItems.to_bytes(1, 'big'), 1) + + for itemName in master_stat: + itemData = self.item_name_to_data[itemName] + amountOfItems = 0 + if itemName in local_stat: + amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["StatIncrease"][itemName] + if itemName in server_stat: + amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["StatIncrease"][itemName] + + if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), + "big") != amountOfItems \ + and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Slot1 + 0x1B2, 1), + "big") >= 5: + self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, + amountOfItems.to_bytes(1, 'big'), 1) + + for itemName in master_boost: + itemData = self.item_name_to_data[itemName] + amountOfItems = 0 + if itemName in local_boost: + amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["Boost"][itemName] + if itemName in server_boost: + amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["Boost"][itemName] + amountOfBoostsInInvo = int.from_bytes( + self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), + "big") + amountOfUsedBoosts = int.from_bytes( + self.kh2.read_bytes(self.kh2.base_address + self.Save + self.boost_to_anchor_dict[itemName], 1), + "big") + # Ap Boots start at +50 for some reason + if itemName == "AP Boost": + amountOfUsedBoosts -= 50 + if (amountOfBoostsInInvo + amountOfUsedBoosts) <= amountOfItems and amountOfBoostsInInvo < 255: + self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, + (amountOfBoostsInInvo + 1).to_bytes(1, 'big'), 1) + + except Exception as e: + logger.info("Line 573") + if self.kh2connected: + logger.info("Connection Lost.") + self.kh2connected = False + logger.info(e) + + +def finishedGame(ctx: KH2Context, message): + if ctx.kh2slotdata['FinalXemnas'] == 1: + if 0x1301ED in message[0]["locations"]: + ctx.finalxemnas = True + # three proofs + if ctx.kh2slotdata['Goal'] == 0: + if int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x36B2, 1), "big") > 0 \ + and int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x36B3, 1), "big") > 0 \ + and int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x36B4, 1), "big") > 0: + if ctx.kh2slotdata['FinalXemnas'] == 1: + if ctx.finalxemnas: + return True + else: + return False + else: + return True + else: + return False + elif ctx.kh2slotdata['Goal'] == 1: + if int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x3641, 1), "big") >= \ + ctx.kh2slotdata['LuckyEmblemsRequired']: + ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B2, (1).to_bytes(1, 'big'), 1) + ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B3, (1).to_bytes(1, 'big'), 1) + ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B4, (1).to_bytes(1, 'big'), 1) + if ctx.kh2slotdata['FinalXemnas'] == 1: + if ctx.finalxemnas: + return True + else: + return False + else: + return True + else: + return False + elif ctx.kh2slotdata['Goal'] == 2: + for boss in ctx.kh2slotdata["hitlist"]: + if boss in message[0]["locations"]: + ctx.amountOfPieces += 1 + if ctx.amountOfPieces >= ctx.kh2slotdata["BountyRequired"]: + ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B2, (1).to_bytes(1, 'big'), 1) + ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B3, (1).to_bytes(1, 'big'), 1) + ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B4, (1).to_bytes(1, 'big'), 1) + if ctx.kh2slotdata['FinalXemnas'] == 1: + if ctx.finalxemnas: + return True + else: + return False + else: + return True + else: + return False + + +async def kh2_watcher(ctx: KH2Context): + while not ctx.exit_event.is_set(): + try: + if ctx.kh2connected and ctx.serverconneced: + ctx.sending = [] + await asyncio.create_task(ctx.checkWorldLocations()) + await asyncio.create_task(ctx.checkLevels()) + await asyncio.create_task(ctx.checkSlots()) + await asyncio.create_task(ctx.verifyChests()) + await asyncio.create_task(ctx.verifyItems()) + await asyncio.create_task(ctx.verifyLevel()) + message = [{"cmd": 'LocationChecks', "locations": ctx.sending}] + if finishedGame(ctx, message): + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) + ctx.finished_game = True + location_ids = [] + location_ids = [location for location in message[0]["locations"] if location not in location_ids] + for location in location_ids: + currentWorld = int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + 0x0714DB8, 1), "big") + if location not in ctx.kh2seedsave["worldIdChecks"][str(currentWorld)]: + ctx.kh2seedsave["worldIdChecks"][str(currentWorld)].append(location) + if location in ctx.kh2LocalItems: + item = ctx.kh2slotdata["LocalItems"][str(location)] + await asyncio.create_task(ctx.give_item(item, "LocalItems")) + await ctx.send_msgs(message) + elif not ctx.kh2connected and ctx.serverconneced: + logger.info("Game is not open. Disconnecting from Server.") + await ctx.disconnect() + except Exception as e: + logger.info("Line 661") + if ctx.kh2connected: + logger.info("Connection Lost.") + ctx.kh2connected = False + logger.info(e) + await asyncio.sleep(0.5) + + +if __name__ == '__main__': + async def main(args): + ctx = KH2Context(args.connect, args.password) + ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop") + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + progression_watcher = asyncio.create_task( + kh2_watcher(ctx), name="KH2ProgressionWatcher") + + await ctx.exit_event.wait() + ctx.server_address = None + + await progression_watcher + + await ctx.shutdown() + + + import colorama + + parser = get_base_parser(description="KH2 Client, for text interfacing.") + + args, rest = parser.parse_known_args() + colorama.init() + asyncio.run(main(args)) + colorama.deinit() diff --git a/Launcher.py b/Launcher.py index c4d9b6fea0..b1cc0fb4ea 100644 --- a/Launcher.py +++ b/Launcher.py @@ -159,6 +159,8 @@ components: Iterable[Component] = ( # Zillion Component('Zillion Client', 'ZillionClient', file_identifier=SuffixIdentifier('.apzl')), + #Kingdom Hearts 2 + Component('KH2 Client', "KH2Client"), # Functions Component('Open host.yaml', func=open_host_yaml), Component('Open Patch', func=open_patch), diff --git a/WebHostLib/downloads.py b/WebHostLib/downloads.py index 02ea7320ef..5cf503be1b 100644 --- a/WebHostLib/downloads.py +++ b/WebHostLib/downloads.py @@ -88,6 +88,8 @@ def download_slot_file(room_id, player_id: int): fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_SP.apsm64ex" elif slot_data.game == "Dark Souls III": fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}.json" + elif slot_data.game == "Kingdom Hearts 2": + fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.zip" else: return "Game download not supported." return send_file(io.BytesIO(slot_data.data), as_attachment=True, download_name=fname) diff --git a/WebHostLib/templates/macros.html b/WebHostLib/templates/macros.html index 11e333f05e..b2a0c73344 100644 --- a/WebHostLib/templates/macros.html +++ b/WebHostLib/templates/macros.html @@ -31,6 +31,9 @@ {% elif patch.game == "Factorio" %}
Download Factorio Mod... + {% elif patch.game == "Kingdom Hearts 2" %} + + Download Kingdom Hearts 2 Mod... {% elif patch.game == "Ocarina of Time" %} Download APZ5 File... diff --git a/worlds/kh2/Items.py b/worlds/kh2/Items.py new file mode 100644 index 0000000000..442411dbdf --- /dev/null +++ b/worlds/kh2/Items.py @@ -0,0 +1,1021 @@ +import typing + +from BaseClasses import Item +from .Names import ItemName + + +class KH2Item(Item): + game: str = "Kingdom Hearts 2" + + +class ItemData(typing.NamedTuple): + code: typing.Optional[int] + quantity: int = 0 + kh2id: int = 0 + # Save+ mem addr + memaddr: int = 0 + # some items have bitmasks. if bitmask>0 bitor to give item else + bitmask: int = 0 + # if ability then + ability: bool = False + + +Reports_Table = { + ItemName.SecretAnsemsReport1: ItemData(0x130000, 1, 226, 0x36C4, 6), + ItemName.SecretAnsemsReport2: ItemData(0x130001, 1, 227, 0x36C4, 7), + ItemName.SecretAnsemsReport3: ItemData(0x130002, 1, 228, 0x36C5, 0), + ItemName.SecretAnsemsReport4: ItemData(0x130003, 1, 229, 0x36C5, 1), + ItemName.SecretAnsemsReport5: ItemData(0x130004, 1, 230, 0x36C5, 2), + ItemName.SecretAnsemsReport6: ItemData(0x130005, 1, 231, 0x36C5, 3), + ItemName.SecretAnsemsReport7: ItemData(0x130006, 1, 232, 0x36C5, 4), + ItemName.SecretAnsemsReport8: ItemData(0x130007, 1, 233, 0x36C5, 5), + ItemName.SecretAnsemsReport9: ItemData(0x130008, 1, 234, 0x36C5, 6), + ItemName.SecretAnsemsReport10: ItemData(0x130009, 1, 235, 0x36C5, 7), + ItemName.SecretAnsemsReport11: ItemData(0x13000A, 1, 236, 0x36C6, 0), + ItemName.SecretAnsemsReport12: ItemData(0x13000B, 1, 237, 0x36C6, 1), + ItemName.SecretAnsemsReport13: ItemData(0x13000C, 1, 238, 0x36C6, 2), +} + +Progression_Table = { + ItemName.ProofofConnection: ItemData(0x13000D, 1, 593, 0x36B2), + ItemName.ProofofNonexistence: ItemData(0x13000E, 1, 594, 0x36B3), + ItemName.ProofofPeace: ItemData(0x13000F, 1, 595, 0x36B4), + ItemName.PromiseCharm: ItemData(0x130010, 1, 524, 0x3694), + ItemName.NamineSketches: ItemData(0x130011, 1, 368, 0x3642), + ItemName.CastleKey: ItemData(0x130012, 2, 460, 0x365D), # dummy 13 + ItemName.BattlefieldsofWar: ItemData(0x130013, 2, 54, 0x35AE), + ItemName.SwordoftheAncestor: ItemData(0x130014, 2, 55, 0x35AF), + ItemName.BeastsClaw: ItemData(0x130015, 2, 59, 0x35B3), + ItemName.BoneFist: ItemData(0x130016, 2, 60, 0x35B4), + ItemName.ProudFang: ItemData(0x130017, 2, 61, 0x35B5), + ItemName.SkillandCrossbones: ItemData(0x130018, 2, 62, 0x35B6), + ItemName.Scimitar: ItemData(0x130019, 2, 72, 0x35C0), + ItemName.MembershipCard: ItemData(0x13001A, 2, 369, 0x3643), + ItemName.IceCream: ItemData(0x13001B, 3, 375, 0x3649), + # Changed to 3 instead of one poster, picture and ice cream respectively + ItemName.WaytotheDawn: ItemData(0x13001C, 1, 73, 0x35C1), + # currently first visit locking doesn't work for twtnw.When goa is updated should be 2 + ItemName.IdentityDisk: ItemData(0x13001D, 2, 74, 0x35C2), + ItemName.TornPages: ItemData(0x13001E, 5, 32, 0x3598), + +} +Forms_Table = { + ItemName.ValorForm: ItemData(0x13001F, 1, 26, 0x36C0, 1), + ItemName.WisdomForm: ItemData(0x130020, 1, 27, 0x36C0, 2), + ItemName.LimitForm: ItemData(0x130021, 1, 563, 0x36CA, 3), + ItemName.MasterForm: ItemData(0x130022, 1, 31, 0x36C0, 6), + ItemName.FinalForm: ItemData(0x130023, 1, 29, 0x36C0, 4), +} +Magic_Table = { + ItemName.FireElement: ItemData(0x130024, 3, 21, 0x3594), + ItemName.BlizzardElement: ItemData(0x130025, 3, 22, 0x3595), + ItemName.ThunderElement: ItemData(0x130026, 3, 23, 0x3596), + ItemName.CureElement: ItemData(0x130027, 3, 24, 0x3597), + ItemName.MagnetElement: ItemData(0x130028, 3, 87, 0x35CF), + ItemName.ReflectElement: ItemData(0x130029, 3, 88, 0x35D0), + ItemName.Genie: ItemData(0x13002A, 1, 159, 0x36C4, 4), + ItemName.PeterPan: ItemData(0x13002B, 1, 160, 0x36C4, 5), + ItemName.Stitch: ItemData(0x13002C, 1, 25, 0x36C0, 0), + ItemName.ChickenLittle: ItemData(0x13002D, 1, 383, 0x36C0, 3), +} + +Movement_Table = { + ItemName.HighJump: ItemData(0x13002E, 4, 94, 0x05E, 0, True), + ItemName.QuickRun: ItemData(0x13002F, 4, 98, 0x062, 0, True), + ItemName.DodgeRoll: ItemData(0x130030, 4, 564, 0x234, 0, True), + ItemName.AerialDodge: ItemData(0x130031, 4, 102, 0x066, 0, True), + ItemName.Glide: ItemData(0x130032, 4, 106, 0x06A, 0, True), +} + +Keyblade_Table = { + ItemName.Oathkeeper: ItemData(0x130033, 1, 42, 0x35A2), + ItemName.Oblivion: ItemData(0x130034, 1, 43, 0x35A3), + ItemName.StarSeeker: ItemData(0x130035, 1, 480, 0x367B), + ItemName.HiddenDragon: ItemData(0x130036, 1, 481, 0x367C), + ItemName.HerosCrest: ItemData(0x130037, 1, 484, 0x367F), + ItemName.Monochrome: ItemData(0x130038, 1, 485, 0x3680), + ItemName.FollowtheWind: ItemData(0x130039, 1, 486, 0x3681), + ItemName.CircleofLife: ItemData(0x13003A, 1, 487, 0x3682), + ItemName.PhotonDebugger: ItemData(0x13003B, 1, 488, 0x3683), + ItemName.GullWing: ItemData(0x13003C, 1, 489, 0x3684), + ItemName.RumblingRose: ItemData(0x13003D, 1, 490, 0x3685), + ItemName.GuardianSoul: ItemData(0x13003E, 1, 491, 0x3686), + ItemName.WishingLamp: ItemData(0x13003F, 1, 492, 0x3687), + ItemName.DecisivePumpkin: ItemData(0x130040, 1, 493, 0x3688), + ItemName.SleepingLion: ItemData(0x130041, 1, 494, 0x3689), + ItemName.SweetMemories: ItemData(0x130042, 1, 495, 0x368A), + ItemName.MysteriousAbyss: ItemData(0x130043, 1, 496, 0x368B), + ItemName.TwoBecomeOne: ItemData(0x130044, 1, 543, 0x3698), + ItemName.FatalCrest: ItemData(0x130045, 1, 497, 0x368C), + ItemName.BondofFlame: ItemData(0x130046, 1, 498, 0x368D), + ItemName.Fenrir: ItemData(0x130047, 1, 499, 0x368E), + ItemName.UltimaWeapon: ItemData(0x130048, 1, 500, 0x368F), + ItemName.WinnersProof: ItemData(0x130049, 1, 544, 0x3699), + ItemName.Pureblood: ItemData(0x13004A, 1, 71, 0x35BF), +} +Staffs_Table = { + ItemName.Centurion2: ItemData(0x13004B, 1, 546, 0x369B), + ItemName.MeteorStaff: ItemData(0x13004C, 1, 150, 0x35F1), + ItemName.NobodyLance: ItemData(0x13004D, 1, 155, 0x35F6), + ItemName.PreciousMushroom: ItemData(0x13004E, 1, 549, 0x369E), + ItemName.PreciousMushroom2: ItemData(0x13004F, 1, 550, 0x369F), + ItemName.PremiumMushroom: ItemData(0x130050, 1, 551, 0x36A0), + ItemName.RisingDragon: ItemData(0x130051, 1, 154, 0x35F5), + ItemName.SaveTheQueen2: ItemData(0x130052, 1, 503, 0x3692), + ItemName.ShamansRelic: ItemData(0x130053, 1, 156, 0x35F7), +} +Shields_Table = { + ItemName.AkashicRecord: ItemData(0x130054, 1, 146, 0x35ED), + ItemName.FrozenPride2: ItemData(0x130055, 1, 553, 0x36A2), + ItemName.GenjiShield: ItemData(0x130056, 1, 145, 0x35EC), + ItemName.MajesticMushroom: ItemData(0x130057, 1, 556, 0x36A5), + ItemName.MajesticMushroom2: ItemData(0x130058, 1, 557, 0x36A6), + ItemName.NobodyGuard: ItemData(0x130059, 1, 147, 0x35EE), + ItemName.OgreShield: ItemData(0x13005A, 1, 141, 0x35E8), + ItemName.SaveTheKing2: ItemData(0x13005B, 1, 504, 0x3693), + ItemName.UltimateMushroom: ItemData(0x13005C, 1, 558, 0x36A7), +} +Accessory_Table = { + ItemName.AbilityRing: ItemData(0x13005D, 1, 8, 0x3587), + ItemName.EngineersRing: ItemData(0x13005E, 1, 9, 0x3588), + ItemName.TechniciansRing: ItemData(0x13005F, 1, 10, 0x3589), + ItemName.SkillRing: ItemData(0x130060, 1, 38, 0x359F), + ItemName.SkillfulRing: ItemData(0x130061, 1, 39, 0x35A0), + ItemName.ExpertsRing: ItemData(0x130062, 1, 11, 0x358A), + ItemName.MastersRing: ItemData(0x130063, 1, 34, 0x359B), + ItemName.CosmicRing: ItemData(0x130064, 1, 52, 0x35AD), + ItemName.ExecutivesRing: ItemData(0x130065, 1, 599, 0x36B5), + ItemName.SardonyxRing: ItemData(0x130066, 1, 12, 0x358B), + ItemName.TourmalineRing: ItemData(0x130067, 1, 13, 0x358C), + ItemName.AquamarineRing: ItemData(0x130068, 1, 14, 0x358D), + ItemName.GarnetRing: ItemData(0x130069, 1, 15, 0x358E), + ItemName.DiamondRing: ItemData(0x13006A, 1, 16, 0x358F), + ItemName.SilverRing: ItemData(0x13006B, 1, 17, 0x3590), + ItemName.GoldRing: ItemData(0x13006C, 1, 18, 0x3591), + ItemName.PlatinumRing: ItemData(0x13006D, 1, 19, 0x3592), + ItemName.MythrilRing: ItemData(0x13006E, 1, 20, 0x3593), + ItemName.OrichalcumRing: ItemData(0x13006F, 1, 28, 0x359A), + ItemName.SoldierEarring: ItemData(0x130070, 1, 40, 0x35A6), + ItemName.FencerEarring: ItemData(0x130071, 1, 46, 0x35A7), + ItemName.MageEarring: ItemData(0x130072, 1, 47, 0x35A8), + ItemName.SlayerEarring: ItemData(0x130073, 1, 48, 0x35AC), + ItemName.Medal: ItemData(0x130074, 1, 53, 0x35B2), + ItemName.MoonAmulet: ItemData(0x130075, 1, 35, 0x359C), + ItemName.StarCharm: ItemData(0x130076, 1, 36, 0x359E), + ItemName.CosmicArts: ItemData(0x130077, 1, 56, 0x35B1), + ItemName.ShadowArchive: ItemData(0x130078, 1, 57, 0x35B2), + ItemName.ShadowArchive2: ItemData(0x130079, 1, 58, 0x35B7), + ItemName.FullBloom: ItemData(0x13007A, 1, 64, 0x35B9), + ItemName.FullBloom2: ItemData(0x13007B, 1, 66, 0x35BB), + ItemName.DrawRing: ItemData(0x13007C, 1, 65, 0x35BA), + ItemName.LuckyRing: ItemData(0x13007D, 1, 63, 0x35B8), +} +Armor_Table = { + ItemName.ElvenBandana: ItemData(0x13007E, 1, 67, 0x35BC), + ItemName.DivineBandana: ItemData(0x13007F, 1, 68, 0x35BD), + ItemName.ProtectBelt: ItemData(0x130080, 1, 78, 0x35C7), + ItemName.GaiaBelt: ItemData(0x130081, 1, 79, 0x35CA), + ItemName.PowerBand: ItemData(0x130082, 1, 69, 0x35BE), + ItemName.BusterBand: ItemData(0x130083, 1, 70, 0x35C6), + ItemName.CosmicBelt: ItemData(0x130084, 1, 111, 0x35D1), + ItemName.FireBangle: ItemData(0x130085, 1, 173, 0x35D7), + ItemName.FiraBangle: ItemData(0x130086, 1, 174, 0x35D8), + ItemName.FiragaBangle: ItemData(0x130087, 1, 197, 0x35D9), + ItemName.FiragunBangle: ItemData(0x130088, 1, 284, 0x35DA), + ItemName.BlizzardArmlet: ItemData(0x130089, 1, 286, 0x35DC), + ItemName.BlizzaraArmlet: ItemData(0x13008A, 1, 287, 0x35DD), + ItemName.BlizzagaArmlet: ItemData(0x13008B, 1, 288, 0x35DE), + ItemName.BlizzagunArmlet: ItemData(0x13008C, 1, 289, 0x35DF), + ItemName.ThunderTrinket: ItemData(0x13008D, 1, 291, 0x35E2), + ItemName.ThundaraTrinket: ItemData(0x13008E, 1, 292, 0x35E3), + ItemName.ThundagaTrinket: ItemData(0x13008F, 1, 293, 0x35E4), + ItemName.ThundagunTrinket: ItemData(0x130090, 1, 294, 0x35E5), + ItemName.ShockCharm: ItemData(0x130091, 1, 132, 0x35D2), + ItemName.ShockCharm2: ItemData(0x130092, 1, 133, 0x35D3), + ItemName.ShadowAnklet: ItemData(0x130093, 1, 296, 0x35F9), + ItemName.DarkAnklet: ItemData(0x130094, 1, 297, 0x35FB), + ItemName.MidnightAnklet: ItemData(0x130095, 1, 298, 0x35FC), + ItemName.ChaosAnklet: ItemData(0x130096, 1, 299, 0x35FD), + ItemName.ChampionBelt: ItemData(0x130097, 1, 305, 0x3603), + ItemName.AbasChain: ItemData(0x130098, 1, 301, 0x35FF), + ItemName.AegisChain: ItemData(0x130099, 1, 302, 0x3600), + ItemName.Acrisius: ItemData(0x13009A, 1, 303, 0x3601), + ItemName.Acrisius2: ItemData(0x13009B, 1, 307, 0x3605), + ItemName.CosmicChain: ItemData(0x13009C, 1, 308, 0x3606), + ItemName.PetiteRibbon: ItemData(0x13009D, 1, 306, 0x3604), + ItemName.Ribbon: ItemData(0x13009E, 1, 304, 0x3602), + ItemName.GrandRibbon: ItemData(0x13009F, 1, 157, 0x35D4), +} +Usefull_Table = { + ItemName.MickyMunnyPouch: ItemData(0x1300A0, 3, 535, 0x3695), # 5000 munny per + ItemName.OletteMunnyPouch: ItemData(0x1300A1, 6, 362, 0x363C), # 2500 munny per + ItemName.HadesCupTrophy: ItemData(0x1300A2, 1, 537, 0x3696), + ItemName.UnknownDisk: ItemData(0x1300A3, 1, 462, 0x365F), + ItemName.OlympusStone: ItemData(0x1300A4, 1, 370, 0x3644), + ItemName.MaxHPUp: ItemData(0x1300A5, 20, 470, 0x3671), + ItemName.MaxMPUp: ItemData(0x1300A6, 4, 471, 0x3672), + ItemName.DriveGaugeUp: ItemData(0x1300A7, 6, 472, 0x3673), + ItemName.ArmorSlotUp: ItemData(0x1300A8, 3, 473, 0x3674), + ItemName.AccessorySlotUp: ItemData(0x1300A9, 3, 474, 0x3675), + ItemName.ItemSlotUp: ItemData(0x1300AA, 5, 463, 0x3660), +} +SupportAbility_Table = { + ItemName.Scan: ItemData(0x1300AB, 2, 138, 0x08A, 0, True), + ItemName.AerialRecovery: ItemData(0x1300AC, 1, 158, 0x09E, 0, True), + ItemName.ComboMaster: ItemData(0x1300AD, 1, 539, 0x21B, 0, True), + ItemName.ComboPlus: ItemData(0x1300AE, 3, 162, 0x0A2, 0, True), + ItemName.AirComboPlus: ItemData(0x1300AF, 3, 163, 0x0A3, 0, True), + ItemName.ComboBoost: ItemData(0x1300B0, 2, 390, 0x186, 0, True), + ItemName.AirComboBoost: ItemData(0x1300B1, 2, 391, 0x187, 0, True), + ItemName.ReactionBoost: ItemData(0x1300B2, 3, 392, 0x188, 0, True), + ItemName.FinishingPlus: ItemData(0x1300B3, 3, 393, 0x189, 0, True), + ItemName.NegativeCombo: ItemData(0x1300B4, 2, 394, 0x18A, 0, True), + ItemName.BerserkCharge: ItemData(0x1300B5, 2, 395, 0x18B, 0, True), + ItemName.DamageDrive: ItemData(0x1300B6, 2, 396, 0x18C, 0, True), + ItemName.DriveBoost: ItemData(0x1300B7, 2, 397, 0x18D, 0, True), + ItemName.FormBoost: ItemData(0x1300B8, 3, 398, 0x18E, 0, True), + ItemName.SummonBoost: ItemData(0x1300B9, 1, 399, 0x18F, 0, True), + ItemName.ExperienceBoost: ItemData(0x1300BA, 2, 401, 0x191, 0, True), + ItemName.Draw: ItemData(0x1300BB, 4, 405, 0x195, 0, True), + ItemName.Jackpot: ItemData(0x1300BC, 2, 406, 0x196, 0, True), + ItemName.LuckyLucky: ItemData(0x1300BD, 3, 407, 0x197, 0, True), + ItemName.DriveConverter: ItemData(0x1300BE, 2, 540, 0x21C, 0, True), + ItemName.FireBoost: ItemData(0x1300BF, 2, 408, 0x198, 0, True), + ItemName.BlizzardBoost: ItemData(0x1300C0, 2, 409, 0x199, 0, True), + ItemName.ThunderBoost: ItemData(0x1300C1, 2, 410, 0x19A, 0, True), + ItemName.ItemBoost: ItemData(0x1300C2, 2, 411, 0x19B, 0, True), + ItemName.MPRage: ItemData(0x1300C3, 2, 412, 0x19C, 0, True), + ItemName.MPHaste: ItemData(0x1300C4, 2, 413, 0x19D, 0, True), + ItemName.MPHastera: ItemData(0x1300C5, 2, 421, 0x1A5, 0, True), + ItemName.MPHastega: ItemData(0x1300C6, 1, 422, 0x1A6, 0, True), + ItemName.Defender: ItemData(0x1300C7, 2, 414, 0x19E, 0, True), + ItemName.DamageControl: ItemData(0x1300C8, 2, 542, 0x21E, 0, True), + ItemName.NoExperience: ItemData(0x1300C9, 1, 404, 0x194, 0, True), + ItemName.LightDarkness: ItemData(0x1300CA, 1, 541, 0x21D, 0, True), + ItemName.MagicLock: ItemData(0x1300CB, 1, 403, 0x193, 0, True), + ItemName.LeafBracer: ItemData(0x1300CC, 1, 402, 0x192, 0, True), + ItemName.CombinationBoost: ItemData(0x1300CD, 1, 400, 0x190, 0, True), + ItemName.OnceMore: ItemData(0x1300CE, 1, 416, 0x1A0, 0, True), + ItemName.SecondChance: ItemData(0x1300CF, 1, 415, 0x19F, 0, True), +} +ActionAbility_Table = { + ItemName.Guard: ItemData(0x1300D0, 1, 82, 0x052, 0, True), + ItemName.UpperSlash: ItemData(0x1300D1, 1, 137, 0x089, 0, True), + ItemName.HorizontalSlash: ItemData(0x1300D2, 1, 271, 0x10F, 0, True), + ItemName.FinishingLeap: ItemData(0x1300D3, 1, 267, 0x10B, 0, True), + ItemName.RetaliatingSlash: ItemData(0x1300D4, 1, 273, 0x111, 0, True), + ItemName.Slapshot: ItemData(0x1300D5, 1, 262, 0x106, 0, True), + ItemName.DodgeSlash: ItemData(0x1300D6, 1, 263, 0x107, 0, True), + ItemName.FlashStep: ItemData(0x1300D7, 1, 559, 0x22F, 0, True), + ItemName.SlideDash: ItemData(0x1300D8, 1, 264, 0x108, 0, True), + ItemName.VicinityBreak: ItemData(0x1300D9, 1, 562, 0x232, 0, True), + ItemName.GuardBreak: ItemData(0x1300DA, 1, 265, 0x109, 0, True), + ItemName.Explosion: ItemData(0x1300DB, 1, 266, 0x10A, 0, True), + ItemName.AerialSweep: ItemData(0x1300DC, 1, 269, 0x10D, 0, True), + ItemName.AerialDive: ItemData(0x1300DD, 1, 560, 0x230, 0, True), + ItemName.AerialSpiral: ItemData(0x1300DE, 1, 270, 0x10E, 0, True), + ItemName.AerialFinish: ItemData(0x1300DF, 1, 272, 0x110, 0, True), + ItemName.MagnetBurst: ItemData(0x1300E0, 1, 561, 0x231, 0, True), + ItemName.Counterguard: ItemData(0x1300E1, 1, 268, 0x10C, 0, True), + ItemName.AutoValor: ItemData(0x1300E2, 1, 385, 0x181, 0, True), + ItemName.AutoWisdom: ItemData(0x1300E3, 1, 386, 0x182, 0, True), + ItemName.AutoLimit: ItemData(0x1300E4, 1, 568, 0x238, 0, True), + ItemName.AutoMaster: ItemData(0x1300E5, 1, 387, 0x183, 0, True), + ItemName.AutoFinal: ItemData(0x1300E6, 1, 388, 0x184, 0, True), + ItemName.AutoSummon: ItemData(0x1300E7, 1, 389, 0x185, 0, True), + ItemName.TrinityLimit: ItemData(0x1300E8, 1, 198, 0x0C6, 0, True), +} +Items_Table = { + ItemName.PowerBoost: ItemData(0x1300E9, 1, 276, 0x3666), + ItemName.MagicBoost: ItemData(0x1300EA, 1, 277, 0x3667), + ItemName.DefenseBoost: ItemData(0x1300EB, 1, 278, 0x3668), + ItemName.APBoost: ItemData(0x1300EC, 1, 279, 0x3669), +} + +# These items cannot be in other games so these are done locally in kh2 +DonaldAbility_Table = { + ItemName.DonaldFire: ItemData(0x1300ED, 1, 165, 0xA5, 0, True), + ItemName.DonaldBlizzard: ItemData(0x1300EE, 1, 166, 0xA6, 0, True), + ItemName.DonaldThunder: ItemData(0x1300EF, 1, 167, 0xA7, 0, True), + ItemName.DonaldCure: ItemData(0x1300F0, 1, 168, 0xA8, 0, True), + ItemName.Fantasia: ItemData(0x1300F1, 1, 199, 0xC7, 0, True), + ItemName.FlareForce: ItemData(0x1300F2, 1, 200, 0xC8, 0, True), + ItemName.DonaldMPRage: ItemData(0x1300F3, 3, 412, 0x19C, 0, True), + ItemName.DonaldJackpot: ItemData(0x1300F4, 1, 406, 0x196, 0, True), + ItemName.DonaldLuckyLucky: ItemData(0x1300F5, 3, 407, 0x197, 0, True), + ItemName.DonaldFireBoost: ItemData(0x1300F6, 2, 408, 0x198, 0, True), + ItemName.DonaldBlizzardBoost: ItemData(0x1300F7, 2, 409, 0x199, 0, True), + ItemName.DonaldThunderBoost: ItemData(0x1300F8, 2, 410, 0x19A, 0, True), + ItemName.DonaldMPHaste: ItemData(0x1300F9, 1, 413, 0x19D, 0, True), + ItemName.DonaldMPHastera: ItemData(0x1300FA, 2, 421, 0x1A5, 0, True), + ItemName.DonaldMPHastega: ItemData(0x1300FB, 2, 422, 0x1A6, 0, True), + ItemName.DonaldAutoLimit: ItemData(0x1300FC, 1, 417, 0x1A1, 0, True), + ItemName.DonaldHyperHealing: ItemData(0x1300FD, 2, 419, 0x1A3, 0, True), + ItemName.DonaldAutoHealing: ItemData(0x1300FE, 1, 420, 0x1A4, 0, True), + ItemName.DonaldItemBoost: ItemData(0x1300FF, 1, 411, 0x19B, 0, True), + ItemName.DonaldDamageControl: ItemData(0x130100, 2, 542, 0x21E, 0, True), + ItemName.DonaldDraw: ItemData(0x130101, 1, 405, 0x195, 0, True), +} +GoofyAbility_Table = { + ItemName.GoofyTornado: ItemData(0x130102, 1, 423, 0x1A7, 0, True), + ItemName.GoofyTurbo: ItemData(0x130103, 1, 425, 0x1A9, 0, True), + ItemName.GoofyBash: ItemData(0x130104, 1, 429, 0x1AD, 0, True), + ItemName.TornadoFusion: ItemData(0x130105, 1, 201, 0xC9, 0, True), + ItemName.Teamwork: ItemData(0x130106, 1, 202, 0xCA, 0, True), + ItemName.GoofyDraw: ItemData(0x130107, 1, 405, 0x195, 0, True), + ItemName.GoofyJackpot: ItemData(0x130108, 1, 406, 0x196, 0, True), + ItemName.GoofyLuckyLucky: ItemData(0x130109, 1, 407, 0x197, 0, True), + ItemName.GoofyItemBoost: ItemData(0x13010A, 2, 411, 0x19B, 0, True), + ItemName.GoofyMPRage: ItemData(0x13010B, 2, 412, 0x19C, 0, True), + ItemName.GoofyDefender: ItemData(0x13010C, 2, 414, 0x19E, 0, True), + ItemName.GoofyDamageControl: ItemData(0x13010D, 3, 542, 0x21E, 0, True), + ItemName.GoofyAutoLimit: ItemData(0x13010E, 1, 417, 0x1A1, 0, True), + ItemName.GoofySecondChance: ItemData(0x13010F, 1, 415, 0x19F, 0, True), + ItemName.GoofyOnceMore: ItemData(0x130110, 1, 416, 0x1A0, 0, True), + ItemName.GoofyAutoChange: ItemData(0x130111, 1, 418, 0x1A2, 0, True), + ItemName.GoofyHyperHealing: ItemData(0x130112, 2, 419, 0x1A3, 0, True), + ItemName.GoofyAutoHealing: ItemData(0x130113, 1, 420, 0x1A4, 0, True), + ItemName.GoofyMPHaste: ItemData(0x130114, 1, 413, 0x19D, 0, True), + ItemName.GoofyMPHastera: ItemData(0x130115, 1, 421, 0x1A5, 0, True), + ItemName.GoofyMPHastega: ItemData(0x130116, 1, 422, 0x1A6, 0, True), + ItemName.GoofyProtect: ItemData(0x130117, 2, 596, 0x254, 0, True), + ItemName.GoofyProtera: ItemData(0x130118, 2, 597, 0x255, 0, True), + ItemName.GoofyProtega: ItemData(0x130119, 2, 598, 0x256, 0, True), + +} + +Misc_Table = { + ItemName.LuckyEmblem: ItemData(0x13011A, 0, 367, 0x3641), # letter item + ItemName.Victory: ItemData(0x13011B, 0, 263, 0x111), + ItemName.Bounty: ItemData(0x13011C, 0, 461, 0, 0), # Dummy 14 + # ItemName.UniversalKey:ItemData(0x130129,0,365,0x363F,0)#Tournament Poster + +} +# Items that are prone to duping. +# anchors for checking form keyblade +# Save+32F4 Valor Form Save+339C Master Form Save+33D4 Final Form +# Have to use the kh2id for checking stuff that sora has equipped +# Equipped abilities have an offset of 0x8000 so check for if whatever || whatever+0x8000 +CheckDupingItems = { + "Items": { + ItemName.ProofofConnection, + ItemName.ProofofNonexistence, + ItemName.ProofofPeace, + ItemName.PromiseCharm, + ItemName.NamineSketches, + ItemName.CastleKey, + ItemName.BattlefieldsofWar, + ItemName.SwordoftheAncestor, + ItemName.BeastsClaw, + ItemName.BoneFist, + ItemName.ProudFang, + ItemName.SkillandCrossbones, + ItemName.Scimitar, + ItemName.MembershipCard, + ItemName.IceCream, + ItemName.WaytotheDawn, + ItemName.IdentityDisk, + ItemName.TornPages, + ItemName.LuckyEmblem, + ItemName.MickyMunnyPouch, + ItemName.OletteMunnyPouch, + ItemName.HadesCupTrophy, + ItemName.UnknownDisk, + ItemName.OlympusStone, + }, + "Magic": { + ItemName.FireElement, + ItemName.BlizzardElement, + ItemName.ThunderElement, + ItemName.CureElement, + ItemName.MagnetElement, + ItemName.ReflectElement, + }, + "Bitmask": { + ItemName.ValorForm, + ItemName.WisdomForm, + ItemName.LimitForm, + ItemName.MasterForm, + ItemName.FinalForm, + ItemName.Genie, + ItemName.PeterPan, + ItemName.Stitch, + ItemName.ChickenLittle, + ItemName.SecretAnsemsReport1, + ItemName.SecretAnsemsReport2, + ItemName.SecretAnsemsReport3, + ItemName.SecretAnsemsReport4, + ItemName.SecretAnsemsReport5, + ItemName.SecretAnsemsReport6, + ItemName.SecretAnsemsReport7, + ItemName.SecretAnsemsReport8, + ItemName.SecretAnsemsReport9, + ItemName.SecretAnsemsReport10, + ItemName.SecretAnsemsReport11, + ItemName.SecretAnsemsReport12, + ItemName.SecretAnsemsReport13, + + }, + "Weapons": { + "Keyblades": { + ItemName.Oathkeeper, + ItemName.Oblivion, + ItemName.StarSeeker, + ItemName.HiddenDragon, + ItemName.HerosCrest, + ItemName.Monochrome, + ItemName.FollowtheWind, + ItemName.CircleofLife, + ItemName.PhotonDebugger, + ItemName.GullWing, + ItemName.RumblingRose, + ItemName.GuardianSoul, + ItemName.WishingLamp, + ItemName.DecisivePumpkin, + ItemName.SleepingLion, + ItemName.SweetMemories, + ItemName.MysteriousAbyss, + ItemName.TwoBecomeOne, + ItemName.FatalCrest, + ItemName.BondofFlame, + ItemName.Fenrir, + ItemName.UltimaWeapon, + ItemName.WinnersProof, + ItemName.Pureblood, + }, + "Staffs": { + ItemName.Centurion2, + ItemName.MeteorStaff, + ItemName.NobodyLance, + ItemName.PreciousMushroom, + ItemName.PreciousMushroom2, + ItemName.PremiumMushroom, + ItemName.RisingDragon, + ItemName.SaveTheQueen2, + ItemName.ShamansRelic, + }, + "Shields": { + ItemName.AkashicRecord, + ItemName.FrozenPride2, + ItemName.GenjiShield, + ItemName.MajesticMushroom, + ItemName.MajesticMushroom2, + ItemName.NobodyGuard, + ItemName.OgreShield, + ItemName.SaveTheKing2, + ItemName.UltimateMushroom, + } + }, + "Equipment": { + "Accessories": { + ItemName.AbilityRing, + ItemName.EngineersRing, + ItemName.TechniciansRing, + ItemName.SkillRing, + ItemName.SkillfulRing, + ItemName.ExpertsRing, + ItemName.MastersRing, + ItemName.CosmicRing, + ItemName.ExecutivesRing, + ItemName.SardonyxRing, + ItemName.TourmalineRing, + ItemName.AquamarineRing, + ItemName.GarnetRing, + ItemName.DiamondRing, + ItemName.SilverRing, + ItemName.GoldRing, + ItemName.PlatinumRing, + ItemName.MythrilRing, + ItemName.OrichalcumRing, + ItemName.SoldierEarring, + ItemName.FencerEarring, + ItemName.MageEarring, + ItemName.SlayerEarring, + ItemName.Medal, + ItemName.MoonAmulet, + ItemName.StarCharm, + ItemName.CosmicArts, + ItemName.ShadowArchive, + ItemName.ShadowArchive2, + ItemName.FullBloom, + ItemName.FullBloom2, + ItemName.DrawRing, + ItemName.LuckyRing, + }, + "Armor": { + ItemName.ElvenBandana, + ItemName.DivineBandana, + ItemName.ProtectBelt, + ItemName.GaiaBelt, + ItemName.PowerBand, + ItemName.BusterBand, + ItemName.CosmicBelt, + ItemName.FireBangle, + ItemName.FiraBangle, + ItemName.FiragaBangle, + ItemName.FiragunBangle, + ItemName.BlizzardArmlet, + ItemName.BlizzaraArmlet, + ItemName.BlizzagaArmlet, + ItemName.BlizzagunArmlet, + ItemName.ThunderTrinket, + ItemName.ThundaraTrinket, + ItemName.ThundagaTrinket, + ItemName.ThundagunTrinket, + ItemName.ShockCharm, + ItemName.ShockCharm2, + ItemName.ShadowAnklet, + ItemName.DarkAnklet, + ItemName.MidnightAnklet, + ItemName.ChaosAnklet, + ItemName.ChampionBelt, + ItemName.AbasChain, + ItemName.AegisChain, + ItemName.Acrisius, + ItemName.Acrisius2, + ItemName.CosmicChain, + ItemName.PetiteRibbon, + ItemName.Ribbon, + ItemName.GrandRibbon, + } + }, + "Stat Increases": { + ItemName.MaxHPUp, + ItemName.MaxMPUp, + ItemName.DriveGaugeUp, + ItemName.ArmorSlotUp, + ItemName.AccessorySlotUp, + ItemName.ItemSlotUp, + }, + "Abilities": { + "Sora": { + ItemName.Scan, + ItemName.AerialRecovery, + ItemName.ComboMaster, + ItemName.ComboPlus, + ItemName.AirComboPlus, + ItemName.ComboBoost, + ItemName.AirComboBoost, + ItemName.ReactionBoost, + ItemName.FinishingPlus, + ItemName.NegativeCombo, + ItemName.BerserkCharge, + ItemName.DamageDrive, + ItemName.DriveBoost, + ItemName.FormBoost, + ItemName.SummonBoost, + ItemName.ExperienceBoost, + ItemName.Draw, + ItemName.Jackpot, + ItemName.LuckyLucky, + ItemName.DriveConverter, + ItemName.FireBoost, + ItemName.BlizzardBoost, + ItemName.ThunderBoost, + ItemName.ItemBoost, + ItemName.MPRage, + ItemName.MPHaste, + ItemName.MPHastera, + ItemName.MPHastega, + ItemName.Defender, + ItemName.DamageControl, + ItemName.NoExperience, + ItemName.LightDarkness, + ItemName.MagicLock, + ItemName.LeafBracer, + ItemName.CombinationBoost, + ItemName.OnceMore, + ItemName.SecondChance, + ItemName.Guard, + ItemName.UpperSlash, + ItemName.HorizontalSlash, + ItemName.FinishingLeap, + ItemName.RetaliatingSlash, + ItemName.Slapshot, + ItemName.DodgeSlash, + ItemName.FlashStep, + ItemName.SlideDash, + ItemName.VicinityBreak, + ItemName.GuardBreak, + ItemName.Explosion, + ItemName.AerialSweep, + ItemName.AerialDive, + ItemName.AerialSpiral, + ItemName.AerialFinish, + ItemName.MagnetBurst, + ItemName.Counterguard, + ItemName.AutoValor, + ItemName.AutoWisdom, + ItemName.AutoLimit, + ItemName.AutoMaster, + ItemName.AutoFinal, + ItemName.AutoSummon, + ItemName.TrinityLimit, + ItemName.HighJump, + ItemName.QuickRun, + ItemName.DodgeRoll, + ItemName.AerialDodge, + ItemName.Glide, + }, + "Donald": { + ItemName.DonaldFire, + ItemName.DonaldBlizzard, + ItemName.DonaldThunder, + ItemName.DonaldCure, + ItemName.Fantasia, + ItemName.FlareForce, + ItemName.DonaldMPRage, + ItemName.DonaldJackpot, + ItemName.DonaldLuckyLucky, + ItemName.DonaldFireBoost, + ItemName.DonaldBlizzardBoost, + ItemName.DonaldThunderBoost, + ItemName.DonaldMPHaste, + ItemName.DonaldMPHastera, + ItemName.DonaldMPHastega, + ItemName.DonaldAutoLimit, + ItemName.DonaldHyperHealing, + ItemName.DonaldAutoHealing, + ItemName.DonaldItemBoost, + ItemName.DonaldDamageControl, + ItemName.DonaldDraw, + }, + "Goofy": { + ItemName.GoofyTornado, + ItemName.GoofyTurbo, + ItemName.GoofyBash, + ItemName.TornadoFusion, + ItemName.Teamwork, + ItemName.GoofyDraw, + ItemName.GoofyJackpot, + ItemName.GoofyLuckyLucky, + ItemName.GoofyItemBoost, + ItemName.GoofyMPRage, + ItemName.GoofyDefender, + ItemName.GoofyDamageControl, + ItemName.GoofyAutoLimit, + ItemName.GoofySecondChance, + ItemName.GoofyOnceMore, + ItemName.GoofyAutoChange, + ItemName.GoofyHyperHealing, + ItemName.GoofyAutoHealing, + ItemName.GoofyMPHaste, + ItemName.GoofyMPHastera, + ItemName.GoofyMPHastega, + ItemName.GoofyProtect, + ItemName.GoofyProtera, + ItemName.GoofyProtega, + } + }, + "Boosts": { + ItemName.PowerBoost, + ItemName.MagicBoost, + ItemName.DefenseBoost, + ItemName.APBoost, + } +} + +Progression_Dicts = { + # Items that are classified as progression + "Progression": { + # Wincons + ItemName.Victory, + ItemName.LuckyEmblem, + ItemName.Bounty, + ItemName.ProofofConnection, + ItemName.ProofofNonexistence, + ItemName.ProofofPeace, + ItemName.PromiseCharm, + # visit locking + ItemName.NamineSketches, + # dummy 13 + ItemName.CastleKey, + ItemName.BattlefieldsofWar, + ItemName.SwordoftheAncestor, + ItemName.BeastsClaw, + ItemName.BoneFist, + ItemName.ProudFang, + ItemName.SkillandCrossbones, + ItemName.Scimitar, + ItemName.MembershipCard, + ItemName.IceCream, + ItemName.WaytotheDawn, + ItemName.IdentityDisk, + ItemName.TornPages, + # forms + ItemName.ValorForm, + ItemName.WisdomForm, + ItemName.LimitForm, + ItemName.MasterForm, + ItemName.FinalForm, + # magic + ItemName.FireElement, + ItemName.BlizzardElement, + ItemName.ThunderElement, + ItemName.CureElement, + ItemName.MagnetElement, + ItemName.ReflectElement, + ItemName.Genie, + ItemName.PeterPan, + ItemName.Stitch, + ItemName.ChickenLittle, + # movement + ItemName.HighJump, + ItemName.QuickRun, + ItemName.DodgeRoll, + ItemName.AerialDodge, + ItemName.Glide, + # abilities + ItemName.Scan, + ItemName.AerialRecovery, + ItemName.ComboMaster, + ItemName.ComboPlus, + ItemName.AirComboPlus, + ItemName.ComboBoost, + ItemName.AirComboBoost, + ItemName.ReactionBoost, + ItemName.FinishingPlus, + ItemName.NegativeCombo, + ItemName.BerserkCharge, + ItemName.DamageDrive, + ItemName.DriveBoost, + ItemName.FormBoost, + ItemName.SummonBoost, + ItemName.ExperienceBoost, + ItemName.Draw, + ItemName.Jackpot, + ItemName.LuckyLucky, + ItemName.DriveConverter, + ItemName.FireBoost, + ItemName.BlizzardBoost, + ItemName.ThunderBoost, + ItemName.ItemBoost, + ItemName.MPRage, + ItemName.MPHaste, + ItemName.MPHastera, + ItemName.MPHastega, + ItemName.Defender, + ItemName.DamageControl, + ItemName.NoExperience, + ItemName.LightDarkness, + ItemName.MagicLock, + ItemName.LeafBracer, + ItemName.CombinationBoost, + ItemName.OnceMore, + ItemName.SecondChance, + ItemName.Guard, + ItemName.UpperSlash, + ItemName.HorizontalSlash, + ItemName.FinishingLeap, + ItemName.RetaliatingSlash, + ItemName.Slapshot, + ItemName.DodgeSlash, + ItemName.FlashStep, + ItemName.SlideDash, + ItemName.VicinityBreak, + ItemName.GuardBreak, + ItemName.Explosion, + ItemName.AerialSweep, + ItemName.AerialDive, + ItemName.AerialSpiral, + ItemName.AerialFinish, + ItemName.MagnetBurst, + ItemName.Counterguard, + ItemName.AutoValor, + ItemName.AutoWisdom, + ItemName.AutoLimit, + ItemName.AutoMaster, + ItemName.AutoFinal, + ItemName.AutoSummon, + ItemName.TrinityLimit, + # keyblades + ItemName.Oathkeeper, + ItemName.Oblivion, + ItemName.StarSeeker, + ItemName.HiddenDragon, + ItemName.HerosCrest, + ItemName.Monochrome, + ItemName.FollowtheWind, + ItemName.CircleofLife, + ItemName.PhotonDebugger, + ItemName.GullWing, + ItemName.RumblingRose, + ItemName.GuardianSoul, + ItemName.WishingLamp, + ItemName.DecisivePumpkin, + ItemName.SleepingLion, + ItemName.SweetMemories, + ItemName.MysteriousAbyss, + ItemName.TwoBecomeOne, + ItemName.FatalCrest, + ItemName.BondofFlame, + ItemName.Fenrir, + ItemName.UltimaWeapon, + ItemName.WinnersProof, + ItemName.Pureblood, + # Staffs + ItemName.Centurion2, + ItemName.MeteorStaff, + ItemName.NobodyLance, + ItemName.PreciousMushroom, + ItemName.PreciousMushroom2, + ItemName.PremiumMushroom, + ItemName.RisingDragon, + ItemName.SaveTheQueen2, + ItemName.ShamansRelic, + # Shields + ItemName.AkashicRecord, + ItemName.FrozenPride2, + ItemName.GenjiShield, + ItemName.MajesticMushroom, + ItemName.MajesticMushroom2, + ItemName.NobodyGuard, + ItemName.OgreShield, + ItemName.SaveTheKing2, + ItemName.UltimateMushroom, + # Party Limits + ItemName.FlareForce, + ItemName.Fantasia, + ItemName.Teamwork, + ItemName.TornadoFusion + }, + "2VisitLocking": { + ItemName.CastleKey, + ItemName.BattlefieldsofWar, + ItemName.SwordoftheAncestor, + ItemName.BeastsClaw, + ItemName.BoneFist, + ItemName.ProudFang, + ItemName.SkillandCrossbones, + ItemName.Scimitar, + ItemName.MembershipCard, + ItemName.IceCream, + ItemName.WaytotheDawn, + ItemName.IdentityDisk, + ItemName.IceCream, + ItemName.NamineSketches + }, + "AllVisitLocking": { + ItemName.CastleKey, + ItemName.CastleKey, + ItemName.BattlefieldsofWar, + ItemName.BattlefieldsofWar, + ItemName.SwordoftheAncestor, + ItemName.SwordoftheAncestor, + ItemName.BeastsClaw, + ItemName.BeastsClaw, + ItemName.BoneFist, + ItemName.BoneFist, + ItemName.ProudFang, + ItemName.ProudFang, + ItemName.SkillandCrossbones, + ItemName.SkillandCrossbones, + ItemName.Scimitar, + ItemName.Scimitar, + ItemName.MembershipCard, + ItemName.MembershipCard, + ItemName.WaytotheDawn, + ItemName.IdentityDisk, + ItemName.IdentityDisk, + ItemName.IceCream, + ItemName.IceCream, + ItemName.IceCream, + ItemName.NamineSketches, + } +} + +exclusionItem_table = { + "Ability": { + ItemName.Scan, + ItemName.AerialRecovery, + ItemName.ComboMaster, + ItemName.ComboPlus, + ItemName.AirComboPlus, + ItemName.ComboBoost, + ItemName.AirComboBoost, + ItemName.ReactionBoost, + ItemName.FinishingPlus, + ItemName.NegativeCombo, + ItemName.BerserkCharge, + ItemName.DamageDrive, + ItemName.DriveBoost, + ItemName.FormBoost, + ItemName.SummonBoost, + ItemName.ExperienceBoost, + ItemName.Draw, + ItemName.Jackpot, + ItemName.LuckyLucky, + ItemName.DriveConverter, + ItemName.FireBoost, + ItemName.BlizzardBoost, + ItemName.ThunderBoost, + ItemName.ItemBoost, + ItemName.MPRage, + ItemName.MPHaste, + ItemName.MPHastera, + ItemName.MPHastega, + ItemName.Defender, + ItemName.DamageControl, + ItemName.NoExperience, + ItemName.LightDarkness, + ItemName.MagicLock, + ItemName.LeafBracer, + ItemName.CombinationBoost, + ItemName.DamageDrive, + ItemName.OnceMore, + ItemName.SecondChance, + ItemName.Guard, + ItemName.UpperSlash, + ItemName.HorizontalSlash, + ItemName.FinishingLeap, + ItemName.RetaliatingSlash, + ItemName.Slapshot, + ItemName.DodgeSlash, + ItemName.FlashStep, + ItemName.SlideDash, + ItemName.VicinityBreak, + ItemName.GuardBreak, + ItemName.Explosion, + ItemName.AerialSweep, + ItemName.AerialDive, + ItemName.AerialSpiral, + ItemName.AerialFinish, + ItemName.MagnetBurst, + ItemName.Counterguard, + ItemName.AutoValor, + ItemName.AutoWisdom, + ItemName.AutoLimit, + ItemName.AutoMaster, + ItemName.AutoFinal, + ItemName.AutoSummon, + ItemName.TrinityLimit, + ItemName.HighJump, + ItemName.QuickRun, + ItemName.DodgeRoll, + ItemName.AerialDodge, + ItemName.Glide, + }, + "StatUps": { + ItemName.MaxHPUp, + ItemName.MaxMPUp, + ItemName.DriveGaugeUp, + ItemName.ArmorSlotUp, + ItemName.AccessorySlotUp, + ItemName.ItemSlotUp, + }, +} + +item_dictionary_table = {**Reports_Table, + **Progression_Table, + **Forms_Table, + **Magic_Table, + **Armor_Table, + **Movement_Table, + **Staffs_Table, + **Shields_Table, + **Keyblade_Table, + **Accessory_Table, + **Usefull_Table, + **SupportAbility_Table, + **ActionAbility_Table, + **Items_Table, + **Misc_Table, + **Items_Table, + **DonaldAbility_Table, + **GoofyAbility_Table, + } + +lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_dictionary_table.items() if + data.code} + +item_groups: typing.Dict[str, list] = {"Drive Form": [item_name for item_name in Forms_Table.keys()], + "Growth": [item_name for item_name in Movement_Table.keys()], + "Donald Limit": [ItemName.FlareForce, ItemName.Fantasia], + "Goofy Limit": [ItemName.Teamwork, ItemName.TornadoFusion], + "Magic": [ItemName.FireElement, ItemName.BlizzardElement, + ItemName.ThunderElement, + ItemName.CureElement, ItemName.MagnetElement, + ItemName.ReflectElement], + "Summon": [ItemName.ChickenLittle, ItemName.Genie, ItemName.Stitch, + ItemName.PeterPan], + "Gap Closer": [ItemName.SlideDash, ItemName.FlashStep], + "Ground Finisher": [ItemName.GuardBreak, ItemName.Explosion, + ItemName.FinishingLeap], + "Visit Lock": [item_name for item_name in + Progression_Dicts["2VisitLocking"]], + "Keyblade": [item_name for item_name in Keyblade_Table.keys()], + "Fire": [ItemName.FireElement], + "Blizzard": [ItemName.BlizzardElement], + "Thunder": [ItemName.ThunderElement], + "Cure": [ItemName.CureElement], + "Magnet": [ItemName.MagnetElement], + "Reflect": [ItemName.ReflectElement], + "Proof": [ItemName.ProofofNonexistence, ItemName.ProofofPeace, + ItemName.ProofofConnection], + "Filler": [ + ItemName.PowerBoost, ItemName.MagicBoost, + ItemName.DefenseBoost, ItemName.APBoost] + } + +# lookup_kh2id_to_name: typing.Dict[int, str] = {data.kh2id: item_name for item_name, data in +# item_dictionary_table.items() if data.kh2id} diff --git a/worlds/kh2/Locations.py b/worlds/kh2/Locations.py new file mode 100644 index 0000000000..87ec02be9b --- /dev/null +++ b/worlds/kh2/Locations.py @@ -0,0 +1,1780 @@ +import typing + +from BaseClasses import Location +from .Names import LocationName, RegionName, ItemName + + +class KH2Location(Location): + game: str = "Kingdom Hearts 2" + + +class LocationData(typing.NamedTuple): + code: typing.Optional[int] + locid: int + yml: str + charName: str = "Sora" + charNumber: int = 1 + + +# data's addrcheck sys3 addr obtained roomid bit index is eventid +LoD_Checks = { + LocationName.BambooGroveDarkShard: LocationData(0x130000, 245, "Chest"), + LocationName.BambooGroveEther: LocationData(0x130001, 497, "Chest"), + LocationName.BambooGroveMythrilShard: LocationData(0x130002, 498, "Chest"), + LocationName.EncampmentAreaMap: LocationData(0x130003, 350, "Chest"), + LocationName.Mission3: LocationData(0x130004, 417, "Chest"), + LocationName.CheckpointHiPotion: LocationData(0x130005, 21, "Chest"), + LocationName.CheckpointMythrilShard: LocationData(0x130006, 121, "Chest"), + LocationName.MountainTrailLightningShard: LocationData(0x130007, 22, "Chest"), + LocationName.MountainTrailRecoveryRecipe: LocationData(0x130008, 23, "Chest"), + LocationName.MountainTrailEther: LocationData(0x130009, 122, "Chest"), + LocationName.MountainTrailMythrilShard: LocationData(0x13000A, 123, "Chest"), + LocationName.VillageCaveAreaMap: LocationData(0x13000B, 495, "Chest"), + LocationName.VillageCaveDarkShard: LocationData(0x13000C, 125, "Chest"), + LocationName.VillageCaveAPBoost: LocationData(0x13000D, 124, "Chest"), + LocationName.VillageCaveBonus: LocationData(0x13000E, 43, "Get Bonus"), + LocationName.RidgeFrostShard: LocationData(0x13000F, 24, "Chest"), + LocationName.RidgeAPBoost: LocationData(0x130010, 126, "Chest"), + LocationName.ShanYu: LocationData(0x130011, 9, "Double Get Bonus"), + LocationName.ShanYuGetBonus: LocationData(0x130012, 9, "Second Get Bonus"), + LocationName.HiddenDragon: LocationData(0x130013, 257, "Chest"), + +} +LoD2_Checks = { + LocationName.ThroneRoomTornPages: LocationData(0x130014, 25, "Chest"), + LocationName.ThroneRoomPalaceMap: LocationData(0x130015, 127, "Chest"), + LocationName.ThroneRoomAPBoost: LocationData(0x130016, 26, "Chest"), + LocationName.ThroneRoomQueenRecipe: LocationData(0x130017, 27, "Chest"), + LocationName.ThroneRoomAPBoost2: LocationData(0x130018, 128, "Chest"), + LocationName.ThroneRoomOgreShield: LocationData(0x130019, 129, "Chest"), + LocationName.ThroneRoomMythrilCrystal: LocationData(0x13001A, 130, "Chest"), + LocationName.ThroneRoomOrichalcum: LocationData(0x13001B, 131, "Chest"), + LocationName.StormRider: LocationData(0x13001C, 10, "Get Bonus"), + LocationName.XigbarDataDefenseBoost: LocationData(0x13001D, 555, "Chest"), + +} +AG_Checks = { + LocationName.AgrabahMap: LocationData(0x13001E, 353, "Chest"), + LocationName.AgrabahDarkShard: LocationData(0x13001F, 28, "Chest"), + LocationName.AgrabahMythrilShard: LocationData(0x130020, 29, "Chest"), + LocationName.AgrabahHiPotion: LocationData(0x130021, 30, "Chest"), + LocationName.AgrabahAPBoost: LocationData(0x130022, 132, "Chest"), + LocationName.AgrabahMythrilStone: LocationData(0x130023, 133, "Chest"), + LocationName.AgrabahMythrilShard2: LocationData(0x130024, 249, "Chest"), + LocationName.AgrabahSerenityShard: LocationData(0x130025, 501, "Chest"), + LocationName.BazaarMythrilGem: LocationData(0x130026, 31, "Chest"), + LocationName.BazaarPowerShard: LocationData(0x130027, 32, "Chest"), + LocationName.BazaarHiPotion: LocationData(0x130028, 33, "Chest"), + LocationName.BazaarAPBoost: LocationData(0x130029, 134, "Chest"), + LocationName.BazaarMythrilShard: LocationData(0x13002A, 135, "Chest"), + LocationName.PalaceWallsSkillRing: LocationData(0x13002B, 136, "Chest"), + LocationName.PalaceWallsMythrilStone: LocationData(0x13002C, 520, "Chest"), + LocationName.CaveEntrancePowerStone: LocationData(0x13002D, 250, "Chest"), + LocationName.CaveEntranceMythrilShard: LocationData(0x13002E, 251, "Chest"), + LocationName.ValleyofStoneMythrilStone: LocationData(0x13002F, 35, "Chest"), + LocationName.ValleyofStoneAPBoost: LocationData(0x130030, 36, "Chest"), + LocationName.ValleyofStoneMythrilShard: LocationData(0x130031, 137, "Chest"), + LocationName.ValleyofStoneHiPotion: LocationData(0x130032, 138, "Chest"), + LocationName.AbuEscort: LocationData(0x130033, 42, "Get Bonus"), + LocationName.ChasmofChallengesCaveofWondersMap: LocationData(0x130034, 487, "Chest"), + LocationName.ChasmofChallengesAPBoost: LocationData(0x130035, 37, "Chest"), + LocationName.TreasureRoom: LocationData(0x130036, 46, "Get Bonus"), + LocationName.TreasureRoomAPBoost: LocationData(0x130037, 502, "Chest"), + LocationName.TreasureRoomSerenityGem: LocationData(0x130038, 503, "Chest"), + LocationName.ElementalLords: LocationData(0x130039, 37, "Get Bonus"), + LocationName.LampCharm: LocationData(0x13003A, 300, "Chest"), + +} +AG2_Checks = { + LocationName.RuinedChamberTornPages: LocationData(0x13003B, 34, "Chest"), + LocationName.RuinedChamberRuinsMap: LocationData(0x13003C, 486, "Chest"), + LocationName.GenieJafar: LocationData(0x13003D, 15, "Get Bonus"), + LocationName.WishingLamp: LocationData(0x13003E, 303, "Chest"), + LocationName.LexaeusBonus: LocationData(0x13003F, 65, "Get Bonus"), + LocationName.LexaeusASStrengthBeyondStrength: LocationData(0x130040, 545, "Chest"), + LocationName.LexaeusDataLostIllusion: LocationData(0x130041, 550, "Chest"), +} +DC_Checks = { + LocationName.DCCourtyardMythrilShard: LocationData(0x130042, 16, "Chest"), + LocationName.DCCourtyardStarRecipe: LocationData(0x130043, 17, "Chest"), + LocationName.DCCourtyardAPBoost: LocationData(0x130044, 18, "Chest"), + LocationName.DCCourtyardMythrilStone: LocationData(0x130045, 92, "Chest"), + LocationName.DCCourtyardBlazingStone: LocationData(0x130046, 93, "Chest"), + LocationName.DCCourtyardBlazingShard: LocationData(0x130047, 247, "Chest"), + LocationName.DCCourtyardMythrilShard2: LocationData(0x130048, 248, "Chest"), + LocationName.LibraryTornPages: LocationData(0x130049, 91, "Chest"), + LocationName.DisneyCastleMap: LocationData(0x13004A, 332, "Chest"), + LocationName.MinnieEscort: LocationData(0x13004B, 38, "Double Get Bonus"), + LocationName.MinnieEscortGetBonus: LocationData(0x13004C, 38, "Second Get Bonus"), + +} +TR_Checks = { + LocationName.CornerstoneHillMap: LocationData(0x13004D, 79, "Chest"), + LocationName.CornerstoneHillFrostShard: LocationData(0x13004E, 12, "Chest"), + LocationName.PierMythrilShard: LocationData(0x13004F, 81, "Chest"), + LocationName.PierHiPotion: LocationData(0x130050, 82, "Chest"), + LocationName.WaterwayMythrilStone: LocationData(0x130051, 83, "Chest"), + LocationName.WaterwayAPBoost: LocationData(0x130052, 84, "Chest"), + LocationName.WaterwayFrostStone: LocationData(0x130053, 85, "Chest"), + LocationName.WindowofTimeMap: LocationData(0x130054, 368, "Chest"), + LocationName.BoatPete: LocationData(0x130055, 16, "Get Bonus"), + LocationName.FuturePete: LocationData(0x130056, 17, "Double Get Bonus"), + LocationName.FuturePeteGetBonus: LocationData(0x130057, 17, "Second Get Bonus"), + LocationName.Monochrome: LocationData(0x130058, 261, "Chest"), + LocationName.WisdomForm: LocationData(0x130059, 262, "Chest"), + LocationName.MarluxiaGetBonus: LocationData(0x13005A, 67, "Get Bonus"), + LocationName.MarluxiaASEternalBlossom: LocationData(0x13005B, 548, "Chest"), + LocationName.MarluxiaDataLostIllusion: LocationData(0x13005C, 553, "Chest"), + LocationName.LingeringWillBonus: LocationData(0x13005D, 70, "Get Bonus"), + LocationName.LingeringWillProofofConnection: LocationData(0x13005E, 587, "Chest"), + LocationName.LingeringWillManifestIllusion: LocationData(0x13005F, 591, "Chest"), + +} +# the mismatch might be here +HundredAcre1_Checks = { + LocationName.PoohsHouse100AcreWoodMap: LocationData(0x130060, 313, "Chest"), + LocationName.PoohsHouseAPBoost: LocationData(0x130061, 97, "Chest"), + LocationName.PoohsHouseMythrilStone: LocationData(0x130062, 98, "Chest"), +} +HundredAcre2_Checks = { + LocationName.PigletsHouseDefenseBoost: LocationData(0x130063, 105, "Chest"), + LocationName.PigletsHouseAPBoost: LocationData(0x130064, 103, "Chest"), + LocationName.PigletsHouseMythrilGem: LocationData(0x130065, 104, "Chest"), +} +HundredAcre3_Checks = { + LocationName.RabbitsHouseDrawRing: LocationData(0x130066, 314, "Chest"), + LocationName.RabbitsHouseMythrilCrystal: LocationData(0x130067, 100, "Chest"), + LocationName.RabbitsHouseAPBoost: LocationData(0x130068, 101, "Chest"), +} +HundredAcre4_Checks = { + LocationName.KangasHouseMagicBoost: LocationData(0x130069, 108, "Chest"), + LocationName.KangasHouseAPBoost: LocationData(0x13006A, 106, "Chest"), + LocationName.KangasHouseOrichalcum: LocationData(0x13006B, 107, "Chest"), +} +HundredAcre5_Checks = { + LocationName.SpookyCaveMythrilGem: LocationData(0x13006C, 110, "Chest"), + LocationName.SpookyCaveAPBoost: LocationData(0x13006D, 111, "Chest"), + LocationName.SpookyCaveOrichalcum: LocationData(0x13006E, 112, "Chest"), + LocationName.SpookyCaveGuardRecipe: LocationData(0x13006F, 113, "Chest"), + LocationName.SpookyCaveMythrilCrystal: LocationData(0x130070, 115, "Chest"), + LocationName.SpookyCaveAPBoost2: LocationData(0x130071, 116, "Chest"), + LocationName.SweetMemories: LocationData(0x130072, 284, "Chest"), + LocationName.SpookyCaveMap: LocationData(0x130073, 485, "Chest"), +} +HundredAcre6_Checks = { + LocationName.StarryHillCosmicRing: LocationData(0x130074, 312, "Chest"), + LocationName.StarryHillStyleRecipe: LocationData(0x130075, 94, "Chest"), + LocationName.StarryHillCureElement: LocationData(0x130076, 285, "Chest"), + LocationName.StarryHillOrichalcumPlus: LocationData(0x130077, 539, "Chest"), +} +Oc_Checks = { + LocationName.PassageMythrilShard: LocationData(0x130078, 7, "Chest"), + LocationName.PassageMythrilStone: LocationData(0x130079, 8, "Chest"), + LocationName.PassageEther: LocationData(0x13007A, 144, "Chest"), + LocationName.PassageAPBoost: LocationData(0x13007B, 145, "Chest"), + LocationName.PassageHiPotion: LocationData(0x13007C, 146, "Chest"), + LocationName.InnerChamberUnderworldMap: LocationData(0x13007D, 2, "Chest"), + LocationName.InnerChamberMythrilShard: LocationData(0x13007E, 243, "Chest"), + LocationName.Cerberus: LocationData(0x13007F, 5, "Get Bonus"), + LocationName.ColiseumMap: LocationData(0x130080, 338, "Chest"), + LocationName.Urns: LocationData(0x130081, 57, "Get Bonus"), + LocationName.UnderworldEntrancePowerBoost: LocationData(0x130082, 242, "Chest"), + LocationName.CavernsEntranceLucidShard: LocationData(0x130083, 3, "Chest"), + LocationName.CavernsEntranceAPBoost: LocationData(0x130084, 11, "Chest"), + LocationName.CavernsEntranceMythrilShard: LocationData(0x130085, 504, "Chest"), + LocationName.TheLostRoadBrightShard: LocationData(0x130086, 9, "Chest"), + LocationName.TheLostRoadEther: LocationData(0x130087, 10, "Chest"), + LocationName.TheLostRoadMythrilShard: LocationData(0x130088, 148, "Chest"), + LocationName.TheLostRoadMythrilStone: LocationData(0x130089, 149, "Chest"), + LocationName.AtriumLucidStone: LocationData(0x13008A, 150, "Chest"), + LocationName.AtriumAPBoost: LocationData(0x13008B, 151, "Chest"), + LocationName.DemyxOC: LocationData(0x13008C, 58, "Get Bonus"), + LocationName.SecretAnsemReport5: LocationData(0x13008D, 529, "Chest"), + LocationName.OlympusStone: LocationData(0x13008E, 293, "Chest"), + LocationName.TheLockCavernsMap: LocationData(0x13008F, 244, "Chest"), + LocationName.TheLockMythrilShard: LocationData(0x130090, 5, "Chest"), + LocationName.TheLockAPBoost: LocationData(0x130091, 142, "Chest"), + LocationName.PeteOC: LocationData(0x130092, 6, "Get Bonus"), + LocationName.Hydra: LocationData(0x130093, 7, "Double Get Bonus"), + LocationName.HydraGetBonus: LocationData(0x130094, 7, "Second Get Bonus"), + LocationName.HerosCrest: LocationData(0x130095, 260, "Chest"), + +} +Oc2_Checks = { + LocationName.AuronsStatue: LocationData(0x130096, 295, "Chest"), + LocationName.Hades: LocationData(0x130097, 8, "Double Get Bonus"), + LocationName.HadesGetBonus: LocationData(0x130098, 8, "Second Get Bonus"), + LocationName.GuardianSoul: LocationData(0x130099, 272, "Chest"), + LocationName.ZexionBonus: LocationData(0x13009A, 66, "Get Bonus"), + LocationName.ZexionASBookofShadows: LocationData(0x13009B, 546, "Chest"), + LocationName.ZexionDataLostIllusion: LocationData(0x13009C, 551, "Chest"), +} +Oc2Cups = { + LocationName.ProtectBeltPainandPanicCup: LocationData(0x13009D, 513, "Chest"), + LocationName.SerenityGemPainandPanicCup: LocationData(0x13009E, 540, "Chest"), + LocationName.RisingDragonCerberusCup: LocationData(0x13009F, 515, "Chest"), + LocationName.SerenityCrystalCerberusCup: LocationData(0x1300A0, 542, "Chest"), + LocationName.GenjiShieldTitanCup: LocationData(0x1300A1, 514, "Chest"), + LocationName.SkillfulRingTitanCup: LocationData(0x1300A2, 541, "Chest"), + LocationName.FatalCrestGoddessofFateCup: LocationData(0x1300A3, 516, "Chest"), + LocationName.OrichalcumPlusGoddessofFateCup: LocationData(0x1300A4, 517, "Chest"), + LocationName.HadesCupTrophyParadoxCups: LocationData(0x1300A5, 518, "Chest"), +} + +BC_Checks = { + LocationName.BCCourtyardAPBoost: LocationData(0x1300A6, 39, "Chest"), + LocationName.BCCourtyardHiPotion: LocationData(0x1300A7, 40, "Chest"), + LocationName.BCCourtyardMythrilShard: LocationData(0x1300A8, 505, "Chest"), + LocationName.BellesRoomCastleMap: LocationData(0x1300A9, 46, "Chest"), + LocationName.BellesRoomMegaRecipe: LocationData(0x1300AA, 240, "Chest"), + LocationName.TheEastWingMythrilShard: LocationData(0x1300AB, 63, "Chest"), + LocationName.TheEastWingTent: LocationData(0x1300AC, 155, "Chest"), + LocationName.TheWestHallHiPotion: LocationData(0x1300AD, 41, "Chest"), + LocationName.TheWestHallPowerShard: LocationData(0x1300AE, 207, "Chest"), + LocationName.TheWestHallAPBoostPostDungeon: LocationData(0x1300AF, 158, "Chest"), + LocationName.TheWestHallBrightStone: LocationData(0x1300B0, 159, "Chest"), + LocationName.TheWestHallMythrilShard: LocationData(0x1300B1, 206, "Chest"), + LocationName.Thresholder: LocationData(0x1300B2, 2, "Get Bonus"), + LocationName.DungeonBasementMap: LocationData(0x1300B3, 239, "Chest"), + LocationName.DungeonAPBoost: LocationData(0x1300B4, 43, "Chest"), + LocationName.SecretPassageMythrilShard: LocationData(0x1300B5, 44, "Chest"), + LocationName.SecretPassageHiPotion: LocationData(0x1300B6, 168, "Chest"), + LocationName.SecretPassageLucidShard: LocationData(0x1300B7, 45, "Chest"), + LocationName.TheWestHallMythrilShard2: LocationData(0x1300B8, 208, "Chest"), + LocationName.TheWestWingMythrilShard: LocationData(0x1300B9, 42, "Chest"), + LocationName.TheWestWingTent: LocationData(0x1300BA, 164, "Chest"), + LocationName.Beast: LocationData(0x1300BB, 12, "Get Bonus"), + LocationName.TheBeastsRoomBlazingShard: LocationData(0x1300BC, 241, "Chest"), + LocationName.DarkThorn: LocationData(0x1300BD, 3, "Double Get Bonus"), + LocationName.DarkThornGetBonus: LocationData(0x1300BE, 3, "Second Get Bonus"), + LocationName.DarkThornCureElement: LocationData(0x1300BF, 299, "Chest"), + +} +BC2_Checks = { + LocationName.RumblingRose: LocationData(0x1300C0, 270, "Chest"), + LocationName.CastleWallsMap: LocationData(0x1300C1, 325, "Chest"), + LocationName.Xaldin: LocationData(0x1300C2, 4, "Double Get Bonus"), + LocationName.XaldinGetBonus: LocationData(0x1300C3, 4, "Second Get Bonus"), + LocationName.SecretAnsemReport4: LocationData(0x1300C4, 528, "Chest"), + LocationName.XaldinDataDefenseBoost: LocationData(0x1300C5, 559, "Chest"), +} +SP_Checks = { + LocationName.PitCellAreaMap: LocationData(0x1300C6, 316, "Chest"), + LocationName.PitCellMythrilCrystal: LocationData(0x1300C7, 64, "Chest"), + LocationName.CanyonDarkCrystal: LocationData(0x1300C8, 65, "Chest"), + LocationName.CanyonMythrilStone: LocationData(0x1300C9, 171, "Chest"), + LocationName.CanyonMythrilGem: LocationData(0x1300CA, 253, "Chest"), + LocationName.CanyonFrostCrystal: LocationData(0x1300CB, 521, "Chest"), + LocationName.Screens: LocationData(0x1300CC, 45, "Get Bonus"), + LocationName.HallwayPowerCrystal: LocationData(0x1300CD, 49, "Chest"), + LocationName.HallwayAPBoost: LocationData(0x1300CE, 50, "Chest"), + LocationName.CommunicationsRoomIOTowerMap: LocationData(0x1300CF, 255, "Chest"), + LocationName.CommunicationsRoomGaiaBelt: LocationData(0x1300D0, 499, "Chest"), + LocationName.HostileProgram: LocationData(0x1300D1, 31, "Double Get Bonus"), + LocationName.HostileProgramGetBonus: LocationData(0x1300D2, 31, "Second Get Bonus"), + LocationName.PhotonDebugger: LocationData(0x1300D3, 267, "Chest"), + +} +SP2_Checks = { + LocationName.SolarSailer: LocationData(0x1300D4, 61, "Get Bonus"), + LocationName.CentralComputerCoreAPBoost: LocationData(0x1300D5, 177, "Chest"), + LocationName.CentralComputerCoreOrichalcumPlus: LocationData(0x1300D6, 178, "Chest"), + LocationName.CentralComputerCoreCosmicArts: LocationData(0x1300D7, 51, "Chest"), + LocationName.CentralComputerCoreMap: LocationData(0x1300D8, 488, "Chest"), + LocationName.MCP: LocationData(0x1300D9, 32, "Double Get Bonus"), + LocationName.MCPGetBonus: LocationData(0x1300DA, 32, "Second Get Bonus"), + LocationName.LarxeneBonus: LocationData(0x1300DB, 68, "Get Bonus"), + LocationName.LarxeneASCloakedThunder: LocationData(0x1300DC, 547, "Chest"), + LocationName.LarxeneDataLostIllusion: LocationData(0x1300DD, 552, "Chest"), +} +HT_Checks = { + LocationName.GraveyardMythrilShard: LocationData(0x1300DE, 53, "Chest"), + LocationName.GraveyardSerenityGem: LocationData(0x1300DF, 212, "Chest"), + LocationName.FinklesteinsLabHalloweenTownMap: LocationData(0x1300E0, 211, "Chest"), + LocationName.TownSquareMythrilStone: LocationData(0x1300E1, 209, "Chest"), + LocationName.TownSquareEnergyShard: LocationData(0x1300E2, 210, "Chest"), + LocationName.HinterlandsLightningShard: LocationData(0x1300E3, 54, "Chest"), + LocationName.HinterlandsMythrilStone: LocationData(0x1300E4, 213, "Chest"), + LocationName.HinterlandsAPBoost: LocationData(0x1300E5, 214, "Chest"), + LocationName.CandyCaneLaneMegaPotion: LocationData(0x1300E6, 55, "Chest"), + LocationName.CandyCaneLaneMythrilGem: LocationData(0x1300E7, 56, "Chest"), + LocationName.CandyCaneLaneLightningStone: LocationData(0x1300E8, 216, "Chest"), + LocationName.CandyCaneLaneMythrilStone: LocationData(0x1300E9, 217, "Chest"), + LocationName.SantasHouseChristmasTownMap: LocationData(0x1300EA, 57, "Chest"), + LocationName.SantasHouseAPBoost: LocationData(0x1300EB, 58, "Chest"), + LocationName.PrisonKeeper: LocationData(0x1300EC, 18, "Get Bonus"), + LocationName.OogieBoogie: LocationData(0x1300ED, 19, "Get Bonus"), + LocationName.OogieBoogieMagnetElement: LocationData(0x1300EE, 301, "Chest"), +} +HT2_Checks = { + LocationName.Lock: LocationData(0x1300EF, 40, "Get Bonus"), + LocationName.Present: LocationData(0x1300F0, 297, "Chest"), + LocationName.DecoyPresents: LocationData(0x1300F1, 298, "Chest"), + LocationName.Experiment: LocationData(0x1300F2, 20, "Get Bonus"), + LocationName.DecisivePumpkin: LocationData(0x1300F3, 275, "Chest"), + LocationName.VexenBonus: LocationData(0x1300F4, 64, "Get Bonus"), + LocationName.VexenASRoadtoDiscovery: LocationData(0x1300F5, 544, "Chest"), + LocationName.VexenDataLostIllusion: LocationData(0x1300F6, 549, "Chest"), +} +PR_Checks = { + LocationName.RampartNavalMap: LocationData(0x1300F7, 70, "Chest"), + LocationName.RampartMythrilStone: LocationData(0x1300F8, 219, "Chest"), + LocationName.RampartDarkShard: LocationData(0x1300F9, 220, "Chest"), + LocationName.TownDarkStone: LocationData(0x1300FA, 71, "Chest"), + LocationName.TownAPBoost: LocationData(0x1300FB, 72, "Chest"), + LocationName.TownMythrilShard: LocationData(0x1300FC, 73, "Chest"), + LocationName.TownMythrilGem: LocationData(0x1300FD, 221, "Chest"), + LocationName.CaveMouthBrightShard: LocationData(0x1300FE, 74, "Chest"), + LocationName.CaveMouthMythrilShard: LocationData(0x1300FF, 223, "Chest"), + LocationName.IsladeMuertaMap: LocationData(0x130100, 329, "Chest"), + LocationName.BoatFight: LocationData(0x130101, 62, "Get Bonus"), + LocationName.InterceptorBarrels: LocationData(0x130102, 39, "Get Bonus"), + LocationName.PowderStoreAPBoost1: LocationData(0x130103, 369, "Chest"), + LocationName.PowderStoreAPBoost2: LocationData(0x130104, 370, "Chest"), + LocationName.MoonlightNookMythrilShard: LocationData(0x130105, 75, "Chest"), + LocationName.MoonlightNookSerenityGem: LocationData(0x130106, 224, "Chest"), + LocationName.MoonlightNookPowerStone: LocationData(0x130107, 371, "Chest"), + LocationName.Barbossa: LocationData(0x130108, 21, "Double Get Bonus"), + LocationName.BarbossaGetBonus: LocationData(0x130109, 21, "Second Get Bonus"), + LocationName.FollowtheWind: LocationData(0x13010A, 263, "Chest"), + +} +PR2_Checks = { + LocationName.GrimReaper1: LocationData(0x13010B, 59, "Get Bonus"), + LocationName.InterceptorsHoldFeatherCharm: LocationData(0x13010C, 252, "Chest"), + LocationName.SeadriftKeepAPBoost: LocationData(0x13010D, 76, "Chest"), + LocationName.SeadriftKeepOrichalcum: LocationData(0x13010E, 225, "Chest"), + LocationName.SeadriftKeepMeteorStaff: LocationData(0x13010F, 372, "Chest"), + LocationName.SeadriftRowSerenityGem: LocationData(0x130110, 77, "Chest"), + LocationName.SeadriftRowKingRecipe: LocationData(0x130111, 78, "Chest"), + LocationName.SeadriftRowMythrilCrystal: LocationData(0x130112, 373, "Chest"), + LocationName.SeadriftRowCursedMedallion: LocationData(0x130113, 296, "Chest"), + LocationName.SeadriftRowShipGraveyardMap: LocationData(0x130114, 331, "Chest"), + LocationName.GrimReaper2: LocationData(0x130115, 22, "Get Bonus"), + LocationName.SecretAnsemReport6: LocationData(0x130116, 530, "Chest"), + LocationName.LuxordDataAPBoost: LocationData(0x130117, 557, "Chest"), +} +HB_Checks = { + LocationName.MarketplaceMap: LocationData(0x130118, 362, "Chest"), + LocationName.BoroughDriveRecovery: LocationData(0x130119, 194, "Chest"), + LocationName.BoroughAPBoost: LocationData(0x13011A, 195, "Chest"), + LocationName.BoroughHiPotion: LocationData(0x13011B, 196, "Chest"), + LocationName.BoroughMythrilShard: LocationData(0x13011C, 305, "Chest"), + LocationName.BoroughDarkShard: LocationData(0x13011D, 506, "Chest"), + LocationName.MerlinsHouseMembershipCard: LocationData(0x13011E, 256, "Chest"), + LocationName.MerlinsHouseBlizzardElement: LocationData(0x13011F, 292, "Chest"), + LocationName.Bailey: LocationData(0x130120, 47, "Get Bonus"), + LocationName.BaileySecretAnsemReport7: LocationData(0x130121, 531, "Chest"), + LocationName.BaseballCharm: LocationData(0x130122, 258, "Chest"), +} +HB2_Checks = { + LocationName.PosternCastlePerimeterMap: LocationData(0x130123, 310, "Chest"), + LocationName.PosternMythrilGem: LocationData(0x130124, 189, "Chest"), + LocationName.PosternAPBoost: LocationData(0x130125, 190, "Chest"), + LocationName.CorridorsMythrilStone: LocationData(0x130126, 200, "Chest"), + LocationName.CorridorsMythrilCrystal: LocationData(0x130127, 201, "Chest"), + LocationName.CorridorsDarkCrystal: LocationData(0x130128, 202, "Chest"), + LocationName.CorridorsAPBoost: LocationData(0x130129, 307, "Chest"), + LocationName.AnsemsStudyMasterForm: LocationData(0x13012A, 276, "Chest"), + LocationName.AnsemsStudySleepingLion: LocationData(0x13012B, 266, "Chest"), + LocationName.AnsemsStudySkillRecipe: LocationData(0x13012C, 184, "Chest"), + LocationName.AnsemsStudyUkuleleCharm: LocationData(0x13012D, 183, "Chest"), + LocationName.RestorationSiteMoonRecipe: LocationData(0x13012E, 309, "Chest"), + LocationName.RestorationSiteAPBoost: LocationData(0x13012F, 507, "Chest"), + LocationName.DemyxHB: LocationData(0x130130, 28, "Double Get Bonus"), + LocationName.DemyxHBGetBonus: LocationData(0x130131, 28, "Second Get Bonus"), + LocationName.FFFightsCureElement: LocationData(0x130132, 361, "Chest"), + LocationName.CrystalFissureTornPages: LocationData(0x130133, 179, "Chest"), + LocationName.CrystalFissureTheGreatMawMap: LocationData(0x130134, 489, "Chest"), + LocationName.CrystalFissureEnergyCrystal: LocationData(0x130135, 180, "Chest"), + LocationName.CrystalFissureAPBoost: LocationData(0x130136, 181, "Chest"), + LocationName.ThousandHeartless: LocationData(0x130137, 60, "Get Bonus"), + LocationName.ThousandHeartlessSecretAnsemReport1: LocationData(0x130138, 525, "Chest"), + LocationName.ThousandHeartlessIceCream: LocationData(0x130139, 269, "Chest"), + LocationName.ThousandHeartlessPicture: LocationData(0x13013A, 511, "Chest"), + LocationName.PosternGullWing: LocationData(0x13013B, 491, "Chest"), + LocationName.HeartlessManufactoryCosmicChain: LocationData(0x13013C, 311, "Chest"), + LocationName.SephirothBonus: LocationData(0x13013D, 35, "Get Bonus"), + LocationName.SephirothFenrir: LocationData(0x13013E, 282, "Chest"), + LocationName.WinnersProof: LocationData(0x13013F, 588, "Chest"), + LocationName.ProofofPeace: LocationData(0x130140, 589, "Chest"), + LocationName.DemyxDataAPBoost: LocationData(0x130141, 560, "Chest"), + LocationName.CoRDepthsAPBoost: LocationData(0x130142, 562, "Chest"), + LocationName.CoRDepthsPowerCrystal: LocationData(0x130143, 563, "Chest"), + LocationName.CoRDepthsFrostCrystal: LocationData(0x130144, 564, "Chest"), + LocationName.CoRDepthsManifestIllusion: LocationData(0x130145, 565, "Chest"), + LocationName.CoRDepthsAPBoost2: LocationData(0x130146, 566, "Chest"), + LocationName.CoRMineshaftLowerLevelDepthsofRemembranceMap: LocationData(0x130147, 580, "Chest"), + LocationName.CoRMineshaftLowerLevelAPBoost: LocationData(0x130148, 578, "Chest"), + +} +CoR_Checks = { + LocationName.CoRDepthsUpperLevelRemembranceGem: LocationData(0x130149, 567, "Chest"), + LocationName.CoRMiningAreaSerenityGem: LocationData(0x13014A, 568, "Chest"), + LocationName.CoRMiningAreaAPBoost: LocationData(0x13014B, 569, "Chest"), + LocationName.CoRMiningAreaSerenityCrystal: LocationData(0x13014C, 570, "Chest"), + LocationName.CoRMiningAreaManifestIllusion: LocationData(0x13014D, 571, "Chest"), + LocationName.CoRMiningAreaSerenityGem2: LocationData(0x13014E, 572, "Chest"), + LocationName.CoRMiningAreaDarkRemembranceMap: LocationData(0x13014F, 573, "Chest"), + LocationName.CoRMineshaftMidLevelPowerBoost: LocationData(0x130150, 581, "Chest"), + LocationName.CoREngineChamberSerenityCrystal: LocationData(0x130151, 574, "Chest"), + LocationName.CoREngineChamberRemembranceCrystal: LocationData(0x130152, 575, "Chest"), + LocationName.CoREngineChamberAPBoost: LocationData(0x130153, 576, "Chest"), + LocationName.CoREngineChamberManifestIllusion: LocationData(0x130154, 577, "Chest"), + LocationName.CoRMineshaftUpperLevelMagicBoost: LocationData(0x130155, 582, "Chest"), + LocationName.CoRMineshaftUpperLevelAPBoost: LocationData(0x130156, 579, "Chest"), + LocationName.TransporttoRemembrance: LocationData(0x130157, 72, "Get Bonus"), +} +PL_Checks = { + LocationName.GorgeSavannahMap: LocationData(0x130158, 492, "Chest"), + LocationName.GorgeDarkGem: LocationData(0x130159, 404, "Chest"), + LocationName.GorgeMythrilStone: LocationData(0x13015A, 405, "Chest"), + LocationName.ElephantGraveyardFrostGem: LocationData(0x13015B, 401, "Chest"), + LocationName.ElephantGraveyardMythrilStone: LocationData(0x13015C, 402, "Chest"), + LocationName.ElephantGraveyardBrightStone: LocationData(0x13015D, 403, "Chest"), + LocationName.ElephantGraveyardAPBoost: LocationData(0x13015E, 508, "Chest"), + LocationName.ElephantGraveyardMythrilShard: LocationData(0x13015F, 509, "Chest"), + LocationName.PrideRockMap: LocationData(0x130160, 418, "Chest"), + LocationName.PrideRockMythrilStone: LocationData(0x130161, 392, "Chest"), + LocationName.PrideRockSerenityCrystal: LocationData(0x130162, 393, "Chest"), + LocationName.WildebeestValleyEnergyStone: LocationData(0x130163, 396, "Chest"), + LocationName.WildebeestValleyAPBoost: LocationData(0x130164, 397, "Chest"), + LocationName.WildebeestValleyMythrilGem: LocationData(0x130165, 398, "Chest"), + LocationName.WildebeestValleyMythrilStone: LocationData(0x130166, 399, "Chest"), + LocationName.WildebeestValleyLucidGem: LocationData(0x130167, 400, "Chest"), + LocationName.WastelandsMythrilShard: LocationData(0x130168, 406, "Chest"), + LocationName.WastelandsSerenityGem: LocationData(0x130169, 407, "Chest"), + LocationName.WastelandsMythrilStone: LocationData(0x13016A, 408, "Chest"), + LocationName.JungleSerenityGem: LocationData(0x13016B, 409, "Chest"), + LocationName.JungleMythrilStone: LocationData(0x13016C, 410, "Chest"), + LocationName.JungleSerenityCrystal: LocationData(0x13016D, 411, "Chest"), + LocationName.OasisMap: LocationData(0x13016E, 412, "Chest"), + LocationName.OasisTornPages: LocationData(0x13016F, 493, "Chest"), + LocationName.OasisAPBoost: LocationData(0x130170, 413, "Chest"), + LocationName.CircleofLife: LocationData(0x130171, 264, "Chest"), + LocationName.Hyenas1: LocationData(0x130172, 49, "Get Bonus"), + LocationName.Scar: LocationData(0x130173, 29, "Get Bonus"), + LocationName.ScarFireElement: LocationData(0x130174, 302, "Chest"), + +} +PL2_Checks = { + LocationName.Hyenas2: LocationData(0x130175, 50, "Get Bonus"), + LocationName.Groundshaker: LocationData(0x130176, 30, "Double Get Bonus"), + LocationName.GroundshakerGetBonus: LocationData(0x130177, 30, "Second Get Bonus"), + LocationName.SaixDataDefenseBoost: LocationData(0x130178, 556, "Chest"), +} +STT_Checks = { + LocationName.TwilightTownMap: LocationData(0x130179, 319, "Chest"), + LocationName.MunnyPouchOlette: LocationData(0x13017A, 288, "Chest"), + LocationName.StationDusks: LocationData(0x13017B, 54, "Get Bonus", "Roxas", 14), + LocationName.StationofSerenityPotion: LocationData(0x13017C, 315, "Chest"), + LocationName.StationofCallingPotion: LocationData(0x13017D, 472, "Chest"), + LocationName.TwilightThorn: LocationData(0x13017E, 33, "Get Bonus", "Roxas", 14), + LocationName.Axel1: LocationData(0x13017F, 73, "Get Bonus", "Roxas", 14), + LocationName.JunkChampionBelt: LocationData(0x130180, 389, "Chest"), + LocationName.JunkMedal: LocationData(0x130181, 390, "Chest"), + LocationName.TheStruggleTrophy: LocationData(0x130182, 519, "Chest"), + LocationName.CentralStationPotion1: LocationData(0x130183, 428, "Chest"), + LocationName.STTCentralStationHiPotion: LocationData(0x130184, 429, "Chest"), + LocationName.CentralStationPotion2: LocationData(0x130185, 430, "Chest"), + LocationName.SunsetTerraceAbilityRing: LocationData(0x130186, 434, "Chest"), + LocationName.SunsetTerraceHiPotion: LocationData(0x130187, 435, "Chest"), + LocationName.SunsetTerracePotion1: LocationData(0x130188, 436, "Chest"), + LocationName.SunsetTerracePotion2: LocationData(0x130189, 437, "Chest"), + LocationName.MansionFoyerHiPotion: LocationData(0x13018A, 449, "Chest"), + LocationName.MansionFoyerPotion1: LocationData(0x13018B, 450, "Chest"), + LocationName.MansionFoyerPotion2: LocationData(0x13018C, 451, "Chest"), + LocationName.MansionDiningRoomElvenBandanna: LocationData(0x13018D, 455, "Chest"), + LocationName.MansionDiningRoomPotion: LocationData(0x13018E, 456, "Chest"), + LocationName.NaminesSketches: LocationData(0x13018F, 289, "Chest"), + LocationName.MansionMap: LocationData(0x130190, 483, "Chest"), + LocationName.MansionLibraryHiPotion: LocationData(0x130191, 459, "Chest"), + LocationName.Axel2: LocationData(0x130192, 34, "Get Bonus", "Roxas", 14), + LocationName.MansionBasementCorridorHiPotion: LocationData(0x130193, 463, "Chest"), + LocationName.RoxasDataMagicBoost: LocationData(0x130194, 558, "Chest"), + +} +TT_Checks = { + LocationName.OldMansionPotion: LocationData(0x130195, 447, "Chest"), + LocationName.OldMansionMythrilShard: LocationData(0x130196, 448, "Chest"), + LocationName.TheWoodsPotion: LocationData(0x130197, 442, "Chest"), + LocationName.TheWoodsMythrilShard: LocationData(0x130198, 443, "Chest"), + LocationName.TheWoodsHiPotion: LocationData(0x130199, 444, "Chest"), + LocationName.TramCommonHiPotion: LocationData(0x13019A, 420, "Chest"), + LocationName.TramCommonAPBoost: LocationData(0x13019B, 421, "Chest"), + LocationName.TramCommonTent: LocationData(0x13019C, 422, "Chest"), + LocationName.TramCommonMythrilShard1: LocationData(0x13019D, 423, "Chest"), + LocationName.TramCommonPotion1: LocationData(0x13019E, 424, "Chest"), + LocationName.TramCommonMythrilShard2: LocationData(0x13019F, 425, "Chest"), + LocationName.TramCommonPotion2: LocationData(0x1301A0, 484, "Chest"), + LocationName.StationPlazaSecretAnsemReport2: LocationData(0x1301A1, 526, "Chest"), + LocationName.MunnyPouchMickey: LocationData(0x1301A2, 290, "Chest"), + LocationName.CrystalOrb: LocationData(0x1301A3, 291, "Chest"), + LocationName.CentralStationTent: LocationData(0x1301A4, 431, "Chest"), + LocationName.TTCentralStationHiPotion: LocationData(0x1301A5, 432, "Chest"), + LocationName.CentralStationMythrilShard: LocationData(0x1301A6, 433, "Chest"), + LocationName.TheTowerPotion: LocationData(0x1301A7, 465, "Chest"), + LocationName.TheTowerHiPotion: LocationData(0x1301A8, 466, "Chest"), + LocationName.TheTowerEther: LocationData(0x1301A9, 522, "Chest"), + LocationName.TowerEntrywayEther: LocationData(0x1301AA, 467, "Chest"), + LocationName.TowerEntrywayMythrilShard: LocationData(0x1301AB, 468, "Chest"), + LocationName.SorcerersLoftTowerMap: LocationData(0x1301AC, 469, "Chest"), + LocationName.TowerWardrobeMythrilStone: LocationData(0x1301AD, 470, "Chest"), + LocationName.StarSeeker: LocationData(0x1301AE, 304, "Chest"), + LocationName.ValorForm: LocationData(0x1301AF, 286, "Chest"), + +} +TT2_Checks = { + LocationName.SeifersTrophy: LocationData(0x1301B0, 294, "Chest"), + LocationName.Oathkeeper: LocationData(0x1301B1, 265, "Chest"), + LocationName.LimitForm: LocationData(0x1301B2, 543, "Chest"), +} +TT3_Checks = { + LocationName.UndergroundConcourseMythrilGem: LocationData(0x1301B3, 479, "Chest"), + LocationName.UndergroundConcourseAPBoost: LocationData(0x1301B4, 481, "Chest"), + LocationName.UndergroundConcourseOrichalcum: LocationData(0x1301B5, 480, "Chest"), + LocationName.UndergroundConcourseMythrilCrystal: LocationData(0x1301B6, 482, "Chest"), + LocationName.TunnelwayOrichalcum: LocationData(0x1301B7, 477, "Chest"), + LocationName.TunnelwayMythrilCrystal: LocationData(0x1301B8, 478, "Chest"), + LocationName.SunsetTerraceOrichalcumPlus: LocationData(0x1301B9, 438, "Chest"), + LocationName.SunsetTerraceMythrilShard: LocationData(0x1301BA, 439, "Chest"), + LocationName.SunsetTerraceMythrilCrystal: LocationData(0x1301BB, 440, "Chest"), + LocationName.SunsetTerraceAPBoost: LocationData(0x1301BC, 441, "Chest"), + LocationName.MansionNobodies: LocationData(0x1301BD, 56, "Get Bonus"), + LocationName.MansionFoyerMythrilCrystal: LocationData(0x1301BE, 452, "Chest"), + LocationName.MansionFoyerMythrilStone: LocationData(0x1301BF, 453, "Chest"), + LocationName.MansionFoyerSerenityCrystal: LocationData(0x1301C0, 454, "Chest"), + LocationName.MansionDiningRoomMythrilCrystal: LocationData(0x1301C1, 457, "Chest"), + LocationName.MansionDiningRoomMythrilStone: LocationData(0x1301C2, 458, "Chest"), + LocationName.MansionLibraryOrichalcum: LocationData(0x1301C3, 460, "Chest"), + LocationName.BeamSecretAnsemReport10: LocationData(0x1301C4, 534, "Chest"), + LocationName.MansionBasementCorridorUltimateRecipe: LocationData(0x1301C5, 464, "Chest"), + LocationName.BetwixtandBetween: LocationData(0x1301C6, 63, "Get Bonus"), + LocationName.BetwixtandBetweenBondofFlame: LocationData(0x1301C7, 317, "Chest"), + LocationName.AxelDataMagicBoost: LocationData(0x1301C8, 561, "Chest"), +} +TWTNW_Checks = { + LocationName.FragmentCrossingMythrilStone: LocationData(0x1301C9, 374, "Chest"), + LocationName.FragmentCrossingMythrilCrystal: LocationData(0x1301CA, 375, "Chest"), + LocationName.FragmentCrossingAPBoost: LocationData(0x1301CB, 376, "Chest"), + LocationName.FragmentCrossingOrichalcum: LocationData(0x1301CC, 377, "Chest"), + LocationName.Roxas: LocationData(0x1301CD, 69, "Double Get Bonus"), + LocationName.RoxasGetBonus: LocationData(0x1301CE, 69, "Second Get Bonus"), + LocationName.RoxasSecretAnsemReport8: LocationData(0x1301CF, 532, "Chest"), + LocationName.TwoBecomeOne: LocationData(0x1301D0, 277, "Chest"), + LocationName.MemorysSkyscaperMythrilCrystal: LocationData(0x1301D1, 391, "Chest"), + LocationName.MemorysSkyscaperAPBoost: LocationData(0x1301D2, 523, "Chest"), + LocationName.MemorysSkyscaperMythrilStone: LocationData(0x1301D3, 524, "Chest"), + LocationName.TheBrinkofDespairDarkCityMap: LocationData(0x1301D4, 335, "Chest"), + LocationName.TheBrinkofDespairOrichalcumPlus: LocationData(0x1301D5, 500, "Chest"), + LocationName.NothingsCallMythrilGem: LocationData(0x1301D6, 378, "Chest"), + LocationName.NothingsCallOrichalcum: LocationData(0x1301D7, 379, "Chest"), + LocationName.TwilightsViewCosmicBelt: LocationData(0x1301D8, 336, "Chest"), +} +TWTNW2_Checks = { + LocationName.XigbarBonus: LocationData(0x1301D9, 23, "Get Bonus"), + LocationName.XigbarSecretAnsemReport3: LocationData(0x1301DA, 527, "Chest"), + LocationName.NaughtsSkywayMythrilGem: LocationData(0x1301DB, 380, "Chest"), + LocationName.NaughtsSkywayOrichalcum: LocationData(0x1301DC, 381, "Chest"), + LocationName.NaughtsSkywayMythrilCrystal: LocationData(0x1301DD, 382, "Chest"), + LocationName.Oblivion: LocationData(0x1301DE, 278, "Chest"), + LocationName.CastleThatNeverWasMap: LocationData(0x1301DF, 496, "Chest"), + LocationName.Luxord: LocationData(0x1301E0, 24, "Double Get Bonus"), + LocationName.LuxordGetBonus: LocationData(0x1301E1, 24, "Second Get Bonus"), + LocationName.LuxordSecretAnsemReport9: LocationData(0x1301E2, 533, "Chest"), + LocationName.SaixBonus: LocationData(0x1301E3, 25, "Get Bonus"), + LocationName.SaixSecretAnsemReport12: LocationData(0x1301E4, 536, "Chest"), + LocationName.PreXemnas1SecretAnsemReport11: LocationData(0x1301E5, 535, "Chest"), + LocationName.RuinandCreationsPassageMythrilStone: LocationData(0x1301E6, 385, "Chest"), + LocationName.RuinandCreationsPassageAPBoost: LocationData(0x1301E7, 386, "Chest"), + LocationName.RuinandCreationsPassageMythrilCrystal: LocationData(0x1301E8, 387, "Chest"), + LocationName.RuinandCreationsPassageOrichalcum: LocationData(0x1301E9, 388, "Chest"), + LocationName.Xemnas1: LocationData(0x1301EA, 26, "Double Get Bonus"), + LocationName.Xemnas1GetBonus: LocationData(0x1301EB, 26, "Second Get Bonus"), + LocationName.Xemnas1SecretAnsemReport13: LocationData(0x1301EC, 537, "Chest"), + LocationName.FinalXemnas: LocationData(0x1301ED, 71, "Get Bonus"), + LocationName.XemnasDataPowerBoost: LocationData(0x1301EE, 554, "Chest"), +} + +SoraLevels = { + LocationName.Lvl1: LocationData(0x1301EF, 1, "Levels"), + LocationName.Lvl2: LocationData(0x1301F0, 2, "Levels"), + LocationName.Lvl3: LocationData(0x1301F1, 3, "Levels"), + LocationName.Lvl4: LocationData(0x1301F2, 4, "Levels"), + LocationName.Lvl5: LocationData(0x1301F3, 5, "Levels"), + LocationName.Lvl6: LocationData(0x1301F4, 6, "Levels"), + LocationName.Lvl7: LocationData(0x1301F5, 7, "Levels"), + LocationName.Lvl8: LocationData(0x1301F6, 8, "Levels"), + LocationName.Lvl9: LocationData(0x1301F7, 9, "Levels"), + LocationName.Lvl10: LocationData(0x1301F8, 10, "Levels"), + LocationName.Lvl11: LocationData(0x1301F9, 11, "Levels"), + LocationName.Lvl12: LocationData(0x1301FA, 12, "Levels"), + LocationName.Lvl13: LocationData(0x1301FB, 13, "Levels"), + LocationName.Lvl14: LocationData(0x1301FC, 14, "Levels"), + LocationName.Lvl15: LocationData(0x1301FD, 15, "Levels"), + LocationName.Lvl16: LocationData(0x1301FE, 16, "Levels"), + LocationName.Lvl17: LocationData(0x1301FF, 17, "Levels"), + LocationName.Lvl18: LocationData(0x130200, 18, "Levels"), + LocationName.Lvl19: LocationData(0x130201, 19, "Levels"), + LocationName.Lvl20: LocationData(0x130202, 20, "Levels"), + LocationName.Lvl21: LocationData(0x130203, 21, "Levels"), + LocationName.Lvl22: LocationData(0x130204, 22, "Levels"), + LocationName.Lvl23: LocationData(0x130205, 23, "Levels"), + LocationName.Lvl24: LocationData(0x130206, 24, "Levels"), + LocationName.Lvl25: LocationData(0x130207, 25, "Levels"), + LocationName.Lvl26: LocationData(0x130208, 26, "Levels"), + LocationName.Lvl27: LocationData(0x130209, 27, "Levels"), + LocationName.Lvl28: LocationData(0x13020A, 28, "Levels"), + LocationName.Lvl29: LocationData(0x13020B, 29, "Levels"), + LocationName.Lvl30: LocationData(0x13020C, 30, "Levels"), + LocationName.Lvl31: LocationData(0x13020D, 31, "Levels"), + LocationName.Lvl32: LocationData(0x13020E, 32, "Levels"), + LocationName.Lvl33: LocationData(0x13020F, 33, "Levels"), + LocationName.Lvl34: LocationData(0x130210, 34, "Levels"), + LocationName.Lvl35: LocationData(0x130211, 35, "Levels"), + LocationName.Lvl36: LocationData(0x130212, 36, "Levels"), + LocationName.Lvl37: LocationData(0x130213, 37, "Levels"), + LocationName.Lvl38: LocationData(0x130214, 38, "Levels"), + LocationName.Lvl39: LocationData(0x130215, 39, "Levels"), + LocationName.Lvl40: LocationData(0x130216, 40, "Levels"), + LocationName.Lvl41: LocationData(0x130217, 41, "Levels"), + LocationName.Lvl42: LocationData(0x130218, 42, "Levels"), + LocationName.Lvl43: LocationData(0x130219, 43, "Levels"), + LocationName.Lvl44: LocationData(0x13021A, 44, "Levels"), + LocationName.Lvl45: LocationData(0x13021B, 45, "Levels"), + LocationName.Lvl46: LocationData(0x13021C, 46, "Levels"), + LocationName.Lvl47: LocationData(0x13021D, 47, "Levels"), + LocationName.Lvl48: LocationData(0x13021E, 48, "Levels"), + LocationName.Lvl49: LocationData(0x13021F, 49, "Levels"), + LocationName.Lvl50: LocationData(0x130220, 50, "Levels"), + LocationName.Lvl51: LocationData(0x130221, 51, "Levels"), + LocationName.Lvl52: LocationData(0x130222, 52, "Levels"), + LocationName.Lvl53: LocationData(0x130223, 53, "Levels"), + LocationName.Lvl54: LocationData(0x130224, 54, "Levels"), + LocationName.Lvl55: LocationData(0x130225, 55, "Levels"), + LocationName.Lvl56: LocationData(0x130226, 56, "Levels"), + LocationName.Lvl57: LocationData(0x130227, 57, "Levels"), + LocationName.Lvl58: LocationData(0x130228, 58, "Levels"), + LocationName.Lvl59: LocationData(0x130229, 59, "Levels"), + LocationName.Lvl60: LocationData(0x13022A, 60, "Levels"), + LocationName.Lvl61: LocationData(0x13022B, 61, "Levels"), + LocationName.Lvl62: LocationData(0x13022C, 62, "Levels"), + LocationName.Lvl63: LocationData(0x13022D, 63, "Levels"), + LocationName.Lvl64: LocationData(0x13022E, 64, "Levels"), + LocationName.Lvl65: LocationData(0x13022F, 65, "Levels"), + LocationName.Lvl66: LocationData(0x130230, 66, "Levels"), + LocationName.Lvl67: LocationData(0x130231, 67, "Levels"), + LocationName.Lvl68: LocationData(0x130232, 68, "Levels"), + LocationName.Lvl69: LocationData(0x130233, 69, "Levels"), + LocationName.Lvl70: LocationData(0x130234, 70, "Levels"), + LocationName.Lvl71: LocationData(0x130235, 71, "Levels"), + LocationName.Lvl72: LocationData(0x130236, 72, "Levels"), + LocationName.Lvl73: LocationData(0x130237, 73, "Levels"), + LocationName.Lvl74: LocationData(0x130238, 74, "Levels"), + LocationName.Lvl75: LocationData(0x130239, 75, "Levels"), + LocationName.Lvl76: LocationData(0x13023A, 76, "Levels"), + LocationName.Lvl77: LocationData(0x13023B, 77, "Levels"), + LocationName.Lvl78: LocationData(0x13023C, 78, "Levels"), + LocationName.Lvl79: LocationData(0x13023D, 79, "Levels"), + LocationName.Lvl80: LocationData(0x13023E, 80, "Levels"), + LocationName.Lvl81: LocationData(0x13023F, 81, "Levels"), + LocationName.Lvl82: LocationData(0x130240, 82, "Levels"), + LocationName.Lvl83: LocationData(0x130241, 83, "Levels"), + LocationName.Lvl84: LocationData(0x130242, 84, "Levels"), + LocationName.Lvl85: LocationData(0x130243, 85, "Levels"), + LocationName.Lvl86: LocationData(0x130244, 86, "Levels"), + LocationName.Lvl87: LocationData(0x130245, 87, "Levels"), + LocationName.Lvl88: LocationData(0x130246, 88, "Levels"), + LocationName.Lvl89: LocationData(0x130247, 89, "Levels"), + LocationName.Lvl90: LocationData(0x130248, 90, "Levels"), + LocationName.Lvl91: LocationData(0x130249, 91, "Levels"), + LocationName.Lvl92: LocationData(0x13024A, 92, "Levels"), + LocationName.Lvl93: LocationData(0x13024B, 93, "Levels"), + LocationName.Lvl94: LocationData(0x13024C, 94, "Levels"), + LocationName.Lvl95: LocationData(0x13024D, 95, "Levels"), + LocationName.Lvl96: LocationData(0x13024E, 96, "Levels"), + LocationName.Lvl97: LocationData(0x13024F, 97, "Levels"), + LocationName.Lvl98: LocationData(0x130250, 98, "Levels"), + LocationName.Lvl99: LocationData(0x130251, 99, "Levels"), +} +Form_Checks = { + LocationName.Valorlvl2: LocationData(0x130253, 2, "Forms", 1), + LocationName.Valorlvl3: LocationData(0x130254, 3, "Forms", 1), + LocationName.Valorlvl4: LocationData(0x130255, 4, "Forms", 1), + LocationName.Valorlvl5: LocationData(0x130256, 5, "Forms", 1), + LocationName.Valorlvl6: LocationData(0x130257, 6, "Forms", 1), + LocationName.Valorlvl7: LocationData(0x130258, 7, "Forms", 1), + + LocationName.Wisdomlvl2: LocationData(0x13025A, 2, "Forms", 2), + LocationName.Wisdomlvl3: LocationData(0x13025B, 3, "Forms", 2), + LocationName.Wisdomlvl4: LocationData(0x13025C, 4, "Forms", 2), + LocationName.Wisdomlvl5: LocationData(0x13025D, 5, "Forms", 2), + LocationName.Wisdomlvl6: LocationData(0x13025E, 6, "Forms", 2), + LocationName.Wisdomlvl7: LocationData(0x13025F, 7, "Forms", 2), + + LocationName.Limitlvl2: LocationData(0x130261, 2, "Forms", 3), + LocationName.Limitlvl3: LocationData(0x130262, 3, "Forms", 3), + LocationName.Limitlvl4: LocationData(0x130263, 4, "Forms", 3), + LocationName.Limitlvl5: LocationData(0x130264, 5, "Forms", 3), + LocationName.Limitlvl6: LocationData(0x130265, 6, "Forms", 3), + LocationName.Limitlvl7: LocationData(0x130266, 7, "Forms", 3), + + LocationName.Masterlvl2: LocationData(0x130268, 2, "Forms", 4), + LocationName.Masterlvl3: LocationData(0x130269, 3, "Forms", 4), + LocationName.Masterlvl4: LocationData(0x13026A, 4, "Forms", 4), + LocationName.Masterlvl5: LocationData(0x13026B, 5, "Forms", 4), + LocationName.Masterlvl6: LocationData(0x13026C, 6, "Forms", 4), + LocationName.Masterlvl7: LocationData(0x13026D, 7, "Forms", 4), + + LocationName.Finallvl2: LocationData(0x13026F, 2, "Forms", 5), + LocationName.Finallvl3: LocationData(0x130270, 3, "Forms", 5), + LocationName.Finallvl4: LocationData(0x130271, 4, "Forms", 5), + LocationName.Finallvl5: LocationData(0x130272, 5, "Forms", 5), + LocationName.Finallvl6: LocationData(0x130273, 6, "Forms", 5), + LocationName.Finallvl7: LocationData(0x130274, 7, "Forms", 5), +} +GoA_Checks = { + LocationName.GardenofAssemblageMap: LocationData(0x130275, 585, "Chest"), + LocationName.GoALostIllusion: LocationData(0x130276, 586, "Chest"), + LocationName.ProofofNonexistence: LocationData(0x130277, 590, "Chest"), +} +Keyblade_Slots = { + LocationName.FAKESlot: LocationData(0x130278, 116, "Keyblade"), + LocationName.DetectionSaberSlot: LocationData(0x130279, 83, "Keyblade"), + LocationName.EdgeofUltimaSlot: LocationData(0x13027A, 84, "Keyblade"), + LocationName.KingdomKeySlot: LocationData(0x13027B, 80, "Keyblade"), + LocationName.OathkeeperSlot: LocationData(0x13027C, 81, "Keyblade"), + LocationName.OblivionSlot: LocationData(0x13027D, 82, "Keyblade"), + LocationName.StarSeekerSlot: LocationData(0x13027E, 123, "Keyblade"), + LocationName.HiddenDragonSlot: LocationData(0x13027F, 124, "Keyblade"), + LocationName.HerosCrestSlot: LocationData(0x130280, 127, "Keyblade"), + LocationName.MonochromeSlot: LocationData(0x130281, 128, "Keyblade"), + LocationName.FollowtheWindSlot: LocationData(0x130282, 129, "Keyblade"), + LocationName.CircleofLifeSlot: LocationData(0x130283, 130, "Keyblade"), + LocationName.PhotonDebuggerSlot: LocationData(0x130284, 131, "Keyblade"), + LocationName.GullWingSlot: LocationData(0x130285, 132, "Keyblade"), + LocationName.RumblingRoseSlot: LocationData(0x130286, 133, "Keyblade"), + LocationName.GuardianSoulSlot: LocationData(0x130287, 134, "Keyblade"), + LocationName.WishingLampSlot: LocationData(0x130288, 135, "Keyblade"), + LocationName.DecisivePumpkinSlot: LocationData(0x130289, 136, "Keyblade"), + LocationName.SweetMemoriesSlot: LocationData(0x13028A, 138, "Keyblade"), + LocationName.MysteriousAbyssSlot: LocationData(0x13028B, 139, "Keyblade"), + LocationName.SleepingLionSlot: LocationData(0x13028C, 137, "Keyblade"), + LocationName.BondofFlameSlot: LocationData(0x13028D, 141, "Keyblade"), + LocationName.TwoBecomeOneSlot: LocationData(0x13028E, 148, "Keyblade"), + LocationName.FatalCrestSlot: LocationData(0x13028F, 140, "Keyblade"), + LocationName.FenrirSlot: LocationData(0x130290, 142, "Keyblade"), + LocationName.UltimaWeaponSlot: LocationData(0x130291, 143, "Keyblade"), + LocationName.WinnersProofSlot: LocationData(0x130292, 149, "Keyblade"), + LocationName.PurebloodSlot: LocationData(0x1302DB, 85, "Keyblade"), +} +# checks are given when talking to the computer in the GoA +Critical_Checks = { + LocationName.Crit_1: LocationData(0x130293, 1, "Critical"), + LocationName.Crit_2: LocationData(0x130294, 1, "Critical"), + LocationName.Crit_3: LocationData(0x130295, 1, "Critical"), + LocationName.Crit_4: LocationData(0x130296, 1, "Critical"), + LocationName.Crit_5: LocationData(0x130297, 1, "Critical"), + LocationName.Crit_6: LocationData(0x130298, 1, "Critical"), + LocationName.Crit_7: LocationData(0x130299, 1, "Critical"), +} + +Donald_Checks = { + LocationName.DonaldScreens: LocationData(0x13029A, 45, "Get Bonus", "Donald", 2), + LocationName.DonaldDemyxHBGetBonus: LocationData(0x13029B, 28, "Get Bonus", "Donald", 2), + LocationName.DonaldDemyxOC: LocationData(0x13029C, 58, "Get Bonus", "Donald", 2), + LocationName.DonaldBoatPete: LocationData(0x13029D, 16, "Double Get Bonus", "Donald", 2), + LocationName.DonaldBoatPeteGetBonus: LocationData(0x13029E, 16, "Second Get Bonus", "Donald", 2), + LocationName.DonaldPrisonKeeper: LocationData(0x13029F, 18, "Get Bonus", "Donald", 2), + LocationName.DonaldScar: LocationData(0x1302A0, 29, "Get Bonus", "Donald", 2), + LocationName.DonaldSolarSailer: LocationData(0x1302A1, 61, "Get Bonus", "Donald", 2), + LocationName.DonaldExperiment: LocationData(0x1302A2, 20, "Get Bonus", "Donald", 2), + LocationName.DonaldBoatFight: LocationData(0x1302A3, 62, "Get Bonus", "Donald", 2), + LocationName.DonaldMansionNobodies: LocationData(0x1302A4, 56, "Get Bonus", "Donald", 2), + LocationName.DonaldThresholder: LocationData(0x1302A5, 2, "Get Bonus", "Donald", 2), + LocationName.DonaldXaldinGetBonus: LocationData(0x1302A6, 4, "Get Bonus", "Donald", 2), + LocationName.DonaladGrimReaper2: LocationData(0x1302A7, 22, "Get Bonus", "Donald", 2), + + LocationName.CometStaff: LocationData(0x1302A8, 90, "Keyblade", "Donald"), + LocationName.HammerStaff: LocationData(0x1302A9, 87, "Keyblade", "Donald"), + LocationName.LordsBroom: LocationData(0x1302AA, 91, "Keyblade", "Donald"), + LocationName.MagesStaff: LocationData(0x1302AB, 86, "Keyblade", "Donald"), + LocationName.MeteorStaff: LocationData(0x1302AC, 89, "Keyblade", "Donald"), + LocationName.NobodyLance: LocationData(0x1302AD, 94, "Keyblade", "Donald"), + LocationName.PreciousMushroom: LocationData(0x1302AE, 154, "Keyblade", "Donald"), + LocationName.PreciousMushroom2: LocationData(0x1302AF, 155, "Keyblade", "Donald"), + LocationName.PremiumMushroom: LocationData(0x1302B0, 156, "Keyblade", "Donald"), + LocationName.RisingDragon: LocationData(0x1302B1, 93, "Keyblade", "Donald"), + LocationName.SaveTheQueen2: LocationData(0x1302B2, 146, "Keyblade", "Donald"), + LocationName.ShamansRelic: LocationData(0x1302B3, 95, "Keyblade", "Donald"), + LocationName.VictoryBell: LocationData(0x1302B4, 88, "Keyblade", "Donald"), + LocationName.WisdomWand: LocationData(0x1302B5, 92, "Keyblade", "Donald"), + LocationName.Centurion2: LocationData(0x1302B6, 151, "Keyblade", "Donald"), + LocationName.DonaldAbuEscort: LocationData(0x1302B7, 42, "Get Bonus", "Donald", 2), + LocationName.DonaldStarting1: LocationData(0x1302B8, 2, "Critical", "Donald"), + LocationName.DonaldStarting2: LocationData(0x1302B9, 2, "Critical", "Donald"), +} + +Goofy_Checks = { + LocationName.GoofyBarbossa: LocationData(0x1302BA, 21, "Double Get Bonus", "Goofy", 3), + LocationName.GoofyBarbossaGetBonus: LocationData(0x1302BB, 21, "Second Get Bonus", "Goofy", 3), + LocationName.GoofyGrimReaper1: LocationData(0x1302BC, 59, "Get Bonus", "Goofy", 3), + LocationName.GoofyHostileProgram: LocationData(0x1302BD, 31, "Get Bonus", "Goofy", 3), + LocationName.GoofyHyenas1: LocationData(0x1302BE, 49, "Get Bonus", "Goofy", 3), + LocationName.GoofyHyenas2: LocationData(0x1302BF, 50, "Get Bonus", "Goofy", 3), + LocationName.GoofyLock: LocationData(0x1302C0, 40, "Get Bonus", "Goofy", 3), + LocationName.GoofyOogieBoogie: LocationData(0x1302C1, 19, "Get Bonus", "Goofy", 3), + LocationName.GoofyPeteOC: LocationData(0x1302C2, 6, "Get Bonus", "Goofy", 3), + LocationName.GoofyFuturePete: LocationData(0x1302C3, 17, "Get Bonus", "Goofy", 3), + LocationName.GoofyShanYu: LocationData(0x1302C4, 9, "Get Bonus", "Goofy", 3), + LocationName.GoofyStormRider: LocationData(0x1302C5, 10, "Get Bonus", "Goofy", 3), + LocationName.GoofyBeast: LocationData(0x1302C6, 12, "Get Bonus", "Goofy", 3), + LocationName.GoofyInterceptorBarrels: LocationData(0x1302C7, 39, "Get Bonus", "Goofy", 3), + LocationName.GoofyTreasureRoom: LocationData(0x1302C8, 46, "Get Bonus", "Goofy", 3), + LocationName.GoofyZexion: LocationData(0x1302C9, 66, "Get Bonus", "Goofy", 3), + + LocationName.AdamantShield: LocationData(0x1302CA, 100, "Keyblade", "Goofy"), + LocationName.AkashicRecord: LocationData(0x1302CB, 107, "Keyblade", "Goofy"), + LocationName.ChainGear: LocationData(0x1302CC, 101, "Keyblade", "Goofy"), + LocationName.DreamCloud: LocationData(0x1302CD, 104, "Keyblade", "Goofy"), + LocationName.FallingStar: LocationData(0x1302CE, 103, "Keyblade", "Goofy"), + LocationName.FrozenPride2: LocationData(0x1302CF, 158, "Keyblade", "Goofy"), + LocationName.GenjiShield: LocationData(0x1302D0, 106, "Keyblade", "Goofy"), + LocationName.KnightDefender: LocationData(0x1302D1, 105, "Keyblade", "Goofy"), + LocationName.KnightsShield: LocationData(0x1302D2, 99, "Keyblade", "Goofy"), + LocationName.MajesticMushroom: LocationData(0x1302D3, 161, "Keyblade", "Goofy"), + LocationName.MajesticMushroom2: LocationData(0x1302D4, 162, "Keyblade", "Goofy"), + LocationName.NobodyGuard: LocationData(0x1302D5, 108, "Keyblade", "Goofy"), + LocationName.OgreShield: LocationData(0x1302D6, 102, "Keyblade", "Goofy"), + LocationName.SaveTheKing2: LocationData(0x1302D7, 147, "Keyblade", "Goofy"), + LocationName.UltimateMushroom: LocationData(0x1302D8, 163, "Keyblade", "Goofy"), + LocationName.GoofyStarting1: LocationData(0x1302D9, 3, "Critical", "Goofy"), + LocationName.GoofyStarting2: LocationData(0x1302DA, 3, "Critical", "Goofy"), +} +exclusion_table = { + "Popups": { + LocationName.SweetMemories, + LocationName.SpookyCaveMap, + LocationName.StarryHillCureElement, + LocationName.StarryHillOrichalcumPlus, + LocationName.AgrabahMap, + LocationName.LampCharm, + LocationName.WishingLamp, + LocationName.DarkThornCureElement, + LocationName.RumblingRose, + LocationName.CastleWallsMap, + LocationName.SecretAnsemReport4, + LocationName.DisneyCastleMap, + LocationName.WindowofTimeMap, + LocationName.Monochrome, + LocationName.WisdomForm, + LocationName.LingeringWillProofofConnection, + LocationName.LingeringWillManifestIllusion, + LocationName.OogieBoogieMagnetElement, + LocationName.Present, + LocationName.DecoyPresents, + LocationName.DecisivePumpkin, + LocationName.MarketplaceMap, + LocationName.MerlinsHouseMembershipCard, + LocationName.MerlinsHouseBlizzardElement, + LocationName.BaileySecretAnsemReport7, + LocationName.BaseballCharm, + LocationName.AnsemsStudyMasterForm, + LocationName.AnsemsStudySkillRecipe, + LocationName.AnsemsStudySleepingLion, + LocationName.FFFightsCureElement, + LocationName.ThousandHeartlessSecretAnsemReport1, + LocationName.ThousandHeartlessIceCream, + LocationName.ThousandHeartlessPicture, + LocationName.WinnersProof, + LocationName.ProofofPeace, + LocationName.SephirothFenrir, + LocationName.EncampmentAreaMap, + LocationName.Mission3, + LocationName.VillageCaveAreaMap, + LocationName.HiddenDragon, + LocationName.ColiseumMap, + LocationName.SecretAnsemReport6, + LocationName.OlympusStone, + LocationName.HerosCrest, + LocationName.AuronsStatue, + LocationName.GuardianSoul, + LocationName.ProtectBeltPainandPanicCup, + LocationName.SerenityGemPainandPanicCup, + LocationName.RisingDragonCerberusCup, + LocationName.SerenityCrystalCerberusCup, + LocationName.GenjiShieldTitanCup, + LocationName.SkillfulRingTitanCup, + LocationName.FatalCrestGoddessofFateCup, + LocationName.OrichalcumPlusGoddessofFateCup, + LocationName.HadesCupTrophyParadoxCups, + LocationName.IsladeMuertaMap, + LocationName.FollowtheWind, + LocationName.SeadriftRowCursedMedallion, + LocationName.SeadriftRowShipGraveyardMap, + LocationName.SecretAnsemReport5, + LocationName.CircleofLife, + LocationName.ScarFireElement, + LocationName.TwilightTownMap, + LocationName.MunnyPouchOlette, + LocationName.JunkChampionBelt, + LocationName.JunkMedal, + LocationName.TheStruggleTrophy, + LocationName.NaminesSketches, + LocationName.MansionMap, + LocationName.PhotonDebugger, + LocationName.StationPlazaSecretAnsemReport2, + LocationName.MunnyPouchMickey, + LocationName.CrystalOrb, + LocationName.StarSeeker, + LocationName.ValorForm, + LocationName.SeifersTrophy, + LocationName.Oathkeeper, + LocationName.LimitForm, + LocationName.BeamSecretAnsemReport10, + LocationName.BetwixtandBetweenBondofFlame, + LocationName.TwoBecomeOne, + LocationName.RoxasSecretAnsemReport8, + LocationName.XigbarSecretAnsemReport3, + LocationName.Oblivion, + LocationName.CastleThatNeverWasMap, + LocationName.LuxordSecretAnsemReport9, + LocationName.SaixSecretAnsemReport12, + LocationName.PreXemnas1SecretAnsemReport11, + LocationName.Xemnas1SecretAnsemReport13, + LocationName.XemnasDataPowerBoost, + LocationName.AxelDataMagicBoost, + LocationName.RoxasDataMagicBoost, + LocationName.SaixDataDefenseBoost, + LocationName.DemyxDataAPBoost, + LocationName.LuxordDataAPBoost, + LocationName.VexenDataLostIllusion, + LocationName.LarxeneDataLostIllusion, + LocationName.XaldinDataDefenseBoost, + LocationName.MarluxiaDataLostIllusion, + LocationName.LexaeusDataLostIllusion, + LocationName.XigbarDataDefenseBoost, + LocationName.VexenASRoadtoDiscovery, + LocationName.LarxeneASCloakedThunder, + LocationName.ZexionASBookofShadows, + LocationName.ZexionDataLostIllusion, + LocationName.LexaeusASStrengthBeyondStrength, + LocationName.MarluxiaASEternalBlossom + }, + "Datas": { + LocationName.XemnasDataPowerBoost, + LocationName.AxelDataMagicBoost, + LocationName.RoxasDataMagicBoost, + LocationName.SaixDataDefenseBoost, + LocationName.DemyxDataAPBoost, + LocationName.LuxordDataAPBoost, + LocationName.VexenDataLostIllusion, + LocationName.VexenBonus, + LocationName.VexenASRoadtoDiscovery, + LocationName.LarxeneDataLostIllusion, + LocationName.LarxeneBonus, + LocationName.LarxeneASCloakedThunder, + LocationName.XaldinDataDefenseBoost, + LocationName.MarluxiaDataLostIllusion, + LocationName.MarluxiaASEternalBlossom, + LocationName.MarluxiaGetBonus, + LocationName.LexaeusDataLostIllusion, + LocationName.LexaeusBonus, + LocationName.LexaeusASStrengthBeyondStrength, + LocationName.XigbarDataDefenseBoost, + LocationName.ZexionDataLostIllusion, + LocationName.ZexionBonus, + LocationName.ZexionASBookofShadows, + }, + "SuperBosses": { + LocationName.LingeringWillBonus, + LocationName.LingeringWillProofofConnection, + LocationName.LingeringWillManifestIllusion, + LocationName.SephirothBonus, + LocationName.SephirothFenrir, + }, + + # 23 checks spread through 50 levels + "Level50": { + LocationName.Lvl2, + LocationName.Lvl4, + LocationName.Lvl7, + LocationName.Lvl9, + LocationName.Lvl10, + LocationName.Lvl12, + LocationName.Lvl14, + LocationName.Lvl15, + LocationName.Lvl17, + LocationName.Lvl20, + LocationName.Lvl23, + LocationName.Lvl25, + LocationName.Lvl28, + LocationName.Lvl30, + LocationName.Lvl32, + LocationName.Lvl34, + LocationName.Lvl36, + LocationName.Lvl39, + LocationName.Lvl41, + LocationName.Lvl44, + LocationName.Lvl46, + LocationName.Lvl48, + LocationName.Lvl50, + }, + # 23 checks spread through 99 levels + "Level99": { + LocationName.Lvl7, + LocationName.Lvl9, + LocationName.Lvl12, + LocationName.Lvl15, + LocationName.Lvl17, + LocationName.Lvl20, + LocationName.Lvl23, + LocationName.Lvl25, + LocationName.Lvl28, + LocationName.Lvl31, + LocationName.Lvl33, + LocationName.Lvl36, + LocationName.Lvl39, + LocationName.Lvl41, + LocationName.Lvl44, + LocationName.Lvl47, + LocationName.Lvl49, + LocationName.Lvl53, + LocationName.Lvl59, + LocationName.Lvl65, + LocationName.Lvl73, + LocationName.Lvl85, + LocationName.Lvl99, + }, + "Level50Sanity": { + LocationName.Lvl2, + LocationName.Lvl3, + LocationName.Lvl4, + LocationName.Lvl5, + LocationName.Lvl6, + LocationName.Lvl7, + LocationName.Lvl8, + LocationName.Lvl9, + LocationName.Lvl10, + LocationName.Lvl11, + LocationName.Lvl12, + LocationName.Lvl13, + LocationName.Lvl14, + LocationName.Lvl15, + LocationName.Lvl16, + LocationName.Lvl17, + LocationName.Lvl18, + LocationName.Lvl19, + LocationName.Lvl20, + LocationName.Lvl21, + LocationName.Lvl22, + LocationName.Lvl23, + LocationName.Lvl24, + LocationName.Lvl25, + LocationName.Lvl26, + LocationName.Lvl27, + LocationName.Lvl28, + LocationName.Lvl29, + LocationName.Lvl30, + LocationName.Lvl31, + LocationName.Lvl32, + LocationName.Lvl33, + LocationName.Lvl34, + LocationName.Lvl35, + LocationName.Lvl36, + LocationName.Lvl37, + LocationName.Lvl38, + LocationName.Lvl39, + LocationName.Lvl40, + LocationName.Lvl41, + LocationName.Lvl42, + LocationName.Lvl43, + LocationName.Lvl44, + LocationName.Lvl45, + LocationName.Lvl46, + LocationName.Lvl47, + LocationName.Lvl48, + LocationName.Lvl49, + LocationName.Lvl50, + }, + "Level99Sanity": { + LocationName.Lvl51, + LocationName.Lvl52, + LocationName.Lvl53, + LocationName.Lvl54, + LocationName.Lvl55, + LocationName.Lvl56, + LocationName.Lvl57, + LocationName.Lvl58, + LocationName.Lvl59, + LocationName.Lvl60, + LocationName.Lvl61, + LocationName.Lvl62, + LocationName.Lvl63, + LocationName.Lvl64, + LocationName.Lvl65, + LocationName.Lvl66, + LocationName.Lvl67, + LocationName.Lvl68, + LocationName.Lvl69, + LocationName.Lvl70, + LocationName.Lvl71, + LocationName.Lvl72, + LocationName.Lvl73, + LocationName.Lvl74, + LocationName.Lvl75, + LocationName.Lvl76, + LocationName.Lvl77, + LocationName.Lvl78, + LocationName.Lvl79, + LocationName.Lvl80, + LocationName.Lvl81, + LocationName.Lvl82, + LocationName.Lvl83, + LocationName.Lvl84, + LocationName.Lvl85, + LocationName.Lvl86, + LocationName.Lvl87, + LocationName.Lvl88, + LocationName.Lvl89, + LocationName.Lvl90, + LocationName.Lvl91, + LocationName.Lvl92, + LocationName.Lvl93, + LocationName.Lvl94, + LocationName.Lvl95, + LocationName.Lvl96, + LocationName.Lvl97, + LocationName.Lvl98, + LocationName.Lvl99, + }, + "Critical": { + LocationName.Crit_1, + LocationName.Crit_2, + LocationName.Crit_3, + LocationName.Crit_4, + LocationName.Crit_5, + LocationName.Crit_6, + LocationName.Crit_7, + }, + "Hitlist": { + LocationName.XemnasDataPowerBoost, + LocationName.AxelDataMagicBoost, + LocationName.RoxasDataMagicBoost, + LocationName.SaixDataDefenseBoost, + LocationName.DemyxDataAPBoost, + LocationName.LuxordDataAPBoost, + LocationName.VexenDataLostIllusion, + LocationName.LarxeneDataLostIllusion, + LocationName.XaldinDataDefenseBoost, + LocationName.MarluxiaDataLostIllusion, + LocationName.LexaeusDataLostIllusion, + LocationName.XigbarDataDefenseBoost, + LocationName.ZexionDataLostIllusion, + LocationName.SephirothFenrir, + LocationName.LingeringWillProofofConnection, + LocationName.StarryHillOrichalcumPlus, + LocationName.Valorlvl7, + LocationName.Wisdomlvl7, + LocationName.Limitlvl7, + LocationName.Masterlvl7, + LocationName.Finallvl7, + LocationName.TransporttoRemembrance, + LocationName.OrichalcumPlusGoddessofFateCup, + LocationName.HadesCupTrophyParadoxCups, + }, + "Cups": { + LocationName.ProtectBeltPainandPanicCup, + LocationName.SerenityGemPainandPanicCup, + LocationName.RisingDragonCerberusCup, + LocationName.SerenityCrystalCerberusCup, + LocationName.GenjiShieldTitanCup, + LocationName.SkillfulRingTitanCup, + LocationName.FatalCrestGoddessofFateCup, + LocationName.OrichalcumPlusGoddessofFateCup, + LocationName.HadesCupTrophyParadoxCups, + }, + "WeaponSlots": { + LocationName.FAKESlot: ItemName.ValorForm, + LocationName.DetectionSaberSlot: ItemName.MasterForm, + LocationName.EdgeofUltimaSlot: ItemName.FinalForm, + LocationName.OathkeeperSlot: ItemName.Oathkeeper, + LocationName.OblivionSlot: ItemName.Oblivion, + LocationName.StarSeekerSlot: ItemName.StarSeeker, + LocationName.HiddenDragonSlot: ItemName.HiddenDragon, + LocationName.HerosCrestSlot: ItemName.HerosCrest, + LocationName.MonochromeSlot: ItemName.Monochrome, + LocationName.FollowtheWindSlot: ItemName.FollowtheWind, + LocationName.CircleofLifeSlot: ItemName.CircleofLife, + LocationName.PhotonDebuggerSlot: ItemName.PhotonDebugger, + LocationName.GullWingSlot: ItemName.GullWing, + LocationName.RumblingRoseSlot: ItemName.RumblingRose, + LocationName.GuardianSoulSlot: ItemName.GuardianSoul, + LocationName.WishingLampSlot: ItemName.WishingLamp, + LocationName.DecisivePumpkinSlot: ItemName.DecisivePumpkin, + LocationName.SweetMemoriesSlot: ItemName.SweetMemories, + LocationName.MysteriousAbyssSlot: ItemName.MysteriousAbyss, + LocationName.SleepingLionSlot: ItemName.SleepingLion, + LocationName.BondofFlameSlot: ItemName.BondofFlame, + LocationName.TwoBecomeOneSlot: ItemName.TwoBecomeOne, + LocationName.FatalCrestSlot: ItemName.FatalCrest, + LocationName.FenrirSlot: ItemName.Fenrir, + LocationName.UltimaWeaponSlot: ItemName.UltimaWeapon, + LocationName.WinnersProofSlot: ItemName.WinnersProof, + LocationName.PurebloodSlot: ItemName.Pureblood, + # goofy + LocationName.AkashicRecord: ItemName.AkashicRecord, + LocationName.FrozenPride2: ItemName.FrozenPride2, + LocationName.GenjiShield: ItemName.GenjiShield, + LocationName.MajesticMushroom: ItemName.MajesticMushroom, + LocationName.MajesticMushroom2: ItemName.MajesticMushroom2, + LocationName.NobodyGuard: ItemName.NobodyGuard, + LocationName.OgreShield: ItemName.OgreShield, + LocationName.SaveTheKing2: ItemName.SaveTheKing2, + LocationName.UltimateMushroom: ItemName.UltimateMushroom, + # donald + LocationName.MeteorStaff: ItemName.MeteorStaff, + LocationName.NobodyLance: ItemName.NobodyLance, + LocationName.PreciousMushroom: ItemName.PreciousMushroom, + LocationName.PreciousMushroom2: ItemName.PreciousMushroom2, + LocationName.PremiumMushroom: ItemName.PremiumMushroom, + LocationName.RisingDragon: ItemName.RisingDragon, + LocationName.SaveTheQueen2: ItemName.SaveTheQueen2, + LocationName.ShamansRelic: ItemName.ShamansRelic, + LocationName.Centurion2: ItemName.Centurion2, + }, + "Chests": { + LocationName.BambooGroveDarkShard, + LocationName.BambooGroveEther, + LocationName.BambooGroveMythrilShard, + LocationName.CheckpointHiPotion, + LocationName.CheckpointMythrilShard, + LocationName.MountainTrailLightningShard, + LocationName.MountainTrailRecoveryRecipe, + LocationName.MountainTrailEther, + LocationName.MountainTrailMythrilShard, + LocationName.VillageCaveAPBoost, + LocationName.VillageCaveDarkShard, + LocationName.RidgeFrostShard, + LocationName.RidgeAPBoost, + LocationName.ThroneRoomTornPages, + LocationName.ThroneRoomPalaceMap, + LocationName.ThroneRoomAPBoost, + LocationName.ThroneRoomQueenRecipe, + LocationName.ThroneRoomAPBoost2, + LocationName.ThroneRoomOgreShield, + LocationName.ThroneRoomMythrilCrystal, + LocationName.ThroneRoomOrichalcum, + LocationName.AgrabahDarkShard, + LocationName.AgrabahMythrilShard, + LocationName.AgrabahHiPotion, + LocationName.AgrabahAPBoost, + LocationName.AgrabahMythrilStone, + LocationName.AgrabahMythrilShard2, + LocationName.AgrabahSerenityShard, + LocationName.BazaarMythrilGem, + LocationName.BazaarPowerShard, + LocationName.BazaarHiPotion, + LocationName.BazaarAPBoost, + LocationName.BazaarMythrilShard, + LocationName.PalaceWallsSkillRing, + LocationName.PalaceWallsMythrilStone, + LocationName.CaveEntrancePowerStone, + LocationName.CaveEntranceMythrilShard, + LocationName.ValleyofStoneMythrilStone, + LocationName.ValleyofStoneAPBoost, + LocationName.ValleyofStoneMythrilShard, + LocationName.ValleyofStoneHiPotion, + LocationName.ChasmofChallengesCaveofWondersMap, + LocationName.ChasmofChallengesAPBoost, + LocationName.TreasureRoomAPBoost, + LocationName.TreasureRoomSerenityGem, + LocationName.RuinedChamberTornPages, + LocationName.RuinedChamberRuinsMap, + LocationName.DCCourtyardMythrilShard, + LocationName.DCCourtyardStarRecipe, + LocationName.DCCourtyardAPBoost, + LocationName.DCCourtyardMythrilStone, + LocationName.DCCourtyardBlazingStone, + LocationName.DCCourtyardBlazingShard, + LocationName.DCCourtyardMythrilShard2, + LocationName.LibraryTornPages, + LocationName.CornerstoneHillMap, + LocationName.CornerstoneHillFrostShard, + LocationName.PierMythrilShard, + LocationName.PierHiPotion, + LocationName.WaterwayMythrilStone, + LocationName.WaterwayAPBoost, + LocationName.WaterwayFrostStone, + LocationName.PoohsHouse100AcreWoodMap, + LocationName.PoohsHouseAPBoost, + LocationName.PoohsHouseMythrilStone, + LocationName.PigletsHouseDefenseBoost, + LocationName.PigletsHouseAPBoost, + LocationName.PigletsHouseMythrilGem, + LocationName.RabbitsHouseDrawRing, + LocationName.RabbitsHouseMythrilCrystal, + LocationName.RabbitsHouseAPBoost, + LocationName.KangasHouseMagicBoost, + LocationName.KangasHouseAPBoost, + LocationName.KangasHouseOrichalcum, + LocationName.SpookyCaveMythrilGem, + LocationName.SpookyCaveAPBoost, + LocationName.SpookyCaveOrichalcum, + LocationName.SpookyCaveGuardRecipe, + LocationName.SpookyCaveMythrilCrystal, + LocationName.SpookyCaveAPBoost2, + LocationName.StarryHillCosmicRing, + LocationName.StarryHillStyleRecipe, + LocationName.RampartNavalMap, + LocationName.RampartMythrilStone, + LocationName.RampartDarkShard, + LocationName.TownDarkStone, + LocationName.TownAPBoost, + LocationName.TownMythrilShard, + LocationName.TownMythrilGem, + LocationName.CaveMouthBrightShard, + LocationName.CaveMouthMythrilShard, + LocationName.PowderStoreAPBoost1, + LocationName.PowderStoreAPBoost2, + LocationName.MoonlightNookMythrilShard, + LocationName.MoonlightNookSerenityGem, + LocationName.MoonlightNookPowerStone, + LocationName.InterceptorsHoldFeatherCharm, + LocationName.SeadriftKeepAPBoost, + LocationName.SeadriftKeepOrichalcum, + LocationName.SeadriftKeepMeteorStaff, + LocationName.SeadriftRowSerenityGem, + LocationName.SeadriftRowKingRecipe, + LocationName.SeadriftRowMythrilCrystal, + LocationName.PassageMythrilShard, + LocationName.PassageMythrilStone, + LocationName.PassageEther, + LocationName.PassageAPBoost, + LocationName.PassageHiPotion, + LocationName.InnerChamberUnderworldMap, + LocationName.InnerChamberMythrilShard, + LocationName.UnderworldEntrancePowerBoost, + LocationName.CavernsEntranceLucidShard, + LocationName.CavernsEntranceAPBoost, + LocationName.CavernsEntranceMythrilShard, + LocationName.TheLostRoadBrightShard, + LocationName.TheLostRoadEther, + LocationName.TheLostRoadMythrilShard, + LocationName.TheLostRoadMythrilStone, + LocationName.AtriumLucidStone, + LocationName.AtriumAPBoost, + LocationName.TheLockCavernsMap, + LocationName.TheLockMythrilShard, + LocationName.TheLockAPBoost, + LocationName.BCCourtyardAPBoost, + LocationName.BCCourtyardHiPotion, + LocationName.BCCourtyardMythrilShard, + LocationName.BellesRoomCastleMap, + LocationName.BellesRoomMegaRecipe, + LocationName.TheEastWingMythrilShard, + LocationName.TheEastWingTent, + LocationName.TheWestHallHiPotion, + LocationName.TheWestHallPowerShard, + LocationName.TheWestHallMythrilShard2, + LocationName.TheWestHallBrightStone, + LocationName.TheWestHallMythrilShard, + LocationName.DungeonBasementMap, + LocationName.DungeonAPBoost, + LocationName.SecretPassageMythrilShard, + LocationName.SecretPassageHiPotion, + LocationName.SecretPassageLucidShard, + LocationName.TheWestHallAPBoostPostDungeon, + LocationName.TheWestWingMythrilShard, + LocationName.TheWestWingTent, + LocationName.TheBeastsRoomBlazingShard, + LocationName.PitCellAreaMap, + LocationName.PitCellMythrilCrystal, + LocationName.CanyonDarkCrystal, + LocationName.CanyonMythrilStone, + LocationName.CanyonMythrilGem, + LocationName.CanyonFrostCrystal, + LocationName.HallwayPowerCrystal, + LocationName.HallwayAPBoost, + LocationName.CommunicationsRoomIOTowerMap, + LocationName.CommunicationsRoomGaiaBelt, + LocationName.CentralComputerCoreAPBoost, + LocationName.CentralComputerCoreOrichalcumPlus, + LocationName.CentralComputerCoreCosmicArts, + LocationName.CentralComputerCoreMap, + LocationName.GraveyardMythrilShard, + LocationName.GraveyardSerenityGem, + LocationName.FinklesteinsLabHalloweenTownMap, + LocationName.TownSquareMythrilStone, + LocationName.TownSquareEnergyShard, + LocationName.HinterlandsLightningShard, + LocationName.HinterlandsMythrilStone, + LocationName.HinterlandsAPBoost, + LocationName.CandyCaneLaneMegaPotion, + LocationName.CandyCaneLaneMythrilGem, + LocationName.CandyCaneLaneLightningStone, + LocationName.CandyCaneLaneMythrilStone, + LocationName.SantasHouseChristmasTownMap, + LocationName.SantasHouseAPBoost, + LocationName.BoroughDriveRecovery, + LocationName.BoroughAPBoost, + LocationName.BoroughHiPotion, + LocationName.BoroughMythrilShard, + LocationName.BoroughDarkShard, + LocationName.PosternCastlePerimeterMap, + LocationName.PosternMythrilGem, + LocationName.PosternAPBoost, + LocationName.CorridorsMythrilStone, + LocationName.CorridorsMythrilCrystal, + LocationName.CorridorsDarkCrystal, + LocationName.CorridorsAPBoost, + LocationName.AnsemsStudyUkuleleCharm, + LocationName.RestorationSiteMoonRecipe, + LocationName.RestorationSiteAPBoost, + LocationName.CoRDepthsAPBoost, + LocationName.CoRDepthsPowerCrystal, + LocationName.CoRDepthsFrostCrystal, + LocationName.CoRDepthsManifestIllusion, + LocationName.CoRDepthsAPBoost2, + LocationName.CoRMineshaftLowerLevelDepthsofRemembranceMap, + LocationName.CoRMineshaftLowerLevelAPBoost, + LocationName.CrystalFissureTornPages, + LocationName.CrystalFissureTheGreatMawMap, + LocationName.CrystalFissureEnergyCrystal, + LocationName.CrystalFissureAPBoost, + LocationName.PosternGullWing, + LocationName.HeartlessManufactoryCosmicChain, + LocationName.CoRDepthsUpperLevelRemembranceGem, + LocationName.CoRMiningAreaSerenityGem, + LocationName.CoRMiningAreaAPBoost, + LocationName.CoRMiningAreaSerenityCrystal, + LocationName.CoRMiningAreaManifestIllusion, + LocationName.CoRMiningAreaSerenityGem2, + LocationName.CoRMiningAreaDarkRemembranceMap, + LocationName.CoRMineshaftMidLevelPowerBoost, + LocationName.CoREngineChamberSerenityCrystal, + LocationName.CoREngineChamberRemembranceCrystal, + LocationName.CoREngineChamberAPBoost, + LocationName.CoREngineChamberManifestIllusion, + LocationName.CoRMineshaftUpperLevelMagicBoost, + LocationName.CoRMineshaftUpperLevelAPBoost, + LocationName.GorgeSavannahMap, + LocationName.GorgeDarkGem, + LocationName.GorgeMythrilStone, + LocationName.ElephantGraveyardFrostGem, + LocationName.ElephantGraveyardMythrilStone, + LocationName.ElephantGraveyardBrightStone, + LocationName.ElephantGraveyardAPBoost, + LocationName.ElephantGraveyardMythrilShard, + LocationName.PrideRockMap, + LocationName.PrideRockMythrilStone, + LocationName.PrideRockSerenityCrystal, + LocationName.WildebeestValleyEnergyStone, + LocationName.WildebeestValleyAPBoost, + LocationName.WildebeestValleyMythrilGem, + LocationName.WildebeestValleyMythrilStone, + LocationName.WildebeestValleyLucidGem, + LocationName.WastelandsMythrilShard, + LocationName.WastelandsSerenityGem, + LocationName.WastelandsMythrilStone, + LocationName.JungleSerenityGem, + LocationName.JungleMythrilStone, + LocationName.JungleSerenityCrystal, + LocationName.OasisMap, + LocationName.OasisTornPages, + LocationName.OasisAPBoost, + LocationName.StationofSerenityPotion, + LocationName.StationofCallingPotion, + LocationName.CentralStationPotion1, + LocationName.STTCentralStationHiPotion, + LocationName.CentralStationPotion2, + LocationName.SunsetTerraceAbilityRing, + LocationName.SunsetTerraceHiPotion, + LocationName.SunsetTerracePotion1, + LocationName.SunsetTerracePotion2, + LocationName.MansionFoyerHiPotion, + LocationName.MansionFoyerPotion1, + LocationName.MansionFoyerPotion2, + LocationName.MansionDiningRoomElvenBandanna, + LocationName.MansionDiningRoomPotion, + LocationName.MansionLibraryHiPotion, + LocationName.MansionBasementCorridorHiPotion, + LocationName.OldMansionPotion, + LocationName.OldMansionMythrilShard, + LocationName.TheWoodsPotion, + LocationName.TheWoodsMythrilShard, + LocationName.TheWoodsHiPotion, + LocationName.TramCommonHiPotion, + LocationName.TramCommonAPBoost, + LocationName.TramCommonTent, + LocationName.TramCommonMythrilShard1, + LocationName.TramCommonPotion1, + LocationName.TramCommonMythrilShard2, + LocationName.TramCommonPotion2, + LocationName.CentralStationTent, + LocationName.TTCentralStationHiPotion, + LocationName.CentralStationMythrilShard, + LocationName.TheTowerPotion, + LocationName.TheTowerHiPotion, + LocationName.TheTowerEther, + LocationName.TowerEntrywayEther, + LocationName.TowerEntrywayMythrilShard, + LocationName.SorcerersLoftTowerMap, + LocationName.TowerWardrobeMythrilStone, + LocationName.UndergroundConcourseMythrilGem, + LocationName.UndergroundConcourseAPBoost, + LocationName.UndergroundConcourseMythrilCrystal, + LocationName.UndergroundConcourseOrichalcum, + LocationName.TunnelwayOrichalcum, + LocationName.TunnelwayMythrilCrystal, + LocationName.SunsetTerraceOrichalcumPlus, + LocationName.SunsetTerraceMythrilShard, + LocationName.SunsetTerraceMythrilCrystal, + LocationName.SunsetTerraceAPBoost, + LocationName.MansionFoyerMythrilCrystal, + LocationName.MansionFoyerMythrilStone, + LocationName.MansionFoyerSerenityCrystal, + LocationName.MansionDiningRoomMythrilCrystal, + LocationName.MansionDiningRoomMythrilStone, + LocationName.MansionLibraryOrichalcum, + LocationName.MansionBasementCorridorUltimateRecipe, + LocationName.FragmentCrossingMythrilStone, + LocationName.FragmentCrossingMythrilCrystal, + LocationName.FragmentCrossingAPBoost, + LocationName.FragmentCrossingOrichalcum, + LocationName.MemorysSkyscaperMythrilCrystal, + LocationName.MemorysSkyscaperAPBoost, + LocationName.MemorysSkyscaperMythrilStone, + LocationName.TheBrinkofDespairDarkCityMap, + LocationName.TheBrinkofDespairOrichalcumPlus, + LocationName.NothingsCallMythrilGem, + LocationName.NothingsCallOrichalcum, + LocationName.TwilightsViewCosmicBelt, + LocationName.NaughtsSkywayMythrilGem, + LocationName.NaughtsSkywayOrichalcum, + LocationName.NaughtsSkywayMythrilCrystal, + LocationName.RuinandCreationsPassageMythrilStone, + LocationName.RuinandCreationsPassageAPBoost, + LocationName.RuinandCreationsPassageMythrilCrystal, + LocationName.RuinandCreationsPassageOrichalcum, + LocationName.GardenofAssemblageMap, + LocationName.GoALostIllusion, + LocationName.ProofofNonexistence, + } +} + +AllWeaponSlot = { + LocationName.FAKESlot, + LocationName.DetectionSaberSlot, + LocationName.EdgeofUltimaSlot, + LocationName.KingdomKeySlot, + LocationName.OathkeeperSlot, + LocationName.OblivionSlot, + LocationName.StarSeekerSlot, + LocationName.HiddenDragonSlot, + LocationName.HerosCrestSlot, + LocationName.MonochromeSlot, + LocationName.FollowtheWindSlot, + LocationName.CircleofLifeSlot, + LocationName.PhotonDebuggerSlot, + LocationName.GullWingSlot, + LocationName.RumblingRoseSlot, + LocationName.GuardianSoulSlot, + LocationName.WishingLampSlot, + LocationName.DecisivePumpkinSlot, + LocationName.SweetMemoriesSlot, + LocationName.MysteriousAbyssSlot, + LocationName.SleepingLionSlot, + LocationName.BondofFlameSlot, + LocationName.TwoBecomeOneSlot, + LocationName.FatalCrestSlot, + LocationName.FenrirSlot, + LocationName.UltimaWeaponSlot, + LocationName.WinnersProofSlot, + LocationName.PurebloodSlot, + LocationName.Centurion2, + LocationName.CometStaff, + LocationName.HammerStaff, + LocationName.LordsBroom, + LocationName.MagesStaff, + LocationName.MeteorStaff, + LocationName.NobodyLance, + LocationName.PreciousMushroom, + LocationName.PreciousMushroom2, + LocationName.PremiumMushroom, + LocationName.RisingDragon, + LocationName.SaveTheQueen2, + LocationName.ShamansRelic, + LocationName.VictoryBell, + LocationName.WisdomWand, + + LocationName.AdamantShield, + LocationName.AkashicRecord, + LocationName.ChainGear, + LocationName.DreamCloud, + LocationName.FallingStar, + LocationName.FrozenPride2, + LocationName.GenjiShield, + LocationName.KnightDefender, + LocationName.KnightsShield, + LocationName.MajesticMushroom, + LocationName.MajesticMushroom2, + LocationName.NobodyGuard, + LocationName.OgreShield, + LocationName.SaveTheKing2, + LocationName.UltimateMushroom, } +RegionTable = { + "FirstVisits": { + RegionName.LoD_Region, + RegionName.Ag_Region, + RegionName.Dc_Region, + RegionName.Pr_Region, + RegionName.Oc_Region, + RegionName.Bc_Region, + RegionName.Sp_Region, + RegionName.Ht_Region, + RegionName.Hb_Region, + RegionName.Pl_Region, + RegionName.STT_Region, + RegionName.TT_Region, + RegionName.Twtnw_Region, + }, + "SecondVisits": { + RegionName.LoD2_Region, + RegionName.Ag2_Region, + RegionName.Tr_Region, + RegionName.Pr2_Region, + RegionName.Oc2_Region, + RegionName.Bc2_Region, + RegionName.Sp2_Region, + RegionName.Ht2_Region, + RegionName.Hb2_Region, + RegionName.Pl2_Region, + RegionName.STT_Region, + RegionName.Twtnw2_Region, + }, + "ValorRegion": { + RegionName.LoD_Region, + RegionName.Ag_Region, + RegionName.Dc_Region, + RegionName.Pr_Region, + RegionName.Oc_Region, + RegionName.Bc_Region, + RegionName.Sp_Region, + RegionName.Ht_Region, + RegionName.Hb_Region, + RegionName.TT_Region, + RegionName.Twtnw_Region, + }, + "WisdomRegion": { + RegionName.LoD_Region, + RegionName.Ag_Region, + RegionName.Dc_Region, + RegionName.Pr_Region, + RegionName.Oc_Region, + RegionName.Bc_Region, + RegionName.Sp_Region, + RegionName.Ht_Region, + RegionName.Hb_Region, + RegionName.TT_Region, + RegionName.Twtnw_Region, + }, + "LimitRegion": { + RegionName.LoD_Region, + RegionName.Ag_Region, + RegionName.Dc_Region, + RegionName.Pr_Region, + RegionName.Oc_Region, + RegionName.Bc_Region, + RegionName.Sp_Region, + RegionName.Ht_Region, + RegionName.Hb_Region, + RegionName.TT_Region, + RegionName.Twtnw_Region, + RegionName.STT_Region, + }, + "MasterRegion": { + RegionName.LoD_Region, + RegionName.Ag_Region, + RegionName.Dc_Region, + RegionName.Pr_Region, + RegionName.Oc_Region, + RegionName.Bc_Region, + RegionName.Sp_Region, + RegionName.Ht_Region, + RegionName.Hb_Region, + RegionName.TT_Region, + RegionName.Twtnw_Region, + }, # could add lod2 and bc2 as an option since those spawns are rng + "FinalRegion": { + RegionName.TT3_Region, + RegionName.Twtnw_PostRoxas, + RegionName.Twtnw2_Region, + } +} + +all_locations = { + **TWTNW_Checks, + **TWTNW2_Checks, + **TT_Checks, + **TT2_Checks, + **TT3_Checks, + **STT_Checks, + **PL_Checks, + **PL2_Checks, + **CoR_Checks, + **HB_Checks, + **HB2_Checks, + **HT_Checks, + **HT2_Checks, + **PR_Checks, + **PR2_Checks, + **PR_Checks, + **PR2_Checks, + **SP_Checks, + **SP2_Checks, + **BC_Checks, + **BC2_Checks, + **Oc_Checks, + **Oc2_Checks, + **Oc2Cups, + **HundredAcre1_Checks, + **HundredAcre2_Checks, + **HundredAcre3_Checks, + **HundredAcre4_Checks, + **HundredAcre5_Checks, + **HundredAcre6_Checks, + **DC_Checks, + **TR_Checks, + **AG_Checks, + **AG2_Checks, + **LoD_Checks, + **LoD2_Checks, + **SoraLevels, + **Form_Checks, + **GoA_Checks, + **Keyblade_Slots, + **Critical_Checks, + **Donald_Checks, + **Goofy_Checks, +} + +location_table = {} + + +def setup_locations(): + totallocation_table = {**TWTNW_Checks, **TWTNW2_Checks, **TT_Checks, **TT2_Checks, **TT3_Checks, **STT_Checks, + **PL_Checks, **PL2_Checks, **CoR_Checks, **HB_Checks, **HB2_Checks, + **PR_Checks, **PR2_Checks, **PR_Checks, **PR2_Checks, **SP_Checks, **SP2_Checks, **BC_Checks, + **BC2_Checks, **HT_Checks, **HT2_Checks, + **Oc_Checks, **Oc2_Checks, **Oc2Cups, **Critical_Checks, **Donald_Checks, **Goofy_Checks, + **HundredAcre1_Checks, **HundredAcre2_Checks, **HundredAcre3_Checks, **HundredAcre4_Checks, + **HundredAcre5_Checks, **HundredAcre6_Checks, + **DC_Checks, **TR_Checks, **AG_Checks, **AG2_Checks, **LoD_Checks, **LoD2_Checks, + **SoraLevels, + **Form_Checks, **GoA_Checks, **Keyblade_Slots} + return totallocation_table + + +lookup_id_to_Location: typing.Dict[int, str] = {data.code: item_name for item_name, data in location_table.items() if + data.code} diff --git a/worlds/kh2/Names/ItemName.py b/worlds/kh2/Names/ItemName.py new file mode 100644 index 0000000000..57cfcbe060 --- /dev/null +++ b/worlds/kh2/Names/ItemName.py @@ -0,0 +1,404 @@ +# reports +SecretAnsemsReport1 = "Secret Ansem's Report 1" +SecretAnsemsReport2 = "Secret Ansem's Report 2" +SecretAnsemsReport3 = "Secret Ansem's Report 3" +SecretAnsemsReport4 = "Secret Ansem's Report 4" +SecretAnsemsReport5 = "Secret Ansem's Report 5" +SecretAnsemsReport6 = "Secret Ansem's Report 6" +SecretAnsemsReport7 = "Secret Ansem's Report 7" +SecretAnsemsReport8 = "Secret Ansem's Report 8" +SecretAnsemsReport9 = "Secret Ansem's Report 9" +SecretAnsemsReport10 = "Secret Ansem's Report 10" +SecretAnsemsReport11 = "Secret Ansem's Report 11" +SecretAnsemsReport12 = "Secret Ansem's Report 12" +SecretAnsemsReport13 = "Secret Ansem's Report 13" + +# progression +ProofofConnection = "Proof of Connection" +ProofofNonexistence = "Proof of Nonexistence" +ProofofPeace = "Proof of Peace" +PromiseCharm = "Promise Charm" +BattlefieldsofWar = "Battlefields of War" +SwordoftheAncestor = "Sword of the Ancestor" +BeastsClaw = "Beast's Claw" +BoneFist = "Bone Fist" +ProudFang = "Proud Fang" +SkillandCrossbones = "Skill and Crossbones" +Scimitar = "Scimitar" +MembershipCard = "Membership Card" +IceCream = "Ice Cream" +WaytotheDawn = "Way to the Dawn" +IdentityDisk = "Identity Disk" +NamineSketches = "Namine Sketches" +CastleKey = "Disney Castle Key" +TornPages = "Torn Page" +TornPages = "Torn Page" +TornPages = "Torn Page" +TornPages = "Torn Page" +TornPages = "Torn Page" +ValorForm = "Valor Form" +WisdomForm = "Wisdom Form" +LimitForm = "Limit Form" +MasterForm = "Master Form" +FinalForm = "Final Form" + +# magic and summons +FireElement = "Fire Element" + +BlizzardElement = "Blizzard Element" + +ThunderElement = "Thunder Element" + +CureElement = "Cure Element" + +MagnetElement = "Magnet Element" + +ReflectElement = "Reflect Element" + +Genie = "Genie" +PeterPan = "Peter Pan" +Stitch = "Stitch" +ChickenLittle = "Chicken Little" + +#movement +HighJump = "High Jump" + + +QuickRun = "Quick Run" + + +AerialDodge = "Aerial Dodge" + + +Glide = "Glide" + + +DodgeRoll = "Dodge Roll" + + +#keyblades +Oathkeeper = "Oathkeeper" +Oblivion = "Oblivion" +StarSeeker = "Star Seeker" +HiddenDragon = "Hidden Dragon" +HerosCrest = "Hero's Crest" +Monochrome = "Monochrome" +FollowtheWind = "Follow the Wind" +CircleofLife = "Circle of Life" +PhotonDebugger = "Photon Debugger" +GullWing = "Gull Wing" +RumblingRose = "Rumbling Rose" +GuardianSoul = "Guardian Soul" +WishingLamp = "Wishing Lamp" +DecisivePumpkin = "Decisive Pumpkin" +SleepingLion = "Sleeping Lion" +SweetMemories = "Sweet Memories" +MysteriousAbyss = "Mysterious Abyss" +TwoBecomeOne = "Two Become One" +FatalCrest = "Fatal Crest" +BondofFlame = "Bond of Flame" +Fenrir = "Fenrir" +UltimaWeapon = "Ultima Weapon" +WinnersProof = "Winner's Proof" +Pureblood = "Pureblood" + +# stafs +HammerStaff = "Hammer Staff" +LordsBroom = "Lord's Broom" +MagesStaff = "Mages Staff" +MeteorStaff = "Meteor Staff" +CometStaff = "Comet Staff" +Centurion2 = "Centurion+" +MeteorStaff = "Meteor Staff" +NobodyLance = "Nobody Lance" +PreciousMushroom = "Precious Mushroom" +PreciousMushroom2 = "Precious Mushroom+" +PremiumMushroom = "Premium Mushroom" +RisingDragon = "Rising Dragon" +SaveTheQueen2 = "Save The Queen+" +ShamansRelic = "Shaman's Relic" +VictoryBell = "Victory Bell" +WisdomWand = "Wisdom Wand" +# shelds + +AkashicRecord = "Akashic Record" +FrozenPride2 = "Frozen Pride+" +GenjiShield = "Genji Shield" +MajesticMushroom = "Majestic Mushroom" +MajesticMushroom2 = "Majestic Mushroom+" +NobodyGuard = "Nobody Guard" +OgreShield = "Ogre Shield" +SaveTheKing2 = "Save The King+" +UltimateMushroom = "Ultimate Mushroom" + +# accesrosies +AbilityRing = "Ability Ring" +EngineersRing = "Engineer's Ring" +TechniciansRing = "Technician's Ring" +SkillRing = "Skill Ring" +SkillfulRing = "Skillful Ring" +ExpertsRing = "Expert's Ring" +MastersRing = "Master's Ring" +CosmicRing = "Cosmic Ring" +ExecutivesRing = "Executive's Ring" +SardonyxRing = "Sardonyx Ring" +TourmalineRing = "Tourmaline Ring" +AquamarineRing = "Aquamarine Ring" +GarnetRing = "Garnet Ring" +DiamondRing = "Diamond Ring" +SilverRing = "Silver Ring" +GoldRing = "Gold Ring" +PlatinumRing = "Platinum Ring" +MythrilRing = "Mythril Ring" +OrichalcumRing = "Orichalcum Ring" +SoldierEarring = "Soldier Earring" +FencerEarring = "Fencer Earring" +MageEarring = "Mage Earring" +SlayerEarring = "Slayer Earring" +Medal = "Medal" +MoonAmulet = "Moon Amulet" +StarCharm = "Star Charm" +CosmicArts = "Cosmic Arts" +ShadowArchive = "Shadow Archive" +ShadowArchive2 = "Shadow Archive+" +FullBloom = "Full Bloom" +FullBloom2 = "Full Bloom+" +DrawRing = "Draw Ring" +LuckyRing = "Lucky Ring" + +# armorers +ElvenBandana = "Elven Bandana" +DivineBandana = "Divine Bandana" +ProtectBelt = "Protect Belt" +GaiaBelt = "Gaia Belt" +PowerBand = "Power Band" +BusterBand = "Buster Band" +CosmicBelt = "Cosmic Belt" +FireBangle = "Fire Bangle" +FiraBangle = "Fira Bangle" +FiragaBangle = "Firaga Bangle" +FiragunBangle = "Firagun Bangle" +BlizzardArmlet = "Blizzard Armlet" +BlizzaraArmlet = "Blizzara Armlet" +BlizzagaArmlet = "Blizzaga Armlet" +BlizzagunArmlet = "Blizzagun Armlet" +ThunderTrinket = "Thunder Trinket" +ThundaraTrinket = "Thundara Trinket" +ThundagaTrinket = "Thundaga Trinket" +ThundagunTrinket = "Thundagun Trinket" +ShockCharm = "Shock Charm" +ShockCharm2 = "Shock Charm+" +ShadowAnklet = "Shadow Anklet" +DarkAnklet = "Dark Anklet" +MidnightAnklet = "Midnight Anklet" +ChaosAnklet = "Chaos Anklet" +ChampionBelt = "Champion Belt" +AbasChain = "Abas Chain" +AegisChain = "Aegis Chain" +Acrisius = "Acrisius" +Acrisius2 = "Acrisius+" +CosmicChain = "Cosmic Chain" +PetiteRibbon = "Petite Ribbon" +Ribbon = "Ribbon" +GrandRibbon = "Grand Ribbon" + +# usefull and stat incre +MickyMunnyPouch = "Mickey Munny Pouch" +OletteMunnyPouch = "Olette Munny Pouch" +HadesCupTrophy = "Hades Cup Trophy" +UnknownDisk = "Unknown Disk" +OlympusStone = "Olympus Stone" +MaxHPUp = "Max HP Up" +MaxMPUp = "Max MP Up" +DriveGaugeUp = "Drive Gauge Up" +ArmorSlotUp = "Armor Slot Up" +AccessorySlotUp = "Accessory Slot Up" +ItemSlotUp = "Item Slot Up" + +# support abilitys +Scan = "Scan" +AerialRecovery = "Aerial Recovery" +ComboMaster = "Combo Master" +ComboPlus = "Combo Plus" +AirComboPlus = "Air Combo Plus" +ComboBoost = "Combo Boost" +AirComboBoost = "Air Combo Boost" +ReactionBoost = "Reaction Boost" +FinishingPlus = "Finishing Plus" +NegativeCombo = "Negative Combo" +BerserkCharge = "Berserk Charge" +DamageDrive = "Damage Drive" +DriveBoost = "Drive Boost" +FormBoost = "Form Boost" +SummonBoost = "Summon Boost" +ExperienceBoost = "Experience Boost" +Draw = "Draw" +Jackpot = "Jackpot" +LuckyLucky = "Lucky Lucky" +DriveConverter = "Drive Converter" +FireBoost = "Fire Boost" +BlizzardBoost = "Blizzard Boost" +ThunderBoost = "Thunder Boost" +ItemBoost = "Item Boost" +MPRage = "MP Rage" +MPHaste = "MP Haste" +MPHastera = "MP Hastera" +MPHastega = "MP Hastega" +Defender = "Defender" +DamageControl = "Damage Control" +NoExperience = "No Experience" +LightDarkness = "Light & Darkness" + +# level ability +MagicLock = "Magic Lock-On" +LeafBracer = "Leaf Bracer" +CombinationBoost = "Combination Boost" +DamageDrive = "Damage Drive" +OnceMore = "Once More" +SecondChance = "Second Chance" + +# action abilitys +Guard = "Guard" +UpperSlash = "Upper Slash" +HorizontalSlash = "Horizontal Slash" +FinishingLeap = "Finishing Leap" +RetaliatingSlash = "Retaliating Slash" +Slapshot = "Slapshot" +DodgeSlash = "Dodge Slash" +FlashStep = "Flash Step" +SlideDash = "Slide Dash" +VicinityBreak = "Vicinity Break" +GuardBreak = "Guard Break" +Explosion = "Explosion" +AerialSweep = "Aerial Sweep" +AerialDive = "Aerial Dive" +AerialSpiral = "Aerial Spiral" +AerialFinish = "Aerial Finish" +MagnetBurst = "Magnet Burst" +Counterguard = "Counterguard" +AutoValor = "Auto Valor" +AutoWisdom = "Auto Wisdom" +AutoLimit = "Auto Limit" +AutoMaster = "Auto Master" +AutoFinal = "Auto Final" +AutoSummon = "Auto Summon" +TrinityLimit = "Trinity Limit" + +# items +Potion = "Potion" +HiPotion = "Hi-Potion" +Ether = "Ether" +Elixir = "Elixir" +MegaPotion = "Mega-Potion" +MegaEther = "Mega-Ether" +Megalixir = "Megalixir" +Tent = "Tent" +DriveRecovery = "Drive Recovery" +HighDriveRecovery = "High Drive Recovery" +PowerBoost = "Power Boost" +MagicBoost = "Magic Boost" +DefenseBoost = "Defense Boost" +APBoost = "AP Boost" + +# donald abilitys +DonaldFire = "Donald Fire" +DonaldBlizzard = "Donald Blizzard" +DonaldThunder = "Donald Thunder" +DonaldCure = "Donald Cure" +Fantasia = "Donald Fantasia" +FlareForce = "Donald Flare Force" +DonaldMPRage = "Donald MP Rage" +DonaldJackpot = "Donald Jackpot" +DonaldLuckyLucky = "Donald Lucky Lucky" +DonaldFireBoost = "Donald Fire Boost" +DonaldBlizzardBoost = "Donald Blizzard Boost" +DonaldThunderBoost = "Donald Thunder Boost" +DonaldFireBoost = "Donald Fire Boost" +DonaldBlizzardBoost = "Donald Blizzard Boost" +DonaldThunderBoost = "Donald Thunder Boost" +DonaldMPRage = "Donald MP Rage" +DonaldMPHastera = "Donald MP Hastera" +DonaldAutoLimit = "Donald Auto Limit" +DonaldHyperHealing = "Donald Hyper Healing" +DonaldAutoHealing = "Donald Auto Healing" +DonaldMPHastega = "Donald MP Hastega" +DonaldItemBoost = "Donald Item Boost" +DonaldDamageControl = "Donald Damage Control" +DonaldHyperHealing = "Donald Hyper Healing" +DonaldMPRage = "Donald MP Rage" +DonaldMPHaste = "Donald MP Haste" +DonaldMPHastera = "Donald MP Hastera" +DonaldMPHastega = "Donald MP Hastega" +DonaldMPHaste = "Donald MP Haste" +DonaldDamageControl = "Donald Damage Control" +DonaldMPHastera = "Donald MP Hastera" +DonaldDraw = "Donald Draw" + +# goofy abili +GoofyTornado = "Goofy Tornado" +GoofyTurbo = "Goofy Turbo" +GoofyBash = "Goofy Bash" +TornadoFusion = "Tornado Fusion" +Teamwork = "Teamwork" +GoofyDraw = "Goofy Draw" +GoofyJackpot = "Goofy Jackpot" +GoofyLuckyLucky = "Goofy Lucky Lucky" +GoofyItemBoost = "Goofy Item Boost" +GoofyMPRage = "Goofy MP Rage" +GoofyDefender = "Goofy Defender" +GoofyDamageControl = "Goofy Damage Control" +GoofyAutoLimit = "Goofy Auto Limit" +GoofySecondChance = "Goofy Second Chance" +GoofyOnceMore = "Goofy Once More" +GoofyAutoChange = "Goofy Auto Change" +GoofyHyperHealing = "Goofy Hyper Healing" +GoofyAutoHealing = "Goofy Auto Healing" +GoofyDefender = "Goofy Defender" +GoofyHyperHealing = "Goofy Hyper Healing" +GoofyMPHaste = "Goofy MP Haste" +GoofyMPHastera = "Goofy MP Hastera" +GoofyMPRage = "Goofy MP Rage" +GoofyMPHastega = "Goofy MP Hastega" +GoofyItemBoost = "Goofy Item Boost" +GoofyDamageControl = "Goofy Damage Control" +GoofyProtect = "Goofy Protect" +GoofyProtera = "Goofy Protera" +GoofyProtega = "Goofy Protega" +GoofyDamageControl = "Goofy Damage Control" +GoofyProtect = "Goofy Protect" +GoofyProtera = "Goofy Protera" +GoofyProtega = "Goofy Protega" + +Victory = "Victory" +LuckyEmblem = "Lucky Emblem" +Bounty="Bounty" + +UniversalKey="Universal Key" +# Keyblade Slots +FAKESlot = "FAKE (Slot)" +DetectionSaberSlot = "Detection Saber (Slot)" +EdgeofUltimaSlot = "Edge of Ultima (Slot)" +KingdomKeySlot = "Kingdom Key (Slot)" +OathkeeperSlot = "Oathkeeper (Slot)" +OblivionSlot = "Oblivion (Slot)" +StarSeekerSlot = "Star Seeker (Slot)" +HiddenDragonSlot = "Hidden Dragon (Slot)" +HerosCrestSlot = "Hero's Crest (Slot)" +MonochromeSlot = "Monochrome (Slot)" +FollowtheWindSlot = "Follow the Wind (Slot)" +CircleofLifeSlot = "Circle of Life (Slot)" +PhotonDebuggerSlot = "Photon Debugger (Slot)" +GullWingSlot = "Gull Wing (Slot)" +RumblingRoseSlot = "Rumbling Rose (Slot)" +GuardianSoulSlot = "Guardian Soul (Slot)" +WishingLampSlot = "Wishing Lamp (Slot)" +DecisivePumpkinSlot = "Decisive Pumpkin (Slot)" +SweetMemoriesSlot = "Sweet Memories (Slot)" +MysteriousAbyssSlot = "Mysterious Abyss (Slot)" +SleepingLionSlot = "Sleeping Lion (Slot)" +BondofFlameSlot = "Bond of Flame (Slot)" +TwoBecomeOneSlot = "Two Become One (Slot)" +FatalCrestSlot = "Fatal Crest (Slot)" +FenrirSlot = "Fenrir (Slot)" +UltimaWeaponSlot = "Ultima Weapon (Slot)" +WinnersProofSlot = "Winner's Proof (Slot)" diff --git a/worlds/kh2/Names/LocationName.py b/worlds/kh2/Names/LocationName.py new file mode 100644 index 0000000000..1a6c4d07fb --- /dev/null +++ b/worlds/kh2/Names/LocationName.py @@ -0,0 +1,763 @@ +BambooGroveDarkShard = "(LoD) Bamboo Grove Dark Shard" +BambooGroveEther = "(LoD) Bamboo Grove Ether" +BambooGroveMythrilShard = "(LoD) Bamboo Grove Mythril Shard" +EncampmentAreaMap = "(LoD) Bamboo Grove Encampment Area Map" +Mission3 = "(LoD) Mission 3" +CheckpointHiPotion = "(LoD) Checkpoint Hi-Potion" +CheckpointMythrilShard = "(LoD) Checkpoint Mythril Shard" +MountainTrailLightningShard = "(LoD) Mountain Trail Lightning Shard" +MountainTrailRecoveryRecipe = "(LoD) Mountain Trail Recovery Recipe" +MountainTrailEther = "(LoD) Mountain Trail Ether" +MountainTrailMythrilShard = "(LoD) Mountain Trail Mythril Shard" +VillageCaveAreaMap = "(LoD) Village Cave Area Map" +VillageCaveAPBoost = "(LoD) Village Cave AP Boost" +VillageCaveDarkShard = "(LoD) Village Cave Dark Shard" +VillageCaveBonus = "(LoD) Village Cave Bonus: Sora Slot 1" +RidgeFrostShard = "(LoD) Ridge Frost Shard" +RidgeAPBoost = "(LoD) Ridge AP Boost" +ShanYu = "(LoD) Shan-Yu Bonus: Sora Slot 1" +ShanYuGetBonus = "(LoD) Shan-Yu Bonus: Sora Slot 2" +HiddenDragon = "(LoD) Hidden Dragon" +ThroneRoomTornPages = "(LoD2) Throne Room Torn Pages" +ThroneRoomPalaceMap = "(LoD2) Throne Room Palace Map" +ThroneRoomAPBoost = "(LoD2) Throne Room AP Boost" +ThroneRoomQueenRecipe = "(LoD2) Throne Room Queen Recipe" +ThroneRoomAPBoost2 = "(LoD2) Throne Room AP Boost 2" +ThroneRoomOgreShield = "(LoD2) Throne Room Ogre Shield" +ThroneRoomMythrilCrystal = "(LoD2) Throne Room Mythril Crystal" +ThroneRoomOrichalcum = "(LoD2) Throne Room Orichalcum" +StormRider = "(LoD2) Storm Rider Bonus: Sora Slot 1" +XigbarDataDefenseBoost = "Data Xigbar" + +AgrabahMap = "(AG) Agrabah Map" +AgrabahDarkShard = "(AG) Agrabah Dark Shard" +AgrabahMythrilShard = "(AG) Agrabah Mythril Shard" +AgrabahHiPotion = "(AG) Agrabah Hi-Potion" +AgrabahAPBoost = "(AG) Agrabah AP Boost" +AgrabahMythrilStone = "(AG) Agrabah Mythril Stone" +AgrabahMythrilShard2 = "(AG) Agrabah Mythril Shard 2" +AgrabahSerenityShard = "(AG) Agrabah Serenity Shard" +BazaarMythrilGem = "(AG) Bazaar Mythril Gem" +BazaarPowerShard = "(AG) Bazaar Power Shard" +BazaarHiPotion = "(AG) Bazaar Hi-Potion" +BazaarAPBoost = "(AG) Bazaar AP Boost" +BazaarMythrilShard = "(AG) Bazaar Mythril Shard" +PalaceWallsSkillRing = "(AG) Palace Walls Skill Ring" +PalaceWallsMythrilStone = "(AG) Palace Walls Mythril Stone" +CaveEntrancePowerStone = "(AG) Cave Entrance Power Stone" +CaveEntranceMythrilShard = "(AG) Cave Entrance Mythril Shard" +ValleyofStoneMythrilStone = "(AG) Valley of Stone Mythril Stone" +ValleyofStoneAPBoost = "(AG) Valley of Stone AP Boost" +ValleyofStoneMythrilShard = "(AG) Valley of Stone Mythril Shard" +ValleyofStoneHiPotion = "(AG) Valley of Stone Hi-Potion" +AbuEscort = "(AG) Abu Escort Bonus: Sora Slot 1" +ChasmofChallengesCaveofWondersMap = "(AG) Chasm of Challenges Cave of Wonders Map" +ChasmofChallengesAPBoost = "(AG) Chasm of Challenges AP Boost" +TreasureRoom = "(AG) Treasure Room" +TreasureRoomAPBoost = "(AG) Treasure Room AP Boost" +TreasureRoomSerenityGem = "(AG) Treasure Room Serenity Gem" +ElementalLords = "(AG) Elemental Lords Bonus: Sora Slot 1" +LampCharm = "(AG) Lamp Charm" +RuinedChamberTornPages = "(AG2) Ruined Chamber Torn Pages" +RuinedChamberRuinsMap = "(AG2) Ruined Chamber Ruins Map" +GenieJafar = "(AG2) Genie Jafar" +WishingLamp = "(AG2) Wishing Lamp" +LexaeusBonus = "Lexaeus Bonus: Sora Slot 1" +LexaeusASStrengthBeyondStrength = "AS Lexaeus" +LexaeusDataLostIllusion = "Data Lexaeus" +DCCourtyardMythrilShard = "(DC) Courtyard Mythril Shard" +DCCourtyardStarRecipe = "(DC) Courtyard Star Recipe" +DCCourtyardAPBoost = "(DC) Courtyard AP Boost" +DCCourtyardMythrilStone = "(DC) Courtyard Mythril Stone" +DCCourtyardBlazingStone = "(DC) Courtyard Blazing Stone" +DCCourtyardBlazingShard = "(DC) Courtyard Blazing Shard" +DCCourtyardMythrilShard2 = "(DC) Courtyard Mythril Shard 2" +LibraryTornPages = "(DC) Library Torn Pages" +DisneyCastleMap = "(DC) Disney Castle Map" +MinnieEscort = "(DC) Minnie Escort Bonus: Sora Slot 1" +MinnieEscortGetBonus = "(DC) Minnie Escort Bonus: Sora Slot 2" +CornerstoneHillMap = "(TR) Cornerstone Hill Map" +CornerstoneHillFrostShard = "(TR) Cornerstone Hill Frost Shard" +PierMythrilShard = "(TR) Pier Mythril Shard" +PierHiPotion = "(TR) Pier Hi-Potion" +WaterwayMythrilStone = "(TR) Waterway Mythril Stone" +WaterwayAPBoost = "(TR) Waterway AP Boost" +WaterwayFrostStone = "(TR) Waterway Frost Stone" +WindowofTimeMap = "(TR) Window of Time Map" +BoatPete = "(TR) Boat Pete Bonus: Sora Slot 1" +FuturePete = "(TR) Future Pete Bonus: Sora Slot 1" +FuturePeteGetBonus = "(TR) Future Pete Bonus: Sora Slot 2" +Monochrome = "(TR) Monochrome" +WisdomForm = "(TR) Wisdom Form" +MarluxiaGetBonus = "Marluxia Bonus: Sora Slot 1" +MarluxiaASEternalBlossom = "AS Marluxia" +MarluxiaDataLostIllusion = "Data Marluxia" +LingeringWillBonus = "Lingering Will Bonus: Sora Slot 1" +LingeringWillProofofConnection = "Lingering Will Proof of Connection" +LingeringWillManifestIllusion = "Lingering Will Manifest Illusion" +PoohsHouse100AcreWoodMap = "(100Acre) Pooh's House 100 Acre Wood Map" +PoohsHouseAPBoost = "(100Acre) Pooh's House AP Boost" +PoohsHouseMythrilStone = "(100Acre) Pooh's House Mythril Stone" +PigletsHouseDefenseBoost = "(100Acre) Piglet's House Defense Boost" +PigletsHouseAPBoost = "(100Acre) Piglet's House AP Boost" +PigletsHouseMythrilGem = "(100Acre) Piglet's House Mythril Gem" +RabbitsHouseDrawRing = "(100Acre) Rabbit's House Draw Ring" +RabbitsHouseMythrilCrystal = "(100Acre) Rabbit's House Mythril Crystal" +RabbitsHouseAPBoost = "(100Acre) Rabbit's House AP Boost" +KangasHouseMagicBoost = "(100Acre) Kanga's House Magic Boost" +KangasHouseAPBoost = "(100Acre) Kanga's House AP Boost" +KangasHouseOrichalcum = "(100Acre) Kanga's House Orichalcum" +SpookyCaveMythrilGem = "(100Acre) Spooky Cave Mythril Gem" +SpookyCaveAPBoost = "(100Acre) Spooky Cave AP Boost" +SpookyCaveOrichalcum = "(100Acre) Spooky Cave Orichalcum" +SpookyCaveGuardRecipe = "(100Acre) Spooky Cave Guard Recipe" +SpookyCaveMythrilCrystal = "(100Acre) Spooky Cave Mythril Crystal" +SpookyCaveAPBoost2 = "(100Acre) Spooky Cave AP Boost 2" +SweetMemories = "(100Acre) Sweet Memories" +SpookyCaveMap = "(100Acre) Spooky Cave Map" +StarryHillCosmicRing = "(100Acre) Starry Hill Cosmic Ring" +StarryHillStyleRecipe = "(100Acre) Starry Hill Style Recipe" +StarryHillCureElement = "(100Acre) Starry Hill Cure Element" +StarryHillOrichalcumPlus = "(100Acre) Starry Hill Orichalcum+" +PassageMythrilShard = "(OC) Passage Mythril Shard" +PassageMythrilStone = "(OC) Passage Mythril Stone" +PassageEther = "(OC) Passage Ether" +PassageAPBoost = "(OC) Passage AP Boost" +PassageHiPotion = "(OC) Passage Hi-Potion" +InnerChamberUnderworldMap = "(OC) Inner Chamber Underworld Map" +InnerChamberMythrilShard = "(OC) Inner Chamber Mythril Shard" +Cerberus = "(OC) Cerberus Bonus: Sora Slot 1" +ColiseumMap = "(OC) Coliseum Map" +Urns = "(OC) Urns Bonus: Sora Slot 1" +UnderworldEntrancePowerBoost = "(OC) Underworld Entrance Power Boost" +CavernsEntranceLucidShard = "(OC) Caverns Entrance Lucid Shard" +CavernsEntranceAPBoost = "(OC) Caverns Entrance AP Boost" +CavernsEntranceMythrilShard = "(OC) Caverns Entrance Mythril Shard" +TheLostRoadBrightShard = "(OC) The Lost Road Bright Shard" +TheLostRoadEther = "(OC) The Lost Road Ether" +TheLostRoadMythrilShard = "(OC) The Lost Road Mythril Shard" +TheLostRoadMythrilStone = "(OC) The Lost Road Mythril Stone" +AtriumLucidStone = "(OC) Atrium Lucid Stone" +AtriumAPBoost = "(OC) Atrium AP Boost" +DemyxOC = "(OC) Demyx Bonus: Sora Slot 1" +SecretAnsemReport5 = "(OC) Secret Ansem Report 5 (Demyx)" +OlympusStone = "(OC) Olympus Stone" +TheLockCavernsMap = "(OC) The Lock Caverns Map" +TheLockMythrilShard = "(OC) The Lock Mythril Shard" +TheLockAPBoost = "(OC) The Lock AP Boost" +PeteOC = "(OC) Pete OC Bonus: Sora Slot 1" +Hydra = "(OC) Hydra Bonus: Sora Slot 1" +HydraGetBonus = "(OC) Hydra Bonus: Sora Slot 2" +HerosCrest = "(OC) Hero's Crest" +AuronsStatue = "(OC2) Auron's Statue" +Hades = "(OC2) Hades Bonus: Sora Slot 1" +HadesGetBonus = "(OC2) Hades Bonus: Sora Slot 2" +GuardianSoul = "(OC2) Guardian Soul" +ProtectBeltPainandPanicCup = "Protect Belt Pain and Panic Cup" +SerenityGemPainandPanicCup = "Serenity Gem Pain and Panic Cup" +RisingDragonCerberusCup = "Rising Dragon Cerberus Cup" +SerenityCrystalCerberusCup = "Serenity Crystal Cerberus Cup" +GenjiShieldTitanCup = "Genji Shield Titan Cup" +SkillfulRingTitanCup = "Skillful Ring Titan Cup" +FatalCrestGoddessofFateCup = "Fatal Crest Goddess of Fate Cup" +OrichalcumPlusGoddessofFateCup = "Orichalcum+ Goddess of Fate Cup" +HadesCupTrophyParadoxCups = "Hades Cup Trophy Paradox Cups" +ZexionBonus = "Zexion Bonus: Sora Slot 1" +ZexionASBookofShadows = "AS Zexion" +ZexionDataLostIllusion = "Data Zexion" + + +BCCourtyardAPBoost = "(BC) Courtyard AP Boost" +BCCourtyardHiPotion = "(BC) Courtyard Hi-Potion" +BCCourtyardMythrilShard = "(BC) Courtyard Mythril Shard" +BellesRoomCastleMap = "(BC) Belle's Room Castle Map" +BellesRoomMegaRecipe = "(BC) Belle's Room Mega-Recipe" +TheEastWingMythrilShard = "(BC) The East Wing Mythril Shard" +TheEastWingTent = "(BC) The East Wing Tent" +TheWestHallHiPotion = "(BC) The West Hall Hi-Potion" +TheWestHallPowerShard = "(BC) The West Hall Power Shard" +TheWestHallMythrilShard2 = "(BC) The West Hall Mythril Shard 2" +TheWestHallBrightStone = "(BC) The West Hall Bright Stone" +TheWestHallMythrilShard = "(BC) The West Hall Mythril Shard" +Thresholder = "(BC) Thresholder Bonus: Sora Slot 1" +DungeonBasementMap = "(BC) Dungeon Basement Map" +DungeonAPBoost = "(BC) Dungeon AP Boost" +SecretPassageMythrilShard = "(BC) Secret Passage Mythril Shard" +SecretPassageHiPotion = "(BC) Secret Passage Hi-Potion" +SecretPassageLucidShard = "(BC) Secret Passage Lucid Shard" +TheWestHallAPBoostPostDungeon = "(BC) The West Hall AP Boost Post Dungeon" +TheWestWingMythrilShard = "(BC) The West Wing Mythril Shard" +TheWestWingTent = "(BC) The West Wing Tent" +Beast = "(BC) Beast Bonus: Sora Slot 1" +TheBeastsRoomBlazingShard = "(BC) The Beast's Room Blazing Shard" +DarkThorn = "(BC) Dark Thorn Bonus: Sora Slot 1" +DarkThornGetBonus = "(BC) Dark Thorn Bonus: Sora Slot 2" +DarkThornCureElement = "(BC) Dark Thorn Cure Element" +RumblingRose = "(BC2) Rumbling Rose" +CastleWallsMap = "(BC2) Castle Walls Map" +Xaldin = "(BC2) Xaldin Bonus: Sora Slot 1" +XaldinGetBonus = "(BC2) Xaldin Bonus: Sora Slot 2" +SecretAnsemReport4 = "(BC2) Secret Ansem Report 4 (Xaldin)" +XaldinDataDefenseBoost = "Data Xaldin" + + + +PitCellAreaMap = "(SP) Pit Cell Area Map" +PitCellMythrilCrystal = "(SP) Pit Cell Mythril Crystal" +CanyonDarkCrystal = "(SP) Canyon Dark Crystal" +CanyonMythrilStone = "(SP) Canyon Mythril Stone" +CanyonMythrilGem = "(SP) Canyon Mythril Gem" +CanyonFrostCrystal = "(SP) Canyon Frost Crystal" +Screens = "(SP) Screens Bonus: Sora Slot 1" +HallwayPowerCrystal = "(SP) Hallway Power Crystal" +HallwayAPBoost = "(SP) Hallway AP Boost" +CommunicationsRoomIOTowerMap = "(SP) Communications Room I/O Tower Map" +CommunicationsRoomGaiaBelt = "(SP) Communications Room Gaia Belt" +HostileProgram = "(SP) Hostile Program Bonus: Sora Slot 1" +HostileProgramGetBonus = "(SP) Hostile Program Bonus: Sora Slot 2" +PhotonDebugger = "(SP) Photon Debugger" +SolarSailer = "(SP2) Solar Sailer" +CentralComputerCoreAPBoost = "(SP2) Central Computer Core AP Boost" +CentralComputerCoreOrichalcumPlus = "(SP2) Central Computer Core Orichalcum+" +CentralComputerCoreCosmicArts = "(SP2) Central Computer Core Cosmic Arts" +CentralComputerCoreMap = "(SP2) Central Computer Core Map" +MCP = "(SP2) MCP Bonus: Sora Slot 1" +MCPGetBonus = "(SP2) MCP Bonus: Sora Slot 2" +LarxeneBonus = "Larxene Bonus: Sora Slot 1" +LarxeneASCloakedThunder = "AS Larxene" +LarxeneDataLostIllusion = "Data Larxene" + +GraveyardMythrilShard = "(HT) Graveyard Mythril Shard" +GraveyardSerenityGem = "(HT) Graveyard Serenity Gem" +FinklesteinsLabHalloweenTownMap = "(HT) Finklestein's Lab Halloween Town Map" +TownSquareMythrilStone = "(HT) Town Square Mythril Stone" +TownSquareEnergyShard = "(HT) Town Square Energy Shard" +HinterlandsLightningShard = "(HT) Hinterlands Lightning Shard" +HinterlandsMythrilStone = "(HT) Hinterlands Mythril Stone" +HinterlandsAPBoost = "(HT) Hinterlands AP Boost" +CandyCaneLaneMegaPotion = "(HT) Candy Cane Lane Mega-Potion" +CandyCaneLaneMythrilGem = "(HT) Candy Cane Lane Mythril Gem" +CandyCaneLaneLightningStone = "(HT) Candy Cane Lane Lightning Stone" +CandyCaneLaneMythrilStone = "(HT) Candy Cane Lane Mythril Stone" +SantasHouseChristmasTownMap = "(HT) Santa's House Christmas Town Map" +SantasHouseAPBoost = "(HT) Santa's House AP Boost" +PrisonKeeper = "(HT) Prison Keeper Bonus: Sora Slot 1" +OogieBoogie = "(HT) Oogie Boogie Bonus: Sora Slot 1" +OogieBoogieMagnetElement = "(HT) Oogie Boogie Magnet Element" +Lock = "(HT2) Lock, Shock and Barrel Bonus: Sora Slot 1" +Present = "(HT2) Present" +DecoyPresents = "(HT2) Decoy Presents" +Experiment = "(HT2) Experiment Bonus: Sora Slot 1" +DecisivePumpkin = "(HT2) Decisive Pumpkin" +VexenBonus = "Vexen Bonus: Sora Slot 1" +VexenASRoadtoDiscovery = "AS Vexen" +VexenDataLostIllusion = "Data Vexen" + +RampartNavalMap = "(PR) Rampart Naval Map" +RampartMythrilStone = "(PR) Rampart Mythril Stone" +RampartDarkShard = "(PR) Rampart Dark Shard" +TownDarkStone = "(PR) Town Dark Stone" +TownAPBoost = "(PR) Town AP Boost" +TownMythrilShard = "(PR) Town Mythril Shard" +TownMythrilGem = "(PR) Town Mythril Gem" +CaveMouthBrightShard = "(PR) Cave Mouth Bright Shard" +CaveMouthMythrilShard = "(PR) Cave Mouth Mythril Shard" +IsladeMuertaMap = "(PR) Isla de Muerta Map" +BoatFight = "(PR) Boat Fight Bonus: Sora Slot 1" +InterceptorBarrels = "(PR) Interceptor Barrels Bonus: Sora Slot 1" +PowderStoreAPBoost1 = "(PR) Powder Store AP Boost 1" +PowderStoreAPBoost2 = "(PR) Powder Store AP Boost 2" +MoonlightNookMythrilShard = "(PR) Moonlight Nook Mythril Shard" +MoonlightNookSerenityGem = "(PR) Moonlight Nook Serenity Gem" +MoonlightNookPowerStone = "(PR) Moonlight Nook Power Stone" +Barbossa = "(PR) Barbossa Bonus: Sora Slot 1" +BarbossaGetBonus = "(PR) Barbossa Bonus: Sora Slot 2" +FollowtheWind = "(PR) Follow the Wind" +GrimReaper1 = "(PR2) Grim Reaper 1 Bonus: Sora Slot 1" +InterceptorsHoldFeatherCharm = "(PR2) Interceptor's Hold Feather Charm" +SeadriftKeepAPBoost = "(PR2) Seadrift Keep AP Boost" +SeadriftKeepOrichalcum = "(PR2) Seadrift Keep Orichalcum" +SeadriftKeepMeteorStaff = "(PR2) Seadrift Keep Meteor Staff" +SeadriftRowSerenityGem = "(PR2) Seadrift Row Serenity Gem" +SeadriftRowKingRecipe = "(PR2) Seadrift Row King Recipe" +SeadriftRowMythrilCrystal = "(PR2) Seadrift Row Mythril Crystal" +SeadriftRowCursedMedallion = "(PR2) Seadrift Row Cursed Medallion" +SeadriftRowShipGraveyardMap = "(PR2) Seadrift Row Ship Graveyard Map" +GrimReaper2 = "(PR2) Grim Reaper 2 Bonus: Sora Slot 1" +SecretAnsemReport6 = "(PR2) Secret Ansem Report 6 (Grim Reaper 2)" + +LuxordDataAPBoost = "Data Luxord" + +MarketplaceMap = "(HB) Marketplace Map" +BoroughDriveRecovery = "(HB) Borough Drive Recovery" +BoroughAPBoost = "(HB) Borough AP Boost" +BoroughHiPotion = "(HB) Borough Hi-Potion" +BoroughMythrilShard = "(HB) Borough Mythril Shard" +BoroughDarkShard = "(HB) Borough Dark Shard" +MerlinsHouseMembershipCard = "(HB) Merlin's House Membership Card" +MerlinsHouseBlizzardElement = "(HB) Merlin's House Blizzard Element" +Bailey = "(HB) Bailey Bonus: Sora Slot 1" +BaileySecretAnsemReport7 = "(HB) Bailey Secret Ansem Report 7" +BaseballCharm = "(HB) Baseball Charm" +PosternCastlePerimeterMap = "(HB2) Postern Castle Perimeter Map" +PosternMythrilGem = "(HB2) Postern Mythril Gem" +PosternAPBoost = "(HB2) Postern AP Boost" +CorridorsMythrilStone = "(HB2) Corridors Mythril Stone" +CorridorsMythrilCrystal = "(HB2) Corridors Mythril Crystal" +CorridorsDarkCrystal = "(HB2) Corridors Dark Crystal" +CorridorsAPBoost = "(HB2) Corridors AP Boost" +AnsemsStudyMasterForm = "(HB2) Ansem's Study Master Form" +AnsemsStudySleepingLion = "(HB2) Ansem's Study Sleeping Lion" +AnsemsStudySkillRecipe = "(HB2) Ansem's Study Skill Recipe" +AnsemsStudyUkuleleCharm = "(HB2) Ansem's Study Ukulele Charm" +RestorationSiteMoonRecipe = "(HB2) Restoration Site Moon Recipe" +RestorationSiteAPBoost = "(HB2) Restoration Site AP Boost" +DemyxHB = "(HB2) Demyx Bonus: Sora Slot 1" +DemyxHBGetBonus = "(HB2) Demyx Bonus: Sora Slot 2" +FFFightsCureElement = "(HB2) FF Fights Cure Element" +CrystalFissureTornPages = "(HB2) Crystal Fissure Torn Pages" +CrystalFissureTheGreatMawMap = "(HB2) Crystal Fissure The Great Maw Map" +CrystalFissureEnergyCrystal = "(HB2) Crystal Fissure Energy Crystal" +CrystalFissureAPBoost = "(HB2) Crystal Fissure AP Boost" +ThousandHeartless = "(HB2) 1000 Heartless" +ThousandHeartlessSecretAnsemReport1 = "(HB2) 1000 Heartless Secret Ansem Report 1" +ThousandHeartlessIceCream = "(HB2) 1000 Heartless Ice Cream" +ThousandHeartlessPicture = "(HB2) 1000 Heartless Picture" +PosternGullWing = "(HB2) Postern Gull Wing" +HeartlessManufactoryCosmicChain = "(HB2) Heartless Manufactory Cosmic Chain" +SephirothBonus = "Sephiroth Bonus: Sora Slot 1" +SephirothFenrir = "Sephiroth Fenrir" +WinnersProof = "(HB2) Winner's Proof" +ProofofPeace = "(HB2) Proof of Peace" +DemyxDataAPBoost = "Data Demyx" + +CoRDepthsAPBoost = "(CoR) Depths AP Boost" +CoRDepthsPowerCrystal = "(CoR) Depths Power Crystal" +CoRDepthsFrostCrystal = "(CoR) Depths Frost Crystal" +CoRDepthsManifestIllusion = "(CoR) Depths Manifest Illusion" +CoRDepthsAPBoost2 = "(CoR) Depths AP Boost 2" +CoRMineshaftLowerLevelDepthsofRemembranceMap = "(CoR) Mineshaft Lower Level Depths of Remembrance Map" +CoRMineshaftLowerLevelAPBoost = "(CoR) Mineshaft Lower Level AP Boost" +CoRDepthsUpperLevelRemembranceGem = "(CoR) Depths Upper Level Remembrance Gem" +CoRMiningAreaSerenityGem = "(CoR) Mining Area Serenity Gem" +CoRMiningAreaAPBoost = "(CoR) Mining Area AP Boost" +CoRMiningAreaSerenityCrystal = "(CoR) Mining Area Serenity Crystal" +CoRMiningAreaManifestIllusion = "(CoR) Mining Area Manifest Illusion" +CoRMiningAreaSerenityGem2 = "(CoR) Mining Area Serenity Gem 2" +CoRMiningAreaDarkRemembranceMap = "(CoR) Mining Area Dark Remembrance Map" +CoRMineshaftMidLevelPowerBoost = "(CoR) Mineshaft Mid Level Power Boost" +CoREngineChamberSerenityCrystal = "(CoR) Engine Chamber Serenity Crystal" +CoREngineChamberRemembranceCrystal = "(CoR) Engine Chamber Remembrance Crystal" +CoREngineChamberAPBoost = "(CoR) Engine Chamber AP Boost" +CoREngineChamberManifestIllusion = "(CoR) Engine Chamber Manifest Illusion" +CoRMineshaftUpperLevelMagicBoost = "(CoR) Mineshaft Upper Level Magic Boost" +CoRMineshaftUpperLevelAPBoost = "(CoR) Mineshaft Upper Level AP Boost" +TransporttoRemembrance = "Transport to Remembrance" + +GorgeSavannahMap = "(PL) Gorge Savannah Map" +GorgeDarkGem = "(PL) Gorge Dark Gem" +GorgeMythrilStone = "(PL) Gorge Mythril Stone" +ElephantGraveyardFrostGem = "(PL) Elephant Graveyard Frost Gem" +ElephantGraveyardMythrilStone = "(PL) Elephant Graveyard Mythril Stone" +ElephantGraveyardBrightStone = "(PL) Elephant Graveyard Bright Stone" +ElephantGraveyardAPBoost = "(PL) Elephant Graveyard AP Boost" +ElephantGraveyardMythrilShard = "(PL) Elephant Graveyard Mythril Shard" +PrideRockMap = "(PL) Pride Rock Map" +PrideRockMythrilStone = "(PL) Pride Rock Mythril Stone" +PrideRockSerenityCrystal = "(PL) Pride Rock Serenity Crystal" +WildebeestValleyEnergyStone = "(PL) Wildebeest Valley Energy Stone" +WildebeestValleyAPBoost = "(PL) Wildebeest Valley AP Boost" +WildebeestValleyMythrilGem = "(PL) Wildebeest Valley Mythril Gem" +WildebeestValleyMythrilStone = "(PL) Wildebeest Valley Mythril Stone" +WildebeestValleyLucidGem = "(PL) Wildebeest Valley Lucid Gem" +WastelandsMythrilShard = "(PL) Wastelands Mythril Shard" +WastelandsSerenityGem = "(PL) Wastelands Serenity Gem" +WastelandsMythrilStone = "(PL) Wastelands Mythril Stone" +JungleSerenityGem = "(PL) Jungle Serenity Gem" +JungleMythrilStone = "(PL) Jungle Mythril Stone" +JungleSerenityCrystal = "(PL) Jungle Serenity Crystal" +OasisMap = "(PL) Oasis Map" +OasisTornPages = "(PL) Oasis Torn Pages" +OasisAPBoost = "(PL) Oasis AP Boost" +CircleofLife = "(PL) Circle of Life" +Hyenas1 = "(PL) Hyenas 1 Bonus: Sora Slot 1" +Scar = "(PL) Scar Bonus: Sora Slot 1" +ScarFireElement = "(PL) Scar Fire Element" +Hyenas2 = "(PL2) Hyenas 2 Bonus: Sora Slot 1" +Groundshaker = "(PL2) Groundshaker Bonus: Sora Slot 1" +GroundshakerGetBonus = "(PL2) Groundshaker Bonus: Sora Slot 2" +SaixDataDefenseBoost = "Data Saix" + +TwilightTownMap = "(STT) Twilight Town Map" +MunnyPouchOlette = "(STT) Munny Pouch Olette" +StationDusks = "(STT) Station Dusks" +StationofSerenityPotion = "(STT) Station of Serenity Potion" +StationofCallingPotion = "(STT) Station of Calling Potion" +TwilightThorn = "(STT) Twilight Thorn" +Axel1 = "(STT) Axel 1" +JunkChampionBelt = "(STT) Junk Champion Belt" +JunkMedal = "(STT) Junk Medal" +TheStruggleTrophy = "(STT) The Struggle Trophy" +CentralStationPotion1 = "(STT) Central Station Potion 1" +STTCentralStationHiPotion = "(STT) Central Station Hi-Potion" +CentralStationPotion2 = "(STT) Central Station Potion 2" +SunsetTerraceAbilityRing = "(STT) Sunset Terrace Ability Ring" +SunsetTerraceHiPotion = "(STT) Sunset Terrace Hi-Potion" +SunsetTerracePotion1 = "(STT) Sunset Terrace Potion 1" +SunsetTerracePotion2 = "(STT) Sunset Terrace Potion 2" +MansionFoyerHiPotion = "(STT) Mansion Foyer Hi-Potion" +MansionFoyerPotion1 = "(STT) Mansion Foyer Potion 1" +MansionFoyerPotion2 = "(STT) Mansion Foyer Potion 2" +MansionDiningRoomElvenBandanna = "(STT) Mansion Dining Room Elven Bandanna" +MansionDiningRoomPotion = "(STT) Mansion Dining Room Potion" +NaminesSketches = "(STT) Namine's Sketches" +MansionMap = "(STT) Mansion Map" +MansionLibraryHiPotion = "(STT) Mansion Library Hi-Potion" +Axel2 = "(STT) Axel 2" +MansionBasementCorridorHiPotion = "(STT) Mansion Basement Corridor Hi-Potion" +RoxasDataMagicBoost = "Data Roxas" + +OldMansionPotion = "(TT) Old Mansion Potion" +OldMansionMythrilShard = "(TT) Old Mansion Mythril Shard" +TheWoodsPotion = "(TT) The Woods Potion" +TheWoodsMythrilShard = "(TT) The Woods Mythril Shard" +TheWoodsHiPotion = "(TT) The Woods Hi-Potion" +TramCommonHiPotion = "(TT) Tram Common Hi-Potion" +TramCommonAPBoost = "(TT) Tram Common AP Boost" +TramCommonTent = "(TT) Tram Common Tent" +TramCommonMythrilShard1 = "(TT) Tram Common Mythril Shard 1" +TramCommonPotion1 = "(TT) Tram Common Potion 1" +TramCommonMythrilShard2 = "(TT) Tram Common Mythril Shard 2" +TramCommonPotion2 = "(TT) Tram Common Potion 2" +StationPlazaSecretAnsemReport2 = "(TT) Station Plaza Secret Ansem Report 2" +MunnyPouchMickey = "(TT) Munny Pouch Mickey" +CrystalOrb = "(TT) Crystal Orb" +CentralStationTent = "(TT) Central Station Tent" +TTCentralStationHiPotion = "(TT) Central Station Hi-Potion" +CentralStationMythrilShard = "(TT) Central Station Mythril Shard" +TheTowerPotion = "(TT) The Tower Potion" +TheTowerHiPotion = "(TT) The Tower Hi-Potion" +TheTowerEther = "(TT) The Tower Ether" +TowerEntrywayEther = "(TT) Tower Entryway Ether" +TowerEntrywayMythrilShard = "(TT) Tower Entryway Mythril Shard" +SorcerersLoftTowerMap = "(TT) Sorcerer's Loft Tower Map" +TowerWardrobeMythrilStone = "(TT) Tower Wardrobe Mythril Stone" +StarSeeker = "(TT) Star Seeker" +ValorForm = "(TT) Valor Form" +SeifersTrophy = "(TT2) Seifer's Trophy" +Oathkeeper = "(TT2) Oathkeeper" +LimitForm = "(TT2) Limit Form" +UndergroundConcourseMythrilGem = "(TT3) Underground Concourse Mythril Gem" +UndergroundConcourseOrichalcum = "(TT3) Underground Concourse Orichalcum" +UndergroundConcourseAPBoost = "(TT3) Underground Concourse AP Boost" +UndergroundConcourseMythrilCrystal = "(TT3) Underground Concourse Mythril Crystal" +TunnelwayOrichalcum = "(TT3) Tunnelway Orichalcum" +TunnelwayMythrilCrystal = "(TT3) Tunnelway Mythril Crystal" +SunsetTerraceOrichalcumPlus = "(TT3) Sunset Terrace Orichalcum+" +SunsetTerraceMythrilShard = "(TT3) Sunset Terrace Mythril Shard" +SunsetTerraceMythrilCrystal = "(TT3) Sunset Terrace Mythril Crystal" +SunsetTerraceAPBoost = "(TT3) Sunset Terrace AP Boost" +MansionNobodies = "(TT3) Mansion Nobodies Bonus: Sora Slot 1" +MansionFoyerMythrilCrystal = "(TT3) Mansion Foyer Mythril Crystal" +MansionFoyerMythrilStone = "(TT3) Mansion Foyer Mythril Stone" +MansionFoyerSerenityCrystal = "(TT3) Mansion Foyer Serenity Crystal" +MansionDiningRoomMythrilCrystal = "(TT3) Mansion Dining Room Mythril Crystal" +MansionDiningRoomMythrilStone = "(TT3) Mansion Dining Room Mythril Stone" +MansionLibraryOrichalcum = "(TT3) Mansion Library Orichalcum" +BeamSecretAnsemReport10 = "(TT3) Beam Secret Ansem Report 10" +MansionBasementCorridorUltimateRecipe = "(TT3) Mansion Basement Corridor Ultimate Recipe" +BetwixtandBetween = "(TT3) Betwixt and Between" +BetwixtandBetweenBondofFlame = "(TT3) Betwixt and Between Bond of Flame" +AxelDataMagicBoost = "Data Axel" + +FragmentCrossingMythrilStone = "(TWTNW) Fragment Crossing Mythril Stone" +FragmentCrossingMythrilCrystal = "(TWTNW) Fragment Crossing Mythril Crystal" +FragmentCrossingAPBoost = "(TWTNW) Fragment Crossing AP Boost" +FragmentCrossingOrichalcum = "(TWTNW) Fragment Crossing Orichalcum" +Roxas = "(TWTNW) Roxas Bonus: Sora Slot 1" +RoxasGetBonus = "(TWTNW) Roxas Bonus: Sora Slot 2" +RoxasSecretAnsemReport8 = "(TWTNW) Roxas Secret Ansem Report 8" +TwoBecomeOne = "(TWTNW) Two Become One" +MemorysSkyscaperMythrilCrystal = "(TWTNW) Memory's Skyscaper Mythril Crystal" +MemorysSkyscaperAPBoost = "(TWTNW) Memory's Skyscaper AP Boost" +MemorysSkyscaperMythrilStone = "(TWTNW) Memory's Skyscaper Mythril Stone" +TheBrinkofDespairDarkCityMap = "(TWTNW) The Brink of Despair Dark City Map" +TheBrinkofDespairOrichalcumPlus = "(TWTNW) The Brink of Despair Orichalcum+" +NothingsCallMythrilGem = "(TWTNW) Nothing's Call Mythril Gem" +NothingsCallOrichalcum = "(TWTNW) Nothing's Call Orichalcum" +TwilightsViewCosmicBelt = "(TWTNW) Twilight's View Cosmic Belt" +XigbarBonus = "(TWTNW) Xigbar Bonus: Sora Slot 1" +XigbarSecretAnsemReport3 = "(TWTNW) Xigbar Secret Ansem Report 3" +NaughtsSkywayMythrilGem = "(TWTNW) Naught's Skyway Mythril Gem" +NaughtsSkywayOrichalcum = "(TWTNW) Naught's Skyway Orichalcum" +NaughtsSkywayMythrilCrystal = "(TWTNW) Naught's Skyway Mythril Crystal" +Oblivion = "(TWTNW) Oblivion" +CastleThatNeverWasMap = "(TWTNW) Castle That Never Was Map" +Luxord = "(TWTNW) Luxord" +LuxordGetBonus = "(TWTNW) Luxord Bonus: Sora Slot 1" +LuxordSecretAnsemReport9 = "(TWTNW) Luxord Secret Ansem Report 9" +SaixBonus = "(TWTNW) Saix Bonus: Sora Slot 1" +SaixSecretAnsemReport12 = "(TWTNW) Saix Secret Ansem Report 12" +PreXemnas1SecretAnsemReport11 = "(TWTNW) Secret Ansem Report 11 (Pre-Xemnas 1)" +RuinandCreationsPassageMythrilStone = "(TWTNW) Ruin and Creation's Passage Mythril Stone" +RuinandCreationsPassageAPBoost = "(TWTNW) Ruin and Creation's Passage AP Boost" +RuinandCreationsPassageMythrilCrystal = "(TWTNW) Ruin and Creation's Passage Mythril Crystal" +RuinandCreationsPassageOrichalcum = "(TWTNW) Ruin and Creation's Passage Orichalcum" +Xemnas1 = "(TWTNW) Xemnas 1 Bonus: Sora Slot 1" +Xemnas1GetBonus = "(TWTNW) Xemnas 1 Bonus: Sora Slot 2" +Xemnas1SecretAnsemReport13 = "(TWTNW) Xemnas 1 Secret Ansem Report 13" +FinalXemnas = "Final Xemnas" +XemnasDataPowerBoost = "Data Xemnas" +Lvl1 ="Level 01" +Lvl2 ="Level 02" +Lvl3 ="Level 03" +Lvl4 ="Level 04" +Lvl5 ="Level 05" +Lvl6 ="Level 06" +Lvl7 ="Level 07" +Lvl8 ="Level 08" +Lvl9 ="Level 09" +Lvl10 ="Level 10" +Lvl11 ="Level 11" +Lvl12 ="Level 12" +Lvl13 ="Level 13" +Lvl14 ="Level 14" +Lvl15 ="Level 15" +Lvl16 ="Level 16" +Lvl17 ="Level 17" +Lvl18 ="Level 18" +Lvl19 ="Level 19" +Lvl20 ="Level 20" +Lvl21 ="Level 21" +Lvl22 ="Level 22" +Lvl23 ="Level 23" +Lvl24 ="Level 24" +Lvl25 ="Level 25" +Lvl26 ="Level 26" +Lvl27 ="Level 27" +Lvl28 ="Level 28" +Lvl29 ="Level 29" +Lvl30 ="Level 30" +Lvl31 ="Level 31" +Lvl32 ="Level 32" +Lvl33 ="Level 33" +Lvl34 ="Level 34" +Lvl35 ="Level 35" +Lvl36 ="Level 36" +Lvl37 ="Level 37" +Lvl38 ="Level 38" +Lvl39 ="Level 39" +Lvl40 ="Level 40" +Lvl41 ="Level 41" +Lvl42 ="Level 42" +Lvl43 ="Level 43" +Lvl44 ="Level 44" +Lvl45 ="Level 45" +Lvl46 ="Level 46" +Lvl47 ="Level 47" +Lvl48 ="Level 48" +Lvl49 ="Level 49" +Lvl50 ="Level 50" +Lvl51 ="Level 51" +Lvl52 ="Level 52" +Lvl53 ="Level 53" +Lvl54 ="Level 54" +Lvl55 ="Level 55" +Lvl56 ="Level 56" +Lvl57 ="Level 57" +Lvl58 ="Level 58" +Lvl59 ="Level 59" +Lvl60 ="Level 60" +Lvl61 ="Level 61" +Lvl62 ="Level 62" +Lvl63 ="Level 63" +Lvl64 ="Level 64" +Lvl65 ="Level 65" +Lvl66 ="Level 66" +Lvl67 ="Level 67" +Lvl68 ="Level 68" +Lvl69 ="Level 69" +Lvl70 ="Level 70" +Lvl71 ="Level 71" +Lvl72 ="Level 72" +Lvl73 ="Level 73" +Lvl74 ="Level 74" +Lvl75 ="Level 75" +Lvl76 ="Level 76" +Lvl77 ="Level 77" +Lvl78 ="Level 78" +Lvl79 ="Level 79" +Lvl80 ="Level 80" +Lvl81 ="Level 81" +Lvl82 ="Level 82" +Lvl83 ="Level 83" +Lvl84 ="Level 84" +Lvl85 ="Level 85" +Lvl86 ="Level 86" +Lvl87 ="Level 87" +Lvl88 ="Level 88" +Lvl89 ="Level 89" +Lvl90 ="Level 90" +Lvl91 ="Level 91" +Lvl92 ="Level 92" +Lvl93 ="Level 93" +Lvl94 ="Level 94" +Lvl95 ="Level 95" +Lvl96 ="Level 96" +Lvl97 ="Level 97" +Lvl98 ="Level 98" +Lvl99 ="Level 99" +Valorlvl1 ="Valor level 1" +Valorlvl2 ="Valor level 2" +Valorlvl3 ="Valor level 3" +Valorlvl4 ="Valor level 4" +Valorlvl5 ="Valor level 5" +Valorlvl6 ="Valor level 6" +Valorlvl7 ="Valor level 7" +Wisdomlvl1 ="Wisdom level 1" +Wisdomlvl2 ="Wisdom level 2" +Wisdomlvl3 ="Wisdom level 3" +Wisdomlvl4 ="Wisdom level 4" +Wisdomlvl5 ="Wisdom level 5" +Wisdomlvl6 ="Wisdom level 6" +Wisdomlvl7 ="Wisdom level 7" +Limitlvl1 ="Limit level 1" +Limitlvl2 ="Limit level 2" +Limitlvl3 ="Limit level 3" +Limitlvl4 ="Limit level 4" +Limitlvl5 ="Limit level 5" +Limitlvl6 ="Limit level 6" +Limitlvl7 ="Limit level 7" +Masterlvl1 ="Master level 1" +Masterlvl2 ="Master level 2" +Masterlvl3 ="Master level 3" +Masterlvl4 ="Master level 4" +Masterlvl5 ="Master level 5" +Masterlvl6 ="Master level 6" +Masterlvl7 ="Master level 7" +Finallvl1 ="Final level 1" +Finallvl2 ="Final level 2" +Finallvl3 ="Final level 3" +Finallvl4 ="Final level 4" +Finallvl5 ="Final level 5" +Finallvl6 ="Final level 6" +Finallvl7 ="Final level 7" + +GardenofAssemblageMap ="Garden of Assemblage Map" +GoALostIllusion ="GoA Lost Illusion" +ProofofNonexistence ="Proof of Nonexistence Location" + +test= "test" + + +Crit_1 ="Critical Starting Ability 1" +Crit_2 ="Critical Starting Ability 2" +Crit_3 ="Critical Starting Ability 3" +Crit_4 ="Critical Starting Ability 4" +Crit_5 ="Critical Starting Ability 5" +Crit_6 ="Critical Starting Ability 6" +Crit_7 ="Critical Starting Ability 7" +DonaldStarting1 ="Donald Starting Item 1" +DonaldStarting2 ="Donald Starting Item 2" +GoofyStarting1 ="Goofy Starting Item 1" +GoofyStarting2 ="Goofy Starting Item 2" + + +DonaldScreens ="(SP) Screens Bonus: Donald Slot 1" +DonaldDemyxHBGetBonus ="(HB) Demyx Bonus: Donald Slot 1" +DonaldDemyxOC ="(OC) Demyx Bonus: Donald Slot 1" +DonaldBoatPete ="(TR) Boat Pete Bonus: Donald Slot 1" +DonaldBoatPeteGetBonus ="(TR) Boat Pete Bonus: Donald Slot 2" +DonaldPrisonKeeper ="(HT) Prison Keeper Bonus: Donald Slot 1" +DonaldScar ="(PL) Scar Bonus: Donald Slot 1" +DonaldSolarSailer ="(SP2) Solar Sailer Bonus: Donald Slot 1" +DonaldExperiment ="(HT2) Experiment Bonus: Donald Slot 1" +DonaldBoatFight ="(PR) Boat Fight Bonus: Donald Slot 1" +DonaldMansionNobodies ="(TT3) Mansion Nobodies Bonus: Donald Slot 1" +DonaldThresholder ="(BC) Thresholder Bonus: Donald Slot 1" +DonaldXaldinGetBonus ="(BC2) Xaldin Bonus: Donald Slot 1" +DonaladGrimReaper2 ="(PR2) Grim Reaper 2 Bonus: Donald Slot 1" +DonaldAbuEscort ="(AG) Abu Escort Bonus: Donald Slot 1" + +GoofyBarbossa ="(PR) Barbossa Bonus: Goofy Slot 1" +GoofyBarbossaGetBonus ="(PR) Barbossa Bonus: Goofy Slot 2" +GoofyGrimReaper1 ="(PR2) Grim Reaper 1 Bonus: Goofy Slot 1" +GoofyHostileProgram ="(SP) Hostile Program Bonus: Goofy Slot 1" +GoofyHyenas1 ="(PL) Hyenas 1 Bonus: Goofy Slot 1" +GoofyHyenas2 ="(PL2) Hyenas 2 Bonus: Goofy Slot 1" +GoofyLock ="(HT2) Lock, Shock and Barrel Bonus: Goofy Slot 1" +GoofyOogieBoogie ="(HT) Oogie Boogie Bonus: Goofy Slot 1" +GoofyPeteOC ="(OC) Pete Bonus: Goofy Slot 1" +GoofyFuturePete ="(TR) Future Pete Bonus: Goofy Slot 1" +GoofyShanYu ="(LoD) Shan-Yu Bonus: Goofy Slot 1" +GoofyStormRider ="(LoD2) Storm Rider Bonus: Goofy Slot 1" +GoofyBeast ="(BC) Beast Bonus: Goofy Slot 1" +GoofyInterceptorBarrels ="(PR) Interceptor Barrels Bonus: Goofy Slot 1" +GoofyTreasureRoom ="(AG) Treasure Room Heartless Bonus: Goofy Slot 1" +GoofyZexion ="Zexion Bonus: Goofy Slot 1" + + +AdamantShield ="Adamant Shield Slot" +AkashicRecord ="Akashic Record Slot" +ChainGear ="Chain Gear Slot" +DreamCloud ="Dream Cloud Slot" +FallingStar ="Falling Star Slot" +FrozenPride2 ="Frozen Pride+ Slot" +GenjiShield ="Genji Shield Slot" +KnightDefender ="Knight Defender Slot" +KnightsShield ="Knight's Shield Slot" +MajesticMushroom ="Majestic Mushroom Slot" +MajesticMushroom2 ="Majestic Mushroom+ Slot" +NobodyGuard ="Nobody Guard Slot" +OgreShield ="Ogre Shield Slot" +SaveTheKing2 ="Save The King+ Slot" +UltimateMushroom ="Ultimate Mushroom Slot" + +HammerStaff ="Hammer Staff Slot" +LordsBroom ="Lord's Broom Slot" +MagesStaff ="Mages Staff Slot" +MeteorStaff ="Meteor Staff Slot" +CometStaff ="Comet Staff Slot" +Centurion2 ="Centurion+ Slot" +MeteorStaff ="Meteor Staff Slot" +NobodyLance ="Nobody Lance Slot" +PreciousMushroom ="Precious Mushroom Slot" +PreciousMushroom2 ="Precious Mushroom+ Slot" +PremiumMushroom ="Premium Mushroom Slot" +RisingDragon ="Rising Dragon Slot" +SaveTheQueen2 ="Save The Queen+ Slot" +ShamansRelic ="Shaman's Relic Slot" +VictoryBell ="Victory Bell Slot" +WisdomWand ="Wisdom Wand Slot" + +#Keyblade Slots +FAKESlot ="FAKE Slot" +DetectionSaberSlot ="Detection Saber Slot" +EdgeofUltimaSlot ="Edge of Ultima Slot" +KingdomKeySlot ="Kingdom Key Slot" +OathkeeperSlot ="Oathkeeper Slot" +OblivionSlot ="Oblivion Slot" +StarSeekerSlot ="Star Seeker Slot" +HiddenDragonSlot ="Hidden Dragon Slot" +HerosCrestSlot ="Hero's Crest Slot" +MonochromeSlot ="Monochrome Slot" +FollowtheWindSlot ="Follow the Wind Slot" +CircleofLifeSlot ="Circle of Life Slot" +PhotonDebuggerSlot ="Photon Debugger Slot" +GullWingSlot ="Gull Wing Slot" +RumblingRoseSlot ="Rumbling Rose Slot" +GuardianSoulSlot ="Guardian Soul Slot" +WishingLampSlot ="Wishing Lamp Slot" +DecisivePumpkinSlot ="Decisive Pumpkin Slot" +SweetMemoriesSlot ="Sweet Memories Slot" +MysteriousAbyssSlot ="Mysterious Abyss Slot" +SleepingLionSlot ="Sleeping Lion Slot" +BondofFlameSlot ="Bond of Flame Slot" +TwoBecomeOneSlot ="Two Become One Slot" +FatalCrestSlot ="Fatal Crest Slot" +FenrirSlot ="Fenrir Slot" +UltimaWeaponSlot ="Ultima Weapon Slot" +WinnersProofSlot ="Winner's Proof Slot" +PurebloodSlot ="Pureblood Slot" + +#Final_Region ="Final Form" diff --git a/worlds/kh2/Names/RegionName.py b/worlds/kh2/Names/RegionName.py new file mode 100644 index 0000000000..d07b5d3de3 --- /dev/null +++ b/worlds/kh2/Names/RegionName.py @@ -0,0 +1,90 @@ +LoD_Region ="Land of Dragons" +LoD2_Region ="Land of Dragons 2" + +Ag_Region ="Agrabah" +Ag2_Region ="Agrabah 2" + +Dc_Region ="Disney Castle" +Tr_Region ="Timeless River" + +HundredAcre1_Region ="Pooh's House" +HundredAcre2_Region ="Piglet's House" +HundredAcre3_Region ="Rabbit's House" +HundredAcre4_Region ="Roo's House" +HundredAcre5_Region ="Spookey Cave" +HundredAcre6_Region ="Starry Hill" + +Pr_Region ="Port Royal" +Pr2_Region ="Port Royal 2" +Gr2_Region ="Grim Reaper 2" + +Oc_Region ="Olympus Coliseum" +Oc2_Region ="Olympus Coliseum 2" +Oc2_pain_and_panic_Region ="Pain and Panic Cup" +Oc2_titan_Region ="Titan Cup" +Oc2_cerberus_Region ="Cerberus Cup" +Oc2_gof_Region ="Goddest of Fate Cup" +Oc2Cups_Region ="Olympus Coliseum Cups" +HadesCups_Region ="Olympus Coliseum Hade's Paradox" + +Bc_Region ="Beast's Castle" +Bc2_Region ="Beast's Castle 2" +Xaldin_Region ="Xaldin" + +Sp_Region ="Space Paranoids" +Sp2_Region ="Space Paranoids 2" +Mcp_Region ="Master Control Program" + +Ht_Region ="Holloween Town" +Ht2_Region ="Holloween Town 2" + +Hb_Region ="Hollow Bastion" +Hb2_Region ="Hollow Bastion 2" +ThousandHeartless_Region ="Thousand Hearless" +Mushroom13_Region ="Mushroom 13" +CoR_Region ="Cavern of Rememberance" +Transport_Region ="Transport to Rememberance" + +Pl_Region ="Pride Lands" +Pl2_Region ="Pride Lands 2" + +STT_Region ="Simulated Twilight Town" + +TT_Region ="Twlight Town" +TT2_Region ="Twlight Town 2" +TT3_Region ="Twlight Town 3" + +Twtnw_Region ="The World That Never Was (First Visit)" +Twtnw_PostRoxas ="The World That Never Was (Post Roxas)" +Twtnw_PostXigbar ="The World That Never Was (Post Xigbar)" +Twtnw2_Region ="The World That Never Was (Second Visit)" #before riku transformation + +SoraLevels_Region ="Sora's Levels" +GoA_Region ="Garden Of Assemblage" +Keyblade_Region ="Keyblade Slots" + +Valor_Region ="Valor Form" +Wisdom_Region ="Wisdom Form" +Limit_Region ="Limit Form" +Master_Region ="Master Form" +Final_Region ="Final Form" + +Terra_Region ="Lingering Will" +Sephi_Region ="Sephiroth" +Marluxia_Region ="Marluxia" +Larxene_Region ="Larxene" +Vexen_Region ="Vexen" +Lexaeus_Region ="Lexaeus" +Zexion_Region ="Zexion" + +LevelsVS1 ="Levels Region (1 Visit Locking Item)" +LevelsVS3 ="Levels Region (3 Visit Locking Items)" +LevelsVS6 ="Levels Region (6 Visit Locking Items)" +LevelsVS9 ="Levels Region (9 Visit Locking Items)" +LevelsVS12 ="Levels Region (12 Visit Locking Items)" +LevelsVS15 ="Levels Region (15 Visit Locking Items)" +LevelsVS18 ="Levels Region (18 Visit Locking Items)" +LevelsVS21 ="Levels Region (21 Visit Locking Items)" +LevelsVS24 ="Levels Region (24 Visit Locking Items)" +LevelsVS26 ="Levels Region (26 Visit Locking Items)" + diff --git a/worlds/kh2/OpenKH.py b/worlds/kh2/OpenKH.py new file mode 100644 index 0000000000..a1f9cc362e --- /dev/null +++ b/worlds/kh2/OpenKH.py @@ -0,0 +1,246 @@ +import yaml +import os +import Utils +import zipfile + +from .Items import item_dictionary_table, CheckDupingItems +from .Locations import all_locations, SoraLevels, exclusion_table, AllWeaponSlot +from .Names import LocationName +from .XPValues import lvlStats, formExp, soraExp +from worlds.Files import APContainer + + +class KH2Container(APContainer): + game: str = 'Kingdom Hearts 2' + + def __init__(self, patch_data: dict, base_path: str, output_directory: str, + player=None, player_name: str = "", server: str = ""): + self.patch_data = patch_data + self.file_path = base_path + container_path = os.path.join(output_directory, base_path + ".zip") + super().__init__(container_path, player, player_name, server) + + def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None: + for filename, yml in self.patch_data.items(): + opened_zipfile.writestr(filename, yml) + for root, dirs, files in os.walk(os.path.join(os.path.dirname(__file__), "mod_template")): + for file in files: + opened_zipfile.write(os.path.join(root, file), + os.path.relpath(os.path.join(root, file), + os.path.join(os.path.dirname(__file__), "mod_template"))) + # opened_zipfile.writestr(self.zpf_path, self.patch_data) + super().write_contents(opened_zipfile) + + +def patch_kh2(self, output_directory): + def increaseStat(i): + if lvlStats[i] == "str": + self.strength += 2 + elif lvlStats[i] == "mag": + self.magic += 2 + elif lvlStats[i] == "def": + self.defense += 1 + elif lvlStats[i] == "ap": + self.ap += 3 + + self.formattedTrsr = {} + self.formattedBons = [] + self.formattedLvup = {"Sora": {}} + self.formattedBons = {} + self.formattedFmlv = {} + self.formattedItem = {"Stats": []} + self.formattedPlrp = [] + self.strength = 2 + self.magic = 6 + self.defense = 2 + self.ap = 0 + self.dblbonus = 0 + formexp = None + formName = None + levelsetting = list() + slotDataDuping = set() + for values in CheckDupingItems.values(): + if isinstance(values, set): + slotDataDuping |= values + else: + for inner_values in values.values(): + slotDataDuping |= inner_values + + if self.multiworld.Keyblade_Minimum[self.player].value > self.multiworld.Keyblade_Maximum[self.player].value: + print( + f"{self.multiworld.get_file_safe_player_name(self.player)} has Keyblade Minimum greater than Keyblade Maximum") + keyblademin = self.multiworld.Keyblade_Maximum[self.player].value + keyblademax = self.multiworld.Keyblade_Minimum[self.player].value + else: + keyblademin = self.multiworld.Keyblade_Minimum[self.player].value + keyblademax = self.multiworld.Keyblade_Maximum[self.player].value + + if self.multiworld.LevelDepth[self.player] == "level_50": + levelsetting.extend(exclusion_table["Level50"]) + + elif self.multiworld.LevelDepth[self.player] == "level_99": + levelsetting.extend(exclusion_table["Level99"]) + + elif self.multiworld.LevelDepth[self.player] in ["level_50_sanity", "level_99_sanity"]: + levelsetting.extend(exclusion_table["Level50Sanity"]) + + if self.multiworld.LevelDepth[self.player] == "level_99_sanity": + levelsetting.extend(exclusion_table["Level99Sanity"]) + + mod_name = f"AP-{self.multiworld.seed_name}-P{self.player}-{self.multiworld.get_file_safe_player_name(self.player)}" + + for location in self.multiworld.get_filled_locations(self.player): + + data = all_locations[location.name] + if location.item.player == self.player: + itemcode = item_dictionary_table[location.item.name].kh2id + if location.item.name in slotDataDuping and \ + location.name not in AllWeaponSlot: + self.LocalItems[location.address] = item_dictionary_table[location.item.name].code + else: + itemcode = 90 # castle map + + if data.yml == "Chest": + self.formattedTrsr[data.locid] = {"ItemId": itemcode} + + elif data.yml in ["Get Bonus", "Double Get Bonus", "Second Get Bonus"]: + if data.yml == "Get Bonus": + self.dblbonus = 0 + # if double bonus then addresses dbl bonus so the next check gets 2 items on it + if data.yml == "Double Get Bonus": + self.dblbonus = itemcode + continue + if data.locid not in self.formattedBons.keys(): + self.formattedBons[data.locid] = {} + self.formattedBons[data.locid][data.charName] = { + "RewardId": data.locid, + "CharacterId": data.charNumber, + "HpIncrease": 0, + "MpIncrease": 0, + "DriveGaugeUpgrade": 0, + "ItemSlotUpgrade": 0, + "AccessorySlotUpgrade": 0, + "ArmorSlotUpgrade": 0, + "BonusItem1": itemcode, + "BonusItem2": self.dblbonus, + "Padding": 0 + + } + # putting dbl bonus at 0 again, so we don't have the same item placed multiple time + self.dblbonus = 0 + elif data.yml == "Keyblade": + self.formattedItem["Stats"].append({ + "Id": data.locid, + "Attack": self.multiworld.per_slot_randoms[self.player].randint(keyblademin, keyblademax), + "Magic": self.multiworld.per_slot_randoms[self.player].randint(keyblademin, keyblademax), + "Defense": 0, + "Ability": itemcode, + "AbilityPoints": 0, + "Unknown08": 100, + "FireResistance": 100, + "IceResistance": 100, + "LightningResistance": 100, + "DarkResistance": 100, + "Unknown0d": 100, + "GeneralResistance": 100, + "Unknown": 0 + }) + + elif data.yml == "Forms": + # loc id is form lvl + # char name is the form name number :) + if data.locid == 2: + formDict = {1: "Valor", 2: "Wisdom", 3: "Limit", 4: "Master", 5: "Final"} + formDictExp = { + 1: self.multiworld.Valor_Form_EXP[self.player].value, + 2: self.multiworld.Wisdom_Form_EXP[self.player].value, + 3: self.multiworld.Limit_Form_EXP[self.player].value, + 4: self.multiworld.Master_Form_EXP[self.player].value, + 5: self.multiworld.Final_Form_EXP[self.player].value} + formexp = formDictExp[data.charName] + formName = formDict[data.charName] + self.formattedFmlv[formName] = [] + self.formattedFmlv[formName].append({ + "Ability": 1, + "Experience": int(formExp[data.charName][data.locid] / formexp), + "FormId": data.charName, + "FormLevel": 1, + "GrowthAbilityLevel": 0, + }) + # row is form column is lvl + self.formattedFmlv[formName].append({ + "Ability": itemcode, + "Experience": int(formExp[data.charName][data.locid] / formexp), + "FormId": data.charName, + "FormLevel": data.locid, + "GrowthAbilityLevel": 0, + }) + + # Summons have no checks on them so done fully locally + self.formattedFmlv["Summon"] = [] + for x in range(1, 7): + self.formattedFmlv["Summon"].append({ + "Ability": 123, + "Experience": int(formExp[0][x] / self.multiworld.Summon_EXP[self.player].value), + "FormId": 0, + "FormLevel": x, + "GrowthAbilityLevel": 0, + }) + # levels done down here because of optional settings that can take locations out of the pool. + self.i = 1 + for location in SoraLevels: + increaseStat(self.multiworld.per_slot_randoms[self.player].randint(0, 3)) + if location in levelsetting: + data = self.multiworld.get_location(location, self.player) + if data.item.player == self.player: + itemcode = item_dictionary_table[data.item.name].kh2id + else: + itemcode = 461 + else: + increaseStat(self.multiworld.per_slot_randoms[self.player].randint(0, 3)) + itemcode = 0 + self.formattedLvup["Sora"][self.i] = { + "Exp": int(soraExp[self.i] / self.multiworld.Sora_Level_EXP[self.player].value), + "Strength": self.strength, + "Magic": self.magic, + "Defense": self.defense, + "Ap": self.ap, + "SwordAbility": itemcode, + "ShieldAbility": itemcode, + "StaffAbility": itemcode, + "Padding": 0, + "Character": "Sora", + "Level": self.i + } + self.i += 1 + # averaging stats for the struggle bats + for x in {122, 144, 145}: + self.formattedItem["Stats"].append({ + "Id": x, + "Attack": int((keyblademin + keyblademax) / 2), + "Magic": int((keyblademin + keyblademax) / 2), + "Defense": 0, + "Ability": 405, + "AbilityPoints": 0, + "Unknown08": 100, + "FireResistance": 100, + "IceResistance": 100, + "LightningResistance": 100, + "DarkResistance": 100, + "Unknown0d": 100, + "GeneralResistance": 100, + "Unknown": 0 + }) + mod_dir = os.path.join(output_directory, mod_name + "_" + Utils.__version__) + + openkhmod = { + "TrsrList.yml": yaml.dump(self.formattedTrsr, line_break="\n"), + "LvupList.yml": yaml.dump(self.formattedLvup, line_break="\n"), + "BonsList.yml": yaml.dump(self.formattedBons, line_break="\n"), + "ItemList.yml": yaml.dump(self.formattedItem, line_break="\n"), + "FmlvList.yml": yaml.dump(self.formattedFmlv, line_break="\n"), + } + + mod = KH2Container(openkhmod, mod_dir, output_directory, self.player, + self.multiworld.get_file_safe_player_name(self.player)) + mod.write() diff --git a/worlds/kh2/Options.py b/worlds/kh2/Options.py new file mode 100644 index 0000000000..03f60f28b6 --- /dev/null +++ b/worlds/kh2/Options.py @@ -0,0 +1,253 @@ +from Options import Choice, Option, Range, Toggle, OptionSet +import typing + + +class SoraEXP(Range): + """Sora Level Exp Multiplier""" + display_name = "Sora Level EXP" + range_start = 1 + range_end = 10 + default = 5 + + +class FinalEXP(Range): + """Final Form Exp Multiplier""" + display_name = "Final Form EXP" + range_start = 1 + range_end = 10 + default = 3 + + +class MasterEXP(Range): + """Master Form Exp Multiplier""" + display_name = "Master Form EXP" + range_start = 1 + range_end = 10 + default = 3 + + +class LimitEXP(Range): + """Limit Form Exp Multiplier""" + display_name = "Limit Form EXP" + range_start = 1 + range_end = 10 + default = 3 + + +class WisdomEXP(Range): + """Wisdom Form Exp Multiplier""" + display_name = "Wisdom Form EXP" + range_start = 1 + range_end = 10 + default = 3 + + +class ValorEXP(Range): + """Valor Form Exp Multiplier""" + display_name = "Valor Form EXP" + range_start = 1 + range_end = 10 + default = 3 + + +class SummonEXP(Range): + """Summon Exp Multiplier""" + display_name = "Summon level EXP" + range_start = 1 + range_end = 10 + default = 5 + + +class Schmovement(Choice): + """Level of Growth You Start With""" + display_name = "Schmovement" + option_level_0 = 0 + option_level_1 = 1 + option_level_2 = 2 + option_level_3 = 3 + option_level_4 = 4 + default = 1 + + +class RandomGrowth(Range): + """Amount of Random Growth Abilities You Start With""" + display_name = "Random Starting Growth" + range_start = 0 + range_end = 20 + default = 0 + + +class KeybladeMin(Range): + """Minimum Stats for the Keyblade""" + display_name = "Keyblade Minimum Stats" + range_start = 0 + range_end = 20 + default = 3 + + +class KeybladeMax(Range): + """Maximum Stats for the Keyblade""" + display_name = "Keyblade Max Stats" + range_start = 0 + range_end = 20 + default = 7 + + +class Visitlocking(Choice): + """Determines the level of visit locking + No Visit Locking:No visit locks(everything is sphere 1.Not Recommended)# BK is making a lot of money + Second Visit Locking:Start with 13 visit locking items for every first visit. + First and Second Visit Locking:One item for First Visit Two For Second Visit""" + display_name = "Visit locking" + option_no_visit_locking = 0 # starts with 27 visit locking + option_second_visit_locking = 1 # starts with 13 (no icecream/picture) + option_first_and_second_visit_locking = 2 # starts with nothing + default = 2 + + +class RandomVisitLockingItem(Range): + display_name = "Random Visit Locking Item" + range_start = 0 + range_end = 27 + default = 3 + + +class SuperBosses(Toggle): + """Terra, Sephiroth and Data Fights Toggle.""" + display_name = "Super Bosses" + default = False + + +class Cups(Choice): + """Olympus Cups Toggles + No Cups: No Cups. + Cups: Has every cup except Paradox. + Cups and Hades Paradox: Has Every Cup On.""" + display_name = "Olympus Cups" + option_no_cups = 0 + option_cups = 1 + option_cups_and_hades_paradox = 2 + default = 1 + + +class LevelDepth(Choice): + """Determines How many locations you want on levels + Level 50:23 checks spread through 50 levels + Level 99:23 checks spread through 99 levels + Level 50 sanity:check per level for 50 levels + Level 99 sanity:check per level for 99 levels + Level 1: no checks on levels(checks are replaced with stats)""" + display_name = "Level Depth" + option_level_50 = 0 + option_level_99 = 1 + option_level_50_sanity = 2 + option_level_99_sanity = 3 + option_level_1 = 4 + default = 0 + + +class PromiseCharm(Toggle): + """Add Promise Charm to the Pool""" + display_name = "Promise Charm" + default = False + + +class KeybladeAbilities(Choice): + """Action:Has Action Abilities on Keyblades + Support:Has Support Abilities on Keyblades""" + display_name = "Keyblade Abilities" + option_support = 0 + option_action = 1 + option_both = 2 + default = 2 + + +class BlacklistKeyblade(OptionSet): + """Black List these Abilities on Keyblades""" + display_name = "Blacklist Keyblade Abilities" + verify_item_name = True + + +class Goal(Choice): + """Win Condition + Three Proofs: Obtain the Three Proofs and Have a Gold Crown on Sora's Head. + Lucky Emblem Hunt: Acquire The Amount of Lucky Emblems you have set. + Hitlist: Kill the Superbosses on the "Hitlist" then synth ultima weapon.""" + display_name = "Goal" + option_three_proofs = 0 + option_lucky_emblem_hunt = 1 + option_hitlist = 2 + default = 0 + + +class FinalXemnas(Toggle): + """Kill Final Xemnas to Beat the Game. + This is in addition to your Goal. I.E. get three proofs+kill final Xemnas""" + display_name = "Final Xemnas" + default = True + + +class LuckyEmblemsRequired(Range): + """Number of Lucky Emblems to collect to Open The Final Door bosses. + If Goal is not Lucky Emblem Hunt this does nothing.""" + display_name = "Lucky Emblems Required" + range_start = 0 + range_end = 60 + default = 25 + + +class LuckyEmblemsAmount(Range): + """Number of Lucky Emblems that are in the pool. + If Goal is not Lucky Emblem Hunt this does nothing.""" + display_name = "Lucky Emblems Available" + range_start = 0 + range_end = 60 + default = 40 + + +class BountyRequired(Range): + """Number of Bounties that are Required. + If Goal is not Hitlist this does nothing.""" + display_name = "Bounties Required" + range_start = 0 + range_end = 24 + default = 7 + + +class BountyAmount(Range): + """Number of Bounties that are in the pool. + If Goal is not Hitlist this does nothing.""" + display_name = "Bounties Available" + range_start = 0 + range_end = 24 + default = 13 + + +KH2_Options: typing.Dict[str, type(Option)] = { + "Sora_Level_EXP": SoraEXP, + "Final_Form_EXP": FinalEXP, + "Master_Form_EXP": MasterEXP, + "Limit_Form_EXP": LimitEXP, + "Wisdom_Form_EXP": WisdomEXP, + "Valor_Form_EXP": ValorEXP, + "Summon_EXP": SummonEXP, + "Schmovement": Schmovement, + "Keyblade_Minimum": KeybladeMin, + "Keyblade_Maximum": KeybladeMax, + "Visitlocking": Visitlocking, + "RandomVisitLockingItem": RandomVisitLockingItem, + "SuperBosses": SuperBosses, + "LevelDepth": LevelDepth, + "Promise_Charm": PromiseCharm, + "KeybladeAbilities": KeybladeAbilities, + "BlacklistKeyblade": BlacklistKeyblade, + "RandomGrowth": RandomGrowth, + "Goal": Goal, + "FinalXemnas": FinalXemnas, + "LuckyEmblemsAmount": LuckyEmblemsAmount, + "LuckyEmblemsRequired": LuckyEmblemsRequired, + "BountyAmount": BountyAmount, + "BountyRequired": BountyRequired, + "Cups": Cups, + +} diff --git a/worlds/kh2/Regions.py b/worlds/kh2/Regions.py new file mode 100644 index 0000000000..36fc0c046b --- /dev/null +++ b/worlds/kh2/Regions.py @@ -0,0 +1,1242 @@ +import typing + +from BaseClasses import MultiWorld, Region, Entrance + +from .Locations import KH2Location, RegionTable +from .Names import LocationName, ItemName, RegionName + + +def create_regions(world, player: int, active_locations): + menu_region = create_region(world, player, active_locations, 'Menu', None) + + goa_region_locations = [ + LocationName.Crit_1, + LocationName.Crit_2, + LocationName.Crit_3, + LocationName.Crit_4, + LocationName.Crit_5, + LocationName.Crit_6, + LocationName.Crit_7, + LocationName.GardenofAssemblageMap, + LocationName.GoALostIllusion, + LocationName.ProofofNonexistence, + LocationName.DonaldStarting1, + LocationName.DonaldStarting2, + LocationName.GoofyStarting1, + LocationName.GoofyStarting2, + ] + + goa_region = create_region(world, player, active_locations, RegionName.GoA_Region, + goa_region_locations) + + lod_Region_locations = [ + LocationName.BambooGroveDarkShard, + LocationName.BambooGroveEther, + LocationName.BambooGroveMythrilShard, + LocationName.EncampmentAreaMap, + LocationName.Mission3, + LocationName.CheckpointHiPotion, + LocationName.CheckpointMythrilShard, + LocationName.MountainTrailLightningShard, + LocationName.MountainTrailRecoveryRecipe, + LocationName.MountainTrailEther, + LocationName.MountainTrailMythrilShard, + LocationName.VillageCaveAreaMap, + LocationName.VillageCaveAPBoost, + LocationName.VillageCaveDarkShard, + LocationName.VillageCaveBonus, + LocationName.RidgeFrostShard, + LocationName.RidgeAPBoost, + LocationName.ShanYu, + LocationName.ShanYuGetBonus, + LocationName.HiddenDragon, + LocationName.GoofyShanYu, + ] + lod_Region = create_region(world, player, active_locations, RegionName.LoD_Region, + lod_Region_locations) + lod2_Region_locations = [ + LocationName.ThroneRoomTornPages, + LocationName.ThroneRoomPalaceMap, + LocationName.ThroneRoomAPBoost, + LocationName.ThroneRoomQueenRecipe, + LocationName.ThroneRoomAPBoost2, + LocationName.ThroneRoomOgreShield, + LocationName.ThroneRoomMythrilCrystal, + LocationName.ThroneRoomOrichalcum, + LocationName.StormRider, + LocationName.XigbarDataDefenseBoost, + LocationName.GoofyStormRider, + ] + lod2_Region = create_region(world, player, active_locations, RegionName.LoD2_Region, + lod2_Region_locations) + ag_region_locations = [ + LocationName.AgrabahMap, + LocationName.AgrabahDarkShard, + LocationName.AgrabahMythrilShard, + LocationName.AgrabahHiPotion, + LocationName.AgrabahAPBoost, + LocationName.AgrabahMythrilStone, + LocationName.AgrabahMythrilShard2, + LocationName.AgrabahSerenityShard, + LocationName.BazaarMythrilGem, + LocationName.BazaarPowerShard, + LocationName.BazaarHiPotion, + LocationName.BazaarAPBoost, + LocationName.BazaarMythrilShard, + LocationName.PalaceWallsSkillRing, + LocationName.PalaceWallsMythrilStone, + LocationName.CaveEntrancePowerStone, + LocationName.CaveEntranceMythrilShard, + LocationName.ValleyofStoneMythrilStone, + LocationName.ValleyofStoneAPBoost, + LocationName.ValleyofStoneMythrilShard, + LocationName.ValleyofStoneHiPotion, + LocationName.AbuEscort, + LocationName.ChasmofChallengesCaveofWondersMap, + LocationName.ChasmofChallengesAPBoost, + LocationName.TreasureRoom, + LocationName.TreasureRoomAPBoost, + LocationName.TreasureRoomSerenityGem, + LocationName.ElementalLords, + LocationName.LampCharm, + LocationName.GoofyTreasureRoom, + LocationName.DonaldAbuEscort, + ] + ag_region = create_region(world, player, active_locations, RegionName.Ag_Region, + ag_region_locations) + ag2_region_locations = [ + LocationName.RuinedChamberTornPages, + LocationName.RuinedChamberRuinsMap, + LocationName.GenieJafar, + LocationName.WishingLamp, + ] + ag2_region = create_region(world, player, active_locations, RegionName.Ag2_Region, + ag2_region_locations) + lexaeus_region_locations = [ + LocationName.LexaeusBonus, + LocationName.LexaeusASStrengthBeyondStrength, + LocationName.LexaeusDataLostIllusion, + ] + lexaeus_region = create_region(world, player, active_locations, RegionName.Lexaeus_Region, + lexaeus_region_locations) + + dc_region_locations = [ + LocationName.DCCourtyardMythrilShard, + LocationName.DCCourtyardStarRecipe, + LocationName.DCCourtyardAPBoost, + LocationName.DCCourtyardMythrilStone, + LocationName.DCCourtyardBlazingStone, + LocationName.DCCourtyardBlazingShard, + LocationName.DCCourtyardMythrilShard2, + LocationName.LibraryTornPages, + LocationName.DisneyCastleMap, + LocationName.MinnieEscort, + LocationName.MinnieEscortGetBonus, + ] + dc_region = create_region(world, player, active_locations, RegionName.Dc_Region, + dc_region_locations) + tr_region_locations = [ + LocationName.CornerstoneHillMap, + LocationName.CornerstoneHillFrostShard, + LocationName.PierMythrilShard, + LocationName.PierHiPotion, + LocationName.WaterwayMythrilStone, + LocationName.WaterwayAPBoost, + LocationName.WaterwayFrostStone, + LocationName.WindowofTimeMap, + LocationName.BoatPete, + LocationName.FuturePete, + LocationName.FuturePeteGetBonus, + LocationName.Monochrome, + LocationName.WisdomForm, + LocationName.DonaldBoatPete, + LocationName.DonaldBoatPeteGetBonus, + LocationName.GoofyFuturePete, + ] + tr_region = create_region(world, player, active_locations, RegionName.Tr_Region, + tr_region_locations) + marluxia_region_locations = [ + LocationName.MarluxiaGetBonus, + LocationName.MarluxiaASEternalBlossom, + LocationName.MarluxiaDataLostIllusion, + ] + marluxia_region = create_region(world, player, active_locations, RegionName.Marluxia_Region, + marluxia_region_locations) + terra_region_locations = [ + LocationName.LingeringWillBonus, + LocationName.LingeringWillProofofConnection, + LocationName.LingeringWillManifestIllusion, + ] + terra_region = create_region(world, player, active_locations, RegionName.Terra_Region, + terra_region_locations) + + hundred_acre1_region_locations = [ + LocationName.PoohsHouse100AcreWoodMap, + LocationName.PoohsHouseAPBoost, + LocationName.PoohsHouseMythrilStone, + ] + hundred_acre1_region = create_region(world, player, active_locations, RegionName.HundredAcre1_Region, + hundred_acre1_region_locations) + hundred_acre2_region_locations = [ + LocationName.PigletsHouseDefenseBoost, + LocationName.PigletsHouseAPBoost, + LocationName.PigletsHouseMythrilGem, + ] + hundred_acre2_region = create_region(world, player, active_locations, RegionName.HundredAcre2_Region, + hundred_acre2_region_locations) + hundred_acre3_region_locations = [ + LocationName.RabbitsHouseDrawRing, + LocationName.RabbitsHouseMythrilCrystal, + LocationName.RabbitsHouseAPBoost, + ] + hundred_acre3_region = create_region(world, player, active_locations, RegionName.HundredAcre3_Region, + hundred_acre3_region_locations) + hundred_acre4_region_locations = [ + LocationName.KangasHouseMagicBoost, + LocationName.KangasHouseAPBoost, + LocationName.KangasHouseOrichalcum, + ] + hundred_acre4_region = create_region(world, player, active_locations, RegionName.HundredAcre4_Region, + hundred_acre4_region_locations) + hundred_acre5_region_locations = [ + LocationName.SpookyCaveMythrilGem, + LocationName.SpookyCaveAPBoost, + LocationName.SpookyCaveOrichalcum, + LocationName.SpookyCaveGuardRecipe, + LocationName.SpookyCaveMythrilCrystal, + LocationName.SpookyCaveAPBoost2, + LocationName.SweetMemories, + LocationName.SpookyCaveMap, + ] + hundred_acre5_region = create_region(world, player, active_locations, RegionName.HundredAcre5_Region, + hundred_acre5_region_locations) + hundred_acre6_region_locations = [ + LocationName.StarryHillCosmicRing, + LocationName.StarryHillStyleRecipe, + LocationName.StarryHillCureElement, + LocationName.StarryHillOrichalcumPlus, + ] + hundred_acre6_region = create_region(world, player, active_locations, RegionName.HundredAcre6_Region, + hundred_acre6_region_locations) + pr_region_locations = [ + LocationName.RampartNavalMap, + LocationName.RampartMythrilStone, + LocationName.RampartDarkShard, + LocationName.TownDarkStone, + LocationName.TownAPBoost, + LocationName.TownMythrilShard, + LocationName.TownMythrilGem, + LocationName.CaveMouthBrightShard, + LocationName.CaveMouthMythrilShard, + LocationName.IsladeMuertaMap, + LocationName.BoatFight, + LocationName.InterceptorBarrels, + LocationName.PowderStoreAPBoost1, + LocationName.PowderStoreAPBoost2, + LocationName.MoonlightNookMythrilShard, + LocationName.MoonlightNookSerenityGem, + LocationName.MoonlightNookPowerStone, + LocationName.Barbossa, + LocationName.BarbossaGetBonus, + LocationName.FollowtheWind, + LocationName.DonaldBoatFight, + LocationName.GoofyBarbossa, + LocationName.GoofyBarbossaGetBonus, + LocationName.GoofyInterceptorBarrels, + ] + pr_region = create_region(world, player, active_locations, RegionName.Pr_Region, + pr_region_locations) + pr2_region_locations = [ + LocationName.GrimReaper1, + LocationName.InterceptorsHoldFeatherCharm, + LocationName.SeadriftKeepAPBoost, + LocationName.SeadriftKeepOrichalcum, + LocationName.SeadriftKeepMeteorStaff, + LocationName.SeadriftRowSerenityGem, + LocationName.SeadriftRowKingRecipe, + LocationName.SeadriftRowMythrilCrystal, + LocationName.SeadriftRowCursedMedallion, + LocationName.SeadriftRowShipGraveyardMap, + LocationName.GoofyGrimReaper1, + + ] + pr2_region = create_region(world, player, active_locations, RegionName.Pr2_Region, + pr2_region_locations) + gr2_region_locations = [ + LocationName.DonaladGrimReaper2, + LocationName.GrimReaper2, + LocationName.SecretAnsemReport6, + LocationName.LuxordDataAPBoost, + ] + gr2_region = create_region(world, player, active_locations, RegionName.Gr2_Region, + gr2_region_locations) + oc_region_locations = [ + LocationName.PassageMythrilShard, + LocationName.PassageMythrilStone, + LocationName.PassageEther, + LocationName.PassageAPBoost, + LocationName.PassageHiPotion, + LocationName.InnerChamberUnderworldMap, + LocationName.InnerChamberMythrilShard, + LocationName.Cerberus, + LocationName.ColiseumMap, + LocationName.Urns, + LocationName.UnderworldEntrancePowerBoost, + LocationName.CavernsEntranceLucidShard, + LocationName.CavernsEntranceAPBoost, + LocationName.CavernsEntranceMythrilShard, + LocationName.TheLostRoadBrightShard, + LocationName.TheLostRoadEther, + LocationName.TheLostRoadMythrilShard, + LocationName.TheLostRoadMythrilStone, + LocationName.AtriumLucidStone, + LocationName.AtriumAPBoost, + LocationName.DemyxOC, + LocationName.SecretAnsemReport5, + LocationName.OlympusStone, + LocationName.TheLockCavernsMap, + LocationName.TheLockMythrilShard, + LocationName.TheLockAPBoost, + LocationName.PeteOC, + LocationName.Hydra, + LocationName.HydraGetBonus, + LocationName.HerosCrest, + LocationName.DonaldDemyxOC, + LocationName.GoofyPeteOC, + ] + oc_region = create_region(world, player, active_locations, RegionName.Oc_Region, + oc_region_locations) + oc2_region_locations = [ + LocationName.AuronsStatue, + LocationName.Hades, + LocationName.HadesGetBonus, + LocationName.GuardianSoul, + + ] + oc2_region = create_region(world, player, active_locations, RegionName.Oc2_Region, + oc2_region_locations) + oc2_pain_and_panic_locations = [ + LocationName.ProtectBeltPainandPanicCup, + LocationName.SerenityGemPainandPanicCup, + ] + oc2_titan_locations = [ + LocationName.GenjiShieldTitanCup, + LocationName.SkillfulRingTitanCup, + ] + oc2_cerberus_locations = [ + LocationName.RisingDragonCerberusCup, + LocationName.SerenityCrystalCerberusCup, + ] + oc2_gof_cup_locations = [ + LocationName.FatalCrestGoddessofFateCup, + LocationName.OrichalcumPlusGoddessofFateCup, + LocationName.HadesCupTrophyParadoxCups, + ] + zexion_region_locations = [ + LocationName.ZexionBonus, + LocationName.ZexionASBookofShadows, + LocationName.ZexionDataLostIllusion, + LocationName.GoofyZexion, + ] + oc2_pain_and_panic_cup = create_region(world, player, active_locations, RegionName.Oc2_pain_and_panic_Region, + oc2_pain_and_panic_locations) + oc2_titan_cup = create_region(world, player, active_locations, RegionName.Oc2_titan_Region, oc2_titan_locations) + oc2_cerberus_cup = create_region(world, player, active_locations, RegionName.Oc2_cerberus_Region, + oc2_cerberus_locations) + oc2_gof_cup = create_region(world, player, active_locations, RegionName.Oc2_gof_Region, oc2_gof_cup_locations) + zexion_region = create_region(world, player, active_locations, RegionName.Zexion_Region, zexion_region_locations) + + bc_region_locations = [ + LocationName.BCCourtyardAPBoost, + LocationName.BCCourtyardHiPotion, + LocationName.BCCourtyardMythrilShard, + LocationName.BellesRoomCastleMap, + LocationName.BellesRoomMegaRecipe, + LocationName.TheEastWingMythrilShard, + LocationName.TheEastWingTent, + LocationName.TheWestHallHiPotion, + LocationName.TheWestHallPowerShard, + LocationName.TheWestHallMythrilShard2, + LocationName.TheWestHallBrightStone, + LocationName.TheWestHallMythrilShard, + LocationName.Thresholder, + LocationName.DungeonBasementMap, + LocationName.DungeonAPBoost, + LocationName.SecretPassageMythrilShard, + LocationName.SecretPassageHiPotion, + LocationName.SecretPassageLucidShard, + LocationName.TheWestHallAPBoostPostDungeon, + LocationName.TheWestWingMythrilShard, + LocationName.TheWestWingTent, + LocationName.Beast, + LocationName.TheBeastsRoomBlazingShard, + LocationName.DarkThorn, + LocationName.DarkThornGetBonus, + LocationName.DarkThornCureElement, + LocationName.DonaldThresholder, + LocationName.GoofyBeast, + ] + bc_region = create_region(world, player, active_locations, RegionName.Bc_Region, + bc_region_locations) + bc2_region_locations = [ + LocationName.RumblingRose, + LocationName.CastleWallsMap, + + ] + bc2_region = create_region(world, player, active_locations, RegionName.Bc2_Region, + bc2_region_locations) + xaldin_region_locations = [ + LocationName.Xaldin, + LocationName.XaldinGetBonus, + LocationName.DonaldXaldinGetBonus, + LocationName.SecretAnsemReport4, + LocationName.XaldinDataDefenseBoost, + ] + xaldin_region = create_region(world, player, active_locations, RegionName.Xaldin_Region, + xaldin_region_locations) + sp_region_locations = [ + LocationName.PitCellAreaMap, + LocationName.PitCellMythrilCrystal, + LocationName.CanyonDarkCrystal, + LocationName.CanyonMythrilStone, + LocationName.CanyonMythrilGem, + LocationName.CanyonFrostCrystal, + LocationName.Screens, + LocationName.HallwayPowerCrystal, + LocationName.HallwayAPBoost, + LocationName.CommunicationsRoomIOTowerMap, + LocationName.CommunicationsRoomGaiaBelt, + LocationName.HostileProgram, + LocationName.HostileProgramGetBonus, + LocationName.PhotonDebugger, + LocationName.DonaldScreens, + LocationName.GoofyHostileProgram, + + ] + sp_region = create_region(world, player, active_locations, RegionName.Sp_Region, + sp_region_locations) + sp2_region_locations = [ + LocationName.SolarSailer, + LocationName.CentralComputerCoreAPBoost, + LocationName.CentralComputerCoreOrichalcumPlus, + LocationName.CentralComputerCoreCosmicArts, + LocationName.CentralComputerCoreMap, + + LocationName.DonaldSolarSailer, + ] + + sp2_region = create_region(world, player, active_locations, RegionName.Sp2_Region, + sp2_region_locations) + mcp_region_locations = [ + LocationName.MCP, + LocationName.MCPGetBonus, + ] + mcp_region = create_region(world, player, active_locations, RegionName.Mcp_Region, + mcp_region_locations) + larxene_region_locations = [ + LocationName.LarxeneBonus, + LocationName.LarxeneASCloakedThunder, + LocationName.LarxeneDataLostIllusion, + ] + larxene_region = create_region(world, player, active_locations, RegionName.Larxene_Region, + larxene_region_locations) + ht_region_locations = [ + LocationName.GraveyardMythrilShard, + LocationName.GraveyardSerenityGem, + LocationName.FinklesteinsLabHalloweenTownMap, + LocationName.TownSquareMythrilStone, + LocationName.TownSquareEnergyShard, + LocationName.HinterlandsLightningShard, + LocationName.HinterlandsMythrilStone, + LocationName.HinterlandsAPBoost, + LocationName.CandyCaneLaneMegaPotion, + LocationName.CandyCaneLaneMythrilGem, + LocationName.CandyCaneLaneLightningStone, + LocationName.CandyCaneLaneMythrilStone, + LocationName.SantasHouseChristmasTownMap, + LocationName.SantasHouseAPBoost, + LocationName.PrisonKeeper, + LocationName.OogieBoogie, + LocationName.OogieBoogieMagnetElement, + LocationName.DonaldPrisonKeeper, + LocationName.GoofyOogieBoogie, + ] + ht_region = create_region(world, player, active_locations, RegionName.Ht_Region, + ht_region_locations) + ht2_region_locations = [ + LocationName.Lock, + LocationName.Present, + LocationName.DecoyPresents, + LocationName.Experiment, + LocationName.DecisivePumpkin, + + LocationName.DonaldExperiment, + LocationName.GoofyLock, + ] + ht2_region = create_region(world, player, active_locations, RegionName.Ht2_Region, + ht2_region_locations) + vexen_region_locations = [ + LocationName.VexenBonus, + LocationName.VexenASRoadtoDiscovery, + LocationName.VexenDataLostIllusion, + ] + vexen_region = create_region(world, player, active_locations, RegionName.Vexen_Region, + vexen_region_locations) + hb_region_locations = [ + LocationName.MarketplaceMap, + LocationName.BoroughDriveRecovery, + LocationName.BoroughAPBoost, + LocationName.BoroughHiPotion, + LocationName.BoroughMythrilShard, + LocationName.BoroughDarkShard, + LocationName.MerlinsHouseMembershipCard, + LocationName.MerlinsHouseBlizzardElement, + LocationName.Bailey, + LocationName.BaileySecretAnsemReport7, + LocationName.BaseballCharm, + ] + hb_region = create_region(world, player, active_locations, RegionName.Hb_Region, + hb_region_locations) + hb2_region_locations = [ + LocationName.PosternCastlePerimeterMap, + LocationName.PosternMythrilGem, + LocationName.PosternAPBoost, + LocationName.CorridorsMythrilStone, + LocationName.CorridorsMythrilCrystal, + LocationName.CorridorsDarkCrystal, + LocationName.CorridorsAPBoost, + LocationName.AnsemsStudyMasterForm, + LocationName.AnsemsStudySleepingLion, + LocationName.AnsemsStudySkillRecipe, + LocationName.AnsemsStudyUkuleleCharm, + LocationName.RestorationSiteMoonRecipe, + LocationName.RestorationSiteAPBoost, + LocationName.CoRDepthsAPBoost, + LocationName.CoRDepthsPowerCrystal, + LocationName.CoRDepthsFrostCrystal, + LocationName.CoRDepthsManifestIllusion, + LocationName.CoRDepthsAPBoost2, + LocationName.CoRMineshaftLowerLevelDepthsofRemembranceMap, + LocationName.CoRMineshaftLowerLevelAPBoost, + LocationName.DonaldDemyxHBGetBonus, + ] + hb2_region = create_region(world, player, active_locations, RegionName.Hb2_Region, + hb2_region_locations) + onek_region_locations = [ + LocationName.DemyxHB, + LocationName.DemyxHBGetBonus, + LocationName.FFFightsCureElement, + LocationName.CrystalFissureTornPages, + LocationName.CrystalFissureTheGreatMawMap, + LocationName.CrystalFissureEnergyCrystal, + LocationName.CrystalFissureAPBoost, + LocationName.ThousandHeartless, + LocationName.ThousandHeartlessSecretAnsemReport1, + LocationName.ThousandHeartlessIceCream, + LocationName.ThousandHeartlessPicture, + LocationName.PosternGullWing, + LocationName.HeartlessManufactoryCosmicChain, + LocationName.DemyxDataAPBoost, + ] + onek_region = create_region(world, player, active_locations, RegionName.ThousandHeartless_Region, + onek_region_locations) + mushroom_region_locations = [ + LocationName.WinnersProof, + LocationName.ProofofPeace, + ] + mushroom_region = create_region(world, player, active_locations, RegionName.Mushroom13_Region, + mushroom_region_locations) + sephi_region_locations = [ + LocationName.SephirothBonus, + LocationName.SephirothFenrir, + ] + sephi_region = create_region(world, player, active_locations, RegionName.Sephi_Region, + sephi_region_locations) + + cor_region_locations = [ + LocationName.CoRDepthsUpperLevelRemembranceGem, + LocationName.CoRMiningAreaSerenityGem, + LocationName.CoRMiningAreaAPBoost, + LocationName.CoRMiningAreaSerenityCrystal, + LocationName.CoRMiningAreaManifestIllusion, + LocationName.CoRMiningAreaSerenityGem2, + LocationName.CoRMiningAreaDarkRemembranceMap, + LocationName.CoRMineshaftMidLevelPowerBoost, + LocationName.CoREngineChamberSerenityCrystal, + LocationName.CoREngineChamberRemembranceCrystal, + LocationName.CoREngineChamberAPBoost, + LocationName.CoREngineChamberManifestIllusion, + LocationName.CoRMineshaftUpperLevelMagicBoost, + ] + cor_region = create_region(world, player, active_locations, RegionName.CoR_Region, + cor_region_locations) + transport_region_locations = [ + LocationName.CoRMineshaftUpperLevelAPBoost, + LocationName.TransporttoRemembrance, + ] + transport_region = create_region(world, player, active_locations, RegionName.Transport_Region, + transport_region_locations) + pl_region_locations = [ + LocationName.GorgeSavannahMap, + LocationName.GorgeDarkGem, + LocationName.GorgeMythrilStone, + LocationName.ElephantGraveyardFrostGem, + LocationName.ElephantGraveyardMythrilStone, + LocationName.ElephantGraveyardBrightStone, + LocationName.ElephantGraveyardAPBoost, + LocationName.ElephantGraveyardMythrilShard, + LocationName.PrideRockMap, + LocationName.PrideRockMythrilStone, + LocationName.PrideRockSerenityCrystal, + LocationName.WildebeestValleyEnergyStone, + LocationName.WildebeestValleyAPBoost, + LocationName.WildebeestValleyMythrilGem, + LocationName.WildebeestValleyMythrilStone, + LocationName.WildebeestValleyLucidGem, + LocationName.WastelandsMythrilShard, + LocationName.WastelandsSerenityGem, + LocationName.WastelandsMythrilStone, + LocationName.JungleSerenityGem, + LocationName.JungleMythrilStone, + LocationName.JungleSerenityCrystal, + LocationName.OasisMap, + LocationName.OasisTornPages, + LocationName.OasisAPBoost, + LocationName.CircleofLife, + LocationName.Hyenas1, + LocationName.Scar, + LocationName.ScarFireElement, + LocationName.DonaldScar, + LocationName.GoofyHyenas1, + + ] + pl_region = create_region(world, player, active_locations, RegionName.Pl_Region, + pl_region_locations) + pl2_region_locations = [ + LocationName.Hyenas2, + LocationName.Groundshaker, + LocationName.GroundshakerGetBonus, + LocationName.SaixDataDefenseBoost, + LocationName.GoofyHyenas2, + ] + pl2_region = create_region(world, player, active_locations, RegionName.Pl2_Region, + pl2_region_locations) + + stt_region_locations = [ + LocationName.TwilightTownMap, + LocationName.MunnyPouchOlette, + LocationName.StationDusks, + LocationName.StationofSerenityPotion, + LocationName.StationofCallingPotion, + LocationName.TwilightThorn, + LocationName.Axel1, + LocationName.JunkChampionBelt, + LocationName.JunkMedal, + LocationName.TheStruggleTrophy, + LocationName.CentralStationPotion1, + LocationName.STTCentralStationHiPotion, + LocationName.CentralStationPotion2, + LocationName.SunsetTerraceAbilityRing, + LocationName.SunsetTerraceHiPotion, + LocationName.SunsetTerracePotion1, + LocationName.SunsetTerracePotion2, + LocationName.MansionFoyerHiPotion, + LocationName.MansionFoyerPotion1, + LocationName.MansionFoyerPotion2, + LocationName.MansionDiningRoomElvenBandanna, + LocationName.MansionDiningRoomPotion, + LocationName.NaminesSketches, + LocationName.MansionMap, + LocationName.MansionLibraryHiPotion, + LocationName.Axel2, + LocationName.MansionBasementCorridorHiPotion, + LocationName.RoxasDataMagicBoost, + ] + stt_region = create_region(world, player, active_locations, RegionName.STT_Region, + stt_region_locations) + + tt_region_locations = [ + LocationName.OldMansionPotion, + LocationName.OldMansionMythrilShard, + LocationName.TheWoodsPotion, + LocationName.TheWoodsMythrilShard, + LocationName.TheWoodsHiPotion, + LocationName.TramCommonHiPotion, + LocationName.TramCommonAPBoost, + LocationName.TramCommonTent, + LocationName.TramCommonMythrilShard1, + LocationName.TramCommonPotion1, + LocationName.TramCommonMythrilShard2, + LocationName.TramCommonPotion2, + LocationName.StationPlazaSecretAnsemReport2, + LocationName.MunnyPouchMickey, + LocationName.CrystalOrb, + LocationName.CentralStationTent, + LocationName.TTCentralStationHiPotion, + LocationName.CentralStationMythrilShard, + LocationName.TheTowerPotion, + LocationName.TheTowerHiPotion, + LocationName.TheTowerEther, + LocationName.TowerEntrywayEther, + LocationName.TowerEntrywayMythrilShard, + LocationName.SorcerersLoftTowerMap, + LocationName.TowerWardrobeMythrilStone, + LocationName.StarSeeker, + LocationName.ValorForm, + ] + tt_region = create_region(world, player, active_locations, RegionName.TT_Region, + tt_region_locations) + tt2_region_locations = [ + LocationName.SeifersTrophy, + LocationName.Oathkeeper, + LocationName.LimitForm, + ] + tt2_region = create_region(world, player, active_locations, RegionName.TT2_Region, + tt2_region_locations) + tt3_region_locations = [ + LocationName.UndergroundConcourseMythrilGem, + LocationName.UndergroundConcourseAPBoost, + LocationName.UndergroundConcourseMythrilCrystal, + LocationName.UndergroundConcourseOrichalcum, + LocationName.TunnelwayOrichalcum, + LocationName.TunnelwayMythrilCrystal, + LocationName.SunsetTerraceOrichalcumPlus, + LocationName.SunsetTerraceMythrilShard, + LocationName.SunsetTerraceMythrilCrystal, + LocationName.SunsetTerraceAPBoost, + LocationName.MansionNobodies, + LocationName.MansionFoyerMythrilCrystal, + LocationName.MansionFoyerMythrilStone, + LocationName.MansionFoyerSerenityCrystal, + LocationName.MansionDiningRoomMythrilCrystal, + LocationName.MansionDiningRoomMythrilStone, + LocationName.MansionLibraryOrichalcum, + LocationName.BeamSecretAnsemReport10, + LocationName.MansionBasementCorridorUltimateRecipe, + LocationName.BetwixtandBetween, + LocationName.BetwixtandBetweenBondofFlame, + LocationName.AxelDataMagicBoost, + LocationName.DonaldMansionNobodies, + ] + tt3_region = create_region(world, player, active_locations, RegionName.TT3_Region, + tt3_region_locations) + + twtnw_region_locations = [ + LocationName.FragmentCrossingMythrilStone, + LocationName.FragmentCrossingMythrilCrystal, + LocationName.FragmentCrossingAPBoost, + LocationName.FragmentCrossingOrichalcum, + ] + + twtnw_region = create_region(world, player, active_locations, RegionName.Twtnw_Region, + twtnw_region_locations) + twtnw_postroxas_region_locations = [ + LocationName.Roxas, + LocationName.RoxasGetBonus, + LocationName.RoxasSecretAnsemReport8, + LocationName.TwoBecomeOne, + LocationName.MemorysSkyscaperMythrilCrystal, + LocationName.MemorysSkyscaperAPBoost, + LocationName.MemorysSkyscaperMythrilStone, + LocationName.TheBrinkofDespairDarkCityMap, + LocationName.TheBrinkofDespairOrichalcumPlus, + LocationName.NothingsCallMythrilGem, + LocationName.NothingsCallOrichalcum, + LocationName.TwilightsViewCosmicBelt, + + ] + twtnw_postroxas_region = create_region(world, player, active_locations, RegionName.Twtnw_PostRoxas, + twtnw_postroxas_region_locations) + twtnw_postxigbar_region_locations = [ + LocationName.XigbarBonus, + LocationName.XigbarSecretAnsemReport3, + LocationName.NaughtsSkywayMythrilGem, + LocationName.NaughtsSkywayOrichalcum, + LocationName.NaughtsSkywayMythrilCrystal, + LocationName.Oblivion, + LocationName.CastleThatNeverWasMap, + LocationName.Luxord, + LocationName.LuxordGetBonus, + LocationName.LuxordSecretAnsemReport9, + ] + twtnw_postxigbar_region = create_region(world, player, active_locations, RegionName.Twtnw_PostXigbar, + twtnw_postxigbar_region_locations) + twtnw2_region_locations = [ + LocationName.SaixBonus, + LocationName.SaixSecretAnsemReport12, + LocationName.PreXemnas1SecretAnsemReport11, + LocationName.RuinandCreationsPassageMythrilStone, + LocationName.RuinandCreationsPassageAPBoost, + LocationName.RuinandCreationsPassageMythrilCrystal, + LocationName.RuinandCreationsPassageOrichalcum, + LocationName.Xemnas1, + LocationName.Xemnas1GetBonus, + LocationName.Xemnas1SecretAnsemReport13, + LocationName.FinalXemnas, + LocationName.XemnasDataPowerBoost, + ] + twtnw2_region = create_region(world, player, active_locations, RegionName.Twtnw2_Region, + twtnw2_region_locations) + + valor_region_locations = [ + LocationName.Valorlvl2, + LocationName.Valorlvl3, + LocationName.Valorlvl4, + LocationName.Valorlvl5, + LocationName.Valorlvl6, + LocationName.Valorlvl7, + ] + valor_region = create_region(world, player, active_locations, RegionName.Valor_Region, + valor_region_locations) + wisdom_region_locations = [ + LocationName.Wisdomlvl2, + LocationName.Wisdomlvl3, + LocationName.Wisdomlvl4, + LocationName.Wisdomlvl5, + LocationName.Wisdomlvl6, + LocationName.Wisdomlvl7, + ] + wisdom_region = create_region(world, player, active_locations, RegionName.Wisdom_Region, + wisdom_region_locations) + limit_region_locations = [ + LocationName.Limitlvl2, + LocationName.Limitlvl3, + LocationName.Limitlvl4, + LocationName.Limitlvl5, + LocationName.Limitlvl6, + LocationName.Limitlvl7, + ] + limit_region = create_region(world, player, active_locations, RegionName.Limit_Region, + limit_region_locations) + master_region_locations = [ + LocationName.Masterlvl2, + LocationName.Masterlvl3, + LocationName.Masterlvl4, + LocationName.Masterlvl5, + LocationName.Masterlvl6, + LocationName.Masterlvl7, + ] + master_region = create_region(world, player, active_locations, RegionName.Master_Region, + master_region_locations) + final_region_locations = [ + LocationName.Finallvl2, + LocationName.Finallvl3, + LocationName.Finallvl4, + LocationName.Finallvl5, + LocationName.Finallvl6, + LocationName.Finallvl7, + ] + final_region = create_region(world, player, active_locations, RegionName.Final_Region, + final_region_locations) + keyblade_region_locations = [ + LocationName.FAKESlot, + LocationName.DetectionSaberSlot, + LocationName.EdgeofUltimaSlot, + LocationName.KingdomKeySlot, + LocationName.OathkeeperSlot, + LocationName.OblivionSlot, + LocationName.StarSeekerSlot, + LocationName.HiddenDragonSlot, + LocationName.HerosCrestSlot, + LocationName.MonochromeSlot, + LocationName.FollowtheWindSlot, + LocationName.CircleofLifeSlot, + LocationName.PhotonDebuggerSlot, + LocationName.GullWingSlot, + LocationName.RumblingRoseSlot, + LocationName.GuardianSoulSlot, + LocationName.WishingLampSlot, + LocationName.DecisivePumpkinSlot, + LocationName.SweetMemoriesSlot, + LocationName.MysteriousAbyssSlot, + LocationName.SleepingLionSlot, + LocationName.BondofFlameSlot, + LocationName.TwoBecomeOneSlot, + LocationName.FatalCrestSlot, + LocationName.FenrirSlot, + LocationName.UltimaWeaponSlot, + LocationName.WinnersProofSlot, + LocationName.PurebloodSlot, + LocationName.Centurion2, + LocationName.CometStaff, + LocationName.HammerStaff, + LocationName.LordsBroom, + LocationName.MagesStaff, + LocationName.MeteorStaff, + LocationName.NobodyLance, + LocationName.PreciousMushroom, + LocationName.PreciousMushroom2, + LocationName.PremiumMushroom, + LocationName.RisingDragon, + LocationName.SaveTheQueen2, + LocationName.ShamansRelic, + LocationName.VictoryBell, + LocationName.WisdomWand, + + LocationName.AdamantShield, + LocationName.AkashicRecord, + LocationName.ChainGear, + LocationName.DreamCloud, + LocationName.FallingStar, + LocationName.FrozenPride2, + LocationName.GenjiShield, + LocationName.KnightDefender, + LocationName.KnightsShield, + LocationName.MajesticMushroom, + LocationName.MajesticMushroom2, + LocationName.NobodyGuard, + LocationName.OgreShield, + LocationName.SaveTheKing2, + LocationName.UltimateMushroom, + ] + keyblade_region = create_region(world, player, active_locations, RegionName.Keyblade_Region, + keyblade_region_locations) + + world.regions += [ + lod_Region, + lod2_Region, + ag_region, + ag2_region, + lexaeus_region, + dc_region, + tr_region, + terra_region, + marluxia_region, + hundred_acre1_region, + hundred_acre2_region, + hundred_acre3_region, + hundred_acre4_region, + hundred_acre5_region, + hundred_acre6_region, + pr_region, + pr2_region, + gr2_region, + oc_region, + oc2_region, + oc2_pain_and_panic_cup, + oc2_titan_cup, + oc2_cerberus_cup, + oc2_gof_cup, + zexion_region, + bc_region, + bc2_region, + xaldin_region, + sp_region, + sp2_region, + mcp_region, + larxene_region, + ht_region, + ht2_region, + vexen_region, + hb_region, + hb2_region, + onek_region, + mushroom_region, + sephi_region, + cor_region, + transport_region, + pl_region, + pl2_region, + stt_region, + tt_region, + tt2_region, + tt3_region, + twtnw_region, + twtnw_postroxas_region, + twtnw_postxigbar_region, + twtnw2_region, + goa_region, + menu_region, + valor_region, + wisdom_region, + limit_region, + master_region, + final_region, + keyblade_region, + ] + # Level region depends on level depth. + # for every 5 levels there should be +3 visit locking + levelVL1 = [] + levelVL3 = [] + levelVL6 = [] + levelVL9 = [] + levelVL12 = [] + levelVL15 = [] + levelVL18 = [] + levelVL21 = [] + levelVL24 = [] + levelVL26 = [] + # level 50 + if world.LevelDepth[player] == "level_50": + levelVL1 = [LocationName.Lvl2, LocationName.Lvl4, LocationName.Lvl7, LocationName.Lvl9, LocationName.Lvl10] + levelVL3 = [LocationName.Lvl12, LocationName.Lvl14, LocationName.Lvl15, LocationName.Lvl17, + LocationName.Lvl20, ] + levelVL6 = [LocationName.Lvl23, LocationName.Lvl25, LocationName.Lvl28, LocationName.Lvl30] + levelVL9 = [LocationName.Lvl32, LocationName.Lvl34, LocationName.Lvl36, LocationName.Lvl39, LocationName.Lvl41] + levelVL12 = [LocationName.Lvl44, LocationName.Lvl46, LocationName.Lvl48] + levelVL15 = [LocationName.Lvl50] + # level 99 + elif world.LevelDepth[player] == "level_99": + levelVL1 = [LocationName.Lvl7, LocationName.Lvl9, ] + levelVL3 = [LocationName.Lvl12, LocationName.Lvl15, LocationName.Lvl17, LocationName.Lvl20] + levelVL6 = [LocationName.Lvl23, LocationName.Lvl25, LocationName.Lvl28] + levelVL9 = [LocationName.Lvl31, LocationName.Lvl33, LocationName.Lvl36, LocationName.Lvl39] + levelVL12 = [LocationName.Lvl41, LocationName.Lvl44, LocationName.Lvl47, LocationName.Lvl49] + levelVL15 = [LocationName.Lvl53, LocationName.Lvl59] + levelVL18 = [LocationName.Lvl65] + levelVL21 = [LocationName.Lvl73] + levelVL24 = [LocationName.Lvl85] + levelVL26 = [LocationName.Lvl99] + # level sanity + # has to be [] instead of {} for in + elif world.LevelDepth[player] in ["level_50_sanity", "level_99_sanity"]: + levelVL1 = [LocationName.Lvl2, LocationName.Lvl3, LocationName.Lvl4, LocationName.Lvl5, LocationName.Lvl6, + LocationName.Lvl7, LocationName.Lvl8, LocationName.Lvl9, LocationName.Lvl10] + levelVL3 = [LocationName.Lvl11, LocationName.Lvl12, LocationName.Lvl13, LocationName.Lvl14, LocationName.Lvl15, + LocationName.Lvl16, LocationName.Lvl17, LocationName.Lvl18, LocationName.Lvl19, LocationName.Lvl20] + levelVL6 = [LocationName.Lvl21, LocationName.Lvl22, LocationName.Lvl23, LocationName.Lvl24, LocationName.Lvl25, + LocationName.Lvl26, LocationName.Lvl27, LocationName.Lvl28, LocationName.Lvl29, LocationName.Lvl30] + levelVL9 = [LocationName.Lvl31, LocationName.Lvl32, LocationName.Lvl33, LocationName.Lvl34, LocationName.Lvl35, + LocationName.Lvl36, LocationName.Lvl37, LocationName.Lvl38, LocationName.Lvl39, LocationName.Lvl40] + levelVL12 = [LocationName.Lvl41, LocationName.Lvl42, LocationName.Lvl43, LocationName.Lvl44, LocationName.Lvl45, + LocationName.Lvl46, LocationName.Lvl47, LocationName.Lvl48, LocationName.Lvl49, LocationName.Lvl50] + # level 99 sanity + if world.LevelDepth[player] == "level_99_sanity": + levelVL15 = [LocationName.Lvl51, LocationName.Lvl52, LocationName.Lvl53, LocationName.Lvl54, + LocationName.Lvl55, LocationName.Lvl56, LocationName.Lvl57, LocationName.Lvl58, + LocationName.Lvl59, LocationName.Lvl60] + levelVL18 = [LocationName.Lvl61, LocationName.Lvl62, LocationName.Lvl63, LocationName.Lvl64, + LocationName.Lvl65, LocationName.Lvl66, LocationName.Lvl67, LocationName.Lvl68, + LocationName.Lvl69, LocationName.Lvl70] + levelVL21 = [LocationName.Lvl71, LocationName.Lvl72, LocationName.Lvl73, LocationName.Lvl74, + LocationName.Lvl75, LocationName.Lvl76, LocationName.Lvl77, LocationName.Lvl78, + LocationName.Lvl79, LocationName.Lvl80] + levelVL24 = [LocationName.Lvl81, LocationName.Lvl82, LocationName.Lvl83, LocationName.Lvl84, + LocationName.Lvl85, LocationName.Lvl86, LocationName.Lvl87, LocationName.Lvl88, + LocationName.Lvl89, LocationName.Lvl90] + levelVL26 = [LocationName.Lvl91, LocationName.Lvl92, LocationName.Lvl93, LocationName.Lvl94, + LocationName.Lvl95, LocationName.Lvl96, LocationName.Lvl97, LocationName.Lvl98, + LocationName.Lvl99] + + level_regionVL1 = create_region(world, player, active_locations, RegionName.LevelsVS1, + levelVL1) + level_regionVL3 = create_region(world, player, active_locations, RegionName.LevelsVS3, + levelVL3) + level_regionVL6 = create_region(world, player, active_locations, RegionName.LevelsVS6, + levelVL6) + level_regionVL9 = create_region(world, player, active_locations, RegionName.LevelsVS9, + levelVL9) + level_regionVL12 = create_region(world, player, active_locations, RegionName.LevelsVS12, + levelVL12) + level_regionVL15 = create_region(world, player, active_locations, RegionName.LevelsVS15, + levelVL15) + level_regionVL18 = create_region(world, player, active_locations, RegionName.LevelsVS18, + levelVL18) + level_regionVL21 = create_region(world, player, active_locations, RegionName.LevelsVS21, + levelVL21) + level_regionVL24 = create_region(world, player, active_locations, RegionName.LevelsVS24, + levelVL24) + level_regionVL26 = create_region(world, player, active_locations, RegionName.LevelsVS26, + levelVL26) + world.regions += [level_regionVL1, level_regionVL3, level_regionVL6, level_regionVL9, level_regionVL12, + level_regionVL15, level_regionVL18, level_regionVL21, level_regionVL24, level_regionVL26] + + +def connect_regions(world: MultiWorld, player: int): + # connecting every first visit to the GoA + + names: typing.Dict[str, int] = {} + + connect(world, player, names, "Menu", RegionName.Keyblade_Region) + connect(world, player, names, "Menu", RegionName.GoA_Region) + + connect(world, player, names, RegionName.GoA_Region, RegionName.LoD_Region, + lambda state: state.kh_lod_unlocked(player, 1)) + connect(world, player, names, RegionName.LoD_Region, RegionName.LoD2_Region, + lambda state: state.kh_lod_unlocked(player, 2)) + + connect(world, player, names, RegionName.GoA_Region, RegionName.Oc_Region, + lambda state: state.kh_oc_unlocked(player, 1)) + connect(world, player, names, RegionName.Oc_Region, RegionName.Oc2_Region, + lambda state: state.kh_oc_unlocked(player, 2)) + connect(world, player, names, RegionName.Oc2_Region, RegionName.Zexion_Region, + lambda state: state.kh_datazexion(player)) + + connect(world, player, names, RegionName.Oc2_Region, RegionName.Oc2_pain_and_panic_Region, + lambda state: state.kh_painandpanic(player)) + connect(world, player, names, RegionName.Oc2_Region, RegionName.Oc2_cerberus_Region, + lambda state: state.kh_cerberuscup(player)) + connect(world, player, names, RegionName.Oc2_Region, RegionName.Oc2_titan_Region, + lambda state: state.kh_titan(player)) + connect(world, player, names, RegionName.Oc2_Region, RegionName.Oc2_gof_Region, + lambda state: state.kh_gof(player)) + + connect(world, player, names, RegionName.GoA_Region, RegionName.Ag_Region, + lambda state: state.kh_ag_unlocked(player, 1)) + connect(world, player, names, RegionName.Ag_Region, RegionName.Ag2_Region, + lambda state: state.kh_ag_unlocked(player, 2) + and (state.has(ItemName.FireElement, player) + and state.has(ItemName.BlizzardElement, player) + and state.has(ItemName.ThunderElement, player))) + connect(world, player, names, RegionName.Ag2_Region, RegionName.Lexaeus_Region, + lambda state: state.kh_datalexaeus(player)) + + connect(world, player, names, RegionName.GoA_Region, RegionName.Dc_Region, + lambda state: state.kh_dc_unlocked(player, 1)) + connect(world, player, names, RegionName.Dc_Region, RegionName.Tr_Region, + lambda state: state.kh_dc_unlocked(player, 2)) + connect(world, player, names, RegionName.Tr_Region, RegionName.Marluxia_Region, + lambda state: state.kh_datamarluxia(player)) + connect(world, player, names, RegionName.Tr_Region, RegionName.Terra_Region, lambda state: state.kh_terra(player)) + + connect(world, player, names, RegionName.GoA_Region, RegionName.Pr_Region, + lambda state: state.kh_pr_unlocked(player, 1)) + connect(world, player, names, RegionName.Pr_Region, RegionName.Pr2_Region, + lambda state: state.kh_pr_unlocked(player, 2)) + connect(world, player, names, RegionName.Pr2_Region, RegionName.Gr2_Region, + lambda state: state.kh_gr2(player)) + + connect(world, player, names, RegionName.GoA_Region, RegionName.Bc_Region, + lambda state: state.kh_bc_unlocked(player, 1)) + connect(world, player, names, RegionName.Bc_Region, RegionName.Bc2_Region, + lambda state: state.kh_bc_unlocked(player, 2)) + connect(world, player, names, RegionName.Bc2_Region, RegionName.Xaldin_Region, + lambda state: state.kh_xaldin(player)) + + connect(world, player, names, RegionName.GoA_Region, RegionName.Sp_Region, + lambda state: state.kh_sp_unlocked(player, 1)) + connect(world, player, names, RegionName.Sp_Region, RegionName.Sp2_Region, + lambda state: state.kh_sp_unlocked(player, 2)) + connect(world, player, names, RegionName.Sp2_Region, RegionName.Mcp_Region, + lambda state: state.kh_mcp(player)) + connect(world, player, names, RegionName.Mcp_Region, RegionName.Larxene_Region, + lambda state: state.kh_datalarxene(player)) + + connect(world, player, names, RegionName.GoA_Region, RegionName.Ht_Region, + lambda state: state.kh_ht_unlocked(player, 1)) + connect(world, player, names, RegionName.Ht_Region, RegionName.Ht2_Region, + lambda state: state.kh_ht_unlocked(player, 2)) + connect(world, player, names, RegionName.Ht2_Region, RegionName.Vexen_Region, + lambda state: state.kh_datavexen(player)) + + connect(world, player, names, RegionName.GoA_Region, RegionName.Hb_Region, + lambda state: state.kh_hb_unlocked(player, 1)) + connect(world, player, names, RegionName.Hb_Region, RegionName.Hb2_Region, + lambda state: state.kh_hb_unlocked(player, 2)) + connect(world, player, names, RegionName.Hb2_Region, RegionName.ThousandHeartless_Region, + lambda state: state.kh_onek(player)) + connect(world, player, names, RegionName.ThousandHeartless_Region, RegionName.Mushroom13_Region, + lambda state: state.has(ItemName.ProofofPeace, player)) + connect(world, player, names, RegionName.ThousandHeartless_Region, RegionName.Sephi_Region, + lambda state: state.kh_sephi(player)) + + connect(world, player, names, RegionName.Hb2_Region, RegionName.CoR_Region, lambda state: state.kh_cor(player)) + connect(world, player, names, RegionName.CoR_Region, RegionName.Transport_Region, lambda state: + state.has(ItemName.HighJump, player, 3) + and state.has(ItemName.AerialDodge, player, 3) + and state.has(ItemName.Glide, player, 3)) + + connect(world, player, names, RegionName.GoA_Region, RegionName.Pl_Region, + lambda state: state.kh_pl_unlocked(player, 1)) + connect(world, player, names, RegionName.Pl_Region, RegionName.Pl2_Region, + lambda state: state.kh_pl_unlocked(player, 2) and ( + state.has(ItemName.BerserkCharge, player) or state.kh_reflect(player))) + + connect(world, player, names, RegionName.GoA_Region, RegionName.STT_Region, + lambda state: state.kh_stt_unlocked(player, 1)) + + connect(world, player, names, RegionName.GoA_Region, RegionName.TT_Region, + lambda state: state.kh_tt_unlocked(player, 1)) + connect(world, player, names, RegionName.TT_Region, RegionName.TT2_Region, + lambda state: state.kh_tt_unlocked(player, 2)) + connect(world, player, names, RegionName.TT2_Region, RegionName.TT3_Region, + lambda state: state.kh_tt_unlocked(player, 3)) + + connect(world, player, names, RegionName.GoA_Region, RegionName.Twtnw_Region, + lambda state: state.kh_twtnw_unlocked(player, 0)) + connect(world, player, names, RegionName.Twtnw_Region, RegionName.Twtnw_PostRoxas, + lambda state: state.kh_roxastools(player)) + connect(world, player, names, RegionName.Twtnw_PostRoxas, RegionName.Twtnw_PostXigbar, + lambda state: state.kh_basetools(player) and (state.kh_donaldlimit(player) or ( + state.has(ItemName.FinalForm, player) and state.has(ItemName.FireElement, player)))) + connect(world, player, names, RegionName.Twtnw_PostRoxas, RegionName.Twtnw2_Region, + lambda state: state.kh_twtnw_unlocked(player, 1)) + + hundredacrevisits = {RegionName.HundredAcre1_Region: 0, RegionName.HundredAcre2_Region: 1, + RegionName.HundredAcre3_Region: 2, + RegionName.HundredAcre4_Region: 3, RegionName.HundredAcre5_Region: 4, + RegionName.HundredAcre6_Region: 5} + for visit, tornpage in hundredacrevisits.items(): + connect(world, player, names, RegionName.GoA_Region, visit, + lambda state: (state.has(ItemName.TornPages, player, tornpage))) + + connect(world, player, names, RegionName.GoA_Region, RegionName.LevelsVS1, + lambda state: state.kh_visit_locking_amount(player, 1)) + connect(world, player, names, RegionName.LevelsVS1, RegionName.LevelsVS3, + lambda state: state.kh_visit_locking_amount(player, 3)) + connect(world, player, names, RegionName.LevelsVS3, RegionName.LevelsVS6, + lambda state: state.kh_visit_locking_amount(player, 6)) + connect(world, player, names, RegionName.LevelsVS6, RegionName.LevelsVS9, + lambda state: state.kh_visit_locking_amount(player, 9)) + connect(world, player, names, RegionName.LevelsVS9, RegionName.LevelsVS12, + lambda state: state.kh_visit_locking_amount(player, 12)) + connect(world, player, names, RegionName.LevelsVS12, RegionName.LevelsVS15, + lambda state: state.kh_visit_locking_amount(player, 15)) + connect(world, player, names, RegionName.LevelsVS15, RegionName.LevelsVS18, + lambda state: state.kh_visit_locking_amount(player, 18)) + connect(world, player, names, RegionName.LevelsVS18, RegionName.LevelsVS21, + lambda state: state.kh_visit_locking_amount(player, 21)) + connect(world, player, names, RegionName.LevelsVS21, RegionName.LevelsVS24, + lambda state: state.kh_visit_locking_amount(player, 24)) + connect(world, player, names, RegionName.LevelsVS24, RegionName.LevelsVS26, + lambda state: state.kh_visit_locking_amount(player, 25)) # 25 because of goa twtnw bugs with visit locking. + + for region in RegionTable["ValorRegion"]: + connect(world, player, names, region, RegionName.Valor_Region, + lambda state: state.has(ItemName.ValorForm, player)) + for region in RegionTable["WisdomRegion"]: + connect(world, player, names, region, RegionName.Wisdom_Region, + lambda state: state.has(ItemName.WisdomForm, player)) + for region in RegionTable["LimitRegion"]: + connect(world, player, names, region, RegionName.Limit_Region, + lambda state: state.has(ItemName.LimitForm, player)) + for region in RegionTable["MasterRegion"]: + connect(world, player, names, region, RegionName.Master_Region, + lambda state: state.has(ItemName.MasterForm, player) and state.has(ItemName.DriveConverter, player)) + for region in RegionTable["FinalRegion"]: + connect(world, player, names, region, RegionName.Final_Region, + lambda state: state.has(ItemName.FinalForm, player)) + + +# shamelessly stolen from the sa2b +def connect(world: MultiWorld, player: int, used_names: typing.Dict[str, int], source: str, target: str, + rule: typing.Optional[typing.Callable] = None): + source_region = world.get_region(source, player) + target_region = world.get_region(target, player) + + if target not in used_names: + used_names[target] = 1 + name = target + else: + used_names[target] += 1 + name = target + (' ' * used_names[target]) + + connection = Entrance(player, name, source_region) + + if rule: + connection.access_rule = rule + + source_region.exits.append(connection) + connection.connect(target_region) + + +def create_region(world: MultiWorld, player: int, active_locations, name: str, locations=None): + ret = Region(name, player, world) + if locations: + for location in locations: + loc_id = active_locations.get(location, 0) + if loc_id: + location = KH2Location(player, location, loc_id.code, ret) + ret.locations.append(location) + + return ret diff --git a/worlds/kh2/Rules.py b/worlds/kh2/Rules.py new file mode 100644 index 0000000000..b86ae4a2db --- /dev/null +++ b/worlds/kh2/Rules.py @@ -0,0 +1,96 @@ + +from BaseClasses import MultiWorld + +from .Items import exclusionItem_table +from .Locations import STT_Checks, exclusion_table +from .Names import LocationName, ItemName +from ..generic.Rules import add_rule, forbid_items, set_rule + + +def set_rules(world: MultiWorld, player: int): + + add_rule(world.get_location(LocationName.RoxasDataMagicBoost, player), + lambda state: state.kh_dataroxas(player)) + add_rule(world.get_location(LocationName.DemyxDataAPBoost, player), + lambda state: state.kh_datademyx(player)) + add_rule(world.get_location(LocationName.SaixDataDefenseBoost, player), + lambda state: state.kh_datasaix(player)) + add_rule(world.get_location(LocationName.XaldinDataDefenseBoost, player), + lambda state: state.kh_dataxaldin(player)) + add_rule(world.get_location(LocationName.XemnasDataPowerBoost, player), + lambda state: state.kh_dataxemnas(player)) + add_rule(world.get_location(LocationName.XigbarDataDefenseBoost, player), + lambda state: state.kh_dataxigbar(player)) + add_rule(world.get_location(LocationName.VexenDataLostIllusion, player), + lambda state: state.kh_dataaxel(player)) + add_rule(world.get_location(LocationName.LuxordDataAPBoost, player), + lambda state: state.kh_dataluxord(player)) + + for slot, weapon in exclusion_table["WeaponSlots"].items(): + add_rule(world.get_location(slot, player), lambda state: state.has(weapon, player)) + formLogicTable = { + ItemName.ValorForm: [LocationName.Valorlvl4, + LocationName.Valorlvl5, + LocationName.Valorlvl6, + LocationName.Valorlvl7], + ItemName.WisdomForm: [LocationName.Wisdomlvl4, + LocationName.Wisdomlvl5, + LocationName.Wisdomlvl6, + LocationName.Wisdomlvl7], + ItemName.LimitForm: [LocationName.Limitlvl4, + LocationName.Limitlvl5, + LocationName.Limitlvl6, + LocationName.Limitlvl7], + ItemName.MasterForm: [LocationName.Masterlvl4, + LocationName.Masterlvl5, + LocationName.Masterlvl6, + LocationName.Masterlvl7], + ItemName.FinalForm: [LocationName.Finallvl4, + LocationName.Finallvl5, + LocationName.Finallvl6, + LocationName.Finallvl7] + } + + for form in formLogicTable: + for i in range(4): + location = world.get_location(formLogicTable[form][i], player) + set_rule(location, lambda state, i=i + 1, form=form: state.kh_amount_of_forms(player, i, form)) + + if world.Goal[player] == "three_proofs": + add_rule(world.get_location(LocationName.FinalXemnas, player), + lambda state: state.kh_three_proof_unlocked(player)) + if world.FinalXemnas[player]: + world.completion_condition[player] = lambda state: state.kh_victory(player) + else: + world.completion_condition[player] = lambda state: state.kh_three_proof_unlocked(player) + # lucky emblem hunt + elif world.Goal[player] == "lucky_emblem_hunt": + add_rule(world.get_location(LocationName.FinalXemnas, player), + lambda state: state.kh_lucky_emblem_unlocked(player, world.LuckyEmblemsRequired[player].value)) + if world.FinalXemnas[player]: + world.completion_condition[player] = lambda state: state.kh_victory(player) + else: + world.completion_condition[player] = lambda state: state.kh_lucky_emblem_unlocked(player, world.LuckyEmblemsRequired[player].value) + # hitlist if == 2 + else: + add_rule(world.get_location(LocationName.FinalXemnas, player), + lambda state: state.kh_hitlist(player, world.BountyRequired[player].value)) + if world.FinalXemnas[player]: + world.completion_condition[player] = lambda state: state.kh_victory(player) + else: + world.completion_condition[player] = lambda state: state.kh_hitlist(player, world.BountyRequired[player].value) + + # Forbid Abilities on popups due to game limitations + for location in exclusion_table["Popups"]: + forbid_items(world.get_location(location, player), exclusionItem_table["Ability"]) + forbid_items(world.get_location(location, player), exclusionItem_table["StatUps"]) + + for location in STT_Checks: + forbid_items(world.get_location(location, player), exclusionItem_table["StatUps"]) + + # Santa's house also breaks with stat ups + for location in {LocationName.SantasHouseChristmasTownMap, LocationName.SantasHouseAPBoost}: + forbid_items(world.get_location(location, player), exclusionItem_table["StatUps"]) + + add_rule(world.get_location(LocationName.TransporttoRemembrance, player), + lambda state: state.kh_transport(player)) diff --git a/worlds/kh2/WorldLocations.py b/worlds/kh2/WorldLocations.py new file mode 100644 index 0000000000..172874c2b7 --- /dev/null +++ b/worlds/kh2/WorldLocations.py @@ -0,0 +1,845 @@ +import typing +from .Names import LocationName + + +class WorldLocationData(typing.NamedTuple): + # save+ + addrObtained: int + # bitmask + bitIndex: int + + +LoD_Checks = { + LocationName.BambooGroveDarkShard: WorldLocationData(0x23AC, 1), + LocationName.BambooGroveEther: WorldLocationData(0x23D9, 7), + LocationName.BambooGroveMythrilShard: WorldLocationData(0x23DA, 0), + LocationName.EncampmentAreaMap: WorldLocationData(0x1D94, 6), + LocationName.Mission3: WorldLocationData(0x1D96, 0), + LocationName.CheckpointHiPotion: WorldLocationData(0x23AD, 1), + LocationName.CheckpointMythrilShard: WorldLocationData(0x23AD, 2), + LocationName.MountainTrailLightningShard: WorldLocationData(0x23AD, 3), + LocationName.MountainTrailRecoveryRecipe: WorldLocationData(0x23AD, 4), + LocationName.MountainTrailEther: WorldLocationData(0x23AD, 5), + LocationName.MountainTrailMythrilShard: WorldLocationData(0x23AD, 6), + LocationName.VillageCaveAreaMap: WorldLocationData(0x1D96, 6), + LocationName.VillageCaveDarkShard: WorldLocationData(0x23AE, 0), + LocationName.VillageCaveAPBoost: WorldLocationData(0x23AD, 7), + LocationName.VillageCaveBonus: WorldLocationData(0x3709, 3), + LocationName.RidgeFrostShard: WorldLocationData(0x23AE, 1), + LocationName.RidgeAPBoost: WorldLocationData(0x23AE, 2), + LocationName.ShanYu: WorldLocationData(0x3705, 1), + LocationName.ShanYuGetBonus: WorldLocationData(0x3705, 1), + LocationName.GoofyShanYu: WorldLocationData(0x3705, 1), + LocationName.HiddenDragon: WorldLocationData(0x1D92, 2), + LocationName.ThroneRoomTornPages: WorldLocationData(0x23AE, 3), + LocationName.ThroneRoomPalaceMap: WorldLocationData(0x23AE, 4), + LocationName.ThroneRoomAPBoost: WorldLocationData(0x23AE, 5), + LocationName.ThroneRoomQueenRecipe: WorldLocationData(0x23AE, 6), + LocationName.ThroneRoomAPBoost2: WorldLocationData(0x23AE, 7), + LocationName.ThroneRoomOgreShield: WorldLocationData(0x23AF, 0), + LocationName.ThroneRoomMythrilCrystal: WorldLocationData(0x23AF, 1), + LocationName.ThroneRoomOrichalcum: WorldLocationData(0x23AF, 2), + LocationName.StormRider: WorldLocationData(0x3705, 2), + LocationName.GoofyStormRider: WorldLocationData(0x3705, 2), + +} +AG_Checks = { + LocationName.AgrabahMap: WorldLocationData(0x1D74, 4), + LocationName.AgrabahDarkShard: WorldLocationData(0x23AF, 3), + LocationName.AgrabahMythrilShard: WorldLocationData(0x23AF, 4), + LocationName.AgrabahHiPotion: WorldLocationData(0x23AF, 5), + LocationName.AgrabahAPBoost: WorldLocationData(0x23AF, 6), + LocationName.AgrabahMythrilStone: WorldLocationData(0x23AF, 7), + LocationName.AgrabahMythrilShard2: WorldLocationData(0x23B0, 0), + LocationName.AgrabahSerenityShard: WorldLocationData(0x23DA, 3), + LocationName.BazaarMythrilGem: WorldLocationData(0x23B0, 1), + LocationName.BazaarPowerShard: WorldLocationData(0x23B0, 2), + LocationName.BazaarHiPotion: WorldLocationData(0x23B0, 3), + LocationName.BazaarAPBoost: WorldLocationData(0x23B0, 4), + LocationName.BazaarMythrilShard: WorldLocationData(0x23B0, 5), + LocationName.PalaceWallsSkillRing: WorldLocationData(0x23B0, 6), + LocationName.PalaceWallsMythrilStone: WorldLocationData(0x23DB, 5), + LocationName.CaveEntrancePowerStone: WorldLocationData(0x23B0, 7), + LocationName.CaveEntranceMythrilShard: WorldLocationData(0x23B1, 0), + LocationName.ValleyofStoneMythrilStone: WorldLocationData(0x23B1, 2), + LocationName.ValleyofStoneAPBoost: WorldLocationData(0x23B1, 3), + LocationName.ValleyofStoneMythrilShard: WorldLocationData(0x23B1, 4), + LocationName.ValleyofStoneHiPotion: WorldLocationData(0x23B1, 5), + LocationName.AbuEscort: WorldLocationData(0x3709, 2), + LocationName.DonaldAbuEscort: WorldLocationData(0x3709, 2), + LocationName.ChasmofChallengesCaveofWondersMap: WorldLocationData(0x23D8, 7), + LocationName.ChasmofChallengesAPBoost: WorldLocationData(0x23B1, 6), + LocationName.TreasureRoom: WorldLocationData(0x3709, 6), + LocationName.GoofyTreasureRoom: WorldLocationData(0x3709, 6), + LocationName.TreasureRoomAPBoost: WorldLocationData(0x23DA, 4), + LocationName.TreasureRoomSerenityGem: WorldLocationData(0x23DA, 5), + LocationName.ElementalLords: WorldLocationData(0x3708, 5), + LocationName.LampCharm: WorldLocationData(0x1D72, 4), + LocationName.RuinedChamberTornPages: WorldLocationData(0x23B1, 1), + LocationName.RuinedChamberRuinsMap: WorldLocationData(0x23D8, 6), + LocationName.GenieJafar: WorldLocationData(0x3705, 7), + LocationName.WishingLamp: WorldLocationData(0x1D77, 3), + +} +DC_Checks = { + LocationName.DCCourtyardMythrilShard: WorldLocationData(0x23B4, 1), + LocationName.DCCourtyardStarRecipe: WorldLocationData(0x23B4, 2), + LocationName.DCCourtyardAPBoost: WorldLocationData(0x23B4, 3), + LocationName.DCCourtyardMythrilStone: WorldLocationData(0x23B4, 4), + LocationName.DCCourtyardBlazingStone: WorldLocationData(0x23B4, 5), + LocationName.DCCourtyardBlazingShard: WorldLocationData(0x23B4, 6), + LocationName.DCCourtyardMythrilShard2: WorldLocationData(0x23B4, 7), + LocationName.LibraryTornPages: WorldLocationData(0x23B4, 0), + LocationName.DisneyCastleMap: WorldLocationData(0x1E10, 4), + LocationName.MinnieEscort: WorldLocationData(0x3708, 6), + LocationName.MinnieEscortGetBonus: WorldLocationData(0x3708, 6), + LocationName.LingeringWillBonus: WorldLocationData(0x370C, 6), + LocationName.LingeringWillProofofConnection: WorldLocationData(0x370C, 6), + LocationName.LingeringWillManifestIllusion: WorldLocationData(0x370C, 6), +} +TR_Checks = { + LocationName.CornerstoneHillMap: WorldLocationData(0x23B2, 0), + LocationName.CornerstoneHillFrostShard: WorldLocationData(0x23B2, 1), + LocationName.PierMythrilShard: WorldLocationData(0x23B2, 3), + LocationName.PierHiPotion: WorldLocationData(0x23B2, 4), + LocationName.WaterwayMythrilStone: WorldLocationData(0x23B2, 5), + LocationName.WaterwayAPBoost: WorldLocationData(0x23B2, 6), + LocationName.WaterwayFrostStone: WorldLocationData(0x23B2, 7), + LocationName.WindowofTimeMap: WorldLocationData(0x1E32, 4), + LocationName.BoatPete: WorldLocationData(0x3706, 0), + LocationName.DonaldBoatPete: WorldLocationData(0x3706, 0), + LocationName.DonaldBoatPeteGetBonus: WorldLocationData(0x3706, 0), + LocationName.FuturePete: WorldLocationData(0x3706, 1), + LocationName.FuturePeteGetBonus: WorldLocationData(0x3706, 1), + LocationName.GoofyFuturePete: WorldLocationData(0x3706, 1), + LocationName.Monochrome: WorldLocationData(0x1E33, 2), + LocationName.WisdomForm: WorldLocationData(0x1E33, 2), +} + +HundredAcreChecks = { + LocationName.PoohsHouse100AcreWoodMap: WorldLocationData(0x23C9, 7), + LocationName.PoohsHouseAPBoost: WorldLocationData(0x23B5, 4), + LocationName.PoohsHouseMythrilStone: WorldLocationData(0x23B5, 5), + LocationName.PigletsHouseDefenseBoost: WorldLocationData(0x23B6, 4), + LocationName.PigletsHouseAPBoost: WorldLocationData(0x23B6, 2), + LocationName.PigletsHouseMythrilGem: WorldLocationData(0x23B6, 3), + LocationName.RabbitsHouseDrawRing: WorldLocationData(0x23CA, 0), + LocationName.RabbitsHouseMythrilCrystal: WorldLocationData(0x23B5, 7), + LocationName.RabbitsHouseAPBoost: WorldLocationData(0x23B6, 0), + LocationName.KangasHouseMagicBoost: WorldLocationData(0x23B6, 7), + LocationName.KangasHouseAPBoost: WorldLocationData(0x23B6, 5), + LocationName.KangasHouseOrichalcum: WorldLocationData(0x23B6, 6), + LocationName.SpookyCaveMythrilGem: WorldLocationData(0x23B7, 1), + LocationName.SpookyCaveAPBoost: WorldLocationData(0x23B7, 2), + LocationName.SpookyCaveOrichalcum: WorldLocationData(0x23B7, 3), + LocationName.SpookyCaveGuardRecipe: WorldLocationData(0x23B7, 4), + LocationName.SpookyCaveMythrilCrystal: WorldLocationData(0x23B7, 6), + LocationName.SpookyCaveAPBoost2: WorldLocationData(0x23B7, 7), + LocationName.SweetMemories: WorldLocationData(0x1DB4, 6), + LocationName.SpookyCaveMap: WorldLocationData(0x1DB4, 6), + LocationName.StarryHillCosmicRing: WorldLocationData(0x23C9, 6), + LocationName.StarryHillStyleRecipe: WorldLocationData(0x23B5, 1), + LocationName.StarryHillCureElement: WorldLocationData(0x1DB5, 5), + LocationName.StarryHillOrichalcumPlus: WorldLocationData(0x1DB5, 5), +} +Oc_Checks = { + LocationName.PassageMythrilShard: WorldLocationData(0x23B9, 6), + LocationName.PassageMythrilStone: WorldLocationData(0x23B9, 7), + LocationName.PassageEther: WorldLocationData(0x23BA, 0), + LocationName.PassageAPBoost: WorldLocationData(0x23BA, 1), + LocationName.PassageHiPotion: WorldLocationData(0x23BA, 2), + LocationName.InnerChamberUnderworldMap: WorldLocationData(0x23B8, 4), + LocationName.InnerChamberMythrilShard: WorldLocationData(0x23B8, 3), + LocationName.Cerberus: WorldLocationData(0x3704, 5), + LocationName.ColiseumMap: WorldLocationData(0x1D5A, 4), + LocationName.Urns: WorldLocationData(0x370B, 1), + LocationName.UnderworldEntrancePowerBoost: WorldLocationData(0x23B8, 0), + LocationName.CavernsEntranceLucidShard: WorldLocationData(0x23B8, 5), + LocationName.CavernsEntranceAPBoost: WorldLocationData(0x23B8, 6), + LocationName.CavernsEntranceMythrilShard: WorldLocationData(0x23DA, 6), + LocationName.TheLostRoadBrightShard: WorldLocationData(0x23BA, 3), + LocationName.TheLostRoadEther: WorldLocationData(0x23BA, 4), + LocationName.TheLostRoadMythrilShard: WorldLocationData(0x23BA, 5), + LocationName.TheLostRoadMythrilStone: WorldLocationData(0x23BA, 6), + LocationName.AtriumLucidStone: WorldLocationData(0x23BA, 7), + LocationName.AtriumAPBoost: WorldLocationData(0x23BB, 0), + LocationName.DemyxOC: WorldLocationData(0x370B, 2), + LocationName.DonaldDemyxOC: WorldLocationData(0x370B, 2), + LocationName.SecretAnsemReport5: WorldLocationData(0x1D5B, 3), + LocationName.OlympusStone: WorldLocationData(0x1D5B, 3), + LocationName.TheLockCavernsMap: WorldLocationData(0x23B9, 4), + LocationName.TheLockMythrilShard: WorldLocationData(0x23B9, 0), + LocationName.TheLockAPBoost: WorldLocationData(0x23B9, 2), + LocationName.PeteOC: WorldLocationData(0x3704, 6), + LocationName.GoofyPeteOC: WorldLocationData(0x3704, 6), + LocationName.Hydra: WorldLocationData(0x3704, 7), + LocationName.HydraGetBonus: WorldLocationData(0x3704, 7), + LocationName.HerosCrest: WorldLocationData(0x1D55, 7), + LocationName.AuronsStatue: WorldLocationData(0x1D5F, 2), + LocationName.Hades: WorldLocationData(0x3705, 0), + LocationName.HadesGetBonus: WorldLocationData(0x3705, 0), + LocationName.GuardianSoul: WorldLocationData(0x1D56, 5), + LocationName.ProtectBeltPainandPanicCup: WorldLocationData(0x1D57, 6), + LocationName.SerenityGemPainandPanicCup: WorldLocationData(0x1D57, 6), + LocationName.RisingDragonCerberusCup: WorldLocationData(0x1D58, 0), + LocationName.SerenityCrystalCerberusCup: WorldLocationData(0x1D58, 0), + LocationName.GenjiShieldTitanCup: WorldLocationData(0x1D58, 1), + LocationName.SkillfulRingTitanCup: WorldLocationData(0x1D58, 1), + LocationName.FatalCrestGoddessofFateCup: WorldLocationData(0x1D58, 4), + LocationName.OrichalcumPlusGoddessofFateCup: WorldLocationData(0x1D58, 4), + LocationName.HadesCupTrophyParadoxCups: WorldLocationData(0x1D5A, 1), +} + +BC_Checks = { + LocationName.BCCourtyardAPBoost: WorldLocationData(0x23BB, 5), + LocationName.BCCourtyardHiPotion: WorldLocationData(0x23BB, 6), + LocationName.BCCourtyardMythrilShard: WorldLocationData(0x23DA, 7), + LocationName.BellesRoomCastleMap: WorldLocationData(0x23BB, 2), + LocationName.BellesRoomMegaRecipe: WorldLocationData(0x23BB, 3), + LocationName.TheEastWingMythrilShard: WorldLocationData(0x23BB, 7), + LocationName.TheEastWingTent: WorldLocationData(0x23BC, 0), + LocationName.TheWestHallHiPotion: WorldLocationData(0x23BC, 1), + LocationName.TheWestHallPowerShard: WorldLocationData(0x23BC, 3), + LocationName.TheWestHallAPBoostPostDungeon: WorldLocationData(0x23BC, 5), + LocationName.TheWestHallBrightStone: WorldLocationData(0x23DB, 0), + LocationName.TheWestHallMythrilShard: WorldLocationData(0x23BC, 2), + LocationName.TheWestHallMythrilShard2: WorldLocationData(0x23BC, 4), + LocationName.Thresholder: WorldLocationData(0x3704, 2), + LocationName.DonaldThresholder: WorldLocationData(0x3704, 2), + LocationName.DungeonBasementMap: WorldLocationData(0x23BD, 0), + LocationName.DungeonAPBoost: WorldLocationData(0x23BD, 1), + LocationName.SecretPassageMythrilShard: WorldLocationData(0x23BD, 2), + LocationName.SecretPassageHiPotion: WorldLocationData(0x23BD, 5), + LocationName.SecretPassageLucidShard: WorldLocationData(0x23BD, 3), + LocationName.TheWestWingMythrilShard: WorldLocationData(0x23BC, 6), + LocationName.TheWestWingTent: WorldLocationData(0x23BC, 7), + LocationName.Beast: WorldLocationData(0x3705, 4), + LocationName.GoofyBeast: WorldLocationData(0x3705, 4), + LocationName.TheBeastsRoomBlazingShard: WorldLocationData(0x23BB, 4), + LocationName.DarkThorn: WorldLocationData(0x3704, 3), + LocationName.DarkThornGetBonus: WorldLocationData(0x3704, 3), + LocationName.DarkThornCureElement: WorldLocationData(0x1D32, 5), + LocationName.RumblingRose: WorldLocationData(0x1D39, 0), + LocationName.CastleWallsMap: WorldLocationData(0x1D39, 0), + LocationName.Xaldin: WorldLocationData(0x3704, 4), + LocationName.XaldinGetBonus: WorldLocationData(0x3704, 4), + LocationName.DonaldXaldinGetBonus: WorldLocationData(0x3704, 4), + LocationName.SecretAnsemReport4: WorldLocationData(0x1D31, 2), + LocationName.XaldinDataDefenseBoost: WorldLocationData(0x1D34, 7), +} +SP_Checks = { + LocationName.PitCellAreaMap: WorldLocationData(0x23CA, 2), + LocationName.PitCellMythrilCrystal: WorldLocationData(0x23BD, 6), + LocationName.CanyonDarkCrystal: WorldLocationData(0x23BE, 1), + LocationName.CanyonMythrilStone: WorldLocationData(0x23BE, 2), + LocationName.CanyonMythrilGem: WorldLocationData(0x23BE, 3), + LocationName.CanyonFrostCrystal: WorldLocationData(0x23DB, 6), + LocationName.Screens: WorldLocationData(0x3709, 5), + LocationName.DonaldScreens: WorldLocationData(0x3709, 5), + LocationName.HallwayPowerCrystal: WorldLocationData(0x23BE, 4), + LocationName.HallwayAPBoost: WorldLocationData(0x23BE, 5), + LocationName.CommunicationsRoomIOTowerMap: WorldLocationData(0x23BF, 1), + LocationName.CommunicationsRoomGaiaBelt: WorldLocationData(0x23DA, 1), + LocationName.HostileProgram: WorldLocationData(0x3707, 7), + LocationName.HostileProgramGetBonus: WorldLocationData(0x3707, 7), + LocationName.GoofyHostileProgram: WorldLocationData(0x3707, 7), + LocationName.PhotonDebugger: WorldLocationData(0x1EB2, 3), + LocationName.SolarSailer: WorldLocationData(0x370B, 5), + LocationName.DonaldSolarSailer: WorldLocationData(0x370B, 5), + LocationName.CentralComputerCoreAPBoost: WorldLocationData(0x23BF, 4), + LocationName.CentralComputerCoreOrichalcumPlus: WorldLocationData(0x23BF, 5), + LocationName.CentralComputerCoreCosmicArts: WorldLocationData(0x23BF, 6), + LocationName.CentralComputerCoreMap: WorldLocationData(0x23D9, 0), + LocationName.MCP: WorldLocationData(0x3708, 0), + LocationName.MCPGetBonus: WorldLocationData(0x3708, 0), +} +HT_Checks = { + LocationName.GraveyardMythrilShard: WorldLocationData(0x23C0, 2), + LocationName.GraveyardSerenityGem: WorldLocationData(0x23C0, 3), + LocationName.FinklesteinsLabHalloweenTownMap: WorldLocationData(0x23C0, 1), + LocationName.TownSquareMythrilStone: WorldLocationData(0x23BF, 7), + LocationName.TownSquareEnergyShard: WorldLocationData(0x23C0, 0), + LocationName.HinterlandsLightningShard: WorldLocationData(0x23C0, 4), + LocationName.HinterlandsMythrilStone: WorldLocationData(0x23C0, 5), + LocationName.HinterlandsAPBoost: WorldLocationData(0x23C0, 6), + LocationName.CandyCaneLaneMegaPotion: WorldLocationData(0x23C1, 0), + LocationName.CandyCaneLaneMythrilGem: WorldLocationData(0x23C1, 1), + LocationName.CandyCaneLaneLightningStone: WorldLocationData(0x23C1, 2), + LocationName.CandyCaneLaneMythrilStone: WorldLocationData(0x23C1, 3), + LocationName.SantasHouseChristmasTownMap: WorldLocationData(0x23C1, 6), + LocationName.SantasHouseAPBoost: WorldLocationData(0x23C1, 4), + LocationName.PrisonKeeper: WorldLocationData(0x3706, 2), + LocationName.DonaldPrisonKeeper: WorldLocationData(0x3706, 2), + LocationName.OogieBoogie: WorldLocationData(0x3706, 3), + LocationName.GoofyOogieBoogie: WorldLocationData(0x3706, 3), + LocationName.OogieBoogieMagnetElement: WorldLocationData(0x1E53, 2), + LocationName.Lock: WorldLocationData(0x3709, 0), + LocationName.GoofyLock: WorldLocationData(0x3709, 0), + LocationName.Present: WorldLocationData(0x1E55, 1), + LocationName.DecoyPresents: WorldLocationData(0x1E55, 4), + LocationName.Experiment: WorldLocationData(0x3706, 4), + LocationName.DonaldExperiment: WorldLocationData(0x3706, 4), + LocationName.DecisivePumpkin: WorldLocationData(0x1E56, 0), + +} +PR_Checks = { + LocationName.RampartNavalMap: WorldLocationData(0x23C2, 1), + LocationName.RampartMythrilStone: WorldLocationData(0x23C2, 2), + LocationName.RampartDarkShard: WorldLocationData(0x23C2, 3), + LocationName.TownDarkStone: WorldLocationData(0x23C2, 4), + LocationName.TownAPBoost: WorldLocationData(0x23C2, 5), + LocationName.TownMythrilShard: WorldLocationData(0x23C2, 6), + LocationName.TownMythrilGem: WorldLocationData(0x23C2, 7), + LocationName.CaveMouthBrightShard: WorldLocationData(0x23C3, 1), + LocationName.CaveMouthMythrilShard: WorldLocationData(0x23C3, 2), + LocationName.IsladeMuertaMap: WorldLocationData(0x1E92, 4), + LocationName.BoatFight: WorldLocationData(0x370B, 6), + LocationName.DonaldBoatFight: WorldLocationData(0x370B, 6), + LocationName.InterceptorBarrels: WorldLocationData(0x3708, 7), + LocationName.GoofyInterceptorBarrels: WorldLocationData(0x3708, 7), + LocationName.PowderStoreAPBoost1: WorldLocationData(0x23CA, 7), + LocationName.PowderStoreAPBoost2: WorldLocationData(0x23CB, 0), + LocationName.MoonlightNookMythrilShard: WorldLocationData(0x23C3, 4), + LocationName.MoonlightNookSerenityGem: WorldLocationData(0x23C3, 5), + LocationName.MoonlightNookPowerStone: WorldLocationData(0x23CB, 1), + LocationName.Barbossa: WorldLocationData(0x3706, 5), + LocationName.BarbossaGetBonus: WorldLocationData(0x3706, 5), + LocationName.GoofyBarbossa: WorldLocationData(0x3706, 5), + LocationName.GoofyBarbossaGetBonus: WorldLocationData(0x3706, 5), + LocationName.FollowtheWind: WorldLocationData(0x1E93, 6), + LocationName.GrimReaper1: WorldLocationData(0x370B, 3), + LocationName.GoofyGrimReaper1: WorldLocationData(0x370B, 3), + LocationName.InterceptorsHoldFeatherCharm: WorldLocationData(0x23C3, 3), + LocationName.SeadriftKeepAPBoost: WorldLocationData(0x23C3, 6), + LocationName.SeadriftKeepOrichalcum: WorldLocationData(0x23C3, 7), + LocationName.SeadriftKeepMeteorStaff: WorldLocationData(0x23CB, 2), + LocationName.SeadriftRowSerenityGem: WorldLocationData(0x23C4, 0), + LocationName.SeadriftRowKingRecipe: WorldLocationData(0x23C4, 1), + LocationName.SeadriftRowMythrilCrystal: WorldLocationData(0x23CB, 3), + LocationName.SeadriftRowCursedMedallion: WorldLocationData(0x1E95, 2), + LocationName.SeadriftRowShipGraveyardMap: WorldLocationData(0x1E95, 2), + LocationName.GrimReaper2: WorldLocationData(0x3706, 6), + LocationName.DonaladGrimReaper2: WorldLocationData(0x3706, 6), + LocationName.SecretAnsemReport6: WorldLocationData(0x1E95, 7), + +} +HB_Checks = { + LocationName.MarketplaceMap: WorldLocationData(0x1D17, 4), + LocationName.BoroughDriveRecovery: WorldLocationData(0x23C6, 1), + LocationName.BoroughAPBoost: WorldLocationData(0x23C6, 2), + LocationName.BoroughHiPotion: WorldLocationData(0x23C6, 3), + LocationName.BoroughMythrilShard: WorldLocationData(0x23C8, 7), + LocationName.BoroughDarkShard: WorldLocationData(0x23DB, 1), + LocationName.MerlinsHouseMembershipCard: WorldLocationData(0x1D10, 6), + LocationName.MerlinsHouseBlizzardElement: WorldLocationData(0x1D10, 6), + # you cannot get these checks without baily so they are all on the same memory value. + LocationName.Bailey: WorldLocationData(0x3709, 7), + LocationName.BaileySecretAnsemReport7: WorldLocationData(0x3709, 7), + LocationName.BaseballCharm: WorldLocationData(0x3709, 7), + LocationName.PosternCastlePerimeterMap: WorldLocationData(0x23C9, 4), + LocationName.PosternMythrilGem: WorldLocationData(0x23C5, 4), + LocationName.PosternAPBoost: WorldLocationData(0x23C5, 5), + LocationName.CorridorsMythrilStone: WorldLocationData(0x23C6, 7), + LocationName.CorridorsMythrilCrystal: WorldLocationData(0x23C7, 0), + LocationName.CorridorsDarkCrystal: WorldLocationData(0x23C7, 1), + LocationName.CorridorsAPBoost: WorldLocationData(0x23C9, 1), + # this is probably gonna be wrong + LocationName.AnsemsStudyMasterForm: WorldLocationData(0x1D12, 6), + LocationName.AnsemsStudySleepingLion: WorldLocationData(0x1D12, 6), + LocationName.AnsemsStudySkillRecipe: WorldLocationData(0x23C4, 7), + LocationName.AnsemsStudyUkuleleCharm: WorldLocationData(0x23C4, 6), + LocationName.RestorationSiteMoonRecipe: WorldLocationData(0x23C9, 3), + LocationName.RestorationSiteAPBoost: WorldLocationData(0x23DB, 2), + LocationName.DemyxHB: WorldLocationData(0x3707, 4), + LocationName.DemyxHBGetBonus: WorldLocationData(0x3707, 4), + LocationName.DonaldDemyxHBGetBonus: WorldLocationData(0x3707, 4), + LocationName.FFFightsCureElement: WorldLocationData(0x1D14, 6), + LocationName.CrystalFissureTornPages: WorldLocationData(0x23C4, 2), + LocationName.CrystalFissureTheGreatMawMap: WorldLocationData(0x23D9, 1), + LocationName.CrystalFissureEnergyCrystal: WorldLocationData(0x23C4, 3), + LocationName.CrystalFissureAPBoost: WorldLocationData(0x23C4, 4), + LocationName.ThousandHeartless: WorldLocationData(0x370B, 4), + LocationName.ThousandHeartlessSecretAnsemReport1: WorldLocationData(0x1D19, 3), + LocationName.ThousandHeartlessIceCream: WorldLocationData(0x1D23, 0), + LocationName.ThousandHeartlessPicture: WorldLocationData(0x1D23, 0), + LocationName.PosternGullWing: WorldLocationData(0x23D9, 3), + LocationName.HeartlessManufactoryCosmicChain: WorldLocationData(0x23C9, 5), + LocationName.SephirothBonus: WorldLocationData(0x3708, 3), + LocationName.SephirothFenrir: WorldLocationData(0x1D1F, 7), + LocationName.WinnersProof: WorldLocationData(0x1D27, 5), + LocationName.ProofofPeace: WorldLocationData(0x1D27, 5), + + LocationName.CoRDepthsAPBoost: WorldLocationData(0x23DC, 2), + LocationName.CoRDepthsPowerCrystal: WorldLocationData(0x23DC, 3), + LocationName.CoRDepthsFrostCrystal: WorldLocationData(0x23DC, 4), + LocationName.CoRDepthsManifestIllusion: WorldLocationData(0x23DC, 5), + LocationName.CoRDepthsAPBoost2: WorldLocationData(0x23DC, 6), + LocationName.CoRMineshaftLowerLevelDepthsofRemembranceMap: WorldLocationData(0x23DE, 4), + LocationName.CoRMineshaftLowerLevelAPBoost: WorldLocationData(0x23DE, 2), + LocationName.CoRDepthsUpperLevelRemembranceGem: WorldLocationData(0x23DC, 7), + LocationName.CoRMiningAreaSerenityGem: WorldLocationData(0x23DD, 0), + LocationName.CoRMiningAreaAPBoost: WorldLocationData(0x23DD, 1), + LocationName.CoRMiningAreaSerenityCrystal: WorldLocationData(0x23DD, 2), + LocationName.CoRMiningAreaManifestIllusion: WorldLocationData(0x23DD, 3), + LocationName.CoRMiningAreaSerenityGem2: WorldLocationData(0x23DD, 4), + LocationName.CoRMiningAreaDarkRemembranceMap: WorldLocationData(0x23DD, 5), + LocationName.CoRMineshaftMidLevelPowerBoost: WorldLocationData(0x23DE, 5), + LocationName.CoREngineChamberSerenityCrystal: WorldLocationData(0x23DD, 6), + LocationName.CoREngineChamberRemembranceCrystal: WorldLocationData(0x23DD, 7), + LocationName.CoREngineChamberAPBoost: WorldLocationData(0x23DE, 0), + LocationName.CoREngineChamberManifestIllusion: WorldLocationData(0x23DE, 1), + LocationName.CoRMineshaftUpperLevelMagicBoost: WorldLocationData(0x23DE, 6), + LocationName.CoRMineshaftUpperLevelAPBoost: WorldLocationData(0x23DE, 3), + LocationName.TransporttoRemembrance: WorldLocationData(0x370D, 0), + + LocationName.LexaeusBonus: WorldLocationData(0x370C, 1), + LocationName.LexaeusASStrengthBeyondStrength: WorldLocationData(0x370C, 1), + LocationName.LexaeusDataLostIllusion: WorldLocationData(0x370C, 1), # + LocationName.MarluxiaGetBonus: WorldLocationData(0x370C, 3), + LocationName.MarluxiaASEternalBlossom: WorldLocationData(0x370C, 3), + LocationName.MarluxiaDataLostIllusion: WorldLocationData(0x370C, 3), # + LocationName.ZexionBonus: WorldLocationData(0x370C, 2), + LocationName.GoofyZexion: WorldLocationData(0x370C, 2), + LocationName.ZexionASBookofShadows: WorldLocationData(0x370C, 2), + LocationName.ZexionDataLostIllusion: WorldLocationData(0x370C, 2), # + LocationName.LarxeneBonus: WorldLocationData(0x370C, 4), + LocationName.LarxeneASCloakedThunder: WorldLocationData(0x370C, 4), + LocationName.LarxeneDataLostIllusion: WorldLocationData(0x370C, 4), # + LocationName.VexenBonus: WorldLocationData(0x370C, 0), + LocationName.VexenASRoadtoDiscovery: WorldLocationData(0x370C, 0), + LocationName.VexenDataLostIllusion: WorldLocationData(0x370C, 0), # + LocationName.DemyxDataAPBoost: WorldLocationData(0x1D26, 5), + LocationName.GardenofAssemblageMap: WorldLocationData(0x23DF, 1), + LocationName.GoALostIllusion: WorldLocationData(0x23DF, 2), + LocationName.ProofofNonexistence: WorldLocationData(0x23DF, 3), + # given when you talk to the computer + LocationName.KingdomKeySlot: WorldLocationData(0x1D27, 3), + LocationName.MagesStaff: WorldLocationData(0x1D27, 3), + LocationName.KnightsShield: WorldLocationData(0x1D27, 3), + LocationName.DonaldStarting1: WorldLocationData(0x1D27, 3), + LocationName.DonaldStarting2: WorldLocationData(0x1D27, 3), + LocationName.GoofyStarting1: WorldLocationData(0x1D27, 3), + LocationName.GoofyStarting2: WorldLocationData(0x1D27, 3), + LocationName.Crit_1: WorldLocationData(0x1D27, 3), + LocationName.Crit_2: WorldLocationData(0x1D27, 3), + LocationName.Crit_3: WorldLocationData(0x1D27, 3), + LocationName.Crit_4: WorldLocationData(0x1D27, 3), + LocationName.Crit_5: WorldLocationData(0x1D27, 3), + LocationName.Crit_6: WorldLocationData(0x1D27, 3), + LocationName.Crit_7: WorldLocationData(0x1D27, 3), + +} +PL_Checks = { + LocationName.GorgeSavannahMap: WorldLocationData(0x23D9, 4), + LocationName.GorgeDarkGem: WorldLocationData(0x23CF, 0), + LocationName.GorgeMythrilStone: WorldLocationData(0x23CF, 1), + LocationName.ElephantGraveyardFrostGem: WorldLocationData(0x23CE, 5), + LocationName.ElephantGraveyardMythrilStone: WorldLocationData(0x23CE, 6), + LocationName.ElephantGraveyardBrightStone: WorldLocationData(0x23CE, 7), + LocationName.ElephantGraveyardAPBoost: WorldLocationData(0x23DB, 3), + LocationName.ElephantGraveyardMythrilShard: WorldLocationData(0x23DB, 4), + LocationName.PrideRockMap: WorldLocationData(0x23D0, 3), + LocationName.PrideRockMythrilStone: WorldLocationData(0x23CD, 4), + LocationName.PrideRockSerenityCrystal: WorldLocationData(0x23CD, 5), + LocationName.WildebeestValleyEnergyStone: WorldLocationData(0x23CE, 0), + LocationName.WildebeestValleyAPBoost: WorldLocationData(0x23CE, 1), + LocationName.WildebeestValleyMythrilGem: WorldLocationData(0x23CE, 2), + LocationName.WildebeestValleyMythrilStone: WorldLocationData(0x23CE, 3), + LocationName.WildebeestValleyLucidGem: WorldLocationData(0x23CE, 4), + LocationName.WastelandsMythrilShard: WorldLocationData(0x23CF, 2), + LocationName.WastelandsSerenityGem: WorldLocationData(0x23CF, 3), + LocationName.WastelandsMythrilStone: WorldLocationData(0x23CF, 4), + LocationName.JungleSerenityGem: WorldLocationData(0x23CF, 5), + LocationName.JungleMythrilStone: WorldLocationData(0x23CF, 6), + LocationName.JungleSerenityCrystal: WorldLocationData(0x23CF, 7), + LocationName.OasisMap: WorldLocationData(0x23D0, 0), + LocationName.OasisTornPages: WorldLocationData(0x23D9, 5), + LocationName.OasisAPBoost: WorldLocationData(0x23D0, 1), + LocationName.CircleofLife: WorldLocationData(0x1DD2, 1), + LocationName.Hyenas1: WorldLocationData(0x370A, 1), + LocationName.GoofyHyenas1: WorldLocationData(0x370A, 1), + LocationName.Scar: WorldLocationData(0x3707, 5), + LocationName.DonaldScar: WorldLocationData(0x3707, 5), + LocationName.ScarFireElement: WorldLocationData(0x1DD4, 7), + LocationName.Hyenas2: WorldLocationData(0x370A, 2), + LocationName.GoofyHyenas2: WorldLocationData(0x370A, 2), + LocationName.Groundshaker: WorldLocationData(0x3707, 6), + LocationName.GroundshakerGetBonus: WorldLocationData(0x3707, 6), + +} +TT_Checks = { + LocationName.TwilightTownMap: WorldLocationData(0x1CD6, 3), + LocationName.MunnyPouchOlette: WorldLocationData(0x1CD6, 5), + LocationName.StationDusks: WorldLocationData(0x370A, 6), + LocationName.StationofSerenityPotion: WorldLocationData(0x23CA, 1), + LocationName.StationofCallingPotion: WorldLocationData(0x23D7, 1), + LocationName.TwilightThorn: WorldLocationData(0x3708, 1), + LocationName.Axel1: WorldLocationData(0x370D, 1), + LocationName.JunkChampionBelt: WorldLocationData(0x1CDC, 2), + LocationName.JunkMedal: WorldLocationData(0x1CDC, 2), + LocationName.TheStruggleTrophy: WorldLocationData(0x1CDC, 2), + LocationName.CentralStationPotion1: WorldLocationData(0x23D1, 5), + LocationName.STTCentralStationHiPotion: WorldLocationData(0x23D1, 6), + LocationName.CentralStationPotion2: WorldLocationData(0x23D1, 7), + LocationName.SunsetTerraceAbilityRing: WorldLocationData(0x23D2, 3), + LocationName.SunsetTerraceHiPotion: WorldLocationData(0x23D2, 4), + LocationName.SunsetTerracePotion1: WorldLocationData(0x23D2, 5), + LocationName.SunsetTerracePotion2: WorldLocationData(0x23D2, 6), + LocationName.MansionFoyerHiPotion: WorldLocationData(0x23D4, 2), + LocationName.MansionFoyerPotion1: WorldLocationData(0x23D4, 3), + LocationName.MansionFoyerPotion2: WorldLocationData(0x23D4, 4), + LocationName.MansionDiningRoomElvenBandanna: WorldLocationData(0x23D5, 0), + LocationName.MansionDiningRoomPotion: WorldLocationData(0x23D5, 1), + LocationName.NaminesSketches: WorldLocationData(0x1CE0, 6), + LocationName.MansionMap: WorldLocationData(0x1CE0, 6), + LocationName.MansionLibraryHiPotion: WorldLocationData(0x23D5, 4), + LocationName.Axel2: WorldLocationData(0x3708, 2), + LocationName.MansionBasementCorridorHiPotion: WorldLocationData(0x23D6, 0), + # stt and tt share the same world id + LocationName.OldMansionPotion: WorldLocationData(0x23D4, 0), + LocationName.OldMansionMythrilShard: WorldLocationData(0x23D4, 1), + LocationName.TheWoodsPotion: WorldLocationData(0x23D3, 3), + LocationName.TheWoodsMythrilShard: WorldLocationData(0x23D3, 4), + LocationName.TheWoodsHiPotion: WorldLocationData(0x23D3, 5), + LocationName.TramCommonHiPotion: WorldLocationData(0x23D0, 5), + LocationName.TramCommonAPBoost: WorldLocationData(0x23D0, 6), + LocationName.TramCommonTent: WorldLocationData(0x23D0, 7), + LocationName.TramCommonMythrilShard1: WorldLocationData(0x23D1, 0), + LocationName.TramCommonPotion1: WorldLocationData(0x23D1, 1), + LocationName.TramCommonMythrilShard2: WorldLocationData(0x23D1, 2), + LocationName.TramCommonPotion2: WorldLocationData(0x23D8, 5), + LocationName.StationPlazaSecretAnsemReport2: WorldLocationData(0x1CE3, 3), + LocationName.MunnyPouchMickey: WorldLocationData(0x1CE3, 3), + LocationName.CrystalOrb: WorldLocationData(0x1CE3, 3), + LocationName.CentralStationTent: WorldLocationData(0x23D2, 0), + LocationName.TTCentralStationHiPotion: WorldLocationData(0x23D2, 1), + LocationName.CentralStationMythrilShard: WorldLocationData(0x23D2, 2), + LocationName.TheTowerPotion: WorldLocationData(0x23D6, 2), + LocationName.TheTowerHiPotion: WorldLocationData(0x23D6, 3), + LocationName.TheTowerEther: WorldLocationData(0x23DB, 7), + LocationName.TowerEntrywayEther: WorldLocationData(0x23D6, 4), + LocationName.TowerEntrywayMythrilShard: WorldLocationData(0x23D6, 5), + LocationName.SorcerersLoftTowerMap: WorldLocationData(0x23D6, 6), + LocationName.TowerWardrobeMythrilStone: WorldLocationData(0x23D6, 7), + LocationName.StarSeeker: WorldLocationData(0x1CE5, 2), + LocationName.ValorForm: WorldLocationData(0x1CE5, 2), + LocationName.SeifersTrophy: WorldLocationData(0x1CE6, 4), + LocationName.Oathkeeper: WorldLocationData(0x1CE6, 7), + LocationName.LimitForm: WorldLocationData(0x1CE6, 7), + LocationName.UndergroundConcourseMythrilGem: WorldLocationData(0x23D8, 0), + LocationName.UndergroundConcourseAPBoost: WorldLocationData(0x23D8, 2), + LocationName.UndergroundConcourseOrichalcum: WorldLocationData(0x23D8, 1), + LocationName.UndergroundConcourseMythrilCrystal: WorldLocationData(0x23D8, 3), + LocationName.TunnelwayOrichalcum: WorldLocationData(0x23D7, 6), + LocationName.TunnelwayMythrilCrystal: WorldLocationData(0x23D7, 7), + LocationName.SunsetTerraceOrichalcumPlus: WorldLocationData(0x23D2, 7), + LocationName.SunsetTerraceMythrilShard: WorldLocationData(0x23D3, 0), + LocationName.SunsetTerraceMythrilCrystal: WorldLocationData(0x23D3, 1), + LocationName.SunsetTerraceAPBoost: WorldLocationData(0x23D3, 2), + LocationName.MansionNobodies: WorldLocationData(0x370B, 0), + LocationName.DonaldMansionNobodies: WorldLocationData(0x370B, 0), + LocationName.MansionFoyerMythrilCrystal: WorldLocationData(0x23D4, 5), + LocationName.MansionFoyerMythrilStone: WorldLocationData(0x23D4, 6), + LocationName.MansionFoyerSerenityCrystal: WorldLocationData(0x23D4, 7), + LocationName.MansionDiningRoomMythrilCrystal: WorldLocationData(0x23D5, 2), + LocationName.MansionDiningRoomMythrilStone: WorldLocationData(0x23D5, 3), + LocationName.MansionLibraryOrichalcum: WorldLocationData(0x23D5, 5), + LocationName.BeamSecretAnsemReport10: WorldLocationData(0x1CE8, 3), + LocationName.MansionBasementCorridorUltimateRecipe: WorldLocationData(0x23D6, 1), + LocationName.BetwixtandBetween: WorldLocationData(0x370B, 7), + LocationName.BetwixtandBetweenBondofFlame: WorldLocationData(0x1CE9, 1), + LocationName.AxelDataMagicBoost: WorldLocationData(0x1CEB, 4), +} +TWTNW_Checks = { + LocationName.FragmentCrossingMythrilStone: WorldLocationData(0x23CB, 4), + LocationName.FragmentCrossingMythrilCrystal: WorldLocationData(0x23CB, 5), + LocationName.FragmentCrossingAPBoost: WorldLocationData(0x23CB, 6), + LocationName.FragmentCrossingOrichalcum: WorldLocationData(0x23CB, 7), + LocationName.Roxas: WorldLocationData(0x370C, 5), + LocationName.RoxasGetBonus: WorldLocationData(0x370C, 5), + LocationName.RoxasSecretAnsemReport8: WorldLocationData(0x1ED1, 1), + LocationName.TwoBecomeOne: WorldLocationData(0x1ED1, 1), + LocationName.MemorysSkyscaperMythrilCrystal: WorldLocationData(0x23CD, 3), + LocationName.MemorysSkyscaperAPBoost: WorldLocationData(0x23DC, 0), + LocationName.MemorysSkyscaperMythrilStone: WorldLocationData(0x23DC, 1), + LocationName.TheBrinkofDespairDarkCityMap: WorldLocationData(0x23CA, 5), + LocationName.TheBrinkofDespairOrichalcumPlus: WorldLocationData(0x23DA, 2), + LocationName.NothingsCallMythrilGem: WorldLocationData(0x23CC, 0), + LocationName.NothingsCallOrichalcum: WorldLocationData(0x23CC, 1), + LocationName.TwilightsViewCosmicBelt: WorldLocationData(0x23CA, 6), + LocationName.XigbarBonus: WorldLocationData(0x3706, 7), + LocationName.XigbarSecretAnsemReport3: WorldLocationData(0x1ED2, 2), + LocationName.NaughtsSkywayMythrilGem: WorldLocationData(0x23CC, 2), + LocationName.NaughtsSkywayOrichalcum: WorldLocationData(0x23CC, 3), + LocationName.NaughtsSkywayMythrilCrystal: WorldLocationData(0x23CC, 4), + LocationName.Oblivion: WorldLocationData(0x1ED2, 4), + LocationName.CastleThatNeverWasMap: WorldLocationData(0x1ED2, 4), + LocationName.Luxord: WorldLocationData(0x3707, 0), + LocationName.LuxordGetBonus: WorldLocationData(0x3707, 0), + LocationName.LuxordSecretAnsemReport9: WorldLocationData(0x1ED2, 7), + LocationName.SaixBonus: WorldLocationData(0x3707, 1), + LocationName.SaixSecretAnsemReport12: WorldLocationData(0x1ED3, 2), + LocationName.PreXemnas1SecretAnsemReport11: WorldLocationData(0x1ED3, 6), + LocationName.RuinandCreationsPassageMythrilStone: WorldLocationData(0x23CC, 7), + LocationName.RuinandCreationsPassageAPBoost: WorldLocationData(0x23CD, 0), + LocationName.RuinandCreationsPassageMythrilCrystal: WorldLocationData(0x23CD, 1), + LocationName.RuinandCreationsPassageOrichalcum: WorldLocationData(0x23CD, 2), + LocationName.Xemnas1: WorldLocationData(0x3707, 2), + LocationName.Xemnas1GetBonus: WorldLocationData(0x3707, 2), + LocationName.Xemnas1SecretAnsemReport13: WorldLocationData(0x1ED4, 5), + LocationName.FinalXemnas: WorldLocationData(0x1ED8, 1), + LocationName.XemnasDataPowerBoost: WorldLocationData(0x1EDA, 2), + LocationName.XigbarDataDefenseBoost: WorldLocationData(0x1ED9, 7), + LocationName.SaixDataDefenseBoost: WorldLocationData(0x1EDA, 0), + LocationName.LuxordDataAPBoost: WorldLocationData(0x1EDA, 1), + LocationName.RoxasDataMagicBoost: WorldLocationData(0x1ED9, 6), +} +SoraLevels = { + # LocationName.Lvl1: WorldLocationData(0xFFFF,1), + LocationName.Lvl2: WorldLocationData(0xFFFF, 2), + LocationName.Lvl3: WorldLocationData(0xFFFF, 3), + LocationName.Lvl4: WorldLocationData(0xFFFF, 4), + LocationName.Lvl5: WorldLocationData(0xFFFF, 5), + LocationName.Lvl6: WorldLocationData(0xFFFF, 6), + LocationName.Lvl7: WorldLocationData(0xFFFF, 7), + LocationName.Lvl8: WorldLocationData(0xFFFF, 8), + LocationName.Lvl9: WorldLocationData(0xFFFF, 9), + LocationName.Lvl10: WorldLocationData(0xFFFF, 10), + LocationName.Lvl11: WorldLocationData(0xFFFF, 11), + LocationName.Lvl12: WorldLocationData(0xFFFF, 12), + LocationName.Lvl13: WorldLocationData(0xFFFF, 13), + LocationName.Lvl14: WorldLocationData(0xFFFF, 14), + LocationName.Lvl15: WorldLocationData(0xFFFF, 15), + LocationName.Lvl16: WorldLocationData(0xFFFF, 16), + LocationName.Lvl17: WorldLocationData(0xFFFF, 17), + LocationName.Lvl18: WorldLocationData(0xFFFF, 18), + LocationName.Lvl19: WorldLocationData(0xFFFF, 19), + LocationName.Lvl20: WorldLocationData(0xFFFF, 20), + LocationName.Lvl21: WorldLocationData(0xFFFF, 21), + LocationName.Lvl22: WorldLocationData(0xFFFF, 22), + LocationName.Lvl23: WorldLocationData(0xFFFF, 23), + LocationName.Lvl24: WorldLocationData(0xFFFF, 24), + LocationName.Lvl25: WorldLocationData(0xFFFF, 25), + LocationName.Lvl26: WorldLocationData(0xFFFF, 26), + LocationName.Lvl27: WorldLocationData(0xFFFF, 27), + LocationName.Lvl28: WorldLocationData(0xFFFF, 28), + LocationName.Lvl29: WorldLocationData(0xFFFF, 29), + LocationName.Lvl30: WorldLocationData(0xFFFF, 30), + LocationName.Lvl31: WorldLocationData(0xFFFF, 31), + LocationName.Lvl32: WorldLocationData(0xFFFF, 32), + LocationName.Lvl33: WorldLocationData(0xFFFF, 33), + LocationName.Lvl34: WorldLocationData(0xFFFF, 34), + LocationName.Lvl35: WorldLocationData(0xFFFF, 35), + LocationName.Lvl36: WorldLocationData(0xFFFF, 36), + LocationName.Lvl37: WorldLocationData(0xFFFF, 37), + LocationName.Lvl38: WorldLocationData(0xFFFF, 38), + LocationName.Lvl39: WorldLocationData(0xFFFF, 39), + LocationName.Lvl40: WorldLocationData(0xFFFF, 40), + LocationName.Lvl41: WorldLocationData(0xFFFF, 41), + LocationName.Lvl42: WorldLocationData(0xFFFF, 42), + LocationName.Lvl43: WorldLocationData(0xFFFF, 43), + LocationName.Lvl44: WorldLocationData(0xFFFF, 44), + LocationName.Lvl45: WorldLocationData(0xFFFF, 45), + LocationName.Lvl46: WorldLocationData(0xFFFF, 46), + LocationName.Lvl47: WorldLocationData(0xFFFF, 47), + LocationName.Lvl48: WorldLocationData(0xFFFF, 48), + LocationName.Lvl49: WorldLocationData(0xFFFF, 49), + LocationName.Lvl50: WorldLocationData(0xFFFF, 50), + LocationName.Lvl51: WorldLocationData(0xFFFF, 51), + LocationName.Lvl52: WorldLocationData(0xFFFF, 52), + LocationName.Lvl53: WorldLocationData(0xFFFF, 53), + LocationName.Lvl54: WorldLocationData(0xFFFF, 54), + LocationName.Lvl55: WorldLocationData(0xFFFF, 55), + LocationName.Lvl56: WorldLocationData(0xFFFF, 56), + LocationName.Lvl57: WorldLocationData(0xFFFF, 57), + LocationName.Lvl58: WorldLocationData(0xFFFF, 58), + LocationName.Lvl59: WorldLocationData(0xFFFF, 59), + LocationName.Lvl60: WorldLocationData(0xFFFF, 60), + LocationName.Lvl61: WorldLocationData(0xFFFF, 61), + LocationName.Lvl62: WorldLocationData(0xFFFF, 62), + LocationName.Lvl63: WorldLocationData(0xFFFF, 63), + LocationName.Lvl64: WorldLocationData(0xFFFF, 64), + LocationName.Lvl65: WorldLocationData(0xFFFF, 65), + LocationName.Lvl66: WorldLocationData(0xFFFF, 66), + LocationName.Lvl67: WorldLocationData(0xFFFF, 67), + LocationName.Lvl68: WorldLocationData(0xFFFF, 68), + LocationName.Lvl69: WorldLocationData(0xFFFF, 69), + LocationName.Lvl70: WorldLocationData(0xFFFF, 70), + LocationName.Lvl71: WorldLocationData(0xFFFF, 71), + LocationName.Lvl72: WorldLocationData(0xFFFF, 72), + LocationName.Lvl73: WorldLocationData(0xFFFF, 73), + LocationName.Lvl74: WorldLocationData(0xFFFF, 74), + LocationName.Lvl75: WorldLocationData(0xFFFF, 75), + LocationName.Lvl76: WorldLocationData(0xFFFF, 76), + LocationName.Lvl77: WorldLocationData(0xFFFF, 77), + LocationName.Lvl78: WorldLocationData(0xFFFF, 78), + LocationName.Lvl79: WorldLocationData(0xFFFF, 79), + LocationName.Lvl80: WorldLocationData(0xFFFF, 80), + LocationName.Lvl81: WorldLocationData(0xFFFF, 81), + LocationName.Lvl82: WorldLocationData(0xFFFF, 82), + LocationName.Lvl83: WorldLocationData(0xFFFF, 83), + LocationName.Lvl84: WorldLocationData(0xFFFF, 84), + LocationName.Lvl85: WorldLocationData(0xFFFF, 85), + LocationName.Lvl86: WorldLocationData(0xFFFF, 86), + LocationName.Lvl87: WorldLocationData(0xFFFF, 87), + LocationName.Lvl88: WorldLocationData(0xFFFF, 88), + LocationName.Lvl89: WorldLocationData(0xFFFF, 89), + LocationName.Lvl90: WorldLocationData(0xFFFF, 90), + LocationName.Lvl91: WorldLocationData(0xFFFF, 91), + LocationName.Lvl92: WorldLocationData(0xFFFF, 92), + LocationName.Lvl93: WorldLocationData(0xFFFF, 93), + LocationName.Lvl94: WorldLocationData(0xFFFF, 94), + LocationName.Lvl95: WorldLocationData(0xFFFF, 95), + LocationName.Lvl96: WorldLocationData(0xFFFF, 96), + LocationName.Lvl97: WorldLocationData(0xFFFF, 97), + LocationName.Lvl98: WorldLocationData(0xFFFF, 98), + LocationName.Lvl99: WorldLocationData(0xFFFF, 99), +} + +ValorLevels = { + # LocationName.Valorlvl1: WorldLocationData(0x32F6, 1), + LocationName.Valorlvl2: WorldLocationData(0x32F6, 2), + LocationName.Valorlvl3: WorldLocationData(0x32F6, 3), + LocationName.Valorlvl4: WorldLocationData(0x32F6, 4), + LocationName.Valorlvl5: WorldLocationData(0x32F6, 5), + LocationName.Valorlvl6: WorldLocationData(0x32F6, 6), + LocationName.Valorlvl7: WorldLocationData(0x32F6, 7), +} + +WisdomLevels = { + # LocationName.Wisdomlvl1: WorldLocationData(0x332E, 1), + LocationName.Wisdomlvl2: WorldLocationData(0x332E, 2), + LocationName.Wisdomlvl3: WorldLocationData(0x332E, 3), + LocationName.Wisdomlvl4: WorldLocationData(0x332E, 4), + LocationName.Wisdomlvl5: WorldLocationData(0x332E, 5), + LocationName.Wisdomlvl6: WorldLocationData(0x332E, 6), + LocationName.Wisdomlvl7: WorldLocationData(0x332E, 7), +} + +LimitLevels = { + # LocationName.Limitlvl1: WorldLocationData(0x3366, 1), + LocationName.Limitlvl2: WorldLocationData(0x3366, 2), + LocationName.Limitlvl3: WorldLocationData(0x3366, 3), + LocationName.Limitlvl4: WorldLocationData(0x3366, 4), + LocationName.Limitlvl5: WorldLocationData(0x3366, 5), + LocationName.Limitlvl6: WorldLocationData(0x3366, 6), + LocationName.Limitlvl7: WorldLocationData(0x3366, 7), +} +MasterLevels = { + # LocationName.Masterlvl1: WorldLocationData(0x339E, 1), + LocationName.Masterlvl2: WorldLocationData(0x339E, 2), + LocationName.Masterlvl3: WorldLocationData(0x339E, 3), + LocationName.Masterlvl4: WorldLocationData(0x339E, 4), + LocationName.Masterlvl5: WorldLocationData(0x339E, 5), + LocationName.Masterlvl6: WorldLocationData(0x339E, 6), + LocationName.Masterlvl7: WorldLocationData(0x339E, 7), +} +FinalLevels = { + # LocationName.Finallvl1: WorldLocationData(0x33D6,1), + LocationName.Finallvl2: WorldLocationData(0x33D6, 2), + LocationName.Finallvl3: WorldLocationData(0x33D6, 3), + LocationName.Finallvl4: WorldLocationData(0x33D6, 4), + LocationName.Finallvl5: WorldLocationData(0x33D6, 5), + LocationName.Finallvl6: WorldLocationData(0x33D6, 6), + LocationName.Finallvl7: WorldLocationData(0x33D6, 7), + +} +weaponSlots = { + LocationName.AdamantShield: WorldLocationData(0x35E6, 1), + LocationName.AkashicRecord: WorldLocationData(0x35ED, 1), + LocationName.ChainGear: WorldLocationData(0x35E7, 1), + LocationName.DreamCloud: WorldLocationData(0x35EA, 1), + LocationName.FallingStar: WorldLocationData(0x35E9, 1), + LocationName.FrozenPride2: WorldLocationData(0x36A2, 1), + LocationName.GenjiShield: WorldLocationData(0x35EC, 1), + LocationName.KnightDefender: WorldLocationData(0x35EB, 1), + LocationName.MajesticMushroom: WorldLocationData(0x36A5, 1), + LocationName.MajesticMushroom2: WorldLocationData(0x36A6, 1), + LocationName.NobodyGuard: WorldLocationData(0x35EE, 1), + LocationName.OgreShield: WorldLocationData(0x35E8, 1), + LocationName.SaveTheKing2: WorldLocationData(0x3693, 1), + LocationName.UltimateMushroom: WorldLocationData(0x36A7, 1), + + LocationName.CometStaff: WorldLocationData(0x35F2, 1), + LocationName.HammerStaff: WorldLocationData(0x35EF, 1), + LocationName.LordsBroom: WorldLocationData(0x35F3, 1), + LocationName.MeteorStaff: WorldLocationData(0x35F1, 1), + LocationName.NobodyLance: WorldLocationData(0x35F6, 1), + LocationName.PreciousMushroom: WorldLocationData(0x369E, 1), + LocationName.PreciousMushroom2: WorldLocationData(0x369F, 1), + LocationName.PremiumMushroom: WorldLocationData(0x36A0, 1), + LocationName.RisingDragon: WorldLocationData(0x35F5, 1), + LocationName.SaveTheQueen2: WorldLocationData(0x3692, 1), + LocationName.ShamansRelic: WorldLocationData(0x35F7, 1), + LocationName.VictoryBell: WorldLocationData(0x35F0, 1), + LocationName.WisdomWand: WorldLocationData(0x35F4, 1), + LocationName.Centurion2: WorldLocationData(0x369B, 1), + + LocationName.OathkeeperSlot: WorldLocationData(0x35A2, 1), + LocationName.OblivionSlot: WorldLocationData(0x35A3, 1), + LocationName.StarSeekerSlot: WorldLocationData(0x367B, 1), + LocationName.HiddenDragonSlot: WorldLocationData(0x367C, 1), + LocationName.HerosCrestSlot: WorldLocationData(0x367F, 1), + LocationName.MonochromeSlot: WorldLocationData(0x3680, 1), + LocationName.FollowtheWindSlot: WorldLocationData(0x3681, 1), + LocationName.CircleofLifeSlot: WorldLocationData(0x3682, 1), + LocationName.PhotonDebuggerSlot: WorldLocationData(0x3683, 1), + LocationName.GullWingSlot: WorldLocationData(0x3684, 1), + LocationName.RumblingRoseSlot: WorldLocationData(0x3685, 1), + LocationName.GuardianSoulSlot: WorldLocationData(0x3686, 1), + LocationName.WishingLampSlot: WorldLocationData(0x3687, 1), + LocationName.DecisivePumpkinSlot: WorldLocationData(0x3688, 1), + LocationName.SleepingLionSlot: WorldLocationData(0x3689, 1), + LocationName.SweetMemoriesSlot: WorldLocationData(0x368A, 1), + LocationName.MysteriousAbyssSlot: WorldLocationData(0x368B, 1), + LocationName.TwoBecomeOneSlot: WorldLocationData(0x3698, 1), + LocationName.FatalCrestSlot: WorldLocationData(0x368C, 1), + LocationName.BondofFlameSlot: WorldLocationData(0x368D, 1), + LocationName.FenrirSlot: WorldLocationData(0x368E, 1), + LocationName.UltimaWeaponSlot: WorldLocationData(0x368F, 1), + LocationName.WinnersProofSlot: WorldLocationData(0x3699, 1), + LocationName.PurebloodSlot: WorldLocationData(0x35BF, 1), +} + +formSlots = { + LocationName.FAKESlot: WorldLocationData(0x36C0, 1), + LocationName.DetectionSaberSlot: WorldLocationData(0x36C0, 6), + LocationName.EdgeofUltimaSlot: WorldLocationData(0x36C0, 4), +} + +tornPageLocks = { + "TornPage1": WorldLocationData(0x1DB7, 4), # --Scenario_1_start + "TornPage2": WorldLocationData(0x1DB7, 7), # --Scenario_2_start + "TornPage3": WorldLocationData(0x1DB8, 2), # --Scenario_3_start + "TornPage4": WorldLocationData(0x1DB8, 4), # --Scenario_4_start + "TornPage5": WorldLocationData(0x1DB8, 7), # --Scenario_5_start +} +all_world_locations = { + **TWTNW_Checks, + **TT_Checks, + **TT_Checks, + **HB_Checks, + **BC_Checks, + **Oc_Checks, + **AG_Checks, + **LoD_Checks, + **HundredAcreChecks, + **PL_Checks, + **DC_Checks, + **TR_Checks, + **HT_Checks, + **HB_Checks, + **PR_Checks, + **SP_Checks, + **TWTNW_Checks, + **HB_Checks, +} + +levels_locations = { + **SoraLevels, + **ValorLevels, + **WisdomLevels, + **LimitLevels, + **MasterLevels, + **FinalLevels, +} diff --git a/worlds/kh2/XPValues.py b/worlds/kh2/XPValues.py new file mode 100644 index 0000000000..46e59acaca --- /dev/null +++ b/worlds/kh2/XPValues.py @@ -0,0 +1,119 @@ +lvlStats = [ + "str", + "mag", + "def", + "ap", + "", +] + +formExp = { + 0: {1: 6, 2: 16, 3: 25, 4: 42, 5: 63, 6: 98, 7: 0}, + 1: {1: 80, 2: 160, 3: 280, 4: 448, 5: 560, 6: 672, 7: 0}, + 2: {1: 12, 2: 24, 3: 48, 4: 76, 5: 133, 6: 157, 7: 0}, + 3: {1: 3, 2: 6, 3: 12, 4: 19, 5: 23, 6: 27, 7: 0}, + 4: {1: 40, 2: 80, 3: 140, 4: 224, 5: 448, 6: 668, 7: 0}, + 5: {1: 12, 2: 24, 3: 48, 4: 76, 5: 133, 6: 157, 7: 0} +} + +soraExp = [ + 0, + 40, + 100, + 184, + 296, + 440, + 620, + 840, + 1128, + 1492, + 1940, + 2480, + 3120, + 3902, + 4838, + 5940, + 7260, + 8814, + 10618, + 12688, + 15088, + 17838, + 20949, + 24433, + 28302, + 32622, + 37407, + 42671, + 48485, + 54865, + 61886, + 69566, + 77984, + 87160, + 97177, + 108057, + 119887, + 132691, + 146560, + 161520, + 177666, + 195026, + 213699, + 233715, + 255177, + 278117, + 302642, + 328786, + 356660, + 386378, + 417978, + 450378, + 483578, + 517578, + 552378, + 587978, + 624378, + 661578, + 699578, + 738378, + 777978, + 818378, + 859578, + 901578, + 944378, + 987978, + 1032378, + 1077578, + 1123578, + 1170378, + 1217978, + 1266378, + 1315578, + 1365578, + 1416378, + 1467978, + 1520378, + 1573578, + 1627578, + 1682378, + 1737978, + 1794378, + 1851578, + 1909578, + 1968378, + 2027978, + 2088378, + 2149578, + 2211578, + 2274378, + 2337978, + 2402378, + 2467578, + 2533578, + 2600378, + 2667978, + 2736378, + 2805578, + 2875578, + 2875578 +] diff --git a/worlds/kh2/__init__.py b/worlds/kh2/__init__.py new file mode 100644 index 0000000000..a9b86fa25d --- /dev/null +++ b/worlds/kh2/__init__.py @@ -0,0 +1,315 @@ + + +from BaseClasses import Tutorial, ItemClassification + +from .Items import * +from .Locations import all_locations, setup_locations, exclusion_table +from .Names import ItemName, LocationName +from .OpenKH import patch_kh2 +from .Options import KH2_Options +from .Regions import create_regions, connect_regions +from .Rules import set_rules +from ..AutoWorld import World, WebWorld +from .logic import KH2Logic + + +class KingdomHearts2Web(WebWorld): + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to playing Kingdom Hearts 2 Final Mix with Archipelago.", + "English", + "setup_en.md", + "setup/en", + ["JaredWeakStrike"] + )] + + +# noinspection PyUnresolvedReferences +class KH2World(World): + """ + Kingdom Hearts II is an action role-playing game developed and published by Square Enix and released in 2005. + It is the sequel to Kingdom Hearts and Kingdom Hearts: Chain of Memories, and like the two previous games, + focuses on Sora and his friends' continued battle against the Darkness. + """ + game: str = "Kingdom Hearts 2" + web = KingdomHearts2Web() + data_version = 1 + option_definitions = KH2_Options + item_name_to_id = {name: data.code for name, data in item_dictionary_table.items()} + location_name_to_id = {item_name: data.code for item_name, data in all_locations.items() if data.code} + item_name_groups = item_groups + + def __init__(self, multiworld: "MultiWorld", player: int): + super().__init__(multiworld, player) + self.BountiesRequired = None + self.BountiesAmount = None + self.hitlist = None + self.LocalItems = {} + self.RandomSuperBoss = list() + self.filler_items = list() + self.item_quantity_dict = {} + self.donald_ability_pool = list() + self.goofy_ability_pool = list() + self.sora_keyblade_ability_pool = list() + self.keyblade_slot_copy = list(Locations.Keyblade_Slots.keys()) + self.totalLocations = len(all_locations.items()) + self.growth_list = list() + for x in range(4): + self.growth_list.extend(Movement_Table.keys()) + self.visitlockingitem = list() + self.visitlockingitem.extend(Progression_Dicts["AllVisitLocking"]) + + def fill_slot_data(self) -> dict: + return {"hitlist": self.hitlist, + "LocalItems": self.LocalItems, + "Goal": self.multiworld.Goal[self.player].value, + "FinalXemnas": self.multiworld.FinalXemnas[self.player].value, + "LuckyEmblemsRequired": self.multiworld.LuckyEmblemsRequired[self.player].value, + "BountyRequired": self.multiworld.BountyRequired[self.player].value} + + def create_item(self, name: str, ) -> Item: + data = item_dictionary_table[name] + if name in Progression_Dicts["Progression"]: + item_classification = ItemClassification.progression + else: + item_classification = ItemClassification.filler + + created_item = KH2Item(name, item_classification, data.code, self.player) + + return created_item + + def generate_early(self) -> None: + # Item Quantity dict because Abilities can be a problem for KH2's Software. + for item, data in item_dictionary_table.items(): + self.item_quantity_dict[item] = data.quantity + + if self.multiworld.KeybladeAbilities[self.player] == "support": + self.sora_keyblade_ability_pool.extend(SupportAbility_Table.keys()) + elif self.multiworld.KeybladeAbilities[self.player] == "action": + self.sora_keyblade_ability_pool.extend(ActionAbility_Table.keys()) + else: + # both action and support on keyblades. + # TODO: make option to just exclude scom + self.sora_keyblade_ability_pool.extend(ActionAbility_Table.keys()) + self.sora_keyblade_ability_pool.extend(SupportAbility_Table.keys()) + + for item, value in self.multiworld.start_inventory[self.player].value.items(): + if item in ActionAbility_Table.keys() or item in SupportAbility_Table.keys(): + # cannot have more than the quantity for abilties + if value > item_dictionary_table[item].quantity: + print(f"{self.multiworld.get_file_safe_player_name(self.player)} cannot have more than {item_dictionary_table[item].quantity} of {item}") + value = item_dictionary_table[item].quantity + self.item_quantity_dict[item] -= value + + # Option to turn off Promise Charm Item + if not self.multiworld.Promise_Charm[self.player]: + self.item_quantity_dict[ItemName.PromiseCharm] = 0 + + for ability in self.multiworld.BlacklistKeyblade[self.player].value: + if ability in self.sora_keyblade_ability_pool: + self.sora_keyblade_ability_pool.remove(ability) + + # Option to turn off all superbosses. Can do this individually but its like 20+ checks + if not self.multiworld.SuperBosses[self.player] and not self.multiworld.Goal[self.player] == "hitlist": + for superboss in exclusion_table["Datas"]: + self.multiworld.exclude_locations[self.player].value.add(superboss) + for superboss in exclusion_table["SuperBosses"]: + self.multiworld.exclude_locations[self.player].value.add(superboss) + + # Option to turn off Olympus Colosseum Cups. + if self.multiworld.Cups[self.player] == "no_cups": + for cup in exclusion_table["Cups"]: + self.multiworld.exclude_locations[self.player].value.add(cup) + # exclude only hades paradox. If cups and hades paradox then nothing is excluded + elif self.multiworld.Cups[self.player] == "cups": + self.multiworld.exclude_locations[self.player].value.add(LocationName.HadesCupTrophyParadoxCups) + + if self.multiworld.Goal[self.player] == "lucky_emblem_hunt": + luckyemblemamount = self.multiworld.LuckyEmblemsAmount[self.player].value + luckyemblemrequired = self.multiworld.LuckyEmblemsRequired[self.player].value + if luckyemblemamount < luckyemblemrequired: + luckyemblemamount = max(luckyemblemamount, luckyemblemrequired) + print(f"Lucky Emblem Amount {self.multiworld.LuckyEmblemsAmount[self.player].value} is less than required \ + {self.multiworld.LuckyEmblemsRequired[self.player].value} for player {self.multiworld.get_file_safe_player_name(self.player)}") + self.item_quantity_dict[ItemName.LuckyEmblem] = item_dictionary_table[ItemName.LuckyEmblem].quantity + luckyemblemamount + # give this proof to unlock the final door once the player has the amount of lucky emblem required + self.item_quantity_dict[ItemName.ProofofNonexistence] = 0 + + # hitlist + elif self.multiworld.Goal[self.player] == "hitlist": + self.RandomSuperBoss.extend(exclusion_table["Hitlist"]) + self.BountiesAmount = self.multiworld.BountyAmount[self.player].value + self.BountiesRequired = self.multiworld.BountyRequired[self.player].value + + for location in self.multiworld.exclude_locations[self.player].value: + if location in self.RandomSuperBoss: + self.RandomSuperBoss.remove(location) + # Testing if the player has the right amount of Bounties for Completion. + if len(self.RandomSuperBoss) < self.BountiesAmount: + print(f"{self.multiworld.get_file_safe_player_name(self.player)} has too many bounties than bosses") + self.BountiesAmount = len(self.RandomSuperBoss) + self.multiworld.BountyAmount[self.player].value = len(self.RandomSuperBoss) + + if len(self.RandomSuperBoss) < self.BountiesRequired: + print(f"{self.multiworld.get_file_safe_player_name(self.player)} has too many required bounties") + self.BountiesRequired = len(self.RandomSuperBoss) + self.multiworld.BountyRequired[self.player].value = len(self.RandomSuperBoss) + + if self.BountiesAmount < self.BountiesRequired: + self.BountiesAmount = max(self.BountiesAmount, self.BountiesRequired) + print(f"Bounties Amount {self.multiworld.BountyAmount[self.player].value} is less than required \ + {self.multiworld.BountyRequired[self.player].value} for player {self.multiworld.get_file_safe_player_name(self.player)}") + + self.multiworld.start_hints[self.player].value.add(ItemName.Bounty) + self.item_quantity_dict[ItemName.ProofofNonexistence] = 0 + + while len(self.sora_keyblade_ability_pool) < len(self.keyblade_slot_copy): + self.sora_keyblade_ability_pool.append( + self.multiworld.per_slot_randoms[self.player].choice(list(SupportAbility_Table.keys()))) + + for item in DonaldAbility_Table.keys(): + data = self.item_quantity_dict[item] + for _ in range(data): + self.donald_ability_pool.append(item) + self.item_quantity_dict[item] = 0 + # 32 is the amount of donald abilities + while len(self.donald_ability_pool) < 32: + self.donald_ability_pool.append(self.multiworld.per_slot_randoms[self.player].choice(self.donald_ability_pool)) + + for item in GoofyAbility_Table.keys(): + data = self.item_quantity_dict[item] + for _ in range(data): + self.goofy_ability_pool.append(item) + self.item_quantity_dict[item] = 0 + # 32 is the amount of goofy abilities + while len(self.goofy_ability_pool) < 33: + self.goofy_ability_pool.append(self.multiworld.per_slot_randoms[self.player].choice(self.goofy_ability_pool)) + + def generate_basic(self): + itempool: typing.List[KH2Item] = [] + + self.hitlist = list() + self.filler_items.extend(item_groups["Filler"]) + + if self.multiworld.FinalXemnas[self.player]: + self.multiworld.get_location(LocationName.FinalXemnas, self.player).place_locked_item( + self.create_item(ItemName.Victory)) + else: + self.multiworld.get_location(LocationName.FinalXemnas, self.player).place_locked_item( + self.create_item(self.multiworld.per_slot_randoms[self.player].choice(self.filler_items))) + self.totalLocations -= 1 + + if self.multiworld.Goal[self.player] == "hitlist": + for bounty in range(self.BountiesAmount): + randomBoss = self.multiworld.per_slot_randoms[self.player].choice(self.RandomSuperBoss) + self.multiworld.get_location(randomBoss, self.player).place_locked_item( + self.create_item(ItemName.Bounty)) + self.hitlist.append(self.location_name_to_id[randomBoss]) + self.RandomSuperBoss.remove(randomBoss) + self.totalLocations -= 1 + + # plando keyblades because they can only have abilities + for keyblade in self.keyblade_slot_copy: + random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.sora_keyblade_ability_pool) + self.multiworld.get_location(keyblade, self.player).place_locked_item(self.create_item(random_ability)) + self.item_quantity_dict[random_ability] -= 1 + self.sora_keyblade_ability_pool.remove(random_ability) + self.totalLocations -= 1 + + # Placing Donald Abilities on donald locations + for donaldLocation in Locations.Donald_Checks.keys(): + random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.donald_ability_pool) + self.multiworld.get_location(donaldLocation, self.player).place_locked_item( + self.create_item(random_ability)) + self.totalLocations -= 1 + self.donald_ability_pool.remove(random_ability) + + # Placing Goofy Abilities on goofy locations + for goofyLocation in Locations.Goofy_Checks.keys(): + random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.goofy_ability_pool) + self.multiworld.get_location(goofyLocation, self.player).place_locked_item(self.create_item(random_ability)) + self.totalLocations -= 1 + self.goofy_ability_pool.remove(random_ability) + + # same item placed because you can only get one of these 2 locations + # they are both under the same flag so the player gets both locations just one of the two items + random_stt_item = self.multiworld.per_slot_randoms[self.player].choice(self.filler_items) + self.multiworld.get_location(LocationName.JunkChampionBelt, self.player).place_locked_item( + self.create_item(random_stt_item)) + self.multiworld.get_location(LocationName.JunkMedal, self.player).place_locked_item( + self.create_item(random_stt_item)) + self.totalLocations -= 2 + + if self.multiworld.Schmovement[self.player] != "level_0": + for _ in range(self.multiworld.Schmovement[self.player].value): + for name in {ItemName.HighJump, ItemName.QuickRun, ItemName.DodgeRoll, ItemName.AerialDodge, + ItemName.Glide}: + self.item_quantity_dict[name] -= 1 + self.growth_list.remove(name) + self.multiworld.push_precollected(self.create_item(name)) + + if self.multiworld.RandomGrowth[self.player] != 0: + max_growth = min(self.multiworld.RandomGrowth[self.player].value, len(self.growth_list)) + for _ in range(max_growth): + random_growth = self.multiworld.per_slot_randoms[self.player].choice(self.growth_list) + self.item_quantity_dict[random_growth] -= 1 + self.growth_list.remove(random_growth) + self.multiworld.push_precollected(self.create_item(random_growth)) + + # no visit locking + if self.multiworld.Visitlocking[self.player] == "no_visit_locking": + for item in self.visitlockingitem: + self.multiworld.push_precollected(self.create_item(item)) + self.item_quantity_dict[item] -= 1 + self.visitlockingitem.remove(item) + + # first and second visit locking + elif self.multiworld.Visitlocking[self.player] == "second_visit_locking": + for item in Progression_Dicts["2VisitLocking"]: + self.item_quantity_dict[item] -= 1 + self.multiworld.push_precollected(self.create_item(item)) + self.visitlockingitem.remove(item) + + for _ in range(self.multiworld.RandomVisitLockingItem[self.player].value): + if len(self.visitlockingitem) <= 0: + break + item = self.multiworld.per_slot_randoms[self.player].choice(self.visitlockingitem) + self.item_quantity_dict[item] -= 1 + self.multiworld.push_precollected(self.create_item(item)) + self.visitlockingitem.remove(item) + + # there are levels but level 1 is there to keep code clean + if self.multiworld.LevelDepth[self.player] == "level_99_sanity": + # level 99 sanity + self.totalLocations -= 1 + elif self.multiworld.LevelDepth[self.player] == "level_50_sanity": + # level 50 sanity + self.totalLocations -= 50 + elif self.multiworld.LevelDepth[self.player] == "level_1": + # level 1. No checks on levels + self.totalLocations -= 99 + else: + # level 50/99 since they contain the same amount of levels + self.totalLocations -= 76 + + for item in item_dictionary_table: + data = self.item_quantity_dict[item] + for _ in range(data): + itempool.append(self.create_item(item)) + + # Creating filler for unfilled locations + while len(itempool) < self.totalLocations: + item = self.multiworld.per_slot_randoms[self.player].choice(self.filler_items) + itempool += [self.create_item(item)] + self.multiworld.itempool += itempool + + def create_regions(self): + location_table = setup_locations() + create_regions(self.multiworld, self.player, location_table) + connect_regions(self.multiworld, self.player) + + def set_rules(self): + set_rules(self.multiworld, self.player) + + def generate_output(self, output_directory: str): + patch_kh2(self, output_directory) diff --git a/worlds/kh2/docs/en_Kingdom Hearts 2.md b/worlds/kh2/docs/en_Kingdom Hearts 2.md new file mode 100644 index 0000000000..da0a21efcd --- /dev/null +++ b/worlds/kh2/docs/en_Kingdom Hearts 2.md @@ -0,0 +1,105 @@ +# Kingdom Hearts 2 + +

Changes from the vanilla game

+ +This randomizer takes Kingdom Hearts 2 and randomizes the locations of the items for a more dynamic play experience. The items that randomize currently are all items within Chests, Popups, Get Bonuses, Form Levels, and Sora's Levels. This allows abilities that Sora would normally have to also be placed on Keyblades with random stats. With several options on ways to finish the game. + +

Where is the settings page

+ +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file. + + +

What is randomized in this game?

+ + +The Chests, Popups, Get Bonuses, Form Levels, and Sora's Levels. + +

What Kingdom Hearts 2 items can appear in other players' worlds?

+ + +Every item in the game with the exception being party members' abilities. + +

What is The Garden of Assemblage "GoA"?

+ + +The Garden of Assemblage Mod made by Sonicshadowsilver2 and Num turns the Garden of Assemblage into a “World Hub” where each portal takes you to one of the game worlds (as opposed to having a world map). This allows you to enter worlds at any time, and world progression is maintained for each world individually. + +

What does another world's item look like in Kingdom Hearts 2?

+ + +In Kingdom Hearts 2, items which need to be sent to other worlds appear in any location that has a item in the vanilla game. They are represented by the Archipelago icon, and must be "picked up" as if it were a normal item. Upon obtaining the item, it will be sent to its home world. + +

When the player receives an item, what happens?

+ + +It is added to your inventory. If you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable. + +

What Happens if I die before Room Saving?

+ + +When you die in Kingdom Hearts 2, you are reverted to the last non-boss room you entered and your status is reverted to what it was at that time. However, in archipelago, any item that you have sent/received will not be taken away from the player, unlike vanilla Kingdom Hearts 2. + + +For example, if you are fighting Roxas and you receive Reflect Element and you die fighting Roxas, you will keep that reflect. You will still need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable. + +

Customization options:

+ + +- Choose a goal from the list below (with an additional option to Kill Final Xemnas alongside your goal). + 1. Obtain Three Proofs. + 2. Obtain a desired amount of Lucky Emblems. + 3. Obtain a desired amount of Bounties that are on late locations. +- Customize how many World Locking Items You Need to Progress in that World. +- Customize the Amount of World Locking Items You Start With. +- Customize how many locations you want on Sora's Levels. +- Customize the EXP Multiplier of everything that affects Sora. +- Customize the Available Abilities on Keyblades. +- Customize the level of Progressive Movement (Growth Abilities) you start with. +- Customize the amount of Progressive Movement (Growth Abilities) you start with. +- Customize start inventory, i.e., begin every run with certain items or spells of your choice. + +

Quality of life:

+ + +With the help of Shananas, Num, and ZakTheRobot we have many QoL features such are: + + +- Faster Wardrobe. +- Faster Water Jafar Chase. +- Carpet Skip. +- Start with Lion Dash. +- Faster Urns. +- Removal of Absent Silhouette and go straight into the Data Fights. +- And much more can be found at [Kingdom Hearts 2 GoA Overview](https://tommadness.github.io/KH2Randomizer/overview/) + +

Recommendation

+ +- Recommended making a save at the start of the GoA before opening anything. This will be the recommended file to load if/when your game crashes. + - If you don't want to have a save in the GoA. Disconnect the client, load the auto save, and then reconnect the client after it loads the auto save. +- Recommended to set fps limit to 60fps. +- Recommended to run the game in windows/borderless windowed mode. Fullscreen is stable but the game can crash if you alt-tab out. +- Recommend viewing [Requirements/logic sheet](https://docs.google.com/spreadsheets/d/1Embae0t7pIrbzvX-NRywk7bTHHEvuFzzQBUUpSUL7Ak/edit?usp=sharing) + +

F.A.Q.

+ +- Why am I not getting magic? + - If you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable. +- Why am I missing worlds/portals in the GoA? + - You are missing the required visit locking item to access the world/portal. +- What versions of Kingdom Hearts 2 are supported? + - Currently `only` the most up to date version on the Epic Game Store is supported `1.0.0.8_WW`. Emulator may be added in the future. +- Why did I crash? + - The port of Kingdom Hearts 2 can and will randomly crash, this is the fault of the game not the randomizer or the archipelago client. + - If you have a continuous/constant crash (in the same area/event every time) you will want to reverify your installed files. This can be done by doing the following: Open Epic Game Store --> Library --> Click Triple Dots --> Manage --> Verify +- Why am I getting dummy items or letters? + - You will need to get the `JaredWeakStrike/APCompanion` (you can find how to get this in the setup guide) +- Why am I not sending or receiving items? + - Make sure you are connected to the KH2 client and the correct room (for more information reference the setup guide) +- Why did I not load in to the correct visit + - You need to trigger a cutscene or visit The World That Never Was for it to update you have recevied the item. +- Why should I install the auto save mod at `KH2FM-Mods-equations19/auto-save`? + - Because Kingdom Hearts 2 is prone to crashes and will keep you from losing your progress. +- How do I load an auto save? + - To load an auto-save, hold down the Select or your equivalent on your prefered controller while choosing a file. Make sure to hold the button down the whole time. +- How do I do a soft reset? + - Hold L1+L2+R1+R2+Start or your equivalent on your prefered controller at the same time to immediately reset the game to the start screen. \ No newline at end of file diff --git a/worlds/kh2/docs/setup_en.md b/worlds/kh2/docs/setup_en.md new file mode 100644 index 0000000000..e54d5c40b3 --- /dev/null +++ b/worlds/kh2/docs/setup_en.md @@ -0,0 +1,48 @@ +# Kingdom Hearts 2 Archipelago Setup Guide +

Quick Links

+ +- [Main Page](../../../../games/Kingdom%20Hearts%202/info/en) +- [Settings Page](../../../../games/Kingdom%20Hearts%202/player-settings) + +

Setting up the Mod Manager

+ +Follow this Guide [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/) + +

Loading A Seed

+ +When you generate a game you will see a download link for a KH2 .zip seed on the room page. Download the seed then open OpenKH Mod Manager and click the green plus and `Select and install Mod Archive`. Make sure the seed is on the top of the list (Highest Priority) + +

Archipelago Compainion Mod and recommended mods

+ +Load this mod just like the GoA ROM you did during the KH2 Rando setup. `JaredWeakStrike/APCompanion` Have this mod second highest priority below the .zip seed +Load this mod just like the GoA ROM you did during the KH2 Rando setup. `KH2FM-Mods-equations19/auto-save` Location doesn't matter, recommended in case of crashes. +Load this mod just like the GoA ROM you did during the KH2 Rando setup. `KH2FM-Mods-equations19/soft-reset` Location doesn't matter, recommneded in case of soft locks. + +

Using the KH2 Client

+ +Once you have started the game through OpenKH Mod Manager and are on the title screen run the ArchipelagoKH2Client.exe. When you successfully connect to the server the client will automatically hook into the game to send/receive checks. If the client ever loses connection to the game, it will also disconnect from the server and you will need to reconnect. Make sure the game is open whenever you try to connect the client to the server otherwise it will immediately disconnect you. Most checks will be sent to you anywhere outside of a load or cutscene but if you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable. + +

Generating a game

+ +

What is a YAML?

+ +YAML is the file format which Archipelago uses in order to configure a player's world. It allows you to dictate which +game you will be playing as well as the settings you would like for that game. + +YAML is a format very similar to JSON however it is made to be more human-readable. If you are ever unsure of the +validity of your YAML file you may check the file by uploading it to the check page on the Archipelago website. Check +page: [YAML Validation Page](/mysterycheck) + +

Creating a YAML

+ +YAML files may be generated on the Archipelago website by visiting the games page and clicking the "Settings Page" link +under any game. Clicking "Export Settings" in a game's settings page will download the YAML to your system. Games +page: [Archipelago Games List](/games) + +In a multiworld there must be one YAML per world. Any number of players can play on each world using either the game's +native coop system or using Archipelago's coop support. Each world will hold one slot in the multiworld and will have a +slot name and, if the relevant game requires it, files to associate it with that multiworld. + +If multiple people plan to play in one world cooperatively then they will only need one YAML for their coop world. If +each player is planning on playing their own game then they will each need a YAML. + diff --git a/worlds/kh2/logic.py b/worlds/kh2/logic.py new file mode 100644 index 0000000000..1c5883f5ce --- /dev/null +++ b/worlds/kh2/logic.py @@ -0,0 +1,312 @@ +from .Names import ItemName +from ..AutoWorld import LogicMixin + + +class KH2Logic(LogicMixin): + def kh_lod_unlocked(self, player, amount): + return self.has(ItemName.SwordoftheAncestor, player, amount) + + def kh_oc_unlocked(self, player, amount): + return self.has(ItemName.BattlefieldsofWar, player, amount) + + def kh_twtnw_unlocked(self, player, amount): + return self.has(ItemName.WaytotheDawn, player, amount) + + def kh_ht_unlocked(self, player, amount): + return self.has(ItemName.BoneFist, player, amount) + + def kh_tt_unlocked(self, player, amount): + return self.has(ItemName.IceCream, player, amount) + + def kh_pr_unlocked(self, player, amount): + return self.has(ItemName.SkillandCrossbones, player, amount) + + def kh_sp_unlocked(self, player, amount): + return self.has(ItemName.IdentityDisk, player, amount) + + def kh_stt_unlocked(self, player: int, amount): + return self.has(ItemName.NamineSketches, player, amount) + + # Using Dummy 13 for this + def kh_dc_unlocked(self, player: int, amount): + return self.has(ItemName.CastleKey, player, amount) + + def kh_hb_unlocked(self, player, amount): + return self.has(ItemName.MembershipCard, player, amount) + + def kh_pl_unlocked(self, player, amount): + return self.has(ItemName.ProudFang, player, amount) + + def kh_ag_unlocked(self, player, amount): + return self.has(ItemName.Scimitar, player, amount) + + def kh_bc_unlocked(self, player, amount): + return self.has(ItemName.BeastsClaw, player, amount) + + def kh_amount_of_forms(self, player, amount, requiredform="None"): + level = 0 + formList = [ItemName.ValorForm, ItemName.WisdomForm, ItemName.LimitForm, ItemName.MasterForm, + ItemName.FinalForm] + # required form is in the logic for region connections + if requiredform != "None": + formList.remove(requiredform) + for form in formList: + if self.has(form, player): + level += 1 + return level >= amount + + def kh_visit_locking_amount(self, player, amount): + visit = 0 + # torn pages are not added since you cannot get exp from that world + for item in {ItemName.CastleKey, ItemName.BattlefieldsofWar, ItemName.SwordoftheAncestor, ItemName.BeastsClaw, + ItemName.BoneFist, ItemName.ProudFang, ItemName.SkillandCrossbones, ItemName.Scimitar, + ItemName.MembershipCard, + ItemName.IceCream, ItemName.WaytotheDawn, + ItemName.IdentityDisk, ItemName.NamineSketches}: + visit += self.item_count(item, player) + return visit >= amount + + def kh_three_proof_unlocked(self, player): + return self.has(ItemName.ProofofConnection, player, 1) \ + and self.has(ItemName.ProofofNonexistence, player, 1) \ + and self.has(ItemName.ProofofPeace, player, 1) + + def kh_hitlist(self, player, amount): + return self.has(ItemName.Bounty, player, amount) + + def kh_lucky_emblem_unlocked(self, player, amount): + return self.has(ItemName.LuckyEmblem, player, amount) + + def kh_victory(self, player): + return self.has(ItemName.Victory, player, 1) + + def kh_summon(self, player, amount): + summonlevel = 0 + for summon in {ItemName.Genie, ItemName.ChickenLittle, ItemName.Stitch, ItemName.PeterPan}: + if self.has(summon, player): + summonlevel += 1 + return summonlevel >= amount + + # magic progression + def kh_fire(self, player): + return self.has(ItemName.FireElement, player, 1) + + def kh_fira(self, player): + return self.has(ItemName.FireElement, player, 2) + + def kh_firaga(self, player): + return self.has(ItemName.FireElement, player, 3) + + def kh_blizzard(self, player): + return self.has(ItemName.BlizzardElement, player, 1) + + def kh_blizzara(self, player): + return self.has(ItemName.BlizzardElement, player, 2) + + def kh_blizzaga(self, player): + return self.has(ItemName.BlizzardElement, player, 3) + + def kh_thunder(self, player): + return self.has(ItemName.ThunderElement, player, 1) + + def kh_thundara(self, player): + return self.has(ItemName.ThunderElement, player, 2) + + def kh_thundaga(self, player): + return self.has(ItemName.ThunderElement, player, 3) + + def kh_magnet(self, player): + return self.has(ItemName.MagnetElement, player, 1) + + def kh_magnera(self, player): + return self.has(ItemName.MagnetElement, player, 2) + + def kh_magnega(self, player): + return self.has(ItemName.MagnetElement, player, 3) + + def kh_reflect(self, player): + return self.has(ItemName.ReflectElement, player, 1) + + def kh_reflera(self, player): + return self.has(ItemName.ReflectElement, player, 2) + + def kh_reflega(self, player): + return self.has(ItemName.ReflectElement, player, 3) + + def kh_highjump(self, player, amount): + return self.has(ItemName.HighJump, player, amount) + + def kh_quickrun(self, player, amount): + return self.has(ItemName.QuickRun, player, amount) + + def kh_dodgeroll(self, player, amount): + return self.has(ItemName.DodgeRoll, player, amount) + + def kh_aerialdodge(self, player, amount): + return self.has(ItemName.AerialDodge, player, amount) + + def kh_glide(self, player, amount): + return self.has(ItemName.Glide, player, amount) + + def kh_comboplus(self, player, amount): + return self.has(ItemName.ComboPlus, player, amount) + + def kh_aircomboplus(self, player, amount): + return self.has(ItemName.AirComboPlus, player, amount) + + def kh_valorgenie(self, player): + return self.has(ItemName.Genie, player) and self.has(ItemName.ValorForm, player) + + def kh_wisdomgenie(self, player): + return self.has(ItemName.Genie, player) and self.has(ItemName.WisdomForm, player) + + def kh_mastergenie(self, player): + return self.has(ItemName.Genie, player) and self.has(ItemName.MasterForm, player) + + def kh_finalgenie(self, player): + return self.has(ItemName.Genie, player) and self.has(ItemName.FinalForm, player) + + def kh_rsr(self, player): + return self.has(ItemName.Slapshot, player, 1) and self.has(ItemName.ComboMaster, player) and self.kh_reflect( + player) + + def kh_gapcloser(self, player): + return self.has(ItemName.FlashStep, player, 1) or self.has(ItemName.SlideDash, player) + + # Crowd Control and Berserk Hori will be used when I add hard logic. + + def kh_crowdcontrol(self, player): + return self.kh_magnera(player) and self.has(ItemName.ChickenLittle, player) \ + or self.kh_magnega(player) and self.kh_mastergenie(player) + + def kh_berserkhori(self, player): + return self.has(ItemName.HorizontalSlash, player, 1) and self.has(ItemName.BerserkCharge, player) + + def kh_donaldlimit(self, player): + return self.has(ItemName.FlareForce, player, 1) or self.has(ItemName.Fantasia, player) + + def kh_goofylimit(self, player): + return self.has(ItemName.TornadoFusion, player, 1) or self.has(ItemName.Teamwork, player) + + def kh_basetools(self, player): + # TODO: if option is easy then add reflect,gap closer and second chance&once more. #option east scom option normal adds gap closer or combo master #hard is what is right now + return self.has(ItemName.Guard, player, 1) and self.has(ItemName.AerialRecovery, player, 1) \ + and self.has(ItemName.FinishingPlus, player, 1) + + def kh_roxastools(self, player): + return self.kh_basetools(player) and ( + self.has(ItemName.QuickRun, player) or self.has(ItemName.NegativeCombo, player, 2)) + + def kh_painandpanic(self, player): + return (self.kh_goofylimit(player) or self.kh_donaldlimit(player)) and self.kh_dc_unlocked(player, 2) + + def kh_cerberuscup(self, player): + return self.kh_amount_of_forms(player, 2) and self.kh_thundara(player) \ + and self.kh_ag_unlocked(player, 1) and self.kh_ht_unlocked(player, 1) \ + and self.kh_pl_unlocked(player, 1) + + def kh_titan(self, player: int): + return self.kh_summon(player, 2) and (self.kh_thundara(player) or self.kh_magnera(player)) \ + and self.kh_oc_unlocked(player, 2) + + def kh_gof(self, player): + return self.kh_titan(player) and self.kh_cerberuscup(player) \ + and self.kh_painandpanic(player) and self.kh_twtnw_unlocked(player, 1) + + def kh_dataroxas(self, player): + return self.kh_basetools(player) and \ + ((self.has(ItemName.LimitForm, player) and self.kh_amount_of_forms(player, 3) and self.has( + ItemName.TrinityLimit, player) and self.kh_gapcloser(player)) + or (self.has(ItemName.NegativeCombo, player, 2) or self.kh_quickrun(player, 2))) + + def kh_datamarluxia(self, player): + return self.kh_basetools(player) and self.kh_reflera(player) \ + and ((self.kh_amount_of_forms(player, 3) and self.has(ItemName.FinalForm, player) and self.kh_fira( + player)) or self.has(ItemName.NegativeCombo, player, 2) or self.kh_donaldlimit(player)) + + def kh_datademyx(self, player): + return self.kh_basetools(player) and self.kh_amount_of_forms(player, 5) and self.kh_firaga(player) \ + and (self.kh_donaldlimit(player) or self.kh_blizzard(player)) + + def kh_datalexaeus(self, player): + return self.kh_basetools(player) and self.kh_amount_of_forms(player, 3) and self.kh_reflera(player) \ + and (self.has(ItemName.NegativeCombo, player, 2) or self.kh_donaldlimit(player)) + + def kh_datasaix(self, player): + return self.kh_basetools(player) and (self.kh_thunder(player) or self.kh_blizzard(player)) \ + and self.kh_highjump(player, 2) and self.kh_aerialdodge(player, 2) and self.kh_glide(player, 2) and self.kh_amount_of_forms(player, 3) \ + and (self.kh_rsr(player) or self.has(ItemName.NegativeCombo, player, 2) or self.has(ItemName.PeterPan, + player)) + + def kh_dataxaldin(self, player): + return self.kh_basetools(player) and self.kh_donaldlimit(player) and self.kh_goofylimit(player) \ + and self.kh_highjump(player, 2) and self.kh_aerialdodge(player, 2) and self.kh_glide(player, + 2) and self.kh_magnet( + player) + # and (self.kh_form_level_unlocked(player, 3) or self.kh_berserkhori(player)) + + def kh_dataxemnas(self, player): + return self.kh_basetools(player) and self.kh_rsr(player) and self.kh_gapcloser(player) \ + and (self.has(ItemName.LimitForm, player) or self.has(ItemName.TrinityLimit, player)) + + def kh_dataxigbar(self, player): + return self.kh_basetools(player) and self.kh_donaldlimit(player) and self.has(ItemName.FinalForm, player) \ + and self.kh_amount_of_forms(player, 3) and self.kh_reflera(player) + + def kh_datavexen(self, player): + return self.kh_basetools(player) and self.kh_donaldlimit(player) and self.has(ItemName.FinalForm, player) \ + and self.kh_amount_of_forms(player, 4) and self.kh_reflera(player) and self.kh_fira(player) + + def kh_datazexion(self, player): + return self.kh_basetools(player) and self.kh_donaldlimit(player) and self.has(ItemName.FinalForm, player) \ + and self.kh_amount_of_forms(player, 3) \ + and self.kh_reflera(player) and self.kh_fira(player) + + def kh_dataaxel(self, player): + return self.kh_basetools(player) \ + and ((self.kh_reflera(player) and self.kh_blizzara(player)) or self.has(ItemName.NegativeCombo, player, 2)) + + def kh_dataluxord(self, player): + return self.kh_basetools(player) and self.kh_reflect(player) + + def kh_datalarxene(self, player): + return self.kh_basetools(player) and self.kh_reflera(player) \ + and ((self.has(ItemName.FinalForm, player) and self.kh_amount_of_forms(player, 4) and self.kh_fire( + player)) + or (self.kh_donaldlimit(player) and self.kh_amount_of_forms(player, 2))) + + def kh_sephi(self, player): + return self.kh_dataxemnas(player) + + def kh_onek(self, player): + return self.kh_reflect(player) or self.has(ItemName.Guard, player) + + def kh_terra(self, player): + return self.has(ItemName.ProofofConnection, player) and self.kh_basetools(player) \ + and self.kh_dodgeroll(player, 2) and self.kh_aerialdodge(player, 2) and self.kh_glide(player, 3) \ + and ((self.kh_comboplus(player, 2) and self.has(ItemName.Explosion, player)) or self.has( + ItemName.NegativeCombo, player, 2)) + + def kh_cor(self, player): + return self.kh_reflect(player) \ + and self.kh_highjump(player, 2) and self.kh_quickrun(player, 2) and self.kh_aerialdodge(player, 2) \ + and (self.has(ItemName.MasterForm, player) and self.kh_fire(player) + or (self.has(ItemName.ChickenLittle, player) and self.kh_donaldlimit(player) and self.kh_glide(player, + 2))) + + def kh_transport(self, player): + return self.kh_basetools(player) and self.kh_reflera(player) \ + and ((self.kh_mastergenie(player) and self.kh_magnera(player) and self.kh_donaldlimit(player)) + or (self.has(ItemName.FinalForm, player) and self.kh_amount_of_forms(player, 4) and self.kh_fira( + player))) + + def kh_gr2(self, player): + return (self.has(ItemName.MasterForm, player) or self.has(ItemName.Stitch, player)) \ + and (self.kh_fire(player) or self.kh_blizzard(player) or self.kh_thunder(player)) + + def kh_xaldin(self, player): + return self.kh_basetools(player) and (self.kh_donaldlimit(player) or self.kh_amount_of_forms(player, 1)) + + def kh_mcp(self, player): + return self.kh_reflect(player) and ( + self.has(ItemName.MasterForm, player) or self.has(ItemName.FinalForm, player)) diff --git a/worlds/kh2/mod_template/mod.yml b/worlds/kh2/mod_template/mod.yml new file mode 100644 index 0000000000..0ceda5181a --- /dev/null +++ b/worlds/kh2/mod_template/mod.yml @@ -0,0 +1,50 @@ +assets: +- method: binarc + multi: + - name: msg/us/jm.bar + - name: msg/uk/jm.bar + name: msg/jp/jm.bar + source: + - method: kh2msg + name: jm + source: + - language: en + name: jm.yml + type: list +- method: binarc + name: 00battle.bin + source: + - method: listpatch + name: fmlv + source: + - name: FmlvList.yml + type: fmlv + type: List + - method: listpatch + name: lvup + source: + - name: LvupList.yml + type: lvup + type: List + - method: listpatch + name: bons + source: + - name: BonsList.yml + type: bons + type: List +- method: binarc + name: 03system.bin + source: + - method: listpatch + name: trsr + source: + - name: TrsrList.yml + type: trsr + type: List + - method: listpatch + name: item + source: + - name: ItemList.yml + type: item + type: List +title: Randomizer Seed diff --git a/worlds/kh2/requirements.txt b/worlds/kh2/requirements.txt new file mode 100644 index 0000000000..14a8bddde2 --- /dev/null +++ b/worlds/kh2/requirements.txt @@ -0,0 +1 @@ +Pymem>=1.10.0 \ No newline at end of file diff --git a/worlds/kh2/test/TestGoal.py b/worlds/kh2/test/TestGoal.py new file mode 100644 index 0000000000..6cc63da334 --- /dev/null +++ b/worlds/kh2/test/TestGoal.py @@ -0,0 +1,27 @@ +from . import KH2TestBase +from ..Names import ItemName,LocationName,RegionName + +class TestDefault(KH2TestBase): + options = {} + + def testEverything(self): + self.collect_all_but([ItemName.Victory]) + self.assertBeatable(True) + + +class TestLuckyEmblem(KH2TestBase): + options = { + "Goal": 1, + } + + def testEverything(self): + self.collect_all_but([ItemName.LuckyEmblem]) + self.assertBeatable(True) + +class TestHitList(KH2TestBase): + options = { + "Goal": 2, + } + def testEverything(self): + self.collect_all_but([ItemName.Bounty]) + self.assertBeatable(True) diff --git a/worlds/kh2/test/__init__.py b/worlds/kh2/test/__init__.py new file mode 100644 index 0000000000..dfef227627 --- /dev/null +++ b/worlds/kh2/test/__init__.py @@ -0,0 +1,5 @@ +from test.TestBase import WorldTestBase + + +class KH2TestBase(WorldTestBase): + game = "Kingdom Hearts 2" From 81a239325d7577c64782d1692ec2fd675aa010ec Mon Sep 17 00:00:00 2001 From: zig-for Date: Tue, 21 Mar 2023 01:26:03 +0900 Subject: [PATCH 071/172] Links Awakening: Implement New Game (#1334) Adds Link's Awakening: DX. Fully imports and forks LADXR, with permission - https://github.com/daid/LADXR --- Launcher.py | 2 + LinksAwakeningClient.py | 609 ++++++++++ Utils.py | 3 + data/sprites/ladx/Bowwow.bdiff | Bin 0 -> 7354 bytes data/sprites/ladx/Bunny.bdiff | Bin 0 -> 3083 bytes data/sprites/ladx/Luigi.bdiff | Bin 0 -> 13112 bytes data/sprites/ladx/Mario.bdiff | Bin 0 -> 9025 bytes data/sprites/ladx/Matty_LA.bdiff | Bin 0 -> 4433 bytes data/sprites/ladx/Richard.bdiff | Bin 0 -> 8207 bytes data/sprites/ladx/Tarin.bdiff | Bin 0 -> 8309 bytes host.yaml | 5 + inno_setup.iss | 45 +- worlds/ladx/Common.py | 2 + worlds/ladx/GpsTracker.py | 92 ++ worlds/ladx/ItemTracker.py | 283 +++++ worlds/ladx/Items.py | 304 +++++ worlds/ladx/LADXR/.tinyci | 18 + worlds/ladx/LADXR/LADXR_LICENSE | 21 + worlds/ladx/LADXR/README.md | 25 + worlds/ladx/LADXR/assembler.py | 845 ++++++++++++++ worlds/ladx/LADXR/backgroundEditor.py | 69 ++ worlds/ladx/LADXR/checkMetadata.py | 270 +++++ worlds/ladx/LADXR/entityData.py | 561 +++++++++ worlds/ladx/LADXR/entranceInfo.py | 136 +++ worlds/ladx/LADXR/generator.py | 427 +++++++ worlds/ladx/LADXR/getGFX.py | 41 + worlds/ladx/LADXR/hints.py | 66 ++ worlds/ladx/LADXR/itempool.py | 278 +++++ worlds/ladx/LADXR/locations/all.py | 26 + worlds/ladx/LADXR/locations/anglerKey.py | 6 + worlds/ladx/LADXR/locations/beachSword.py | 32 + worlds/ladx/LADXR/locations/birdKey.py | 23 + worlds/ladx/LADXR/locations/boomerangGuy.py | 94 ++ worlds/ladx/LADXR/locations/chest.py | 50 + worlds/ladx/LADXR/locations/constants.py | 131 +++ worlds/ladx/LADXR/locations/droppedKey.py | 57 + worlds/ladx/LADXR/locations/faceKey.py | 6 + .../ladx/LADXR/locations/fishingMinigame.py | 13 + worlds/ladx/LADXR/locations/goldLeaf.py | 12 + worlds/ladx/LADXR/locations/heartContainer.py | 15 + worlds/ladx/LADXR/locations/heartPiece.py | 12 + worlds/ladx/LADXR/locations/hookshot.py | 18 + worlds/ladx/LADXR/locations/instrument.py | 9 + worlds/ladx/LADXR/locations/itemInfo.py | 43 + worlds/ladx/LADXR/locations/items.py | 127 ++ worlds/ladx/LADXR/locations/keyLocation.py | 18 + worlds/ladx/LADXR/locations/madBatter.py | 23 + worlds/ladx/LADXR/locations/owlStatue.py | 41 + worlds/ladx/LADXR/locations/seashell.py | 14 + worlds/ladx/LADXR/locations/shop.py | 42 + worlds/ladx/LADXR/locations/song.py | 5 + worlds/ladx/LADXR/locations/startItem.py | 38 + worlds/ladx/LADXR/locations/toadstool.py | 18 + worlds/ladx/LADXR/locations/tradeSequence.py | 55 + worlds/ladx/LADXR/locations/tunicFairy.py | 27 + worlds/ladx/LADXR/locations/witch.py | 31 + worlds/ladx/LADXR/logic/__init__.py | 284 +++++ worlds/ladx/LADXR/logic/dungeon1.py | 46 + worlds/ladx/LADXR/logic/dungeon2.py | 62 + worlds/ladx/LADXR/logic/dungeon3.py | 89 ++ worlds/ladx/LADXR/logic/dungeon4.py | 81 ++ worlds/ladx/LADXR/logic/dungeon5.py | 89 ++ worlds/ladx/LADXR/logic/dungeon6.py | 65 ++ worlds/ladx/LADXR/logic/dungeon7.py | 65 ++ worlds/ladx/LADXR/logic/dungeon8.py | 107 ++ worlds/ladx/LADXR/logic/dungeonColor.py | 49 + worlds/ladx/LADXR/logic/location.py | 57 + worlds/ladx/LADXR/logic/overworld.py | 682 +++++++++++ worlds/ladx/LADXR/logic/requirements.py | 318 +++++ worlds/ladx/LADXR/main.py | 52 + worlds/ladx/LADXR/mapgen/__init__.py | 147 +++ worlds/ladx/LADXR/mapgen/enemygen.py | 59 + worlds/ladx/LADXR/mapgen/imagegenerator.py | 95 ++ worlds/ladx/LADXR/mapgen/locationgen.py | 203 ++++ worlds/ladx/LADXR/mapgen/locations/base.py | 24 + worlds/ladx/LADXR/mapgen/locations/chest.py | 73 ++ .../ladx/LADXR/mapgen/locations/entrance.py | 107 ++ .../LADXR/mapgen/locations/entrance_info.py | 341 ++++++ .../ladx/LADXR/mapgen/locations/seashell.py | 172 +++ worlds/ladx/LADXR/mapgen/logic.py | 146 +++ worlds/ladx/LADXR/mapgen/map.py | 231 ++++ worlds/ladx/LADXR/mapgen/roomgen.py | 78 ++ worlds/ladx/LADXR/mapgen/roomtype/base.py | 54 + worlds/ladx/LADXR/mapgen/roomtype/forest.py | 28 + worlds/ladx/LADXR/mapgen/roomtype/mountain.py | 38 + worlds/ladx/LADXR/mapgen/roomtype/town.py | 16 + worlds/ladx/LADXR/mapgen/roomtype/water.py | 30 + worlds/ladx/LADXR/mapgen/tileset.py | 253 ++++ worlds/ladx/LADXR/mapgen/util.py | 5 + worlds/ladx/LADXR/mapgen/wfc.py | 250 ++++ worlds/ladx/LADXR/patches/aesthetics.py | 436 +++++++ worlds/ladx/LADXR/patches/bank34.py | 125 ++ .../ladx/LADXR/patches/bank3e.asm/bowwow.asm | 303 +++++ .../ladx/LADXR/patches/bank3e.asm/chest.asm | 993 ++++++++++++++++ .../LADXR/patches/bank3e.asm/itemnames.asm | 494 ++++++++ worlds/ladx/LADXR/patches/bank3e.asm/link.asm | 89 ++ .../ladx/LADXR/patches/bank3e.asm/message.asm | 16 + .../LADXR/patches/bank3e.asm/multiworld.asm | 355 ++++++ worlds/ladx/LADXR/patches/bank3e.asm/owl.asm | 63 + worlds/ladx/LADXR/patches/bank3e.py | 225 ++++ worlds/ladx/LADXR/patches/bank3f.py | 386 ++++++ worlds/ladx/LADXR/patches/bingo.py | 1036 +++++++++++++++++ worlds/ladx/LADXR/patches/bomb.py | 20 + worlds/ladx/LADXR/patches/bowwow.py | 207 ++++ worlds/ladx/LADXR/patches/chest.py | 59 + worlds/ladx/LADXR/patches/core.py | 539 +++++++++ worlds/ladx/LADXR/patches/desert.py | 7 + worlds/ladx/LADXR/patches/droppedKey.py | 134 +++ worlds/ladx/LADXR/patches/dungeon.py | 129 ++ worlds/ladx/LADXR/patches/endscreen.py | 139 +++ worlds/ladx/LADXR/patches/enemies.py | 462 ++++++++ worlds/ladx/LADXR/patches/entrances.py | 58 + worlds/ladx/LADXR/patches/fishingMinigame.py | 19 + worlds/ladx/LADXR/patches/goal.py | 317 +++++ worlds/ladx/LADXR/patches/goldenLeaf.py | 34 + worlds/ladx/LADXR/patches/hardMode.py | 64 + worlds/ladx/LADXR/patches/health.py | 33 + worlds/ladx/LADXR/patches/heartPiece.py | 42 + worlds/ladx/LADXR/patches/instrument.py | 24 + worlds/ladx/LADXR/patches/inventory.py | 421 +++++++ worlds/ladx/LADXR/patches/madBatter.py | 42 + worlds/ladx/LADXR/patches/maptweaks.py | 27 + worlds/ladx/LADXR/patches/multiworld.py | 308 +++++ worlds/ladx/LADXR/patches/music.py | 27 + worlds/ladx/LADXR/patches/nyan.bin | Bin 0 -> 5760 bytes worlds/ladx/LADXR/patches/overworld.py | 224 ++++ .../ladx/LADXR/patches/overworld/dive/00.json | 124 ++ .../ladx/LADXR/patches/overworld/dive/01.json | 102 ++ .../ladx/LADXR/patches/overworld/dive/06.json | 113 ++ .../ladx/LADXR/patches/overworld/dive/16.json | 91 ++ .../ladx/LADXR/patches/overworld/dive/62.json | 91 ++ .../ladx/LADXR/patches/overworld/dive/6C.json | 91 ++ .../ladx/LADXR/patches/overworld/dive/71.json | 91 ++ .../ladx/LADXR/patches/overworld/dive/72.json | 80 ++ .../ladx/LADXR/patches/overworld/dive/73.json | 113 ++ .../ladx/LADXR/patches/overworld/dive/81.json | 91 ++ .../ladx/LADXR/patches/overworld/dive/82.json | 91 ++ .../ladx/LADXR/patches/overworld/dive/83.json | 91 ++ .../ladx/LADXR/patches/overworld/dive/91.json | 91 ++ .../ladx/LADXR/patches/overworld/dive/92.json | 102 ++ .../ladx/LADXR/patches/overworld/dive/93.json | 91 ++ .../ladx/LADXR/patches/overworld/dive/A1.json | 91 ++ .../ladx/LADXR/patches/overworld/dive/A2.json | 113 ++ .../ladx/LADXR/patches/overworld/dive/A3.json | 91 ++ .../ladx/LADXR/patches/overworld/dive/B0.json | 80 ++ .../ladx/LADXR/patches/overworld/dive/B1.json | 102 ++ .../ladx/LADXR/patches/overworld/dive/B2.json | 91 ++ .../ladx/LADXR/patches/overworld/dive/B3.json | 91 ++ worlds/ladx/LADXR/patches/owl.py | 144 +++ worlds/ladx/LADXR/patches/phone.py | 60 + worlds/ladx/LADXR/patches/photographer.py | 19 + worlds/ladx/LADXR/patches/reduceRNG.py | 9 + worlds/ladx/LADXR/patches/rooster.py | 40 + worlds/ladx/LADXR/patches/save.py | 54 + worlds/ladx/LADXR/patches/seashell.py | 64 + worlds/ladx/LADXR/patches/shop.py | 152 +++ worlds/ladx/LADXR/patches/softlock.py | 93 ++ worlds/ladx/LADXR/patches/songs.py | 159 +++ worlds/ladx/LADXR/patches/tarin.py | 51 + worlds/ladx/LADXR/patches/titleScreen.py | 89 ++ worlds/ladx/LADXR/patches/tradeSequence.py | 355 ++++++ worlds/ladx/LADXR/patches/trendy.py | 3 + worlds/ladx/LADXR/patches/tunicFairy.py | 45 + worlds/ladx/LADXR/patches/weapons.py | 64 + worlds/ladx/LADXR/patches/witch.py | 58 + worlds/ladx/LADXR/plan.py | 38 + worlds/ladx/LADXR/pointerTable.py | 207 ++++ worlds/ladx/LADXR/rom.py | 75 ++ worlds/ladx/LADXR/romTables.py | 219 ++++ worlds/ladx/LADXR/roomEditor.py | 584 ++++++++++ worlds/ladx/LADXR/settings.py | 321 +++++ worlds/ladx/LADXR/utils.py | 222 ++++ worlds/ladx/LADXR/worldSetup.py | 136 +++ worlds/ladx/Locations.py | 247 ++++ worlds/ladx/Options.py | 366 ++++++ worlds/ladx/Rom.py | 40 + worlds/ladx/Tracker.py | 236 ++++ worlds/ladx/__init__.py | 418 +++++++ worlds/ladx/docs/en_Links Awakening DX.md | 79 ++ worlds/ladx/docs/setup_en.md | 93 ++ 180 files changed, 24191 insertions(+), 2 deletions(-) create mode 100644 LinksAwakeningClient.py create mode 100644 data/sprites/ladx/Bowwow.bdiff create mode 100644 data/sprites/ladx/Bunny.bdiff create mode 100644 data/sprites/ladx/Luigi.bdiff create mode 100644 data/sprites/ladx/Mario.bdiff create mode 100644 data/sprites/ladx/Matty_LA.bdiff create mode 100644 data/sprites/ladx/Richard.bdiff create mode 100644 data/sprites/ladx/Tarin.bdiff create mode 100644 worlds/ladx/Common.py create mode 100644 worlds/ladx/GpsTracker.py create mode 100644 worlds/ladx/ItemTracker.py create mode 100644 worlds/ladx/Items.py create mode 100644 worlds/ladx/LADXR/.tinyci create mode 100644 worlds/ladx/LADXR/LADXR_LICENSE create mode 100644 worlds/ladx/LADXR/README.md create mode 100644 worlds/ladx/LADXR/assembler.py create mode 100644 worlds/ladx/LADXR/backgroundEditor.py create mode 100644 worlds/ladx/LADXR/checkMetadata.py create mode 100644 worlds/ladx/LADXR/entityData.py create mode 100644 worlds/ladx/LADXR/entranceInfo.py create mode 100644 worlds/ladx/LADXR/generator.py create mode 100644 worlds/ladx/LADXR/getGFX.py create mode 100644 worlds/ladx/LADXR/hints.py create mode 100644 worlds/ladx/LADXR/itempool.py create mode 100644 worlds/ladx/LADXR/locations/all.py create mode 100644 worlds/ladx/LADXR/locations/anglerKey.py create mode 100644 worlds/ladx/LADXR/locations/beachSword.py create mode 100644 worlds/ladx/LADXR/locations/birdKey.py create mode 100644 worlds/ladx/LADXR/locations/boomerangGuy.py create mode 100644 worlds/ladx/LADXR/locations/chest.py create mode 100644 worlds/ladx/LADXR/locations/constants.py create mode 100644 worlds/ladx/LADXR/locations/droppedKey.py create mode 100644 worlds/ladx/LADXR/locations/faceKey.py create mode 100644 worlds/ladx/LADXR/locations/fishingMinigame.py create mode 100644 worlds/ladx/LADXR/locations/goldLeaf.py create mode 100644 worlds/ladx/LADXR/locations/heartContainer.py create mode 100644 worlds/ladx/LADXR/locations/heartPiece.py create mode 100644 worlds/ladx/LADXR/locations/hookshot.py create mode 100644 worlds/ladx/LADXR/locations/instrument.py create mode 100644 worlds/ladx/LADXR/locations/itemInfo.py create mode 100644 worlds/ladx/LADXR/locations/items.py create mode 100644 worlds/ladx/LADXR/locations/keyLocation.py create mode 100644 worlds/ladx/LADXR/locations/madBatter.py create mode 100644 worlds/ladx/LADXR/locations/owlStatue.py create mode 100644 worlds/ladx/LADXR/locations/seashell.py create mode 100644 worlds/ladx/LADXR/locations/shop.py create mode 100644 worlds/ladx/LADXR/locations/song.py create mode 100644 worlds/ladx/LADXR/locations/startItem.py create mode 100644 worlds/ladx/LADXR/locations/toadstool.py create mode 100644 worlds/ladx/LADXR/locations/tradeSequence.py create mode 100644 worlds/ladx/LADXR/locations/tunicFairy.py create mode 100644 worlds/ladx/LADXR/locations/witch.py create mode 100644 worlds/ladx/LADXR/logic/__init__.py create mode 100644 worlds/ladx/LADXR/logic/dungeon1.py create mode 100644 worlds/ladx/LADXR/logic/dungeon2.py create mode 100644 worlds/ladx/LADXR/logic/dungeon3.py create mode 100644 worlds/ladx/LADXR/logic/dungeon4.py create mode 100644 worlds/ladx/LADXR/logic/dungeon5.py create mode 100644 worlds/ladx/LADXR/logic/dungeon6.py create mode 100644 worlds/ladx/LADXR/logic/dungeon7.py create mode 100644 worlds/ladx/LADXR/logic/dungeon8.py create mode 100644 worlds/ladx/LADXR/logic/dungeonColor.py create mode 100644 worlds/ladx/LADXR/logic/location.py create mode 100644 worlds/ladx/LADXR/logic/overworld.py create mode 100644 worlds/ladx/LADXR/logic/requirements.py create mode 100644 worlds/ladx/LADXR/main.py create mode 100644 worlds/ladx/LADXR/mapgen/__init__.py create mode 100644 worlds/ladx/LADXR/mapgen/enemygen.py create mode 100644 worlds/ladx/LADXR/mapgen/imagegenerator.py create mode 100644 worlds/ladx/LADXR/mapgen/locationgen.py create mode 100644 worlds/ladx/LADXR/mapgen/locations/base.py create mode 100644 worlds/ladx/LADXR/mapgen/locations/chest.py create mode 100644 worlds/ladx/LADXR/mapgen/locations/entrance.py create mode 100644 worlds/ladx/LADXR/mapgen/locations/entrance_info.py create mode 100644 worlds/ladx/LADXR/mapgen/locations/seashell.py create mode 100644 worlds/ladx/LADXR/mapgen/logic.py create mode 100644 worlds/ladx/LADXR/mapgen/map.py create mode 100644 worlds/ladx/LADXR/mapgen/roomgen.py create mode 100644 worlds/ladx/LADXR/mapgen/roomtype/base.py create mode 100644 worlds/ladx/LADXR/mapgen/roomtype/forest.py create mode 100644 worlds/ladx/LADXR/mapgen/roomtype/mountain.py create mode 100644 worlds/ladx/LADXR/mapgen/roomtype/town.py create mode 100644 worlds/ladx/LADXR/mapgen/roomtype/water.py create mode 100644 worlds/ladx/LADXR/mapgen/tileset.py create mode 100644 worlds/ladx/LADXR/mapgen/util.py create mode 100644 worlds/ladx/LADXR/mapgen/wfc.py create mode 100644 worlds/ladx/LADXR/patches/aesthetics.py create mode 100644 worlds/ladx/LADXR/patches/bank34.py create mode 100644 worlds/ladx/LADXR/patches/bank3e.asm/bowwow.asm create mode 100644 worlds/ladx/LADXR/patches/bank3e.asm/chest.asm create mode 100644 worlds/ladx/LADXR/patches/bank3e.asm/itemnames.asm create mode 100644 worlds/ladx/LADXR/patches/bank3e.asm/link.asm create mode 100644 worlds/ladx/LADXR/patches/bank3e.asm/message.asm create mode 100644 worlds/ladx/LADXR/patches/bank3e.asm/multiworld.asm create mode 100644 worlds/ladx/LADXR/patches/bank3e.asm/owl.asm create mode 100644 worlds/ladx/LADXR/patches/bank3e.py create mode 100644 worlds/ladx/LADXR/patches/bank3f.py create mode 100644 worlds/ladx/LADXR/patches/bingo.py create mode 100644 worlds/ladx/LADXR/patches/bomb.py create mode 100644 worlds/ladx/LADXR/patches/bowwow.py create mode 100644 worlds/ladx/LADXR/patches/chest.py create mode 100644 worlds/ladx/LADXR/patches/core.py create mode 100644 worlds/ladx/LADXR/patches/desert.py create mode 100644 worlds/ladx/LADXR/patches/droppedKey.py create mode 100644 worlds/ladx/LADXR/patches/dungeon.py create mode 100644 worlds/ladx/LADXR/patches/endscreen.py create mode 100644 worlds/ladx/LADXR/patches/enemies.py create mode 100644 worlds/ladx/LADXR/patches/entrances.py create mode 100644 worlds/ladx/LADXR/patches/fishingMinigame.py create mode 100644 worlds/ladx/LADXR/patches/goal.py create mode 100644 worlds/ladx/LADXR/patches/goldenLeaf.py create mode 100644 worlds/ladx/LADXR/patches/hardMode.py create mode 100644 worlds/ladx/LADXR/patches/health.py create mode 100644 worlds/ladx/LADXR/patches/heartPiece.py create mode 100644 worlds/ladx/LADXR/patches/instrument.py create mode 100644 worlds/ladx/LADXR/patches/inventory.py create mode 100644 worlds/ladx/LADXR/patches/madBatter.py create mode 100644 worlds/ladx/LADXR/patches/maptweaks.py create mode 100644 worlds/ladx/LADXR/patches/multiworld.py create mode 100644 worlds/ladx/LADXR/patches/music.py create mode 100644 worlds/ladx/LADXR/patches/nyan.bin create mode 100644 worlds/ladx/LADXR/patches/overworld.py create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/00.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/01.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/06.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/16.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/62.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/6C.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/71.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/72.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/73.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/81.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/82.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/83.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/91.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/92.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/93.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/A1.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/A2.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/A3.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/B0.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/B1.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/B2.json create mode 100644 worlds/ladx/LADXR/patches/overworld/dive/B3.json create mode 100644 worlds/ladx/LADXR/patches/owl.py create mode 100644 worlds/ladx/LADXR/patches/phone.py create mode 100644 worlds/ladx/LADXR/patches/photographer.py create mode 100644 worlds/ladx/LADXR/patches/reduceRNG.py create mode 100644 worlds/ladx/LADXR/patches/rooster.py create mode 100644 worlds/ladx/LADXR/patches/save.py create mode 100644 worlds/ladx/LADXR/patches/seashell.py create mode 100644 worlds/ladx/LADXR/patches/shop.py create mode 100644 worlds/ladx/LADXR/patches/softlock.py create mode 100644 worlds/ladx/LADXR/patches/songs.py create mode 100644 worlds/ladx/LADXR/patches/tarin.py create mode 100644 worlds/ladx/LADXR/patches/titleScreen.py create mode 100644 worlds/ladx/LADXR/patches/tradeSequence.py create mode 100644 worlds/ladx/LADXR/patches/trendy.py create mode 100644 worlds/ladx/LADXR/patches/tunicFairy.py create mode 100644 worlds/ladx/LADXR/patches/weapons.py create mode 100644 worlds/ladx/LADXR/patches/witch.py create mode 100644 worlds/ladx/LADXR/plan.py create mode 100644 worlds/ladx/LADXR/pointerTable.py create mode 100644 worlds/ladx/LADXR/rom.py create mode 100644 worlds/ladx/LADXR/romTables.py create mode 100644 worlds/ladx/LADXR/roomEditor.py create mode 100644 worlds/ladx/LADXR/settings.py create mode 100644 worlds/ladx/LADXR/utils.py create mode 100644 worlds/ladx/LADXR/worldSetup.py create mode 100644 worlds/ladx/Locations.py create mode 100644 worlds/ladx/Options.py create mode 100644 worlds/ladx/Rom.py create mode 100644 worlds/ladx/Tracker.py create mode 100644 worlds/ladx/__init__.py create mode 100644 worlds/ladx/docs/en_Links Awakening DX.md create mode 100644 worlds/ladx/docs/setup_en.md diff --git a/Launcher.py b/Launcher.py index b1cc0fb4ea..be6fbd76c8 100644 --- a/Launcher.py +++ b/Launcher.py @@ -134,6 +134,8 @@ components: Iterable[Component] = ( Component('SNI Client', 'SNIClient', file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3', '.apsmw', '.apl2ac')), + Component('Links Awakening DX Client', 'LinksAwakeningClient', + file_identifier=SuffixIdentifier('.apladx')), Component('LttP Adjuster', 'LttPAdjuster'), # Factorio Component('Factorio Client', 'FactorioClient'), diff --git a/LinksAwakeningClient.py b/LinksAwakeningClient.py new file mode 100644 index 0000000000..e0557e4af4 --- /dev/null +++ b/LinksAwakeningClient.py @@ -0,0 +1,609 @@ +import ModuleUpdate +ModuleUpdate.update() + +import Utils + +if __name__ == "__main__": + Utils.init_logging("LinksAwakeningContext", exception_logger="Client") + +import asyncio +import base64 +import binascii +import io +import logging +import select +import socket +import time +import typing +import urllib + +import colorama + + +from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger, + server_loop) +from NetUtils import ClientStatus +from worlds.ladx.Common import BASE_ID as LABaseID +from worlds.ladx.GpsTracker import GpsTracker +from worlds.ladx.ItemTracker import ItemTracker +from worlds.ladx.LADXR.checkMetadata import checkMetadataTable +from worlds.ladx.Locations import get_locations_to_id, meta_to_name +from worlds.ladx.Tracker import LocationTracker, MagpieBridge + +class GameboyException(Exception): + pass + + +class RetroArchDisconnectError(GameboyException): + pass + + +class InvalidEmulatorStateError(GameboyException): + pass + + +class BadRetroArchResponse(GameboyException): + pass + + +def magpie_logo(): + from kivy.uix.image import CoreImage + binary_data = """ +iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXN +SR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA +7DAcdvqGQAAADGSURBVDhPhVLBEcIwDHOYhjHCBuXHj2OTbAL8+ +MEGZIxOQ1CinOOk0Op0bmo7tlXXeR9FJMYDLOD9mwcLjQK7+hSZ +wgcWMZJOAGeGKtChNHFL0j+FZD3jSCuo0w7l03wDrWdg00C4/aW +eDEYNenuzPOfPspBnxf0kssE80vN0L8361j10P03DK4x6FHabuV +ear8fHme+b17rwSjbAXeUMLb+EVTV2QHm46MWQanmnydA98KsVS +XkV+qFpGQXrLhT/fqraQeQLuplpNH5g+WkAAAAASUVORK5CYII=""" + binary_data = base64.b64decode(binary_data) + data = io.BytesIO(binary_data) + return CoreImage(data, ext="png").texture + + +class LAClientConstants: + # Connector version + VERSION = 0x01 + # + # Memory locations of LADXR + ROMGameID = 0x0051 # 4 bytes + SlotName = 0x0134 + # Unused + # ROMWorldID = 0x0055 + # ROMConnectorVersion = 0x0056 + # RO: We should only act if this is higher then 6, as it indicates that the game is running normally + wGameplayType = 0xDB95 + # RO: Starts at 0, increases every time an item is received from the server and processed + wLinkSyncSequenceNumber = 0xDDF6 + wLinkStatusBits = 0xDDF7 # RW: + # Bit0: wLinkGive* contains valid data, set from script cleared from ROM. + wLinkHealth = 0xDB5A + wLinkGiveItem = 0xDDF8 # RW + wLinkGiveItemFrom = 0xDDF9 # RW + # All of these six bytes are unused, we can repurpose + # wLinkSendItemRoomHigh = 0xDDFA # RO + # wLinkSendItemRoomLow = 0xDDFB # RO + # wLinkSendItemTarget = 0xDDFC # RO + # wLinkSendItemItem = 0xDDFD # RO + # wLinkSendShopItem = 0xDDFE # RO, which item to send (1 based, order of the shop items) + # RO, which player to send to, but it's just the X position of the NPC used, so 0x18 is player 0 + # wLinkSendShopTarget = 0xDDFF + + + wRecvIndex = 0xDDFE # 0xDB58 + wCheckAddress = 0xC0FF - 0x4 + WRamCheckSize = 0x4 + WRamSafetyValue = bytearray([0]*WRamCheckSize) + + MinGameplayValue = 0x06 + MaxGameplayValue = 0x1A + VictoryGameplayAndSub = 0x0102 + + +class RAGameboy(): + cache = [] + cache_start = 0 + cache_size = 0 + last_cache_read = None + socket = None + + def __init__(self, address, port) -> None: + self.address = address + self.port = port + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + assert (self.socket) + self.socket.setblocking(False) + + def get_retroarch_version(self): + self.send(b'VERSION\n') + select.select([self.socket], [], []) + response_str, addr = self.socket.recvfrom(16) + return response_str.rstrip() + + def get_retroarch_status(self, timeout): + self.send(b'GET_STATUS\n') + select.select([self.socket], [], [], timeout) + response_str, addr = self.socket.recvfrom(1000, ) + return response_str.rstrip() + + def set_cache_limits(self, cache_start, cache_size): + self.cache_start = cache_start + self.cache_size = cache_size + + def send(self, b): + if type(b) is str: + b = b.encode('ascii') + self.socket.sendto(b, (self.address, self.port)) + + def recv(self): + select.select([self.socket], [], []) + response, _ = self.socket.recvfrom(4096) + return response + + async def async_recv(self): + response = await asyncio.get_event_loop().sock_recv(self.socket, 4096) + return response + + async def check_safe_gameplay(self, throw=True): + async def check_wram(): + check_values = await self.async_read_memory(LAClientConstants.wCheckAddress, LAClientConstants.WRamCheckSize) + + if check_values != LAClientConstants.WRamSafetyValue: + if throw: + raise InvalidEmulatorStateError() + return False + return True + + if not await check_wram(): + if throw: + raise InvalidEmulatorStateError() + return False + + gameplay_value = await self.async_read_memory(LAClientConstants.wGameplayType) + gameplay_value = gameplay_value[0] + # In gameplay or credits + if not (LAClientConstants.MinGameplayValue <= gameplay_value <= LAClientConstants.MaxGameplayValue) and gameplay_value != 0x1: + if throw: + logger.info("invalid emu state") + raise InvalidEmulatorStateError() + return False + if not await check_wram(): + return False + return True + + # We're sadly unable to update the whole cache at once + # as RetroArch only gives back some number of bytes at a time + # So instead read as big as chunks at a time as we can manage + async def update_cache(self): + # First read the safety address - if it's invalid, bail + self.cache = [] + + if not await self.check_safe_gameplay(): + return + + cache = [] + remaining_size = self.cache_size + while remaining_size: + block = await self.async_read_memory(self.cache_start + len(cache), remaining_size) + remaining_size -= len(block) + cache += block + + if not await self.check_safe_gameplay(): + return + + self.cache = cache + self.last_cache_read = time.time() + + async def read_memory_cache(self, addresses): + # TODO: can we just update once per frame? + if not self.last_cache_read or self.last_cache_read + 0.1 < time.time(): + await self.update_cache() + if not self.cache: + return None + assert (len(self.cache) == self.cache_size) + for address in addresses: + assert self.cache_start <= address <= self.cache_start + self.cache_size + r = {address: self.cache[address - self.cache_start] + for address in addresses} + return r + + async def async_read_memory_safe(self, address, size=1): + # whenever we do a read for a check, we need to make sure that we aren't reading + # garbage memory values - we also need to protect against reading a value, then the emulator resetting + # + # ...actually, we probably _only_ need the post check + + # Check before read + if not await self.check_safe_gameplay(): + return None + + # Do read + r = await self.async_read_memory(address, size) + + # Check after read + if not await self.check_safe_gameplay(): + return None + + return r + + def read_memory(self, address, size=1): + command = "READ_CORE_MEMORY" + + self.send(f'{command} {hex(address)} {size}\n') + response = self.recv() + + splits = response.decode().split(" ", 2) + + assert (splits[0] == command) + # Ignore the address for now + + # TODO: transform to bytes + if splits[2][:2] == "-1" or splits[0] != "READ_CORE_MEMORY": + raise BadRetroArchResponse() + return bytearray.fromhex(splits[2]) + + async def async_read_memory(self, address, size=1): + command = "READ_CORE_MEMORY" + + self.send(f'{command} {hex(address)} {size}\n') + response = await self.async_recv() + response = response[:-1] + splits = response.decode().split(" ", 2) + + assert (splits[0] == command) + # Ignore the address for now + + # TODO: transform to bytes + return bytearray.fromhex(splits[2]) + + def write_memory(self, address, bytes): + command = "WRITE_CORE_MEMORY" + + self.send(f'{command} {hex(address)} {" ".join(hex(b) for b in bytes)}') + select.select([self.socket], [], []) + response, _ = self.socket.recvfrom(4096) + + splits = response.decode().split(" ", 3) + + assert (splits[0] == command) + + if splits[2] == "-1": + logger.info(splits[3]) + + +class LinksAwakeningClient(): + socket = None + gameboy = None + tracker = None + auth = None + game_crc = None + pending_deathlink = False + deathlink_debounce = True + recvd_checks = {} + + def msg(self, m): + logger.info(m) + s = f"SHOW_MSG {m}\n" + self.gameboy.send(s) + + def __init__(self, retroarch_address="127.0.0.1", retroarch_port=55355): + self.gameboy = RAGameboy(retroarch_address, retroarch_port) + + async def wait_for_retroarch_connection(self): + logger.info("Waiting on connection to Retroarch...") + while True: + try: + version = self.gameboy.get_retroarch_version() + NO_CONTENT = b"GET_STATUS CONTENTLESS" + status = NO_CONTENT + core_type = None + GAME_BOY = b"game_boy" + while status == NO_CONTENT or core_type != GAME_BOY: + try: + status = self.gameboy.get_retroarch_status(0.1) + if status.count(b" ") < 2: + await asyncio.sleep(1.0) + continue + + GET_STATUS, PLAYING, info = status.split(b" ", 2) + if status.count(b",") < 2: + await asyncio.sleep(1.0) + continue + core_type, rom_name, self.game_crc = info.split(b",", 2) + if core_type != GAME_BOY: + logger.info( + f"Core type should be '{GAME_BOY}', found {core_type} instead - wrong type of ROM?") + await asyncio.sleep(1.0) + continue + except (BlockingIOError, TimeoutError) as e: + await asyncio.sleep(0.1) + pass + logger.info(f"Connected to Retroarch {version} {info}") + self.gameboy.read_memory(0x1000) + return + except ConnectionResetError: + await asyncio.sleep(1.0) + pass + + def reset_auth(self): + auth = binascii.hexlify(self.gameboy.read_memory(0x0134, 12)).decode() + + if self.auth: + assert (auth == self.auth) + + self.auth = auth + + async def wait_and_init_tracker(self): + await self.wait_for_game_ready() + self.tracker = LocationTracker(self.gameboy) + self.item_tracker = ItemTracker(self.gameboy) + self.gps_tracker = GpsTracker(self.gameboy) + + async def recved_item_from_ap(self, item_id, from_player, next_index): + # Don't allow getting an item until you've got your first check + if not self.tracker.has_start_item(): + return + + # Spin until we either: + # get an exception from a bad read (emu shut down or reset) + # beat the game + # the client handles the last pending item + status = (await self.gameboy.async_read_memory_safe(LAClientConstants.wLinkStatusBits))[0] + while not (await self.is_victory()) and status & 1 == 1: + time.sleep(0.1) + status = (await self.gameboy.async_read_memory_safe(LAClientConstants.wLinkStatusBits))[0] + + item_id -= LABaseID + # The player name table only goes up to 100, so don't go past that + # Even if it didn't, the remote player _index_ byte is just a byte, so 255 max + if from_player > 100: + from_player = 100 + + next_index += 1 + self.gameboy.write_memory(LAClientConstants.wLinkGiveItem, [ + item_id, from_player]) + status |= 1 + status = self.gameboy.write_memory(LAClientConstants.wLinkStatusBits, [status]) + self.gameboy.write_memory(LAClientConstants.wRecvIndex, [next_index]) + + async def wait_for_game_ready(self): + logger.info("Waiting on game to be in valid state...") + while not await self.gameboy.check_safe_gameplay(throw=False): + pass + logger.info("Ready!") + last_index = 0 + + async def is_victory(self): + return (await self.gameboy.read_memory_cache([LAClientConstants.wGameplayType]))[LAClientConstants.wGameplayType] == 1 + + async def main_tick(self, item_get_cb, win_cb, deathlink_cb): + await self.tracker.readChecks(item_get_cb) + await self.item_tracker.readItems() + await self.gps_tracker.read_location() + + next_index = self.gameboy.read_memory(LAClientConstants.wRecvIndex)[0] + if next_index != self.last_index: + self.last_index = next_index + # logger.info(f"Got new index {next_index}") + + current_health = (await self.gameboy.read_memory_cache([LAClientConstants.wLinkHealth]))[LAClientConstants.wLinkHealth] + if self.deathlink_debounce and current_health != 0: + self.deathlink_debounce = False + elif not self.deathlink_debounce and current_health == 0: + # logger.info("YOU DIED.") + await deathlink_cb() + self.deathlink_debounce = True + + if self.pending_deathlink: + logger.info("Got a deathlink") + self.gameboy.write_memory(LAClientConstants.wLinkHealth, [0]) + self.pending_deathlink = False + self.deathlink_debounce = True + + if await self.is_victory(): + await win_cb() + + recv_index = (await self.gameboy.async_read_memory_safe(LAClientConstants.wRecvIndex))[0] + + # Play back one at a time + if recv_index in self.recvd_checks: + item = self.recvd_checks[recv_index] + await self.recved_item_from_ap(item.item, item.player, recv_index) + + +all_tasks = set() + +def create_task_log_exception(awaitable) -> asyncio.Task: + async def _log_exception(awaitable): + try: + return await awaitable + except Exception as e: + logger.exception(e) + pass + finally: + all_tasks.remove(task) + task = asyncio.create_task(_log_exception(awaitable)) + all_tasks.add(task) + + +class LinksAwakeningContext(CommonContext): + tags = {"AP"} + game = "Links Awakening DX" + items_handling = 0b101 + want_slot_data = True + la_task = None + client = None + # TODO: does this need to re-read on reset? + found_checks = [] + last_resend = time.time() + + magpie = MagpieBridge() + magpie_task = None + won = False + + def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str]) -> None: + self.client = LinksAwakeningClient() + super().__init__(server_address, password) + + def run_gui(self) -> None: + import webbrowser + import kvui + from kvui import Button, GameManager + from kivy.uix.image import Image + + class LADXManager(GameManager): + logging_pairs = [ + ("Client", "Archipelago"), + ("Tracker", "Tracker"), + ] + base_title = "Archipelago Links Awakening DX Client" + + def build(self): + b = super().build() + + button = Button(text="", size=(30, 30), size_hint_x=None, + on_press=lambda _: webbrowser.open('https://magpietracker.us/?enable_autotracker=1')) + image = Image(size=(16, 16), texture=magpie_logo()) + button.add_widget(image) + + def set_center(_, center): + image.center = center + button.bind(center=set_center) + + self.connect_layout.add_widget(button) + return b + + self.ui = LADXManager(self) + self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + + async def send_checks(self): + message = [{"cmd": 'LocationChecks', "locations": self.found_checks}] + await self.send_msgs(message) + + ENABLE_DEATHLINK = False + async def send_deathlink(self): + if self.ENABLE_DEATHLINK: + message = [{"cmd": 'Deathlink', + 'time': time.time(), + 'cause': 'Had a nightmare', + # 'source': self.slot_info[self.slot].name, + }] + await self.send_msgs(message) + + async def send_victory(self): + if not self.won: + message = [{"cmd": "StatusUpdate", + "status": ClientStatus.CLIENT_GOAL}] + logger.info("victory!") + await self.send_msgs(message) + self.won = True + + async def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None: + if self.ENABLE_DEATHLINK: + self.client.pending_deathlink = True + + def new_checks(self, item_ids, ladxr_ids): + self.found_checks += item_ids + create_task_log_exception(self.send_checks()) + create_task_log_exception(self.magpie.send_new_checks(ladxr_ids)) + + async def server_auth(self, password_requested: bool = False): + if password_requested and not self.password: + await super(LinksAwakeningContext, self).server_auth(password_requested) + self.auth = self.client.auth + await self.get_username() + await self.send_connect() + + def on_package(self, cmd: str, args: dict): + if cmd == "Connected": + self.game = self.slot_info[self.slot].game + # TODO - use watcher_event + if cmd == "ReceivedItems": + for index, item in enumerate(args["items"], args["index"]): + self.client.recvd_checks[index] = item + + item_id_lookup = get_locations_to_id() + + async def run_game_loop(self): + def on_item_get(ladxr_checks): + checks = [self.item_id_lookup[meta_to_name( + checkMetadataTable[check.id])] for check in ladxr_checks] + self.new_checks(checks, [check.id for check in ladxr_checks]) + + async def victory(): + await self.send_victory() + + async def deathlink(): + await self.send_deathlink() + + self.magpie_task = asyncio.create_task(self.magpie.serve()) + + # yield to allow UI to start + await asyncio.sleep(0) + + while True: + try: + # TODO: cancel all client tasks + logger.info("(Re)Starting game loop") + self.found_checks.clear() + await self.client.wait_for_retroarch_connection() + self.client.reset_auth() + await self.client.wait_and_init_tracker() + + while True: + await self.client.main_tick(on_item_get, victory, deathlink) + await asyncio.sleep(0.1) + now = time.time() + if self.last_resend + 5.0 < now: + self.last_resend = now + await self.send_checks() + self.magpie.set_checks(self.client.tracker.all_checks) + await self.magpie.set_item_tracker(self.client.item_tracker) + await self.magpie.send_gps(self.client.gps_tracker) + + except GameboyException: + time.sleep(1.0) + pass + + +async def main(): + parser = get_base_parser(description="Link's Awakening Client.") + parser.add_argument("--url", help="Archipelago connection url") + + parser.add_argument('diff_file', default="", type=str, nargs="?", + help='Path to a .apladx Archipelago Binary Patch file') + args = parser.parse_args() + logger.info(args) + + if args.diff_file: + import Patch + logger.info("patch file was supplied - creating rom...") + meta, rom_file = Patch.create_rom_file(args.diff_file) + if "server" in meta: + args.url = meta["server"] + logger.info(f"wrote rom file to {rom_file}") + + if args.url: + url = urllib.parse.urlparse(args.url) + args.connect = url.netloc + if url.password: + args.password = urllib.parse.unquote(url.password) + + ctx = LinksAwakeningContext(args.connect, args.password) + + ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop") + + # TODO: nothing about the lambda about has to be in a lambda + ctx.la_task = create_task_log_exception(ctx.run_game_loop()) + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + + await ctx.exit_event.wait() + await ctx.shutdown() + +if __name__ == '__main__': + colorama.init() + asyncio.run(main()) + colorama.deinit() diff --git a/Utils.py b/Utils.py index f7305a1e2a..23acb9f180 100644 --- a/Utils.py +++ b/Utils.py @@ -260,6 +260,9 @@ def get_default_options() -> OptionsType: "lttp_options": { "rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc", }, + "ladx_options": { + "rom_file": "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc", + }, "server_options": { "host": None, "port": 38281, diff --git a/data/sprites/ladx/Bowwow.bdiff b/data/sprites/ladx/Bowwow.bdiff new file mode 100644 index 0000000000000000000000000000000000000000..bdfe9f42f23bf24c4ea8901ef9ba1dde5f4827a6 GIT binary patch literal 7354 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 XNBPQyn4D6uElTZHxZ+2Vu literal 0 HcmV?d00001 diff --git a/data/sprites/ladx/Luigi.bdiff b/data/sprites/ladx/Luigi.bdiff new file mode 100644 index 0000000000000000000000000000000000000000..1897a65d01463797e61e535ea26cb79882f42171 GIT binary patch literal 13112 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 literal 0 HcmV?d00001 diff --git a/data/sprites/ladx/Mario.bdiff b/data/sprites/ladx/Mario.bdiff new file mode 100644 index 0000000000000000000000000000000000000000..389fa94ab12862aef4cb035ff2ae688704ed25bd GIT binary patch literal 9025 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 XP>{Zp(BMK^XgL>hML1B900000cik=_ literal 0 HcmV?d00001 diff --git a/data/sprites/ladx/Richard.bdiff b/data/sprites/ladx/Richard.bdiff new file mode 100644 index 0000000000000000000000000000000000000000..a244d7026b42cfe902c28e77685720ed54ff2349 GIT binary patch literal 8207 zcmYkA2T&8t)`kN~=)n*|Z=rW-p%+6BkkCQ8G?6YKML;x^(2?E|si7!Rqzlq}uL6Q1 z0s_*D4aJ}DyLaaP@67JmIlHrGW@q-9caEBghK`mNQalC>_*d_N|A_!s;(r%vR<80A zs+N2vmhB&{eF4PSCx8E|roVr>lrN61zW=@dd%LUiiTR&UgD;1-7Cfdw?%%-H_V9B7 z=1K&>d76lj^xr%G8z2A(VEUgm&@ir49Z^W&%h#!N+s#;Us|+bL+Cl;@knDu;Jt6`Q zsfN*V|Dgq#B;B$r~I0z7RUF@HVkn8b3F%uP`;}E9bs+yQxLz0d5lu=&XD3T=7s-g=B z5PQS$hM_Wa+8Id_zDHUal6PI1s0aywK$t`F7gz)|hyl2KX!Q?#Pff$=z$3k=xTKjs zXbN>>e7dRut;YRGKF1GzKaS$B_LZJm@a7_p7(KU@c!d*xq0J4$O0Ot(D|hWKAB~gE zLcilRI*(6LdR%CjqpW0)UH?~6A!XmWxFVxUR}Yk1&Bgac zAnTMM{y8*Cdr?DR!w+Dbd*$x zembd9Ywj86jP%D1c4>z^m%%Q2((+A1p=bdFh!n1$=%cH{Hwz+B1)$QY2v4t~?y_Jxc zjuuoeGhm@90#!<6x|A>zL1JM}(0ac3qz1fi@d`o5U_x zPG%UOk?458%q&Si0u#MdUKAUbmu>vO?CWZxSM;E#7mGMzsi*rxRXPdoif z#tV-8jq??*qj!HO-svtO2N|rozm4FrJ(E(FoXpqk5Dd!jDrF`>q?rs%j zBv=xy+%hk93{qMG`M4-252aM`glAyv{fRe3(0X~?EB z7B^@!bQT8{qZidUDQEz|sNbrC7O}i(_ms5jZ>~zPenl%`8@A;+h+~_HJ?515we-+h zFJSaNWpPVH8MElE#x#uyPSHRfZgQN+BJul1qMDq{n6z0mXW0WSjD)!(2zQldDjl2) zA<=_Y+Gijk(HdUnlYKm?ksB}O>f0Wrm=8nrm=8e9gR!%QO~Ki>+-->eFRSvNfJ z*WvaHEl@*=tz~Q4yfkgsf8|NBEog1HAh`5L(QBUYyR=_VsR$=J$CjEMvmC5y?^v3s zKx%sLeLg)CuupkrmC3C{PvL?4{i&i3w*(jQM*2}QCiG|lZT0=T>&S6Uwc=sg@YK~c zf2B3mVKaR4LzE(_IwSt%77ft;Pj8e^w0uMl{{*m6sjnWxM2TAWIj5V}nJ7H|NZp$m zOVZpVu4m%x!&~?RgGzy|pG?WjT?wd&&-hBI`rAL|eR$kn!xHEtD8ZB@VR!MzQZ35795`ORe;8ISpexl+99jAKZsdjx6Yrwztv1-7o9M7tId8 zp*T-kBo~!wk$k7vFR;bs*`E^V(7x7?7|qRq(+mtE8PDLr_0eQTQ-J^iYO7#J+8i5 zKfI!Eoz_tUSs_&REx3>=n2N}x`T0bUAP^``ACnDlQN`qwARZGuIhf>8LdyTXHW!`N zE&%-TpRawbqyL2e%u|04{$FMt_8wQUh+{6d`2L`HVeP@2fC}xPMsG3twY9@{K^{TA zSH*Zj#vWs=a&+Vg7=}joFP|ebxX| zQMDHmjm;Q|Bh0TPA?M~<&>_)<1T1^$JaQ$Ey&~g8yC?_cR#6#(W3Q-mYmy;E8sf<} zF>|OHs?5U+#78A$9B&|qw12v!)%?@tbjWCaN2C5SB zn3+lb!4eV)V1_7b&68uHapGjeOh|VxBt+m_Tst^5#Typd$@l@T808#8^{0Y+ACh`-`(p9|B74#fy>dtB3(fv>9n9x&v0d$B;rD> z0bkrFWvyflTK5o8h;y>Zj568WtP?kub5DXjFJEU1Ibpz;KCdi|bafSv&O=T^kWNPF z^G-$2&)`dpz}&Bwf~heaxj6Ttl@C{k!`z~)iDndRwB8YfV&bn7lygF_at88_r;d^> z?L&^MPoswRza$4`dM4T!KXSXBK%wm%Jnlqf6o4C0C)g`!NO6SA&SHk5c3l}p7;b!I z7|-xo&`nOZCQ1@Wbd$G|%u*17rZY*EjMP-w(FF?@(+dk&Ws{jN*^OX?>Md=yDXPZe zu=v@OzYSXvL1A$OkDGtLE}U&=0S4L^`x7P=Z{)KqOAF1bMri%mTyEkjV?05}vaE32oMsi0TQu#0$IRm57Q7oPnV$qA$YW@Or zOEG593_6~>J+Q2#*UlkLQ<4Xhx~oq@EZ)QDc>|9MjKvwoNa%VNM{{Y~+aBN6*&d(k z9l;<>7M3#dr_%k&jO-tH@JW>mS1Rmt?$A|?>eHm&k*^t_ZhO!zU^TzfhoRPKM9Ak|XSztQ6Yk3WP z-bg>KmOIq!;to;n^gHGB?k^VwD%^z#h&5;<9uL!v>#4kB+Ohddtn-db&2syqvpFY? zY}Dt@2=y{_M*APh1?7Od;C#;M={|y{t9rk5A`#cf2tS%FPQ9Jga4v(A*inrn8hfQ7 zd#1tqp}gXJz3bDXaK zQ!v=1(@?|Ow>|W>$t_)_xd7SIpKSY|_Pz&1u}UQfGH4etzg8KCUv`cPdFK1fgsn-1 zH9A?i=NumPSLZyfyq#ZXR8px4*S@=6*t9$f%TZ(+*atr#ABYN4=#DfkIelZt-S(*d z$0@H~Yk_FN&Qbu9((Zxg-P#|9L%hlTuLV5fNt)!#B{iPX;3+5`HY6_BK8)t!Raaf) zo+(R9mI#$KmA{?4Z&T+#X~oTA>xXRj;LPFfR35|yDX6i-<3@95^3{)_OSs zOlLWkn0b${d40!*2FmZ>5`=wnU;}2H_BG~&OdI{mrN(;1Q=O>W4CV1_L&?aV@2_0! zI_F#CYhe7ZgThxnFFem~7EyhPRD7Xle;|6HMZUOJRzt#0nNb9j@LC7o3Y>^(bsL@Lvq?wSI%r<7;sm# zOm2MCN!q@A9;28D($+&YMUR~$`S15-Je@Sgq%f^kfS)CCREnG>kxvazLNubxgg<-1 z)84=Qp5NAZK*mkiY3YCb8=rHceGn;H$1kw!WdUt*#l+-_r?qOaX-}+OEZ4BAgqSNPM!L8 zHIH8p5}qezua&}Eq+k@?cl|+Nk#iE~!MZ$?Bw^o11HXIcP0xwnm0!&1XW~Yit@6@T zIEDRAS$2TB2dUuUUMKhg^)cm~fVA7pnYX~xkPpw3z{J6?IDLjSaAN2Eo3o5V`SVP! zk<{C@9wx_n%YCXkZbDxfcL>W`g%mG--6hG@=j0|3tcG{`3iE45CKZiLQ5Z z#sV*^3No_}=Bl6j(Rk{4fwGB8z##eGtkI97|De&FNXrTZ;%4dQ;H9EtC>ShAT*T>| z8=-m6<_^b_?x3_&hz%c}?`C3V)QiB!ey#G!4+|@9JzI0}G0`z%Dsj0-GZ2=N)rhtE zB#ZL>L7?&Us*8^uEA>a}pY11Yi|jOrWt}wE(&#&vgiK3$aM5Rg=h)taH<{&=BOl5k zH(aq(4(K!6x7C^RUZd3BI?aPoS9WibQtof8JY-|hf1HrEKk@EC31cJ~;Ut`_#AApI z>G76og3;&#`=j#Jn%0Vd2D*ic_83i?GLjr!wj0r~c2!VB1*vLuHu1d@bs~$>=ZOA;A$Vrtx_#mdXpa%CdjT2=Qi6-}S8`tTT zbYhO544*Zv2d&(t7VPeJC>KjSVOKR{)Nd_`nNl(laF7>%t^PR{WU^woy#A4dT9o`* z`07Dm^R_Sh4g-jj&W|=-sNP0Z$*oB@+#%*xHmSYu0&S})Ck|-FNv`c`6;quUXHF%%hzc= z?-|Y;bUFB9oT#go#o%gcj~Od)0<-hVX7n-K#;_qiV+3eE_BEy+t$s4yN@$qQYl+A; z`Bb;kK^MDnm|$6`Jc!-a=bBq(?cKMZv0Gf?A{kb(4ED3A+ML+xo%h)xGhssbQO#qp z1<|JRw$c1!(^KLAlkptU3?~l8zXjYnlRqCa0C}>vBu?Q2aJDo8eYz~o?q4QP{r4v# zuU86RKILtyUfyJ9zr~xJrs-cNAN*lxE9{ks-Mx^SQLnt93X`6<@T=;uJ0(@h8^oimcZA!!O;^ zdWq=Y3yQ~_il6o$G5#D~v0m@cI-(+ZqP*qrEBXvvltdr5(ny9<=;|>rXb3!_4o&tb znvavWe-712HrE9IM9NTo(u!vtvYLD&M#g>hs-5wp)}K|Y=+X^$*jda1QQ~-cS#Jtn zyt3WqfbOaQidTO&rIrf|KoO+k_b$A@yq6A+YU4&aL@CIWf1YAycAX?6=kB-XkmJ?_ z^ZH80G^9$L2EkrejLaMvc=C&_cDx(i+dl5C*w&XBl^o2$mGg(z`hdRf1vDmq4yqp| ze&&FvO65t|mF(_==!!SoywHK3gN7`v%N#v#xk;bpwCN>Qa+p0reL~59am$4K7n#f{ zS5%J*Ee|taD7o=mK|~Y98i%e{5IB{zD|_S2&{U@Z%+zJ2?ly~kML&I(X>KqeM=5q` z=(`-lA5219V=45MZ%?pm-7x-u6aB{HPNqLK)!O7af z$^I{Bk?3ZG>V$i)KZz=4hQxIT~fy?Jr zNja}+?krC)=KOmL*Q4nk!;9i7-Aegxg!Mhudu8J7P|L3}%1^UM(?$nVvno;Sxxx4w-P<462e|PBWBxR88k^ zA6koanBfuPG1qBfQ~E7##87G_;B%Q3mJBAo1-@nTC)9ze^->{3Jg&H7@d6yJtQ%YT zY=&zbj(V|}Hp%PS#xQ$nZ0r1Ggz!Xu$hFDRTw zwYO&7C4Q`|9h=@1OLq8TL*aVEzrMo4jE{McHZP$^NN2OMgF)_m$;C}v$1=<`J61gx z5LZ9%tAB65^%1K3{K2!rn@h8B(I?o=kGDgDoII&s9Xh<8)T zSc!}Ra^9TEzF$34AQ+8<5puH``Dx1%B%z!8i%G*@yG?nj%@8B*=qQa*;J5d&1}#Pw zQZKb{DeU?GA=OVn;~je6kLL)I+_X25cp!t*~GSbyy4{O#mTaw+sx#& zh!?RRKdGNccA>xX({B_*bO=5%r`Ehryv9bYWkE$tzYVwEt2WP1h|HnW7t2TnakSgS z5t$B(9u|ibu`Bu2iln%exjFm7 zp%pndzp03E7S`6iJN~R`^01z3yOTvI(}~YXiYxbGeX%}0uB@~)ZAtj^G)SGZ{hiOe z9ol^6Cv+@B|6cIc7M3gVCg0=r!>}-Jm~UAC+f-5U)tB8glZee+Z_pE)6^8wIEW))Q zwgc(g^13%l8amh!dM8RqZv6CTk^_UGQmnamFQxpigLg3%-4is)u$JY7Jo&?3b!!^C zST7@O0aDf#K-R>y3oTZ8XGoOTalLLpMNfj7Qm7{YpCGF{jR(DP7^PdH(RF2`*6Q5%FFkBKUo}~ zdb<8f5bDd>4?Ng^`Y_!2w-x1E^)G=%21>s8ZEppVx}moF)-rb;vO}unSN>8mi<3ao zJLRJ6M{ge@g~z9=klQ#RDoM?~Z@1{@*T{d!z1dO?z)x7q(;j~t=*nH*HMpWyam>*2 z2tD2*uNU>bN3YpKfJjoT-<;!@HWSYU*0Tz{i{G%>ot&#s2H08*X!Lps(FcDWu@AOwtm=tu`cwT;dbo$ z@mC`GMecxk7dfhX!_Z11a10qb0US%gR(IPED-$6M(aZF=Hyir69Ca-6B=oB9jpq)? zoW|W{fOX^se#(RkFo5$5|jpc!fP%&q2EI`NNN0tk`;lVAIDu)R$C7|Je;1 zO@!jFZR%kGB{*Cg3fl@QB=fe0lYMn*i~0Qv8w(_ zf{OCC&yr()I+*@h>w#(ajo3lm9zzE_e^(k;@k_= zMBE6xgPq8xl@;c#W}?jX03q9}`myLM|bTfi>#l`)?@y1cTE+&PU9 ziWj{#>UnVI%j>MoS*fW3+p5u7iTUqz>dj2d(dSd?Bv3QAScw1w7us$l{UIpRO`1!OFT3CGfF?@o*CQAwub8aEM{GBIRRp{Ttb`EfLe>yjBHXY z@NR{>Rup_@>f_w03zgJ!V^bfMv*kr}?l3wS%B4!lf7r4K=l2Krr>2muE_;@nK9nDRy`rl^yh3JRi3v@99`+b`po-Xzdp21!yU*MQ-@`~mS+2Z{)iclA2$G|} zZl?+al{*GplqfXivzw?DU4 zTh2mf4H&u*X#;^Rp%0yhQpG4s-#%i)rr%T&s)*Xm((Kwg{g4szHqg*jpacb)iSSeQ z`^N8Cep0f7omZ1jYve@SU7C%p-K-tm{0_h5hdVfm(F`Fxts9<>W$=r&(4LUz)8e7) z*b2iIB!8^?XKL{%6O?mw>hSgrxrsS4{dL+3FnsZ|q#F=pUN|FeR@SiOUyzn)e0bg0 z=c@aLh0uMLtR_7a*j|3@aoe`qq1v1Ssy62})Gc8=ZBABF@IhjzUWI-#IhL{SRt_H^ z5e5wIrtJ|MB99K$Ol4KGFRBn3qi-EFBzzs?MSjQQo09j&XsCY998HQ`^R(d^T&Oix2lo478;F&Nst5nNA6_*Hv{mb|5K=%p??3(iFZ})e=g#)-kFP%%EVl+`BJTcHvUaYC$a2GxK`0vK=v-4# zqOPhXxT`r2ef4W@qd5j)DctdYbqC}q>-zr^kEL9g9Bg~&hc-7Qc^*T$eGzn z6_750Bmszm@MRXVA`k%Jf=wg{3jl+W2v8&d03*YXfe5sbbaWsUPz+NU34o*w&w&KY zfY}KSV|bD>03Asb9eV-778b*cq(kCohR4R}01gNMb0X`NYYA?~C8NB?6=3IZ#Z-d@ z079e6kS+;yv}&Y4MzGz#8}L9~Gyy1-!774c4Y@V5C;scXfYpABi<9?gp5W-iFWLSP z=0hF5Bg#Q?ez@$!(OJnJvDSda+xV_yhq6>Vp1C3Ei0zNVrJ2_Bbi?5p61o!ZF6;bm z+rYkiGncHNEDD{!@lSon=yA`u76+22Rs0q{?d;)?shdMZGEu6=smzN@lF4*x0}Ka( zLf#`9R`O)L_X%noh=ffsJ~;{2>a^_!v}ecYcW2PK3MZ>XH}PtbyMJ2N_W;i)sURSB z^LQNeD8JSi0$J1hu5s_B))IBcs^~fe{qdZW>4to;ax!96;ag-+soPiJy|}06cd%n$ z^Xb|Uf{YYZZQ|CWK`&g1N=MwpEO>=c&Lif4yCNPxB?U)eAv5#G0tPMV!dzB;)*fV9oD^Br-^l-TA(z-PFG1_2fG0#(^q9 z=&zW}HErW`RFyPi{af)OFZIk6*$tQ5^tf0B%KUG9_NAE)lcW~6g=HdX*@e}eklKmq zmyGJJ8jjU*ZIumAeKWna*Dzp}*!I6tN@LatExsH+XPm3@KP$FJv%c9a_HGLJ<-0;Gd zO@k&r8C`XC&8a?g`p(BqtfiCqII{@R>3=&gqc+eOz{J>YG1D zZ5sA=mQyD`m?1MG;b@38AD2voTf^+V3p07i>f!l%Q-JtkDtN*@Xbq{bR`i<@%GT`g z9`0ErGzwi{n2O4+pcd`8CVAb}86g>pOOWvf$&ih>5mGT+B5k{+k-hAeh z2kNId>qI~E$g^&6xILpimm6ah_1#W~3A@jU2&#IBt#(v}x{#+E*}iT&u~%6Y-*I4H z0Y{Mv7ePP-PH$~m7S4Jcc+d(+hi2a2yUV($mFyS8qNRpE@nYErOZV?WC8VtLJC0n^Tlu zv6=A)Onn|15xzxkpypTM?+Q4M(b852gL%ZYY@@x{D9a5n?#NL^M_3iglJ(V1Uky22 z`hR%*Uswu$%i#k6(j)#+GUCs}puZGLnu{w|!|r!~ExXUD=;*!*4_dtYXz}mg8KC(& zcx>KHWfoeVOy?TFlb5&?|4lrfSq5Bad;k@pTbXypN*ZNGZMV3ja-da0fQp1 zNy^L90RSWbfd7y0bpJLYl)wo3*N9+CTROjq4gK(=nbXhAT14IF99AzWUG|>HdAGSg| zLk(yB#YncvPT;m@-?cvvuWU+!5A6kO+N7#5#Z$oDO@?^;rHHj?c*MrLMMOTA5x8RK z(=A(r6qYF7csV;8JBflwy#Zc32xA)SF3N>APZ?V*soc!*$efx@A)5@vPVa3bfqBYS zf>LEv2_STA(Ki`gF7wg z&Iccpt|8i*lTnUAg~Dm!=18VsRECBWz{c>{vggnENgMjbE?HuB^s9^rtu8pPgt<+d zw;1y`Z-XK4$`~bcDtuFhiHk7;CfB4 zRze1sicZ>TRPSb=Eew2ao4n{;)aTE!Pjl3PQl0c^7MrWag)ebzYqV;rEP1y%IbggK zLaYTHAhE_89WNh$I9=>Ex>A#5eJ^udT|(Kuy%-$J=>r!yW%|#O8!vw&W{? zoA@1QrwK09QPpbu;@e8kjrq-w1PewZ;Wq*<@5b%xd}vAbZqYm;!2}|n<2s1k(a7kL z8}9PI-kE%$H_;R2udWs!q2}{&lSkM`$5ajH2QxJuH`O_jH<#Kj*gvO;TsEam+<=U+ z=+&}3;M)3p_=RYA^NfejkP@H{&{;dZ_l~pDl4ud~YPj1=inE#usIprdabKh$A7#OR z{XOH{(v8~dT1D?;Po~IFY}D=HddB}{&8X~PGk{{_KS%ceYx+N@_UZqfeq?^q|FB7S zlDYhm+fmPlu!rCLm$IFE(+>OEAN8DGdn%uPx_9>D=bKO6L4==O-#^`X9{*D!=Somr zH$K*-Y6`2b3nkGf!(YSU2&k(f7Mo=>8wbzG61ZYP>Ko0tx@6Sof@Xi;te|DA6Efn z%Eka-2kuJj|6mOabOrw_zyN@f%72st06?jv1|$I067_MJN;~$t-};#O1rN1_VKw|% zXgqIdG@PA|MHdpy?#hm4w4uBrb+DPU1u>E&X6eR1!3_@z)gK}7g2)Vn5m8I%5P_Tv zO|3~sUOh2_96)wu{?Qo>m|q}b@L&LKBnVXo0P%)K0zi>?x_<$HM*_f9>bzJ|N5yyo zR*6Mq3vY)X&(O!FFU%h5PKkU(7l08ZBDDvxboB>_k~x?9jzWX7Yg#b2ZZ)-Pwi1g7 zEfJn~4K4U@q<@Bac1PpKG((lOl{M9IEz?G%Co(kh6DK`CecoIHF;n@f_jBa848S^_ zf@=jm0!Sf93Qv2V{v=H{hMI)}mtZ=c!Zn_)hk5|ltuS<=PVgt9fZEabWHewrq#mM9 zlDjnDNsaWWCldMO+i*EL%m(d97UM*v`@Y6}uq5Mf3`Vqm_US?vL#C#RF|T~=8HRR4 zQ-ixw`Sak7wXKga=p&2LmMeFBj`auaH)H6o(QkUw$xHS(aA z(pUR{5xyl>m6V8PE$D;J3%4OU-KNcDXR2m)ba3iQE!SR>8mR;-rbRRJ8B6ieNEh+UVRigm=jx%Z9>X6k zQ|o)iu>6skQSz$`sPhi@I+YS8`s|V5AYQow^6wCSg>5E@V@UUVqpuW&!kgXMc_`i@J9PNH zcJQAi@voYga@p=$AHc*ODfOETrVpGhgBzyQI)UROEt4PeQy&61%nw6>JNHG)`&%ij zTcosiHIvF8iqd{rK5&w~C16!#ev8^vRc5r%>e2}euVE^x4PIpU zRgvOhIw6&s0}Mcxp->piGFm3Js=i-?efHksgiSD!Yx8u(l5flG^+3z+-_f(TN?SLY z8k&NuZDmJ7VyUeLZf&2d4R0&C1fdnX-J{LsTKPHmyJ!HdViZHTK%ubVe`X{DH0C#`} zF>ZwtmgN~lPwh3i`W_xnwt5?l3~`M>clc`-aL9E`97T!TB_qN1`6TQ{c?q|n=4b74zyTO*J*eq9JLx;0ed1gWTdX6WG{>J_sFB;~Q z^PEf;NErk@p<#aSkwZ3+unz9?f7kFQ8#x|}prFpA;vS1tq_#@GVU^xO%NxTiv>X#V zw;WygV|L)%&^tPD{trExT53<_zqdo%S?fOF>?NP4K!T0qFZ~inZ@651WpnHesX=kR zggbhdDLJZsUIJ%k=2_lDY|uMIw`3{AdmBVcgxT!R*DQrWq;@f_$ub~`wOvC!OT)AJSq=8 zJup)o2#LX?r8bZVyTrOx+RjMS=F^zm3gIgBY-><^q8!r@V-2$Y{uxub$a$_Zxcn4v zxlqX<6t&D83Ym~U2}Y!1FJ1>+%I|Da!Kl>2RNLhKsbTm*Hud>sZ7iSG z^^%Itz~1E;A1dwXx(nk={A{wlL5%INsS_RTP0uuFNl`xfk8`^6(Vc2}7tbdw7JIP3MrU76 zgY5=ZdcVJCx~pLBn05kqKN*)+PdGUzs!IxSV&>A_yOwupW!@W%zgeqzR8+}uxX!_#C zst7N-ty0XNYH#*3ti;cC{MwSao=a}#BQjw8>nAf^_`s5}#ClQntc(4f^S%|1zQC@3$?Dn5;{TfHNZ z{##lD=;$U2xV7UbC@3FJu@>f?AEqj14j&T09$Wu* z`l&oL*LgjF_V_Ec`2qGNc;ts){#}NFV-ne0{bz6_kJufgav-6m$dbpsS9IDg=XGMW zMVynqpKN&-iLjlEzk%SY`^1<+hEJ)4)%czFP(fvQE-^@GOIgV){821vATa4Yfizu~ zf1FELv^($*z|HU}+5We{bP*qPgy-(N8*SC)AD!OIEw462TIZ^!_wQl6q`#{srQh)2d zzvolj+CfyaEZ$U4Axpk@X>U`k=|*{N{Ubc{jkoUbyKbUD9q~8Ul$8=iO!s%83QPVv zyM&?wq?QKJOUzazNz?X1@#MbR^OOt$S|5}4dS?oX&gu*Y`K8L76&X_PxMB1Yht~?Z zOKBx;XMv->wAGE?U->*SsC*ibTa;C3_Ffh>jdC(q#xAQDmq~-LSgUXbr8{TAW0lXq zwRAhiG67XN0dRJ|jLUFQdZc&XPRx#)un<0#z0n1n;$yG&&bG`yUD)E0OHzr=p5GG{ z_Qg64Emit1Iyc##fOX~3^$z}ShOe2lvY*$ORGD+Xg1LHXckHbfZc1ic2mLj-FB|tId?=-UsMg;U#yN3kqqi-y4MvfzRtbUenr}XGY_Vh7#fi2Q@;;&`);#s zQKr#1^J5=4y>&RcSvG-h36#+!7pr`{@CUuO&%*aL z&#HWz1H)Dlb(`8;yhtk=>h6tEv~h;uPf)Bjaj)Y*yX_;yn97dAXP*T+3Pi}Vgi8#g z9njHj-cOGGecJVnwBJ2I6@-5lHJzBEGHhsG9y|Az^TPCo=Wkq@H19;)^xM1;(J>1* zYxu10_$}1(=Gfny&=mlS;vC7v$aQE4A@E7@ArfDi{CLq=cm%d{`Re48^|R#>Af z*+vJG#^%7rutyiwOAsS*tNRs)Vx=ql1;OK0Aq7aE3YS)WsCjnF@5gvYIdkeCkg;zq@-&GUd&|4J_;p6JVDHQxjYsL>jfR>pPqj0S z6|UC`#Er@9<|rFKDli3(Pty3m>3x0f%27hNol9=!Mlo%ks*l*=-G+S8O~Ep)e!v-P zI|#j~Ntj;lp5~NwBuN3+iI!P)Fs3g45$^eQw;pOo3iYGOC^}f7oF=Tut`-`Zs@dcp z5Hht@R2iyNfd(Pt2Wj9i$H_&=;LyQKEbV-C{lXBkWu;ru*6%idbDQNc%f}zcN6vb} z)8*jm>H;vh!dWN1u*&x65yDr(NAZUSw)xby({;L7NuAnpq><%JC*fr!EW3U|=cynY z`7uj@R@0=Af~4{>Y_8F$uS-tML-P3^=FeA~@025sFXZb_d&f+DO;UEK#jpvL${Q_N zlLEstQF6M^CW7^QyeIPtbU*6rO)BLCpuTK!ES9><{56_p3yTXOnFW+pK#F%&G$vQ3 z$<$Yh%BeX;gK@P}qf9g1GQ%gmYBaIl`&^H*$GQ0ZQGT}MDGkpKd~^0|%qio5w#@`i zt$YY&63wifB@o8<#kT`zBU7pTQ*Uf`0B5oXTWRfR3>VU13$AsxCsA*9SumV$0CzsS z>b%ZJl^uIzu0HkMWMOsu3{O}eW>M}ro49KqK$8@>1XYOY+==+jAl*CMqbc;YwPn{R zt-$X4n(+;G{-6G`8S1a!zV(=Y*^lWEyxscpwA$+NkIBS};BekT;z(h=@X+Q3*O?bR z()=;@>0#!Mi-1Uw%&N`3hxL;(JC(7{CS%b~sX%9=;OOG`Pwmc%@G>&cz;QOY&t((y z?*2~l(ZIpjW4XH#!YKrfKn+~^GNYu3Bl=a$)SV_tebH1M-?0Y|SNJrB32AvbQL*%O z0{n!1BKN1TcTrmzZ?v)=@s@79n3c-Bg=Y4yHe;a6;ZWaLeHu2^)A>N|9V0$1g3pVj zlEUc(_sY3sVy>cje1UPc_%!AsF3J+hjr?}=046!n@5yO&bN(%|*bmMS&;M8zS62et z{7SWVR=T!#_x?#Vo=@wU^^hL#s1Sn>)gJ2o5D|q-N9?MkW0wY?wcbm6d*8!?U*fw6 zRHU%tMF!pnhF&k^B(A>cv?LGO*-{BBs&iPgN|7dnC~w3iY^-i+EfEsjxFdjrv2WPO zZN+)|CTzHkD!o8aY(-xa0&|{g3-p4)%@cHYX9{jwS3;j$)(+>WMN_8;+2W<$GsMN^ zFm;P4<9i4nv3|$S!V-yc_TN8l%7aVP7WV0)dF10vH`Sjw-H+!>vO_x)$60kP)CYhLxP0T^e6=oxMM=a6NclD@XU*Yw|DG8jmtop9Cz+nR5GGXl1gu_8E@n_IZ9W%@)qF zWtqcvqK#2bDV0%NdR;hC8Mc`7?o#iM;x6PssS2DH|US5>e;=7vvkhN9Y|>XmS{XSrKB?27z^veHuC8h*rp7qX zyxw9v$DJY8nCRZ{`00J^InrLMXMV`%lMF(S`5znp?G;((F21&T;9JU16O@3?<#s09 zu;?{?o!UFceuug@!#zYJ7q@=8ob%PViVTIr3lHXcHFf7(8QbqfZOa&PV+B?ZW81Ez zG{PM)P2DHxB)tx2KN&UEE@+!9>F}%DaWXMPh7#ViHHtxJem=5$WQN#F+JdBCZtYC2 z=$;d4^eBQHhRvq^R((qLg7|GmyMeq zEV-t6!ODUCm6!N<(j@-D7MQM;*A?tHc#5NOlS-RbmsEgzGAfGon8t2>RYI!xa5K$} zMJd)PCAK?8DQ35iIL8@2w?BLxJDV7hdKc{YHMOX!y!(ZWChMWBwAx6p23+S=)sSt@ z36!yjQERZ`?vF2{{dC_8ln-_Fayya@MJzjLk7Wd^n%P?K8)W2XioQq5TbNoY!v>&z z_v20N<}+ruvGr{gKIy6Y-tMC8&;1^%EEo2?#jyZ1htH~b#Pv7`VCcg?d%C%hbY?2K z;S~-Oo5wu}h@g4_rxwdsES)w#kEg$s>Z|s=bygn^ 0 then Result := redrom - else if Assigned(RedRomFilePage) then + else if Assigned(RedROMFilePage) then begin R := CompareStr(GetMD5OfFile(RedROMFilePage.Values[0]), '3d45c1ee9abd5738df46d2bdda8b57dc') if R <> 0 then @@ -592,7 +611,7 @@ function GetBlueROMPath(Param: string): string; begin if Length(bluerom) > 0 then Result := bluerom - else if Assigned(BlueRomFilePage) then + else if Assigned(BlueROMFilePage) then begin R := CompareStr(GetMD5OfFile(BlueROMFilePage.Values[0]), '50927e843568814f7ed45ec4f944bd8b') if R <> 0 then @@ -604,6 +623,22 @@ begin Result := ''; end; +function GetLADXROMPath(Param: string): string; +begin + if Length(ladxrom) > 0 then + Result := ladxrom + else if Assigned(LADXROMFilePage) then + begin + R := CompareStr(GetMD5OfFile(LADXROMFilePage.Values[0]), '07c211479386825042efb4ad31bb525f') + if R <> 0 then + MsgBox('Link''s Awakening DX ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); + + Result := LADXROMFilePage.Values[0] + end + else + Result := ''; + end; + procedure InitializeWizard(); begin AddOoTRomPage(); @@ -640,6 +675,10 @@ begin if Length(bluerom) = 0 then BlueROMFilePage:= AddGBRomPage('Pokemon Blue (UE) [S][!].gb'); + ladxrom := CheckRom('Legend of Zelda, The - Link''s Awakening DX (USA, Europe) (SGB Enhanced).gbc','07c211479386825042efb4ad31bb525f'); + if Length(ladxrom) = 0 then + LADXROMFilePage:= AddGBRomPage('Legend of Zelda, The - Link''s Awakening DX (USA, Europe) (SGB Enhanced).gbc'); + l2acrom := CheckRom('Lufia II - Rise of the Sinistrals (USA).sfc', '6efc477d6203ed2b3b9133c1cd9e9c5d'); if Length(l2acrom) = 0 then L2ACROMFilePage:= AddRomPage('Lufia II - Rise of the Sinistrals (USA).sfc'); @@ -669,4 +708,6 @@ begin Result := not (WizardIsComponentSelected('generator/pkmn_r') or WizardIsComponentSelected('client/pkmn/red')); if (assigned(BlueROMFilePage)) and (PageID = BlueROMFilePage.ID) then Result := not (WizardIsComponentSelected('generator/pkmn_b') or WizardIsComponentSelected('client/pkmn/blue')); + if (assigned(LADXROMFilePage)) and (PageID = LADXROMFilePage.ID) then + Result := not (WizardIsComponentSelected('generator/ladx') or WizardIsComponentSelected('client/ladx')); end; diff --git a/worlds/ladx/Common.py b/worlds/ladx/Common.py new file mode 100644 index 0000000000..e85e9767b9 --- /dev/null +++ b/worlds/ladx/Common.py @@ -0,0 +1,2 @@ +LINKS_AWAKENING = "Links Awakening DX" +BASE_ID = 10000000 \ No newline at end of file diff --git a/worlds/ladx/GpsTracker.py b/worlds/ladx/GpsTracker.py new file mode 100644 index 0000000000..1ea465eb16 --- /dev/null +++ b/worlds/ladx/GpsTracker.py @@ -0,0 +1,92 @@ +import json +roomAddress = 0xFFF6 +mapIdAddress = 0xFFF7 +indoorFlagAddress = 0xDBA5 +entranceRoomOffset = 0xD800 +screenCoordAddress = 0xFFFA + +mapMap = { + 0x00: 0x01, + 0x01: 0x01, + 0x02: 0x01, + 0x03: 0x01, + 0x04: 0x01, + 0x05: 0x01, + 0x06: 0x02, + 0x07: 0x02, + 0x08: 0x02, + 0x09: 0x02, + 0x0A: 0x02, + 0x0B: 0x02, + 0x0C: 0x02, + 0x0D: 0x02, + 0x0E: 0x02, + 0x0F: 0x02, + 0x10: 0x02, + 0x11: 0x02, + 0x12: 0x02, + 0x13: 0x02, + 0x14: 0x02, + 0x15: 0x02, + 0x16: 0x02, + 0x17: 0x02, + 0x18: 0x02, + 0x19: 0x02, + 0x1D: 0x01, + 0x1E: 0x01, + 0x1F: 0x01, + 0xFF: 0x03, +} + +class GpsTracker: + room = None + location_changed = False + screenX = 0 + screenY = 0 + indoors = None + + def __init__(self, gameboy) -> None: + self.gameboy = gameboy + + async def read_byte(self, b): + return (await self.gameboy.async_read_memory(b))[0] + + async def read_location(self): + indoors = await self.read_byte(indoorFlagAddress) + + if indoors != self.indoors and self.indoors != None: + self.indoorsChanged = True + + self.indoors = indoors + + mapId = await self.read_byte(mapIdAddress) + if mapId not in mapMap: + print(f'Unknown map ID {hex(mapId)}') + return + + mapDigit = mapMap[mapId] << 8 if indoors else 0 + last_room = self.room + self.room = await self.read_byte(roomAddress) + mapDigit + + coords = await self.read_byte(screenCoordAddress) + self.screenX = coords & 0x0F + self.screenY = (coords & 0xF0) >> 4 + + if (self.room != last_room): + self.location_changed = True + + last_message = {} + async def send_location(self, socket, diff=False): + if self.room is None: + return + message = { + "type":"location", + "refresh": True, + "version":"1.0", + "room": f'0x{self.room:02X}', + "x": self.screenX, + "y": self.screenY, + } + if message != self.last_message: + self.last_message = message + await socket.send(json.dumps(message)) diff --git a/worlds/ladx/ItemTracker.py b/worlds/ladx/ItemTracker.py new file mode 100644 index 0000000000..92ef71633e --- /dev/null +++ b/worlds/ladx/ItemTracker.py @@ -0,0 +1,283 @@ +import json +gameStateAddress = 0xDB95 +validGameStates = {0x0B, 0x0C} +gameStateResetThreshold = 0x06 + +inventorySlotCount = 16 +inventoryStartAddress = 0xDB00 +inventoryEndAddress = inventoryStartAddress + inventorySlotCount + +inventoryItemIds = { + 0x02: 'BOMB', + 0x05: 'BOW', + 0x06: 'HOOKSHOT', + 0x07: 'MAGIC_ROD', + 0x08: 'PEGASUS_BOOTS', + 0x09: 'OCARINA', + 0x0A: 'FEATHER', + 0x0B: 'SHOVEL', + 0x0C: 'MAGIC_POWDER', + 0x0D: 'BOOMERANG', + 0x0E: 'TOADSTOOL', + 0x0F: 'ROOSTER', +} + +dungeonKeyDoors = [ + { # D1 + 0xD907: [0x04], + 0xD909: [0x40], + 0xD90F: [0x01], + }, + { # D2 + 0xD921: [0x02], + 0xD925: [0x02], + 0xD931: [0x02], + 0xD932: [0x08], + 0xD935: [0x04], + }, + { # D3 + 0xD945: [0x40], + 0xD946: [0x40], + 0xD949: [0x40], + 0xD94A: [0x40], + 0xD956: [0x01, 0x02, 0x04, 0x08], + }, + { # D4 + 0xD969: [0x04], + 0xD96A: [0x40], + 0xD96E: [0x40], + 0xD978: [0x01], + 0xD979: [0x04], + }, + { # D5 + 0xD98C: [0x40], + 0xD994: [0x40], + 0xD99F: [0x04], + }, + { # D6 + 0xD9C3: [0x40], + 0xD9C6: [0x40], + 0xD9D0: [0x04], + }, + { # D7 + 0xDA10: [0x04], + 0xDA1E: [0x40], + 0xDA21: [0x40], + }, + { # D8 + 0xDA39: [0x02], + 0xDA3B: [0x01], + 0xDA42: [0x40], + 0xDA43: [0x40], + 0xDA44: [0x40], + 0xDA49: [0x40], + 0xDA4A: [0x01], + }, + { # D0(9) + 0xDDE5: [0x02], + 0xDDE9: [0x04], + 0xDDF0: [0x04], + }, +] + +dungeonItemAddresses = [ + 0xDB16, # D1 + 0xDB1B, # D2 + 0xDB20, # D3 + 0xDB25, # D4 + 0xDB2A, # D5 + 0xDB2F, # D6 + 0xDB34, # D7 + 0xDB39, # D8 + 0xDDDA, # Color Dungeon +] + +dungeonItemOffsets = { + 'MAP{}': 0, + 'COMPASS{}': 1, + 'STONE_BEAK{}': 2, + 'NIGHTMARE_KEY{}': 3, + 'KEY{}': 4, +} + +class Item: + def __init__(self, id, address, threshold=0, mask=None, increaseOnly=False, count=False, max=None): + self.id = id + self.address = address + self.threshold = threshold + self.mask = mask + self.increaseOnly = increaseOnly + self.count = count + self.value = 0 if increaseOnly else None + self.rawValue = 0 + self.diff = 0 + self.max = max + + def set(self, byte, extra): + oldValue = self.value + + if self.mask: + byte = byte & self.mask + + if not self.count: + byte = int(byte > self.threshold) + else: + # LADX seems to store one decimal digit per nibble + byte = byte - (byte // 16 * 6) + + byte += extra + + if self.max and byte > self.max: + byte = self.max + + if self.increaseOnly: + if byte > self.rawValue: + self.value += byte - self.rawValue + else: + self.value = byte + + self.rawValue = byte + + if oldValue != self.value: + self.diff += self.value - (oldValue or 0) + +class ItemTracker: + def __init__(self, gameboy) -> None: + self.gameboy = gameboy + self.loadItems() + pass + extraItems = {} + + async def readRamByte(self, byte): + return (await self.gameboy.read_memory_cache([byte]))[byte] + + def loadItems(self): + self.items = [ + Item('BOMB', None), + Item('BOW', None), + Item('HOOKSHOT', None), + Item('MAGIC_ROD', None), + Item('PEGASUS_BOOTS', None), + Item('OCARINA', None), + Item('FEATHER', None), + Item('SHOVEL', None), + Item('MAGIC_POWDER', None), + Item('BOOMERANG', None), + Item('TOADSTOOL', None), + Item('ROOSTER', None), + Item('SWORD', 0xDB4E, count=True), + Item('POWER_BRACELET', 0xDB43, count=True), + Item('SHIELD', 0xDB44, count=True), + Item('BOWWOW', 0xDB56), + Item('MAX_POWDER_UPGRADE', 0xDB76, threshold=0x20), + Item('MAX_BOMBS_UPGRADE', 0xDB77, threshold=0x30), + Item('MAX_ARROWS_UPGRADE', 0xDB78, threshold=0x30), + Item('TAIL_KEY', 0xDB11), + Item('SLIME_KEY', 0xDB15), + Item('ANGLER_KEY', 0xDB12), + Item('FACE_KEY', 0xDB13), + Item('BIRD_KEY', 0xDB14), + Item('FLIPPERS', 0xDB3E), + Item('SEASHELL', 0xDB41, count=True), + Item('GOLD_LEAF', 0xDB42, count=True, max=5), + Item('INSTRUMENT1', 0xDB65, mask=1 << 1), + Item('INSTRUMENT2', 0xDB66, mask=1 << 1), + Item('INSTRUMENT3', 0xDB67, mask=1 << 1), + Item('INSTRUMENT4', 0xDB68, mask=1 << 1), + Item('INSTRUMENT5', 0xDB69, mask=1 << 1), + Item('INSTRUMENT6', 0xDB6A, mask=1 << 1), + Item('INSTRUMENT7', 0xDB6B, mask=1 << 1), + Item('INSTRUMENT8', 0xDB6C, mask=1 << 1), + Item('TRADING_ITEM_YOSHI_DOLL', 0xDB40, mask=1 << 0), + Item('TRADING_ITEM_RIBBON', 0xDB40, mask=1 << 1), + Item('TRADING_ITEM_DOG_FOOD', 0xDB40, mask=1 << 2), + Item('TRADING_ITEM_BANANAS', 0xDB40, mask=1 << 3), + Item('TRADING_ITEM_STICK', 0xDB40, mask=1 << 4), + Item('TRADING_ITEM_HONEYCOMB', 0xDB40, mask=1 << 5), + Item('TRADING_ITEM_PINEAPPLE', 0xDB40, mask=1 << 6), + Item('TRADING_ITEM_HIBISCUS', 0xDB40, mask=1 << 7), + Item('TRADING_ITEM_LETTER', 0xDB7F, mask=1 << 0), + Item('TRADING_ITEM_BROOM', 0xDB7F, mask=1 << 1), + Item('TRADING_ITEM_FISHING_HOOK', 0xDB7F, mask=1 << 2), + Item('TRADING_ITEM_NECKLACE', 0xDB7F, mask=1 << 3), + Item('TRADING_ITEM_SCALE', 0xDB7F, mask=1 << 4), + Item('TRADING_ITEM_MAGNIFYING_GLASS', 0xDB7F, mask=1 << 5), + Item('SONG1', 0xDB49, mask=1 << 2), + Item('SONG2', 0xDB49, mask=1 << 1), + Item('SONG3', 0xDB49, mask=1 << 0), + Item('RED_TUNIC', 0xDB6D, mask=1 << 0), + Item('BLUE_TUNIC', 0xDB6D, mask=1 << 1), + Item('GREAT_FAIRY', 0xDDE1, mask=1 << 4), + ] + + for i in range(len(dungeonItemAddresses)): + for item, offset in dungeonItemOffsets.items(): + if item.startswith('KEY'): + self.items.append(Item(item.format(i + 1), dungeonItemAddresses[i] + offset, count=True)) + else: + self.items.append(Item(item.format(i + 1), dungeonItemAddresses[i] + offset)) + + self.itemDict = {item.id: item for item in self.items} + + async def readItems(state): + extraItems = state.extraItems + missingItems = {x for x in state.items if x.address == None} + + # Add keys for opened key doors + for i in range(len(dungeonKeyDoors)): + item = f'KEY{i + 1}' + extraItems[item] = 0 + + for address, masks in dungeonKeyDoors[i].items(): + for mask in masks: + value = await state.readRamByte(address) & mask + if value > 0: + extraItems[item] += 1 + + # Main inventory items + for i in range(inventoryStartAddress, inventoryEndAddress): + value = await state.readRamByte(i) + + if value in inventoryItemIds: + item = state.itemDict[inventoryItemIds[value]] + extra = extraItems[item.id] if item.id in extraItems else 0 + item.set(1, extra) + missingItems.remove(item) + + for item in missingItems: + extra = extraItems[item.id] if item.id in extraItems else 0 + item.set(0, extra) + + # All other items + for item in [x for x in state.items if x.address]: + extra = extraItems[item.id] if item.id in extraItems else 0 + item.set(await state.readRamByte(item.address), extra) + + async def sendItems(self, socket, diff=False): + if not self.items: + return + message = { + "type":"item", + "refresh": True, + "version":"1.0", + "diff": diff, + "items": [], + } + items = self.items + if diff: + items = [item for item in items if item.diff != 0] + if not items: + return + for item in items: + value = item.diff if diff else item.value + + message["items"].append( + { + 'id': item.id, + 'qty': value, + } + ) + + item.diff = 0 + + await socket.send(json.dumps(message)) \ No newline at end of file diff --git a/worlds/ladx/Items.py b/worlds/ladx/Items.py new file mode 100644 index 0000000000..ff5db6950a --- /dev/null +++ b/worlds/ladx/Items.py @@ -0,0 +1,304 @@ +from BaseClasses import Item, ItemClassification +from . import Common +import typing +from enum import IntEnum +from .LADXR.locations.constants import CHEST_ITEMS + +class ItemData(typing.NamedTuple): + item_name: str + ladxr_id: str + classification: ItemClassification + mark_only_first_progression: bool = False + created_for_players = set() + @property + def item_id(self): + return CHEST_ITEMS[self.ladxr_id] + + +class DungeonItemType(IntEnum): + INSTRUMENT = 0 + NIGHTMARE_KEY = 1 + KEY = 2 + STONE_BEAK = 3 + MAP = 4 + COMPASS = 5 + +class DungeonItemData(ItemData): + @property + def dungeon_index(self): + return int(self.ladxr_id[-1]) + + @property + def dungeon_item_type(self): + s = self.ladxr_id[:-1] + return DungeonItemType.__dict__[s] + +class LinksAwakeningItem(Item): + game: str = Common.LINKS_AWAKENING + + def __init__(self, item_data, world, player): + classification = item_data.classification + if callable(classification): + classification = classification(world, player) + # this doesn't work lol + MARK_FIRST_ITEM = False + if MARK_FIRST_ITEM: + if item_data.mark_only_first_progression: + if player in item_data.created_for_players: + classification = ItemClassification.filler + else: + item_data.created_for_players.add(player) + super().__init__(item_data.item_name, classification, Common.BASE_ID + item_data.item_id, player) + self.item_data = item_data + +# TODO: use _NAMES instead? +class ItemName: + POWER_BRACELET = "Progressive Power Bracelet" + SHIELD = "Progressive Shield" + BOW = "Bow" + HOOKSHOT = "Hookshot" + MAGIC_ROD = "Magic Rod" + PEGASUS_BOOTS = "Pegasus Boots" + OCARINA = "Ocarina" + FEATHER = "Feather" + SHOVEL = "Shovel" + MAGIC_POWDER = "Magic Powder" + BOMB = "Bomb" + SWORD = "Progressive Sword" + FLIPPERS = "Flippers" + MAGNIFYING_LENS = "Magnifying Lens" + MEDICINE = "Medicine" + TAIL_KEY = "Tail Key" + ANGLER_KEY = "Angler Key" + FACE_KEY = "Face Key" + BIRD_KEY = "Bird Key" + SLIME_KEY = "Slime Key" + GOLD_LEAF = "Gold Leaf" + RUPEES_20 = "20 Rupees" + RUPEES_50 = "50 Rupees" + RUPEES_100 = "100 Rupees" + RUPEES_200 = "200 Rupees" + RUPEES_500 = "500 Rupees" + SEASHELL = "Seashell" + MESSAGE = "Master Stalfos' Message" + GEL = "Gel" + BOOMERANG = "Boomerang" + HEART_PIECE = "Heart Piece" + BOWWOW = "BowWow" + ARROWS_10 = "10 Arrows" + SINGLE_ARROW = "Single Arrow" + ROOSTER = "Rooster" + MAX_POWDER_UPGRADE = "Max Powder Upgrade" + MAX_BOMBS_UPGRADE = "Max Bombs Upgrade" + MAX_ARROWS_UPGRADE = "Max Arrows Upgrade" + RED_TUNIC = "Red Tunic" + BLUE_TUNIC = "Blue Tunic" + HEART_CONTAINER = "Heart Container" + BAD_HEART_CONTAINER = "Bad Heart Container" + TOADSTOOL = "Toadstool" + KEY = "Key" + KEY1 = "Small Key (Tail Cave)" + KEY2 = "Small Key (Bottle Grotto)" + KEY3 = "Small Key (Key Cavern)" + KEY4 = "Small Key (Angler's Tunnel)" + KEY5 = "Small Key (Catfish's Maw)" + KEY6 = "Small Key (Face Shrine)" + KEY7 = "Small Key (Eagle's Tower)" + KEY8 = "Small Key (Turtle Rock)" + KEY9 = "Small Key (Color Dungeon)" + NIGHTMARE_KEY = "Nightmare Key" + NIGHTMARE_KEY1 = "Nightmare Key (Tail Cave)" + NIGHTMARE_KEY2 = "Nightmare Key (Bottle Grotto)" + NIGHTMARE_KEY3 = "Nightmare Key (Key Cavern)" + NIGHTMARE_KEY4 = "Nightmare Key (Angler's Tunnel)" + NIGHTMARE_KEY5 = "Nightmare Key (Catfish's Maw)" + NIGHTMARE_KEY6 = "Nightmare Key (Face Shrine)" + NIGHTMARE_KEY7 = "Nightmare Key (Eagle's Tower)" + NIGHTMARE_KEY8 = "Nightmare Key (Turtle Rock)" + NIGHTMARE_KEY9 = "Nightmare Key (Color Dungeon)" + MAP = "Map" + MAP1 = "Dungeon Map (Tail Cave)" + MAP2 = "Dungeon Map (Bottle Grotto)" + MAP3 = "Dungeon Map (Key Cavern)" + MAP4 = "Dungeon Map (Angler's Tunnel)" + MAP5 = "Dungeon Map (Catfish's Maw)" + MAP6 = "Dungeon Map (Face Shrine)" + MAP7 = "Dungeon Map (Eagle's Tower)" + MAP8 = "Dungeon Map (Turtle Rock)" + MAP9 = "Dungeon Map (Color Dungeon)" + COMPASS = "Compass" + COMPASS1 = "Compass (Tail Cave)" + COMPASS2 = "Compass (Bottle Grotto)" + COMPASS3 = "Compass (Key Cavern)" + COMPASS4 = "Compass (Angler's Tunnel)" + COMPASS5 = "Compass (Catfish's Maw)" + COMPASS6 = "Compass (Face Shrine)" + COMPASS7 = "Compass (Eagle's Tower)" + COMPASS8 = "Compass (Turtle Rock)" + COMPASS9 = "Compass (Color Dungeon)" + STONE_BEAK = "Stone Beak" + STONE_BEAK1 = "Stone Beak (Tail Cave)" + STONE_BEAK2 = "Stone Beak (Bottle Grotto)" + STONE_BEAK3 = "Stone Beak (Key Cavern)" + STONE_BEAK4 = "Stone Beak (Angler's Tunnel)" + STONE_BEAK5 = "Stone Beak (Catfish's Maw)" + STONE_BEAK6 = "Stone Beak (Face Shrine)" + STONE_BEAK7 = "Stone Beak (Eagle's Tower)" + STONE_BEAK8 = "Stone Beak (Turtle Rock)" + STONE_BEAK9 = "Stone Beak (Color Dungeon)" + SONG1 = "Ballad of the Wind Fish" + SONG2 = "Manbo's Mambo" + SONG3 = "Frog's Song of Soul" + INSTRUMENT1 = "Full Moon Cello" + INSTRUMENT2 = "Conch Horn" + INSTRUMENT3 = "Sea Lily's Bell" + INSTRUMENT4 = "Surf Harp" + INSTRUMENT5 = "Wind Marimba" + INSTRUMENT6 = "Coral Triangle" + INSTRUMENT7 = "Organ of Evening Calm" + INSTRUMENT8 = "Thunder Drum" + TRADING_ITEM_YOSHI_DOLL = "Yoshi Doll" + TRADING_ITEM_RIBBON = "Ribbon" + TRADING_ITEM_DOG_FOOD = "Dog Food" + TRADING_ITEM_BANANAS = "Bananas" + TRADING_ITEM_STICK = "Stick" + TRADING_ITEM_HONEYCOMB = "Honeycomb" + TRADING_ITEM_PINEAPPLE = "Pineapple" + TRADING_ITEM_HIBISCUS = "Hibiscus" + TRADING_ITEM_LETTER = "Letter" + TRADING_ITEM_BROOM = "Broom" + TRADING_ITEM_FISHING_HOOK = "Fishing Hook" + TRADING_ITEM_NECKLACE = "Necklace" + TRADING_ITEM_SCALE = "Scale" + TRADING_ITEM_MAGNIFYING_GLASS = "Magnifying Glass" + +trade_item_prog = ItemClassification.progression + +links_awakening_items = [ + ItemData(ItemName.POWER_BRACELET, "POWER_BRACELET", ItemClassification.progression), + ItemData(ItemName.SHIELD, "SHIELD", ItemClassification.progression), + ItemData(ItemName.BOW, "BOW", ItemClassification.progression), + ItemData(ItemName.HOOKSHOT, "HOOKSHOT", ItemClassification.progression), + ItemData(ItemName.MAGIC_ROD, "MAGIC_ROD", ItemClassification.progression), + ItemData(ItemName.PEGASUS_BOOTS, "PEGASUS_BOOTS", ItemClassification.progression), + ItemData(ItemName.OCARINA, "OCARINA", ItemClassification.progression), + ItemData(ItemName.FEATHER, "FEATHER", ItemClassification.progression), + ItemData(ItemName.SHOVEL, "SHOVEL", ItemClassification.progression), + ItemData(ItemName.MAGIC_POWDER, "MAGIC_POWDER", ItemClassification.progression, True), + ItemData(ItemName.BOMB, "BOMB", ItemClassification.progression, True), + ItemData(ItemName.SWORD, "SWORD", ItemClassification.progression), + ItemData(ItemName.FLIPPERS, "FLIPPERS", ItemClassification.progression), + ItemData(ItemName.MAGNIFYING_LENS, "MAGNIFYING_LENS", ItemClassification.progression), + ItemData(ItemName.MEDICINE, "MEDICINE", ItemClassification.useful), + ItemData(ItemName.TAIL_KEY, "TAIL_KEY", ItemClassification.progression), + ItemData(ItemName.ANGLER_KEY, "ANGLER_KEY", ItemClassification.progression), + ItemData(ItemName.FACE_KEY, "FACE_KEY", ItemClassification.progression), + ItemData(ItemName.BIRD_KEY, "BIRD_KEY", ItemClassification.progression), + ItemData(ItemName.SLIME_KEY, "SLIME_KEY", ItemClassification.progression), + ItemData(ItemName.GOLD_LEAF, "GOLD_LEAF", ItemClassification.progression), + ItemData(ItemName.RUPEES_20, "RUPEES_20", ItemClassification.filler), + ItemData(ItemName.RUPEES_50, "RUPEES_50", ItemClassification.useful), + ItemData(ItemName.RUPEES_100, "RUPEES_100", ItemClassification.progression_skip_balancing), + ItemData(ItemName.RUPEES_200, "RUPEES_200", ItemClassification.progression_skip_balancing), + ItemData(ItemName.RUPEES_500, "RUPEES_500", ItemClassification.progression_skip_balancing), + ItemData(ItemName.SEASHELL, "SEASHELL", ItemClassification.progression_skip_balancing), + ItemData(ItemName.MESSAGE, "MESSAGE", ItemClassification.progression), + ItemData(ItemName.GEL, "GEL", ItemClassification.trap), + ItemData(ItemName.BOOMERANG, "BOOMERANG", ItemClassification.progression), + ItemData(ItemName.HEART_PIECE, "HEART_PIECE", ItemClassification.filler), + ItemData(ItemName.BOWWOW, "BOWWOW", ItemClassification.progression), + ItemData(ItemName.ARROWS_10, "ARROWS_10", ItemClassification.filler), + ItemData(ItemName.SINGLE_ARROW, "SINGLE_ARROW", ItemClassification.filler), + ItemData(ItemName.ROOSTER, "ROOSTER", ItemClassification.progression), + ItemData(ItemName.MAX_POWDER_UPGRADE, "MAX_POWDER_UPGRADE", ItemClassification.filler), + ItemData(ItemName.MAX_BOMBS_UPGRADE, "MAX_BOMBS_UPGRADE", ItemClassification.filler), + ItemData(ItemName.MAX_ARROWS_UPGRADE, "MAX_ARROWS_UPGRADE", ItemClassification.filler), + ItemData(ItemName.RED_TUNIC, "RED_TUNIC", ItemClassification.useful), + ItemData(ItemName.BLUE_TUNIC, "BLUE_TUNIC", ItemClassification.useful), + ItemData(ItemName.HEART_CONTAINER, "HEART_CONTAINER", ItemClassification.useful), + #ItemData(ItemName.BAD_HEART_CONTAINER, "BAD_HEART_CONTAINER", ItemClassification.trap), + ItemData(ItemName.TOADSTOOL, "TOADSTOOL", ItemClassification.progression), + DungeonItemData(ItemName.KEY, "KEY", ItemClassification.progression), + DungeonItemData(ItemName.KEY1, "KEY1", ItemClassification.progression), + DungeonItemData(ItemName.KEY2, "KEY2", ItemClassification.progression), + DungeonItemData(ItemName.KEY3, "KEY3", ItemClassification.progression), + DungeonItemData(ItemName.KEY4, "KEY4", ItemClassification.progression), + DungeonItemData(ItemName.KEY5, "KEY5", ItemClassification.progression), + DungeonItemData(ItemName.KEY6, "KEY6", ItemClassification.progression), + DungeonItemData(ItemName.KEY7, "KEY7", ItemClassification.progression), + DungeonItemData(ItemName.KEY8, "KEY8", ItemClassification.progression), + DungeonItemData(ItemName.KEY9, "KEY9", ItemClassification.progression), + DungeonItemData(ItemName.NIGHTMARE_KEY, "NIGHTMARE_KEY", ItemClassification.progression), + DungeonItemData(ItemName.NIGHTMARE_KEY1, "NIGHTMARE_KEY1", ItemClassification.progression), + DungeonItemData(ItemName.NIGHTMARE_KEY2, "NIGHTMARE_KEY2", ItemClassification.progression), + DungeonItemData(ItemName.NIGHTMARE_KEY3, "NIGHTMARE_KEY3", ItemClassification.progression), + DungeonItemData(ItemName.NIGHTMARE_KEY4, "NIGHTMARE_KEY4", ItemClassification.progression), + DungeonItemData(ItemName.NIGHTMARE_KEY5, "NIGHTMARE_KEY5", ItemClassification.progression), + DungeonItemData(ItemName.NIGHTMARE_KEY6, "NIGHTMARE_KEY6", ItemClassification.progression), + DungeonItemData(ItemName.NIGHTMARE_KEY7, "NIGHTMARE_KEY7", ItemClassification.progression), + DungeonItemData(ItemName.NIGHTMARE_KEY8, "NIGHTMARE_KEY8", ItemClassification.progression), + DungeonItemData(ItemName.NIGHTMARE_KEY9, "NIGHTMARE_KEY9", ItemClassification.progression), + DungeonItemData(ItemName.MAP, "MAP", ItemClassification.filler), + DungeonItemData(ItemName.MAP1, "MAP1", ItemClassification.filler), + DungeonItemData(ItemName.MAP2, "MAP2", ItemClassification.filler), + DungeonItemData(ItemName.MAP3, "MAP3", ItemClassification.filler), + DungeonItemData(ItemName.MAP4, "MAP4", ItemClassification.filler), + DungeonItemData(ItemName.MAP5, "MAP5", ItemClassification.filler), + DungeonItemData(ItemName.MAP6, "MAP6", ItemClassification.filler), + DungeonItemData(ItemName.MAP7, "MAP7", ItemClassification.filler), + DungeonItemData(ItemName.MAP8, "MAP8", ItemClassification.filler), + DungeonItemData(ItemName.MAP9, "MAP9", ItemClassification.filler), + DungeonItemData(ItemName.COMPASS, "COMPASS", ItemClassification.filler), + DungeonItemData(ItemName.COMPASS1, "COMPASS1", ItemClassification.filler), + DungeonItemData(ItemName.COMPASS2, "COMPASS2", ItemClassification.filler), + DungeonItemData(ItemName.COMPASS3, "COMPASS3", ItemClassification.filler), + DungeonItemData(ItemName.COMPASS4, "COMPASS4", ItemClassification.filler), + DungeonItemData(ItemName.COMPASS5, "COMPASS5", ItemClassification.filler), + DungeonItemData(ItemName.COMPASS6, "COMPASS6", ItemClassification.filler), + DungeonItemData(ItemName.COMPASS7, "COMPASS7", ItemClassification.filler), + DungeonItemData(ItemName.COMPASS8, "COMPASS8", ItemClassification.filler), + DungeonItemData(ItemName.COMPASS9, "COMPASS9", ItemClassification.filler), + DungeonItemData(ItemName.STONE_BEAK, "STONE_BEAK", ItemClassification.filler), + DungeonItemData(ItemName.STONE_BEAK1, "STONE_BEAK1", ItemClassification.filler), + DungeonItemData(ItemName.STONE_BEAK2, "STONE_BEAK2", ItemClassification.filler), + DungeonItemData(ItemName.STONE_BEAK3, "STONE_BEAK3", ItemClassification.filler), + DungeonItemData(ItemName.STONE_BEAK4, "STONE_BEAK4", ItemClassification.filler), + DungeonItemData(ItemName.STONE_BEAK5, "STONE_BEAK5", ItemClassification.filler), + DungeonItemData(ItemName.STONE_BEAK6, "STONE_BEAK6", ItemClassification.filler), + DungeonItemData(ItemName.STONE_BEAK7, "STONE_BEAK7", ItemClassification.filler), + DungeonItemData(ItemName.STONE_BEAK8, "STONE_BEAK8", ItemClassification.filler), + DungeonItemData(ItemName.STONE_BEAK9, "STONE_BEAK9", ItemClassification.filler), + ItemData(ItemName.SONG1, "SONG1", ItemClassification.progression), + ItemData(ItemName.SONG2, "SONG2", ItemClassification.useful), + ItemData(ItemName.SONG3, "SONG3", ItemClassification.progression), + DungeonItemData(ItemName.INSTRUMENT1, "INSTRUMENT1", ItemClassification.progression), + DungeonItemData(ItemName.INSTRUMENT2, "INSTRUMENT2", ItemClassification.progression), + DungeonItemData(ItemName.INSTRUMENT3, "INSTRUMENT3", ItemClassification.progression), + DungeonItemData(ItemName.INSTRUMENT4, "INSTRUMENT4", ItemClassification.progression), + DungeonItemData(ItemName.INSTRUMENT5, "INSTRUMENT5", ItemClassification.progression), + DungeonItemData(ItemName.INSTRUMENT6, "INSTRUMENT6", ItemClassification.progression), + DungeonItemData(ItemName.INSTRUMENT7, "INSTRUMENT7", ItemClassification.progression), + DungeonItemData(ItemName.INSTRUMENT8, "INSTRUMENT8", ItemClassification.progression), + ItemData(ItemName.TRADING_ITEM_YOSHI_DOLL, "TRADING_ITEM_YOSHI_DOLL", trade_item_prog), + ItemData(ItemName.TRADING_ITEM_RIBBON, "TRADING_ITEM_RIBBON", trade_item_prog), + ItemData(ItemName.TRADING_ITEM_DOG_FOOD, "TRADING_ITEM_DOG_FOOD", trade_item_prog), + ItemData(ItemName.TRADING_ITEM_BANANAS, "TRADING_ITEM_BANANAS", trade_item_prog), + ItemData(ItemName.TRADING_ITEM_STICK, "TRADING_ITEM_STICK", trade_item_prog), + ItemData(ItemName.TRADING_ITEM_HONEYCOMB, "TRADING_ITEM_HONEYCOMB", trade_item_prog), + ItemData(ItemName.TRADING_ITEM_PINEAPPLE, "TRADING_ITEM_PINEAPPLE", trade_item_prog), + ItemData(ItemName.TRADING_ITEM_HIBISCUS, "TRADING_ITEM_HIBISCUS", trade_item_prog), + ItemData(ItemName.TRADING_ITEM_LETTER, "TRADING_ITEM_LETTER", trade_item_prog), + ItemData(ItemName.TRADING_ITEM_BROOM, "TRADING_ITEM_BROOM", trade_item_prog), + ItemData(ItemName.TRADING_ITEM_FISHING_HOOK, "TRADING_ITEM_FISHING_HOOK", trade_item_prog), + ItemData(ItemName.TRADING_ITEM_NECKLACE, "TRADING_ITEM_NECKLACE", trade_item_prog), + ItemData(ItemName.TRADING_ITEM_SCALE, "TRADING_ITEM_SCALE", trade_item_prog), + ItemData(ItemName.TRADING_ITEM_MAGNIFYING_GLASS, "TRADING_ITEM_MAGNIFYING_GLASS", trade_item_prog) +] + +ladxr_item_to_la_item_name = { + item.ladxr_id: item.item_name for item in links_awakening_items +} + +links_awakening_items_by_name = { + item.item_name : item for item in links_awakening_items +} diff --git a/worlds/ladx/LADXR/.tinyci b/worlds/ladx/LADXR/.tinyci new file mode 100644 index 0000000000..292c640496 --- /dev/null +++ b/worlds/ladx/LADXR/.tinyci @@ -0,0 +1,18 @@ +[tinyci] +enabled = True + +[build-test] +directory = _test +commands = + python3 ../main.py ../input.gbc --timeout 120 --output /dev/null + python3 ../main.py ../input.gbc --timeout 120 -s seashells=0 -s heartpiece=0 -s dungeon_items=keysanity --output /dev/null + python3 ../main.py ../input.gbc --timeout 120 -s logic=glitched -s dungeon_items=keysanity -s heartpiece=0 -s seashells=0 -s heartcontainers=0 -s instruments=1 -s owlstatues=both -s dungeonshuffle=1 -s witch=0 -s boomerang=gift -s steal=never -s goal=random --output /dev/null + python3 ../main.py ../input.gbc --timeout 120 -s logic=casual -s dungeon_items=keysy -s itempool=casual --output /dev/null + python3 ../main.py ../input.gbc --timeout 120 -s textmode=none --output /dev/null + python3 ../main.py ../input.gbc --timeout 120 -s overworld=dungeondive --output /dev/null +ignore = + python3 ../main.py ../input.gbc --timeout 120 --seashells --heartpiece --entranceshuffle simple --output /dev/null + python3 ../main.py ../input.gbc --timeout 120 --seashells --heartpiece --entranceshuffle advanced --output /dev/null + python3 ../main.py ../input.gbc --timeout 120 --seashells --heartpiece --entranceshuffle insanity --output /dev/null + python3 ../main.py ../input.gbc --timeout 120 --seashells --heartpiece --spoilerformat text --spoilerfilename /dev/null --output /dev/null + python3 ../main.py ../input.gbc --timeout 120 --seashells --heartpiece --spoilerformat json --spoilerfilename /dev/null --output /dev/null diff --git a/worlds/ladx/LADXR/LADXR_LICENSE b/worlds/ladx/LADXR/LADXR_LICENSE new file mode 100644 index 0000000000..3a83b309af --- /dev/null +++ b/worlds/ladx/LADXR/LADXR_LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Daid + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/worlds/ladx/LADXR/README.md b/worlds/ladx/LADXR/README.md new file mode 100644 index 0000000000..eeea602daf --- /dev/null +++ b/worlds/ladx/LADXR/README.md @@ -0,0 +1,25 @@ +# Legend Of Zelda: Link's Awakening DX: Randomizer +Or, LADXR for short. + +## What is this? + +See https://daid.github.io/LADXR/ + +## Usage + +The only requirements are: to use python3, and the English v1.0 ROM for Links Awakening DX. + +The proper SHA-1 for the rom is `d90ac17e9bf17b6c61624ad9f05447bdb5efc01a`. + +Basic usage: +`python3 main.py zelda.gbc` + +The script will generate a new rom with item locations shuffled. There are many options, see `-h` on the script for details. + +## Development + +This is still in the early stage of development. Important bits are: +* `randomizer.py`: Contains the actual logic to randomize the rom, and checks to make sure it can be solved. +* `logic/*.py`: Contains the logic definitions of what connects to what in the world and what it requires to access that part. +* `locations/*.py`: Contains definitions of location types, and what items can be there. As well as the code on how to place an item there. For example the Chest class has a list of all items that can be in a chest. And the needed rom patch to put that an item in a specific chest. +* `patches/*.py`: Various patches on the code that are not directly related to a specific location. But more general fixes diff --git a/worlds/ladx/LADXR/assembler.py b/worlds/ladx/LADXR/assembler.py new file mode 100644 index 0000000000..07fcfde566 --- /dev/null +++ b/worlds/ladx/LADXR/assembler.py @@ -0,0 +1,845 @@ +import binascii +from typing import Optional, Dict, ItemsView, List, Union, Tuple +import unicodedata + +from . import utils +import re + + +REGS8 = {"A": 7, "B": 0, "C": 1, "D": 2, "E": 3, "H": 4, "L": 5, "[HL]": 6} +REGS16A = {"BC": 0, "DE": 1, "HL": 2, "SP": 3} +REGS16B = {"BC": 0, "DE": 1, "HL": 2, "AF": 3} +FLAGS = {"NZ": 0x00, "Z": 0x08, "NC": 0x10, "C": 0x18} + +CONST_MAP: Dict[str, int] = {} + + +class ExprBase: + def asReg8(self) -> Optional[int]: + return None + + def isA(self, kind: str, value: Optional[str] = None) -> bool: + return False + + +class Token(ExprBase): + def __init__(self, kind: str, value: Union[str, int], line_nr: int) -> None: + self.kind = kind + self.value = value + self.line_nr = line_nr + + def isA(self, kind: str, value: Optional[str] = None) -> bool: + return self.kind == kind and (value is None or value == self.value) + + def __repr__(self) -> str: + return "[%s:%s:%d]" % (self.kind, self.value, self.line_nr) + + def asReg8(self) -> Optional[int]: + if self.kind == 'ID': + return REGS8.get(str(self.value), None) + return None + + +class REF(ExprBase): + def __init__(self, expr: ExprBase) -> None: + self.expr = expr + + def asReg8(self) -> Optional[int]: + if self.expr.isA('ID', 'HL'): + return REGS8['[HL]'] + return None + + def __repr__(self) -> str: + return "[%s]" % (self.expr) + + +class OP(ExprBase): + def __init__(self, op: str, left: ExprBase, right: Optional[ExprBase] = None): + self.op = op + self.left = left + self.right = right + + def __repr__(self) -> str: + return "%s %s %s" % (self.left, self.op, self.right) + + @staticmethod + def make(op: str, left: ExprBase, right: Optional[ExprBase] = None) -> ExprBase: + if left.isA('NUMBER') and right is not None and right.isA('NUMBER'): + assert isinstance(right, Token) and isinstance(right.value, int) + assert isinstance(left, Token) and isinstance(left.value, int) + if op == '+': + left.value += right.value + return left + if op == '-': + left.value -= right.value + return left + if op == '*': + left.value *= right.value + return left + if op == '/': + left.value //= right.value + return left + if left.isA('NUMBER') and right is None: + assert isinstance(left, Token) and isinstance(left.value, int) + if op == '+': + return left + if op == '-': + left.value = -left.value + return left + return OP(op, left, right) + + +class Tokenizer: + TOKEN_REGEX = re.compile('|'.join('(?P<%s>%s)' % pair for pair in [ + ('NUMBER', r'\d+(\.\d*)?'), + ('HEX', r'\$[0-9A-Fa-f]+'), + ('ASSIGN', r':='), + ('COMMENT', r';[^\n]+'), + ('LABEL', r':'), + ('DIRECTIVE', r'#[A-Za-z_]+'), + ('STRING', '[a-zA-Z]?"[^"]*"'), + ('ID', r'\.?[A-Za-z_][A-Za-z0-9_\.]*'), + ('OP', r'[+\-*/,\(\)]'), + ('REFOPEN', r'\['), + ('REFCLOSE', r'\]'), + ('NEWLINE', r'\n'), + ('SKIP', r'[ \t]+'), + ('MISMATCH', r'.'), + ])) + + def __init__(self, code: str) -> None: + self.__tokens: List[Token] = [] + line_num = 1 + for mo in self.TOKEN_REGEX.finditer(code): + kind = mo.lastgroup + assert kind is not None + value: Union[str, int] = mo.group() + if kind == 'MISMATCH': + print(code.split("\n")[line_num-1]) + raise RuntimeError("Syntax error on line: %d: %s\n%s", line_num, value) + elif kind == 'SKIP': + pass + elif kind == 'COMMENT': + pass + else: + if kind == 'NUMBER': + value = int(value) + elif kind == 'HEX': + value = int(str(value)[1:], 16) + kind = 'NUMBER' + elif kind == 'ID': + value = str(value).upper() + self.__tokens.append(Token(kind, value, line_num)) + if kind == 'NEWLINE': + line_num += 1 + self.__tokens.append(Token('NEWLINE', '\n', line_num)) + + def peek(self) -> Token: + return self.__tokens[0] + + def pop(self) -> Token: + return self.__tokens.pop(0) + + def expect(self, kind: str, value: Optional[str] = None) -> None: + pop = self.pop() + if not pop.isA(kind, value): + if value is not None: + raise SyntaxError("%s != %s:%s" % (pop, kind, value)) + raise SyntaxError("%s != %s" % (pop, kind)) + + def __bool__(self) -> bool: + return bool(self.__tokens) + + +class Assembler: + SIMPLE_INSTR = { + 'NOP': 0x00, + 'RLCA': 0x07, + 'RRCA': 0x0F, + 'STOP': 0x010, + 'RLA': 0x17, + 'RRA': 0x1F, + 'DAA': 0x27, + 'CPL': 0x2F, + 'SCF': 0x37, + 'CCF': 0x3F, + 'HALT': 0x76, + 'RETI': 0xD9, + 'DI': 0xF3, + 'EI': 0xFB, + } + + LINK_REL8 = 0 + LINK_ABS8 = 1 + LINK_ABS16 = 2 + + def __init__(self, base_address: Optional[int] = None) -> None: + self.__base_address = base_address or -1 + self.__result = bytearray() + self.__label: Dict[str, int] = {} + self.__constant: Dict[str, int] = {} + self.__link: Dict[int, Tuple[int, ExprBase]] = {} + self.__scope: Optional[str] = None + + self.__tok = Tokenizer("") + + def process(self, code: str) -> None: + conditional_stack = [True] + self.__tok = Tokenizer(code) + try: + while self.__tok: + start = self.__tok.pop() + if start.kind == 'NEWLINE': + pass # Empty newline + elif start.kind == 'DIRECTIVE': + if start.value == '#IF': + t = self.parseExpression() + assert isinstance(t, Token) + conditional_stack.append(conditional_stack[-1] and t.value != 0) + self.__tok.expect('NEWLINE') + elif start.value == '#ELSE': + conditional_stack[-1] = not conditional_stack[-1] and conditional_stack[-2] + self.__tok.expect('NEWLINE') + elif start.value == '#ENDIF': + conditional_stack.pop() + assert conditional_stack + self.__tok.expect('NEWLINE') + else: + raise SyntaxError(start) + elif not conditional_stack[-1]: + while not self.__tok.pop().isA('NEWLINE'): + pass + elif start.kind == 'ID': + if start.value == 'DB': + self.instrDB() + self.__tok.expect('NEWLINE') + elif start.value == 'DW': + self.instrDW() + self.__tok.expect('NEWLINE') + elif start.value == 'LD': + self.instrLD() + self.__tok.expect('NEWLINE') + elif start.value == 'LDH': + self.instrLDH() + self.__tok.expect('NEWLINE') + elif start.value == 'LDI': + self.instrLDI() + self.__tok.expect('NEWLINE') + elif start.value == 'LDD': + self.instrLDD() + self.__tok.expect('NEWLINE') + elif start.value == 'INC': + self.instrINC() + self.__tok.expect('NEWLINE') + elif start.value == 'DEC': + self.instrDEC() + self.__tok.expect('NEWLINE') + elif start.value == 'ADD': + self.instrADD() + self.__tok.expect('NEWLINE') + elif start.value == 'ADC': + self.instrALU(0x88) + self.__tok.expect('NEWLINE') + elif start.value == 'SUB': + self.instrALU(0x90) + self.__tok.expect('NEWLINE') + elif start.value == 'SBC': + self.instrALU(0x98) + self.__tok.expect('NEWLINE') + elif start.value == 'AND': + self.instrALU(0xA0) + self.__tok.expect('NEWLINE') + elif start.value == 'XOR': + self.instrALU(0xA8) + self.__tok.expect('NEWLINE') + elif start.value == 'OR': + self.instrALU(0xB0) + self.__tok.expect('NEWLINE') + elif start.value == 'CP': + self.instrALU(0xB8) + self.__tok.expect('NEWLINE') + elif start.value == 'BIT': + self.instrBIT(0x40) + self.__tok.expect('NEWLINE') + elif start.value == 'RES': + self.instrBIT(0x80) + self.__tok.expect('NEWLINE') + elif start.value == 'SET': + self.instrBIT(0xC0) + self.__tok.expect('NEWLINE') + elif start.value == 'RET': + self.instrRET() + self.__tok.expect('NEWLINE') + elif start.value == 'CALL': + self.instrCALL() + self.__tok.expect('NEWLINE') + elif start.value == 'RLC': + self.instrCB(0x00) + self.__tok.expect('NEWLINE') + elif start.value == 'RRC': + self.instrCB(0x08) + self.__tok.expect('NEWLINE') + elif start.value == 'RL': + self.instrCB(0x10) + self.__tok.expect('NEWLINE') + elif start.value == 'RR': + self.instrCB(0x18) + self.__tok.expect('NEWLINE') + elif start.value == 'SLA': + self.instrCB(0x20) + self.__tok.expect('NEWLINE') + elif start.value == 'SRA': + self.instrCB(0x28) + self.__tok.expect('NEWLINE') + elif start.value == 'SWAP': + self.instrCB(0x30) + self.__tok.expect('NEWLINE') + elif start.value == 'SRL': + self.instrCB(0x38) + self.__tok.expect('NEWLINE') + elif start.value == 'RST': + self.instrRST() + self.__tok.expect('NEWLINE') + elif start.value == 'JP': + self.instrJP() + self.__tok.expect('NEWLINE') + elif start.value == 'JR': + self.instrJR() + self.__tok.expect('NEWLINE') + elif start.value == 'PUSH': + self.instrPUSHPOP(0xC5) + self.__tok.expect('NEWLINE') + elif start.value == 'POP': + self.instrPUSHPOP(0xC1) + self.__tok.expect('NEWLINE') + elif start.value in self.SIMPLE_INSTR: + self.__result.append(self.SIMPLE_INSTR[str(start.value)]) + self.__tok.expect('NEWLINE') + elif self.__tok.peek().kind == 'LABEL': + self.__tok.pop() + self.addLabel(str(start.value)) + elif self.__tok.peek().kind == 'ASSIGN': + self.__tok.pop() + value = self.__tok.pop() + if value.kind != 'NUMBER': + raise SyntaxError(start) + self.addConstant(str(start.value), int(value.value)) + else: + raise SyntaxError(start) + else: + raise SyntaxError(start) + except SyntaxError: + print("Syntax error on line: %s" % code.split("\n")[self.__tok.peek().line_nr-1]) + raise + + def insert8(self, expr: ExprBase) -> None: + if expr.isA('NUMBER'): + assert isinstance(expr, Token) + value = int(expr.value) + else: + self.__link[len(self.__result)] = (Assembler.LINK_ABS8, expr) + value = 0 + assert 0 <= value < 256 + self.__result.append(value) + + def insertRel8(self, expr: ExprBase) -> None: + if expr.isA('NUMBER'): + assert isinstance(expr, Token) + self.__result.append(int(expr.value)) + else: + self.__link[len(self.__result)] = (Assembler.LINK_REL8, expr) + self.__result.append(0x00) + + def insert16(self, expr: ExprBase) -> None: + if expr.isA('NUMBER'): + assert isinstance(expr, Token) + value = int(expr.value) + else: + self.__link[len(self.__result)] = (Assembler.LINK_ABS16, expr) + value = 0 + assert 0 <= value <= 0xFFFF + self.__result.append(value & 0xFF) + self.__result.append(value >> 8) + + def insertString(self, string: str) -> None: + if string.startswith('"') and string.endswith('"'): + string = string[1:-1] + string = unicodedata.normalize('NFKD', string) + self.__result += string.encode("latin1", "ignore") + elif string.startswith("m\"") and string.endswith("\""): + self.__result += utils.formatText(string[2:-1].replace("|", "\n")) + else: + raise SyntaxError + + def instrLD(self) -> None: + left_param = self.parseParam() + self.__tok.expect('OP', ',') + right_param = self.parseParam() + lr8 = left_param.asReg8() + rr8 = right_param.asReg8() + if lr8 is not None and rr8 is not None: + self.__result.append(0x40 | (lr8 << 3) | rr8) + elif left_param.isA('ID', 'A') and isinstance(right_param, REF): + if right_param.expr.isA('ID', 'BC'): + self.__result.append(0x0A) + elif right_param.expr.isA('ID', 'DE'): + self.__result.append(0x1A) + elif right_param.expr.isA('ID', 'HL+'): # TODO + self.__result.append(0x2A) + elif right_param.expr.isA('ID', 'HL-'): # TODO + self.__result.append(0x3A) + elif right_param.expr.isA('ID', 'C'): + self.__result.append(0xF2) + else: + self.__result.append(0xFA) + self.insert16(right_param.expr) + elif right_param.isA('ID', 'A') and isinstance(left_param, REF): + if left_param.expr.isA('ID', 'BC'): + self.__result.append(0x02) + elif left_param.expr.isA('ID', 'DE'): + self.__result.append(0x12) + elif left_param.expr.isA('ID', 'HL+'): # TODO + self.__result.append(0x22) + elif left_param.expr.isA('ID', 'HL-'): # TODO + self.__result.append(0x32) + elif left_param.expr.isA('ID', 'C'): + self.__result.append(0xE2) + else: + self.__result.append(0xEA) + self.insert16(left_param.expr) + elif left_param.isA('ID', 'BC'): + self.__result.append(0x01) + self.insert16(right_param) + elif left_param.isA('ID', 'DE'): + self.__result.append(0x11) + self.insert16(right_param) + elif left_param.isA('ID', 'HL'): + self.__result.append(0x21) + self.insert16(right_param) + elif left_param.isA('ID', 'SP'): + if right_param.isA('ID', 'HL'): + self.__result.append(0xF9) + else: + self.__result.append(0x31) + self.insert16(right_param) + elif right_param.isA('ID', 'SP') and isinstance(left_param, REF): + self.__result.append(0x08) + self.insert16(left_param.expr) + elif lr8 is not None: + self.__result.append(0x06 | (lr8 << 3)) + self.insert8(right_param) + else: + raise SyntaxError + + def instrLDH(self) -> None: + left_param = self.parseParam() + self.__tok.expect('OP', ',') + right_param = self.parseParam() + if left_param.isA('ID', 'A') and isinstance(right_param, REF): + if right_param.expr.isA('ID', 'C'): + self.__result.append(0xF2) + else: + self.__result.append(0xF0) + self.insert8(right_param.expr) + elif right_param.isA('ID', 'A') and isinstance(left_param, REF): + if left_param.expr.isA('ID', 'C'): + self.__result.append(0xE2) + else: + self.__result.append(0xE0) + self.insert8(left_param.expr) + else: + raise SyntaxError + + def instrLDI(self) -> None: + left_param = self.parseParam() + self.__tok.expect('OP', ',') + right_param = self.parseParam() + if left_param.isA('ID', 'A') and isinstance(right_param, REF) and right_param.expr.isA('ID', 'HL'): + self.__result.append(0x2A) + elif right_param.isA('ID', 'A') and isinstance(left_param, REF) and left_param.expr.isA('ID', 'HL'): + self.__result.append(0x22) + else: + raise SyntaxError + + def instrLDD(self) -> None: + left_param = self.parseParam() + self.__tok.expect('OP', ',') + right_param = self.parseParam() + if left_param.isA('ID', 'A') and isinstance(right_param, REF) and right_param.expr.isA('ID', 'HL'): + self.__result.append(0x3A) + elif right_param.isA('ID', 'A') and isinstance(left_param, REF) and left_param.expr.isA('ID', 'HL'): + self.__result.append(0x32) + else: + raise SyntaxError + + def instrINC(self) -> None: + param = self.parseParam() + r8 = param.asReg8() + if r8 is not None: + self.__result.append(0x04 | (r8 << 3)) + elif param.isA('ID', 'BC'): + self.__result.append(0x03) + elif param.isA('ID', 'DE'): + self.__result.append(0x13) + elif param.isA('ID', 'HL'): + self.__result.append(0x23) + elif param.isA('ID', 'SP'): + self.__result.append(0x33) + else: + raise SyntaxError + + def instrDEC(self) -> None: + param = self.parseParam() + r8 = param.asReg8() + if r8 is not None: + self.__result.append(0x05 | (r8 << 3)) + elif param.isA('ID', 'BC'): + self.__result.append(0x0B) + elif param.isA('ID', 'DE'): + self.__result.append(0x1B) + elif param.isA('ID', 'HL'): + self.__result.append(0x2B) + elif param.isA('ID', 'SP'): + self.__result.append(0x3B) + else: + raise SyntaxError + + def instrADD(self) -> None: + left_param = self.parseParam() + self.__tok.expect('OP', ',') + right_param = self.parseParam() + + if left_param.isA('ID', 'A'): + rr8 = right_param.asReg8() + if rr8 is not None: + self.__result.append(0x80 | rr8) + else: + self.__result.append(0xC6) + self.insert8(right_param) + elif left_param.isA('ID', 'HL') and right_param.isA('ID') and isinstance(right_param, Token) and right_param.value in REGS16A: + self.__result.append(0x09 | REGS16A[str(right_param.value)] << 4) + elif left_param.isA('ID', 'SP'): + self.__result.append(0xE8) + self.insert8(right_param) + else: + raise SyntaxError + + def instrALU(self, code_value: int) -> None: + param = self.parseParam() + if param.isA('ID', 'A') and self.__tok.peek().isA('OP', ','): + self.__tok.pop() + param = self.parseParam() + r8 = param.asReg8() + if r8 is not None: + self.__result.append(code_value | r8) + else: + self.__result.append(code_value | 0x46) + self.insert8(param) + + def instrRST(self) -> None: + param = self.parseParam() + if param.isA('NUMBER') and isinstance(param, Token) and (int(param.value) & ~0x38) == 0: + self.__result.append(0xC7 | int(param.value)) + else: + raise SyntaxError + + def instrPUSHPOP(self, code_value: int) -> None: + param = self.parseParam() + if param.isA('ID') and isinstance(param, Token) and str(param.value) in REGS16B: + self.__result.append(code_value | (REGS16B[str(param.value)] << 4)) + else: + raise SyntaxError + + def instrJR(self) -> None: + param = self.parseParam() + if self.__tok.peek().isA('OP', ','): + self.__tok.pop() + condition = param + param = self.parseParam() + if condition.isA('ID') and isinstance(condition, Token) and str(condition.value) in FLAGS: + self.__result.append(0x20 | FLAGS[str(condition.value)]) + else: + raise SyntaxError + else: + self.__result.append(0x18) + self.insertRel8(param) + + def instrCB(self, code_value: int) -> None: + param = self.parseParam() + r8 = param.asReg8() + if r8 is not None: + self.__result.append(0xCB) + self.__result.append(code_value | r8) + else: + raise SyntaxError + + def instrBIT(self, code_value: int) -> None: + left_param = self.parseParam() + self.__tok.expect('OP', ',') + right_param = self.parseParam() + rr8 = right_param.asReg8() + if left_param.isA('NUMBER') and isinstance(left_param, Token) and rr8 is not None: + self.__result.append(0xCB) + self.__result.append(code_value | (int(left_param.value) << 3) | rr8) + else: + raise SyntaxError + + def instrRET(self) -> None: + if self.__tok.peek().isA('ID'): + condition = self.__tok.pop() + if condition.isA('ID') and condition.value in FLAGS: + self.__result.append(0xC0 | FLAGS[str(condition.value)]) + else: + raise SyntaxError + else: + self.__result.append(0xC9) + + def instrCALL(self) -> None: + param = self.parseParam() + if self.__tok.peek().isA('OP', ','): + self.__tok.pop() + condition = param + param = self.parseParam() + if condition.isA('ID') and isinstance(condition, Token) and condition.value in FLAGS: + self.__result.append(0xC4 | FLAGS[str(condition.value)]) + else: + raise SyntaxError + else: + self.__result.append(0xCD) + self.insert16(param) + + def instrJP(self) -> None: + param = self.parseParam() + if self.__tok.peek().isA('OP', ','): + self.__tok.pop() + condition = param + param = self.parseParam() + if condition.isA('ID') and isinstance(condition, Token) and condition.value in FLAGS: + self.__result.append(0xC2 | FLAGS[str(condition.value)]) + else: + raise SyntaxError + elif param.isA('ID', 'HL'): + self.__result.append(0xE9) + return + else: + self.__result.append(0xC3) + self.insert16(param) + + def instrDW(self) -> None: + param = self.parseExpression() + self.insert16(param) + while self.__tok.peek().isA('OP', ','): + self.__tok.pop() + param = self.parseExpression() + self.insert16(param) + + def instrDB(self) -> None: + param = self.parseExpression() + if param.isA('STRING'): + assert isinstance(param, Token) + self.insertString(str(param.value)) + else: + self.insert8(param) + while self.__tok.peek().isA('OP', ','): + self.__tok.pop() + param = self.parseExpression() + if param.isA('STRING'): + assert isinstance(param, Token) + self.insertString(str(param.value)) + else: + self.insert8(param) + + def addLabel(self, label: str) -> None: + if label.startswith("."): + assert self.__scope is not None + label = self.__scope + label + else: + assert "." not in label, label + self.__scope = label + assert label not in self.__label, "Duplicate label: %s" % (label) + assert label not in self.__constant, "Duplicate label: %s" % (label) + self.__label[label] = len(self.__result) + + def addConstant(self, name: str, value: int) -> None: + assert name not in self.__constant, "Duplicate constant: %s" % (name) + assert name not in self.__label, "Duplicate constant: %s" % (name) + self.__constant[name] = value + + def parseParam(self) -> ExprBase: + t = self.__tok.peek() + if t.kind == 'REFOPEN': + self.__tok.pop() + expr = self.parseExpression() + self.__tok.expect('REFCLOSE') + return REF(expr) + return self.parseExpression() + + def parseExpression(self) -> ExprBase: + t = self.parseAddSub() + return t + + def parseAddSub(self) -> ExprBase: + t = self.parseFactor() + p = self.__tok.peek() + if p.isA('OP', '+') or p.isA('OP', '-'): + self.__tok.pop() + return OP.make(str(p.value), t, self.parseAddSub()) + return t + + def parseFactor(self) -> ExprBase: + t = self.parseUnary() + p = self.__tok.peek() + if p.isA('OP', '*') or p.isA('OP', '/'): + self.__tok.pop() + return OP.make(str(p.value), t, self.parseFactor()) + return t + + def parseUnary(self) -> ExprBase: + t = self.__tok.pop() + if t.isA('OP', '-') or t.isA('OP', '+'): + return OP.make(str(t.value), self.parseUnary()) + elif t.isA('OP', '('): + result = self.parseExpression() + self.__tok.expect('OP', ')') + return result + if t.kind not in ('ID', 'NUMBER', 'STRING'): + raise SyntaxError + if t.isA('ID') and t.value in CONST_MAP: + t.kind = 'NUMBER' + t.value = CONST_MAP[str(t.value)] + elif t.isA('ID') and t.value in self.__constant: + t.kind = 'NUMBER' + t.value = self.__constant[str(t.value)] + elif t.isA('ID') and str(t.value).startswith("."): + assert self.__scope is not None + t.value = self.__scope + str(t.value) + return t + + def link(self) -> None: + for offset, (link_type, link_expr) in self.__link.items(): + expr = self.resolveExpr(link_expr) + assert expr is not None + assert expr.isA('NUMBER'), expr + assert isinstance(expr, Token) + value = int(expr.value) + if link_type == Assembler.LINK_REL8: + byte = (value - self.__base_address) - offset - 1 + assert -128 <= byte <= 127, expr + self.__result[offset] = byte & 0xFF + elif link_type == Assembler.LINK_ABS8: + assert 0 <= value <= 0xFF + self.__result[offset] = value & 0xFF + elif link_type == Assembler.LINK_ABS16: + assert self.__base_address >= 0, "Cannot place absolute values in a relocatable code piece" + assert 0 <= value <= 0xFFFF + self.__result[offset] = value & 0xFF + self.__result[offset + 1] = value >> 8 + else: + raise RuntimeError + + def resolveExpr(self, expr: Optional[ExprBase]) -> Optional[ExprBase]: + if expr is None: + return None + elif isinstance(expr, OP): + left = self.resolveExpr(expr.left) + assert left is not None + return OP.make(expr.op, left, self.resolveExpr(expr.right)) + elif isinstance(expr, Token) and expr.isA('ID') and isinstance(expr, Token) and expr.value in self.__label: + return Token('NUMBER', self.__label[str(expr.value)] + self.__base_address, expr.line_nr) + return expr + + def getResult(self) -> bytearray: + return self.__result + + def getLabels(self) -> ItemsView[str, int]: + return self.__label.items() + + +def const(name: str, value: int) -> None: + name = name.upper() + assert name not in CONST_MAP + CONST_MAP[name] = value + + +def resetConsts() -> None: + CONST_MAP.clear() + + +def ASM(code: str, base_address: Optional[int] = None, labels_result: Optional[Dict[str, int]] = None) -> bytes: + asm = Assembler(base_address) + asm.process(code) + asm.link() + if labels_result is not None: + assert base_address is not None + for label, offset in asm.getLabels(): + labels_result[label] = base_address + offset + return binascii.hexlify(asm.getResult()) + + +def allOpcodesTest() -> None: + import json + opcodes = json.load(open("Opcodes.json", "rt")) + for label in (False, True): + for prefix, codes in opcodes.items(): + for num, op in codes.items(): + if op['mnemonic'].startswith('ILLEGAL_') or op['mnemonic'] == 'PREFIX': + continue + params = [] + postfix = '' + for o in op['operands']: + name = o['name'] + if name == 'd16' or name == 'a16': + if label: + name = 'LABEL' + else: + name = '$0000' + if name == 'd8' or name == 'a8': + name = '$00' + if name == 'r8': + if label and num != '0xE8': + name = 'LABEL' + else: + name = '$00' + if name[-1] == 'H' and name[0].isnumeric(): + name = '$' + name[:-1] + if o['immediate']: + params.append(name) + else: + params.append("[%s]" % (name)) + if 'increment' in o and o['increment']: + postfix = 'I' + if 'decrement' in o and o['decrement']: + postfix = 'D' + code = op["mnemonic"] + postfix + " " + ", ".join(params) + code = code.strip() + try: + data = ASM("LABEL:\n%s" % (code), 0x0000) + if prefix == 'cbprefixed': + assert data[0:2] == b'cb' + data = data[2:] + assert data[0:2] == num[2:].encode('ascii').lower(), data[0:2] + b"!=" + num[2:].encode('ascii').lower() + except Exception as e: + print("%s\t\t|%r|\t%s" % (code, e, num)) + print(op) + + +if __name__ == "__main__": + #allOpcodesTest() + const("CONST1", 1) + const("CONST2", 2) + ASM(""" + ld a, (123) + ld hl, $1234 + 456 + ld hl, $1234 + CONST1 + ld hl, label + ld hl, label.end - label + ld c, label.end - label +label: + nop +.end: + """, 0) + ASM(""" + jr label +label: + """) + assert ASM("db 1 + 2 * 3") == b'07' diff --git a/worlds/ladx/LADXR/backgroundEditor.py b/worlds/ladx/LADXR/backgroundEditor.py new file mode 100644 index 0000000000..cab58850fa --- /dev/null +++ b/worlds/ladx/LADXR/backgroundEditor.py @@ -0,0 +1,69 @@ + +class BackgroundEditor: + def __init__(self, rom, index, *, attributes=False): + self.__index = index + self.__is_attributes = attributes + + self.tiles = {} + if attributes: + data = rom.background_attributes[index] + else: + data = rom.background_tiles[index] + idx = 0 + while data[idx] != 0x00: + addr = data[idx] << 8 | data[idx + 1] + amount = (data[idx + 2] & 0x3F) + 1 + repeat = (data[idx + 2] & 0x40) == 0x40 + vertical = (data[idx + 2] & 0x80) == 0x80 + idx += 3 + for n in range(amount): + self.tiles[addr] = data[idx] + if not repeat: + idx += 1 + addr += 0x20 if vertical else 0x01 + if repeat: + idx += 1 + + def dump(self): + if not self.tiles: + return + low = min(self.tiles.keys()) & 0xFFE0 + high = (max(self.tiles.keys()) | 0x001F) + 1 + print("0x%02x " % (self.__index) + "".join(map(lambda n: "%2X" % (n), range(0x20)))) + for addr in range(low, high, 0x20): + print("%04x " % (addr) + "".join(map(lambda n: ("%02X" % (self.tiles[addr + n])) if addr + n in self.tiles else " ", range(0x20)))) + + def store(self, rom): + # NOTE: This is not a very good encoder, but the background back has so much free space that we really don't care. + # Improvements can be done to find long sequences of bytes and store those as repeated. + result = bytearray() + low = min(self.tiles.keys()) + high = max(self.tiles.keys()) + 1 + while low < high: + if low not in self.tiles: + low += 1 + continue + different_count = 1 + while low + different_count in self.tiles and different_count < 0x40: + different_count += 1 + same_count = 1 + while low + same_count in self.tiles and self.tiles[low] == self.tiles[low + same_count] and same_count < 0x40: + same_count += 1 + if same_count > different_count - 4 and same_count > 2: + result.append(low >> 8) + result.append(low & 0xFF) + result.append((same_count - 1) | 0x40) + result.append(self.tiles[low]) + low += same_count + else: + result.append(low >> 8) + result.append(low & 0xFF) + result.append(different_count - 1) + for n in range(different_count): + result.append(self.tiles[low + n]) + low += different_count + result.append(0x00) + if self.__is_attributes: + rom.background_attributes[self.__index] = result + else: + rom.background_tiles[self.__index] = result diff --git a/worlds/ladx/LADXR/checkMetadata.py b/worlds/ladx/LADXR/checkMetadata.py new file mode 100644 index 0000000000..e8b91c05e8 --- /dev/null +++ b/worlds/ladx/LADXR/checkMetadata.py @@ -0,0 +1,270 @@ +class CheckMetadata: + __slots__ = "name", "area" + def __init__(self, name, area): + self.name = name + self.area = area + + def __repr__(self): + result = "%s - %s" % (self.area, self.name) + return result + + +checkMetadataTable = { + "None": CheckMetadata("Unset Room", "None"), + "0x1F5": CheckMetadata("Boomerang Guy Item", "Toronbo Shores"), #http://artemis251.fobby.net/zelda/maps/underworld1/01F5.GIF + "0x2A3": CheckMetadata("Tarin's Gift", "Mabe Village"), #http://artemis251.fobby.net/zelda/maps/underworld2/02A3.GIF + "0x301-0": CheckMetadata("Tunic Fairy Item 1", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/0301.GIF + "0x301-1": CheckMetadata("Tunic Fairy Item 2", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/0301.GIF + "0x2A2": CheckMetadata("Witch Item", "Koholint Prairie"), #http://artemis251.fobby.net/zelda/maps/underworld2/02A2.GIF + "0x2A1": CheckMetadata("Shop 200 Item", "Mabe Village"), #http://artemis251.fobby.net/zelda/maps/underworld2/02A1.GIF + "0x2A7": CheckMetadata("Shop 980 Item", "Mabe Village"), #http://artemis251.fobby.net/zelda/maps/underworld2/02A1.GIF + "0x2A1-2": CheckMetadata("Shop 10 Item", "Mabe Village"), #http://artemis251.fobby.net/zelda/maps/underworld2/02A1.GIF + "0x113": CheckMetadata("Pit Button Chest", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/0113.GIF + "0x115": CheckMetadata("Four Zol Chest", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/0115.GIF + "0x10E": CheckMetadata("Spark, Mini-Moldorm Chest", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/010E.GIF + "0x116": CheckMetadata("Hardhat Beetles Key", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/0116.GIF + "0x10D": CheckMetadata("Mini-Moldorm Spawn Chest", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/010D.GIF + "0x114": CheckMetadata("Two Stalfos, Two Keese Chest", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/0114.GIF + "0x10C": CheckMetadata("Bombable Wall Seashell Chest", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/010C.GIF + "0x103-Owl": CheckMetadata("Spiked Beetle Owl", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/0103.GIF + "0x104-Owl": CheckMetadata("Movable Block Owl", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/0104.GIF + "0x11D": CheckMetadata("Feather Chest", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/011D.GIF + "0x108": CheckMetadata("Nightmare Key Chest", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/0108.GIF + "0x10A": CheckMetadata("Three of a Kind Chest", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/010A.GIF + "0x10A-Owl": CheckMetadata("Three of a Kind Owl", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/010A.GIF + "0x106": CheckMetadata("Moldorm Heart Container", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/0106.GIF + "0x102": CheckMetadata("Full Moon Cello", "Tail Cave"), #http://artemis251.fobby.net/zelda/maps/underworld1/0102.GIF + "0x136": CheckMetadata("Entrance Chest", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0136.GIF + "0x12E": CheckMetadata("Hardhat Beetle Pit Chest", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/012E.GIF + "0x132": CheckMetadata("Two Stalfos Key", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0132.GIF + "0x137": CheckMetadata("Mask-Mimic Chest", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0137.GIF + "0x133-Owl": CheckMetadata("Switch Owl", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0133.GIF + "0x138": CheckMetadata("First Switch Locked Chest", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0138.GIF + "0x139": CheckMetadata("Button Spawn Chest", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0139.GIF + "0x134": CheckMetadata("Mask-Mimic Key", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0134.GIF + "0x126": CheckMetadata("Vacuum Mouth Chest", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0126.GIF + "0x121": CheckMetadata("Outside Boo Buddies Room Chest", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0121.GIF + "0x129-Owl": CheckMetadata("After Hinox Owl", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0129.GIF + "0x12F-Owl": CheckMetadata("Before First Staircase Owl", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/012F.GIF + "0x120": CheckMetadata("Boo Buddies Room Chest", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0120.GIF + "0x122": CheckMetadata("Second Switch Locked Chest", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0122.GIF + "0x127": CheckMetadata("Enemy Order Room Chest", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/0127.GIF + "0x12B": CheckMetadata("Genie Heart Container", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/012B.GIF + "0x12A": CheckMetadata("Conch Horn", "Bottle Grotto"), #http://artemis251.fobby.net/zelda/maps/underworld1/012A.GIF + "0x153": CheckMetadata("Vacuum Mouth Chest", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0153.GIF + "0x151": CheckMetadata("Two Bombite, Sword Stalfos, Zol Chest", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0151.GIF + "0x14F": CheckMetadata("Four Zol Chest", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/014F.GIF + "0x14E": CheckMetadata("Two Stalfos, Zol Chest", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/014E.GIF + "0x154": CheckMetadata("North Key Room Key", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0154.GIF + "0x154-Owl": CheckMetadata("North Key Room Owl", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0154.GIF + "0x150": CheckMetadata("Sword Stalfos, Keese Switch Chest", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0150.GIF + "0x14C": CheckMetadata("Zol Switch Chest", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/014C.GIF + "0x155": CheckMetadata("West Key Room Key", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0155.GIF + "0x158": CheckMetadata("South Key Room Key", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0158.GIF + "0x14D": CheckMetadata("After Stairs Key", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/014D.GIF + "0x147-Owl": CheckMetadata("Tile Arrow Owl", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0147.GIF + "0x147": CheckMetadata("Tile Arrow Ledge Chest", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0147.GIF + "0x146": CheckMetadata("Boots Chest", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0146.GIF + "0x142": CheckMetadata("Three Zol, Stalfos Chest", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0142.GIF + "0x141": CheckMetadata("Three Bombite Key", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0141.GIF + "0x148": CheckMetadata("Two Zol, Two Pairodd Key", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0148.GIF + "0x144": CheckMetadata("Two Zol, Stalfos Ledge Chest", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0144.GIF + "0x140-Owl": CheckMetadata("Flying Bomb Owl", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0140.GIF + "0x15B": CheckMetadata("Nightmare Door Key", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/015B.GIF + "0x15A": CheckMetadata("Slime Eye Heart Container", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/015A.GIF + "0x159": CheckMetadata("Sea Lily's Bell", "Key Cavern"), #http://artemis251.fobby.net/zelda/maps/underworld1/0159.GIF + "0x179": CheckMetadata("Watery Statue Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/0179.GIF + "0x16A": CheckMetadata("NW of Boots Pit Ledge Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/016A.GIF + "0x178": CheckMetadata("Two Spiked Beetle, Zol Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/0178.GIF + "0x17B": CheckMetadata("Crystal Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/017B.GIF + "0x171": CheckMetadata("Lower Bomb Locked Watery Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/0171.GIF + "0x165": CheckMetadata("Upper Bomb Locked Watery Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/0165.GIF + "0x175": CheckMetadata("Flipper Locked Before Boots Pit Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/0175.GIF + "0x16F-Owl": CheckMetadata("Spiked Beetle Owl", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/016F.GIF + "0x169": CheckMetadata("Pit Key", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/0169.GIF + "0x16E": CheckMetadata("Flipper Locked After Boots Pit Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/016E.GIF + "0x16D": CheckMetadata("Blob Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/016D.GIF + "0x168": CheckMetadata("Spark Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/0168.GIF + "0x160": CheckMetadata("Flippers Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/0160.GIF + "0x176": CheckMetadata("Nightmare Key Ledge Chest", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/0176.GIF + "0x166": CheckMetadata("Angler Fish Heart Container", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/01FF.GIF + "0x162": CheckMetadata("Surf Harp", "Angler's Tunnel"), #http://artemis251.fobby.net/zelda/maps/underworld1/0162.GIF + "0x1A0": CheckMetadata("Entrance Hookshottable Chest", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/01A0.GIF + "0x19E": CheckMetadata("Spark, Two Iron Mask Chest", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/019E.GIF + "0x181": CheckMetadata("Crystal Key", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/0181.GIF + "0x19A-Owl": CheckMetadata("Crystal Owl", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/019A.GIF + "0x19B": CheckMetadata("Flying Bomb Chest South", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/019B.GIF + "0x197": CheckMetadata("Three Iron Mask Chest", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/0197.GIF + "0x196": CheckMetadata("Hookshot Note Chest", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/0196.GIF + "0x18A-Owl": CheckMetadata("Star Owl", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/018A.GIF + "0x18E": CheckMetadata("Two Stalfos, Star Pit Chest", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/018E.GIF + "0x188": CheckMetadata("Swort Stalfos, Star, Bridge Chest", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/0188.GIF + "0x18F": CheckMetadata("Flying Bomb Chest East", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/018F.GIF + "0x180": CheckMetadata("Master Stalfos Item", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/0180.GIF + "0x183": CheckMetadata("Three Stalfos Chest", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/0183.GIF + "0x186": CheckMetadata("Nightmare Key/Torch Cross Chest", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/0186.GIF + "0x185": CheckMetadata("Slime Eel Heart Container", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/0185.GIF + "0x182": CheckMetadata("Wind Marimba", "Catfish's Maw"), #http://artemis251.fobby.net/zelda/maps/underworld1/0182.GIF + "0x1CF": CheckMetadata("Mini-Moldorm, Spark Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01CF.GIF + "0x1C9": CheckMetadata("Flying Heart, Statue Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01C9.GIF + "0x1BB-Owl": CheckMetadata("Corridor Owl", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01BB.GIF + "0x1CE": CheckMetadata("L2 Bracelet Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01CE.GIF + "0x1C0": CheckMetadata("Three Wizzrobe, Switch Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01C0.GIF + "0x1B9": CheckMetadata("Stairs Across Statues Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01B9.GIF + "0x1B3": CheckMetadata("Switch, Star Above Statues Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01B3.GIF + "0x1B4": CheckMetadata("Two Wizzrobe Key", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01B4.GIF + "0x1B0": CheckMetadata("Top Left Horse Heads Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01B0.GIF + "0x06C": CheckMetadata("Raft Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/overworld/006C.GIF + "0x1BE": CheckMetadata("Water Tektite Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01BE.GIF + "0x1D1": CheckMetadata("Four Wizzrobe Ledge Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01D1.GIF + "0x1D7-Owl": CheckMetadata("Blade Trap Owl", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01D7.GIF + "0x1C3": CheckMetadata("Tile Room Key", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01C3.GIF + "0x1B1": CheckMetadata("Top Right Horse Heads Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01B1.GIF + "0x1B6-Owl": CheckMetadata("Pot Owl", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01B6.GIF + "0x1B6": CheckMetadata("Pot Locked Chest", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01B6.GIF + "0x1BC": CheckMetadata("Facade Heart Container", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01BC.GIF + "0x1B5": CheckMetadata("Coral Triangle", "Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld1/01B5.GIF + "0x210": CheckMetadata("Entrance Key", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/0210.GIF + "0x216-Owl": CheckMetadata("Ball Owl", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/0216.GIF + "0x212": CheckMetadata("Horse Head, Bubble Chest", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/0212.GIF + "0x204-Owl": CheckMetadata("Beamos Owl", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/0204.GIF + "0x204": CheckMetadata("Beamos Ledge Chest", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/0204.GIF + "0x209": CheckMetadata("Switch Wrapped Chest", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/0209.GIF + "0x211": CheckMetadata("Three of a Kind, No Pit Chest", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/0211.GIF + "0x21B": CheckMetadata("Hinox Key", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/021B.GIF + "0x201": CheckMetadata("Kirby Ledge Chest", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/0201.GIF + "0x21C-Owl": CheckMetadata("Three of a Kind, Pit Owl", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/021C.GIF + "0x21C": CheckMetadata("Three of a Kind, Pit Chest", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/021C.GIF + "0x224": CheckMetadata("Nightmare Key/After Grim Creeper Chest", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/0224.GIF + "0x21A": CheckMetadata("Mirror Shield Chest", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/021A.GIF + "0x220": CheckMetadata("Conveyor Beamos Chest", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/0220.GIF + "0x223": CheckMetadata("Evil Eagle Heart Container", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/02E8.GIF + "0x22C": CheckMetadata("Organ of Evening Calm", "Eagle's Tower"), #http://artemis251.fobby.net/zelda/maps/underworld2/022C.GIF + "0x24F": CheckMetadata("Push Block Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/024F.GIF + "0x24D": CheckMetadata("Left of Hinox Zamboni Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/024D.GIF + "0x25C": CheckMetadata("Vacuum Mouth Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/025C.GIF + "0x24C": CheckMetadata("Left Vire Key", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/024C.GIF + "0x255": CheckMetadata("Spark, Pit Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0255.GIF + "0x246": CheckMetadata("Two Torches Room Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0246.GIF + "0x253-Owl": CheckMetadata("Beamos Owl", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0253.GIF + "0x259": CheckMetadata("Right Lava Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0259.GIF + "0x25A": CheckMetadata("Zamboni, Two Zol Key", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/025A.GIF + "0x25F": CheckMetadata("Four Ropes Pot Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/025F.GIF + "0x245-Owl": CheckMetadata("Bombable Blocks Owl", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0245.GIF + "0x23E": CheckMetadata("Gibdos on Cracked Floor Key", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/023E.GIF + "0x235": CheckMetadata("Lava Ledge Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0235.GIF + "0x237": CheckMetadata("Magic Rod Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0237.GIF + "0x240": CheckMetadata("Beamos Blocked Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0240.GIF + "0x23D": CheckMetadata("Dodongo Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/023D.GIF + "0x000": CheckMetadata("Outside Heart Piece", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/overworld/0000.GIF + "0x241": CheckMetadata("Lava Arrow Statue Key", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0241.GIF + "0x241-Owl": CheckMetadata("Lava Arrow Statue Owl", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0241.GIF + "0x23A": CheckMetadata("West of Boss Door Ledge Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/023A.GIF + "0x232": CheckMetadata("Nightmare Key/Big Zamboni Chest", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0232.GIF + "0x234": CheckMetadata("Hot Head Heart Container", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0234.GIF + "0x230": CheckMetadata("Thunder Drum", "Turtle Rock"), #http://artemis251.fobby.net/zelda/maps/underworld2/0230.GIF + "0x314": CheckMetadata("Lower Small Key", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/0314.GIF + "0x308-Owl": CheckMetadata("Upper Key Owl", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/0308.GIF + "0x308": CheckMetadata("Upper Small Key", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/0308.GIF + "0x30F-Owl": CheckMetadata("Entrance Owl", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/030F.GIF + "0x30F": CheckMetadata("Entrance Chest", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/030F.GIF + "0x311": CheckMetadata("Two Socket Chest", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/0311.GIF + "0x302": CheckMetadata("Nightmare Key Chest", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/0302.GIF + "0x306": CheckMetadata("Zol Chest", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/0306.GIF + "0x307": CheckMetadata("Bullshit Room", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/0307.GIF + "0x30A-Owl": CheckMetadata("Puzzowl", "Color Dungeon"), #http://artemis251.fobby.net/zelda/maps/underworld3/030A.GIF + "0x2BF": CheckMetadata("Dream Hut East", "Mabe Village"), #http://artemis251.fobby.net/zelda/maps/underworld2/02BF.GIF + "0x2BE": CheckMetadata("Dream Hut West", "Mabe Village"), #http://artemis251.fobby.net/zelda/maps/underworld2/02BE.GIF + "0x2A4": CheckMetadata("Well Heart Piece", "Mabe Village"), #http://artemis251.fobby.net/zelda/maps/underworld2/02A4.GIF + "0x2B1": CheckMetadata("Fishing Game Heart Piece", "Mabe Village"), #http://artemis251.fobby.net/zelda/maps/underworld2/02B1.GIF + "0x0A3": CheckMetadata("Bush Field", "Mabe Village"), #http://artemis251.fobby.net/zelda/maps/overworld/00A3.GIF + "0x2B2": CheckMetadata("Dog House Dig", "Mabe Village"), #http://artemis251.fobby.net/zelda/maps/underworld2/02B2.GIF + "0x0D2": CheckMetadata("Outside D1 Tree Bonk", "Toronbo Shores"), #http://artemis251.fobby.net/zelda/maps/overworld/00D2.GIF + "0x0E5": CheckMetadata("West of Ghost House Chest", "Toronbo Shores"), #http://artemis251.fobby.net/zelda/maps/overworld/00E5.GIF + "0x1E3": CheckMetadata("Ghost House Barrel", "Martha's Bay"), #http://artemis251.fobby.net/zelda/maps/underworld1/01E3.GIF + "0x044": CheckMetadata("Heart Piece of Shame", "Koholint Prairie"), #http://artemis251.fobby.net/zelda/maps/overworld/0044.GIF + "0x071": CheckMetadata("Two Zol, Moblin Chest", "Mysterious Woods"), #http://artemis251.fobby.net/zelda/maps/overworld/0071.GIF + "0x1E1": CheckMetadata("Mad Batter", "Mysterious Woods"), #http://artemis251.fobby.net/zelda/maps/underworld1/01E1.GIF + "0x034": CheckMetadata("Swampy Chest", "Goponga Swamp"), #http://artemis251.fobby.net/zelda/maps/overworld/0034.GIF + "0x041": CheckMetadata("Tail Key Chest", "Mysterious Woods"), #http://artemis251.fobby.net/zelda/maps/overworld/0041.GIF + "0x2BD": CheckMetadata("Cave Crystal Chest", "Mysterious Woods"), #http://artemis251.fobby.net/zelda/maps/underworld2/02BD.GIF + "0x2AB": CheckMetadata("Cave Skull Heart Piece", "Mysterious Woods"), #http://artemis251.fobby.net/zelda/maps/underworld2/02AB.GIF + "0x2B3": CheckMetadata("Hookshot Cave", "Mysterious Woods"), #http://artemis251.fobby.net/zelda/maps/underworld2/02B3.GIF + "0x2AE": CheckMetadata("Write Cave West", "Goponga Swamp"), #http://artemis251.fobby.net/zelda/maps/underworld2/02AE.GIF + "0x011-Owl": CheckMetadata("North of Write Owl", "Goponga Swamp"), #http://artemis251.fobby.net/zelda/maps/overworld/0011.GIF #might come out as "0x11 + "0x2AF": CheckMetadata("Write Cave East", "Goponga Swamp"), #http://artemis251.fobby.net/zelda/maps/underworld2/02AF.GIF + "0x035-Owl": CheckMetadata("Moblin Cave Owl", "Tal Tal Heights"), #http://artemis251.fobby.net/zelda/maps/overworld/0035.GIF + "0x2DF": CheckMetadata("Graveyard Connector", "Koholint Prairie"), #http://artemis251.fobby.net/zelda/maps/underworld2/02DF.GIF + "0x074": CheckMetadata("Ghost Grave Dig", "Koholint Prairie"), #http://artemis251.fobby.net/zelda/maps/overworld/0074.GIF + "0x2E2": CheckMetadata("Moblin Cave", "Tal Tal Heights"), #http://artemis251.fobby.net/zelda/maps/underworld2/02E2.GIF + "0x2CD": CheckMetadata("Cave East of Mabe", "Ukuku Prairie"), #http://artemis251.fobby.net/zelda/maps/underworld2/02CD.GIF + "0x2F4": CheckMetadata("Boots 'n' Bomb Cave Chest", "Ukuku Prairie"), #http://artemis251.fobby.net/zelda/maps/underworld2/02F4.GIF + "0x2E5": CheckMetadata("Boots 'n' Bomb Cave Bombable Wall", "Ukuku Prairie"), #http://artemis251.fobby.net/zelda/maps/underworld2/02E5.GIF + "0x0A5": CheckMetadata("Outside D3 Ledge Dig", "Ukuku Prairie"), #http://artemis251.fobby.net/zelda/maps/overworld/00A5.GIF + "0x0A6": CheckMetadata("Outside D3 Island Bush", "Ukuku Prairie"), #http://artemis251.fobby.net/zelda/maps/overworld/00A6.GIF + "0x08B": CheckMetadata("East of Seashell Mansion Bush", "Ukuku Prairie"), #http://artemis251.fobby.net/zelda/maps/overworld/008B.GIF + "0x0A4": CheckMetadata("East of Mabe Tree Bonk", "Ukuku Prairie"), #http://artemis251.fobby.net/zelda/maps/overworld/00A4.GIF + "0x2E9": CheckMetadata("Seashell Mansion", "Ukuku Prairie"), + "0x1FD": CheckMetadata("Boots Pit", "Kanalet Castle"), #http://artemis251.fobby.net/zelda/maps/underworld1/01FD.GIF + "0x0B9": CheckMetadata("Rock Seashell", "Donut Plains"), #http://artemis251.fobby.net/zelda/maps/overworld/00B9.GIF + "0x0E9": CheckMetadata("Lone Bush", "Martha's Bay"), #http://artemis251.fobby.net/zelda/maps/overworld/00E9.GIF + "0x0F8": CheckMetadata("Island Bush of Destiny", "Martha's Bay"), #http://artemis251.fobby.net/zelda/maps/overworld/00F8.GIF + "0x0A8": CheckMetadata("Donut Plains Ledge Dig", "Donut Plains"), #http://artemis251.fobby.net/zelda/maps/overworld/00A8.GIF + "0x0A8-Owl": CheckMetadata("Donut Plains Ledge Owl", "Donut Plains"), #http://artemis251.fobby.net/zelda/maps/overworld/00A8.GIF + "0x1E0": CheckMetadata("Mad Batter", "Martha's Bay"), #http://artemis251.fobby.net/zelda/maps/underworld1/01E0.GIF + "0x0C6-Owl": CheckMetadata("Slime Key Owl", "Pothole Field"), #http://artemis251.fobby.net/zelda/maps/overworld/00C6.GIF + "0x0C6": CheckMetadata("Slime Key Dig", "Pothole Field"), #http://artemis251.fobby.net/zelda/maps/overworld/00C6.GIF + "0x2C8": CheckMetadata("Under Richard's House", "Pothole Field"), #http://artemis251.fobby.net/zelda/maps/underworld2/02C8.GIF + "0x078": CheckMetadata("In the Moat Heart Piece", "Kanalet Castle"), #http://artemis251.fobby.net/zelda/maps/overworld/0078.GIF + "0x05A": CheckMetadata("Bomberman Meets Whack-a-mole Leaf", "Kanalet Castle"), #http://artemis251.fobby.net/zelda/maps/overworld/005A.GIF + "0x058": CheckMetadata("Crow Rock Leaf", "Kanalet Castle"), #http://artemis251.fobby.net/zelda/maps/overworld/0058.GIF + "0x2D2": CheckMetadata("Darknut, Zol, Bubble Leaf", "Kanalet Castle"), #http://artemis251.fobby.net/zelda/maps/underworld2/02D2.GIF + "0x2C5": CheckMetadata("Bombable Darknut Leaf", "Kanalet Castle"), #http://artemis251.fobby.net/zelda/maps/underworld2/02C5.GIF + "0x2C6": CheckMetadata("Ball and Chain Darknut Leaf", "Kanalet Castle"), #http://artemis251.fobby.net/zelda/maps/underworld2/02C6.GIF + "0x0DA": CheckMetadata("Peninsula Dig", "Martha's Bay"), #http://artemis251.fobby.net/zelda/maps/overworld/00DA.GIF + "0x0DA-Owl": CheckMetadata("Peninsula Owl", "Martha's Bay"), #http://artemis251.fobby.net/zelda/maps/overworld/00DA.GIF + "0x0CF-Owl": CheckMetadata("Desert Owl", "Yarna Desert"), #http://artemis251.fobby.net/zelda/maps/overworld/00CF.GIF + "0x2E6": CheckMetadata("Bomb Arrow Cave", "Yarna Desert"), #http://artemis251.fobby.net/zelda/maps/underworld2/02E6.GIF + "0x1E8": CheckMetadata("Cave Under Lanmola", "Yarna Desert"), #http://artemis251.fobby.net/zelda/maps/underworld1/01E8.GIF + "0x0FF": CheckMetadata("Rock Seashell", "Yarna Desert"), #http://artemis251.fobby.net/zelda/maps/overworld/00FF.GIF + "0x018": CheckMetadata("Access Tunnel Exterior", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/overworld/0018.GIF + "0x2BB": CheckMetadata("Access Tunnel Interior", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/underworld2/02BB.GIF + "0x28A": CheckMetadata("Paphl Cave", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/underworld2/028A.GIF + "0x1F2": CheckMetadata("Damp Cave Heart Piece", "Tal Tal Heights"), #http://artemis251.fobby.net/zelda/maps/underworld1/01F2.GIF + "0x2FC": CheckMetadata("Under Armos Cave", "Southern Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld2/02FC.GIF + "0x08F-Owl": CheckMetadata("Outside Owl", "Southern Face Shrine"), #http://artemis251.fobby.net/zelda/maps/overworld/008F.GIF + "0x05C": CheckMetadata("West", "Rapids Ride"), #http://artemis251.fobby.net/zelda/maps/overworld/005C.GIF + "0x05D": CheckMetadata("East", "Rapids Ride"), #http://artemis251.fobby.net/zelda/maps/overworld/005D.GIF + "0x05D-Owl": CheckMetadata("Owl", "Rapids Ride"), #http://artemis251.fobby.net/zelda/maps/overworld/005D.GIF + "0x01E-Owl": CheckMetadata("Outside D7 Owl", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/overworld/001E.GIF + "0x00C": CheckMetadata("Bridge Rock", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/overworld/000C.GIF + "0x2F2": CheckMetadata("Five Chest Game", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/underworld2/02F2.GIF + "0x01D": CheckMetadata("Outside Five Chest Game", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/overworld/001D.GIF + "0x004": CheckMetadata("Outside Mad Batter", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/overworld/0004.GIF + "0x1E2": CheckMetadata("Mad Batter", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/underworld1/01E2.GIF + "0x2BA": CheckMetadata("Access Tunnel Bombable Heart Piece", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/underworld2/02BA.GIF + "0x0F2": CheckMetadata("Sword on the Beach", "Toronbo Shores"), #http://artemis251.fobby.net/zelda/maps/overworld/00F2.GIF + "0x050": CheckMetadata("Toadstool", "Mysterious Woods"), #http://artemis251.fobby.net/zelda/maps/overworld/0050.GIF + "0x0CE": CheckMetadata("Lanmola", "Yarna Desert"), #http://artemis251.fobby.net/zelda/maps/overworld/00CE.GIF + "0x27F": CheckMetadata("Armos Knight", "Southern Face Shrine"), #http://artemis251.fobby.net/zelda/maps/underworld2/027F.GIF + "0x27A": CheckMetadata("Bird Key Cave", "Tal Tal Mountains"), #http://artemis251.fobby.net/zelda/maps/underworld2/027A.GIF + "0x092": CheckMetadata("Ballad of the Wind Fish", "Mabe Village"), + "0x2FD": CheckMetadata("Manbo's Mambo", "Tal Tal Heights"), + "0x2FB": CheckMetadata("Mamu", "Ukuku Prairie"), + "0x1E4": CheckMetadata("Rooster", "Mabe Village"), + + "0x2A0-Trade": CheckMetadata("Trendy Game", "Mabe Village"), + "0x2A6-Trade": CheckMetadata("Papahl's Wife", "Mabe Village"), + "0x2B2-Trade": CheckMetadata("YipYip", "Mabe Village"), + "0x2FE-Trade": CheckMetadata("Banana Sale", "Toronbo Shores"), + "0x07B-Trade": CheckMetadata("Kiki", "Ukuku Prairie"), + "0x087-Trade": CheckMetadata("Honeycomb", "Ukuku Prairie"), + "0x2D7-Trade": CheckMetadata("Bear Cook", "Animal Village"), + "0x019-Trade": CheckMetadata("Papahl", "Tal Tal Heights"), + "0x2D9-Trade": CheckMetadata("Goat", "Animal Village"), + "0x2A8-Trade": CheckMetadata("MrWrite", "Goponga Swamp"), + "0x0CD-Trade": CheckMetadata("Grandma", "Animal Village"), + "0x2F5-Trade": CheckMetadata("Fisher", "Martha's Bay"), + "0x0C9-Trade": CheckMetadata("Mermaid", "Martha's Bay"), + "0x297-Trade": CheckMetadata("Mermaid Statue", "Martha's Bay"), +} diff --git a/worlds/ladx/LADXR/entityData.py b/worlds/ladx/LADXR/entityData.py new file mode 100644 index 0000000000..405a1ea65c --- /dev/null +++ b/worlds/ladx/LADXR/entityData.py @@ -0,0 +1,561 @@ +COUNT = 0xFB +NAME = [ + "ARROW", + "BOOMERANG", + "BOMB", + "HOOKSHOT_CHAIN", + "HOOKSHOT_HIT", + "LIFTABLE_ROCK", + "PUSHED_BLOCK", + "CHEST_WITH_ITEM", + "MAGIC_POWDER_SPRINKLE", + "OCTOROCK", + "OCTOROCK_ROCK", + "MOBLIN", + "MOBLIN_ARROW", + "TEKTITE", + "LEEVER", + "ARMOS_STATUE", + "HIDING_GHINI", + "GIANT_GHINI", + "GHINI", + "BROKEN_HEART_CONTAINER", + "MOBLIN_SWORD", + "ANTI_FAIRY", + "SPARK_COUNTER_CLOCKWISE", + "SPARK_CLOCKWISE", + "POLS_VOICE", + "KEESE", + "STALFOS_AGGRESSIVE", + "GEL", + "MINI_GEL", + "DISABLED", + "STALFOS_EVASIVE", + "GIBDO", + "HARDHAT_BEETLE", + "WIZROBE", + "WIZROBE_PROJECTILE", + "LIKE_LIKE", + "IRON_MASK", + "SMALL_EXPLOSION_ENEMY", + "SMALL_EXPLOSION_ENEMY_2", + "SPIKE_TRAP", + "MIMIC", + "MINI_MOLDORM", + "LASER", + "LASER_BEAM", + "SPIKED_BEETLE", + "DROPPABLE_HEART", + "DROPPABLE_RUPEE", + "DROPPABLE_FAIRY", + "KEY_DROP_POINT", + "SWORD", + "32", + "PIECE_OF_POWER", + "GUARDIAN_ACORN", + "HEART_PIECE", + "HEART_CONTAINER", + "DROPPABLE_ARROWS", + "DROPPABLE_BOMBS", + "INSTRUMENT_OF_THE_SIRENS", + "SLEEPY_TOADSTOOL", + "DROPPABLE_MAGIC_POWDER", + "HIDING_SLIME_KEY", + "DROPPABLE_SECRET_SEASHELL", + "MARIN", + "RACOON", + "WITCH", + "OWL_EVENT", + "OWL_STATUE", + "SEASHELL_MANSION_TREES", + "YARNA_TALKING_BONES", + "BOULDERS", + "MOVING_BLOCK_LEFT_TOP", + "MOVING_BLOCK_LEFT_BOTTOM", + "MOVING_BLOCK_BOTTOM_LEFT", + "MOVING_BLOCK_BOTTOM_RIGHT", + "COLOR_DUNGEON_BOOK", + "POT", + "DISABLED", + "SHOP_OWNER", + "4D", + "TRENDY_GAME_OWNER", + "BOO_BUDDY", + "KNIGHT", + "TRACTOR_DEVICE", + "TRACTOR_DEVICE_REVERSE", + "FISHERMAN_FISHING_GAME", + "BOUNCING_BOMBITE", + "TIMER_BOMBITE", + "PAIRODD", + "PAIRODD_PROJECTILE", + "MOLDORM", + "FACADE", + "SLIME_EYE", + "GENIE", + "SLIME_EEL", + "GHOMA", + "MASTER_STALFOS", + "DODONGO_SNAKE", + "WARP", + "HOT_HEAD", + "EVIL_EAGLE", + "SOUTH_FACE_SHRINE_DOOR", + "ANGLER_FISH", + "CRYSTAL_SWITCH", + "67", + "68", + "MOVING_BLOCK_MOVER", + "RAFT_RAFT_OWNER", + "TEXT_DEBUGGER", + "CUCCO", + "BOW_WOW", + "BUTTERFLY", + "DOG", + "KID_70", + "KID_71", + "KID_72", + "KID_73", + "PAPAHLS_WIFE", + "GRANDMA_ULRIRA", + "MR_WRITE", + "GRANDPA_ULRIRA", + "YIP_YIP", + "MADAM_MEOWMEOW", + "CROW", + "CRAZY_TRACY", + "GIANT_GOPONGA_FLOWER", + "GOPONGA_FLOWER_PROJECTILE", + "GOPONGA_FLOWER", + "TURTLE_ROCK_HEAD", + "TELEPHONE", + "ROLLING_BONES", + "ROLLING_BONES_BAR", + "DREAM_SHRINE_BED", + "BIG_FAIRY", + "MR_WRITES_BIRD", + "FLOATING_ITEM", + "DESERT_LANMOLA", + "ARMOS_KNIGHT", + "HINOX", + "TILE_GLINT_SHOWN", + "TILE_GLINT_HIDDEN", + "8C", + "8D", + "CUE_BALL", + "MASKED_MIMIC_GORIYA", + "THREE_OF_A_KIND", + "ANTI_KIRBY", + "SMASHER", + "MAD_BOMBER", + "KANALET_BOMBABLE_WALL", + "RICHARD", + "RICHARD_FROG", + "DIVE_SPOT", + "HORSE_PIECE", + "WATER_TEKTITE", + "FLYING_TILES", + "HIDING_GEL", + "STAR", + "LIFTABLE_STATUE", + "FIREBALL_SHOOTER", + "GOOMBA", + "PEAHAT", + "SNAKE", + "PIRANHA_PLANT", + "SIDE_VIEW_PLATFORM_HORIZONTAL", + "SIDE_VIEW_PLATFORM_VERTICAL", + "SIDE_VIEW_PLATFORM", + "SIDE_VIEW_WEIGHTS", + "SMASHABLE_PILLAR", + "WRECKING_BALL", + "BLOOPER", + "CHEEP_CHEEP_HORIZONTAL", + "CHEEP_CHEEP_VERTICAL", + "CHEEP_CHEEP_JUMPING", + "KIKI_THE_MONKEY", + "WINGED_OCTOROK", + "TRADING_ITEM", + "PINCER", + "HOLE_FILLER", + "BEETLE_SPAWNER", + "HONEYCOMB", + "TARIN", + "BEAR", + "PAPAHL", + "MERMAID", + "FISHERMAN_UNDER_BRIDGE", + "BUZZ_BLOB", + "BOMBER", + "BUSH_CRAWLER", + "GRIM_CREEPER", + "VIRE", + "BLAINO", + "ZOMBIE", + "MAZE_SIGNPOST", + "MARIN_AT_THE_SHORE", + "MARIN_AT_TAL_TAL_HEIGHTS", + "MAMU_AND_FROGS", + "WALRUS", + "URCHIN", + "SAND_CRAB", + "MANBO_AND_FISHES", + "BUNNY_CALLING_MARIN", + "MUSICAL_NOTE", + "MAD_BATTER", + "ZORA", + "FISH", + "BANANAS_SCHULE_SALE", + "MERMAID_STATUE", + "SEASHELL_MANSION", + "ANIMAL_D0", + "ANIMAL_D1", + "ANIMAL_D2", + "BUNNY_D3", + "GHOST", + "ROOSTER", + "SIDE_VIEW_POT", + "THWIMP", + "THWOMP", + "THWOMP_RAMMABLE", + "PODOBOO", + "GIANT_BUBBLE", + "FLYING_ROOSTER_EVENTS", + "BOOK", + "EGG_SONG_EVENT", + "SWORD_BEAM", + "MONKEY", + "WITCH_RAT", + "FLAME_SHOOTER", + "POKEY", + "MOBLIN_KING", + "FLOATING_ITEM_2", + "FINAL_NIGHTMARE", + "KANALET_CASTLE_GATE_SWITCH", + "ENDING_OWL_STAIR_CLIMBING", + "COLOR_SHELL_RED", + "COLOR_SHELL_GREEN", + "COLOR_SHELL_BLUE", + "COLOR_GHOUL_RED", + "COLOR_GHOUL_GREEN", + "COLOR_GHOUL_BLUE", + "ROTOSWITCH_RED", + "ROTOSWITCH_YELLOW", + "ROTOSWITCH_BLUE", + "FLYING_HOPPER_BOMBS", + "HOPPER", + "AVALAUNCH", + "BOUNCING_BOULDER", + "COLOR_GUARDIAN_BLUE", + "COLOR_GUARDIAN_RED", + "GIANT_BUZZ_BLOB", + "HARDHIT_BEETLE", + "PHOTOGRAPHER", +] + +def _moblinSpriteData(room): + if room.room in (0x002, 0x013): # Tal tal heights exception + return (2, 0x9C) # Hooded stalfos + if room.room < 0x100: + x = room.room & 0x0F + y = (room.room >> 4) & 0x0F + if x < 0x04: # Left side is woods and mountain moblins + return (2, 0x7C) # Moblin + if 0x08 <= x <= 0x0B and 4 <= y <= 0x07: # Castle + return (2, 0x92) # Knight + # Everything else is pigs + return (2, 0x83) # Pig + elif room.room < 0x1DF: # Dungeons contain hooded stalfos + return (2, 0x9C) # Hooded stalfos + elif room.room < 0x200: # Caves contain moblins + return (2, 0x7C) # Moblin + elif room.room < 0x276: # Dungeons contain hooded stalfos + return (2, 0x9C) # Hooded stalfos + elif room.room < 0x300: # Caves contain moblins + x = room.room & 0x0F + y = (room.room >> 4) & 0x0F + if 2 <= x <= 6 and 0x0C <= y <= 0x0D: # Castle indoors + return (2, 0x92) # Knight + return (2, 0x7C) # Moblin + else: # Dungeon contains hooded stalfos + return (2, 0x9C) # Hooded stalfos + +_CAVES_B_ROOMS = {0x2B6, 0x2B7, 0x2B8, 0x2B9, 0x285, 0x286, 0x2FD, 0x2F3, 0x2ED, 0x2EE, 0x2EA, 0x2EB, 0x2EC, 0x287, 0x2F1, 0x2F2, 0x2FE, 0x2EF, 0x2BA, 0x2BB, 0x2BC, 0x28D, 0x2F9, 0x2FA, 0x280, 0x281, 0x282, 0x283, 0x284, 0x28C, 0x288, 0x28A, 0x290, 0x291, 0x292, 0x28E, 0x29A, 0x289, 0x28B, 0x297, 0x293, 0x294, 0x295, 0x296, 0x2AB, 0x2AC, 0x298, 0x27A, 0x27B, 0x2E6, 0x2E7, 0x2BD, 0x27C, 0x27D, 0x27E, 0x2F6, 0x2F7, 0x2DE, 0x2DF} + +# For each entity, which sprite slot is used and which value should be used. +SPRITE_DATA = { + 0x09: (2, 0xE3), # OCTOROCK + 0x0B: _moblinSpriteData, # MOBLIN + 0x0D: (1, 0x87), # TEKTITE + 0x0E: (1, 0x81), # LEEVER + 0x0F: (2, 0x78), # ARMOS_STATUE + 0x10: (1, 0x42), # HIDING_GHINI + 0x11: (2, 0x8A), # GIANT_GHINI + 0x12: (1, 0x42), # GHINI + 0x14: _moblinSpriteData, # MOBLIN_SWORD + 0x15: (1, 0x91), # ANTI_FAIRY + 0x16: (1, {0x91, 0x65}), # SPARK_COUNTER_CLOCKWISE + 0x17: (1, {0x91, 0x65}), # SPARK_CLOCKWISE + 0x18: (3, 0x93), # POLS_VOICE + 0x19: lambda room: (2, 0x90) if room.room in _CAVES_B_ROOMS else (0, 0x90), # KEESE + 0x1A: (0, {0x90, 0x77}), # STALFOS_AGGRESSIVE + 0x1B: None, # GEL + 0x1C: (1, 0x91), # MINI_GEL + 0x1E: (0, {0x90, 0x77}), # STALFOS_EVASIVE + 0x1F: lambda room: (0, 0x77) if 0x230 <= room.room <= 0x26B else (0, 0x90, 3, 0x93), # GIBDO + 0x20: lambda room: (2, 0x90) if room.room in _CAVES_B_ROOMS else (0, 0x90), # HARDHAT_BEETLE + 0x21: (2, 0x95), # WIZROBE + 0x23: (3, 0x93), # LIKE_LIKE + 0x24: (2, 0x94, 3, 0x9F), # IRON_MASK + 0x27: (1, 0x91), # SPIKE_TRAP + 0x28: (2, 0x96), # MIMIC + 0x29: (3, 0x98), # MINI_MOLDORM + 0x2A: (3, 0x99), # LASER + 0x2C: lambda room: (2, 0x9B) if 0x15E <= room.room <= 0x17F else (3, 0x9B), # SPIKED_BEETLE + 0x2D: None, # DROPPABLE_HEART + 0x2E: None, # DROPPABLE_RUPEE + 0x2F: None, # DROPPABLE_FAIRY + 0x30: None, # KEY_DROP_POINT + 0x31: None, # SWORD + 0x35: None, # HEART_PIECE + 0x37: None, # DROPPABLE_ARROWS + 0x38: None, # DROPPABLE_BOMBS + 0x39: (2, 0x4F), # INSTRUMENT_OF_THE_SIRENS + 0x3A: (1, 0x8E), # SLEEPY_TOADSTOOL + 0x3B: None, # DROPPABLE_MAGIC_POWDER + 0x3C: None, # HIDING_SLIME_KEY + 0x3D: None, # DROPPABLE_SECRET_SEASHELL + 0x3E: lambda room: (0, 0x8D, 2, 0x8F) if room.room == 0x2A3 else (2, 0xE6), # MARIN + 0x3F: lambda room: (1, 0x8E, 3, 0x6A) if room.room == 0x2A3 else (1, 0x6C, 3, 0xC8), # RACOON + 0x40: (2, 0xA3), # WITCH + 0x41: None, # OWL_EVENT + 0x42: lambda room: (1, 0xD5) if room.room == 0x26F else (1, 0x91), # OWL_STATUE + 0x43: None, # SEASHELL_MANSION_TREES + 0x44: None, # YARNA_TALKING_BONES + 0x45: (1, 0x44), # BOULDERS + 0x46: None, # MOVING_BLOCK_LEFT_TOP + 0x47: None, # MOVING_BLOCK_LEFT_BOTTOM + 0x48: None, # MOVING_BLOCK_BOTTOM_LEFT + 0x49: None, # MOVING_BLOCK_BOTTOM_RIGHT + 0x4A: (1, 0xd5), # COLOR_DUNGEON_BOOK + 0x4C: None, # Used by Bingo board, otherwise unused. + 0x4D: (2, 0x88, 3, 0xC7), # SHOP_OWNER + 0x4F: (2, 0x84, 3, 0x89), # TRENDY_GAME_OWNER + 0x50: (2, 0x97), # BOO_BUDDY + 0x51: (3, 0x9A), # KNIGHT + 0x52: lambda room: (3, {0x7b, 0xa6}) if 0x120 <= room.room <= 0x13F else (0, {0x7b, 0xa6}), # TRACTOR_DEVICE + 0x53: lambda room: (3, {0x7b, 0xa6}) if 0x120 <= room.room <= 0x13F else (0, {0x7b, 0xa6}), # TRACTOR_DEVICE_REVERSE + 0x54: lambda room: (0, 0xA0, 1, 0xA1) if room.room == 0x2B1 else (3, 0x4e), # FISHERMAN_FISHING_GAME + 0x55: (3, 0x9d), # BOUNCING_BOMBITE + 0x56: (3, 0x9d), # TIMER_BOMBITE + 0x57: (3, 0x9e), # PAIRODD + 0x59: (2, 0xb0, 3, 0xb1), # MOLDORM + 0x5A: (0, 0x66, 2, 0xb2, 3, 0xb3), # FACADE + 0x5B: (2, 0xb4, 3, 0xb5), # SLIME_EYE + 0x5C: (2, 0xb6, 3, 0xb7), # GENIE + 0x5D: (2, 0xb8, 3, 0xb9), # SLIME_EEL + 0x5E: (2, 0xa8), # GHOMA + 0x5F: (2, 0x62, 3, 0x63), # MASTER_STALFOS + 0x60: lambda room: (3, 0xaa) if 0x230 <= room.room <= 0x26B else (2, 0xaa), # DODONGO_SNAKE + 0x61: None, # WARP + 0x62: (2, 0xba, 3, 0xbb), # HOT_HEAD + 0x63: (0, 0xbc, 1, 0xbd, 2, 0xbe, 3, 0xbf), # EVIL_EAGLE + 0x65: (0, 0xac, 1, 0xad, 2, 0xae, 3, 0xaf), # ANGLER_FISH + 0x66: (1, 0x91), # CRYSTAL_SWITCH + 0x69: (0, 0x66), # MOVING_BLOCK_MOVER + 0x6A: lambda room: (1, 0x87, 2, 0x84) if room.room >= 0x100 else (1, 0x87), # RAFT_RAFT_OWNER + 0x6C: None, # CUCCU + 0x6D: (3, 0xA4), # BOW_WOW + 0x6E: (1, {0xE5, 0xC4}), # BUTTERFLY + 0x6F: (1, 0xE5), # DOG + 0x70: (3, 0xE7), # KID_70 + 0x71: (3, 0xE7), # KID_71 + 0x72: (3, 0xE7), # KID_72 + 0x73: (3, 0xDC), # KID_73 + 0x74: (2, 0x45), # PAPAHLS_WIFE + 0x75: (2, 0x43), # GRANDMA_ULRIRA + 0x76: lambda room: (3, 0x74) if room.room == 0x2D9 else (3, 0x4b), # MR_WRITE + 0x77: (3, 0x46), # GRANDPA_ULRIRA + 0x78: (3, 0x48), # YIP_YIP + 0x79: (2, 0x47), # MADAM_MEOWMEOW + 0x7A: lambda room: (1, 0xC6) if room.room < 0x040 else (1, 0x42), # CROW + 0x7B: (2, 0x49), # CRAZY_TRACY + 0x7C: (3, 0x40), # GIANT_GOPONGA_FLOWER + 0x7E: (1, 0x4A), # GOPONGA_FLOWER + 0x7F: (3, 0x41), # TURTLE_ROCK_HEAD + 0x80: (1, 0x4C), # TELEPHONE + 0x81: lambda room: (3, 0xAB) if 0x230 <= room.room <= 0x26B else (2, 0xAB), # ROLLING_BONES (sometimes in slot 3?) + 0x82: lambda room: (3, 0xAB) if 0x230 <= room.room <= 0x26B else (2, 0xAB), # ROLLING_BONES_BAR (sometimes in slot 3?) + 0x83: (1, 0x8D), # DREAM_SHRINE_BED + 0x84: (1, 0x4D), # BIG_FAIRY + 0x85: (2, 0x4C), # MR_WRITES_BIRD + 0x86: None, # FLOATING_ITEM + 0x87: (3, 0x52), # DESERT_LANMOLA + 0x88: (3, 0x53), # ARMOS_KNIGHT + 0x89: (2, 0x54), # HINOX + 0x8A: None, # TILE_GLINT_SHOWN + 0x8B: None, # TILE_GLINT_HIDDEN + 0x8E: (2, 0x56), # CUE_BALL + 0x8F: lambda room: (2, 0x86) if room.room == 0x1F5 else (2, 0x58), # MASKED_MIMIC_GORIYA + 0x90: (3, 0x59), # THREE_OF_A_KIND + 0x91: (2, 0x55), # ANTI_KIRBY + 0x92: (2, 0x57), # SMASHER + 0x93: (3, 0x5A), # MAD_BOMBER + 0x94: (2, 0x92), # KANALET_BOMBABLE_WALL + 0x95: (1, 0x5b), # RICHARD + 0x96: (2, 0x5c), # RICHARD_FROG + 0x97: None, # DIVE_SPOT + 0x98: (2, 0x5e), # HORSE_PIECE + 0x99: (3, 0x60), # WATER_TEKTITE + 0x9A: lambda room: (0, 0x66) if 0x200 <= room.room <= 0x22F else (0, 0xa6), # FLYING_TILES + 0x9B: None, # HIDING_GEL + 0x9C: (3, 0x60), # STAR + 0x9D: (0, 0xa6), # LIFTABLE_STATUE + 0x9E: None, # FIREBALL_SHOOTER + 0x9F: (0, 0x5f), # GOOMBA + 0xA0: (0, {0x5f, 0x68}), # PEAHAT + 0xA1: (0, {0x5f, 0x7b}), # SNAKE + 0xA2: (3, 0x64), # PIRANHA_PLANT + 0xA3: (1, 0x65), # SIDE_VIEW_PLATFORM_HORIZONTAL + 0xA4: (1, 0x65), # SIDE_VIEW_PLATFORM_VERTICAL + 0xA5: (1, 0x65), # SIDE_VIEW_PLATFORM + 0xA6: (1, 0x65), # SIDE_VIEW_WEIGHTS + 0xA7: (0, 0x66), # SMASHABLE_PILLAR + 0xA9: (2, 0x5d), # BLOOPER + 0xAA: (2, 0x5d), # CHEEP_CHEEP_HORIZONTAL + 0xAB: (2, 0x5d), # CHEEP_CHEEP_VERTICAL + 0xAC: (2, 0x5d), # CHEEP_CHEEP_JUMPING + 0xAD: (3, 0x67), # KIKI_THE_MONKEY + 0xAE: (1, 0xE3), # WINGED_OCTOROCK + 0xAF: None, # TRADING_ITEM + 0xB0: (2, 0x8B), # PINCER + 0xB1: (0, 0x7b), # HOLE_FILLER (or 0x77) + 0xB2: (3, 0x8C), # BEETLE_SPAWNER + 0xB3: (3, 0x6B), # HONEYCOMB + 0xB4: (1, 0x6C), # TARIN + 0xB5: (3, 0x69), # BEAR + 0xB6: (3, 0x6D), # PAPAHL + 0xB7: (3, 0x71), # MERMAID + 0xB8: (1, 0xa1, 2, 0x75, 3, 0x4e), # FISHERMAN_UNDER_BRIDGE + 0xB9: (2, 0x79), # BUZZ_BLOB + 0xBA: (3, 0x76), # BOMBER + 0xBB: (3, 0x76), # BUSH_CRAWLER + 0xBC: (2, 0xa9), # GRIM_CREEPER + 0xBD: (2, 0x7a), # VIRE + 0xBE: (2, 0xa7), # BLAINO + 0xBF: (2, 0x82), # ZOMBIE + 0xC0: None, # MAZE_SIGNPOST + 0xC1: (2, 0x8F), # MARIN_AT_THE_SHORE + 0xC2: (1, 0x6C, 2, 0x8F), # MARIN_AT_TAL_TAL_HEIGHTS + 0xC3: (1, 0x7d, 2, 0x7e, 3, 0x7F), # MAMU_AND_FROGS + 0xC4: (2, 0x6E, 3, 0x6F), # WALRUS + 0xC5: (1, 0x81), # URCHIN + 0xC6: (1, 0x81), # SAND_CRAB + 0xC7: (0, 0xC0, 1, 0xc1, 2, 0xc2, 3, 0xc3), # MANBO_AND_FISHES + 0xCA: (3, 0xc7), # MAD_BATTER + 0xCB: (1, 0x61), # ZORA + 0xCC: (1, 0x4A), # FISH + 0xCD: lambda room: (1, 0xCC, 2, 0xCD, 3, 0xCE) if room.room == 0x2DD else (1, 0xD1, 2, 0xD2, 3, 0x6A) if room.room == 0x2FE else (3, 0xD4), # BANANAS_SCHULE_SALE + 0xCE: (3, 0x73), # MERMAID_STATUE + 0xCF: (1, 0xC9, 2, 0xCA, 3, 0xCB), # SEASHELL_MANSION + 0xD0: (1, 0xC4), # ANIMAL_D0 + 0xD1: (3, 0xCF), # ANIMAL_D1 + 0xD2: (3, 0xCF), # ANIMAL_D2 + 0xD3: (1, 0xC4), # BUNNY_D3 + 0xD6: (1, 0x65), # SIDE_VIEW_POT + 0xD7: (1, 0x65), # THWIMP + 0xD8: (2, 0xDA, 3, 0xDB), # THWOMP + 0xD9: (1, 0xD9), # THWOMP_RAMMABLE + 0xDA: (3, 0x64), # PODOBOO + 0xDB: (2, 0xDA), # GIANT_BUBBLE + 0xDC: lambda room: (0, 0xDD, 2, 0xDE) if room.room == 0x1E4 else (2, 0xD3, 3, 0xDD) if room.room == 0x29F else (3, 0xDC), # FLYING_ROOSTER_EVENTS + 0xDD: (1, 0xD5), # BOOK + 0xDE: None, # EGG_SONG_EVENT + 0xE0: (3, 0xD4), # MONKEY + 0xE1: (1, 0xDF), # WITCH_RAT + 0xE2: (3, 0xF4), # FLAME_SHOOTER + 0xE3: (3, 0x8C), # POKEY + 0xE4: (1, 0x80, 3, 0xA5), # MOBLIN_KING + 0xE5: None, # FLOATING_ITEM_2 + 0xE6: (0, 0xe8, 1, 0xe9, 2, 0xea, 3, 0xeb), # FINAL_NIGHTMARE + 0xE7: None, # KANALET_CASTLE_GATE_SWITCH + 0xE9: (0, 0x04, 1, 0x05), # COLOR_SHELL_RED + 0xEA: (0, 0x04, 1, 0x05), # COLOR_SHELL_GREEN + 0xEB: (0, 0x04, 1, 0x05), # COLOR_SHELL_BLUE + 0xEC: (2, 0x06), # COLOR_GHOUL_RED + 0xED: (2, 0x06), # COLOR_GHOUL_GREEN + 0xEE: (2, 0x06), # COLOR_GHOUL_BLUE + 0xEF: (3, 0x07), # ROTOSWITCH_RED + 0xF0: (3, 0x07), # ROTOSWITCH_YELLOW + 0xF1: (3, 0x07), # ROTOSWITCH_BLUE + 0xF2: (3, 0x07), # FLYING_HOPPER_BOMBS + 0xF3: (3, 0x07), # HOPPER + 0xF4: (0, 0x08, 1, 0x09, 2, 0x0A), # AVALAUNCH + 0xF6: (0, 0x0E), # COLOR_GUARDIAN_BLUE + 0xF7: (0, 0x0E), # COLOR_GUARDIAN_BLUE + 0xF8: (0, 0x0B, 1, 0x0C, 3, 0x0D), # GIANT_BUZZ_BLOB + 0xF9: (0, 0x11, 2, 0x10), # HARDHIT_BEETLE + 0xFA: lambda room: (0, 0x44) if room.room == 0x2F5 else None, # PHOTOGRAPHER +} + +assert len(NAME) == COUNT + + +class Entity: + def __init__(self, index): + self.index = index + self.group = None + self.physics_flags = None + self.bowwow_eat_flag = None + + +class Group: + def __init__(self, index): + self.index = index + self.health = None + self.link_damage = None + + +class EntityData: + def __init__(self, rom): + groups = rom.banks[0x03][0x01F6:0x01F6+COUNT] + group_count = max(groups) + 1 + group_damage_type = rom.banks[0x03][0x03EC:0x03EC+group_count*16] + damage_per_damage_type = rom.banks[0x03][0x073C:0x073C+8*16] + + self.entities = [] + self.groups = [] + for n in range(group_count): + g = Group(n) + g.health = rom.banks[0x03][0x07BC+n] + g.link_damage = rom.banks[0x03][0x07F1+n] + self.groups.append(g) + for n in range(COUNT): + e = Entity(n) + e.group = self.groups[groups[n]] + e.physics_flags = rom.banks[0x03][0x0000 + n] + e.bowwow_eat_flag = rom.banks[0x14][0x1218+n] + self.entities.append(e) + + #print(sum(bowwow_eatable)) + #for n in range(COUNT): + # if bowwow_eatable[n]: + # print(hex(n), NAME[n]) + for n in range(group_count): + entities = list(map(lambda data: NAME[data[0]], filter(lambda data: data[1] == n, enumerate(groups)))) + #print(hex(n), damage_to_link[n], entities) + dmg = bytearray() + for m in range(16): + dmg.append(damage_per_damage_type[m*8+group_damage_type[n*16+m]]) + import binascii + #print(binascii.hexlify(group_damage_type[n*16:n*16+16])) + #print(binascii.hexlify(dmg)) + + +if __name__ == "__main__": + from rom import ROM + import sys + rom = ROM(sys.argv[1]) + ed = EntityData(rom) + for e in ed.entities: + print(NAME[e.index], e.bowwow_eat_flag) diff --git a/worlds/ladx/LADXR/entranceInfo.py b/worlds/ladx/LADXR/entranceInfo.py new file mode 100644 index 0000000000..de1a247355 --- /dev/null +++ b/worlds/ladx/LADXR/entranceInfo.py @@ -0,0 +1,136 @@ + +class EntranceInfo: + def __init__(self, room, alt_room=None, *, type=None, dungeon=None, index=None, instrument_room=None, target=None): + if type is None and dungeon is not None: + type = "dungeon" + assert type is not None, "Missing entrance type" + self.type = type + self.room = room + self.alt_room = alt_room + self.dungeon = dungeon + self.index = index + self.instrument_room = instrument_room + self.target = target + + +ENTRANCE_INFO = { + # Row0-1 + "d8": EntranceInfo(0x10, target=0x25d, dungeon=8, instrument_room=0x230), + "phone_d8": EntranceInfo(0x11, target=0x299, type="dummy"), + "fire_cave_exit": EntranceInfo(0x03, target=0x1ee, type="connector"), + "fire_cave_entrance": EntranceInfo(0x13, target=0x1fe, type="connector"), + "madbatter_taltal": EntranceInfo(0x04, target=0x1e2, type="single"), + "left_taltal_entrance": EntranceInfo(0x15, target=0x2ea, type="connector"), + "obstacle_cave_entrance": EntranceInfo(0x17, target=0x2b6, type="connector"), + "left_to_right_taltalentrance": EntranceInfo(0x07, target=0x2ee, type="connector"), + "obstacle_cave_outside_chest": EntranceInfo(0x18, target=0x2bb, type="connector", index=0), + "obstacle_cave_exit": EntranceInfo(0x18, target=0x2bc, type="connector", index=1), + "papahl_entrance": EntranceInfo(0x19, target=0x289, type="connector"), + "papahl_exit": EntranceInfo(0x0A, target=0x28b, type="connector", index=0), + "rooster_house": EntranceInfo(0x0A, target=0x29f, type="dummy", index=2), + "bird_cave": EntranceInfo(0x0A, target=0x27e, type="single", index=1), + "multichest_left": EntranceInfo(0x1D, target=0x2f9, type="connector", index=0), + "multichest_right": EntranceInfo(0x1D, target=0x2fa, type="connector", index=1), + "multichest_top": EntranceInfo(0x0D, target=0x2f2, type="connector"), + "right_taltal_connector1": EntranceInfo(0x1E, target=0x280, type="connector", index=0), + "right_taltal_connector2": EntranceInfo(0x1F, target=0x282, type="connector", index=0), + "right_taltal_connector3": EntranceInfo(0x1E, target=0x283, type="connector", index=1), + "right_taltal_connector4": EntranceInfo(0x1F, target=0x287, type="connector", index=2), + "right_taltal_connector5": EntranceInfo(0x1F, target=0x28c, type="connector", index=1), + "right_taltal_connector6": EntranceInfo(0x0F, target=0x28e, type="connector"), + "right_fairy": EntranceInfo(0x1F, target=0x1fb, type="dummy", index=3), + "d7": EntranceInfo(0x0E, "Alt0E", target=0x20e, dungeon=7, instrument_room=0x22C), + # Row 2-3 + "writes_cave_left": EntranceInfo(0x20, target=0x2ae, type="connector"), + "writes_cave_right": EntranceInfo(0x21, target=0x2af, type="connector"), + "writes_house": EntranceInfo(0x30, target=0x2a8, type="trade"), + "writes_phone": EntranceInfo(0x31, target=0x29b, type="dummy"), + "d2": EntranceInfo(0x24, target=0x136, dungeon=2, instrument_room=0x12A), + "moblin_cave": EntranceInfo(0x35, target=0x2f0, type="single"), + "photo_house": EntranceInfo(0x37, target=0x2b5, type="dummy"), + "mambo": EntranceInfo(0x2A, target=0x2fd, type="single"), + "d4": EntranceInfo(0x2B, "Alt2B", target=0x17a, dungeon=4, index=0, instrument_room=0x162), + # TODO + # "d4_connector": EntranceInfo(0x2B, "Alt2B", index=1), + # "d4_connector_exit": EntranceInfo(0x2D), + "heartpiece_swim_cave": EntranceInfo(0x2E, target=0x1f2, type="single"), + "raft_return_exit": EntranceInfo(0x2F, target=0x1e7, type="connector"), + "raft_house": EntranceInfo(0x3F, target=0x2b0, type="insanity"), + "raft_return_enter": EntranceInfo(0x8F, target=0x1f7, type="connector"), + # Forest and everything right of it + "hookshot_cave": EntranceInfo(0x42, target=0x2b3, type="single"), + "toadstool_exit": EntranceInfo(0x50, target=0x2ab, type="connector"), + "forest_madbatter": EntranceInfo(0x52, target=0x1e1, type="single"), + "toadstool_entrance": EntranceInfo(0x62, target=0x2bd, type="connector"), + "crazy_tracy": EntranceInfo(0x45, target=0x2ad, type="dummy"), + "witch": EntranceInfo(0x65, target=0x2a2, type="single"), + "graveyard_cave_left": EntranceInfo(0x75, target=0x2de, type="connector"), + "graveyard_cave_right": EntranceInfo(0x76, target=0x2df, type="connector"), + "d0": EntranceInfo(0x77, target=0x312, dungeon=9, index="all", instrument_room=0x301), + # Castle + "castle_jump_cave": EntranceInfo(0x78, target=0x1fd, type="single"), + "castle_main_entrance": EntranceInfo(0x69, target=0x2d3, type="connector"), + "castle_upper_left": EntranceInfo(0x59, target=0x2d5, type="connector", index=0), + "castle_upper_right": EntranceInfo(0x59, target=0x2d6, type="single", index=1), + "castle_secret_exit": EntranceInfo(0x49, target=0x1eb, type="connector"), + "castle_secret_entrance": EntranceInfo(0x4A, target=0x1ec, type="connector"), + "castle_phone": EntranceInfo(0x4B, target=0x2cc, type="dummy"), + # Mabe village + "papahl_house_left": EntranceInfo(0x82, target=0x2a5, type="connector", index=0), + "papahl_house_right": EntranceInfo(0x82, target=0x2a6, type="connector", index=1), + "dream_hut": EntranceInfo(0x83, target=0x2aa, type="single"), + "rooster_grave": EntranceInfo(0x92, target=0x1f4, type="single"), + "shop": EntranceInfo(0x93, target=0x2a1, type="single"), + "madambowwow": EntranceInfo(0xA1, target=0x2a7, type="dummy", index=1), + "kennel": EntranceInfo(0xA1, target=0x2b2, type="single", index=0), + "start_house": EntranceInfo(0xA2, target=0x2a3, type="start"), + "library": EntranceInfo(0xB0, target=0x1fa, type="dummy"), + "ulrira": EntranceInfo(0xB1, target=0x2a9, type="dummy"), + "mabe_phone": EntranceInfo(0xB2, target=0x2cb, type="dummy"), + "trendy_shop": EntranceInfo(0xB3, target=0x2a0, type="trade"), + # Ukuku Prairie + "prairie_left_phone": EntranceInfo(0xA4, target=0x2b4, type="dummy"), + "prairie_left_cave1": EntranceInfo(0x84, target=0x2cd, type="single"), + "prairie_left_cave2": EntranceInfo(0x86, target=0x2f4, type="single"), + "prairie_left_fairy": EntranceInfo(0x87, target=0x1f3, type="dummy"), + "mamu": EntranceInfo(0xD4, target=0x2fb, type="insanity"), + "d3": EntranceInfo(0xB5, target=0x152, dungeon=3, instrument_room=0x159), + "prairie_right_phone": EntranceInfo(0x88, target=0x29c, type="dummy"), + "seashell_mansion": EntranceInfo(0x8A, target=0x2e9, type="single"), + "prairie_right_cave_top": EntranceInfo(0xB8, target=0x292, type="connector", index=1), + "prairie_right_cave_bottom": EntranceInfo(0xC8, target=0x293, type="connector"), + "prairie_right_cave_high": EntranceInfo(0xB8, target=0x295, type="connector", index=0), + "prairie_to_animal_connector": EntranceInfo(0xAA, target=0x2d0, type="connector"), + "animal_to_prairie_connector": EntranceInfo(0xAB, target=0x2d1, type="connector"), + + "d6": EntranceInfo(0x8C, "Alt8C", target=0x1d4, dungeon=6, instrument_room=0x1B5), + "d6_connector_exit": EntranceInfo(0x9C, target=0x1f0, type="connector"), + "d6_connector_entrance": EntranceInfo(0x9D, target=0x1f1, type="connector"), + "armos_fairy": EntranceInfo(0x8D, target=0x1ac, type="dummy"), + "armos_maze_cave": EntranceInfo(0xAE, target=0x2fc, type="single"), + "armos_temple": EntranceInfo(0xAC, target=0x28f, type="single"), + # Beach area + "d1": EntranceInfo(0xD3, target=0x117, dungeon=1, instrument_room=0x102), + "boomerang_cave": EntranceInfo(0xF4, target=0x1f5, type="single", instrument_room="Alt1F5"), # instrument_room is to configure the exit on the alt room layout + "banana_seller": EntranceInfo(0xE3, target=0x2fe, type="trade"), + "ghost_house": EntranceInfo(0xF6, target=0x1e3, type="single"), + + # Lower prairie + "richard_house": EntranceInfo(0xD6, target=0x2c7, type="connector"), + "richard_maze": EntranceInfo(0xC6, target=0x2c9, type="connector"), + "prairie_low_phone": EntranceInfo(0xE8, target=0x29d, type="dummy"), + "prairie_madbatter_connector_entrance": EntranceInfo(0xF9, target=0x1f6, type="connector"), + "prairie_madbatter_connector_exit": EntranceInfo(0xE7, target=0x1e5, type="connector"), + "prairie_madbatter": EntranceInfo(0xE6, target=0x1e0, type="single"), + + "d5": EntranceInfo(0xD9, target=0x1a1, dungeon=5, instrument_room=0x182), + # Animal village + "animal_phone": EntranceInfo(0xDB, target=0x2e3, type="dummy"), + "animal_house1": EntranceInfo(0xCC, target=0x2db, type="dummy", index=0), + "animal_house2": EntranceInfo(0xCC, target=0x2dd, type="dummy", index=1), + "animal_house3": EntranceInfo(0xCD, target=0x2d9, type="trade", index=1), + "animal_house4": EntranceInfo(0xCD, target=0x2da, type="dummy", index=2), + "animal_house5": EntranceInfo(0xDD, target=0x2d7, type="trade"), + "animal_cave": EntranceInfo(0xCD, target=0x2f7, type="single", index=0), + "desert_cave": EntranceInfo(0xCF, target=0x1f9, type="single"), +} diff --git a/worlds/ladx/LADXR/generator.py b/worlds/ladx/LADXR/generator.py new file mode 100644 index 0000000000..0564e276ea --- /dev/null +++ b/worlds/ladx/LADXR/generator.py @@ -0,0 +1,427 @@ +import binascii +import importlib.util +import importlib.machinery +import os + +from .romTables import ROMWithTables +from . import assembler +from . import mapgen +from . import patches +from .patches import overworld as _ +from .patches import dungeon as _ +from .patches import entrances as _ +from .patches import enemies as _ +from .patches import titleScreen as _ +from .patches import aesthetics as _ +from .patches import music as _ +from .patches import core as _ +from .patches import phone as _ +from .patches import photographer as _ +from .patches import owl as _ +from .patches import bank3e as _ +from .patches import bank3f as _ +from .patches import inventory as _ +from .patches import witch as _ +from .patches import tarin as _ +from .patches import fishingMinigame as _ +from .patches import softlock as _ +from .patches import maptweaks as _ +from .patches import chest as _ +from .patches import bomb as _ +from .patches import rooster as _ +from .patches import shop as _ +from .patches import trendy as _ +from .patches import goal as _ +from .patches import hardMode as _ +from .patches import weapons as _ +from .patches import health as _ +from .patches import heartPiece as _ +from .patches import droppedKey as _ +from .patches import goldenLeaf as _ +from .patches import songs as _ +from .patches import bowwow as _ +from .patches import desert as _ +from .patches import reduceRNG as _ +from .patches import madBatter as _ +from .patches import tunicFairy as _ +from .patches import seashell as _ +from .patches import instrument as _ +from .patches import endscreen as _ +from .patches import save as _ +from .patches import bingo as _ +from .patches import multiworld as _ +from .patches import tradeSequence as _ +from . import hints + +from .locations.keyLocation import KeyLocation +from .patches import bank34 + +from ..Options import TrendyGame, Palette + + +# Function to generate a final rom, this patches the rom with all required patches +def generateRom(args, settings, ap_settings, seed, logic, rnd=None, multiworld=None, player_name=None, player_names=[], player_id = 0): + rom = ROMWithTables(args.input_filename) + rom.player_names = player_names + pymods = [] + if args.pymod: + for pymod in args.pymod: + spec = importlib.util.spec_from_loader(pymod, importlib.machinery.SourceFileLoader(pymod, pymod)) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + pymods.append(module) + for pymod in pymods: + pymod.prePatch(rom) + + if settings.gfxmod: + patches.aesthetics.gfxMod(rom, os.path.join("data", "sprites", "ladx", settings.gfxmod)) + + item_list = [item for item in logic.iteminfo_list if not isinstance(item, KeyLocation)] + + assembler.resetConsts() + assembler.const("INV_SIZE", 16) + assembler.const("wHasFlippers", 0xDB3E) + assembler.const("wHasMedicine", 0xDB3F) + assembler.const("wTradeSequenceItem", 0xDB40) # we use it to store flags of which trade items we have + assembler.const("wTradeSequenceItem2", 0xDB7F) # Normally used to store that we have exchanged the trade item, we use it to store flags of which trade items we have + assembler.const("wSeashellsCount", 0xDB41) + assembler.const("wGoldenLeaves", 0xDB42) # New memory location where to store the golden leaf counter + assembler.const("wCollectedTunics", 0xDB6D) # Memory location where to store which tunic options are available + assembler.const("wCustomMessage", 0xC0A0) + + # We store the link info in unused color dungeon flags, so it gets preserved in the savegame. + assembler.const("wLinkSyncSequenceNumber", 0xDDF6) + assembler.const("wLinkStatusBits", 0xDDF7) + assembler.const("wLinkGiveItem", 0xDDF8) + assembler.const("wLinkGiveItemFrom", 0xDDF9) + assembler.const("wLinkSendItemRoomHigh", 0xDDFA) + assembler.const("wLinkSendItemRoomLow", 0xDDFB) + assembler.const("wLinkSendItemTarget", 0xDDFC) + assembler.const("wLinkSendItemItem", 0xDDFD) + + assembler.const("wZolSpawnCount", 0xDE10) + assembler.const("wCuccoSpawnCount", 0xDE11) + assembler.const("wDropBombSpawnCount", 0xDE12) + assembler.const("wLinkSpawnDelay", 0xDE13) + + #assembler.const("HARDWARE_LINK", 1) + assembler.const("HARD_MODE", 1 if settings.hardmode != "none" else 0) + + patches.core.cleanup(rom) + patches.save.singleSaveSlot(rom) + patches.phone.patchPhone(rom) + patches.photographer.fixPhotographer(rom) + patches.core.bugfixWrittingWrongRoomStatus(rom) + patches.core.bugfixBossroomTopPush(rom) + patches.core.bugfixPowderBagSprite(rom) + patches.core.fixEggDeathClearingItems(rom) + patches.core.disablePhotoPrint(rom) + patches.core.easyColorDungeonAccess(rom) + patches.owl.removeOwlEvents(rom) + patches.enemies.fixArmosKnightAsMiniboss(rom) + patches.bank3e.addBank3E(rom, seed, player_id, player_names) + patches.bank3f.addBank3F(rom) + patches.bank34.addBank34(rom, item_list) + patches.core.removeGhost(rom) + patches.core.fixMarinFollower(rom) + patches.core.fixWrongWarp(rom) + patches.core.alwaysAllowSecretBook(rom) + patches.core.injectMainLoop(rom) + + from ..Options import ShuffleSmallKeys, ShuffleNightmareKeys + + if ap_settings["shuffle_small_keys"] != ShuffleSmallKeys.option_original_dungeon or ap_settings["shuffle_nightmare_keys"] != ShuffleNightmareKeys.option_original_dungeon: + patches.inventory.advancedInventorySubscreen(rom) + patches.inventory.moreSlots(rom) + if settings.witch: + patches.witch.updateWitch(rom) + patches.softlock.fixAll(rom) + patches.maptweaks.tweakMap(rom) + patches.chest.fixChests(rom) + patches.shop.fixShop(rom) + patches.rooster.patchRooster(rom) + patches.trendy.fixTrendy(rom) + patches.droppedKey.fixDroppedKey(rom) + patches.madBatter.upgradeMadBatter(rom) + patches.tunicFairy.upgradeTunicFairy(rom) + patches.tarin.updateTarin(rom) + patches.fishingMinigame.updateFinishingMinigame(rom) + patches.health.upgradeHealthContainers(rom) + if settings.owlstatues in ("dungeon", "both"): + patches.owl.upgradeDungeonOwlStatues(rom) + if settings.owlstatues in ("overworld", "both"): + patches.owl.upgradeOverworldOwlStatues(rom) + patches.goldenLeaf.fixGoldenLeaf(rom) + patches.heartPiece.fixHeartPiece(rom) + patches.seashell.fixSeashell(rom) + patches.instrument.fixInstruments(rom) + patches.seashell.upgradeMansion(rom) + patches.songs.upgradeMarin(rom) + patches.songs.upgradeManbo(rom) + patches.songs.upgradeMamu(rom) + if settings.tradequest: + patches.tradeSequence.patchTradeSequence(rom, settings.boomerang) + else: + # Monkey bridge patch, always have the bridge there. + rom.patch(0x00, 0x333D, assembler.ASM("bit 4, e\njr Z, $05"), b"", fill_nop=True) + patches.bowwow.fixBowwow(rom, everywhere=settings.bowwow != 'normal') + if settings.bowwow != 'normal': + patches.bowwow.bowwowMapPatches(rom) + patches.desert.desertAccess(rom) + if settings.overworld == 'dungeondive': + patches.overworld.patchOverworldTilesets(rom) + patches.overworld.createDungeonOnlyOverworld(rom) + elif settings.overworld == 'nodungeons': + patches.dungeon.patchNoDungeons(rom) + elif settings.overworld == 'random': + patches.overworld.patchOverworldTilesets(rom) + mapgen.store_map(rom, logic.world.map) + #if settings.dungeon_items == 'keysy': + # patches.dungeon.removeKeyDoors(rom) + # patches.reduceRNG.slowdownThreeOfAKind(rom) + patches.reduceRNG.fixHorseHeads(rom) + patches.bomb.onlyDropBombsWhenHaveBombs(rom) + # patches.aesthetics.noSwordMusic(rom) + patches.aesthetics.reduceMessageLengths(rom, rnd) + patches.aesthetics.allowColorDungeonSpritesEverywhere(rom) + if settings.music == 'random': + patches.music.randomizeMusic(rom, rnd) + elif settings.music == 'off': + patches.music.noMusic(rom) + if settings.noflash: + patches.aesthetics.removeFlashingLights(rom) + if settings.hardmode == "oracle": + patches.hardMode.oracleMode(rom) + elif settings.hardmode == "hero": + patches.hardMode.heroMode(rom) + elif settings.hardmode == "ohko": + patches.hardMode.oneHitKO(rom) + if settings.superweapons: + patches.weapons.patchSuperWeapons(rom) + if settings.textmode == 'fast': + patches.aesthetics.fastText(rom) + if settings.textmode == 'none': + patches.aesthetics.fastText(rom) + patches.aesthetics.noText(rom) + if not settings.nagmessages: + patches.aesthetics.removeNagMessages(rom) + if settings.lowhpbeep == 'slow': + patches.aesthetics.slowLowHPBeep(rom) + if settings.lowhpbeep == 'none': + patches.aesthetics.removeLowHPBeep(rom) + if 0 <= int(settings.linkspalette): + patches.aesthetics.forceLinksPalette(rom, int(settings.linkspalette)) + if args.romdebugmode: + # The default rom has this build in, just need to set a flag and we get this save. + rom.patch(0, 0x0003, "00", "01") + + # Patch the sword check on the shopkeeper turning around. + if settings.steal == 'never': + rom.patch(4, 0x36F9, "FA4EDB", "3E0000") + elif settings.steal == 'always': + rom.patch(4, 0x36F9, "FA4EDB", "3E0100") + + if settings.hpmode == 'inverted': + patches.health.setStartHealth(rom, 9) + elif settings.hpmode == '1': + patches.health.setStartHealth(rom, 1) + + patches.inventory.songSelectAfterOcarinaSelect(rom) + if settings.quickswap == 'a': + patches.core.quickswap(rom, 1) + elif settings.quickswap == 'b': + patches.core.quickswap(rom, 0) + + # TODO: hints bad + + world_setup = logic.world_setup + + + hints.addHints(rom, rnd, item_list) + + if world_setup.goal == "raft": + patches.goal.setRaftGoal(rom) + elif world_setup.goal in ("bingo", "bingo-full"): + patches.bingo.setBingoGoal(rom, world_setup.bingo_goals, world_setup.goal) + elif world_setup.goal == "seashells": + patches.goal.setSeashellGoal(rom, 20) + else: + patches.goal.setRequiredInstrumentCount(rom, world_setup.goal) + + # Patch the generated logic into the rom + patches.chest.setMultiChest(rom, world_setup.multichest) + if settings.overworld not in {"dungeondive", "random"}: + patches.entrances.changeEntrances(rom, world_setup.entrance_mapping) + for spot in item_list: + if spot.item and spot.item.startswith("*"): + spot.item = spot.item[1:] + mw = None + if spot.item_owner != spot.location_owner: + mw = spot.item_owner + if mw > 255: + # Don't torture the game with higher slot numbers + mw = 255 + spot.patch(rom, spot.item, multiworld=mw) + patches.enemies.changeBosses(rom, world_setup.boss_mapping) + patches.enemies.changeMiniBosses(rom, world_setup.miniboss_mapping) + + if not args.romdebugmode: + patches.core.addFrameCounter(rom, len(item_list)) + + patches.core.warpHome(rom) # Needs to be done after setting the start location. + patches.titleScreen.setRomInfo(rom, binascii.hexlify(seed).decode("ascii").upper(), settings, player_name, player_id) + patches.endscreen.updateEndScreen(rom) + patches.aesthetics.updateSpriteData(rom) + if args.doubletrouble: + patches.enemies.doubleTrouble(rom) + + if ap_settings["trendy_game"] != TrendyGame.option_normal: + + # TODO: if 0 or 4, 5, remove inaccurate conveyor tiles + + from .roomEditor import RoomEditor, Object + room_editor = RoomEditor(rom, 0x2A0) + + if ap_settings["trendy_game"] == TrendyGame.option_easy: + # Set physics flag on all objects + for i in range(0, 6): + rom.banks[0x4][0x6F1E + i -0x4000] = 0x4 + else: + # All levels + # Set physics flag on yoshi + rom.banks[0x4][0x6F21-0x4000] = 0x3 + # Add new conveyor to "push" yoshi (it's only a visual) + room_editor.objects.append(Object(5, 3, 0xD0)) + + if int(ap_settings["trendy_game"]) >= TrendyGame.option_harder: + """ + Data_004_76A0:: + db $FC, $00, $04, $00, $00 + + Data_004_76A5:: + db $00, $04, $00, $FC, $00 + """ + speeds = { + TrendyGame.option_harder: (3, 8), + TrendyGame.option_hardest: (3, 8), + TrendyGame.option_impossible: (3, 16), + } + def speed(): + return rnd.randint(*speeds[ap_settings["trendy_game"]]) + rom.banks[0x4][0x76A0-0x4000] = 0xFF - speed() + rom.banks[0x4][0x76A2-0x4000] = speed() + rom.banks[0x4][0x76A6-0x4000] = speed() + rom.banks[0x4][0x76A8-0x4000] = 0xFF - speed() + if int(ap_settings["trendy_game"]) >= TrendyGame.option_hardest: + rom.banks[0x4][0x76A1-0x4000] = 0xFF - speed() + rom.banks[0x4][0x76A3-0x4000] = speed() + rom.banks[0x4][0x76A5-0x4000] = speed() + rom.banks[0x4][0x76A7-0x4000] = 0xFF - speed() + + room_editor.store(rom) + # This doesn't work, you can set random conveyors, but they aren't used + # for x in range(3, 9): + # for y in range(1, 5): + # room_editor.objects.append(Object(x, y, 0xCF + rnd.randint(0, 3))) + + # Attempt at imitating gb palette, fails + if False: + gb_colors = [ + [0x0f, 0x38, 0x0f], + [0x30, 0x62, 0x30], + [0x8b, 0xac, 0x0f], + [0x9b, 0xbc, 0x0f], + ] + for color in gb_colors: + for channel in range(3): + color[channel] = color[channel] * 31 // 0xbc + + + palette = ap_settings["palette"] + if palette != Palette.option_normal: + ranges = { + # Object palettes + # Overworld palettes + # Dungeon palettes + # Interior palettes + "code/palettes.asm 1": (0x21, 0x1518, 0x34A0), + # Intro/outro(?) + # File select + # S+Q + # Map + "code/palettes.asm 2": (0x21, 0x3536, 0x3FFE), + # Used for transitioning in and out of forest + "backgrounds/palettes.asm": (0x24, 0x3478, 0x3578), + # Haven't yet found menu palette + } + + for name, (bank, start, end) in ranges.items(): + def clamp(x, min, max): + if x < min: + return min + if x > max: + return max + return x + def bin_to_rgb(word): + red = word & 0b11111 + word >>= 5 + green = word & 0b11111 + word >>= 5 + blue = word & 0b11111 + return (red, green, blue) + def rgb_to_bin(r, g, b): + return (b << 10) | (g << 5) | r + + for address in range(start, end, 2): + packed = (rom.banks[bank][address + 1] << 8) | rom.banks[bank][address] + r,g,b = bin_to_rgb(packed) + + # 1 bit + if palette == Palette.option_1bit: + r &= 0b10000 + g &= 0b10000 + b &= 0b10000 + # 2 bit + elif palette == Palette.option_1bit: + r &= 0b11000 + g &= 0b11000 + b &= 0b11000 + # Invert + elif palette == Palette.option_inverted: + r = 31 - r + g = 31 - g + b = 31 - b + # Pink + elif palette == Palette.option_pink: + r = r // 2 + r += 16 + r = int(r) + r = clamp(r, 0, 0x1F) + b = b // 2 + b += 16 + b = int(b) + b = clamp(b, 0, 0x1F) + elif palette == Palette.option_greyscale: + # gray=int(0.299*r+0.587*g+0.114*b) + gray = (r + g + b) // 3 + r = g = b = gray + + packed = rgb_to_bin(r, g, b) + rom.banks[bank][address] = packed & 0xFF + rom.banks[bank][address + 1] = packed >> 8 + + SEED_LOCATION = 0x0134 + SEED_SIZE = 10 + + # TODO: pass this in + # Patch over the title + assert(len(seed) == SEED_SIZE) + gameid = seed + player_id.to_bytes(2, 'big') + rom.patch(0x00, SEED_LOCATION, None, binascii.hexlify(gameid)) + + + for pymod in pymods: + pymod.postPatch(rom) + + + return rom diff --git a/worlds/ladx/LADXR/getGFX.py b/worlds/ladx/LADXR/getGFX.py new file mode 100644 index 0000000000..e7d327ef65 --- /dev/null +++ b/worlds/ladx/LADXR/getGFX.py @@ -0,0 +1,41 @@ +import requests +import PIL.Image +import re + +url = "https://raw.githubusercontent.com/CrystalSaver/Z4RandomizerBeta2/master/" + +for k, v in requests.get(url + "asset-manifest.json").json()['files'].items(): + m = re.match("static/media/Graphics(.+)\\.bin", k) + assert m is not None + if not k.startswith("static/media/Graphics") or not k.endswith(".bin"): + continue + name = m.group(1) + + data = requests.get(url + v).content + + icon = PIL.Image.new("P", (16, 16)) + buffer = bytearray(b'\x00' * 16 * 8) + for idx in range(0x0C0, 0x0C2): + for y in range(16): + a = data[idx * 32 + y * 2] + b = data[idx * 32 + y * 2 + 1] + for x in range(8): + v = 0 + if a & (0x80 >> x): + v |= 1 + if b & (0x80 >> x): + v |= 2 + buffer[x+y*8] = v + tile = PIL.Image.frombytes('P', (8, 16), bytes(buffer)) + x = (idx % 16) * 8 + icon.paste(tile, (x, 0)) + pal = icon.getpalette() + assert pal is not None + pal[0:3] = [150, 150, 255] + pal[3:6] = [0, 0, 0] + pal[6:9] = [59, 180, 112] + pal[9:12] = [251, 221, 197] + icon.putpalette(pal) + icon = icon.resize((32, 32)) + icon.save("gfx/%s.bin.png" % (name)) + open("gfx/%s.bin" % (name), "wb").write(data) diff --git a/worlds/ladx/LADXR/hints.py b/worlds/ladx/LADXR/hints.py new file mode 100644 index 0000000000..f7ef78a3e8 --- /dev/null +++ b/worlds/ladx/LADXR/hints.py @@ -0,0 +1,66 @@ +from .locations.items import * +from .utils import formatText + + +hint_text_ids = [ + # Overworld owl statues + 0x1B6, 0x1B7, 0x1B8, 0x1B9, 0x1BA, 0x1BB, 0x1BC, 0x1BD, 0x1BE, 0x22D, + + 0x288, 0x280, # D1 + 0x28A, 0x289, 0x281, # D2 + 0x282, 0x28C, 0x28B, # D3 + 0x283, # D4 + 0x28D, 0x284, # D5 + 0x285, 0x28F, 0x28E, # D6 + 0x291, 0x290, 0x286, # D7 + 0x293, 0x287, 0x292, # D8 + 0x263, # D0 + + # Hint books + 0x267, # color dungeon + 0x201, # Pre open: 0x200 + 0x203, # Pre open: 0x202 + 0x205, # Pre open: 0x204 + 0x207, # Pre open: 0x206 + 0x209, # Pre open: 0x208 + 0x20B, # Pre open: 0x20A +] + +hint_items = (POWER_BRACELET, SHIELD, BOW, HOOKSHOT, MAGIC_ROD, PEGASUS_BOOTS, OCARINA, FEATHER, SHOVEL, + MAGIC_POWDER, SWORD, FLIPPERS, TAIL_KEY, ANGLER_KEY, FACE_KEY, + BIRD_KEY, SLIME_KEY, GOLD_LEAF, BOOMERANG, BOWWOW) + +hints = [ + "{0} is at {1}", + "If you want {0} start looking in {1}", + "{1} holds {0}", + "They say that {0} is at {1}", + "You might want to look in {1} for a secret", +] +useless_hint = [ + ("Egg", "Mt. Tamaranch"), + ("Marin", "Mabe Village"), + ("Marin", "Mabe Village"), + ("Witch", "Koholint Prairie"), + ("Mermaid", "Martha's Bay"), + ("Nothing", "Tabahl Wasteland"), + ("Animals", "Animal Village"), + ("Sand", "Yarna Desert"), +] + + +def addHints(rom, rnd, spots): + spots = list(sorted(filter(lambda spot: spot.item in hint_items, spots), key=lambda spot: spot.nameId)) + text_ids = hint_text_ids.copy() + rnd.shuffle(text_ids) + for text_id in text_ids: + if len(spots) > 0: + spot_index = rnd.randint(0, len(spots) - 1) + spot = spots.pop(spot_index) + hint = rnd.choice(hints).format("{%s}" % (spot.item), spot.metadata.area) + else: + hint = rnd.choice(hints).format(*rnd.choice(useless_hint)) + rom.texts[text_id] = formatText(hint) + + for text_id in range(0x200, 0x20C, 2): + rom.texts[text_id] = formatText("Read this book?", ask="YES NO") diff --git a/worlds/ladx/LADXR/itempool.py b/worlds/ladx/LADXR/itempool.py new file mode 100644 index 0000000000..5031488337 --- /dev/null +++ b/worlds/ladx/LADXR/itempool.py @@ -0,0 +1,278 @@ +from .locations.items import * + + +DEFAULT_ITEM_POOL = { + SWORD: 2, + FEATHER: 1, + HOOKSHOT: 1, + BOW: 1, + BOMB: 1, + MAGIC_POWDER: 1, + MAGIC_ROD: 1, + OCARINA: 1, + PEGASUS_BOOTS: 1, + POWER_BRACELET: 2, + SHIELD: 2, + SHOVEL: 1, + ROOSTER: 1, + TOADSTOOL: 1, + + TAIL_KEY: 1, SLIME_KEY: 1, ANGLER_KEY: 1, FACE_KEY: 1, BIRD_KEY: 1, + GOLD_LEAF: 5, + + FLIPPERS: 1, + BOWWOW: 1, + SONG1: 1, SONG2: 1, SONG3: 1, + + BLUE_TUNIC: 1, RED_TUNIC: 1, + MAX_ARROWS_UPGRADE: 1, MAX_BOMBS_UPGRADE: 1, MAX_POWDER_UPGRADE: 1, + + HEART_CONTAINER: 8, + HEART_PIECE: 12, + + RUPEES_100: 3, + RUPEES_20: 6, + RUPEES_200: 3, + RUPEES_50: 19, + + SEASHELL: 24, + MEDICINE: 3, + GEL: 4, + MESSAGE: 1, + + COMPASS1: 1, COMPASS2: 1, COMPASS3: 1, COMPASS4: 1, COMPASS5: 1, COMPASS6: 1, COMPASS7: 1, COMPASS8: 1, COMPASS9: 1, + KEY1: 3, KEY2: 5, KEY3: 9, KEY4: 5, KEY5: 3, KEY6: 3, KEY7: 3, KEY8: 7, KEY9: 3, + MAP1: 1, MAP2: 1, MAP3: 1, MAP4: 1, MAP5: 1, MAP6: 1, MAP7: 1, MAP8: 1, MAP9: 1, + NIGHTMARE_KEY1: 1, NIGHTMARE_KEY2: 1, NIGHTMARE_KEY3: 1, NIGHTMARE_KEY4: 1, NIGHTMARE_KEY5: 1, NIGHTMARE_KEY6: 1, NIGHTMARE_KEY7: 1, NIGHTMARE_KEY8: 1, NIGHTMARE_KEY9: 1, + STONE_BEAK1: 1, STONE_BEAK2: 1, STONE_BEAK3: 1, STONE_BEAK4: 1, STONE_BEAK5: 1, STONE_BEAK6: 1, STONE_BEAK7: 1, STONE_BEAK8: 1, STONE_BEAK9: 1, + + INSTRUMENT1: 1, INSTRUMENT2: 1, INSTRUMENT3: 1, INSTRUMENT4: 1, INSTRUMENT5: 1, INSTRUMENT6: 1, INSTRUMENT7: 1, INSTRUMENT8: 1, + + TRADING_ITEM_YOSHI_DOLL: 1, + TRADING_ITEM_RIBBON: 1, + TRADING_ITEM_DOG_FOOD: 1, + TRADING_ITEM_BANANAS: 1, + TRADING_ITEM_STICK: 1, + TRADING_ITEM_HONEYCOMB: 1, + TRADING_ITEM_PINEAPPLE: 1, + TRADING_ITEM_HIBISCUS: 1, + TRADING_ITEM_LETTER: 1, + TRADING_ITEM_BROOM: 1, + TRADING_ITEM_FISHING_HOOK: 1, + TRADING_ITEM_NECKLACE: 1, + TRADING_ITEM_SCALE: 1, + TRADING_ITEM_MAGNIFYING_GLASS: 1, + + "MEDICINE2": 1, "RAFT": 1, "ANGLER_KEYHOLE": 1, "CASTLE_BUTTON": 1 +} + + +class ItemPool: + def __init__(self, logic, settings, rnd): + self.__pool = {} + self.__setup(logic, settings) + self.__randomizeRupees(settings, rnd) + + def add(self, item, count=1): + self.__pool[item] = self.__pool.get(item, 0) + count + + def remove(self, item, count=1): + self.__pool[item] = self.__pool.get(item, 0) - count + if self.__pool[item] == 0: + del self.__pool[item] + + def get(self, item): + return self.__pool.get(item, 0) + + def count(self): + total = 0 + for count in self.__pool.values(): + total += count + return total + + def removeRupees(self, count): + for n in range(count): + self.removeRupee() + + def removeRupee(self): + for item in (RUPEES_20, RUPEES_50, RUPEES_200, RUPEES_500): + if self.get(item) > 0: + self.remove(item) + return + raise RuntimeError("Wanted to remove more rupees from the pool then we have") + + def __setup(self, logic, settings): + default_item_pool = DEFAULT_ITEM_POOL + if settings.overworld == "random": + default_item_pool = logic.world.map.get_item_pool() + for item, count in default_item_pool.items(): + self.add(item, count) + if settings.boomerang != 'default' and settings.overworld != "random": + self.add(BOOMERANG) + if settings.owlstatues == 'both': + self.add(RUPEES_20, 9 + 24) + elif settings.owlstatues == 'dungeon': + self.add(RUPEES_20, 24) + elif settings.owlstatues == 'overworld': + self.add(RUPEES_20, 9) + + if settings.bowwow == 'always': + # Bowwow mode takes a sword from the pool to give as bowwow. So we need to fix that. + self.add(SWORD) + self.remove(BOWWOW) + elif settings.bowwow == 'swordless': + # Bowwow mode takes a sword from the pool to give as bowwow, we need to remove all swords and Bowwow except for 1 + self.add(RUPEES_20, self.get(BOWWOW) + self.get(SWORD) - 1) + self.remove(SWORD, self.get(SWORD) - 1) + self.remove(BOWWOW, self.get(BOWWOW)) + if settings.hpmode == 'inverted': + self.add(BAD_HEART_CONTAINER, self.get(HEART_CONTAINER)) + self.remove(HEART_CONTAINER, self.get(HEART_CONTAINER)) + elif settings.hpmode == 'low': + self.add(HEART_PIECE, self.get(HEART_CONTAINER)) + self.remove(HEART_CONTAINER, self.get(HEART_CONTAINER)) + elif settings.hpmode == 'extralow': + self.add(RUPEES_20, self.get(HEART_CONTAINER)) + self.remove(HEART_CONTAINER, self.get(HEART_CONTAINER)) + + if settings.itempool == 'casual': + self.add(FLIPPERS) + self.add(FEATHER) + self.add(HOOKSHOT) + self.add(BOW) + self.add(BOMB) + self.add(MAGIC_POWDER) + self.add(MAGIC_ROD) + self.add(OCARINA) + self.add(PEGASUS_BOOTS) + self.add(POWER_BRACELET) + self.add(SHOVEL) + self.add(RUPEES_200, 2) + self.removeRupees(13) + + for n in range(9): + self.remove("MAP%d" % (n + 1)) + self.remove("COMPASS%d" % (n + 1)) + self.add("KEY%d" % (n + 1)) + self.add("NIGHTMARE_KEY%d" % (n +1)) + elif settings.itempool == 'pain': + self.add(BAD_HEART_CONTAINER, 12) + self.remove(BLUE_TUNIC) + self.remove(MEDICINE, 2) + self.remove(HEART_PIECE, 4) + self.removeRupees(5) + elif settings.itempool == 'keyup': + for n in range(9): + self.remove("MAP%d" % (n + 1)) + self.remove("COMPASS%d" % (n + 1)) + self.add("KEY%d" % (n +1)) + self.add("NIGHTMARE_KEY%d" % (n +1)) + if settings.owlstatues in ("none", "overworld"): + for n in range(9): + self.remove("STONE_BEAK%d" % (n + 1)) + self.add("KEY%d" % (n +1)) + + # if settings.dungeon_items == 'keysy': + # for n in range(9): + # for amount, item_name in ((9, "KEY"), (1, "NIGHTMARE_KEY")): + # item_name = "%s%d" % (item_name, n + 1) + # if item_name in self.__pool: + # self.add(RUPEES_20, self.__pool[item_name]) + # self.remove(item_name, self.__pool[item_name]) + # self.add(item_name, amount) + + if settings.goal == "seashells": + for n in range(8): + self.remove("INSTRUMENT%d" % (n + 1)) + self.add(SEASHELL, 8) + + if settings.overworld == "dungeondive": + self.remove(SWORD) + self.remove(MAX_ARROWS_UPGRADE) + self.remove(MAX_BOMBS_UPGRADE) + self.remove(MAX_POWDER_UPGRADE) + self.remove(SEASHELL, 24) + self.remove(TAIL_KEY) + self.remove(SLIME_KEY) + self.remove(ANGLER_KEY) + self.remove(FACE_KEY) + self.remove(BIRD_KEY) + self.remove(GOLD_LEAF, 5) + self.remove(SONG2) + self.remove(SONG3) + self.remove(HEART_PIECE, 8) + self.remove(RUPEES_50, 9) + self.remove(RUPEES_20, 2) + self.remove(MEDICINE, 3) + self.remove(MESSAGE) + self.remove(BOWWOW) + self.remove(ROOSTER) + self.remove(GEL, 2) + self.remove("MEDICINE2") + self.remove("RAFT") + self.remove("ANGLER_KEYHOLE") + self.remove("CASTLE_BUTTON") + self.remove(TRADING_ITEM_YOSHI_DOLL) + self.remove(TRADING_ITEM_RIBBON) + self.remove(TRADING_ITEM_DOG_FOOD) + self.remove(TRADING_ITEM_BANANAS) + self.remove(TRADING_ITEM_STICK) + self.remove(TRADING_ITEM_HONEYCOMB) + self.remove(TRADING_ITEM_PINEAPPLE) + self.remove(TRADING_ITEM_HIBISCUS) + self.remove(TRADING_ITEM_LETTER) + self.remove(TRADING_ITEM_BROOM) + self.remove(TRADING_ITEM_FISHING_HOOK) + self.remove(TRADING_ITEM_NECKLACE) + self.remove(TRADING_ITEM_SCALE) + self.remove(TRADING_ITEM_MAGNIFYING_GLASS) + elif not settings.rooster: + self.remove(ROOSTER) + self.add(RUPEES_50) + + if settings.overworld == "nodungeons": + for n in range(9): + for item_name in {KEY, NIGHTMARE_KEY, MAP, COMPASS, STONE_BEAK}: + self.remove(f"{item_name}{n+1}", self.get(f"{item_name}{n+1}")) + self.remove(BLUE_TUNIC) + self.remove(RED_TUNIC) + self.remove(SEASHELL, 2) + self.remove(RUPEES_20, 6) + self.remove(RUPEES_50, 17) + self.remove(MEDICINE, 3) + self.remove(GEL, 4) + self.remove(MESSAGE, 1) + self.remove(BOMB, 1) + self.remove(RUPEES_100, 3) + self.add(RUPEES_500, 3) + + # # In multiworld, put a bit more rupees in the seed, this helps with generation (2nd shop item) + # # As we cheat and can place rupees for the wrong player. + # if settings.multiworld: + # rupees20 = self.__pool.get(RUPEES_20, 0) + # self.add(RUPEES_50, rupees20 // 2) + # self.remove(RUPEES_20, rupees20 // 2) + # rupees50 = self.__pool.get(RUPEES_50, 0) + # self.add(RUPEES_200, rupees50 // 5) + # self.remove(RUPEES_50, rupees50 // 5) + + def __randomizeRupees(self, options, rnd): + # Remove rupees from the item pool and replace them with other items to create more variety + rupee_item = [] + rupee_item_count = [] + for k, v in self.__pool.items(): + if k in {RUPEES_20, RUPEES_50} and v > 0: + rupee_item.append(k) + rupee_item_count.append(v) + rupee_chests = sum(v for k, v in self.__pool.items() if k.startswith("RUPEES_")) + for n in range(rupee_chests // 5): + new_item = rnd.choices((BOMB, SINGLE_ARROW, ARROWS_10, MAGIC_POWDER, MEDICINE), (10, 5, 10, 10, 1))[0] + while True: + remove_item = rnd.choices(rupee_item, rupee_item_count)[0] + if self.get(remove_item) > 0: + break + self.add(new_item) + self.remove(remove_item) + + def toDict(self): + return self.__pool.copy() diff --git a/worlds/ladx/LADXR/locations/all.py b/worlds/ladx/LADXR/locations/all.py new file mode 100644 index 0000000000..a617346051 --- /dev/null +++ b/worlds/ladx/LADXR/locations/all.py @@ -0,0 +1,26 @@ +from .beachSword import BeachSword +from .chest import Chest, DungeonChest +from .droppedKey import DroppedKey +from .seashell import Seashell, SeashellMansion +from .heartContainer import HeartContainer +from .owlStatue import OwlStatue +from .madBatter import MadBatter +from .shop import ShopItem +from .startItem import StartItem +from .toadstool import Toadstool +from .witch import Witch +from .goldLeaf import GoldLeaf, SlimeKey +from .boomerangGuy import BoomerangGuy +from .anglerKey import AnglerKey +from .hookshot import HookshotDrop +from .faceKey import FaceKey +from .birdKey import BirdKey +from .heartPiece import HeartPiece +from .tunicFairy import TunicFairy +from .song import Song +from .instrument import Instrument +from .fishingMinigame import FishingMinigame +from .keyLocation import KeyLocation +from .tradeSequence import TradeSequenceItem + +from .items import * diff --git a/worlds/ladx/LADXR/locations/anglerKey.py b/worlds/ladx/LADXR/locations/anglerKey.py new file mode 100644 index 0000000000..79382de862 --- /dev/null +++ b/worlds/ladx/LADXR/locations/anglerKey.py @@ -0,0 +1,6 @@ +from .droppedKey import DroppedKey + + +class AnglerKey(DroppedKey): + def __init__(self): + super().__init__(0x0CE) \ No newline at end of file diff --git a/worlds/ladx/LADXR/locations/beachSword.py b/worlds/ladx/LADXR/locations/beachSword.py new file mode 100644 index 0000000000..51fc4388e3 --- /dev/null +++ b/worlds/ladx/LADXR/locations/beachSword.py @@ -0,0 +1,32 @@ +from .droppedKey import DroppedKey +from .items import * +from ..roomEditor import RoomEditor +from ..assembler import ASM +from typing import Optional +from ..rom import ROM + + +class BeachSword(DroppedKey): + def __init__(self) -> None: + super().__init__(0x0F2) + + def patch(self, rom: ROM, option: str, *, multiworld: Optional[int] = None) -> None: + if option != SWORD or multiworld is not None: + # Set the heart piece data + super().patch(rom, option, multiworld=multiworld) + + # Patch the room to contain a heart piece instead of the sword on the beach + re = RoomEditor(rom, 0x0F2) + re.removeEntities(0x31) # remove sword + re.addEntity(5, 5, 0x35) # add heart piece + re.store(rom) + + # Prevent shield drops from the like-like from turning into swords. + rom.patch(0x03, 0x1B9C, ASM("ld a, [$DB4E]"), ASM("ld a, $01"), fill_nop=True) + rom.patch(0x03, 0x244D, ASM("ld a, [$DB4E]"), ASM("ld a, $01"), fill_nop=True) + + def read(self, rom: ROM) -> str: + re = RoomEditor(rom, 0x0F2) + if re.hasEntity(0x31): + return SWORD + return super().read(rom) diff --git a/worlds/ladx/LADXR/locations/birdKey.py b/worlds/ladx/LADXR/locations/birdKey.py new file mode 100644 index 0000000000..12418c61aa --- /dev/null +++ b/worlds/ladx/LADXR/locations/birdKey.py @@ -0,0 +1,23 @@ +from .droppedKey import DroppedKey +from ..roomEditor import RoomEditor +from ..assembler import ASM + + +class BirdKey(DroppedKey): + def __init__(self): + super().__init__(0x27A) + + def patch(self, rom, option, *, multiworld=None): + super().patch(rom, option, multiworld=multiworld) + + re = RoomEditor(rom, self.room) + + # Make the bird key accessible without the rooster + re.removeObject(1, 6) + re.removeObject(2, 6) + re.removeObject(3, 5) + re.removeObject(3, 6) + re.moveObject(1, 5, 2, 6) + re.moveObject(2, 5, 3, 6) + re.addEntity(3, 5, 0x9D) + re.store(rom) diff --git a/worlds/ladx/LADXR/locations/boomerangGuy.py b/worlds/ladx/LADXR/locations/boomerangGuy.py new file mode 100644 index 0000000000..92d76cebdf --- /dev/null +++ b/worlds/ladx/LADXR/locations/boomerangGuy.py @@ -0,0 +1,94 @@ +from .itemInfo import ItemInfo +from .constants import * +from ..assembler import ASM +from ..utils import formatText + + +class BoomerangGuy(ItemInfo): + OPTIONS = [BOOMERANG, HOOKSHOT, MAGIC_ROD, PEGASUS_BOOTS, FEATHER, SHOVEL] + + def __init__(self): + super().__init__(0x1F5) + self.setting = 'trade' + + def configure(self, options): + self.MULTIWORLD = False + + self.setting = options.boomerang + if self.setting == 'gift': + self.MULTIWORLD = True + + # Cannot trade: + # SWORD, BOMB, SHIELD, POWER_BRACELET, OCARINA, MAGIC_POWDER, BOW + # Checks for these are at $46A2, and potentially we could remove those. + # But SHIELD, BOMB and MAGIC_POWDER would most likely break things. + # SWORD and POWER_BRACELET would most likely introduce the lv0 shield/bracelet issue + def patch(self, rom, option, *, multiworld=None): + # Always have the boomerang trade guy enabled (normally you need the magnifier) + rom.patch(0x19, 0x05EC, ASM("ld a, [wTradeSequenceItem]\ncp $0E"), ASM("ld a, $0E\ncp $0E"), fill_nop=True) # show the guy + rom.patch(0x00, 0x3199, ASM("ld a, [wTradeSequenceItem]\ncp $0E"), ASM("ld a, $0E\ncp $0E"), fill_nop=True) # load the proper room layout + rom.patch(0x19, 0x05F4, ASM("ld a, [wTradeSequenceItem2]\nand a"), ASM("xor a"), fill_nop=True) + + if self.setting == 'trade': + inv = INVENTORY_MAP[option] + # Patch the check if you traded back the boomerang (so traded twice) + rom.patch(0x19, 0x063F, ASM("cp $0D"), ASM("cp $%s" % (inv))) + # Item to give by "default" (aka, boomerang) + rom.patch(0x19, 0x06C1, ASM("ld a, $0D"), ASM("ld a, $%s" % (inv))) + # Check if inventory slot is boomerang to give back item in this slot + rom.patch(0x19, 0x06FC, ASM("cp $0D"), ASM("cp $%s" % (inv))) + # Put the boomerang ID in the inventory of the boomerang guy (aka, traded back) + rom.patch(0x19, 0x0710, ASM("ld a, $0D"), ASM("ld a, $%s" % (inv))) + + rom.texts[0x222] = formatText("Okay, let's do it!") + rom.texts[0x224] = formatText("You got the {%s} in exchange for the item you had." % (option)) + rom.texts[0x225] = formatText("Give me back my {%s}, I beg you! I'll return the item you gave me" % (option), ask="Okay Not Now") + rom.texts[0x226] = formatText("The item came back to you. You returned the other item.") + else: + # Patch the inventory trade to give an specific item instead + rom.texts[0x221] = formatText("I found a good item washed up on the beach... Want to have it?", ask="Okay No") + rom.patch(0x19, 0x069C, 0x06C6, ASM(""" + ; Mark trade as done + ld a, $06 + ld [$DB7D], a + + ld a, [$472B] + ldh [$F1], a + ld a, $06 + rst 8 + + ld a, $0D + """), fill_nop=True) + # Show the right item above link + rom.patch(0x19, 0x0786, 0x0793, ASM(""" + ld a, [$472B] + ldh [$F1], a + ld a, $01 + rst 8 + """), fill_nop=True) + # Give the proper message for this item + rom.patch(0x19, 0x075A, 0x076A, ASM(""" + ld a, [$472B] + ldh [$F1], a + ld a, $0A + rst 8 + """), fill_nop=True) + rom.patch(0x19, 0x072B, "00", "%02X" % (CHEST_ITEMS[option])) + + # Ignore the trade back. + rom.texts[0x225] = formatText("It's a secret to everybody.") + rom.patch(0x19, 0x0668, ASM("ld a, [$DB7D]"), ASM("ret"), fill_nop=True) + + if multiworld is not None: + rom.banks[0x3E][0x3300 + self.room] = multiworld + + def read(self, rom): + if rom.banks[0x19][0x06C5] == 0x00: + for k, v in CHEST_ITEMS.items(): + if v == rom.banks[0x19][0x072B]: + return k + else: + for k, v in INVENTORY_MAP.items(): + if int(v, 16) == rom.banks[0x19][0x0640]: + return k + raise ValueError() diff --git a/worlds/ladx/LADXR/locations/chest.py b/worlds/ladx/LADXR/locations/chest.py new file mode 100644 index 0000000000..578201bc1e --- /dev/null +++ b/worlds/ladx/LADXR/locations/chest.py @@ -0,0 +1,50 @@ +from .itemInfo import ItemInfo +from .constants import * +from ..assembler import ASM + + +class Chest(ItemInfo): + def __init__(self, room): + super().__init__(room) + self.addr = room + 0x560 + + def patch(self, rom, option, *, multiworld=None): + rom.banks[0x14][self.addr] = CHEST_ITEMS[option] + + if self.room == 0x1B6: + # Patch the code that gives the nightmare key when you throw the pot at the chest in dungeon 6 + # As this is hardcoded for a specific chest type + rom.patch(3, 0x145D, ASM("ld a, $19"), ASM("ld a, $%02x" % (CHEST_ITEMS[option]))) + if multiworld is not None: + rom.banks[0x3E][0x3300 + self.room] = multiworld + + def read(self, rom): + value = rom.banks[0x14][self.addr] + for k, v in CHEST_ITEMS.items(): + if v == value: + return k + raise ValueError("Could not find chest contents in ROM (0x%02x)" % (value)) + + def __repr__(self): + return "%s:%03x" % (self.__class__.__name__, self.room) + + +class DungeonChest(Chest): + def patch(self, rom, option, *, multiworld=None): + if (option.startswith(MAP) and option != MAP) \ + or (option.startswith(COMPASS) and option != COMPASS) \ + or (option.startswith(STONE_BEAK) and option != STONE_BEAK) \ + or (option.startswith(NIGHTMARE_KEY) and option != NIGHTMARE_KEY) \ + or (option.startswith(KEY) and option != KEY): + if self._location.dungeon == int(option[-1]) and multiworld is None: + option = option[:-1] + super().patch(rom, option, multiworld=multiworld) + + def read(self, rom): + result = super().read(rom) + if result in [MAP, COMPASS, STONE_BEAK, NIGHTMARE_KEY, KEY]: + return "%s%d" % (result, self._location.dungeon) + return result + + def __repr__(self): + return "%s:%03x:%d" % (self.__class__.__name__, self.room, self._location.dungeon) diff --git a/worlds/ladx/LADXR/locations/constants.py b/worlds/ladx/LADXR/locations/constants.py new file mode 100644 index 0000000000..7bb8df5b35 --- /dev/null +++ b/worlds/ladx/LADXR/locations/constants.py @@ -0,0 +1,131 @@ +from .items import * + +INVENTORY_MAP = { + SWORD: "01", + BOMB: "02", + POWER_BRACELET: "03", + SHIELD: "04", + BOW: "05", + HOOKSHOT: "06", + MAGIC_ROD: "07", + PEGASUS_BOOTS: "08", + OCARINA: "09", + FEATHER: "0A", + SHOVEL: "0B", + MAGIC_POWDER: "0C", + BOOMERANG: "0D", + TOADSTOOL: "0E", +} +CHEST_ITEMS = { + POWER_BRACELET: 0x00, + SHIELD: 0x01, + BOW: 0x02, + HOOKSHOT: 0x03, + MAGIC_ROD: 0x04, + PEGASUS_BOOTS: 0x05, + OCARINA: 0x06, + FEATHER: 0x07, SHOVEL: 0x08, MAGIC_POWDER: 0x09, BOMB: 0x0A, SWORD: 0x0B, FLIPPERS: 0x0C, + MAGNIFYING_LENS: 0x0D, MEDICINE: 0x10, + TAIL_KEY: 0x11, ANGLER_KEY: 0x12, FACE_KEY: 0x13, BIRD_KEY: 0x14, GOLD_LEAF: 0x15, + RUPEES_50: 0x1B, RUPEES_20: 0x1C, RUPEES_100: 0x1D, RUPEES_200: 0x1E, RUPEES_500: 0x1F, + SEASHELL: 0x20, MESSAGE: 0x21, GEL: 0x22, + MAP: 0x16, COMPASS: 0x17, STONE_BEAK: 0x18, NIGHTMARE_KEY: 0x19, KEY: 0x1A, + ROOSTER: 0x96, + + BOOMERANG: 0x0E, + SLIME_KEY: 0x0F, + + KEY1: 0x23, + KEY2: 0x24, + KEY3: 0x25, + KEY4: 0x26, + KEY5: 0x27, + KEY6: 0x28, + KEY7: 0x29, + KEY8: 0x2A, + KEY9: 0x2B, + + MAP1: 0x2C, + MAP2: 0x2D, + MAP3: 0x2E, + MAP4: 0x2F, + MAP5: 0x30, + MAP6: 0x31, + MAP7: 0x32, + MAP8: 0x33, + MAP9: 0x34, + + COMPASS1: 0x35, + COMPASS2: 0x36, + COMPASS3: 0x37, + COMPASS4: 0x38, + COMPASS5: 0x39, + COMPASS6: 0x3A, + COMPASS7: 0x3B, + COMPASS8: 0x3C, + COMPASS9: 0x3D, + + STONE_BEAK1: 0x3E, + STONE_BEAK2: 0x3F, + STONE_BEAK3: 0x40, + STONE_BEAK4: 0x41, + STONE_BEAK5: 0x42, + STONE_BEAK6: 0x43, + STONE_BEAK7: 0x44, + STONE_BEAK8: 0x45, + STONE_BEAK9: 0x46, + + NIGHTMARE_KEY1: 0x47, + NIGHTMARE_KEY2: 0x48, + NIGHTMARE_KEY3: 0x49, + NIGHTMARE_KEY4: 0x4A, + NIGHTMARE_KEY5: 0x4B, + NIGHTMARE_KEY6: 0x4C, + NIGHTMARE_KEY7: 0x4D, + NIGHTMARE_KEY8: 0x4E, + NIGHTMARE_KEY9: 0x4F, + + TOADSTOOL: 0x50, + + HEART_PIECE: 0x80, + BOWWOW: 0x81, + ARROWS_10: 0x82, + SINGLE_ARROW: 0x83, + + MAX_POWDER_UPGRADE: 0x84, + MAX_BOMBS_UPGRADE: 0x85, + MAX_ARROWS_UPGRADE: 0x86, + + RED_TUNIC: 0x87, + BLUE_TUNIC: 0x88, + HEART_CONTAINER: 0x89, + BAD_HEART_CONTAINER: 0x8A, + + SONG1: 0x8B, + SONG2: 0x8C, + SONG3: 0x8D, + + INSTRUMENT1: 0x8E, + INSTRUMENT2: 0x8F, + INSTRUMENT3: 0x90, + INSTRUMENT4: 0x91, + INSTRUMENT5: 0x92, + INSTRUMENT6: 0x93, + INSTRUMENT7: 0x94, + INSTRUMENT8: 0x95, + + TRADING_ITEM_YOSHI_DOLL: 0x97, + TRADING_ITEM_RIBBON: 0x98, + TRADING_ITEM_DOG_FOOD: 0x99, + TRADING_ITEM_BANANAS: 0x9A, + TRADING_ITEM_STICK: 0x9B, + TRADING_ITEM_HONEYCOMB: 0x9C, + TRADING_ITEM_PINEAPPLE: 0x9D, + TRADING_ITEM_HIBISCUS: 0x9E, + TRADING_ITEM_LETTER: 0x9F, + TRADING_ITEM_BROOM: 0xA0, + TRADING_ITEM_FISHING_HOOK: 0xA1, + TRADING_ITEM_NECKLACE: 0xA2, + TRADING_ITEM_SCALE: 0xA3, + TRADING_ITEM_MAGNIFYING_GLASS: 0xA4, +} diff --git a/worlds/ladx/LADXR/locations/droppedKey.py b/worlds/ladx/LADXR/locations/droppedKey.py new file mode 100644 index 0000000000..baa093bb38 --- /dev/null +++ b/worlds/ladx/LADXR/locations/droppedKey.py @@ -0,0 +1,57 @@ +from .itemInfo import ItemInfo +from .constants import * +patched_already = {} + +class DroppedKey(ItemInfo): + default_item = None + + def __init__(self, room=None): + extra = None + if room == 0x169: # Room in D4 where the key drops down the hole into the sidescroller + extra = 0x017C + elif room == 0x166: # D4 boss, also place the item in out real boss room. + extra = 0x01ff + elif room == 0x223: # D7 boss, also place the item in our real boss room. + extra = 0x02E8 + elif room == 0x092: # Marins song + extra = 0x00DC + elif room == 0x0CE: + extra = 0x01F8 + super().__init__(room, extra) + def patch(self, rom, option, *, multiworld=None): + if (option.startswith(MAP) and option != MAP) or (option.startswith(COMPASS) and option != COMPASS) or option.startswith(STONE_BEAK) or (option.startswith(NIGHTMARE_KEY) and option != NIGHTMARE_KEY )or (option.startswith(KEY) and option != KEY): + if option[-1] == 'P': + print(option) + if self._location.dungeon == int(option[-1]) and multiworld is None and self.room not in {0x166, 0x223}: + option = option[:-1] + rom.banks[0x3E][self.room + 0x3800] = CHEST_ITEMS[option] + #assert room not in patched_already, f"{self} {patched_already[room]}" + #patched_already[room] = self + + + if self.extra: + assert(not self.default_item) + rom.banks[0x3E][self.extra + 0x3800] = CHEST_ITEMS[option] + + if multiworld is not None: + rom.banks[0x3E][0x3300 + self.room] = multiworld + + if self.extra: + rom.banks[0x3E][0x3300 + self.extra] = multiworld + + def read(self, rom): + assert self._location is not None, hex(self.room) + value = rom.banks[0x3E][self.room + 0x3800] + for k, v in CHEST_ITEMS.items(): + if v == value: + if k in [MAP, COMPASS, STONE_BEAK, NIGHTMARE_KEY, KEY]: + assert self._location.dungeon is not None, "Dungeon item outside of dungeon? %r" % (self) + return "%s%d" % (k, self._location.dungeon) + return k + raise ValueError("Could not find chest contents in ROM (0x%02x)" % (value)) + + def __repr__(self): + if self._location and self._location.dungeon: + return "%s:%03x:%d" % (self.__class__.__name__, self.room, self._location.dungeon) + else: + return "%s:%03x" % (self.__class__.__name__, self.room) diff --git a/worlds/ladx/LADXR/locations/faceKey.py b/worlds/ladx/LADXR/locations/faceKey.py new file mode 100644 index 0000000000..1585535f63 --- /dev/null +++ b/worlds/ladx/LADXR/locations/faceKey.py @@ -0,0 +1,6 @@ +from .droppedKey import DroppedKey + + +class FaceKey(DroppedKey): + def __init__(self): + super().__init__(0x27F) diff --git a/worlds/ladx/LADXR/locations/fishingMinigame.py b/worlds/ladx/LADXR/locations/fishingMinigame.py new file mode 100644 index 0000000000..0caaf7fddb --- /dev/null +++ b/worlds/ladx/LADXR/locations/fishingMinigame.py @@ -0,0 +1,13 @@ +from .droppedKey import DroppedKey +from .constants import * + + +class FishingMinigame(DroppedKey): + def __init__(self): + super().__init__(0x2B1) + + def configure(self, options): + if options.heartpiece: + super().configure(options) + else: + self.OPTIONS = [HEART_PIECE] diff --git a/worlds/ladx/LADXR/locations/goldLeaf.py b/worlds/ladx/LADXR/locations/goldLeaf.py new file mode 100644 index 0000000000..10ebab42cc --- /dev/null +++ b/worlds/ladx/LADXR/locations/goldLeaf.py @@ -0,0 +1,12 @@ +from .droppedKey import DroppedKey + + +class GoldLeaf(DroppedKey): + pass # Golden leaves are patched to work exactly like dropped keys + + +class SlimeKey(DroppedKey): + # The slime key is secretly a golden leaf and just normally uses logic depended on the room number. + # As we patched it to act like a dropped key, we can just be a dropped key in the right room + def __init__(self): + super().__init__(0x0C6) diff --git a/worlds/ladx/LADXR/locations/heartContainer.py b/worlds/ladx/LADXR/locations/heartContainer.py new file mode 100644 index 0000000000..e1a8d77569 --- /dev/null +++ b/worlds/ladx/LADXR/locations/heartContainer.py @@ -0,0 +1,15 @@ +from .droppedKey import DroppedKey +from .items import * + + +class HeartContainer(DroppedKey): + # Due to the patches a heartContainers acts like a dropped key. + def configure(self, options): + if options.heartcontainers or options.hpmode == 'extralow': + super().configure(options) + elif options.hpmode == 'inverted': + self.OPTIONS = [BAD_HEART_CONTAINER] + elif options.hpmode == 'low': + self.OPTIONS = [HEART_PIECE] + else: + self.OPTIONS = [HEART_CONTAINER] diff --git a/worlds/ladx/LADXR/locations/heartPiece.py b/worlds/ladx/LADXR/locations/heartPiece.py new file mode 100644 index 0000000000..b6dddf0b7b --- /dev/null +++ b/worlds/ladx/LADXR/locations/heartPiece.py @@ -0,0 +1,12 @@ +from .droppedKey import DroppedKey +from .items import * + + +class HeartPiece(DroppedKey): + # Due to the patches a heartPiece acts like a dropped key. + + def configure(self, options): + if options.heartpiece: + super().configure(options) + else: + self.OPTIONS = [HEART_PIECE] diff --git a/worlds/ladx/LADXR/locations/hookshot.py b/worlds/ladx/LADXR/locations/hookshot.py new file mode 100644 index 0000000000..1e2d24584a --- /dev/null +++ b/worlds/ladx/LADXR/locations/hookshot.py @@ -0,0 +1,18 @@ +from .droppedKey import DroppedKey + + +""" +The hookshot is dropped by the master stalfos. +The master stalfos drops a "key" with, and modifies a bunch of properties: + + ld a, $30 ; $7EE1: $3E $30 + call SpawnNewEntity_trampoline ; $7EE3: $CD $86 $3B + +And then the dropped key handles the rest with room number specific code. +As we patched the dropped key, this requires no extra handling. +""" + + +class HookshotDrop(DroppedKey): + def __init__(self): + super().__init__(0x180) diff --git a/worlds/ladx/LADXR/locations/instrument.py b/worlds/ladx/LADXR/locations/instrument.py new file mode 100644 index 0000000000..2eadb31c65 --- /dev/null +++ b/worlds/ladx/LADXR/locations/instrument.py @@ -0,0 +1,9 @@ +from .droppedKey import DroppedKey + + +class Instrument(DroppedKey): + # Thanks to patches, an instrument is just a dropped key as far as the randomizer is concerned. + + def configure(self, options): + if not options.instruments and not options.goal == "seashells": + self.OPTIONS = ["INSTRUMENT%d" % (self._location.dungeon)] diff --git a/worlds/ladx/LADXR/locations/itemInfo.py b/worlds/ladx/LADXR/locations/itemInfo.py new file mode 100644 index 0000000000..dcd4205f4c --- /dev/null +++ b/worlds/ladx/LADXR/locations/itemInfo.py @@ -0,0 +1,43 @@ +import typing +from ..checkMetadata import checkMetadataTable +from .constants import * + + +class ItemInfo: + MULTIWORLD = True + + def __init__(self, room=None, extra=None): + self.item = None + self._location = None + self.room = room + self.extra = extra + self.metadata = checkMetadataTable.get(self.nameId, checkMetadataTable["None"]) + self.forced_item = None + self.custom_item_name = None + + self.event = None + @property + def location(self): + return self._location + + def setLocation(self, location): + self._location = location + + def getOptions(self): + return self.OPTIONS + + def configure(self, options): + pass + + def read(self, rom): + raise NotImplementedError() + + def patch(self, rom, option, *, multiworld=None): + raise NotImplementedError() + + def __repr__(self): + return self.__class__.__name__ + + @property + def nameId(self): + return "0x%03X" % self.room if self.room is not None else "None" diff --git a/worlds/ladx/LADXR/locations/items.py b/worlds/ladx/LADXR/locations/items.py new file mode 100644 index 0000000000..50186ef2a3 --- /dev/null +++ b/worlds/ladx/LADXR/locations/items.py @@ -0,0 +1,127 @@ +POWER_BRACELET = "POWER_BRACELET" +SHIELD = "SHIELD" +BOW = "BOW" +HOOKSHOT = "HOOKSHOT" +MAGIC_ROD = "MAGIC_ROD" +PEGASUS_BOOTS = "PEGASUS_BOOTS" +OCARINA = "OCARINA" +FEATHER = "FEATHER" +SHOVEL = "SHOVEL" +MAGIC_POWDER = "MAGIC_POWDER" +BOMB = "BOMB" +SWORD = "SWORD" +FLIPPERS = "FLIPPERS" +MAGNIFYING_LENS = "MAGNIFYING_LENS" +MEDICINE = "MEDICINE" +TAIL_KEY = "TAIL_KEY" +ANGLER_KEY = "ANGLER_KEY" +FACE_KEY = "FACE_KEY" +BIRD_KEY = "BIRD_KEY" +SLIME_KEY = "SLIME_KEY" +GOLD_LEAF = "GOLD_LEAF" +RUPEES_50 = "RUPEES_50" +RUPEES_20 = "RUPEES_20" +RUPEES_100 = "RUPEES_100" +RUPEES_200 = "RUPEES_200" +RUPEES_500 = "RUPEES_500" +SEASHELL = "SEASHELL" +MESSAGE = "MESSAGE" +GEL = "GEL" +BOOMERANG = "BOOMERANG" +HEART_PIECE = "HEART_PIECE" +BOWWOW = "BOWWOW" +ARROWS_10 = "ARROWS_10" +SINGLE_ARROW = "SINGLE_ARROW" +ROOSTER = "ROOSTER" + +MAX_POWDER_UPGRADE = "MAX_POWDER_UPGRADE" +MAX_BOMBS_UPGRADE = "MAX_BOMBS_UPGRADE" +MAX_ARROWS_UPGRADE = "MAX_ARROWS_UPGRADE" + +RED_TUNIC = "RED_TUNIC" +BLUE_TUNIC = "BLUE_TUNIC" +HEART_CONTAINER = "HEART_CONTAINER" +BAD_HEART_CONTAINER = "BAD_HEART_CONTAINER" + +TOADSTOOL = "TOADSTOOL" + +KEY = "KEY" +KEY1 = "KEY1" +KEY2 = "KEY2" +KEY3 = "KEY3" +KEY4 = "KEY4" +KEY5 = "KEY5" +KEY6 = "KEY6" +KEY7 = "KEY7" +KEY8 = "KEY8" +KEY9 = "KEY9" + +NIGHTMARE_KEY = "NIGHTMARE_KEY" +NIGHTMARE_KEY1 = "NIGHTMARE_KEY1" +NIGHTMARE_KEY2 = "NIGHTMARE_KEY2" +NIGHTMARE_KEY3 = "NIGHTMARE_KEY3" +NIGHTMARE_KEY4 = "NIGHTMARE_KEY4" +NIGHTMARE_KEY5 = "NIGHTMARE_KEY5" +NIGHTMARE_KEY6 = "NIGHTMARE_KEY6" +NIGHTMARE_KEY7 = "NIGHTMARE_KEY7" +NIGHTMARE_KEY8 = "NIGHTMARE_KEY8" +NIGHTMARE_KEY9 = "NIGHTMARE_KEY9" + +MAP = "MAP" +MAP1 = "MAP1" +MAP2 = "MAP2" +MAP3 = "MAP3" +MAP4 = "MAP4" +MAP5 = "MAP5" +MAP6 = "MAP6" +MAP7 = "MAP7" +MAP8 = "MAP8" +MAP9 = "MAP9" +COMPASS = "COMPASS" +COMPASS1 = "COMPASS1" +COMPASS2 = "COMPASS2" +COMPASS3 = "COMPASS3" +COMPASS4 = "COMPASS4" +COMPASS5 = "COMPASS5" +COMPASS6 = "COMPASS6" +COMPASS7 = "COMPASS7" +COMPASS8 = "COMPASS8" +COMPASS9 = "COMPASS9" +STONE_BEAK = "STONE_BEAK" +STONE_BEAK1 = "STONE_BEAK1" +STONE_BEAK2 = "STONE_BEAK2" +STONE_BEAK3 = "STONE_BEAK3" +STONE_BEAK4 = "STONE_BEAK4" +STONE_BEAK5 = "STONE_BEAK5" +STONE_BEAK6 = "STONE_BEAK6" +STONE_BEAK7 = "STONE_BEAK7" +STONE_BEAK8 = "STONE_BEAK8" +STONE_BEAK9 = "STONE_BEAK9" + +SONG1 = "SONG1" +SONG2 = "SONG2" +SONG3 = "SONG3" + +INSTRUMENT1 = "INSTRUMENT1" +INSTRUMENT2 = "INSTRUMENT2" +INSTRUMENT3 = "INSTRUMENT3" +INSTRUMENT4 = "INSTRUMENT4" +INSTRUMENT5 = "INSTRUMENT5" +INSTRUMENT6 = "INSTRUMENT6" +INSTRUMENT7 = "INSTRUMENT7" +INSTRUMENT8 = "INSTRUMENT8" + +TRADING_ITEM_YOSHI_DOLL = "TRADING_ITEM_YOSHI_DOLL" +TRADING_ITEM_RIBBON = "TRADING_ITEM_RIBBON" +TRADING_ITEM_DOG_FOOD = "TRADING_ITEM_DOG_FOOD" +TRADING_ITEM_BANANAS = "TRADING_ITEM_BANANAS" +TRADING_ITEM_STICK = "TRADING_ITEM_STICK" +TRADING_ITEM_HONEYCOMB = "TRADING_ITEM_HONEYCOMB" +TRADING_ITEM_PINEAPPLE = "TRADING_ITEM_PINEAPPLE" +TRADING_ITEM_HIBISCUS = "TRADING_ITEM_HIBISCUS" +TRADING_ITEM_LETTER = "TRADING_ITEM_LETTER" +TRADING_ITEM_BROOM = "TRADING_ITEM_BROOM" +TRADING_ITEM_FISHING_HOOK = "TRADING_ITEM_FISHING_HOOK" +TRADING_ITEM_NECKLACE = "TRADING_ITEM_NECKLACE" +TRADING_ITEM_SCALE = "TRADING_ITEM_SCALE" +TRADING_ITEM_MAGNIFYING_GLASS = "TRADING_ITEM_MAGNIFYING_GLASS" diff --git a/worlds/ladx/LADXR/locations/keyLocation.py b/worlds/ladx/LADXR/locations/keyLocation.py new file mode 100644 index 0000000000..675bfe0f90 --- /dev/null +++ b/worlds/ladx/LADXR/locations/keyLocation.py @@ -0,0 +1,18 @@ +from .itemInfo import ItemInfo + + +class KeyLocation(ItemInfo): + OPTIONS = [] + + def __init__(self, key): + super().__init__() + self.event = key + + def patch(self, rom, option, *, multiworld=None): + pass + + def read(self, rom): + return self.OPTIONS[0] + + def configure(self, options): + pass diff --git a/worlds/ladx/LADXR/locations/madBatter.py b/worlds/ladx/LADXR/locations/madBatter.py new file mode 100644 index 0000000000..33ce971422 --- /dev/null +++ b/worlds/ladx/LADXR/locations/madBatter.py @@ -0,0 +1,23 @@ +from .itemInfo import ItemInfo +from .constants import * + + +class MadBatter(ItemInfo): + def configure(self, options): + return + + def patch(self, rom, option, *, multiworld=None): + rom.banks[0x18][0x0F90 + (self.room & 0x0F)] = CHEST_ITEMS[option] + if multiworld is not None: + rom.banks[0x3E][0x3300 + self.room] = multiworld + + def read(self, rom): + assert self._location is not None, hex(self.room) + value = rom.banks[0x18][0x0F90 + (self.room & 0x0F)] + for k, v in CHEST_ITEMS.items(): + if v == value: + return k + raise ValueError("Could not find mad batter contents in ROM (0x%02x)" % (value)) + + def __repr__(self): + return "%s:%03x" % (self.__class__.__name__, self.room) diff --git a/worlds/ladx/LADXR/locations/owlStatue.py b/worlds/ladx/LADXR/locations/owlStatue.py new file mode 100644 index 0000000000..1e62519319 --- /dev/null +++ b/worlds/ladx/LADXR/locations/owlStatue.py @@ -0,0 +1,41 @@ +from .itemInfo import ItemInfo +from .constants import * + + +class OwlStatue(ItemInfo): + def configure(self, options): + if options.owlstatues == "both": + return + if options.owlstatues == "dungeon" and self.room >= 0x100: + return + if options.owlstatues == "overworld" and self.room < 0x100: + return + raise RuntimeError("Tried to configure an owlstatue that was not enabled") + self.OPTIONS = [RUPEES_20] + + def patch(self, rom, option, *, multiworld=None): + if option.startswith(MAP) or option.startswith(COMPASS) or option.startswith(STONE_BEAK) or option.startswith(NIGHTMARE_KEY) or option.startswith(KEY): + if self._location.dungeon == int(option[-1]) and multiworld is not None: + option = option[:-1] + rom.banks[0x3E][self.room + 0x3B16] = CHEST_ITEMS[option] + + def read(self, rom): + assert self._location is not None, hex(self.room) + value = rom.banks[0x3E][self.room + 0x3B16] + for k, v in CHEST_ITEMS.items(): + if v == value: + if k in [MAP, COMPASS, STONE_BEAK, NIGHTMARE_KEY, KEY]: + assert self._location.dungeon is not None, "Dungeon item outside of dungeon? %r" % (self) + return "%s%d" % (k, self._location.dungeon) + return k + raise ValueError("Could not find owl statue contents in ROM (0x%02x)" % (value)) + + def __repr__(self): + if self._location and self._location.dungeon: + return "%s:%03x:%d" % (self.__class__.__name__, self.room, self._location.dungeon) + else: + return "%s:%03x" % (self.__class__.__name__, self.room) + + @property + def nameId(self): + return "0x%03X-Owl" % self.room diff --git a/worlds/ladx/LADXR/locations/seashell.py b/worlds/ladx/LADXR/locations/seashell.py new file mode 100644 index 0000000000..5b30cf7e24 --- /dev/null +++ b/worlds/ladx/LADXR/locations/seashell.py @@ -0,0 +1,14 @@ +from .droppedKey import DroppedKey +from .items import * + + +class Seashell(DroppedKey): + # Thanks to patches, a seashell is just a dropped key as far as the randomizer is concerned. + + def configure(self, options): + if not options.seashells: + self.OPTIONS = [SEASHELL] + + +class SeashellMansion(DroppedKey): + pass diff --git a/worlds/ladx/LADXR/locations/shop.py b/worlds/ladx/LADXR/locations/shop.py new file mode 100644 index 0000000000..e3e05d941a --- /dev/null +++ b/worlds/ladx/LADXR/locations/shop.py @@ -0,0 +1,42 @@ +from .itemInfo import ItemInfo +from .constants import * +from ..utils import formatText +from ..assembler import ASM + + +class ShopItem(ItemInfo): + def __init__(self, index): + self.__index = index + # pass in the alternate index for shop 2 + # The "real" room is at 0x2A1, but we store the second item data as if link were in 0x2A7 + room = 0x2A1 + if index == 1: + room = 0x2A7 + super().__init__(room) + + def patch(self, rom, option, *, multiworld=None): + mw_text = "" + if multiworld: + mw_text = f" for player {rom.player_names[multiworld - 1]}" + + if self.__index == 0: + # Old index, maybe not needed any more + rom.patch(0x04, 0x37C5, "08", "%02X" % (CHEST_ITEMS[option])) + rom.texts[0x030] = formatText(f"Deluxe {{%s}} 200 {{RUPEES}}{mw_text}!" % (option), ask="Buy No Way") + rom.banks[0x3E][0x3800 + 0x2A1] = CHEST_ITEMS[option] + if multiworld: + rom.banks[0x3E][0x3300 + 0x2A1] = multiworld + elif self.__index == 1: + rom.patch(0x04, 0x37C6, "02", "%02X" % (CHEST_ITEMS[option])) + rom.texts[0x02C] = formatText(f"{{%s}} Only 980 {{RUPEES}}{mw_text}!" % (option), ask="Buy No Way") + + rom.banks[0x3E][0x3800 + 0x2A7] = CHEST_ITEMS[option] + if multiworld: + rom.banks[0x3E][0x3300 + 0x2A7] = multiworld + + def read(self, rom): + value = rom.banks[0x04][0x37C5 + self.__index] + for k, v in CHEST_ITEMS.items(): + if v == value: + return k + raise ValueError("Could not find shop item contents in ROM (0x%02x)" % (value)) \ No newline at end of file diff --git a/worlds/ladx/LADXR/locations/song.py b/worlds/ladx/LADXR/locations/song.py new file mode 100644 index 0000000000..25937dcef4 --- /dev/null +++ b/worlds/ladx/LADXR/locations/song.py @@ -0,0 +1,5 @@ +from .droppedKey import DroppedKey + + +class Song(DroppedKey): + pass diff --git a/worlds/ladx/LADXR/locations/startItem.py b/worlds/ladx/LADXR/locations/startItem.py new file mode 100644 index 0000000000..95dd6ba54a --- /dev/null +++ b/worlds/ladx/LADXR/locations/startItem.py @@ -0,0 +1,38 @@ +from .itemInfo import ItemInfo +from .constants import * +from .droppedKey import DroppedKey +from ..assembler import ASM +from ..utils import formatText +from ..roomEditor import RoomEditor + + +class StartItem(DroppedKey): + # We need to give something here that we can use to progress. + # FEATHER + OPTIONS = [SWORD, SHIELD, POWER_BRACELET, OCARINA, BOOMERANG, MAGIC_ROD, TAIL_KEY, SHOVEL, HOOKSHOT, PEGASUS_BOOTS, MAGIC_POWDER, BOMB] + + MULTIWORLD = False + + def __init__(self): + super().__init__(0x2A3) + self.give_bowwow = False + + def configure(self, options): + if options.bowwow != 'normal': + # When we have bowwow mode, we pretend to be a sword for logic reasons + self.OPTIONS = [SWORD] + self.give_bowwow = True + if options.randomstartlocation and options.entranceshuffle != 'none': + self.OPTIONS.append(FLIPPERS) + + def patch(self, rom, option, *, multiworld=None): + assert multiworld is None + + if self.give_bowwow: + option = BOWWOW + rom.texts[0xC8] = formatText("Got BowWow!") + + if option != SHIELD: + rom.patch(5, 0x0CDA, ASM("ld a, $22"), ASM("ld a, $00")) # do not change links sprite into the one with a shield + + super().patch(rom, option) diff --git a/worlds/ladx/LADXR/locations/toadstool.py b/worlds/ladx/LADXR/locations/toadstool.py new file mode 100644 index 0000000000..381b023ec8 --- /dev/null +++ b/worlds/ladx/LADXR/locations/toadstool.py @@ -0,0 +1,18 @@ +from .droppedKey import DroppedKey +from .items import * + + +class Toadstool(DroppedKey): + def __init__(self): + super().__init__(0x050) + + def configure(self, options): + if not options.witch: + self.OPTIONS = [TOADSTOOL] + else: + super().configure(options) + + def read(self, rom): + if len(self.OPTIONS) == 1: + return TOADSTOOL + return super().read(rom) diff --git a/worlds/ladx/LADXR/locations/tradeSequence.py b/worlds/ladx/LADXR/locations/tradeSequence.py new file mode 100644 index 0000000000..1587bb5e13 --- /dev/null +++ b/worlds/ladx/LADXR/locations/tradeSequence.py @@ -0,0 +1,55 @@ +from .itemInfo import ItemInfo +from .constants import * +from .droppedKey import DroppedKey + +TradeRequirements = { + TRADING_ITEM_YOSHI_DOLL: None, + TRADING_ITEM_RIBBON: TRADING_ITEM_YOSHI_DOLL, + TRADING_ITEM_DOG_FOOD: TRADING_ITEM_RIBBON, + TRADING_ITEM_BANANAS: TRADING_ITEM_DOG_FOOD, + TRADING_ITEM_STICK: TRADING_ITEM_BANANAS, + TRADING_ITEM_HONEYCOMB: TRADING_ITEM_STICK, + TRADING_ITEM_PINEAPPLE: TRADING_ITEM_HONEYCOMB, + TRADING_ITEM_HIBISCUS: TRADING_ITEM_PINEAPPLE, + TRADING_ITEM_LETTER: TRADING_ITEM_HIBISCUS, + TRADING_ITEM_BROOM: TRADING_ITEM_LETTER, + TRADING_ITEM_FISHING_HOOK: TRADING_ITEM_BROOM, + TRADING_ITEM_NECKLACE: TRADING_ITEM_FISHING_HOOK, + TRADING_ITEM_SCALE: TRADING_ITEM_NECKLACE, + TRADING_ITEM_MAGNIFYING_GLASS: TRADING_ITEM_SCALE, +} +class TradeSequenceItem(DroppedKey): + def __init__(self, room, default_item): + self.unadjusted_room = room + if room == 0x2B2: + # Offset room for trade items to avoid collisions + roomLo = room & 0xFF + roomHi = room ^ roomLo + roomLo = (roomLo + 2) & 0xFF + room = roomHi | roomLo + super().__init__(room) + self.default_item = default_item + + def configure(self, options): + if not options.tradequest: + self.OPTIONS = [self.default_item] + super().configure(options) + + #def patch(self, rom, option, *, multiworld=None): + # rom.banks[0x3E][self.room + 0x3B16] = CHEST_ITEMS[option] + + def read(self, rom): + assert(False) + assert self._location is not None, hex(self.room) + value = rom.banks[0x3E][self.room + 0x3B16] + for k, v in CHEST_ITEMS.items(): + if v == value: + return k + raise ValueError("Could not find owl statue contents in ROM (0x%02x)" % (value)) + + def __repr__(self): + return "%s:%03x" % (self.__class__.__name__, self.room) + + @property + def nameId(self): + return "0x%03X-Trade" % self.unadjusted_room diff --git a/worlds/ladx/LADXR/locations/tunicFairy.py b/worlds/ladx/LADXR/locations/tunicFairy.py new file mode 100644 index 0000000000..84fc9ca735 --- /dev/null +++ b/worlds/ladx/LADXR/locations/tunicFairy.py @@ -0,0 +1,27 @@ +from .itemInfo import ItemInfo +from .constants import * + + +class TunicFairy(ItemInfo): + + def __init__(self, index): + self.index = index + super().__init__(0x301) + + def patch(self, rom, option, *, multiworld=None): + # Old index, maybe not needed anymore + rom.banks[0x36][0x11BF + self.index] = CHEST_ITEMS[option] + rom.banks[0x3e][0x3800 + 0x301 + self.index*3] = CHEST_ITEMS[option] + if multiworld: + rom.banks[0x3e][0x3300 + 0x301 + self.index*3] = multiworld + + def read(self, rom): + value = rom.banks[0x36][0x11BF + self.index] + for k, v in CHEST_ITEMS.items(): + if v == value: + return k + raise ValueError("Could not find tunic fairy contents in ROM (0x%02x)" % (value)) + + @property + def nameId(self): + return "0x%03X-%s" % (self.room, self.index) diff --git a/worlds/ladx/LADXR/locations/witch.py b/worlds/ladx/LADXR/locations/witch.py new file mode 100644 index 0000000000..6435a30e32 --- /dev/null +++ b/worlds/ladx/LADXR/locations/witch.py @@ -0,0 +1,31 @@ +from .constants import * +from .itemInfo import ItemInfo + + +class Witch(ItemInfo): + def __init__(self): + super().__init__(0x2A2) + + def configure(self, options): + if not options.witch: + self.OPTIONS = [MAGIC_POWDER] + + def patch(self, rom, option, *, multiworld=None): + if multiworld or option != MAGIC_POWDER: + + rom.banks[0x3E][self.room + 0x3800] = CHEST_ITEMS[option] + if multiworld is not None: + rom.banks[0x3E][0x3300 + self.room] = multiworld + else: + rom.banks[0x3E][0x3300 + self.room] = 0 + + #rom.patch(0x05, 0x08D5, "09", "%02x" % (CHEST_ITEMS[option])) + + def read(self, rom): + if rom.banks[0x05][0x08EF] != 0x00: + return MAGIC_POWDER + value = rom.banks[0x05][0x08D5] + for k, v in CHEST_ITEMS.items(): + if v == value: + return k + raise ValueError("Could not find witch contents in ROM (0x%02x)" % (value)) diff --git a/worlds/ladx/LADXR/logic/__init__.py b/worlds/ladx/LADXR/logic/__init__.py new file mode 100644 index 0000000000..11a0acfd01 --- /dev/null +++ b/worlds/ladx/LADXR/logic/__init__.py @@ -0,0 +1,284 @@ +from . import overworld +from . import dungeon1 +from . import dungeon2 +from . import dungeon3 +from . import dungeon4 +from . import dungeon5 +from . import dungeon6 +from . import dungeon7 +from . import dungeon8 +from . import dungeonColor +from .requirements import AND, OR, COUNT, COUNTS, FOUND, RequirementsSettings +from .location import Location +from ..locations.items import * +from ..locations.keyLocation import KeyLocation +from ..worldSetup import WorldSetup +from .. import itempool +from .. import mapgen + + +class Logic: + def __init__(self, configuration_options, *, world_setup): + self.world_setup = world_setup + r = RequirementsSettings(configuration_options) + + if configuration_options.overworld == "dungeondive": + world = overworld.DungeonDiveOverworld(configuration_options, r) + elif configuration_options.overworld == "random": + world = mapgen.LogicGenerator(configuration_options, world_setup, r, world_setup.map) + else: + world = overworld.World(configuration_options, world_setup, r) + + if configuration_options.overworld == "nodungeons": + world.updateIndoorLocation("d1", dungeon1.NoDungeon1(configuration_options, world_setup, r).entrance) + world.updateIndoorLocation("d2", dungeon2.NoDungeon2(configuration_options, world_setup, r).entrance) + world.updateIndoorLocation("d3", dungeon3.NoDungeon3(configuration_options, world_setup, r).entrance) + world.updateIndoorLocation("d4", dungeon4.NoDungeon4(configuration_options, world_setup, r).entrance) + world.updateIndoorLocation("d5", dungeon5.NoDungeon5(configuration_options, world_setup, r).entrance) + world.updateIndoorLocation("d6", dungeon6.NoDungeon6(configuration_options, world_setup, r).entrance) + world.updateIndoorLocation("d7", dungeon7.NoDungeon7(configuration_options, world_setup, r).entrance) + world.updateIndoorLocation("d8", dungeon8.NoDungeon8(configuration_options, world_setup, r).entrance) + world.updateIndoorLocation("d0", dungeonColor.NoDungeonColor(configuration_options, world_setup, r).entrance) + elif configuration_options.overworld != "random": + world.updateIndoorLocation("d1", dungeon1.Dungeon1(configuration_options, world_setup, r).entrance) + world.updateIndoorLocation("d2", dungeon2.Dungeon2(configuration_options, world_setup, r).entrance) + world.updateIndoorLocation("d3", dungeon3.Dungeon3(configuration_options, world_setup, r).entrance) + world.updateIndoorLocation("d4", dungeon4.Dungeon4(configuration_options, world_setup, r).entrance) + world.updateIndoorLocation("d5", dungeon5.Dungeon5(configuration_options, world_setup, r).entrance) + world.updateIndoorLocation("d6", dungeon6.Dungeon6(configuration_options, world_setup, r).entrance) + world.updateIndoorLocation("d7", dungeon7.Dungeon7(configuration_options, world_setup, r).entrance) + world.updateIndoorLocation("d8", dungeon8.Dungeon8(configuration_options, world_setup, r).entrance) + world.updateIndoorLocation("d0", dungeonColor.DungeonColor(configuration_options, world_setup, r).entrance) + + if configuration_options.overworld != "random": + for k in world.overworld_entrance.keys(): + assert k in world_setup.entrance_mapping, k + for k in world_setup.entrance_mapping.keys(): + assert k in world.overworld_entrance, k + + for entrance, indoor in world_setup.entrance_mapping.items(): + exterior = world.overworld_entrance[entrance] + if world.indoor_location[indoor] is not None: + exterior.location.connect(world.indoor_location[indoor], exterior.requirement) + if exterior.enterIsSet(): + exterior.location.connect(world.indoor_location[indoor], exterior.one_way_enter_requirement, one_way=True) + if exterior.exitIsSet(): + world.indoor_location[indoor].connect(exterior.location, exterior.one_way_exit_requirement, one_way=True) + + egg_trigger = AND(OCARINA, SONG1) + if configuration_options.logic == 'glitched' or configuration_options.logic == 'hell': + egg_trigger = OR(AND(OCARINA, SONG1), BOMB) + + if world_setup.goal == "seashells": + world.nightmare.connect(world.egg, COUNT(SEASHELL, 20)) + elif world_setup.goal in ("raft", "bingo", "bingo-full"): + world.nightmare.connect(world.egg, egg_trigger) + else: + goal = int(world_setup.goal) + if goal < 0: + world.nightmare.connect(world.egg, None) + elif goal == 0: + world.nightmare.connect(world.egg, egg_trigger) + elif goal == 8: + world.nightmare.connect(world.egg, AND(egg_trigger, INSTRUMENT1, INSTRUMENT2, INSTRUMENT3, INSTRUMENT4, INSTRUMENT5, INSTRUMENT6, INSTRUMENT7, INSTRUMENT8)) + else: + world.nightmare.connect(world.egg, AND(egg_trigger, COUNTS([INSTRUMENT1, INSTRUMENT2, INSTRUMENT3, INSTRUMENT4, INSTRUMENT5, INSTRUMENT6, INSTRUMENT7, INSTRUMENT8], goal))) + + # if configuration_options.dungeon_items == 'keysy': + # for n in range(9): + # for count in range(9): + # world.start.add(KeyLocation("KEY%d" % (n + 1))) + # world.start.add(KeyLocation("NIGHTMARE_KEY%d" % (n + 1))) + + self.world = world + self.start = world.start + self.windfish = world.windfish + self.location_list = [] + self.iteminfo_list = [] + + self.__location_set = set() + self.__recursiveFindAll(self.start) + del self.__location_set + + for ii in self.iteminfo_list: + ii.configure(configuration_options) + + def dumpFlatRequirements(self): + def __rec(location, req): + if hasattr(location, "flat_requirements"): + new_flat_requirements = requirements.mergeFlat(location.flat_requirements, requirements.flatten(req)) + if new_flat_requirements == location.flat_requirements: + return + location.flat_requirements = new_flat_requirements + else: + location.flat_requirements = requirements.flatten(req) + for connection, requirement in location.simple_connections: + __rec(connection, AND(req, requirement) if req else requirement) + for connection, requirement in location.gated_connections: + __rec(connection, AND(req, requirement) if req else requirement) + __rec(self.start, None) + for ii in self.iteminfo_list: + print(ii) + for fr in ii._location.flat_requirements: + print(" " + ", ".join(sorted(map(str, fr)))) + + def __recursiveFindAll(self, location): + if location in self.__location_set: + return + self.location_list.append(location) + self.__location_set.add(location) + for ii in location.items: + self.iteminfo_list.append(ii) + for connection, requirement in location.simple_connections: + self.__recursiveFindAll(connection) + for connection, requirement in location.gated_connections: + self.__recursiveFindAll(connection) + + +class MultiworldLogic: + def __init__(self, settings, rnd=None, *, world_setups=None): + assert rnd or world_setups + self.worlds = [] + self.start = Location() + self.location_list = [self.start] + self.iteminfo_list = [] + + for n in range(settings.multiworld): + options = settings.multiworld_settings[n] + world = None + if world_setups: + world = Logic(options, world_setup=world_setups[n]) + else: + for cnt in range(1000): # Try the world setup in case entrance randomization generates unsolvable logic + world_setup = WorldSetup() + world_setup.randomize(options, rnd) + world = Logic(options, world_setup=world_setup) + if options.entranceshuffle not in ("advanced", "expert", "insanity") or len(world.iteminfo_list) == sum(itempool.ItemPool(options, rnd).toDict().values()): + break + + for ii in world.iteminfo_list: + ii.world = n + + req_done_set = set() + for loc in world.location_list: + loc.simple_connections = [(target, addWorldIdToRequirements(req_done_set, n, req)) for target, req in loc.simple_connections] + loc.gated_connections = [(target, addWorldIdToRequirements(req_done_set, n, req)) for target, req in loc.gated_connections] + loc.items = [MultiworldItemInfoWrapper(n, options, ii) for ii in loc.items] + self.iteminfo_list += loc.items + + self.worlds.append(world) + self.start.simple_connections += world.start.simple_connections + self.start.gated_connections += world.start.gated_connections + self.start.items += world.start.items + world.start.items.clear() + self.location_list += world.location_list + + self.entranceMapping = None + + +class MultiworldMetadataWrapper: + def __init__(self, world, metadata): + self.world = world + self.metadata = metadata + + @property + def name(self): + return self.metadata.name + + @property + def area(self): + return "P%d %s" % (self.world + 1, self.metadata.area) + + +class MultiworldItemInfoWrapper: + def __init__(self, world, configuration_options, target): + self.world = world + self.world_count = configuration_options.multiworld + self.target = target + self.dungeon_items = configuration_options.dungeon_items + self.MULTIWORLD_OPTIONS = None + self.item = None + + @property + def nameId(self): + return self.target.nameId + + @property + def forced_item(self): + if self.target.forced_item is None: + return None + if "_W" in self.target.forced_item: + return self.target.forced_item + return "%s_W%d" % (self.target.forced_item, self.world) + + @property + def room(self): + return self.target.room + + @property + def metadata(self): + return MultiworldMetadataWrapper(self.world, self.target.metadata) + + @property + def MULTIWORLD(self): + return self.target.MULTIWORLD + + def read(self, rom): + world = rom.banks[0x3E][0x3300 + self.target.room] if self.target.MULTIWORLD else self.world + return "%s_W%d" % (self.target.read(rom), world) + + def getOptions(self): + if self.MULTIWORLD_OPTIONS is None: + options = self.target.getOptions() + if self.target.MULTIWORLD and len(options) > 1: + self.MULTIWORLD_OPTIONS = [] + for n in range(self.world_count): + self.MULTIWORLD_OPTIONS += ["%s_W%d" % (t, n) for t in options if n == self.world or self.canMultiworld(t)] + else: + self.MULTIWORLD_OPTIONS = ["%s_W%d" % (t, self.world) for t in options] + return self.MULTIWORLD_OPTIONS + + def patch(self, rom, option): + idx = option.rfind("_W") + world = int(option[idx+2:]) + option = option[:idx] + if not self.target.MULTIWORLD: + assert self.world == world + self.target.patch(rom, option) + else: + self.target.patch(rom, option, multiworld=world) + + # Return true if the item is allowed to be placed in any world, or false if it is + # world specific for this check. + def canMultiworld(self, option): + if self.dungeon_items in {'', 'smallkeys'}: + if option.startswith("MAP"): + return False + if option.startswith("COMPASS"): + return False + if option.startswith("STONE_BEAK"): + return False + if self.dungeon_items in {'', 'localkeys'}: + if option.startswith("KEY"): + return False + if self.dungeon_items in {'', 'localkeys', 'localnightmarekey', 'smallkeys'}: + if option.startswith("NIGHTMARE_KEY"): + return False + return True + + @property + def location(self): + return self.target.location + + def __repr__(self): + return "W%d:%s" % (self.world, repr(self.target)) + + +def addWorldIdToRequirements(req_done_set, world, req): + if req is None: + return None + if isinstance(req, str): + return "%s_W%d" % (req, world) + if req in req_done_set: + return req + return req.copyWithModifiedItemNames(lambda item: "%s_W%d" % (item, world)) diff --git a/worlds/ladx/LADXR/logic/dungeon1.py b/worlds/ladx/LADXR/logic/dungeon1.py new file mode 100644 index 0000000000..82321a1c0d --- /dev/null +++ b/worlds/ladx/LADXR/logic/dungeon1.py @@ -0,0 +1,46 @@ +from .requirements import * +from .location import Location +from ..locations.all import * + + +class Dungeon1: + def __init__(self, options, world_setup, r): + entrance = Location(dungeon=1) + entrance.add(DungeonChest(0x113), DungeonChest(0x115), DungeonChest(0x10E)) + Location(dungeon=1).add(DroppedKey(0x116)).connect(entrance, OR(BOMB, r.push_hardhat)) # hardhat beetles (can kill with bomb) + Location(dungeon=1).add(DungeonChest(0x10D)).connect(entrance, OR(r.attack_hookshot_powder, SHIELD)) # moldorm spawn chest + stalfos_keese_room = Location(dungeon=1).add(DungeonChest(0x114)).connect(entrance, r.attack_hookshot) # 2 stalfos 2 keese room + Location(dungeon=1).add(DungeonChest(0x10C)).connect(entrance, BOMB) # hidden seashell room + dungeon1_upper_left = Location(dungeon=1).connect(entrance, AND(KEY1, FOUND(KEY1, 3))) + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=1).add(OwlStatue(0x103), OwlStatue(0x104)).connect(dungeon1_upper_left, STONE_BEAK1) + feather_chest = Location(dungeon=1).add(DungeonChest(0x11D)).connect(dungeon1_upper_left, SHIELD) # feather location, behind spike enemies. can shield bump into pit (only shield works) + boss_key = Location(dungeon=1).add(DungeonChest(0x108)).connect(entrance, AND(FEATHER, KEY1, FOUND(KEY1, 3))) # boss key + dungeon1_right_side = Location(dungeon=1).connect(entrance, AND(KEY1, FOUND(KEY1, 3))) + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=1).add(OwlStatue(0x10A)).connect(dungeon1_right_side, STONE_BEAK1) + Location(dungeon=1).add(DungeonChest(0x10A)).connect(dungeon1_right_side, OR(r.attack_hookshot, SHIELD)) # three of a kind, shield stops the suit from changing + dungeon1_miniboss = Location(dungeon=1).connect(dungeon1_right_side, AND(r.miniboss_requirements[world_setup.miniboss_mapping[0]], FEATHER)) + dungeon1_boss = Location(dungeon=1).connect(dungeon1_miniboss, NIGHTMARE_KEY1) + Location(dungeon=1).add(HeartContainer(0x106), Instrument(0x102)).connect(dungeon1_boss, r.boss_requirements[world_setup.boss_mapping[0]]) + + if options.logic not in ('normal', 'casual'): + stalfos_keese_room.connect(entrance, r.attack_hookshot_powder) # stalfos jump away when you press a button. + + if options.logic == 'glitched' or options.logic == 'hell': + boss_key.connect(entrance, FEATHER) # super jump + dungeon1_miniboss.connect(dungeon1_right_side, r.miniboss_requirements[world_setup.miniboss_mapping[0]]) # damage boost or buffer pause over the pit to cross or mushroom + + if options.logic == 'hell': + feather_chest.connect(dungeon1_upper_left, SWORD) # keep slashing the spiked beetles until they keep moving 1 pixel close towards you and the pit, to get them to fall + boss_key.connect(entrance, FOUND(KEY1,3)) # damage boost off the hardhat to cross the pit + + self.entrance = entrance + + +class NoDungeon1: + def __init__(self, options, world_setup, r): + entrance = Location(dungeon=1) + Location(dungeon=1).add(HeartContainer(0x106), Instrument(0x102)).connect(entrance, r.boss_requirements[ + world_setup.boss_mapping[0]]) + self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/dungeon2.py b/worlds/ladx/LADXR/logic/dungeon2.py new file mode 100644 index 0000000000..3bb95edbc8 --- /dev/null +++ b/worlds/ladx/LADXR/logic/dungeon2.py @@ -0,0 +1,62 @@ +from .requirements import * +from .location import Location +from ..locations.all import * + + +class Dungeon2: + def __init__(self, options, world_setup, r): + entrance = Location(dungeon=2) + Location(dungeon=2).add(DungeonChest(0x136)).connect(entrance, POWER_BRACELET) # chest at entrance + dungeon2_l2 = Location(dungeon=2).connect(entrance, AND(KEY2, FOUND(KEY2, 5))) # towards map chest + dungeon2_map_chest = Location(dungeon=2).add(DungeonChest(0x12E)).connect(dungeon2_l2, AND(r.attack_hookshot_powder, OR(FEATHER, HOOKSHOT))) # map chest + dungeon2_r2 = Location(dungeon=2).connect(entrance, r.fire) + Location(dungeon=2).add(DroppedKey(0x132)).connect(dungeon2_r2, r.attack_skeleton) + Location(dungeon=2).add(DungeonChest(0x137)).connect(dungeon2_r2, AND(KEY2, FOUND(KEY2, 5), OR(r.rear_attack, r.rear_attack_range))) # compass chest + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=2).add(OwlStatue(0x133)).connect(dungeon2_r2, STONE_BEAK2) + dungeon2_r3 = Location(dungeon=2).add(DungeonChest(0x138)).connect(dungeon2_r2, r.attack_hookshot) # first chest with key, can hookshot the switch in previous room + dungeon2_r4 = Location(dungeon=2).add(DungeonChest(0x139)).connect(dungeon2_r3, FEATHER) # button spawn chest + if options.logic == "casual": + shyguy_key_drop = Location(dungeon=2).add(DroppedKey(0x134)).connect(dungeon2_r3, AND(FEATHER, OR(r.rear_attack, r.rear_attack_range))) # shyguy drop key + else: + shyguy_key_drop = Location(dungeon=2).add(DroppedKey(0x134)).connect(dungeon2_r3, OR(r.rear_attack, AND(FEATHER, r.rear_attack_range))) # shyguy drop key + dungeon2_r5 = Location(dungeon=2).connect(dungeon2_r4, AND(KEY2, FOUND(KEY2, 3))) # push two blocks together room with owl statue + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=2).add(OwlStatue(0x12F)).connect(dungeon2_r5, STONE_BEAK2) # owl statue is before miniboss + miniboss = Location(dungeon=2).add(DungeonChest(0x126)).add(DungeonChest(0x121)).connect(dungeon2_r5, AND(FEATHER, r.miniboss_requirements[world_setup.miniboss_mapping[1]])) # post hinox + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=2).add(OwlStatue(0x129)).connect(miniboss, STONE_BEAK2) # owl statue after the miniboss + + dungeon2_ghosts_room = Location(dungeon=2).connect(miniboss, AND(KEY2, FOUND(KEY2, 5))) + dungeon2_ghosts_chest = Location(dungeon=2).add(DungeonChest(0x120)).connect(dungeon2_ghosts_room, OR(r.fire, BOW)) # bracelet chest + dungeon2_r6 = Location(dungeon=2).add(DungeonChest(0x122)).connect(miniboss, POWER_BRACELET) + dungeon2_boss_key = Location(dungeon=2).add(DungeonChest(0x127)).connect(dungeon2_r6, AND(r.attack_hookshot_powder, OR(BOW, BOMB, MAGIC_ROD, AND(OCARINA, SONG1), POWER_BRACELET))) + dungeon2_pre_stairs_boss = Location(dungeon=2).connect(dungeon2_r6, AND(POWER_BRACELET, KEY2, FOUND(KEY2, 5))) + dungeon2_post_stairs_boss = Location(dungeon=2).connect(dungeon2_pre_stairs_boss, POWER_BRACELET) + dungeon2_pre_boss = Location(dungeon=2).connect(dungeon2_post_stairs_boss, FEATHER) + # If we can get here, we have everything for the boss. So this is also the goal room. + dungeon2_boss = Location(dungeon=2).add(HeartContainer(0x12B), Instrument(0x12a)).connect(dungeon2_pre_boss, AND(NIGHTMARE_KEY2, r.boss_requirements[world_setup.boss_mapping[1]])) + + if options.logic == 'glitched' or options.logic == 'hell': + dungeon2_ghosts_chest.connect(dungeon2_ghosts_room, SWORD) # use sword to spawn ghosts on other side of the room so they run away (logically irrelevant because of torches at start) + dungeon2_r6.connect(miniboss, FEATHER) # superjump to staircase next to hinox. + + if options.logic == 'hell': + dungeon2_map_chest.connect(dungeon2_l2, AND(r.attack_hookshot_powder, PEGASUS_BOOTS)) # use boots to jump over the pits + dungeon2_r4.connect(dungeon2_r3, OR(PEGASUS_BOOTS, HOOKSHOT)) # can use both pegasus boots bonks or hookshot spam to cross the pit room + dungeon2_r4.connect(shyguy_key_drop, r.rear_attack_range, one_way=True) # adjust for alternate requirements for dungeon2_r4 + miniboss.connect(dungeon2_r5, AND(PEGASUS_BOOTS, r.miniboss_requirements[world_setup.miniboss_mapping[1]])) # use boots to dash over the spikes in the 2d section + dungeon2_pre_stairs_boss.connect(dungeon2_r6, AND(HOOKSHOT, OR(BOW, BOMB, MAGIC_ROD, AND(OCARINA, SONG1)), FOUND(KEY2, 5))) # hookshot clip through the pot using both pol's voice + dungeon2_post_stairs_boss.connect(dungeon2_pre_stairs_boss, OR(BOMB, AND(PEGASUS_BOOTS, FEATHER))) # use a bomb to lower the last platform, or boots + feather to cross over top (only relevant in hell logic) + dungeon2_pre_boss.connect(dungeon2_post_stairs_boss, AND(PEGASUS_BOOTS, HOOKSHOT)) # boots bonk off bottom wall + hookshot spam across the two 1 tile pits vertically + + self.entrance = entrance + + +class NoDungeon2: + def __init__(self, options, world_setup, r): + entrance = Location(dungeon=2) + Location(dungeon=2).add(DungeonChest(0x136)).connect(entrance, POWER_BRACELET) # chest at entrance + Location(dungeon=2).add(HeartContainer(0x12B), Instrument(0x12a)).connect(entrance, r.boss_requirements[ + world_setup.boss_mapping[1]]) + self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/dungeon3.py b/worlds/ladx/LADXR/logic/dungeon3.py new file mode 100644 index 0000000000..e65c7da0ba --- /dev/null +++ b/worlds/ladx/LADXR/logic/dungeon3.py @@ -0,0 +1,89 @@ +from .requirements import * +from .location import Location +from ..locations.all import * + + +class Dungeon3: + def __init__(self, options, world_setup, r): + entrance = Location(dungeon=3) + dungeon3_reverse_eye = Location(dungeon=3).add(DungeonChest(0x153)).connect(entrance, PEGASUS_BOOTS) # Right side reverse eye + area2 = Location(dungeon=3).connect(entrance, POWER_BRACELET) + Location(dungeon=3).add(DungeonChest(0x151)).connect(area2, r.attack_hookshot_powder) # First chest with key + area2.add(DungeonChest(0x14F)) # Second chest with slime + area3 = Location(dungeon=3).connect(area2, OR(r.attack_hookshot_powder, PEGASUS_BOOTS)) # need to kill slimes to continue or pass through left path + dungeon3_zol_stalfos = Location(dungeon=3).add(DungeonChest(0x14E)).connect(area3, AND(PEGASUS_BOOTS, r.attack_skeleton)) # 3th chest requires killing the slime behind the crystal pillars + + # now we can go 4 directions, + area_up = Location(dungeon=3).connect(area3, AND(KEY3, FOUND(KEY3, 8))) + dungeon3_north_key_drop = Location(dungeon=3).add(DroppedKey(0x154)).connect(area_up, r.attack_skeleton) # north key drop + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=3).add(OwlStatue(0x154)).connect(area_up, STONE_BEAK3) + dungeon3_raised_blocks_north = Location(dungeon=3).add(DungeonChest(0x14C)) # chest locked behind raised blocks near staircase + dungeon3_raised_blocks_east = Location(dungeon=3).add(DungeonChest(0x150)) # chest locked behind raised blocks next to slime chest + area_up.connect(dungeon3_raised_blocks_north, r.attack_hookshot, one_way=True) # hit switch to reach north chest + area_up.connect(dungeon3_raised_blocks_east, r.attack_hookshot, one_way=True) # hit switch to reach east chest + + area_left = Location(dungeon=3).connect(area3, AND(KEY3, FOUND(KEY3, 8))) + area_left_key_drop = Location(dungeon=3).add(DroppedKey(0x155)).connect(area_left, r.attack_hookshot) # west key drop (no longer requires feather to get across hole), can use boomerang to knock owls into pit + + area_down = Location(dungeon=3).connect(area3, AND(KEY3, FOUND(KEY3, 8))) + dungeon3_south_key_drop = Location(dungeon=3).add(DroppedKey(0x158)).connect(area_down, r.attack_hookshot) # south keydrop, can use boomerang to knock owls into pit + + area_right = Location(dungeon=3).connect(area3, AND(KEY3, FOUND(KEY3, 4))) # We enter the top part of the map here. + Location(dungeon=3).add(DroppedKey(0x14D)).connect(area_right, r.attack_hookshot_powder) # key after the stairs. + + dungeon3_nightmare_key_chest = Location(dungeon=3).add(DungeonChest(0x147)).connect(area_right, AND(BOMB, FEATHER, PEGASUS_BOOTS)) # nightmare key chest + dungeon3_post_dodongo_chest = Location(dungeon=3).add(DungeonChest(0x146)).connect(area_right, AND(r.attack_hookshot_powder, r.miniboss_requirements[world_setup.miniboss_mapping[2]])) # boots after the miniboss + compass_chest = Location(dungeon=3).add(DungeonChest(0x142)).connect(area_right, OR(SWORD, BOMB, AND(SHIELD, r.attack_hookshot_powder))) # bomb only activates with sword, bomb or shield + dungeon3_3_bombite_room = Location(dungeon=3).add(DroppedKey(0x141)).connect(compass_chest, BOMB) # 3 bombite room + Location(dungeon=3).add(DroppedKey(0x148)).connect(area_right, r.attack_no_boomerang) # 2 zol 2 owl drop key + Location(dungeon=3).add(DungeonChest(0x144)).connect(area_right, r.attack_skeleton) # map chest + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=3).add(OwlStatue(0x140), OwlStatue(0x147)).connect(area_right, STONE_BEAK3) + + towards_boss1 = Location(dungeon=3).connect(area_right, AND(KEY3, FOUND(KEY3, 5))) + towards_boss2 = Location(dungeon=3).connect(towards_boss1, AND(KEY3, FOUND(KEY3, 6))) + towards_boss3 = Location(dungeon=3).connect(towards_boss2, AND(KEY3, FOUND(KEY3, 7))) + towards_boss4 = Location(dungeon=3).connect(towards_boss3, AND(KEY3, FOUND(KEY3, 8))) + + # Just the whole area before the boss, requirements for the boss itself and the rooms before it are the same. + pre_boss = Location(dungeon=3).connect(towards_boss4, AND(r.attack_no_boomerang, FEATHER, PEGASUS_BOOTS)) + pre_boss.add(DroppedKey(0x15B)) + + boss = Location(dungeon=3).add(HeartContainer(0x15A), Instrument(0x159)).connect(pre_boss, AND(NIGHTMARE_KEY3, r.boss_requirements[world_setup.boss_mapping[2]])) + + if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': + dungeon3_3_bombite_room.connect(area_right, BOOMERANG) # 3 bombite room from the left side, grab item with boomerang + dungeon3_reverse_eye.connect(entrance, HOOKSHOT) # hookshot the chest to get to the right side + dungeon3_north_key_drop.connect(area_up, POWER_BRACELET) # use pots to kill the enemies + dungeon3_south_key_drop.connect(area_down, POWER_BRACELET) # use pots to kill enemies + + if options.logic == 'glitched' or options.logic == 'hell': + area2.connect(dungeon3_raised_blocks_east, AND(r.attack_hookshot_powder, FEATHER), one_way=True) # use superjump to get over the bottom left block + area3.connect(dungeon3_raised_blocks_north, AND(OR(PEGASUS_BOOTS, HOOKSHOT), FEATHER), one_way=True) # use shagjump (unclipped superjump next to movable block) from north wall to get on the blocks. Instead of boots can also get to that area with a hookshot clip past the movable block + area3.connect(dungeon3_zol_stalfos, HOOKSHOT, one_way=True) # hookshot clip through the northern push block next to raised blocks chest to get to the zol + dungeon3_nightmare_key_chest.connect(area_right, AND(FEATHER, BOMB)) # superjump to right side 3 gap via top wall and jump the 2 gap + dungeon3_post_dodongo_chest.connect(area_right, AND(FEATHER, FOUND(KEY3, 6))) # superjump from keyblock path. use 2 keys to open enough blocks TODO: nag messages to skip a key + + if options.logic == 'hell': + area2.connect(dungeon3_raised_blocks_east, AND(PEGASUS_BOOTS, OR(BOW, MAGIC_ROD)), one_way=True) # use boots superhop to get over the bottom left block + area3.connect(dungeon3_raised_blocks_north, AND(PEGASUS_BOOTS, OR(BOW, MAGIC_ROD)), one_way=True) # use boots superhop off top wall or left wall to get on raised blocks + area_up.connect(dungeon3_zol_stalfos, AND(FEATHER, OR(BOW, MAGIC_ROD, SWORD)), one_way=True) # use superjump near top blocks chest to get to zol without boots, keep wall clip on right wall to get a clip on left wall or use obstacles + area_left_key_drop.connect(area_left, SHIELD) # knock everything into the pit including the teleporting owls + dungeon3_south_key_drop.connect(area_down, SHIELD) # knock everything into the pit including the teleporting owls + dungeon3_nightmare_key_chest.connect(area_right, AND(FEATHER, SHIELD)) # superjump into jumping stalfos and shield bump to right ledge + dungeon3_nightmare_key_chest.connect(area_right, AND(BOMB, PEGASUS_BOOTS, HOOKSHOT)) # boots bonk across the pits with pit buffering and hookshot to the chest + compass_chest.connect(dungeon3_3_bombite_room, OR(BOW, MAGIC_ROD, AND(OR(FEATHER, PEGASUS_BOOTS), OR(SWORD, MAGIC_POWDER))), one_way=True) # 3 bombite room from the left side, use a bombite to blow open the wall without bombs + pre_boss.connect(towards_boss4, AND(r.attack_no_boomerang, FEATHER, POWER_BRACELET)) # use bracelet super bounce glitch to pass through first part underground section + pre_boss.connect(towards_boss4, AND(r.attack_no_boomerang, PEGASUS_BOOTS, "MEDICINE2")) # use medicine invulnerability to pass through the 2d section with a boots bonk to reach the staircase + + self.entrance = entrance + + +class NoDungeon3: + def __init__(self, options, world_setup, r): + entrance = Location(dungeon=3) + Location(dungeon=3).add(HeartContainer(0x15A), Instrument(0x159)).connect(entrance, AND(POWER_BRACELET, r.boss_requirements[ + world_setup.boss_mapping[2]])) + + self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/dungeon4.py b/worlds/ladx/LADXR/logic/dungeon4.py new file mode 100644 index 0000000000..7d71c89f0c --- /dev/null +++ b/worlds/ladx/LADXR/logic/dungeon4.py @@ -0,0 +1,81 @@ +from .requirements import * +from .location import Location +from ..locations.all import * + + +class Dungeon4: + def __init__(self, options, world_setup, r): + entrance = Location(dungeon=4) + entrance.add(DungeonChest(0x179)) # stone slab chest + entrance.add(DungeonChest(0x16A)) # map chest + right_of_entrance = Location(dungeon=4).add(DungeonChest(0x178)).connect(entrance, AND(SHIELD, r.attack_hookshot_powder)) # 1 zol 2 spike beetles 1 spark chest + Location(dungeon=4).add(DungeonChest(0x17B)).connect(right_of_entrance, AND(SHIELD, SWORD)) # room with key chest + rightside_crossroads = Location(dungeon=4).connect(entrance, AND(FEATHER, PEGASUS_BOOTS)) # 2 key chests on the right. + pushable_block_chest = Location(dungeon=4).add(DungeonChest(0x171)).connect(rightside_crossroads, BOMB) # lower chest + puddle_crack_block_chest = Location(dungeon=4).add(DungeonChest(0x165)).connect(rightside_crossroads, OR(BOMB, FLIPPERS)) # top right chest + + double_locked_room = Location(dungeon=4).connect(right_of_entrance, AND(KEY4, FOUND(KEY4, 5)), one_way=True) + right_of_entrance.connect(double_locked_room, KEY4, one_way=True) + after_double_lock = Location(dungeon=4).connect(double_locked_room, AND(KEY4, FOUND(KEY4, 4), OR(FEATHER, FLIPPERS)), one_way=True) + double_locked_room.connect(after_double_lock, AND(KEY4, FOUND(KEY4, 2), OR(FEATHER, FLIPPERS)), one_way=True) + + dungeon4_puddle_before_crossroads = Location(dungeon=4).add(DungeonChest(0x175)).connect(after_double_lock, FLIPPERS) + north_crossroads = Location(dungeon=4).connect(after_double_lock, AND(FEATHER, PEGASUS_BOOTS)) + before_miniboss = Location(dungeon=4).connect(north_crossroads, AND(KEY4, FOUND(KEY4, 3))) + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=4).add(OwlStatue(0x16F)).connect(before_miniboss, STONE_BEAK4) + sidescroller_key = Location(dungeon=4).add(DroppedKey(0x169)).connect(before_miniboss, AND(r.attack_hookshot_powder, FLIPPERS)) # key that drops in the hole and needs swim to get + center_puddle_chest = Location(dungeon=4).add(DungeonChest(0x16E)).connect(before_miniboss, FLIPPERS) # chest with 50 rupees + left_water_area = Location(dungeon=4).connect(before_miniboss, OR(FEATHER, FLIPPERS)) # area left with zol chest and 5 symbol puzzle (water area) + left_water_area.add(DungeonChest(0x16D)) # gel chest + left_water_area.add(DungeonChest(0x168)) # key chest near the puzzle + miniboss = Location(dungeon=4).connect(before_miniboss, AND(KEY4, FOUND(KEY4, 5), r.miniboss_requirements[world_setup.miniboss_mapping[3]])) + terrace_zols_chest = Location(dungeon=4).connect(before_miniboss, FLIPPERS) # flippers to move around miniboss through 5 tile room + miniboss = Location(dungeon=4).connect(terrace_zols_chest, POWER_BRACELET, one_way=True) # reach flippers chest through the miniboss room + terrace_zols_chest.add(DungeonChest(0x160)) # flippers chest + terrace_zols_chest.connect(left_water_area, r.attack_hookshot_powder, one_way=True) # can move from flippers chest south to push the block to left area + + to_the_nightmare_key = Location(dungeon=4).connect(left_water_area, AND(FEATHER, OR(FLIPPERS, PEGASUS_BOOTS))) # 5 symbol puzzle (does not need flippers with boots + feather) + to_the_nightmare_key.add(DungeonChest(0x176)) + + before_boss = Location(dungeon=4).connect(before_miniboss, AND(r.attack_hookshot, FLIPPERS, KEY4, FOUND(KEY4, 5))) + boss = Location(dungeon=4).add(HeartContainer(0x166), Instrument(0x162)).connect(before_boss, AND(NIGHTMARE_KEY4, r.boss_requirements[world_setup.boss_mapping[3]])) + + if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': + sidescroller_key.connect(before_miniboss, AND(FEATHER, BOOMERANG)) # grab the key jumping over the water and boomerang downwards + sidescroller_key.connect(before_miniboss, AND(POWER_BRACELET, FLIPPERS)) # kill the zols with the pots in the room to spawn the key + rightside_crossroads.connect(entrance, FEATHER) # jump across the corners + puddle_crack_block_chest.connect(rightside_crossroads, FEATHER) # jump around the bombable block + north_crossroads.connect(entrance, FEATHER) # jump across the corners + after_double_lock.connect(entrance, FEATHER) # jump across the corners + dungeon4_puddle_before_crossroads.connect(after_double_lock, FEATHER) # With a tight jump feather is enough to cross the puddle without flippers + center_puddle_chest.connect(before_miniboss, FEATHER) # With a tight jump feather is enough to cross the puddle without flippers + miniboss = Location(dungeon=4).connect(terrace_zols_chest, None, one_way=True) # reach flippers chest through the miniboss room without pulling the lever + to_the_nightmare_key.connect(left_water_area, FEATHER) # With a tight jump feather is enough to reach the top left switch without flippers, or use flippers for puzzle and boots to get through 2d section + before_boss.connect(left_water_area, FEATHER) # jump to the bottom right corner of boss door room + + if options.logic == 'glitched' or options.logic == 'hell': + pushable_block_chest.connect(rightside_crossroads, FLIPPERS) # sideways block push to skip bombs + sidescroller_key.connect(before_miniboss, AND(FEATHER, OR(r.attack_hookshot_powder, POWER_BRACELET))) # superjump into the hole to grab the key while falling into the water + miniboss.connect(before_miniboss, FEATHER) # use jesus jump to transition over the water left of miniboss + + if options.logic == 'hell': + rightside_crossroads.connect(entrance, AND(PEGASUS_BOOTS, HOOKSHOT)) # pit buffer into the wall of the first pit, then boots bonk across the center, hookshot to get to the rightmost pit to a second villa buffer on the rightmost pit + pushable_block_chest.connect(rightside_crossroads, OR(PEGASUS_BOOTS, FEATHER)) # use feather to water clip into the top right corner of the bombable block, and sideways block push to gain access. Can boots bonk of top right wall, then water buffer to top of chest and boots bonk to water buffer next to chest + after_double_lock.connect(double_locked_room, AND(FOUND(KEY4, 4), PEGASUS_BOOTS), one_way=True) # use boots bonks to cross the water gaps + north_crossroads.connect(entrance, AND(PEGASUS_BOOTS, HOOKSHOT)) # pit buffer into wall of the first pit, then boots bonk towards the top and hookshot spam to get across (easier with Piece of Power) + after_double_lock.connect(entrance, PEGASUS_BOOTS) # boots bonk + pit buffer to the bottom + dungeon4_puddle_before_crossroads.connect(after_double_lock, AND(PEGASUS_BOOTS, HOOKSHOT)) # boots bonk across the water bottom wall to the bottom left corner, then hookshot up + to_the_nightmare_key.connect(left_water_area, AND(FLIPPERS, PEGASUS_BOOTS)) # Use flippers for puzzle and boots bonk to get through 2d section + before_boss.connect(left_water_area, PEGASUS_BOOTS) # boots bonk across bottom wall then boots bonk to the platform before boss door + + self.entrance = entrance + + +class NoDungeon4: + def __init__(self, options, world_setup, r): + entrance = Location(dungeon=4) + Location(dungeon=4).add(HeartContainer(0x166), Instrument(0x162)).connect(entrance, r.boss_requirements[ + world_setup.boss_mapping[3]]) + + self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/dungeon5.py b/worlds/ladx/LADXR/logic/dungeon5.py new file mode 100644 index 0000000000..b8e013066c --- /dev/null +++ b/worlds/ladx/LADXR/logic/dungeon5.py @@ -0,0 +1,89 @@ +from .requirements import * +from .location import Location +from ..locations.all import * + + +class Dungeon5: + def __init__(self, options, world_setup, r): + entrance = Location(dungeon=5) + start_hookshot_chest = Location(dungeon=5).add(DungeonChest(0x1A0)).connect(entrance, HOOKSHOT) + compass = Location(dungeon=5).add(DungeonChest(0x19E)).connect(entrance, r.attack_hookshot_powder) + fourth_stalfos_area = Location(dungeon=5).add(DroppedKey(0x181)).connect(compass, AND(SWORD, FEATHER)) # crystal rocks can only be broken by sword + + area2 = Location(dungeon=5).connect(entrance, KEY5) + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=5).add(OwlStatue(0x19A)).connect(area2, STONE_BEAK5) + Location(dungeon=5).add(DungeonChest(0x19B)).connect(area2, r.attack_hookshot_powder) # map chest + blade_trap_chest = Location(dungeon=5).add(DungeonChest(0x197)).connect(area2, HOOKSHOT) # key chest on the left + post_gohma = Location(dungeon=5).connect(area2, AND(HOOKSHOT, r.miniboss_requirements[world_setup.miniboss_mapping[4]], KEY5, FOUND(KEY5,2))) # staircase after gohma + staircase_before_boss = Location(dungeon=5).connect(post_gohma, AND(HOOKSHOT, FEATHER)) # bottom right section pits room before boss door. Path via gohma + after_keyblock_boss = Location(dungeon=5).connect(staircase_before_boss, AND(KEY5, FOUND(KEY5, 3))) # top right section pits room before boss door + after_stalfos = Location(dungeon=5).add(DungeonChest(0x196)).connect(area2, AND(SWORD, BOMB)) # Need to defeat master stalfos once for this empty chest; l2 sword beams kill but obscure + if options.owlstatues == "both" or options.owlstatues == "dungeon": + butterfly_owl = Location(dungeon=5).add(OwlStatue(0x18A)).connect(after_stalfos, AND(FEATHER, STONE_BEAK5)) + else: + butterfly_owl = None + after_stalfos.connect(staircase_before_boss, AND(FEATHER, r.attack_hookshot_powder), one_way=True) # pathway from stalfos to staircase: past butterfly room and push the block + north_of_crossroads = Location(dungeon=5).connect(after_stalfos, FEATHER) + first_bridge_chest = Location(dungeon=5).add(DungeonChest(0x18E)).connect(north_of_crossroads, OR(HOOKSHOT, AND(FEATHER, PEGASUS_BOOTS))) # south of bridge + north_bridge_chest = Location(dungeon=5).add(DungeonChest(0x188)).connect(north_of_crossroads, HOOKSHOT) # north bridge chest 50 rupees + east_bridge_chest = Location(dungeon=5).add(DungeonChest(0x18F)).connect(north_of_crossroads, HOOKSHOT) # east bridge chest small key + third_arena = Location(dungeon=5).connect(north_of_crossroads, AND(SWORD, BOMB)) # can beat 3rd m.stalfos + stone_tablet = Location(dungeon=5).add(DungeonChest(0x183)).connect(north_of_crossroads, AND(POWER_BRACELET, r.attack_skeleton)) # stone tablet + boss_key = Location(dungeon=5).add(DungeonChest(0x186)).connect(after_stalfos, AND(FLIPPERS, HOOKSHOT)) # nightmare key + before_boss = Location(dungeon=5).connect(after_keyblock_boss, HOOKSHOT) + boss = Location(dungeon=5).add(HeartContainer(0x185), Instrument(0x182)).connect(before_boss, AND(r.boss_requirements[world_setup.boss_mapping[4]], NIGHTMARE_KEY5)) + + # When we can reach the stone tablet chest, we can also reach the final location of master stalfos + m_stalfos_drop = Location(dungeon=5).add(HookshotDrop()).connect(third_arena, AND(FEATHER, SWORD, BOMB)) # can reach fourth arena from entrance with feather and sword + + if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': + blade_trap_chest.connect(area2, AND(FEATHER, r.attack_hookshot_powder)) # jump past the blade traps + boss_key.connect(after_stalfos, AND(FLIPPERS, FEATHER, PEGASUS_BOOTS)) # boots jump across + after_stalfos.connect(after_keyblock_boss, AND(FEATHER, r.attack_hookshot_powder)) # circumvent stalfos by going past gohma and backwards from boss door + if butterfly_owl: + butterfly_owl.connect(after_stalfos, AND(PEGASUS_BOOTS, STONE_BEAK5)) # boots charge + bonk to cross 2d bridge + after_stalfos.connect(staircase_before_boss, AND(PEGASUS_BOOTS, r.attack_hookshot_powder), one_way=True) # pathway from stalfos to staircase: boots charge + bonk to cross bridge, past butterfly room and push the block + staircase_before_boss.connect(post_gohma, AND(PEGASUS_BOOTS, HOOKSHOT)) # boots bonk in 2d section to skip feather + north_of_crossroads.connect(after_stalfos, HOOKSHOT) # hookshot to the right block to cross pits + first_bridge_chest.connect(north_of_crossroads, FEATHER) # tight jump from bottom wall clipped to make it over the pits + after_keyblock_boss.connect(after_stalfos, AND(FEATHER, r.attack_hookshot_powder)) # jump from bottom left to top right, skipping the keyblock + before_boss.connect(after_stalfos, AND(FEATHER, PEGASUS_BOOTS, r.attack_hookshot_powder)) # cross pits room from bottom left to top left with boots jump + + if options.logic == 'glitched' or options.logic == 'hell': + start_hookshot_chest.connect(entrance, FEATHER) # 1 pit buffer to clip bottom wall and jump across the pits + post_gohma.connect(area2, HOOKSHOT) # glitch through the blocks/pots with hookshot. Zoomerang can be used but has no logical implications because of 2d section requiring hookshot + north_bridge_chest.connect(north_of_crossroads, FEATHER) # 1 pit buffer to clip bottom wall and jump across the pits + east_bridge_chest.connect(first_bridge_chest, FEATHER) # 1 pit buffer to clip bottom wall and jump across the pits + #after_stalfos.connect(staircase_before_boss, AND(FEATHER, OR(SWORD, BOW, MAGIC_ROD))) # use the keyblock to get a wall clip in right wall to perform a superjump over the pushable block TODO: nagmessages + after_stalfos.connect(staircase_before_boss, AND(PEGASUS_BOOTS, FEATHER, OR(SWORD, BOW, MAGIC_ROD))) # charge a boots dash in bottom right corner to the right, jump before hitting the wall and use weapon to the left side before hitting the wall + + if options.logic == 'hell': + start_hookshot_chest.connect(entrance, PEGASUS_BOOTS) # use pit buffer to clip into the bottom wall and boots bonk off the wall again + fourth_stalfos_area.connect(compass, AND(PEGASUS_BOOTS, SWORD)) # do an incredibly hard boots bonk setup to get across the hanging platforms in the 2d section + blade_trap_chest.connect(area2, AND(PEGASUS_BOOTS, r.attack_hookshot_powder)) # boots bonk + pit buffer past the blade traps + post_gohma.connect(area2, AND(PEGASUS_BOOTS, FEATHER, POWER_BRACELET, r.attack_hookshot_powder)) # use boots jump in room with 2 zols + flying arrows to pit buffer above pot, then jump across. Sideways block push + pick up pots to reach post_gohma + staircase_before_boss.connect(post_gohma, AND(PEGASUS_BOOTS, FEATHER)) # to pass 2d section, tight jump on left screen: hug left wall on little platform, then dash right off platform and jump while in midair to bonk against right wall + after_stalfos.connect(staircase_before_boss, AND(FEATHER, SWORD)) # unclipped superjump in bottom right corner of staircase before boss room, jumping left over the pushable block. reverse is push block + after_stalfos.connect(area2, SWORD) # knock master stalfos down 255 times (about 23 minutes) + north_bridge_chest.connect(north_of_crossroads, PEGASUS_BOOTS) # boots bonk across the pits with pit buffering + first_bridge_chest.connect(north_of_crossroads, PEGASUS_BOOTS) # get to first chest via the north chest with pit buffering + east_bridge_chest.connect(first_bridge_chest, PEGASUS_BOOTS) # boots bonk across the pits with pit buffering + third_arena.connect(north_of_crossroads, SWORD) # can beat 3rd m.stalfos with 255 sword spins + m_stalfos_drop.connect(third_arena, AND(FEATHER, SWORD)) # beat master stalfos by knocking it down 255 times x 4 (takes about 1.5h total) + m_stalfos_drop.connect(third_arena, AND(PEGASUS_BOOTS, SWORD)) # can reach fourth arena from entrance with pegasus boots and sword + boss_key.connect(after_stalfos, FLIPPERS) # pit buffer across + if butterfly_owl: + after_keyblock_boss.connect(butterfly_owl, STONE_BEAK5, one_way=True) # pit buffer from top right to bottom in right pits room + before_boss.connect(after_stalfos, AND(FEATHER, SWORD)) # cross pits room from bottom left to top left by unclipped superjump on bottom wall on top of side wall, then jump across + + self.entrance = entrance + + +class NoDungeon5: + def __init__(self, options, world_setup, r): + entrance = Location(dungeon=5) + Location(dungeon=5).add(HeartContainer(0x185), Instrument(0x182)).connect(entrance, r.boss_requirements[ + world_setup.boss_mapping[4]]) + + self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/dungeon6.py b/worlds/ladx/LADXR/logic/dungeon6.py new file mode 100644 index 0000000000..7e51349e3a --- /dev/null +++ b/worlds/ladx/LADXR/logic/dungeon6.py @@ -0,0 +1,65 @@ +from .requirements import * +from .location import Location +from ..locations.all import * + + +class Dungeon6: + def __init__(self, options, world_setup, r, *, raft_game_chest=True): + entrance = Location(dungeon=6) + Location(dungeon=6).add(DungeonChest(0x1CF)).connect(entrance, OR(BOMB, BOW, MAGIC_ROD, COUNT(POWER_BRACELET, 2))) # 50 rupees + Location(dungeon=6).add(DungeonChest(0x1C9)).connect(entrance, COUNT(POWER_BRACELET, 2)) # 100 rupees start + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=6).add(OwlStatue(0x1BB)).connect(entrance, STONE_BEAK6) + + # Power bracelet chest + bracelet_chest = Location(dungeon=6).add(DungeonChest(0x1CE)).connect(entrance, AND(BOMB, FEATHER)) + + # left side + Location(dungeon=6).add(DungeonChest(0x1C0)).connect(entrance, AND(POWER_BRACELET, OR(BOMB, BOW, MAGIC_ROD))) # 3 wizrobes raised blocks dont need to hit the switch + left_side = Location(dungeon=6).add(DungeonChest(0x1B9)).add(DungeonChest(0x1B3)).connect(entrance, AND(POWER_BRACELET, OR(BOMB, BOOMERANG))) + Location(dungeon=6).add(DroppedKey(0x1B4)).connect(left_side, OR(BOMB, BOW, MAGIC_ROD)) # 2 wizrobe drop key + top_left = Location(dungeon=6).add(DungeonChest(0x1B0)).connect(left_side, COUNT(POWER_BRACELET, 2)) # top left chest horseheads + if raft_game_chest: + Location().add(Chest(0x06C)).connect(top_left, POWER_BRACELET) # seashell chest in raft game + + # right side + to_miniboss = Location(dungeon=6).connect(entrance, KEY6) + miniboss = Location(dungeon=6).connect(to_miniboss, AND(BOMB, r.miniboss_requirements[world_setup.miniboss_mapping[5]])) + lower_right_side = Location(dungeon=6).add(DungeonChest(0x1BE)).connect(entrance, AND(OR(BOMB, BOW, MAGIC_ROD), COUNT(POWER_BRACELET, 2))) # waterway key + medicine_chest = Location(dungeon=6).add(DungeonChest(0x1D1)).connect(lower_right_side, FEATHER) # ledge chest medicine + if options.owlstatues == "both" or options.owlstatues == "dungeon": + lower_right_owl = Location(dungeon=6).add(OwlStatue(0x1D7)).connect(lower_right_side, AND(POWER_BRACELET, STONE_BEAK6)) + + center_1 = Location(dungeon=6).add(DroppedKey(0x1C3)).connect(miniboss, AND(COUNT(POWER_BRACELET, 2), FEATHER)) # tile room key drop + center_2_and_upper_right_side = Location(dungeon=6).add(DungeonChest(0x1B1)).connect(center_1, KEY6) # top right chest horseheads + boss_key = Location(dungeon=6).add(DungeonChest(0x1B6)).connect(center_2_and_upper_right_side, AND(KEY6, HOOKSHOT)) + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=6).add(OwlStatue(0x1B6)).connect(boss_key, STONE_BEAK6) + + boss = Location(dungeon=6).add(HeartContainer(0x1BC), Instrument(0x1b5)).connect(center_1, AND(NIGHTMARE_KEY6, r.boss_requirements[world_setup.boss_mapping[5]])) + + if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': + bracelet_chest.connect(entrance, BOMB) # get through 2d section by "fake" jumping to the ladders + center_1.connect(miniboss, AND(COUNT(POWER_BRACELET, 2), PEGASUS_BOOTS)) # use a boots dash to get over the platforms + + if options.logic == 'glitched' or options.logic == 'hell': + entrance.connect(left_side, AND(POWER_BRACELET, FEATHER), one_way=True) # path from entrance to left_side: use superjumps to pass raised blocks + lower_right_side.connect(center_2_and_upper_right_side, AND(FEATHER, OR(SWORD, BOW, MAGIC_ROD)), one_way=True) # path from lower_right_side to center_2: superjump from waterway towards dodongos. superjump next to corner block, so weapons added + center_2_and_upper_right_side.connect(center_1, AND(POWER_BRACELET, FEATHER), one_way=True) # going backwards from dodongos, use a shaq jump to pass by keyblock at tile room + boss_key.connect(lower_right_side, FEATHER) # superjump from waterway to the left. POWER_BRACELET is implied from lower_right_side + + if options.logic == 'hell': + entrance.connect(left_side, AND(POWER_BRACELET, PEGASUS_BOOTS, OR(BOW, MAGIC_ROD)), one_way=True) # can boots superhop off the top right corner in 3 wizrobe raised blocks room + medicine_chest.connect(lower_right_side, AND(PEGASUS_BOOTS, OR(MAGIC_ROD, BOW))) # can boots superhop off the top wall with bow or magic rod + center_1.connect(miniboss, AND(COUNT(POWER_BRACELET, 2))) # use a double damage boost from the sparks to get across (first one is free, second one needs to buffer while in midair for spark to get close enough) + lower_right_side.connect(center_2_and_upper_right_side, FEATHER, one_way=True) # path from lower_right_side to center_2: superjump from waterway towards dodongos. superjump next to corner block is super tight to get enough horizontal distance + + self.entrance = entrance + + +class NoDungeon6: + def __init__(self, options, world_setup, r): + entrance = Location(dungeon=6) + Location(dungeon=6).add(HeartContainer(0x1BC), Instrument(0x1b5)).connect(entrance, r.boss_requirements[ + world_setup.boss_mapping[5]]) + self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/dungeon7.py b/worlds/ladx/LADXR/logic/dungeon7.py new file mode 100644 index 0000000000..594b4d083c --- /dev/null +++ b/worlds/ladx/LADXR/logic/dungeon7.py @@ -0,0 +1,65 @@ +from .requirements import * +from .location import Location +from ..locations.all import * + + +class Dungeon7: + def __init__(self, options, world_setup, r): + entrance = Location(dungeon=7) + first_key = Location(dungeon=7).add(DroppedKey(0x210)).connect(entrance, r.attack_hookshot_powder) + topright_pillar_area = Location(dungeon=7).connect(entrance, KEY7) + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=7).add(OwlStatue(0x216)).connect(topright_pillar_area, STONE_BEAK7) + topright_pillar = Location(dungeon=7).add(DungeonChest(0x212)).connect(topright_pillar_area, POWER_BRACELET) # map chest + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=7).add(OwlStatue(0x204)).connect(topright_pillar_area, STONE_BEAK7) + topright_pillar_area.add(DungeonChest(0x209)) # stone slab chest can be reached by dropping down a hole + three_of_a_kind_north = Location(dungeon=7).add(DungeonChest(0x211)).connect(topright_pillar_area, OR(r.attack_hookshot, AND(FEATHER, SHIELD))) # compass chest; path without feather with hitting switch by falling on the raised blocks. No bracelet because ball does not reset + bottomleftF2_area = Location(dungeon=7).connect(topright_pillar_area, r.attack_hookshot) # area with hinox, be able to hit a switch to reach that area + topleftF1_chest = Location(dungeon=7).add(DungeonChest(0x201)) # top left chest on F1 + bottomleftF2_area.connect(topleftF1_chest, None, one_way = True) # drop down in left most holes of hinox room or tile room + Location(dungeon=7).add(DroppedKey(0x21B)).connect(bottomleftF2_area, r.attack_hookshot) # hinox drop key + # Most of the dungeon can be accessed at this point. + if options.owlstatues == "both" or options.owlstatues == "dungeon": + bottomleft_owl = Location(dungeon=7).add(OwlStatue(0x21C)).connect(bottomleftF2_area, AND(BOMB, STONE_BEAK7)) + nightmare_key = Location(dungeon=7).add(DungeonChest(0x224)).connect(bottomleftF2_area, r.miniboss_requirements[world_setup.miniboss_mapping[6]]) # nightmare key after the miniboss + mirror_shield_chest = Location(dungeon=7).add(DungeonChest(0x21A)).connect(bottomleftF2_area, r.attack_hookshot) # mirror shield chest, need to be able to hit a switch to reach or + bottomleftF2_area.connect(mirror_shield_chest, AND(KEY7, FOUND(KEY7, 3)), one_way = True) # reach mirror shield chest from hinox area by opening keyblock + toprightF1_chest = Location(dungeon=7).add(DungeonChest(0x204)).connect(bottomleftF2_area, r.attack_hookshot) # chest on the F1 right ledge. Added attack_hookshot since switch needs to be hit to get back up + final_pillar_area = Location(dungeon=7).add(DungeonChest(0x21C)).connect(bottomleftF2_area, AND(BOMB, HOOKSHOT)) # chest that needs to spawn to get to the last pillar + final_pillar = Location(dungeon=7).connect(final_pillar_area, POWER_BRACELET) # decouple chest from pillar + + beamos_horseheads_area = Location(dungeon=7).connect(final_pillar, NIGHTMARE_KEY7) # area behind boss door + beamos_horseheads = Location(dungeon=7).add(DungeonChest(0x220)).connect(beamos_horseheads_area, POWER_BRACELET) # 100 rupee chest / medicine chest (DX) behind boss door + pre_boss = Location(dungeon=7).connect(beamos_horseheads_area, HOOKSHOT) # raised plateau before boss staircase + boss = Location(dungeon=7).add(HeartContainer(0x223), Instrument(0x22c)).connect(pre_boss, r.boss_requirements[world_setup.boss_mapping[6]]) + + if options.logic == 'glitched' or options.logic == 'hell': + topright_pillar_area.connect(entrance, AND(FEATHER, SWORD)) # superjump in the center to get on raised blocks, superjump in switch room to right side to walk down. center superjump has to be low so sword added + toprightF1_chest.connect(topright_pillar_area, FEATHER) # superjump from F1 switch room + topleftF2_area = Location(dungeon=7).connect(topright_pillar_area, FEATHER) # superjump in top left pillar room over the blocks from right to left, to reach tile room + topleftF2_area.connect(topleftF1_chest, None, one_way = True) # fall down tile room holes on left side to reach top left chest on ground floor + topleftF1_chest.connect(bottomleftF2_area, AND(PEGASUS_BOOTS, FEATHER), one_way = True) # without hitting the switch, jump on raised blocks at f1 pegs chest (0x209), and boots jump to stairs to reach hinox area + final_pillar_area.connect(bottomleftF2_area, OR(r.attack_hookshot, POWER_BRACELET, AND(FEATHER, SHIELD))) # sideways block push to get to the chest and pillar, kill requirement for 3 of a kind enemies to access chest. Assumes you do not get ball stuck on raised pegs for bracelet path + if options.owlstatues == "both" or options.owlstatues == "dungeon": + bottomleft_owl.connect(bottomleftF2_area, STONE_BEAK7) # sideways block push to get to the owl statue + final_pillar.connect(bottomleftF2_area, BOMB) # bomb trigger pillar + pre_boss.connect(final_pillar, FEATHER) # superjump on top of goomba to extend superjump to boss door plateau + pre_boss.connect(beamos_horseheads_area, None, one_way=True) # can drop down from raised plateau to beamos horseheads area + + if options.logic == 'hell': + topright_pillar_area.connect(entrance, FEATHER) # superjump in the center to get on raised blocks, has to be low + topright_pillar_area.connect(entrance, AND(PEGASUS_BOOTS, OR(BOW, MAGIC_ROD))) # boots superhop in the center to get on raised blocks + toprightF1_chest.connect(topright_pillar_area, AND(PEGASUS_BOOTS, OR(BOW, MAGIC_ROD))) # boots superhop from F1 switch room + pre_boss.connect(final_pillar, AND(PEGASUS_BOOTS, OR(BOW, MAGIC_ROD))) # boots superhop on top of goomba to extend superhop to boss door plateau + + self.entrance = entrance + + +class NoDungeon7: + def __init__(self, options, world_setup, r): + entrance = Location(dungeon=7) + boss = Location(dungeon=7).add(HeartContainer(0x223), Instrument(0x22c)).connect(entrance, r.boss_requirements[ + world_setup.boss_mapping[6]]) + + self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/dungeon8.py b/worlds/ladx/LADXR/logic/dungeon8.py new file mode 100644 index 0000000000..4444ecbb14 --- /dev/null +++ b/worlds/ladx/LADXR/logic/dungeon8.py @@ -0,0 +1,107 @@ +from .requirements import * +from .location import Location +from ..locations.all import * + + +class Dungeon8: + def __init__(self, options, world_setup, r, *, back_entrance_heartpiece=True): + entrance = Location(dungeon=8) + entrance_up = Location(dungeon=8).connect(entrance, FEATHER) + entrance_left = Location(dungeon=8).connect(entrance, r.attack_hookshot_no_bomb) # past hinox + + # left side + entrance_left.add(DungeonChest(0x24D)) # zamboni room chest + Location(dungeon=8).add(DungeonChest(0x25C)).connect(entrance_left, r.attack_hookshot) # eye magnet chest + vire_drop_key = Location(dungeon=8).add(DroppedKey(0x24C)).connect(entrance_left, r.attack_hookshot_no_bomb) # vire drop key + sparks_chest = Location(dungeon=8).add(DungeonChest(0x255)).connect(entrance_left, OR(HOOKSHOT, FEATHER)) # chest before lvl1 miniboss + Location(dungeon=8).add(DungeonChest(0x246)).connect(entrance_left, MAGIC_ROD) # key chest that spawns after creating fire + + # right side + if options.owlstatues == "both" or options.owlstatues == "dungeon": + bottomright_owl = Location(dungeon=8).add(OwlStatue(0x253)).connect(entrance, AND(STONE_BEAK8, FEATHER, POWER_BRACELET)) # Two ways to reach this owl statue, but both require the same (except that one route requires bombs as well) + else: + bottomright_owl = None + slime_chest = Location(dungeon=8).add(DungeonChest(0x259)).connect(entrance, OR(FEATHER, AND(r.attack_hookshot, POWER_BRACELET))) # chest with slime + bottom_right = Location(dungeon=8).add(DroppedKey(0x25A)).connect(entrance, AND(FEATHER, OR(BOMB, AND(r.attack_hookshot_powder, POWER_BRACELET)))) # zamboni key drop; bombs for entrance up through switch room, weapon + bracelet for NW zamboni staircase to bottom right past smasher + bottomright_pot_chest = Location(dungeon=8).add(DungeonChest(0x25F)).connect(bottom_right, POWER_BRACELET) # 4 ropes pot room chest + + map_chest = Location(dungeon=8).add(DungeonChest(0x24F)).connect(entrance_up, None) # use the zamboni to get to the push blocks + lower_center = Location(dungeon=8).connect(entrance_up, KEY8) + upper_center = Location(dungeon=8).connect(lower_center, AND(KEY8, FOUND(KEY8, 2))) + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=8).add(OwlStatue(0x245)).connect(upper_center, STONE_BEAK8) + Location(dungeon=8).add(DroppedKey(0x23E)).connect(upper_center, r.attack_skeleton) # 2 gibdos cracked floor; technically possible to use pits to kill but dumb + medicine_chest = Location(dungeon=8).add(DungeonChest(0x235)).connect(upper_center, AND(FEATHER, HOOKSHOT)) # medicine chest + + middle_center_1 = Location(dungeon=8).connect(upper_center, BOMB) + middle_center_2 = Location(dungeon=8).connect(middle_center_1, AND(KEY8, FOUND(KEY8, 4))) + middle_center_3 = Location(dungeon=8).connect(middle_center_2, KEY8) + miniboss_entrance = Location(dungeon=8).connect(middle_center_3, AND(HOOKSHOT, KEY8, FOUND(KEY8, 7))) # hookshot to get across to keyblock, 7 to fix keylock issues if keys are used on other keyblocks + miniboss = Location(dungeon=8).connect(miniboss_entrance, AND(FEATHER, r.miniboss_requirements[world_setup.miniboss_mapping[7]])) # feather for 2d section, sword to kill + miniboss.add(DungeonChest(0x237)) # fire rod chest + + up_left = Location(dungeon=8).connect(upper_center, AND(r.attack_hookshot_powder, AND(KEY8, FOUND(KEY8, 4)))) + entrance_up.connect(up_left, AND(FEATHER, MAGIC_ROD), one_way=True) # alternate path with fire rod through 2d section to nightmare key + up_left.add(DungeonChest(0x240)) # beamos blocked chest + up_left.connect(entrance_left, None, one_way=True) # path from up_left to entrance_left by dropping of the ledge in torch room + Location(dungeon=8).add(DungeonChest(0x23D)).connect(up_left, BOMB) # dodongo chest + up_left.connect(upper_center, None, one_way=True) # use the outside path of the dungeon to get to the right side + if back_entrance_heartpiece: + Location().add(HeartPiece(0x000)).connect(up_left, None) # Outside the dungeon on the platform + Location(dungeon=8).add(DroppedKey(0x241)).connect(up_left, BOW) # lava statue + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=8).add(OwlStatue(0x241)).connect(up_left, STONE_BEAK8) + Location(dungeon=8).add(DungeonChest(0x23A)).connect(up_left, HOOKSHOT) # ledge chest left of boss door + + top_left_stairs = Location(dungeon=8).connect(entrance_up, AND(FEATHER, MAGIC_ROD)) + top_left_stairs.connect(up_left, None, one_way=True) # jump down from the staircase to the right + nightmare_key = Location(dungeon=8).add(DungeonChest(0x232)).connect(top_left_stairs, AND(FEATHER, SWORD, KEY8, FOUND(KEY8, 7))) + + # Bombing from the center dark rooms to the left so you can access more keys. + # The south walls of center dark room can be bombed from lower_center too with bomb and feather for center dark room access from the south, allowing even more access. Not sure if this should be logic since "obscure" + middle_center_2.connect(up_left, AND(BOMB, FEATHER), one_way=True) # does this even skip a key? both middle_center_2 and up_left come from upper_center with 1 extra key + + bossdoor = Location(dungeon=8).connect(entrance_up, AND(FEATHER, MAGIC_ROD)) + boss = Location(dungeon=8).add(HeartContainer(0x234), Instrument(0x230)).connect(bossdoor, AND(NIGHTMARE_KEY8, r.boss_requirements[world_setup.boss_mapping[7]])) + + if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': + entrance_left.connect(entrance, BOMB) # use bombs to kill vire and hinox + vire_drop_key.connect(entrance_left, BOMB) # use bombs to kill rolling bones and vire + bottom_right.connect(slime_chest, FEATHER) # diagonal jump over the pits to reach rolling rock / zamboni + up_left.connect(lower_center, AND(BOMB, FEATHER)) # blow up hidden walls from peahat room -> dark room -> eye statue room + slime_chest.connect(entrance, AND(r.attack_hookshot_powder, POWER_BRACELET)) # kill vire with powder or bombs + + if options.logic == 'glitched' or options.logic == 'hell': + sparks_chest.connect(entrance_left, OR(r.attack_hookshot, FEATHER, PEGASUS_BOOTS)) # 1 pit buffer across the pit. Add requirements for all the options to get to this area + lower_center.connect(entrance_up, None) # sideways block push in peahat room to get past keyblock + miniboss_entrance.connect(lower_center, AND(BOMB, FEATHER, HOOKSHOT)) # blow up hidden wall for darkroom, use feather + hookshot to clip past keyblock in front of stairs + miniboss_entrance.connect(lower_center, AND(BOMB, FEATHER, FOUND(KEY8, 7))) # same as above, but without clipping past the keyblock + up_left.connect(lower_center, FEATHER) # use jesus jump in refill room left of peahats to clip bottom wall and push bottom block left, to get a place to super jump + up_left.connect(upper_center, FEATHER) # from up left you can jesus jump / lava swim around the key door next to the boss. + top_left_stairs.connect(up_left, AND(FEATHER, SWORD)) # superjump + medicine_chest.connect(upper_center, FEATHER) # jesus super jump + up_left.connect(bossdoor, FEATHER, one_way=True) # superjump off the bottom or right wall to jump over to the boss door + + if options.logic == 'hell': + if bottomright_owl: + bottomright_owl.connect(entrance, AND(SWORD, POWER_BRACELET, PEGASUS_BOOTS, STONE_BEAK8)) # underground section past mimics, boots bonking across the gap to the ladder + bottomright_pot_chest.connect(entrance, AND(SWORD, POWER_BRACELET, PEGASUS_BOOTS)) # underground section past mimics, boots bonking across the gap to the ladder + entrance.connect(bottomright_pot_chest, AND(FEATHER, SWORD), one_way=True) # use NW zamboni staircase backwards, subpixel manip for superjump past the pots + medicine_chest.connect(upper_center, AND(PEGASUS_BOOTS, HOOKSHOT)) # boots bonk + lava buffer to the bottom wall, then bonk onto the middle section + miniboss.connect(miniboss_entrance, AND(PEGASUS_BOOTS, r.miniboss_requirements[world_setup.miniboss_mapping[7]])) # get through 2d section with boots bonks + top_left_stairs.connect(map_chest, AND(PEGASUS_BOOTS, MAGIC_ROD)) # boots bonk + lava buffer from map chest to entrance_up, then boots bonk through 2d section + nightmare_key.connect(top_left_stairs, AND(PEGASUS_BOOTS, SWORD, FOUND(KEY8, 7))) # use a boots bonk to cross the 2d section + the lava in cueball room + bottom_right.connect(entrance_up, AND(POWER_BRACELET, PEGASUS_BOOTS), one_way=True) # take staircase to NW zamboni room, boots bonk onto the lava and water buffer all the way down to push the zamboni + bossdoor.connect(entrance_up, AND(PEGASUS_BOOTS, MAGIC_ROD)) # boots bonk through 2d section + + self.entrance = entrance + + +class NoDungeon8: + def __init__(self, options, world_setup, r): + entrance = Location(dungeon=8) + boss = Location(dungeon=8).add(HeartContainer(0x234)).connect(entrance, r.boss_requirements[ + world_setup.boss_mapping[7]]) + instrument = Location(dungeon=8).add(Instrument(0x230)).connect(boss, FEATHER) # jump over the lava to get to the instrument + + self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/dungeonColor.py b/worlds/ladx/LADXR/logic/dungeonColor.py new file mode 100644 index 0000000000..aa58c0bafa --- /dev/null +++ b/worlds/ladx/LADXR/logic/dungeonColor.py @@ -0,0 +1,49 @@ +from .requirements import * +from .location import Location +from ..locations.all import * + + +class DungeonColor: + def __init__(self, options, world_setup, r): + entrance = Location(dungeon=9) + room2 = Location(dungeon=9).connect(entrance, r.attack_hookshot_powder) + room2.add(DungeonChest(0x314)) # key + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=9).add(OwlStatue(0x308), OwlStatue(0x30F)).connect(room2, STONE_BEAK9) + room2_weapon = Location(dungeon=9).connect(room2, r.attack_hookshot) + room2_weapon.add(DungeonChest(0x311)) # stone beak + room2_lights = Location(dungeon=9).connect(room2, OR(r.attack_hookshot, SHIELD)) + room2_lights.add(DungeonChest(0x30F)) # compass chest + room2_lights.add(DroppedKey(0x308)) + + Location(dungeon=9).connect(room2, AND(KEY9, FOUND(KEY9, 3), r.miniboss_requirements[world_setup.miniboss_mapping["c2"]])).add(DungeonChest(0x302)) # nightmare key after slime mini boss + room3 = Location(dungeon=9).connect(room2, AND(KEY9, FOUND(KEY9, 2), r.miniboss_requirements[world_setup.miniboss_mapping["c1"]])) # After the miniboss + room4 = Location(dungeon=9).connect(room3, POWER_BRACELET) # need to lift a pot to reveal button + room4.add(DungeonChest(0x306)) # map + room4karakoro = Location(dungeon=9).add(DroppedKey(0x307)).connect(room4, r.attack_hookshot) # require item to knock Karakoro enemies into shell + if options.owlstatues == "both" or options.owlstatues == "dungeon": + Location(dungeon=9).add(OwlStatue(0x30A)).connect(room4, STONE_BEAK9) + room5 = Location(dungeon=9).connect(room4, OR(r.attack_hookshot, SHIELD)) # lights room + room6 = Location(dungeon=9).connect(room5, AND(KEY9, FOUND(KEY9, 3))) # room with switch and nightmare door + pre_boss = Location(dungeon=9).connect(room6, OR(r.attack_hookshot, AND(PEGASUS_BOOTS, FEATHER))) # before the boss, require item to hit switch or jump past raised blocks + boss = Location(dungeon=9).connect(pre_boss, AND(NIGHTMARE_KEY9, r.boss_requirements[world_setup.boss_mapping[8]])) + boss.add(TunicFairy(0), TunicFairy(1)) + + if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': + room2.connect(entrance, POWER_BRACELET) # throw pots at enemies + pre_boss.connect(room6, FEATHER) # before the boss, jump past raised blocks without boots + + if options.logic == 'hell': + room2_weapon.connect(room2, SHIELD) # shield bump karakoro into the holes + room4karakoro.connect(room4, SHIELD) # shield bump karakoro into the holes + + self.entrance = entrance + + +class NoDungeonColor: + def __init__(self, options, world_setup, r): + entrance = Location(dungeon=9) + boss = Location(dungeon=9).connect(entrance, r.boss_requirements[world_setup.boss_mapping[8]]) + boss.add(TunicFairy(0), TunicFairy(1)) + + self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/location.py b/worlds/ladx/LADXR/logic/location.py new file mode 100644 index 0000000000..18615a1164 --- /dev/null +++ b/worlds/ladx/LADXR/logic/location.py @@ -0,0 +1,57 @@ +import typing +from .requirements import hasConsumableRequirement, OR +from ..locations.itemInfo import ItemInfo + + +class Location: + def __init__(self, name=None, dungeon=None): + self.name = name + self.items = [] # type: typing.List[ItemInfo] + self.dungeon = dungeon + self.__connected_to = set() + self.simple_connections = [] + self.gated_connections = [] + + def add(self, *item_infos): + for ii in item_infos: + assert isinstance(ii, ItemInfo) + ii.setLocation(self) + self.items.append(ii) + return self + + def connect(self, other, req, *, one_way=False): + assert isinstance(other, Location), type(other) + + if isinstance(req, bool): + if req: + self.connect(other, None, one_way=one_way) + return + + if other in self.__connected_to: + for idx, data in enumerate(self.gated_connections): + if data[0] == other: + if req is None or data[1] is None: + self.gated_connections[idx] = (other, None) + else: + self.gated_connections[idx] = (other, OR(req, data[1])) + break + for idx, data in enumerate(self.simple_connections): + if data[0] == other: + if req is None or data[1] is None: + self.simple_connections[idx] = (other, None) + else: + self.simple_connections[idx] = (other, OR(req, data[1])) + break + else: + self.__connected_to.add(other) + + if hasConsumableRequirement(req): + self.gated_connections.append((other, req)) + else: + self.simple_connections.append((other, req)) + if not one_way: + other.connect(self, req, one_way=True) + return self + + def __repr__(self): + return "<%s:%s:%d:%d:%d>" % (self.__class__.__name__, self.dungeon, len(self.items), len(self.simple_connections), len(self.gated_connections)) diff --git a/worlds/ladx/LADXR/logic/overworld.py b/worlds/ladx/LADXR/logic/overworld.py new file mode 100644 index 0000000000..551cf8353f --- /dev/null +++ b/worlds/ladx/LADXR/logic/overworld.py @@ -0,0 +1,682 @@ +from .requirements import * +from .location import Location +from ..locations.all import * +from ..worldSetup import ENTRANCE_INFO + + +class World: + def __init__(self, options, world_setup, r): + self.overworld_entrance = {} + self.indoor_location = {} + + mabe_village = Location("Mabe Village") + Location().add(HeartPiece(0x2A4)).connect(mabe_village, r.bush) # well + Location().add(FishingMinigame()).connect(mabe_village, AND(r.bush, COUNT("RUPEES", 20))) # fishing game, heart piece is directly done by the minigame. + Location().add(Seashell(0x0A3)).connect(mabe_village, r.bush) # bushes below the shop + Location().add(Seashell(0x0D2)).connect(mabe_village, PEGASUS_BOOTS) # smash into tree next to lv1 + Location().add(Song(0x092)).connect(mabe_village, OCARINA) # Marins song + rooster_cave = Location("Rooster Cave") + Location().add(DroppedKey(0x1E4)).connect(rooster_cave, AND(OCARINA, SONG3)) + + papahl_house = Location("Papahl House") + papahl_house.connect(Location().add(TradeSequenceItem(0x2A6, TRADING_ITEM_RIBBON)), TRADING_ITEM_YOSHI_DOLL) + + trendy_shop = Location("Trendy Shop").add(TradeSequenceItem(0x2A0, TRADING_ITEM_YOSHI_DOLL)) + #trendy_shop.connect(Location()) + + self._addEntrance("papahl_house_left", mabe_village, papahl_house, None) + self._addEntrance("papahl_house_right", mabe_village, papahl_house, None) + self._addEntrance("rooster_grave", mabe_village, rooster_cave, COUNT(POWER_BRACELET, 2)) + self._addEntranceRequirementExit("rooster_grave", None) # if exiting, you do not need l2 bracelet + self._addEntrance("madambowwow", mabe_village, None, None) + self._addEntrance("ulrira", mabe_village, None, None) + self._addEntrance("mabe_phone", mabe_village, None, None) + self._addEntrance("library", mabe_village, None, None) + self._addEntrance("trendy_shop", mabe_village, trendy_shop, r.bush) + self._addEntrance("d1", mabe_village, None, TAIL_KEY) + self._addEntranceRequirementExit("d1", None) # if exiting, you do not need the key + + start_house = Location("Start House").add(StartItem()) + self._addEntrance("start_house", mabe_village, start_house, None) + + shop = Location("Shop") + Location().add(ShopItem(0)).connect(shop, OR(COUNT("RUPEES", 500), SWORD)) + Location().add(ShopItem(1)).connect(shop, OR(COUNT("RUPEES", 1480), SWORD)) + self._addEntrance("shop", mabe_village, shop, None) + + dream_hut = Location("Dream Hut") + dream_hut_right = Location().add(Chest(0x2BF)).connect(dream_hut, SWORD) + if options.logic != "casual": + dream_hut_right.connect(dream_hut, OR(BOOMERANG, HOOKSHOT, FEATHER)) + dream_hut_left = Location().add(Chest(0x2BE)).connect(dream_hut_right, PEGASUS_BOOTS) + self._addEntrance("dream_hut", mabe_village, dream_hut, POWER_BRACELET) + + kennel = Location("Kennel").connect(Location().add(Seashell(0x2B2)), SHOVEL) # in the kennel + kennel.connect(Location().add(TradeSequenceItem(0x2B2, TRADING_ITEM_DOG_FOOD)), TRADING_ITEM_RIBBON) + self._addEntrance("kennel", mabe_village, kennel, None) + + sword_beach = Location("Sword Beach").add(BeachSword()).connect(mabe_village, OR(r.bush, SHIELD, r.attack_hookshot)) + banana_seller = Location("Banana Seller") + banana_seller.connect(Location().add(TradeSequenceItem(0x2FE, TRADING_ITEM_BANANAS)), TRADING_ITEM_DOG_FOOD) + self._addEntrance("banana_seller", sword_beach, banana_seller, r.bush) + boomerang_cave = Location("Boomerang Cave") + if options.boomerang == 'trade': + Location().add(BoomerangGuy()).connect(boomerang_cave, OR(BOOMERANG, HOOKSHOT, MAGIC_ROD, PEGASUS_BOOTS, FEATHER, SHOVEL)) + elif options.boomerang == 'gift': + Location().add(BoomerangGuy()).connect(boomerang_cave, None) + self._addEntrance("boomerang_cave", sword_beach, boomerang_cave, BOMB) + self._addEntranceRequirementExit("boomerang_cave", None) # if exiting, you do not need bombs + + sword_beach_to_ghost_hut = Location("Sword Beach to Ghost House").add(Chest(0x0E5)).connect(sword_beach, POWER_BRACELET) + ghost_hut_outside = Location("Outside Ghost House").connect(sword_beach_to_ghost_hut, POWER_BRACELET) + ghost_hut_inside = Location("Ghost House").connect(Location().add(Seashell(0x1E3)), POWER_BRACELET) + self._addEntrance("ghost_house", ghost_hut_outside, ghost_hut_inside, None) + + ## Forest area + forest = Location("Forest").connect(mabe_village, r.bush) # forest stretches all the way from the start town to the witch hut + Location().add(Chest(0x071)).connect(forest, POWER_BRACELET) # chest at start forest with 2 zols + forest_heartpiece = Location("Forest Heart Piece").add(HeartPiece(0x044)) # next to the forest, surrounded by pits + forest.connect(forest_heartpiece, OR(BOOMERANG, FEATHER, HOOKSHOT, ROOSTER), one_way=True) + + witch_hut = Location().connect(Location().add(Witch()), TOADSTOOL) + self._addEntrance("witch", forest, witch_hut, None) + crazy_tracy_hut = Location("Outside Crazy Tracy's House").connect(forest, POWER_BRACELET) + crazy_tracy_hut_inside = Location("Crazy Tracy's House") + Location().add(KeyLocation("MEDICINE2")).connect(crazy_tracy_hut_inside, FOUND("RUPEES", 50)) + self._addEntrance("crazy_tracy", crazy_tracy_hut, crazy_tracy_hut_inside, None) + start_house.connect(crazy_tracy_hut, SONG2, one_way=True) # Manbo's Mambo into the pond outside Tracy + + forest_madbatter = Location("Forest Mad Batter") + Location().add(MadBatter(0x1E1)).connect(forest_madbatter, MAGIC_POWDER) + self._addEntrance("forest_madbatter", forest, forest_madbatter, POWER_BRACELET) + self._addEntranceRequirementExit("forest_madbatter", None) # if exiting, you do not need bracelet + + forest_cave = Location("Forest Cave") + Location().add(Chest(0x2BD)).connect(forest_cave, SWORD) # chest in forest cave on route to mushroom + log_cave_heartpiece = Location().add(HeartPiece(0x2AB)).connect(forest_cave, POWER_BRACELET) # piece of heart in the forest cave on route to the mushroom + forest_toadstool = Location().add(Toadstool()) + self._addEntrance("toadstool_entrance", forest, forest_cave, None) + self._addEntrance("toadstool_exit", forest_toadstool, forest_cave, None) + + hookshot_cave = Location("Hookshot Cave") + hookshot_cave_chest = Location().add(Chest(0x2B3)).connect(hookshot_cave, OR(HOOKSHOT, ROOSTER)) + self._addEntrance("hookshot_cave", forest, hookshot_cave, POWER_BRACELET) + + swamp = Location("Swamp").connect(forest, AND(OR(MAGIC_POWDER, FEATHER, ROOSTER), r.bush)) + swamp.connect(forest, r.bush, one_way=True) # can go backwards past Tarin + swamp.connect(forest_toadstool, OR(FEATHER, ROOSTER)) + swamp_chest = Location("Swamp Chest").add(Chest(0x034)).connect(swamp, OR(BOWWOW, HOOKSHOT, MAGIC_ROD, BOOMERANG)) + self._addEntrance("d2", swamp, None, OR(BOWWOW, HOOKSHOT, MAGIC_ROD, BOOMERANG)) + forest_rear_chest = Location().add(Chest(0x041)).connect(swamp, r.bush) # tail key + self._addEntrance("writes_phone", swamp, None, None) + + writes_hut_outside = Location("Outside Write's House").connect(swamp, OR(FEATHER, ROOSTER)) # includes the cave behind the hut + writes_house = Location("Write's House") + writes_house.connect(Location().add(TradeSequenceItem(0x2a8, TRADING_ITEM_BROOM)), TRADING_ITEM_LETTER) + self._addEntrance("writes_house", writes_hut_outside, writes_house, None) + if options.owlstatues == "both" or options.owlstatues == "overworld": + writes_hut_outside.add(OwlStatue(0x11)) + writes_cave = Location("Write's Cave") + writes_cave_left_chest = Location().add(Chest(0x2AE)).connect(writes_cave, OR(FEATHER, ROOSTER, HOOKSHOT)) # 1st chest in the cave behind the hut + Location().add(Chest(0x2AF)).connect(writes_cave, POWER_BRACELET) # 2nd chest in the cave behind the hut. + self._addEntrance("writes_cave_left", writes_hut_outside, writes_cave, None) + self._addEntrance("writes_cave_right", writes_hut_outside, writes_cave, None) + + graveyard = Location("Graveyard").connect(forest, OR(FEATHER, ROOSTER, POWER_BRACELET)) # whole area from the graveyard up to the moblin cave + if options.owlstatues == "both" or options.owlstatues == "overworld": + graveyard.add(OwlStatue(0x035)) # Moblin cave owl + self._addEntrance("photo_house", graveyard, None, None) + self._addEntrance("d0", graveyard, None, POWER_BRACELET) + self._addEntranceRequirementExit("d0", None) # if exiting, you do not need bracelet + ghost_grave = Location().connect(forest, POWER_BRACELET) + Location().add(Seashell(0x074)).connect(ghost_grave, AND(r.bush, SHOVEL)) # next to grave cave, digging spot + + graveyard_cave_left = Location() + graveyard_cave_right = Location().connect(graveyard_cave_left, OR(FEATHER, ROOSTER)) + graveyard_heartpiece = Location().add(HeartPiece(0x2DF)).connect(graveyard_cave_right, OR(AND(BOMB, OR(HOOKSHOT, PEGASUS_BOOTS), FEATHER), ROOSTER)) # grave cave + self._addEntrance("graveyard_cave_left", ghost_grave, graveyard_cave_left, POWER_BRACELET) + self._addEntrance("graveyard_cave_right", graveyard, graveyard_cave_right, None) + moblin_cave = Location().connect(Location().add(Chest(0x2E2)), AND(r.attack_hookshot_powder, r.miniboss_requirements[world_setup.miniboss_mapping["moblin_cave"]])) + self._addEntrance("moblin_cave", graveyard, moblin_cave, None) + + # "Ukuku Prairie" + ukuku_prairie = Location().connect(mabe_village, POWER_BRACELET).connect(graveyard, POWER_BRACELET) + ukuku_prairie.connect(Location().add(TradeSequenceItem(0x07B, TRADING_ITEM_STICK)), TRADING_ITEM_BANANAS) + ukuku_prairie.connect(Location().add(TradeSequenceItem(0x087, TRADING_ITEM_HONEYCOMB)), TRADING_ITEM_STICK) + self._addEntrance("prairie_left_phone", ukuku_prairie, None, None) + self._addEntrance("prairie_right_phone", ukuku_prairie, None, None) + self._addEntrance("prairie_left_cave1", ukuku_prairie, Location().add(Chest(0x2CD)), None) # cave next to town + self._addEntrance("prairie_left_fairy", ukuku_prairie, None, BOMB) + self._addEntranceRequirementExit("prairie_left_fairy", None) # if exiting, you do not need bombs + + prairie_left_cave2 = Location() # Bomb cave + Location().add(Chest(0x2F4)).connect(prairie_left_cave2, PEGASUS_BOOTS) + Location().add(HeartPiece(0x2E5)).connect(prairie_left_cave2, AND(BOMB, PEGASUS_BOOTS)) + self._addEntrance("prairie_left_cave2", ukuku_prairie, prairie_left_cave2, BOMB) + self._addEntranceRequirementExit("prairie_left_cave2", None) # if exiting, you do not need bombs + + mamu = Location().connect(Location().add(Song(0x2FB)), AND(OCARINA, COUNT("RUPEES", 1480))) + self._addEntrance("mamu", ukuku_prairie, mamu, AND(OR(AND(FEATHER, PEGASUS_BOOTS), ROOSTER), OR(HOOKSHOT, ROOSTER), POWER_BRACELET)) + + dungeon3_entrance = Location().connect(ukuku_prairie, OR(FEATHER, ROOSTER, FLIPPERS)) + self._addEntrance("d3", dungeon3_entrance, None, SLIME_KEY) + self._addEntranceRequirementExit("d3", None) # if exiting, you do not need to open the door + Location().add(Seashell(0x0A5)).connect(dungeon3_entrance, SHOVEL) # above lv3 + dungeon3_entrance.connect(ukuku_prairie, None, one_way=True) # jump down ledge back to ukuku_prairie + + prairie_island_seashell = Location().add(Seashell(0x0A6)).connect(ukuku_prairie, AND(FLIPPERS, r.bush)) # next to lv3 + Location().add(Seashell(0x08B)).connect(ukuku_prairie, r.bush) # next to seashell house + Location().add(Seashell(0x0A4)).connect(ukuku_prairie, PEGASUS_BOOTS) # smash into tree next to phonehouse + self._addEntrance("castle_jump_cave", ukuku_prairie, Location().add(Chest(0x1FD)), OR(AND(FEATHER, PEGASUS_BOOTS), ROOSTER)) # left of the castle, 5 holes turned into 3 + Location().add(Seashell(0x0B9)).connect(ukuku_prairie, POWER_BRACELET) # under the rock + + left_bay_area = Location() + left_bay_area.connect(ghost_hut_outside, OR(AND(FEATHER, PEGASUS_BOOTS), ROOSTER)) + self._addEntrance("prairie_low_phone", left_bay_area, None, None) + + Location().add(Seashell(0x0E9)).connect(left_bay_area, r.bush) # same screen as mermaid statue + tiny_island = Location().add(Seashell(0x0F8)).connect(left_bay_area, AND(OR(FLIPPERS, ROOSTER), r.bush)) # tiny island + + prairie_plateau = Location() # prairie plateau at the owl statue + if options.owlstatues == "both" or options.owlstatues == "overworld": + prairie_plateau.add(OwlStatue(0x0A8)) + Location().add(Seashell(0x0A8)).connect(prairie_plateau, SHOVEL) # at the owl statue + + prairie_cave = Location() + prairie_cave_secret_exit = Location().connect(prairie_cave, AND(BOMB, OR(FEATHER, ROOSTER))) + self._addEntrance("prairie_right_cave_top", ukuku_prairie, prairie_cave, None) + self._addEntrance("prairie_right_cave_bottom", left_bay_area, prairie_cave, None) + self._addEntrance("prairie_right_cave_high", prairie_plateau, prairie_cave_secret_exit, None) + + bay_madbatter_connector_entrance = Location() + bay_madbatter_connector_exit = Location().connect(bay_madbatter_connector_entrance, FLIPPERS) + bay_madbatter_connector_outside = Location() + bay_madbatter = Location().connect(Location().add(MadBatter(0x1E0)), MAGIC_POWDER) + self._addEntrance("prairie_madbatter_connector_entrance", left_bay_area, bay_madbatter_connector_entrance, AND(OR(FEATHER, ROOSTER), OR(SWORD, MAGIC_ROD, BOOMERANG))) + self._addEntranceRequirementExit("prairie_madbatter_connector_entrance", AND(OR(FEATHER, ROOSTER), r.bush)) # if exiting, you can pick up the bushes by normal means + self._addEntrance("prairie_madbatter_connector_exit", bay_madbatter_connector_outside, bay_madbatter_connector_exit, None) + self._addEntrance("prairie_madbatter", bay_madbatter_connector_outside, bay_madbatter, None) + + seashell_mansion = Location() + if options.goal != "seashells": + Location().add(SeashellMansion(0x2E9)).connect(seashell_mansion, COUNT(SEASHELL, 20)) + else: + seashell_mansion.add(DroppedKey(0x2E9)) + self._addEntrance("seashell_mansion", ukuku_prairie, seashell_mansion, None) + + bay_water = Location() + bay_water.connect(ukuku_prairie, FLIPPERS) + bay_water.connect(left_bay_area, FLIPPERS) + fisher_under_bridge = Location().add(TradeSequenceItem(0x2F5, TRADING_ITEM_NECKLACE)) + fisher_under_bridge.connect(bay_water, AND(TRADING_ITEM_FISHING_HOOK, FEATHER, FLIPPERS)) + bay_water.connect(Location().add(TradeSequenceItem(0x0C9, TRADING_ITEM_SCALE)), AND(TRADING_ITEM_NECKLACE, FLIPPERS)) + d5_entrance = Location().connect(bay_water, FLIPPERS) + self._addEntrance("d5", d5_entrance, None, None) + + # Richard + richard_house = Location() + richard_cave = Location().connect(richard_house, COUNT(GOLD_LEAF, 5)) + richard_cave.connect(richard_house, None, one_way=True) # can exit richard's cave even without leaves + richard_cave_chest = Location().add(Chest(0x2C8)).connect(richard_cave, OR(FEATHER, HOOKSHOT, ROOSTER)) + richard_maze = Location() + self._addEntrance("richard_house", ukuku_prairie, richard_house, None) + self._addEntrance("richard_maze", richard_maze, richard_cave, None) + if options.owlstatues == "both" or options.owlstatues == "overworld": + Location().add(OwlStatue(0x0C6)).connect(richard_maze, r.bush) + Location().add(SlimeKey()).connect(richard_maze, AND(r.bush, SHOVEL)) + + next_to_castle = Location() + if options.tradequest: + ukuku_prairie.connect(next_to_castle, TRADING_ITEM_BANANAS, one_way=True) # can only give bananas from ukuku prairie side + else: + next_to_castle.connect(ukuku_prairie, None) + next_to_castle.connect(ukuku_prairie, FLIPPERS) + self._addEntrance("castle_phone", next_to_castle, None, None) + castle_secret_entrance_left = Location() + castle_secret_entrance_right = Location().connect(castle_secret_entrance_left, FEATHER) + castle_courtyard = Location() + castle_frontdoor = Location().connect(castle_courtyard, r.bush) + castle_frontdoor.connect(ukuku_prairie, "CASTLE_BUTTON") # the button in the castle connector allows access to the castle grounds in ER + self._addEntrance("castle_secret_entrance", next_to_castle, castle_secret_entrance_right, OR(BOMB, BOOMERANG, MAGIC_POWDER, MAGIC_ROD, SWORD)) + self._addEntrance("castle_secret_exit", castle_courtyard, castle_secret_entrance_left, None) + + Location().add(HeartPiece(0x078)).connect(bay_water, FLIPPERS) # in the moat of the castle + castle_inside = Location() + Location().add(KeyLocation("CASTLE_BUTTON")).connect(castle_inside, None) + castle_top_outside = Location() + castle_top_inside = Location() + self._addEntrance("castle_main_entrance", castle_frontdoor, castle_inside, r.bush) + self._addEntrance("castle_upper_left", castle_top_outside, castle_inside, None) + self._addEntrance("castle_upper_right", castle_top_outside, castle_top_inside, None) + Location().add(GoldLeaf(0x05A)).connect(castle_courtyard, OR(SWORD, BOW, MAGIC_ROD)) # mad bomber, enemy hiding in the 6 holes + crow_gold_leaf = Location().add(GoldLeaf(0x058)).connect(castle_courtyard, AND(POWER_BRACELET, r.attack_hookshot_no_bomb)) # bird on tree, can't kill with bomb cause it flies off. immune to magic_powder + Location().add(GoldLeaf(0x2D2)).connect(castle_inside, r.attack_hookshot_powder) # in the castle, kill enemies + Location().add(GoldLeaf(0x2C5)).connect(castle_inside, AND(BOMB, r.attack_hookshot_powder)) # in the castle, bomb wall to show enemy + kanalet_chain_trooper = Location().add(GoldLeaf(0x2C6)) # in the castle, spinning spikeball enemy + castle_top_inside.connect(kanalet_chain_trooper, AND(POWER_BRACELET, r.attack_hookshot), one_way=True) + + animal_village = Location() + animal_village.connect(Location().add(TradeSequenceItem(0x0CD, TRADING_ITEM_FISHING_HOOK)), TRADING_ITEM_BROOM) + cookhouse = Location() + cookhouse.connect(Location().add(TradeSequenceItem(0x2D7, TRADING_ITEM_PINEAPPLE)), TRADING_ITEM_HONEYCOMB) + goathouse = Location() + goathouse.connect(Location().add(TradeSequenceItem(0x2D9, TRADING_ITEM_LETTER)), TRADING_ITEM_HIBISCUS) + mermaid_statue = Location() + mermaid_statue.connect(animal_village, AND(TRADING_ITEM_SCALE, HOOKSHOT)) + mermaid_statue.add(TradeSequenceItem(0x297, TRADING_ITEM_MAGNIFYING_GLASS)) + self._addEntrance("animal_phone", animal_village, None, None) + self._addEntrance("animal_house1", animal_village, None, None) + self._addEntrance("animal_house2", animal_village, None, None) + self._addEntrance("animal_house3", animal_village, goathouse, None) + self._addEntrance("animal_house4", animal_village, None, None) + self._addEntrance("animal_house5", animal_village, cookhouse, None) + animal_village.connect(bay_water, FLIPPERS) + animal_village.connect(ukuku_prairie, OR(HOOKSHOT, ROOSTER)) + animal_village_connector_left = Location() + animal_village_connector_right = Location().connect(animal_village_connector_left, PEGASUS_BOOTS) + self._addEntrance("prairie_to_animal_connector", ukuku_prairie, animal_village_connector_left, OR(BOMB, BOOMERANG, MAGIC_POWDER, MAGIC_ROD, SWORD)) # passage under river blocked by bush + self._addEntrance("animal_to_prairie_connector", animal_village, animal_village_connector_right, None) + if options.owlstatues == "both" or options.owlstatues == "overworld": + animal_village.add(OwlStatue(0x0DA)) + Location().add(Seashell(0x0DA)).connect(animal_village, SHOVEL) # owl statue at the water + desert = Location().connect(animal_village, r.bush) # Note: We moved the walrus blocking the desert. + if options.owlstatues == "both" or options.owlstatues == "overworld": + desert.add(OwlStatue(0x0CF)) + desert_lanmola = Location().add(AnglerKey()).connect(desert, OR(BOW, SWORD, HOOKSHOT, MAGIC_ROD, BOOMERANG)) + + animal_village_bombcave = Location() + self._addEntrance("animal_cave", desert, animal_village_bombcave, BOMB) + self._addEntranceRequirementExit("animal_cave", None) # if exiting, you do not need bombs + animal_village_bombcave_heartpiece = Location().add(HeartPiece(0x2E6)).connect(animal_village_bombcave, OR(AND(BOMB, FEATHER, HOOKSHOT), ROOSTER)) # cave in the upper right of animal town + + desert_cave = Location() + self._addEntrance("desert_cave", desert, desert_cave, None) + desert.connect(desert_cave, None, one_way=True) # Drop down the sinkhole + + Location().add(HeartPiece(0x1E8)).connect(desert_cave, BOMB) # above the quicksand cave + Location().add(Seashell(0x0FF)).connect(desert, POWER_BRACELET) # bottom right corner of the map + + armos_maze = Location().connect(animal_village, POWER_BRACELET) + armos_temple = Location() + Location().add(FaceKey()).connect(armos_temple, r.miniboss_requirements[world_setup.miniboss_mapping["armos_temple"]]) + if options.owlstatues == "both" or options.owlstatues == "overworld": + armos_maze.add(OwlStatue(0x08F)) + self._addEntrance("armos_maze_cave", armos_maze, Location().add(Chest(0x2FC)), None) + self._addEntrance("armos_temple", armos_maze, armos_temple, None) + + armos_fairy_entrance = Location().connect(bay_water, FLIPPERS).connect(animal_village, POWER_BRACELET) + self._addEntrance("armos_fairy", armos_fairy_entrance, None, BOMB) + self._addEntranceRequirementExit("armos_fairy", None) # if exiting, you do not need bombs + + d6_connector_left = Location() + d6_connector_right = Location().connect(d6_connector_left, OR(AND(HOOKSHOT, OR(FLIPPERS, AND(FEATHER, PEGASUS_BOOTS))), ROOSTER)) + d6_entrance = Location() + d6_entrance.connect(bay_water, FLIPPERS, one_way=True) + d6_armos_island = Location().connect(bay_water, FLIPPERS) + self._addEntrance("d6_connector_entrance", d6_armos_island, d6_connector_right, None) + self._addEntrance("d6_connector_exit", d6_entrance, d6_connector_left, None) + self._addEntrance("d6", d6_entrance, None, FACE_KEY) + self._addEntranceRequirementExit("d6", None) # if exiting, you do not need to open the dungeon + + windfish_egg = Location().connect(swamp, POWER_BRACELET).connect(graveyard, POWER_BRACELET) + windfish_egg.connect(graveyard, None, one_way=True) # Ledge jump + + obstacle_cave_entrance = Location() + obstacle_cave_inside = Location().connect(obstacle_cave_entrance, SWORD) + obstacle_cave_inside.connect(obstacle_cave_entrance, FEATHER, one_way=True) # can get past the rock room from right to left pushing blocks and jumping over the pit + obstacle_cave_inside_chest = Location().add(Chest(0x2BB)).connect(obstacle_cave_inside, OR(HOOKSHOT, ROOSTER)) # chest at obstacles + obstacle_cave_exit = Location().connect(obstacle_cave_inside, OR(PEGASUS_BOOTS, ROOSTER)) + + lower_right_taltal = Location() + self._addEntrance("obstacle_cave_entrance", windfish_egg, obstacle_cave_entrance, POWER_BRACELET) + self._addEntrance("obstacle_cave_outside_chest", Location().add(Chest(0x018)), obstacle_cave_inside, None) + self._addEntrance("obstacle_cave_exit", lower_right_taltal, obstacle_cave_exit, None) + + papahl_cave = Location().add(Chest(0x28A)) + papahl = Location().connect(lower_right_taltal, None, one_way=True) + hibiscus_item = Location().add(TradeSequenceItem(0x019, TRADING_ITEM_HIBISCUS)) + papahl.connect(hibiscus_item, TRADING_ITEM_PINEAPPLE, one_way=True) + self._addEntrance("papahl_entrance", lower_right_taltal, papahl_cave, None) + self._addEntrance("papahl_exit", papahl, papahl_cave, None) + + # D4 entrance and related things + below_right_taltal = Location().connect(windfish_egg, POWER_BRACELET) + below_right_taltal.add(KeyLocation("ANGLER_KEYHOLE")) + below_right_taltal.connect(bay_water, FLIPPERS) + below_right_taltal.connect(next_to_castle, ROOSTER) # fly from staircase to staircase on the north side of the moat + lower_right_taltal.connect(below_right_taltal, FLIPPERS, one_way=True) + + heartpiece_swim_cave = Location().connect(Location().add(HeartPiece(0x1F2)), FLIPPERS) + self._addEntrance("heartpiece_swim_cave", below_right_taltal, heartpiece_swim_cave, FLIPPERS) # cave next to level 4 + d4_entrance = Location().connect(below_right_taltal, FLIPPERS) + lower_right_taltal.connect(d4_entrance, AND(ANGLER_KEY, "ANGLER_KEYHOLE"), one_way=True) + self._addEntrance("d4", d4_entrance, None, ANGLER_KEY) + self._addEntranceRequirementExit("d4", FLIPPERS) # if exiting, you can leave with flippers without opening the dungeon + mambo = Location().connect(Location().add(Song(0x2FD)), AND(OCARINA, FLIPPERS)) # Manbo's Mambo + self._addEntrance("mambo", d4_entrance, mambo, FLIPPERS) + + # Raft game. + raft_house = Location("Raft House") + Location().add(KeyLocation("RAFT")).connect(raft_house, COUNT("RUPEES", 100)) + raft_return_upper = Location() + raft_return_lower = Location().connect(raft_return_upper, None, one_way=True) + outside_raft_house = Location().connect(below_right_taltal, HOOKSHOT).connect(below_right_taltal, FLIPPERS, one_way=True) + raft_game = Location() + raft_game.connect(outside_raft_house, "RAFT") + raft_game.add(Chest(0x05C), Chest(0x05D)) # Chests in the rafting game + raft_exit = Location() + if options.logic != "casual": # use raft to reach north armos maze entrances without flippers + raft_game.connect(raft_exit, None, one_way=True) + raft_game.connect(armos_fairy_entrance, None, one_way=True) + self._addEntrance("raft_return_exit", outside_raft_house, raft_return_upper, None) + self._addEntrance("raft_return_enter", raft_exit, raft_return_lower, None) + raft_exit.connect(armos_fairy_entrance, FLIPPERS) + self._addEntrance("raft_house", outside_raft_house, raft_house, None) + if options.owlstatues == "both" or options.owlstatues == "overworld": + raft_game.add(OwlStatue(0x5D)) + + outside_rooster_house = Location().connect(lower_right_taltal, OR(FLIPPERS, ROOSTER)) + self._addEntrance("rooster_house", outside_rooster_house, None, None) + bird_cave = Location() + bird_key = Location().add(BirdKey()) + bird_cave.connect(bird_key, OR(AND(FEATHER, COUNT(POWER_BRACELET, 2)), ROOSTER)) + if options.logic != "casual": + bird_cave.connect(lower_right_taltal, None, one_way=True) # Drop in a hole at bird cave + self._addEntrance("bird_cave", outside_rooster_house, bird_cave, None) + bridge_seashell = Location().add(Seashell(0x00C)).connect(outside_rooster_house, AND(OR(FEATHER, ROOSTER), POWER_BRACELET)) # seashell right of rooster house, there is a hole in the bridge + + multichest_cave = Location() + multichest_cave_secret = Location().connect(multichest_cave, BOMB) + water_cave_hole = Location() # Location with the hole that drops you onto the hearth piece under water + if options.logic != "casual": + water_cave_hole.connect(heartpiece_swim_cave, FLIPPERS, one_way=True) + multichest_outside = Location().add(Chest(0x01D)) # chest after multichest puzzle outside + self._addEntrance("multichest_left", lower_right_taltal, multichest_cave, OR(FLIPPERS, ROOSTER)) + self._addEntrance("multichest_right", water_cave_hole, multichest_cave, None) + self._addEntrance("multichest_top", multichest_outside, multichest_cave_secret, None) + if options.owlstatues == "both" or options.owlstatues == "overworld": + water_cave_hole.add(OwlStatue(0x1E)) # owl statue below d7 + + right_taltal_connector1 = Location() + right_taltal_connector_outside1 = Location() + right_taltal_connector2 = Location() + right_taltal_connector3 = Location() + right_taltal_connector2.connect(right_taltal_connector3, AND(OR(FEATHER, ROOSTER), HOOKSHOT), one_way=True) + right_taltal_connector_outside2 = Location() + right_taltal_connector4 = Location() + d7_platau = Location() + d7_tower = Location() + d7_platau.connect(d7_tower, AND(POWER_BRACELET, BIRD_KEY), one_way=True) + self._addEntrance("right_taltal_connector1", water_cave_hole, right_taltal_connector1, None) + self._addEntrance("right_taltal_connector2", right_taltal_connector_outside1, right_taltal_connector1, None) + self._addEntrance("right_taltal_connector3", right_taltal_connector_outside1, right_taltal_connector2, None) + self._addEntrance("right_taltal_connector4", right_taltal_connector_outside2, right_taltal_connector3, None) + self._addEntrance("right_taltal_connector5", right_taltal_connector_outside2, right_taltal_connector4, None) + self._addEntrance("right_taltal_connector6", d7_platau, right_taltal_connector4, None) + self._addEntrance("right_fairy", right_taltal_connector_outside2, None, BOMB) + self._addEntranceRequirementExit("right_fairy", None) # if exiting, you do not need bombs + self._addEntrance("d7", d7_tower, None, None) + if options.logic != "casual": # D7 area ledge drops + d7_platau.connect(heartpiece_swim_cave, FLIPPERS, one_way=True) + d7_platau.connect(right_taltal_connector_outside1, None, one_way=True) + + mountain_bridge_staircase = Location().connect(outside_rooster_house, OR(HOOKSHOT, ROOSTER)) # cross bridges to staircase + if options.logic != "casual": # ledge drop + mountain_bridge_staircase.connect(windfish_egg, None, one_way=True) + + left_right_connector_cave_entrance = Location() + left_right_connector_cave_exit = Location() + left_right_connector_cave_entrance.connect(left_right_connector_cave_exit, OR(HOOKSHOT, ROOSTER), one_way=True) # pass through the underground passage to left side + taltal_boulder_zone = Location() + self._addEntrance("left_to_right_taltalentrance", mountain_bridge_staircase, left_right_connector_cave_entrance, OR(BOMB, BOOMERANG, MAGIC_POWDER, MAGIC_ROD, SWORD)) + self._addEntrance("left_taltal_entrance", taltal_boulder_zone, left_right_connector_cave_exit, None) + mountain_heartpiece = Location().add(HeartPiece(0x2BA)) # heartpiece in connecting cave + left_right_connector_cave_entrance.connect(mountain_heartpiece, BOMB, one_way=True) # in the connecting cave from right to left. one_way to prevent access to left_side_mountain via glitched logic + + taltal_boulder_zone.add(Chest(0x004)) # top of falling rocks hill + taltal_madbatter = Location().connect(Location().add(MadBatter(0x1E2)), MAGIC_POWDER) + self._addEntrance("madbatter_taltal", taltal_boulder_zone, taltal_madbatter, POWER_BRACELET) + self._addEntranceRequirementExit("madbatter_taltal", None) # if exiting, you do not need bracelet + + outside_fire_cave = Location() + if options.logic != "casual": + outside_fire_cave.connect(writes_hut_outside, None, one_way=True) # Jump down the ledge + taltal_boulder_zone.connect(outside_fire_cave, None, one_way=True) + fire_cave_bottom = Location() + fire_cave_top = Location().connect(fire_cave_bottom, COUNT(SHIELD, 2)) + self._addEntrance("fire_cave_entrance", outside_fire_cave, fire_cave_bottom, BOMB) + self._addEntranceRequirementExit("fire_cave_entrance", None) # if exiting, you do not need bombs + + d8_entrance = Location() + if options.logic != "casual": + d8_entrance.connect(writes_hut_outside, None, one_way=True) # Jump down the ledge + d8_entrance.connect(outside_fire_cave, None, one_way=True) # Jump down the other ledge + self._addEntrance("fire_cave_exit", d8_entrance, fire_cave_top, None) + self._addEntrance("phone_d8", d8_entrance, None, None) + self._addEntrance("d8", d8_entrance, None, AND(OCARINA, SONG3, SWORD)) + self._addEntranceRequirementExit("d8", None) # if exiting, you do not need to wake the turtle + + nightmare = Location("Nightmare") + windfish = Location("Windfish").connect(nightmare, AND(MAGIC_POWDER, SWORD, OR(BOOMERANG, BOW))) + + if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': + hookshot_cave.connect(hookshot_cave_chest, AND(FEATHER, PEGASUS_BOOTS)) # boots jump the gap to the chest + graveyard_cave_left.connect(graveyard_cave_right, HOOKSHOT, one_way=True) # hookshot the block behind the stairs while over the pit + swamp_chest.connect(swamp, None) # Clip past the flower + self._addEntranceRequirement("d2", POWER_BRACELET) # clip the top wall to walk between the goponga flower and the wall + self._addEntranceRequirement("d2", COUNT(SWORD, 2)) # use l2 sword spin to kill goponga flowers + swamp.connect(writes_hut_outside, HOOKSHOT, one_way=True) # hookshot the sign in front of writes hut + graveyard_heartpiece.connect(graveyard_cave_right, FEATHER) # jump to the bottom right tile around the blocks + graveyard_heartpiece.connect(graveyard_cave_right, OR(HOOKSHOT, BOOMERANG)) # push bottom block, wall clip and hookshot/boomerang corner to grab item + + self._addEntranceRequirement("mamu", AND(FEATHER, POWER_BRACELET)) # can clear the gaps at the start with just feather, can reach bottom left sign with a well timed jump while wall clipped + self._addEntranceRequirement("prairie_madbatter_connector_entrance", AND(OR(FEATHER, ROOSTER), OR(MAGIC_POWDER, BOMB))) # use bombs or powder to get rid of a bush on the other side by jumping across and placing the bomb/powder before you fall into the pit + fisher_under_bridge.connect(bay_water, AND(TRADING_ITEM_FISHING_HOOK, FLIPPERS)) # can talk to the fisherman from the water when the boat is low (requires swimming up out of the water a bit) + crow_gold_leaf.connect(castle_courtyard, POWER_BRACELET) # bird on tree at left side kanalet, can use both rocks to kill the crow removing the kill requirement + castle_inside.connect(kanalet_chain_trooper, BOOMERANG, one_way=True) # kill the ball and chain trooper from the left side, then use boomerang to grab the dropped item + animal_village_bombcave_heartpiece.connect(animal_village_bombcave, AND(PEGASUS_BOOTS, FEATHER)) # jump across horizontal 4 gap to heart piece + desert_lanmola.connect(desert, BOMB) # use bombs to kill lanmola + + d6_connector_left.connect(d6_connector_right, AND(OR(FLIPPERS, PEGASUS_BOOTS), FEATHER)) # jump the gap in underground passage to d6 left side to skip hookshot + bird_key.connect(bird_cave, COUNT(POWER_BRACELET, 2)) # corner walk past the one pit on the left side to get to the elephant statue + fire_cave_bottom.connect(fire_cave_top, PEGASUS_BOOTS, one_way=True) # flame skip + + if options.logic == 'glitched' or options.logic == 'hell': + #self._addEntranceRequirement("dream_hut", FEATHER) # text clip TODO: require nag messages + self._addEntranceRequirementEnter("dream_hut", HOOKSHOT) # clip past the rocks in front of dream hut + dream_hut_right.connect(dream_hut_left, FEATHER) # super jump + forest.connect(swamp, BOMB) # bomb trigger tarin + forest.connect(forest_heartpiece, BOMB, one_way=True) # bomb trigger heartpiece + self._addEntranceRequirementEnter("hookshot_cave", HOOKSHOT) # clip past the rocks in front of hookshot cave + swamp.connect(forest_toadstool, None, one_way=True) # villa buffer from top (swamp phonebooth area) to bottom (toadstool area) + writes_hut_outside.connect(swamp, None, one_way=True) # villa buffer from top (writes hut) to bottom (swamp phonebooth area) or damage boost + graveyard.connect(forest_heartpiece, None, one_way=True) # villa buffer from top. + log_cave_heartpiece.connect(forest_cave, FEATHER) # super jump + log_cave_heartpiece.connect(forest_cave, BOMB) # bomb trigger + graveyard_cave_left.connect(graveyard_heartpiece, BOMB, one_way=True) # bomb trigger the heartpiece from the left side + graveyard_heartpiece.connect(graveyard_cave_right, None) # sideways block push from the right staircase. + + prairie_island_seashell.connect(ukuku_prairie, AND(FEATHER, r.bush)) # jesus jump from right side, screen transition on top of the water to reach the island + self._addEntranceRequirement("castle_jump_cave", FEATHER) # 1 pit buffer to clip bottom wall and jump across. + left_bay_area.connect(ghost_hut_outside, FEATHER) # 1 pit buffer to get across + tiny_island.connect(left_bay_area, AND(FEATHER, r.bush)) # jesus jump around + bay_madbatter_connector_exit.connect(bay_madbatter_connector_entrance, FEATHER, one_way=True) # jesus jump (3 screen) through the underground passage leading to martha's bay mad batter + self._addEntranceRequirement("prairie_madbatter_connector_entrance", AND(FEATHER, POWER_BRACELET)) # villa buffer into the top side of the bush, then pick it up + + ukuku_prairie.connect(richard_maze, OR(BOMB, BOOMERANG, MAGIC_POWDER, MAGIC_ROD, SWORD), one_way=True) # break bushes on north side of the maze, and 1 pit buffer into the maze + fisher_under_bridge.connect(bay_water, AND(BOMB, FLIPPERS)) # can bomb trigger the item without having the hook + animal_village.connect(ukuku_prairie, FEATHER) # jesus jump + below_right_taltal.connect(next_to_castle, FEATHER) # jesus jump (north of kanalet castle phonebooth) + animal_village_connector_right.connect(animal_village_connector_left, FEATHER) # text clip past the obstacles (can go both ways), feather to wall clip the obstacle without triggering text or shaq jump in bottom right corner if text is off + animal_village_bombcave_heartpiece.connect(animal_village_bombcave, AND(BOMB, OR(HOOKSHOT, FEATHER, PEGASUS_BOOTS))) # bomb trigger from right side, corner walking top right pit is stupid so hookshot or boots added + animal_village_bombcave_heartpiece.connect(animal_village_bombcave, FEATHER) # villa buffer across the pits + + d6_entrance.connect(ukuku_prairie, FEATHER, one_way=True) # jesus jump (2 screen) from d6 entrance bottom ledge to ukuku prairie + d6_entrance.connect(armos_fairy_entrance, FEATHER, one_way=True) # jesus jump (2 screen) from d6 entrance top ledge to armos fairy entrance + armos_fairy_entrance.connect(d6_armos_island, FEATHER, one_way=True) # jesus jump from top (fairy bomb cave) to armos island + armos_fairy_entrance.connect(raft_exit, FEATHER) # jesus jump (2-ish screen) from fairy cave to lower raft connector + self._addEntranceRequirementEnter("obstacle_cave_entrance", HOOKSHOT) # clip past the rocks in front of obstacle cave entrance + obstacle_cave_inside_chest.connect(obstacle_cave_inside, FEATHER) # jump to the rightmost pits + 1 pit buffer to jump across + obstacle_cave_exit.connect(obstacle_cave_inside, FEATHER) # 1 pit buffer above boots crystals to get past + lower_right_taltal.connect(hibiscus_item, AND(TRADING_ITEM_PINEAPPLE, BOMB), one_way=True) # bomb trigger papahl from below ledge, requires pineapple + + self._addEntranceRequirement("heartpiece_swim_cave", FEATHER) # jesus jump into the cave entrance after jumping down the ledge, can jesus jump back to the ladder 1 screen below + self._addEntranceRequirement("mambo", FEATHER) # jesus jump from (unlocked) d4 entrance to mambo's cave entrance + outside_raft_house.connect(below_right_taltal, FEATHER, one_way=True) # jesus jump from the ledge at raft to the staircase 1 screen south + + self._addEntranceRequirement("multichest_left", FEATHER) # jesus jump past staircase leading up the mountain + outside_rooster_house.connect(lower_right_taltal, FEATHER) # jesus jump (1 or 2 screen depending if angler key is used) to staircase leading up the mountain + d7_platau.connect(water_cave_hole, None, one_way=True) # use save and quit menu to gain control while falling to dodge the water cave hole + mountain_bridge_staircase.connect(outside_rooster_house, AND(PEGASUS_BOOTS, FEATHER)) # cross bridge to staircase with pit buffer to clip bottom wall and jump across + bird_key.connect(bird_cave, AND(FEATHER, HOOKSHOT)) # hookshot jump across the big pits room + right_taltal_connector2.connect(right_taltal_connector3, None, one_way=True) # 2 seperate pit buffers so not obnoxious to get past the two pit rooms before d7 area. 2nd pits can pit buffer on top right screen, bottom wall to scroll on top of the wall on bottom screen + left_right_connector_cave_exit.connect(left_right_connector_cave_entrance, AND(HOOKSHOT, FEATHER), one_way=True) # pass through the passage in reverse using a superjump to get out of the dead end + obstacle_cave_inside.connect(mountain_heartpiece, BOMB, one_way=True) # bomb trigger from boots crystal cave + self._addEntranceRequirement("d8", OR(BOMB, AND(OCARINA, SONG3))) # bomb trigger the head and walk trough, or play the ocarina song 3 and walk through + + if options.logic == 'hell': + dream_hut_right.connect(dream_hut, None) # alternate diagonal movement with orthogonal movement to control the mimics. Get them clipped into the walls to walk past + swamp.connect(forest_toadstool, None) # damage boost from toadstool area across the pit + swamp.connect(forest, AND(r.bush, OR(PEGASUS_BOOTS, HOOKSHOT))) # boots bonk / hookshot spam over the pits right of forest_rear_chest + forest.connect(forest_heartpiece, PEGASUS_BOOTS, one_way=True) # boots bonk across the pits + log_cave_heartpiece.connect(forest_cave, BOOMERANG) # clip the boomerang through the corner gaps on top right to grab the item + log_cave_heartpiece.connect(forest_cave, AND(ROOSTER, OR(PEGASUS_BOOTS, SWORD, BOW, MAGIC_ROD))) # boots rooster hop in bottom left corner to "superjump" into the area. use buffers after picking up rooster to gain height / time to throw rooster again facing up + writes_hut_outside.connect(swamp, None) # damage boost with moblin arrow next to telephone booth + writes_cave_left_chest.connect(writes_cave, None) # damage boost off the zol to get across the pit. + graveyard.connect(crazy_tracy_hut, HOOKSHOT, one_way=True) # use hookshot spam to clip the rock on the right with the crow + graveyard.connect(forest, OR(PEGASUS_BOOTS, HOOKSHOT)) # boots bonk witches hut, or hookshot spam across the pit + graveyard_cave_left.connect(graveyard_cave_right, HOOKSHOT) # hookshot spam over the pit + graveyard_cave_right.connect(graveyard_cave_left, PEGASUS_BOOTS, one_way=True) # boots bonk off the cracked block + + self._addEntranceRequirementEnter("mamu", AND(PEGASUS_BOOTS, POWER_BRACELET)) # can clear the gaps at the start with multiple pit buffers, can reach bottom left sign with bonking along the bottom wall + self._addEntranceRequirement("castle_jump_cave", PEGASUS_BOOTS) # pit buffer to clip bottom wall and boots bonk across + prairie_cave_secret_exit.connect(prairie_cave, AND(BOMB, OR(PEGASUS_BOOTS, HOOKSHOT))) # hookshot spam or boots bonk across pits can go from left to right by pit buffering on top of the bottom wall then boots bonk across + richard_cave_chest.connect(richard_cave, None) # use the zol on the other side of the pit to damage boost across (requires damage from pit + zol) + castle_secret_entrance_right.connect(castle_secret_entrance_left, AND(PEGASUS_BOOTS, "MEDICINE2")) # medicine iframe abuse to get across spikes with a boots bonk + left_bay_area.connect(ghost_hut_outside, PEGASUS_BOOTS) # multiple pit buffers to bonk across the bottom wall + tiny_island.connect(left_bay_area, AND(PEGASUS_BOOTS, r.bush)) # jesus jump around with boots bonks, then one final bonk off the bottom wall to get on the staircase (needs to be centered correctly) + self._addEntranceRequirement("prairie_madbatter_connector_entrance", AND(PEGASUS_BOOTS, OR(MAGIC_POWDER, BOMB, SWORD, MAGIC_ROD, BOOMERANG))) # Boots bonk across the bottom wall, then remove one of the bushes to get on land + self._addEntranceRequirementExit("prairie_madbatter_connector_entrance", AND(PEGASUS_BOOTS, r.bush)) # if exiting, you can pick up the bushes by normal means and boots bonk across the bottom wall + + # bay_water connectors, only left_bay_area, ukuku_prairie and animal_village have to be connected with jesus jumps. below_right_taltal, d6_armos_island and armos_fairy_entrance are accounted for via ukuku prairie in glitch logic + left_bay_area.connect(bay_water, FEATHER) # jesus jump (can always reach bay_water with jesus jumping from every way to enter bay_water, so no one_way) + animal_village.connect(bay_water, FEATHER) # jesus jump (can always reach bay_water with jesus jumping from every way to enter bay_water, so no one_way) + ukuku_prairie.connect(bay_water, FEATHER, one_way=True) # jesus jump + bay_water.connect(d5_entrance, FEATHER) # jesus jump into d5 entrance (wall clip), wall clip + jesus jump to get out + + crow_gold_leaf.connect(castle_courtyard, BOMB) # bird on tree at left side kanalet, place a bomb against the tree and the crow flies off. With well placed second bomb the crow can be killed + mermaid_statue.connect(animal_village, AND(TRADING_ITEM_SCALE, FEATHER)) # early mermaid statue by buffering on top of the right ledge, then superjumping to the left (horizontal pixel perfect) + animal_village_bombcave_heartpiece.connect(animal_village_bombcave, PEGASUS_BOOTS) # boots bonk across bottom wall (both at entrance and in item room) + + d6_armos_island.connect(ukuku_prairie, FEATHER) # jesus jump (3 screen) from seashell mansion to armos island + armos_fairy_entrance.connect(d6_armos_island, PEGASUS_BOOTS, one_way=True) # jesus jump from top (fairy bomb cave) to armos island with fast falling + d6_connector_right.connect(d6_connector_left, PEGASUS_BOOTS) # boots bonk across bottom wall at water and pits (can do both ways) + + obstacle_cave_entrance.connect(obstacle_cave_inside, OR(HOOKSHOT, AND(FEATHER, PEGASUS_BOOTS, OR(SWORD, MAGIC_ROD, BOW)))) # get past crystal rocks by hookshotting into top pushable block, or boots dashing into top wall where the pushable block is to superjump down + obstacle_cave_entrance.connect(obstacle_cave_inside, AND(PEGASUS_BOOTS, ROOSTER)) # get past crystal rocks pushing the top pushable block, then boots dashing up picking up the rooster before bonking. Pause buffer until rooster is fully picked up then throw it down before bonking into wall + d4_entrance.connect(below_right_taltal, FEATHER) # jesus jump a long way + if options.entranceshuffle in ("default", "simple"): # connector cave from armos d6 area to raft shop may not be randomized to add a flippers path since flippers stop you from jesus jumping + below_right_taltal.connect(raft_game, AND(FEATHER, r.attack_hookshot_powder), one_way=True) # jesus jump from heartpiece water cave, around the island and clip past the diagonal gap in the rock, then jesus jump all the way down the waterfall to the chests (attack req for hardlock flippers+feather scenario) + outside_raft_house.connect(below_right_taltal, AND(FEATHER, PEGASUS_BOOTS)) #superjump from ledge left to right, can buffer to land on ledge instead of water, then superjump right which is pixel perfect + bridge_seashell.connect(outside_rooster_house, AND(PEGASUS_BOOTS, POWER_BRACELET)) # boots bonk + bird_key.connect(bird_cave, AND(FEATHER, PEGASUS_BOOTS)) # boots jump above wall, use multiple pit buffers to get across + mountain_bridge_staircase.connect(outside_rooster_house, OR(PEGASUS_BOOTS, FEATHER)) # cross bridge to staircase with pit buffer to clip bottom wall and jump or boots bonk across + left_right_connector_cave_entrance.connect(left_right_connector_cave_exit, AND(PEGASUS_BOOTS, FEATHER), one_way=True) # boots jump to bottom left corner of pits, pit buffer and jump to left + left_right_connector_cave_exit.connect(left_right_connector_cave_entrance, AND(ROOSTER, OR(PEGASUS_BOOTS, SWORD, BOW, MAGIC_ROD)), one_way=True) # pass through the passage in reverse using a boots rooster hop or rooster superjump in the one way passage area + + self.start = start_house + self.egg = windfish_egg + self.nightmare = nightmare + self.windfish = windfish + + def _addEntrance(self, name, outside, inside, requirement): + assert name not in self.overworld_entrance, "Duplicate entrance: %s" % name + assert name in ENTRANCE_INFO + self.overworld_entrance[name] = EntranceExterior(outside, requirement) + self.indoor_location[name] = inside + + def _addEntranceRequirement(self, name, requirement): + assert name in self.overworld_entrance + self.overworld_entrance[name].addRequirement(requirement) + + def _addEntranceRequirementEnter(self, name, requirement): + assert name in self.overworld_entrance + self.overworld_entrance[name].addEnterRequirement(requirement) + + def _addEntranceRequirementExit(self, name, requirement): + assert name in self.overworld_entrance + self.overworld_entrance[name].addExitRequirement(requirement) + + def updateIndoorLocation(self, name, location): + assert name in self.indoor_location + assert self.indoor_location[name] is None + self.indoor_location[name] = location + + +class DungeonDiveOverworld: + def __init__(self, options, r): + self.overworld_entrance = {} + self.indoor_location = {} + + start_house = Location("Start House").add(StartItem()) + Location().add(ShopItem(0)).connect(start_house, OR(COUNT("RUPEES", 200), SWORD)) + Location().add(ShopItem(1)).connect(start_house, OR(COUNT("RUPEES", 980), SWORD)) + Location().add(Song(0x0B1)).connect(start_house, OCARINA) # Marins song + start_house.add(DroppedKey(0xB2)) # Sword on the beach + egg = Location().connect(start_house, AND(r.bush, BOMB)) + Location().add(MadBatter(0x1E1)).connect(start_house, MAGIC_POWDER) + if options.boomerang == 'trade': + Location().add(BoomerangGuy()).connect(start_house, AND(BOMB, OR(BOOMERANG, HOOKSHOT, MAGIC_ROD, PEGASUS_BOOTS, FEATHER, SHOVEL))) + elif options.boomerang == 'gift': + Location().add(BoomerangGuy()).connect(start_house, BOMB) + + nightmare = Location("Nightmare") + windfish = Location("Windfish").connect(nightmare, AND(MAGIC_POWDER, SWORD, OR(BOOMERANG, BOW))) + + self.start = start_house + self.overworld_entrance = { + "d1": EntranceExterior(start_house, None), + "d2": EntranceExterior(start_house, None), + "d3": EntranceExterior(start_house, None), + "d4": EntranceExterior(start_house, None), + "d5": EntranceExterior(start_house, FLIPPERS), + "d6": EntranceExterior(start_house, None), + "d7": EntranceExterior(start_house, None), + "d8": EntranceExterior(start_house, None), + "d0": EntranceExterior(start_house, None), + } + self.egg = egg + self.nightmare = nightmare + self.windfish = windfish + + def updateIndoorLocation(self, name, location): + self.indoor_location[name] = location + + +class EntranceExterior: + def __init__(self, outside, requirement, one_way_enter_requirement="UNSET", one_way_exit_requirement="UNSET"): + self.location = outside + self.requirement = requirement + self.one_way_enter_requirement = one_way_enter_requirement + self.one_way_exit_requirement = one_way_exit_requirement + + def addRequirement(self, new_requirement): + self.requirement = OR(self.requirement, new_requirement) + + def addExitRequirement(self, new_requirement): + if self.one_way_exit_requirement == "UNSET": + self.one_way_exit_requirement = new_requirement + else: + self.one_way_exit_requirement = OR(self.one_way_exit_requirement, new_requirement) + + def addEnterRequirement(self, new_requirement): + if self.one_way_enter_requirement == "UNSET": + self.one_way_enter_requirement = new_requirement + else: + self.one_way_enter_requirement = OR(self.one_way_enter_requirement, new_requirement) + + def enterIsSet(self): + return self.one_way_enter_requirement != "UNSET" + + def exitIsSet(self): + return self.one_way_exit_requirement != "UNSET" diff --git a/worlds/ladx/LADXR/logic/requirements.py b/worlds/ladx/LADXR/logic/requirements.py new file mode 100644 index 0000000000..acc969ba93 --- /dev/null +++ b/worlds/ladx/LADXR/logic/requirements.py @@ -0,0 +1,318 @@ +from typing import Optional +from ..locations.items import * + + +class OR: + __slots__ = ('__items', '__children') + + def __new__(cls, *args): + if True in args: + return True + return super().__new__(cls) + + def __init__(self, *args): + self.__items = [item for item in args if isinstance(item, str)] + self.__children = [item for item in args if type(item) not in (bool, str) and item is not None] + + assert self.__items or self.__children, args + + def __repr__(self) -> str: + return "or%s" % (self.__items+self.__children) + + def remove(self, item) -> None: + if item in self.__items: + self.__items.remove(item) + + def hasConsumableRequirement(self) -> bool: + for item in self.__items: + if isConsumable(item): + print("Consumable OR requirement? %r" % self) + return True + for child in self.__children: + if child.hasConsumableRequirement(): + print("Consumable OR requirement? %r" % self) + return True + return False + + def test(self, inventory) -> bool: + for item in self.__items: + if item in inventory: + return True + for child in self.__children: + if child.test(inventory): + return True + return False + + def consume(self, inventory) -> bool: + for item in self.__items: + if item in inventory: + if isConsumable(item): + inventory[item] -= 1 + if inventory[item] == 0: + del inventory[item] + inventory["%s_USED" % item] = inventory.get("%s_USED" % item, 0) + 1 + return True + for child in self.__children: + if child.consume(inventory): + return True + return False + + def getItems(self, inventory, target_set) -> None: + if self.test(inventory): + return + for item in self.__items: + target_set.add(item) + for child in self.__children: + child.getItems(inventory, target_set) + + def copyWithModifiedItemNames(self, f) -> "OR": + return OR(*(f(item) for item in self.__items), *(child.copyWithModifiedItemNames(f) for child in self.__children)) + + +class AND: + __slots__ = ('__items', '__children') + + def __new__(cls, *args): + if False in args: + return False + return super().__new__(cls) + + def __init__(self, *args): + self.__items = [item for item in args if isinstance(item, str)] + self.__children = [item for item in args if type(item) not in (bool, str) and item is not None] + + def __repr__(self) -> str: + return "and%s" % (self.__items+self.__children) + + def remove(self, item) -> None: + if item in self.__items: + self.__items.remove(item) + + def hasConsumableRequirement(self) -> bool: + for item in self.__items: + if isConsumable(item): + return True + for child in self.__children: + if child.hasConsumableRequirement(): + return True + return False + + def test(self, inventory) -> bool: + for item in self.__items: + if item not in inventory: + return False + for child in self.__children: + if not child.test(inventory): + return False + return True + + def consume(self, inventory) -> bool: + for item in self.__items: + if isConsumable(item): + inventory[item] -= 1 + if inventory[item] == 0: + del inventory[item] + inventory["%s_USED" % item] = inventory.get("%s_USED" % item, 0) + 1 + for child in self.__children: + if not child.consume(inventory): + return False + return True + + def getItems(self, inventory, target_set) -> None: + if self.test(inventory): + return + for item in self.__items: + target_set.add(item) + for child in self.__children: + child.getItems(inventory, target_set) + + def copyWithModifiedItemNames(self, f) -> "AND": + return AND(*(f(item) for item in self.__items), *(child.copyWithModifiedItemNames(f) for child in self.__children)) + + +class COUNT: + __slots__ = ('__item', '__amount') + + def __init__(self, item: str, amount: int) -> None: + self.__item = item + self.__amount = amount + + def __repr__(self) -> str: + return "<%dx%s>" % (self.__amount, self.__item) + + def hasConsumableRequirement(self) -> bool: + if isConsumable(self.__item): + return True + return False + + def test(self, inventory) -> bool: + return inventory.get(self.__item, 0) >= self.__amount + + def consume(self, inventory) -> None: + if isConsumable(self.__item): + inventory[self.__item] -= self.__amount + if inventory[self.__item] == 0: + del inventory[self.__item] + inventory["%s_USED" % self.__item] = inventory.get("%s_USED" % self.__item, 0) + self.__amount + + def getItems(self, inventory, target_set) -> None: + if self.test(inventory): + return + target_set.add(self.__item) + + def copyWithModifiedItemNames(self, f) -> "COUNT": + return COUNT(f(self.__item), self.__amount) + + +class COUNTS: + __slots__ = ('__items', '__amount') + + def __init__(self, items, amount): + self.__items = items + self.__amount = amount + + def __repr__(self) -> str: + return "<%dx%s>" % (self.__amount, self.__items) + + def hasConsumableRequirement(self) -> bool: + for item in self.__items: + if isConsumable(item): + print("Consumable COUNTS requirement? %r" % (self)) + return True + return False + + def test(self, inventory) -> bool: + count = 0 + for item in self.__items: + count += inventory.get(item, 0) + return count >= self.__amount + + def consume(self, inventory) -> None: + for item in self.__items: + if isConsumable(item): + inventory[item] -= self.__amount + if inventory[item] == 0: + del inventory[item] + inventory["%s_USED" % item] = inventory.get("%s_USED" % item, 0) + self.__amount + + def getItems(self, inventory, target_set) -> None: + if self.test(inventory): + return + for item in self.__items: + target_set.add(item) + + def copyWithModifiedItemNames(self, f) -> "COUNTS": + return COUNTS([f(item) for item in self.__items], self.__amount) + + +class FOUND: + __slots__ = ('__item', '__amount') + + def __init__(self, item: str, amount: int) -> None: + self.__item = item + self.__amount = amount + + def __repr__(self) -> str: + return "{%dx%s}" % (self.__amount, self.__item) + + def hasConsumableRequirement(self) -> bool: + return False + + def test(self, inventory) -> bool: + return inventory.get(self.__item, 0) + inventory.get("%s_USED" % self.__item, 0) >= self.__amount + + def consume(self, inventory) -> None: + pass + + def getItems(self, inventory, target_set) -> None: + if self.test(inventory): + return + target_set.add(self.__item) + + def copyWithModifiedItemNames(self, f) -> "FOUND": + return FOUND(f(self.__item), self.__amount) + + +def hasConsumableRequirement(requirements) -> bool: + if isinstance(requirements, str): + return isConsumable(requirements) + if requirements is None: + return False + return requirements.hasConsumableRequirement() + + +def isConsumable(item) -> bool: + if item is None: + return False + #if item.startswith("RUPEES_") or item == "RUPEES": + # return True + if item.startswith("KEY"): + return True + return False + + +class RequirementsSettings: + def __init__(self, options): + self.bush = OR(SWORD, MAGIC_POWDER, MAGIC_ROD, POWER_BRACELET, BOOMERANG) + self.attack = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG) + self.attack_hookshot = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG, HOOKSHOT) # switches, hinox, shrouded stalfos + self.attack_hookshot_powder = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG, HOOKSHOT, MAGIC_POWDER) # zols, keese, moldorm + self.attack_no_bomb = OR(SWORD, BOW, MAGIC_ROD, BOOMERANG, HOOKSHOT) # ? + self.attack_hookshot_no_bomb = OR(SWORD, BOW, MAGIC_ROD, BOOMERANG, HOOKSHOT) # vire + self.attack_no_boomerang = OR(SWORD, BOMB, BOW, MAGIC_ROD, HOOKSHOT) # teleporting owls + self.attack_skeleton = OR(SWORD, BOMB, BOW, BOOMERANG, HOOKSHOT) # cannot kill skeletons with the fire rod + self.rear_attack = OR(SWORD, BOMB) # mimic + self.rear_attack_range = OR(MAGIC_ROD, BOW) # mimic + self.fire = OR(MAGIC_POWDER, MAGIC_ROD) # torches + self.push_hardhat = OR(SHIELD, SWORD, HOOKSHOT, BOOMERANG) + + self.boss_requirements = [ + SWORD, # D1 boss + AND(OR(SWORD, MAGIC_ROD), POWER_BRACELET), # D2 boss + AND(PEGASUS_BOOTS, SWORD), # D3 boss + AND(FLIPPERS, OR(SWORD, MAGIC_ROD, BOW)), # D4 boss + AND(HOOKSHOT, SWORD), # D5 boss + BOMB, # D6 boss + AND(OR(MAGIC_ROD, SWORD, HOOKSHOT), COUNT(SHIELD, 2)), # D7 boss + MAGIC_ROD, # D8 boss + self.attack_hookshot_no_bomb, # D9 boss + ] + self.miniboss_requirements = { + "ROLLING_BONES": self.attack_hookshot, + "HINOX": self.attack_hookshot, + "DODONGO": BOMB, + "CUE_BALL": SWORD, + "GHOMA": OR(BOW, HOOKSHOT), + "SMASHER": POWER_BRACELET, + "GRIM_CREEPER": self.attack_hookshot_no_bomb, + "BLAINO": SWORD, + "AVALAUNCH": self.attack_hookshot, + "GIANT_BUZZ_BLOB": MAGIC_POWDER, + "MOBLIN_KING": SWORD, + "ARMOS_KNIGHT": OR(BOW, MAGIC_ROD, SWORD), + } + + # Adjust for options + if options.bowwow != 'normal': + # We cheat in bowwow mode, we pretend we have the sword, as bowwow can pretty much do all what the sword ca$ # Except for taking out bushes (and crystal pillars are removed) + self.bush.remove(SWORD) + if options.logic == "casual": + # In casual mode, remove the more complex kill methods + self.bush.remove(MAGIC_POWDER) + self.attack_hookshot_powder.remove(MAGIC_POWDER) + self.attack.remove(BOMB) + self.attack_hookshot.remove(BOMB) + self.attack_hookshot_powder.remove(BOMB) + self.attack_no_boomerang.remove(BOMB) + self.attack_skeleton.remove(BOMB) + if options.logic == "hard": + self.boss_requirements[3] = AND(FLIPPERS, OR(SWORD, MAGIC_ROD, BOW, BOMB)) # bomb angler fish + self.boss_requirements[6] = OR(MAGIC_ROD, AND(BOMB, BOW), COUNT(SWORD, 2), AND(OR(SWORD, HOOKSHOT, BOW), SHIELD)) # evil eagle 3 cycle magic rod / bomb arrows / l2 sword, and bow kill + if options.logic == "glitched": + self.boss_requirements[3] = AND(FLIPPERS, OR(SWORD, MAGIC_ROD, BOW, BOMB)) # bomb angler fish + self.boss_requirements[6] = OR(MAGIC_ROD, BOMB, BOW, HOOKSHOT, COUNT(SWORD, 2), AND(SWORD, SHIELD)) # evil eagle off screen kill or 3 cycle with bombs + if options.logic == "hell": + self.boss_requirements[3] = AND(FLIPPERS, OR(SWORD, MAGIC_ROD, BOW, BOMB)) # bomb angler fish + self.boss_requirements[6] = OR(MAGIC_ROD, BOMB, BOW, HOOKSHOT, COUNT(SWORD, 2), AND(SWORD, SHIELD)) # evil eagle off screen kill or 3 cycle with bombs + self.boss_requirements[7] = OR(MAGIC_ROD, COUNT(SWORD, 2)) # hot head sword beams + self.miniboss_requirements["GIANT_BUZZ_BLOB"] = OR(MAGIC_POWDER, COUNT(SWORD,2)) # use sword beams to damage buzz blob diff --git a/worlds/ladx/LADXR/main.py b/worlds/ladx/LADXR/main.py new file mode 100644 index 0000000000..5b563675c0 --- /dev/null +++ b/worlds/ladx/LADXR/main.py @@ -0,0 +1,52 @@ +import binascii +from .romTables import ROMWithTables +import json +from . import logic +import argparse +from .settings import Settings +from typing import Optional, List + +def get_parser(): + + parser = argparse.ArgumentParser(description='Randomize!') + parser.add_argument('input_filename', metavar='input rom', type=str, + help="Rom file to use as input.") + parser.add_argument('-o', '--output', dest="output_filename", metavar='output rom', type=str, required=False, + help="Output filename to use. If not specified [seed].gbc is used.") + parser.add_argument('--dump', dest="dump", type=str, nargs="*", + help="Dump the logic of the given rom (spoilers!)") + parser.add_argument('--spoilerformat', dest="spoilerformat", choices=["none", "console", "text", "json"], default="none", + help="Sets the output format for the generated seed's spoiler log") + parser.add_argument('--spoilerfilename', dest="spoiler_filename", type=str, required=False, + help="Output filename to use for the spoiler log. If not specified, LADXR_[seed].txt/json is used.") + parser.add_argument('--test', dest="test", action="store_true", + help="Test the logic of the given rom, without showing anything.") + parser.add_argument('--romdebugmode', dest="romdebugmode", action="store_true", + help="Patch the rom so that debug mode is enabled, this creates a default save with most items and unlocks some debug features.") + parser.add_argument('--exportmap', dest="exportmap", action="store_true", + help="Export the map (many graphical mistakes)") + parser.add_argument('--emptyplan', dest="emptyplan", type=str, required=False, + help="Write an unfilled plan file") + parser.add_argument('--timeout', type=float, required=False, + help="Timeout generating the seed after the specified number of seconds") + parser.add_argument('--logdirectory', dest="log_directory", type=str, required=False, + help="Directory to write the JSON log file. Generated independently from the spoiler log and omitted by default.") + + parser.add_argument('-s', '--setting', dest="settings", action="append", required=False, + help="Set a configuration setting for rom generation") + parser.add_argument('--short', dest="shortsettings", type=str, required=False, + help="Set a configuration setting for rom generation") + parser.add_argument('--settingjson', dest="settingjson", action="store_true", + help="Dump a json blob which describes all settings") + + parser.add_argument('--plan', dest="plan", metavar='plandomizer', type=str, required=False, + help="Read an item placement plan") + parser.add_argument('--multiworld', dest="multiworld", action="append", required=False, + help="Set configuration for a multiworld player, supply multiple times for settings per player, requires a short setting string per player.") + parser.add_argument('--doubletrouble', dest="doubletrouble", action="store_true", + help="Warning, bugged in various ways") + parser.add_argument('--pymod', dest="pymod", action='append', + help="Load python code mods.") + + return parser + diff --git a/worlds/ladx/LADXR/mapgen/__init__.py b/worlds/ladx/LADXR/mapgen/__init__.py new file mode 100644 index 0000000000..d38c27fbdd --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/__init__.py @@ -0,0 +1,147 @@ +from ..romTables import ROMWithTables +from ..roomEditor import RoomEditor, ObjectWarp +from ..patches import overworld, core +from .tileset import loadTileInfo +from .map import Map, MazeGen +from .wfc import WFCMap, ContradictionException +from .roomgen import setup_room_types +from .imagegenerator import ImageGen +from .util import xyrange +from .locations.entrance import DummyEntrance +from .locationgen import LocationGenerator +from .logic import LogicGenerator +from .enemygen import generate_enemies +from ..assembler import ASM + + +def store_map(rom, the_map: Map): + # Move all exceptions to room FF + # Dig seashells + rom.patch(0x03, 0x220F, ASM("cp $DA"), ASM("cp $FF")) + rom.patch(0x03, 0x2213, ASM("cp $A5"), ASM("cp $FF")) + rom.patch(0x03, 0x2217, ASM("cp $74"), ASM("cp $FF")) + rom.patch(0x03, 0x221B, ASM("cp $3A"), ASM("cp $FF")) + rom.patch(0x03, 0x221F, ASM("cp $A8"), ASM("cp $FF")) + rom.patch(0x03, 0x2223, ASM("cp $B2"), ASM("cp $FF")) + # Force tile 04 under bushes and rocks, instead of conditionally tile 3, else seashells won't spawn. + rom.patch(0x14, 0x1655, 0x1677, "", fill_nop=True) + # Bonk trees + rom.patch(0x03, 0x0F03, ASM("cp $A4"), ASM("cp $FF")) + rom.patch(0x03, 0x0F07, ASM("cp $D2"), ASM("cp $FF")) + # Stairs under rocks + rom.patch(0x14, 0x1638, ASM("cp $52"), ASM("cp $FF")) + rom.patch(0x14, 0x163C, ASM("cp $04"), ASM("cp $FF")) + + # Patch D6 raft game exit, just remove the exit. + re = RoomEditor(rom, 0x1B0) + re.removeObject(7, 0) + re.store(rom) + # Patch D8 back entrance, remove the outside part + re = RoomEditor(rom, 0x23A) + re.objects = [obj for obj in re.objects if not isinstance(obj, ObjectWarp)] + [ObjectWarp(1, 7, 0x23D, 0x58, 0x10)] + re.store(rom) + re = RoomEditor(rom, 0x23D) + re.objects = [obj for obj in re.objects if not isinstance(obj, ObjectWarp)] + [ObjectWarp(1, 7, 0x23A, 0x58, 0x10)] + re.store(rom) + + for room in the_map: + for location in room.locations: + location.prepare(rom) + for n in range(0x00, 0x100): + sx = n & 0x0F + sy = ((n >> 4) & 0x0F) + if sx < the_map.w and sy < the_map.h: + tiles = the_map.get(sx, sy).tiles + else: + tiles = [4] * 80 + tiles[44] = 0xC6 + + re = RoomEditor(rom, n) + # tiles = re.getTileArray() + re.objects = [] + re.entities = [] + room = the_map.get(sx, sy) if sx < the_map.w and sy < the_map.h else None + + tileset = the_map.tilesets[room.tileset_id] if room else None + rom.banks[0x3F][0x3F00 + n] = tileset.main_id if tileset else 0x0F + rom.banks[0x21][0x02EF + n] = tileset.palette_id if tileset and tileset.palette_id is not None else 0x03 + rom.banks[0x1A][0x2476 + n] = tileset.attr_bank if tileset and tileset.attr_bank else 0x22 + rom.banks[0x1A][0x1E76 + n * 2] = (tileset.attr_addr & 0xFF) if tileset and tileset.attr_addr else 0x00 + rom.banks[0x1A][0x1E77 + n * 2] = (tileset.attr_addr >> 8) if tileset and tileset.attr_addr else 0x60 + re.animation_id = tileset.animation_id if tileset and tileset.animation_id is not None else 0x03 + + re.buildObjectList(tiles) + if room: + for idx, tile_id in enumerate(tiles): + if tile_id == 0x61: # Fix issues with the well being used as chimney as well and causing wrong warps + DummyEntrance(room, idx % 10, idx // 10) + re.entities += room.entities + room.locations.sort(key=lambda loc: (loc.y, loc.x, id(loc))) + for location in room.locations: + location.update_room(rom, re) + else: + re.objects.append(ObjectWarp(0x01, 0x10, 0x2A3, 0x50, 0x7C)) + re.store(rom) + + rom.banks[0x21][0x00BF:0x00BF+3] = [0, 0, 0] # Patch out the "load palette on screen transition" exception code. + + # Fix some tile attribute issues + def change_attr(tileset, index, a, b, c, d): + rom.banks[the_map.tilesets[tileset].attr_bank][the_map.tilesets[tileset].attr_addr - 0x4000 + index * 4 + 0] = a + rom.banks[the_map.tilesets[tileset].attr_bank][the_map.tilesets[tileset].attr_addr - 0x4000 + index * 4 + 1] = b + rom.banks[the_map.tilesets[tileset].attr_bank][the_map.tilesets[tileset].attr_addr - 0x4000 + index * 4 + 2] = c + rom.banks[the_map.tilesets[tileset].attr_bank][the_map.tilesets[tileset].attr_addr - 0x4000 + index * 4 + 3] = d + change_attr("mountains", 0x04, 6, 6, 6, 6) + change_attr("mountains", 0x27, 6, 6, 3, 3) + change_attr("mountains", 0x28, 6, 6, 3, 3) + change_attr("mountains", 0x6E, 1, 1, 1, 1) + change_attr("town", 0x59, 2, 2, 2, 2) # Roof tile wrong color + + +def generate(rom_filename, w, h): + rom = ROMWithTables(rom_filename) + overworld.patchOverworldTilesets(rom) + core.cleanup(rom) + tilesets = loadTileInfo(rom) + + the_map = Map(w, h, tilesets) + setup_room_types(the_map) + + MazeGen(the_map) + imggen = ImageGen(tilesets, the_map, rom) + imggen.enabled = False + wfcmap = WFCMap(the_map, tilesets) #, step_callback=imggen.on_step) + try: + wfcmap.initialize() + except ContradictionException as e: + print(f"Failed on setup {e.x // 10} {e.y // 8} {e.x % 10} {e.y % 8}") + imggen.on_step(wfcmap, err=(e.x, e.y)) + return + imggen.on_step(wfcmap) + for x, y in xyrange(w, h): + for n in range(50): + try: + wfcmap.build(x * 10, y * 8, 10, 8) + imggen.on_step(wfcmap) + break + except ContradictionException as e: + print(f"Failed {x} {y} {e.x%10} {e.y%8} {n}") + imggen.on_step(wfcmap, err=(e.x, e.y)) + wfcmap.clear() + if n == 49: + raise RuntimeError("Failed to fill chunk") + print(f"Done {x} {y}") + imggen.on_step(wfcmap) + wfcmap.store_tile_data(the_map) + + LocationGenerator(the_map) + + for room in the_map: + generate_enemies(room) + + if imggen.enabled: + store_map(rom, the_map) + from mapexport import MapExport + MapExport(rom).export_all(w, h, dungeons=False) + rom.save("test.gbc") + return the_map diff --git a/worlds/ladx/LADXR/mapgen/enemygen.py b/worlds/ladx/LADXR/mapgen/enemygen.py new file mode 100644 index 0000000000..45020b93a9 --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/enemygen.py @@ -0,0 +1,59 @@ +from .tileset import walkable_tiles, entrance_tiles +import random + + +ENEMIES = { + "mountains": [ + (0x0B,), + (0x0E,), + (0x29,), + (0x0E, 0x0E), + (0x0E, 0x0E, 0x23), + (0x0D,), (0x0D, 0x0D), + ], + "egg": [], + "basic": [ + (), (), (), (), (), (), + (0x09,), (0x09, 0x09), # octorock + (0x9B, 0x9B), (0x9B, 0x9B, 0x1B), # slimes + (0xBB, 0x9B), # bush crawler + slime + (0xB9,), + (0x0B, 0x23), # likelike + moblin + (0x14, 0x0B, 0x0B), # moblins + sword + (0x0B, 0x23, 0x23), # likelike + moblin + (0xAE, 0xAE), # flying octorock + (0xBA, ), # Bomber + (0x0D, 0x0D), (0x0D, ), + ], + "town": [ + (), (), (0x6C, 0x6E), (0x6E,), (0x6E, 0x6E), + ], + "forest": [ + (0x0B,), # moblins + (0x0B, 0x0B), # moblins + (0x14, 0x0B, 0x0B), # moblins + sword + ], + "beach": [ + (0xC6, 0xC6), + (0x0E, 0x0E, 0xC6), + (0x0E, 0x0E, 0x09), + ], + "water": [], +} + + +def generate_enemies(room): + options = ENEMIES[room.tileset_id] + if not options: + return + positions = [] + for y in range(1, 7): + for x in range(1, 9): + if room.tiles[x + y * 10] in walkable_tiles and room.tiles[x + (y - 1) * 10] not in entrance_tiles: + positions.append((x, y)) + for type_id in random.choice(options): + if not positions: + return + x, y = random.choice(positions) + positions.remove((x, y)) + room.entities.append((x, y, type_id)) diff --git a/worlds/ladx/LADXR/mapgen/imagegenerator.py b/worlds/ladx/LADXR/mapgen/imagegenerator.py new file mode 100644 index 0000000000..fc9d5bbeee --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/imagegenerator.py @@ -0,0 +1,95 @@ +from .tileset import open_tiles, solid_tiles + + +def tx(x): + return x * 16 + x // 10 + + +def ty(y): + return y * 16 + y // 8 + + +class ImageGen: + def __init__(self, tilesets, the_map, rom): + self.tilesets = tilesets + self.map = the_map + self.rom = rom + self.image = None + self.draw = None + self.count = 0 + self.enabled = False + self.__tile_cache = {} + + def on_step(self, wfc, cur=None, err=None): + if not self.enabled: + return + if self.image is None: + import PIL.Image + import PIL.ImageDraw + self.image = PIL.Image.new("RGB", (self.map.w * 161, self.map.h * 129)) + self.draw = PIL.ImageDraw.Draw(self.image) + self.image.paste(0, (0, 0, wfc.w * 16, wfc.h * 16)) + for y in range(wfc.h): + for x in range(wfc.w): + cell = wfc.cell_data[(x, y)] + if len(cell.options) == 1: + tile_id = next(iter(cell.options)) + room = self.map.get(x//10, y//8) + tile = self.get_tile(room.tileset_id, tile_id) + self.image.paste(tile, (tx(x), ty(y))) + else: + self.draw.text((tx(x) + 3, ty(y) + 3), f"{len(cell.options):2}", (255, 255, 255)) + if cell.options.issubset(open_tiles): + self.draw.rectangle((tx(x), ty(y), tx(x) + 15, ty(y) + 15), outline=(0, 128, 0)) + elif cell.options.issubset(solid_tiles): + self.draw.rectangle((tx(x), ty(y), tx(x) + 15, ty(y) + 15), outline=(0, 0, 192)) + if cur: + self.draw.rectangle((tx(cur[0]),ty(cur[1]),tx(cur[0])+15,ty(cur[1])+15), outline=(0, 255, 0)) + if err: + self.draw.rectangle((tx(err[0]),ty(err[1]),tx(err[0])+15,ty(err[1])+15), outline=(255, 0, 0)) + self.image.save(f"_map/tmp{self.count:08}.png") + self.count += 1 + + def get_tile(self, tileset_id, tile_id): + tile = self.__tile_cache.get((tileset_id, tile_id), None) + if tile is not None: + return tile + import PIL.Image + tile = PIL.Image.new("L", (16, 16)) + tileset = self.get_tileset(tileset_id) + metatile = self.rom.banks[0x1A][0x2749 + tile_id * 4:0x2749 + tile_id * 4+4] + + def draw(ox, oy, t): + addr = (t & 0x3FF) << 4 + tile_data = self.rom.banks[t >> 10][addr:addr+0x10] + for y in range(8): + a = tile_data[y * 2] + b = tile_data[y * 2 + 1] + for x in range(8): + v = 0 + bit = 0x80 >> x + if a & bit: + v |= 0x01 + if b & bit: + v |= 0x02 + tile.putpixel((ox+x,oy+y), (255, 192, 128, 32)[v]) + draw(0, 0, tileset[metatile[0]]) + draw(8, 0, tileset[metatile[1]]) + draw(0, 8, tileset[metatile[2]]) + draw(8, 8, tileset[metatile[3]]) + self.__tile_cache[(tileset_id, tile_id)] = tile + return tile + + def get_tileset(self, tileset_id): + subtiles = [0] * 0x100 + for n in range(0, 0x20): + subtiles[n] = (0x0F << 10) + (self.tilesets[tileset_id].main_id << 4) + n + for n in range(0x20, 0x80): + subtiles[n] = (0x0C << 10) + 0x100 + n + for n in range(0x80, 0x100): + subtiles[n] = (0x0C << 10) + n + + addr = (0x000, 0x000, 0x2B0, 0x2C0, 0x2D0, 0x2E0, 0x2F0, 0x2D0, 0x300, 0x310, 0x320, 0x2A0, 0x330, 0x350, 0x360, 0x340, 0x370)[self.tilesets[tileset_id].animation_id or 3] + for n in range(0x6C, 0x70): + subtiles[n] = (0x0C << 10) + addr + n - 0x6C + return subtiles diff --git a/worlds/ladx/LADXR/mapgen/locationgen.py b/worlds/ladx/LADXR/mapgen/locationgen.py new file mode 100644 index 0000000000..0a30d80bd5 --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/locationgen.py @@ -0,0 +1,203 @@ +from .tileset import entrance_tiles, solid_tiles, walkable_tiles +from .map import Map +from .util import xyrange +from .locations.entrance import Entrance +from .locations.chest import Chest, FloorItem +from .locations.seashell import HiddenSeashell, DigSeashell, BonkSeashell +import random +from typing import List + +all_location_constructors = (Chest, FloorItem, HiddenSeashell, DigSeashell, BonkSeashell) + + +def remove_duplicate_tile(tiles, to_find): + try: + idx0 = tiles.index(to_find) + idx1 = tiles.index(to_find, idx0 + 1) + tiles[idx1] = 0x04 + except ValueError: + return + + +class Dijkstra: + def __init__(self, the_map: Map): + self.map = the_map + self.w = the_map.w * 10 + self.h = the_map.h * 8 + self.area = [-1] * (self.w * self.h) + self.distance = [0] * (self.w * self.h) + self.area_size = [] + self.next_area_id = 0 + + def fill(self, start_x, start_y): + size = 0 + todo = [(start_x, start_y, 0)] + while todo: + x, y, distance = todo.pop(0) + room = self.map.get(x // 10, y // 8) + tile_idx = (x % 10) + (y % 8) * 10 + area_idx = x + y * self.w + if room.tiles[tile_idx] not in solid_tiles and self.area[area_idx] == -1: + size += 1 + self.area[area_idx] = self.next_area_id + self.distance[area_idx] = distance + todo += [(x - 1, y, distance + 1), (x + 1, y, distance + 1), (x, y - 1, distance + 1), (x, y + 1, distance + 1)] + self.next_area_id += 1 + self.area_size.append(size) + return self.next_area_id - 1 + + def dump(self): + print(self.area_size) + for y in range(self.map.h * 8): + for x in range(self.map.w * 10): + n = self.area[x + y * self.map.w * 10] + if n < 0: + print(' ', end='') + else: + print(n, end='') + print() + + +class EntranceInfo: + def __init__(self, room, x, y): + self.room = room + self.x = x + self.y = y + self.tile = room.tiles[x + y * 10] + + @property + def map_x(self): + return self.room.x * 10 + self.x + + @property + def map_y(self): + return self.room.y * 8 + self.y + + +class LocationGenerator: + def __init__(self, the_map: Map): + # Find all entrances + entrances: List[EntranceInfo] = [] + for room in the_map: + # Prevent more then one chest or hole-entrance per map + remove_duplicate_tile(room.tiles, 0xA0) + remove_duplicate_tile(room.tiles, 0xC6) + for x, y in xyrange(10, 8): + if room.tiles[x + y * 10] in entrance_tiles: + entrances.append(EntranceInfo(room, x, y)) + if room.tiles[x + y * 10] == 0xA0: + Chest(room, x, y) + todo_entrances = entrances.copy() + + # Find a place to put the start position + start_entrances = [info for info in todo_entrances if info.room.tileset_id == "town"] + if not start_entrances: + start_entrances = entrances + start_entrance = random.choice(start_entrances) + todo_entrances.remove(start_entrance) + + # Setup the start position and fill the basic dijkstra flood fill from there. + Entrance(start_entrance.room, start_entrance.x, start_entrance.y, "start_house") + reachable_map = Dijkstra(the_map) + reachable_map.fill(start_entrance.map_x, start_entrance.map_y) + + # Find each entrance that is not reachable from any other spot, and flood fill from that entrance + for info in entrances: + if reachable_map.area[info.map_x + info.map_y * reachable_map.w] == -1: + reachable_map.fill(info.map_x, info.map_y) + + disabled_entrances = ["boomerang_cave", "seashell_mansion"] + house_entrances = ["rooster_house", "writes_house", "photo_house", "raft_house", "crazy_tracy", "witch", "dream_hut", "shop", "madambowwow", "kennel", "library", "ulrira", "trendy_shop", "armos_temple", "banana_seller", "ghost_house", "animal_house1", "animal_house2", "animal_house3", "animal_house4", "animal_house5"] + cave_entrances = ["madbatter_taltal", "bird_cave", "right_fairy", "moblin_cave", "hookshot_cave", "forest_madbatter", "castle_jump_cave", "rooster_grave", "prairie_left_cave1", "prairie_left_cave2", "prairie_left_fairy", "mamu", "armos_fairy", "armos_maze_cave", "prairie_madbatter", "animal_cave", "desert_cave"] + water_entrances = ["mambo", "heartpiece_swim_cave"] + phone_entrances = ["phone_d8", "writes_phone", "castle_phone", "mabe_phone", "prairie_left_phone", "prairie_right_phone", "prairie_low_phone", "animal_phone"] + dungeon_entrances = ["d7", "d8", "d6", "d5", "d4", "d3", "d2", "d1", "d0"] + connector_entrances = [("fire_cave_entrance", "fire_cave_exit"), ("left_to_right_taltalentrance", "left_taltal_entrance"), ("obstacle_cave_entrance", "obstacle_cave_outside_chest", "obstacle_cave_exit"), ("papahl_entrance", "papahl_exit"), ("multichest_left", "multichest_right", "multichest_top"), ("right_taltal_connector1", "right_taltal_connector2"), ("right_taltal_connector3", "right_taltal_connector4"), ("right_taltal_connector5", "right_taltal_connector6"), ("writes_cave_left", "writes_cave_right"), ("raft_return_enter", "raft_return_exit"), ("toadstool_entrance", "toadstool_exit"), ("graveyard_cave_left", "graveyard_cave_right"), ("castle_main_entrance", "castle_upper_left", "castle_upper_right"), ("castle_secret_entrance", "castle_secret_exit"), ("papahl_house_left", "papahl_house_right"), ("prairie_right_cave_top", "prairie_right_cave_bottom", "prairie_right_cave_high"), ("prairie_to_animal_connector", "animal_to_prairie_connector"), ("d6_connector_entrance", "d6_connector_exit"), ("richard_house", "richard_maze"), ("prairie_madbatter_connector_entrance", "prairie_madbatter_connector_exit")] + + # For each area that is not yet reachable from the start area: + # add a connector cave from a reachable area to this new area. + reachable_areas = [0] + unreachable_areas = list(range(1, reachable_map.next_area_id)) + retry_count = 10000 + while unreachable_areas: + source = random.choice(reachable_areas) + target = random.choice(unreachable_areas) + + source_entrances = [info for info in todo_entrances if reachable_map.area[info.map_x + info.map_y * reachable_map.w] == source] + target_entrances = [info for info in todo_entrances if reachable_map.area[info.map_x + info.map_y * reachable_map.w] == target] + if not source_entrances: + retry_count -= 1 + if retry_count < 1: + raise RuntimeError("Failed to add connectors...") + continue + + source_info = random.choice(source_entrances) + target_info = random.choice(target_entrances) + + connector = random.choice(connector_entrances) + connector_entrances.remove(connector) + Entrance(source_info.room, source_info.x, source_info.y, connector[0]) + todo_entrances.remove(source_info) + Entrance(target_info.room, target_info.x, target_info.y, connector[1]) + todo_entrances.remove(target_info) + + for extra_exit in connector[2:]: + info = random.choice(todo_entrances) + todo_entrances.remove(info) + Entrance(info.room, info.x, info.y, extra_exit) + + unreachable_areas.remove(target) + reachable_areas.append(target) + + # Find areas that only have a single entrance, and try to force something in there. + # As else we have useless dead ends, and that is no fun. + for area_id in range(reachable_map.next_area_id): + area_entrances = [info for info in entrances if reachable_map.area[info.map_x + info.map_y * reachable_map.w] == area_id] + if len(area_entrances) != 1: + continue + cells = [] + for y in range(reachable_map.h): + for x in range(reachable_map.w): + if reachable_map.area[x + y * reachable_map.w] == area_id: + if the_map.get(x // 10, y // 8).tiles[(x % 10) + (y % 8) * 10] in walkable_tiles: + cells.append((reachable_map.distance[x + y * reachable_map.w], x, y)) + cells.sort(reverse=True) + d, x, y = random.choice(cells[:10]) + FloorItem(the_map.get(x // 10, y // 8), x % 10, y % 8) + + # Find potential dungeon entrances + # Assign some dungeons + for n in range(4): + if not todo_entrances: + break + info = random.choice(todo_entrances) + todo_entrances.remove(info) + dungeon = random.choice(dungeon_entrances) + dungeon_entrances.remove(dungeon) + Entrance(info.room, info.x, info.y, dungeon) + + # Assign something to all other entrances + for info in todo_entrances: + options = house_entrances if info.tile == 0xE2 else cave_entrances + entrance = random.choice(options) + options.remove(entrance) + Entrance(info.room, info.x, info.y, entrance) + + # Go over each room, and assign something if nothing is assigned yet + todo_list = [room for room in the_map if not room.locations] + random.shuffle(todo_list) + done_count = {} + for room in todo_list: + options = [] + # figure out what things could potentially be placed here + for constructor in all_location_constructors: + if done_count.get(constructor, 0) >= constructor.MAX_COUNT: + continue + xy = constructor.check_possible(room, reachable_map) + if xy is not None: + options.append((*xy, constructor)) + + if options: + x, y, constructor = random.choice(options) + constructor(room, x, y) + done_count[constructor] = done_count.get(constructor, 0) + 1 diff --git a/worlds/ladx/LADXR/mapgen/locations/base.py b/worlds/ladx/LADXR/mapgen/locations/base.py new file mode 100644 index 0000000000..a6526193fc --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/locations/base.py @@ -0,0 +1,24 @@ +from ...roomEditor import RoomEditor +from ..map import RoomInfo + + +class LocationBase: + MAX_COUNT = 9999 + + def __init__(self, room: RoomInfo, x, y): + self.room = room + self.x = x + self.y = y + room.locations.append(self) + + def prepare(self, rom): + pass + + def update_room(self, rom, re: RoomEditor): + pass + + def connect_logic(self, logic_location): + raise NotImplementedError(self.__class__) + + def get_item_pool(self): + raise NotImplementedError(self.__class__) diff --git a/worlds/ladx/LADXR/mapgen/locations/chest.py b/worlds/ladx/LADXR/mapgen/locations/chest.py new file mode 100644 index 0000000000..4cfeb0bc6b --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/locations/chest.py @@ -0,0 +1,73 @@ +from .base import LocationBase +from ..tileset import solid_tiles, open_tiles, walkable_tiles +from ...roomEditor import RoomEditor +from ...locations.all import HeartPiece, Chest as ChestLocation +import random + + +class Chest(LocationBase): + def __init__(self, room, x, y): + super().__init__(room, x, y) + room.tiles[x + y * 10] = 0xA0 + + def connect_logic(self, logic_location): + logic_location.add(ChestLocation(self.room.x + self.room.y * 16)) + + def get_item_pool(self): + return {None: 1} + + @staticmethod + def check_possible(room, reachable_map): + # Check if we can potentially place a chest here, and what the best spot would be. + options = [] + for y in range(1, 6): + for x in range(1, 9): + if room.tiles[x + y * 10 - 10] not in solid_tiles: # Chest needs to be against a "wall" at the top + continue + if room.tiles[x + y * 10] not in walkable_tiles or room.tiles[x + y * 10 + 10] not in walkable_tiles: + continue + if room.tiles[x - 1 + y * 10] not in solid_tiles and room.tiles[x - 1 + y * 10 + 10] not in open_tiles: + continue + if room.tiles[x + 1 + y * 10] not in solid_tiles and room.tiles[x + 1 + y * 10 + 10] not in open_tiles: + continue + idx = room.x * 10 + x + (room.y * 8 + y) * reachable_map.w + if reachable_map.area[idx] == -1: + continue + options.append((reachable_map.distance[idx], x, y)) + if not options: + return None + options.sort(reverse=True) + options = [(x, y) for d, x, y in options if d > options[0][0] - 4] + return random.choice(options) + + +class FloorItem(LocationBase): + def __init__(self, room, x, y): + super().__init__(room, x, y) + + def update_room(self, rom, re: RoomEditor): + re.entities.append((self.x, self.y, 0x35)) + + def connect_logic(self, logic_location): + logic_location.add(HeartPiece(self.room.x + self.room.y * 16)) + + def get_item_pool(self): + return {None: 1} + + @staticmethod + def check_possible(room, reachable_map): + # Check if we can potentially place a floor item here, and what the best spot would be. + options = [] + for y in range(1, 7): + for x in range(1, 9): + if room.tiles[x + y * 10] not in walkable_tiles: + continue + idx = room.x * 10 + x + (room.y * 8 + y) * reachable_map.w + if reachable_map.area[idx] == -1: + continue + options.append((reachable_map.distance[idx], x, y)) + if not options: + return None + options.sort(reverse=True) + options = [(x, y) for d, x, y in options if d > options[0][0] - 4] + return random.choice(options) diff --git a/worlds/ladx/LADXR/mapgen/locations/entrance.py b/worlds/ladx/LADXR/mapgen/locations/entrance.py new file mode 100644 index 0000000000..be4dde6634 --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/locations/entrance.py @@ -0,0 +1,107 @@ +from ...locations.items import BOMB +from .base import LocationBase +from ...roomEditor import RoomEditor, Object, ObjectWarp +from ...entranceInfo import ENTRANCE_INFO +from ...assembler import ASM +from .entrance_info import INFO + + +class Entrance(LocationBase): + def __init__(self, room, x, y, entrance_name): + super().__init__(room, x, y) + self.entrance_name = entrance_name + self.entrance_info = ENTRANCE_INFO[entrance_name] + self.source_warp = None + self.target_warp_idx = None + + self.inside_logic = None + + def prepare(self, rom): + info = self.entrance_info + re = RoomEditor(rom, info.alt_room if info.alt_room is not None else info.room) + self.source_warp = re.getWarps()[info.index if info.index not in (None, "all") else 0] + re = RoomEditor(rom, self.source_warp.room) + for idx, warp in enumerate(re.getWarps()): + if warp.room == info.room or warp.room == info.alt_room: + self.target_warp_idx = idx + + def update_room(self, rom, re: RoomEditor): + re.objects.append(self.source_warp) + + target = RoomEditor(rom, self.source_warp.room) + warp = target.getWarps()[self.target_warp_idx] + warp.room = self.room.x | (self.room.y << 4) + warp.target_x = self.x * 16 + 8 + warp.target_y = self.y * 16 + 18 + target.store(rom) + + def prepare_logic(self, configuration_options, world_setup, requirements_settings): + if self.entrance_name in INFO and INFO[self.entrance_name].logic is not None: + self.inside_logic = INFO[self.entrance_name].logic(configuration_options, world_setup, requirements_settings) + + def connect_logic(self, logic_location): + if self.entrance_name not in INFO: + raise RuntimeError(f"WARNING: Logic connection to entrance unmapped! {self.entrance_name}") + if self.inside_logic: + req = None + if self.room.tiles[self.x + self.y * 10] == 0xBA: + req = BOMB + logic_location.connect(self.inside_logic, req) + if INFO[self.entrance_name].exits: + return [(name, logic(logic_location)) for name, logic in INFO[self.entrance_name].exits] + return None + + def get_item_pool(self): + if self.entrance_name not in INFO: + return {} + return INFO[self.entrance_name].items or {} + + +class DummyEntrance(LocationBase): + def __init__(self, room, x, y): + super().__init__(room, x, y) + + def update_room(self, rom, re: RoomEditor): + re.objects.append(ObjectWarp(0x01, 0x10, 0x2A3, 0x50, 0x7C)) + + def connect_logic(self, logic_location): + return + + def get_item_pool(self): + return {} + + +class EggEntrance(LocationBase): + def __init__(self, room, x, y): + super().__init__(room, x, y) + + def update_room(self, rom, re: RoomEditor): + # Setup the warps + re.objects.insert(0, Object(5, 3, 0xE1)) # Hide an entrance tile under the tile where the egg will open. + re.objects.append(ObjectWarp(0x01, 0x08, 0x270, 0x50, 0x7C)) + re.entities.append((0, 0, 0xDE)) # egg song event + + egg_inside = RoomEditor(rom, 0x270) + egg_inside.getWarps()[0].room = self.room.x + egg_inside.store(rom) + + # Fix the alt room layout + alt = RoomEditor(rom, "Alt06") + tiles = re.getTileArray() + tiles[25] = 0xC1 + tiles[35] = 0xCB + alt.buildObjectList(tiles, reduce_size=True) + alt.store(rom) + + # Patch which room shows as Alt06 + rom.patch(0x00, 0x31F1, ASM("cp $06"), ASM(f"cp ${self.room.x:02x}")) + rom.patch(0x00, 0x31F5, ASM("ld a, [$D806]"), ASM(f"ld a, [${0xD800 + self.room.x:04x}]")) + rom.patch(0x20, 0x2DE6, ASM("cp $06"), ASM(f"cp ${self.room.x:02x}")) + rom.patch(0x20, 0x2DEA, ASM("ld a, [$D806]"), ASM(f"ld a, [${0xD800 + self.room.x:04x}]")) + rom.patch(0x19, 0x0D1A, ASM("ld hl, $D806"), ASM(f"ld hl, ${0xD800 + self.room.x:04x}")) + + def connect_logic(self, logic_location): + return + + def get_item_pool(self): + return {} diff --git a/worlds/ladx/LADXR/mapgen/locations/entrance_info.py b/worlds/ladx/LADXR/mapgen/locations/entrance_info.py new file mode 100644 index 0000000000..9de2b86101 --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/locations/entrance_info.py @@ -0,0 +1,341 @@ +from ...locations.birdKey import BirdKey +from ...locations.chest import Chest +from ...locations.faceKey import FaceKey +from ...locations.goldLeaf import GoldLeaf +from ...locations.heartPiece import HeartPiece +from ...locations.madBatter import MadBatter +from ...locations.song import Song +from ...locations.startItem import StartItem +from ...locations.tradeSequence import TradeSequenceItem +from ...locations.seashell import Seashell +from ...locations.shop import ShopItem +from ...locations.droppedKey import DroppedKey +from ...locations.witch import Witch +from ...logic import * +from ...logic.dungeon1 import Dungeon1 +from ...logic.dungeon2 import Dungeon2 +from ...logic.dungeon3 import Dungeon3 +from ...logic.dungeon4 import Dungeon4 +from ...logic.dungeon5 import Dungeon5 +from ...logic.dungeon6 import Dungeon6 +from ...logic.dungeon7 import Dungeon7 +from ...logic.dungeon8 import Dungeon8 +from ...logic.dungeonColor import DungeonColor + + +def one_way(loc, req=None): + res = Location() + loc.connect(res, req, one_way=True) + return res + + +class EntranceInfo: + def __init__(self, *, items=None, logic=None, exits=None): + self.items = items + self.logic = logic + self.exits = exits + + +INFO = { + "start_house": EntranceInfo(items={None: 1}, logic=lambda c, w, r: Location().add(StartItem())), + "d0": EntranceInfo( + items={None: 2, KEY9: 3, MAP9: 1, COMPASS9: 1, STONE_BEAK9: 1, NIGHTMARE_KEY9: 1}, + logic=lambda c, w, r: DungeonColor(c, w, r).entrance + ), + "d1": EntranceInfo( + items={None: 3, KEY1: 3, MAP1: 1, COMPASS1: 1, STONE_BEAK1: 1, NIGHTMARE_KEY1: 1, HEART_CONTAINER: 1, INSTRUMENT1: 1}, + logic=lambda c, w, r: Dungeon1(c, w, r).entrance + ), + "d2": EntranceInfo( + items={None: 3, KEY2: 5, MAP2: 1, COMPASS2: 1, STONE_BEAK2: 1, NIGHTMARE_KEY2: 1, HEART_CONTAINER: 1, INSTRUMENT2: 1}, + logic=lambda c, w, r: Dungeon2(c, w, r).entrance + ), + "d3": EntranceInfo( + items={None: 4, KEY3: 9, MAP3: 1, COMPASS3: 1, STONE_BEAK3: 1, NIGHTMARE_KEY3: 1, HEART_CONTAINER: 1, INSTRUMENT3: 1}, + logic=lambda c, w, r: Dungeon3(c, w, r).entrance + ), + "d4": EntranceInfo( + items={None: 4, KEY4: 5, MAP4: 1, COMPASS4: 1, STONE_BEAK4: 1, NIGHTMARE_KEY4: 1, HEART_CONTAINER: 1, INSTRUMENT4: 1}, + logic=lambda c, w, r: Dungeon4(c, w, r).entrance + ), + "d5": EntranceInfo( + items={None: 5, KEY5: 3, MAP5: 1, COMPASS5: 1, STONE_BEAK5: 1, NIGHTMARE_KEY5: 1, HEART_CONTAINER: 1, INSTRUMENT5: 1}, + logic=lambda c, w, r: Dungeon5(c, w, r).entrance + ), + "d6": EntranceInfo( + items={None: 6, KEY6: 3, MAP6: 1, COMPASS6: 1, STONE_BEAK6: 1, NIGHTMARE_KEY6: 1, HEART_CONTAINER: 1, INSTRUMENT6: 1}, + logic=lambda c, w, r: Dungeon6(c, w, r, raft_game_chest=False).entrance + ), + "d7": EntranceInfo( + items={None: 4, KEY7: 3, MAP7: 1, COMPASS7: 1, STONE_BEAK7: 1, NIGHTMARE_KEY7: 1, HEART_CONTAINER: 1, INSTRUMENT7: 1}, + logic=lambda c, w, r: Dungeon7(c, w, r).entrance + ), + "d8": EntranceInfo( + items={None: 6, KEY8: 7, MAP8: 1, COMPASS8: 1, STONE_BEAK8: 1, NIGHTMARE_KEY8: 1, HEART_CONTAINER: 1, INSTRUMENT8: 1}, + logic=lambda c, w, r: Dungeon8(c, w, r, back_entrance_heartpiece=False).entrance + ), + + "writes_cave_left": EntranceInfo( + items={None: 2}, + logic=lambda c, w, r: Location().connect( + Location().add(Chest(0x2AE)), OR(FEATHER, ROOSTER, HOOKSHOT) + ).connect( + Location().add(Chest(0x2AF)), POWER_BRACELET + ), + exits=[("writes_cave_right", lambda loc: loc)], + ), + "writes_cave_right": EntranceInfo(), + + "castle_main_entrance": EntranceInfo( + items={None: 2}, + logic=lambda c, w, r: Location().connect( + Location().add(GoldLeaf(0x2D2)), r.attack_hookshot_powder # in the castle, kill enemies + ).connect( + Location().add(GoldLeaf(0x2C5)), AND(BOMB, r.attack_hookshot_powder) # in the castle, bomb wall to show enemy + ), + exits=[("castle_upper_left", lambda loc: loc)], + ), + "castle_upper_left": EntranceInfo(), + + "castle_upper_right": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().connect(Location().add(GoldLeaf(0x2C6)), AND(POWER_BRACELET, r.attack_hookshot)), + ), + + "right_taltal_connector1": EntranceInfo( + logic=lambda c, w, r: Location(), + exits=[("right_taltal_connector2", lambda loc: loc)], + ), + "right_taltal_connector2": EntranceInfo(), + + "fire_cave_entrance": EntranceInfo( + logic=lambda c, w, r: Location(), + exits=[("fire_cave_exit", lambda loc: Location().connect(loc, COUNT(SHIELD, 2)))], + ), + "fire_cave_exit": EntranceInfo(), + + "graveyard_cave_left": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().connect(Location().add(HeartPiece(0x2DF)), OR(AND(BOMB, OR(HOOKSHOT, PEGASUS_BOOTS), FEATHER), ROOSTER)), + exits=[("graveyard_cave_right", lambda loc: Location().connect(loc, OR(FEATHER, ROOSTER)))], + ), + "graveyard_cave_right": EntranceInfo(), + + "raft_return_enter": EntranceInfo( + logic=lambda c, w, r: Location(), + exits=[("raft_return_exit", one_way)], + ), + "raft_return_exit": EntranceInfo(), + + "prairie_right_cave_top": EntranceInfo( + logic=lambda c, w, r: Location(), + exits=[("prairie_right_cave_bottom", lambda loc: loc), ("prairie_right_cave_high", lambda loc: Location().connect(loc, AND(BOMB, OR(FEATHER, ROOSTER))))], + ), + "prairie_right_cave_bottom": EntranceInfo(), + "prairie_right_cave_high": EntranceInfo(), + + "armos_maze_cave": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().add(Chest(0x2FC)), + ), + "right_taltal_connector3": EntranceInfo( + logic=lambda c, w, r: Location(), + exits=[("right_taltal_connector4", lambda loc: one_way(loc, AND(OR(FEATHER, ROOSTER), HOOKSHOT)))], + ), + "right_taltal_connector4": EntranceInfo(), + + "obstacle_cave_entrance": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().connect(Location().add(Chest(0x2BB)), AND(SWORD, OR(HOOKSHOT, ROOSTER))), + exits=[ + ("obstacle_cave_outside_chest", lambda loc: Location().connect(loc, SWORD)), + ("obstacle_cave_exit", lambda loc: Location().connect(loc, AND(SWORD, OR(PEGASUS_BOOTS, ROOSTER)))) + ], + ), + "obstacle_cave_outside_chest": EntranceInfo(), + "obstacle_cave_exit": EntranceInfo(), + + "d6_connector_entrance": EntranceInfo( + logic=lambda c, w, r: Location(), + exits=[("d6_connector_exit", lambda loc: Location().connect(loc, OR(AND(HOOKSHOT, OR(FLIPPERS, AND(FEATHER, PEGASUS_BOOTS))), ROOSTER)))], + ), + "d6_connector_exit": EntranceInfo(), + + "multichest_left": EntranceInfo( + logic=lambda c, w, r: Location(), + exits=[ + ("multichest_right", lambda loc: loc), + ("multichest_top", lambda loc: Location().connect(loc, BOMB)), + ], + ), + "multichest_right": EntranceInfo(), + "multichest_top": EntranceInfo(), + + "prairie_madbatter_connector_entrance": EntranceInfo( + logic=lambda c, w, r: Location(), + exits=[("prairie_madbatter_connector_exit", lambda loc: Location().connect(loc, FLIPPERS))], + ), + "prairie_madbatter_connector_exit": EntranceInfo(), + + "papahl_house_left": EntranceInfo( + logic=lambda c, w, r: Location(), + exits=[("papahl_house_right", lambda loc: loc)], + ), + "papahl_house_right": EntranceInfo(), + + "prairie_to_animal_connector": EntranceInfo( + logic=lambda c, w, r: Location(), + exits=[("animal_to_prairie_connector", lambda loc: Location().connect(loc, PEGASUS_BOOTS))], + ), + "animal_to_prairie_connector": EntranceInfo(), + + "castle_secret_entrance": EntranceInfo( + logic=lambda c, w, r: Location(), + exits=[("castle_secret_exit", lambda loc: Location().connect(loc, FEATHER))], + ), + "castle_secret_exit": EntranceInfo(), + + "papahl_entrance": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().add(Chest(0x28A)), + exits=[("papahl_exit", lambda loc: loc)], + ), + "papahl_exit": EntranceInfo(), + + "right_taltal_connector5": EntranceInfo( + logic=lambda c, w, r: Location(), + exits=[("right_taltal_connector6", lambda loc: loc)], + ), + "right_taltal_connector6": EntranceInfo(), + + "toadstool_entrance": EntranceInfo( + items={None: 2}, + logic=lambda c, w, r: Location().connect(Location().add(Chest(0x2BD)), SWORD).connect( # chest in forest cave on route to mushroom + Location().add(HeartPiece(0x2AB), POWER_BRACELET)), # piece of heart in the forest cave on route to the mushroom + exits=[("right_taltal_connector6", lambda loc: loc)], + ), + "toadstool_exit": EntranceInfo(), + + "richard_house": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().connect(Location().add(Chest(0x2C8)), AND(COUNT(GOLD_LEAF, 5), OR(FEATHER, HOOKSHOT, ROOSTER))), + exits=[("richard_maze", lambda loc: Location().connect(loc, COUNT(GOLD_LEAF, 5)))], + ), + "richard_maze": EntranceInfo(), + + "left_to_right_taltalentrance": EntranceInfo( + exits=[("left_taltal_entrance", lambda loc: one_way(loc, OR(HOOKSHOT, ROOSTER)))], + ), + "left_taltal_entrance": EntranceInfo(), + + "boomerang_cave": EntranceInfo(), # TODO boomerang gift + "trendy_shop": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().connect(Location().add(TradeSequenceItem(0x2A0, TRADING_ITEM_YOSHI_DOLL)), FOUND("RUPEES", 50)) + ), + "moblin_cave": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().connect(Location().add(Chest(0x2E2)), AND(r.attack_hookshot_powder, r.miniboss_requirements[w.miniboss_mapping["moblin_cave"]])) + ), + "prairie_madbatter": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().connect(Location().add(MadBatter(0x1E0)), MAGIC_POWDER) + ), + "ulrira": EntranceInfo(), + "rooster_house": EntranceInfo(), + "animal_house2": EntranceInfo(), + "animal_house4": EntranceInfo(), + "armos_fairy": EntranceInfo(), + "right_fairy": EntranceInfo(), + "photo_house": EntranceInfo(), + + "bird_cave": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().connect(Location().add(BirdKey()), OR(AND(FEATHER, COUNT(POWER_BRACELET, 2)), ROOSTER)) + ), + "mamu": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().connect(Location().add(Song(0x2FB)), AND(OCARINA, COUNT("RUPEES", 300))) + ), + "armos_temple": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().connect(Location().add(FaceKey()), r.miniboss_requirements[w.miniboss_mapping["armos_temple"]]) + ), + "animal_house1": EntranceInfo(), + "madambowwow": EntranceInfo(), + "library": EntranceInfo(), + "kennel": EntranceInfo( + items={None: 1, TRADING_ITEM_RIBBON: 1}, + logic=lambda c, w, r: Location().connect(Location().add(Seashell(0x2B2)), SHOVEL).connect(Location().add(TradeSequenceItem(0x2B2, TRADING_ITEM_DOG_FOOD)), TRADING_ITEM_RIBBON) + ), + "dream_hut": EntranceInfo( + items={None: 2}, + logic=lambda c, w, r: Location().connect(Location().add(Chest(0x2BF)), OR(SWORD, BOOMERANG, HOOKSHOT, FEATHER)).connect(Location().add(Chest(0x2BE)), AND(OR(SWORD, BOOMERANG, HOOKSHOT, FEATHER), PEGASUS_BOOTS)) + ), + "hookshot_cave": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().connect(Location().add(Chest(0x2B3)), OR(HOOKSHOT, ROOSTER)) + ), + "madbatter_taltal": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().connect(Location().add(MadBatter(0x1E2)), MAGIC_POWDER) + ), + "forest_madbatter": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().connect(Location().add(MadBatter(0x1E1)), MAGIC_POWDER) + ), + "banana_seller": EntranceInfo( + items={TRADING_ITEM_DOG_FOOD: 1}, + logic=lambda c, w, r: Location().connect(Location().add(TradeSequenceItem(0x2FE, TRADING_ITEM_BANANAS)), TRADING_ITEM_DOG_FOOD) + ), + "shop": EntranceInfo( + items={None: 2}, + logic=lambda c, w, r: Location().connect(Location().add(ShopItem(0)), COUNT("RUPEES", 200)).connect(Location().add(ShopItem(1)), COUNT("RUPEES", 980)) + ), + "ghost_house": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().connect(Location().add(Seashell(0x1E3)), POWER_BRACELET) + ), + "writes_house": EntranceInfo( + items={TRADING_ITEM_LETTER: 1}, + logic=lambda c, w, r: Location().connect(Location().add(TradeSequenceItem(0x2A8, TRADING_ITEM_BROOM)), TRADING_ITEM_LETTER) + ), + "animal_house3": EntranceInfo( + items={TRADING_ITEM_HIBISCUS: 1}, + logic=lambda c, w, r: Location().connect(Location().add(TradeSequenceItem(0x2D9, TRADING_ITEM_LETTER)), TRADING_ITEM_HIBISCUS) + ), + "animal_house5": EntranceInfo( + items={TRADING_ITEM_HONEYCOMB: 1}, + logic=lambda c, w, r: Location().connect(Location().add(TradeSequenceItem(0x2D7, TRADING_ITEM_PINEAPPLE)), TRADING_ITEM_HONEYCOMB) + ), + "crazy_tracy": EntranceInfo( + items={"MEDICINE2": 1}, + logic=lambda c, w, r: Location().connect(Location().add(KeyLocation("MEDICINE2")), FOUND("RUPEES", 50)) + ), + "rooster_grave": EntranceInfo( + logic=lambda c, w, r: Location().connect(Location().add(DroppedKey(0x1E4)), AND(OCARINA, SONG3)) + ), + "desert_cave": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().connect(Location().add(HeartPiece(0x1E8)), BOMB) + ), + "witch": EntranceInfo( + items={TOADSTOOL: 1}, + logic=lambda c, w, r: Location().connect(Location().add(Witch()), TOADSTOOL) + ), + "prairie_left_cave1": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().add(Chest(0x2CD)) + ), + "prairie_left_cave2": EntranceInfo( + items={None: 2}, + logic=lambda c, w, r: Location().connect(Location().add(Chest(0x2F4)), PEGASUS_BOOTS).connect(Location().add(HeartPiece(0x2E5)), AND(BOMB, PEGASUS_BOOTS)) + ), + "castle_jump_cave": EntranceInfo( + items={None: 1}, + logic=lambda c, w, r: Location().add(Chest(0x1FD)) + ), + "raft_house": EntranceInfo(), + "prairie_left_fairy": EntranceInfo(), + "seashell_mansion": EntranceInfo(), # TODO: Not sure if we can guarantee enough shells +} diff --git a/worlds/ladx/LADXR/mapgen/locations/seashell.py b/worlds/ladx/LADXR/mapgen/locations/seashell.py new file mode 100644 index 0000000000..521d0c500b --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/locations/seashell.py @@ -0,0 +1,172 @@ +from ..logic import Location, PEGASUS_BOOTS, SHOVEL +from .base import LocationBase +from ..tileset import solid_tiles, open_tiles, walkable_tiles +from ...roomEditor import RoomEditor +from ...assembler import ASM +from ...locations.all import Seashell +import random + + +class HiddenSeashell(LocationBase): + def __init__(self, room, x, y): + super().__init__(room, x, y) + if room.tiles[x + y * 10] not in (0x20, 0x5C): + if random.randint(0, 1): + room.tiles[x + y * 10] = 0x20 # rock + else: + room.tiles[x + y * 10] = 0x5C # bush + + def update_room(self, rom, re: RoomEditor): + re.entities.append((self.x, self.y, 0x3D)) + + def connect_logic(self, logic_location): + logic_location.add(Seashell(self.room.x + self.room.y * 16)) + + def get_item_pool(self): + return {None: 1} + + @staticmethod + def check_possible(room, reachable_map): + # Check if we can potentially place a hidden seashell here + # First see if we have a nice bush or rock to hide under + options = [] + for y in range(1, 7): + for x in range(1, 9): + if room.tiles[x + y * 10] not in {0x20, 0x5C}: + continue + idx = room.x * 10 + x + (room.y * 8 + y) * reachable_map.w + if reachable_map.area[idx] == -1: + continue + options.append((reachable_map.distance[idx], x, y)) + if not options: + # No existing bush, we can always add one. So find a nice spot + for y in range(1, 7): + for x in range(1, 9): + if room.tiles[x + y * 10] not in walkable_tiles: + continue + if room.tiles[x + y * 10] == 0x1E: # ocean edge + continue + idx = room.x * 10 + x + (room.y * 8 + y) * reachable_map.w + if reachable_map.area[idx] == -1: + continue + options.append((reachable_map.distance[idx], x, y)) + if not options: + return None + options.sort(reverse=True) + options = [(x, y) for d, x, y in options if d > options[0][0] - 4] + return random.choice(options) + + +class DigSeashell(LocationBase): + MAX_COUNT = 6 + + def __init__(self, room, x, y): + super().__init__(room, x, y) + if room.tileset_id == "beach": + room.tiles[x + y * 10] = 0x08 + for ox, oy in [(-1, 0), (1, 0), (0, -1), (0, 1)]: + if room.tiles[x + ox + (y + oy) * 10] != 0x1E: + room.tiles[x + ox + (y + oy) * 10] = 0x24 + else: + room.tiles[x + y * 10] = 0x04 + for ox, oy in [(-1, 0), (1, 0), (0, -1), (0, 1)]: + room.tiles[x + ox + (y + oy) * 10] = 0x0A + + def update_room(self, rom, re: RoomEditor): + re.entities.append((self.x, self.y, 0x3D)) + if rom.banks[0x03][0x2210] == 0xFF: + rom.patch(0x03, 0x220F, ASM("cp $FF"), ASM(f"cp ${self.room.x | (self.room.y << 4):02x}")) + elif rom.banks[0x03][0x2214] == 0xFF: + rom.patch(0x03, 0x2213, ASM("cp $FF"), ASM(f"cp ${self.room.x | (self.room.y << 4):02x}")) + elif rom.banks[0x03][0x2218] == 0xFF: + rom.patch(0x03, 0x2217, ASM("cp $FF"), ASM(f"cp ${self.room.x | (self.room.y << 4):02x}")) + elif rom.banks[0x03][0x221C] == 0xFF: + rom.patch(0x03, 0x221B, ASM("cp $FF"), ASM(f"cp ${self.room.x | (self.room.y << 4):02x}")) + elif rom.banks[0x03][0x2220] == 0xFF: + rom.patch(0x03, 0x221F, ASM("cp $FF"), ASM(f"cp ${self.room.x | (self.room.y << 4):02x}")) + elif rom.banks[0x03][0x2224] == 0xFF: + rom.patch(0x03, 0x2223, ASM("cp $FF"), ASM(f"cp ${self.room.x | (self.room.y << 4):02x}")) + + def connect_logic(self, logic_location): + logic_location.connect(Location().add(Seashell(self.room.x + self.room.y * 16)), SHOVEL) + + def get_item_pool(self): + return {None: 1} + + @staticmethod + def check_possible(room, reachable_map): + options = [] + for y in range(1, 7): + for x in range(1, 9): + if room.tiles[x + y * 10] not in walkable_tiles: + continue + if room.tiles[x - 1 + y * 10] not in walkable_tiles: + continue + if room.tiles[x + 1 + y * 10] not in walkable_tiles: + continue + if room.tiles[x + (y - 1) * 10] not in walkable_tiles: + continue + if room.tiles[x + (y + 1) * 10] not in walkable_tiles: + continue + idx = room.x * 10 + x + (room.y * 8 + y) * reachable_map.w + if reachable_map.area[idx] == -1: + continue + options.append((x, y)) + if not options: + return None + return random.choice(options) + + +class BonkSeashell(LocationBase): + MAX_COUNT = 2 + + def __init__(self, room, x, y): + super().__init__(room, x, y) + self.tree_x = x + self.tree_y = y + for offsetx, offsety in [(-1, 0), (-1, 1), (2, 0), (2, 1), (0, -1), (1, -1), (0, 2), (1, 2)]: + if room.tiles[x + offsetx + (y + offsety) * 10] in walkable_tiles: + self.x += offsetx + self.y += offsety + break + + def update_room(self, rom, re: RoomEditor): + re.entities.append((self.tree_x, self.tree_y, 0x3D)) + if rom.banks[0x03][0x0F04] == 0xFF: + rom.patch(0x03, 0x0F03, ASM("cp $FF"), ASM(f"cp ${self.room.x|(self.room.y<<4):02x}")) + elif rom.banks[0x03][0x0F08] == 0xFF: + rom.patch(0x03, 0x0F07, ASM("cp $FF"), ASM(f"cp ${self.room.x|(self.room.y<<4):02x}")) + else: + raise RuntimeError("To many bonk seashells") + + def connect_logic(self, logic_location): + logic_location.connect(Location().add(Seashell(self.room.x + self.room.y * 16)), PEGASUS_BOOTS) + + def get_item_pool(self): + return {None: 1} + + @staticmethod + def check_possible(room, reachable_map): + # Check if we can potentially place a hidden seashell here + # Find potential trees + options = [] + for y in range(1, 6): + for x in range(1, 8): + if room.tiles[x + y * 10] != 0x25: + continue + if room.tiles[x + y * 10 + 1] != 0x26: + continue + if room.tiles[x + y * 10 + 10] != 0x27: + continue + if room.tiles[x + y * 10 + 11] != 0x28: + continue + idx = room.x * 10 + x + (room.y * 8 + y) * reachable_map.w + top_reachable = reachable_map.area[idx - reachable_map.w] != -1 or reachable_map.area[idx - reachable_map.w + 1] != -1 + bottom_reachable = reachable_map.area[idx + reachable_map.w * 2] != -1 or reachable_map.area[idx + reachable_map.w * 2 + 1] != -1 + left_reachable = reachable_map.area[idx - 1] != -1 or reachable_map.area[idx + reachable_map.w - 1] != -1 + right_reachable = reachable_map.area[idx + 2] != -1 or reachable_map.area[idx + reachable_map.w + 2] != -1 + if (top_reachable and bottom_reachable) or (left_reachable and right_reachable): + options.append((x, y)) + if not options: + return None + return random.choice(options) diff --git a/worlds/ladx/LADXR/mapgen/logic.py b/worlds/ladx/LADXR/mapgen/logic.py new file mode 100644 index 0000000000..607a00c26f --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/logic.py @@ -0,0 +1,146 @@ +from .map import Map +from .locations.entrance import Entrance +from ..logic import * +from .tileset import walkable_tiles, entrance_tiles + + +class LogicGenerator: + def __init__(self, configuration_options, world_setup, requirements_settings, the_map: Map): + self.w = the_map.w * 10 + self.h = the_map.h * 8 + self.map = the_map + self.logic_map = [None] * (self.w * self.h) + self.location_lookup = {} + self.configuration_options = configuration_options + self.world_setup = world_setup + self.requirements_settings = requirements_settings + + self.entrance_map = {} + for room in the_map: + for location in room.locations: + self.location_lookup[(room.x * 10 + location.x, room.y * 8 + location.y)] = location + if isinstance(location, Entrance): + location.prepare_logic(configuration_options, world_setup, requirements_settings) + self.entrance_map[location.entrance_name] = location + + start = self.entrance_map["start_house"] + self.start = Location() + self.egg = self.start # TODO + self.nightmare = Location() + self.windfish = Location().connect(self.nightmare, AND(MAGIC_POWDER, SWORD, OR(BOOMERANG, BOW))) + self.fill_walkable(self.start, start.room.x * 10 + start.x, start.room.y * 8 + start.y) + + logic_str_map = {None: "."} + for y in range(self.h): + line = "" + for x in range(self.w): + if self.logic_map[x + y * self.w] not in logic_str_map: + logic_str_map[self.logic_map[x + y * self.w]] = chr(len(logic_str_map)+48) + line += logic_str_map[self.logic_map[x + y * self.w]] + print(line) + + for room in the_map: + for location in room.locations: + if self.logic_map[(room.x * 10 + location.x) + (room.y * 8 + location.y) * self.w] is None: + raise RuntimeError(f"Location not mapped to logic: {room} {location.__class__.__name__} {location.x} {location.y}") + + tmp = set() + def r(n): + if n in tmp: + return + tmp.add(n) + for item in n.items: + print(item) + for o, req in n.simple_connections: + r(o) + for o, req in n.gated_connections: + r(o) + r(self.start) + + def fill_walkable(self, location, x, y): + tile_options = walkable_tiles | entrance_tiles + for x, y in self.flood_fill_logic(location, tile_options, x, y): + if self.logic_map[x + y * self.w] is not None: + continue + tile = self.map.get_tile(x, y) + if tile == 0x5C: # bush + other_location = Location() + location.connect(other_location, self.requirements_settings.bush) + self.fill_bush(other_location, x, y) + elif tile == 0x20: # rock + other_location = Location() + location.connect(other_location, POWER_BRACELET) + self.fill_rock(other_location, x, y) + elif tile == 0xE8: # pit + if self.map.get_tile(x - 1, y) in tile_options and self.map.get_tile(x + 1, y) in tile_options: + if self.logic_map[x - 1 + y * self.w] == location and self.logic_map[x + 1 + y * self.w] is None: + other_location = Location().connect(location, FEATHER) + self.fill_walkable(other_location, x + 1, y) + if self.logic_map[x - 1 + y * self.w] is None and self.logic_map[x + 1 + y * self.w] == location: + other_location = Location().connect(location, FEATHER) + self.fill_walkable(other_location, x - 1, y) + if self.map.get_tile(x, y - 1) in tile_options and self.map.get_tile(x, y + 1) in tile_options: + if self.logic_map[x + (y - 1) * self.w] == location and self.logic_map[x + (y + 1) * self.w] is None: + other_location = Location().connect(location, FEATHER) + self.fill_walkable(other_location, x, y + 1) + if self.logic_map[x + (y - 1) * self.w] is None and self.logic_map[x + (y + 1) * self.w] == location: + other_location = Location().connect(location, FEATHER) + self.fill_walkable(other_location, x, y - 1) + + def fill_bush(self, location, x, y): + for x, y in self.flood_fill_logic(location, {0x5C}, x, y): + if self.logic_map[x + y * self.w] is not None: + continue + tile = self.map.get_tile(x, y) + if tile in walkable_tiles or tile in entrance_tiles: + other_location = Location() + location.connect(other_location, self.requirements_settings.bush) + self.fill_walkable(other_location, x, y) + + def fill_rock(self, location, x, y): + for x, y in self.flood_fill_logic(location, {0x20}, x, y): + if self.logic_map[x + y * self.w] is not None: + continue + tile = self.map.get_tile(x, y) + if tile in walkable_tiles or tile in entrance_tiles: + other_location = Location() + location.connect(other_location, POWER_BRACELET) + self.fill_walkable(other_location, x, y) + + def flood_fill_logic(self, location, tile_types, x, y): + assert self.map.get_tile(x, y) in tile_types + todo = [(x, y)] + entrance_todo = [] + + edge_set = set() + while todo: + x, y = todo.pop() + if self.map.get_tile(x, y) not in tile_types: + edge_set.add((x, y)) + continue + if self.logic_map[x + y * self.w] is not None: + continue + self.logic_map[x + y * self.w] = location + if (x, y) in self.location_lookup: + room_location = self.location_lookup[(x, y)] + result = room_location.connect_logic(location) + if result: + entrance_todo += result + + if x < self.w - 1 and self.logic_map[x + 1 + y * self.w] is None: + todo.append((x + 1, y)) + if x > 0 and self.logic_map[x - 1 + y * self.w] is None: + todo.append((x - 1, y)) + if y < self.h - 1 and self.logic_map[x + y * self.w + self.w] is None: + todo.append((x, y + 1)) + if y > 0 and self.logic_map[x + y * self.w - self.w] is None: + if self.map.get_tile(x, y - 1) == 0xA0: # Chest, can only be collected from the south + self.location_lookup[(x, y - 1)].connect_logic(location) + self.logic_map[x + (y - 1) * self.w] = location + todo.append((x, y - 1)) + + for entrance_name, logic_connection in entrance_todo: + entrance = self.entrance_map[entrance_name] + entrance.connect_logic(logic_connection) + self.fill_walkable(logic_connection, entrance.room.x * 10 + entrance.x, entrance.room.y * 8 + entrance.y) + return edge_set diff --git a/worlds/ladx/LADXR/mapgen/map.py b/worlds/ladx/LADXR/mapgen/map.py new file mode 100644 index 0000000000..0c9be58bcd --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/map.py @@ -0,0 +1,231 @@ +import random +from .tileset import solid_tiles, open_tiles +from ..locations.items import * + + +PRIMARY_ITEMS = [POWER_BRACELET, SHIELD, BOW, HOOKSHOT, MAGIC_ROD, PEGASUS_BOOTS, OCARINA, FEATHER, SHOVEL, MAGIC_POWDER, BOMB, SWORD, FLIPPERS, SONG1] +SECONDARY_ITEMS = [BOOMERANG, RED_TUNIC, BLUE_TUNIC, MAX_POWDER_UPGRADE, MAX_BOMBS_UPGRADE, MAX_ARROWS_UPGRADE, GEL] + +HORIZONTAL = 0 +VERTICAL = 1 + + +class RoomEdge: + def __init__(self, direction): + self.__solid = False + self.__open_range = None + self.direction = direction + self.__open_min = 2 if direction == HORIZONTAL else 1 + self.__open_max = 8 if direction == HORIZONTAL else 7 + + def force_solid(self): + self.__open_min = -1 + self.__open_max = -1 + self.__open_range = None + self.__solid = True + + def set_open_min(self, value): + if self.__open_min < 0: + return + self.__open_min = max(self.__open_min, value) + + def set_open_max(self, value): + if self.__open_max < 0: + return + self.__open_max = min(self.__open_max, value) + + def set_solid(self): + self.__open_range = None + self.__solid = True + + def can_open(self): + return self.__open_min > -1 + + def set_open(self): + cnt = random.randint(1, self.__open_max - self.__open_min) + if random.randint(1, 100) < 50: + cnt = 1 + offset = random.randint(self.__open_min, self.__open_max - cnt) + self.__open_range = (offset, offset + cnt) + self.__solid = False + + def is_solid(self): + return self.__solid + + def get_open_range(self): + return self.__open_range + + def seed(self, wfc, x, y): + for offset, cell in self.__cells(wfc, x, y): + if self.__open_range and self.__open_range[0] <= offset < self.__open_range[1]: + cell.init_options.intersection_update(open_tiles) + elif self.__solid: + cell.init_options.intersection_update(solid_tiles) + + def __cells(self, wfc, x, y): + if self.direction == HORIZONTAL: + for n in range(1, 9): + yield n, wfc.cell_data[(x + n, y)] + else: + for n in range(1, 7): + yield n, wfc.cell_data[(x, y + n)] + + +class RoomInfo: + def __init__(self, x, y): + self.x = x + self.y = y + self.tileset_id = "basic" + self.room_type = None + self.tiles = None + self.edge_left = None + self.edge_up = None + self.edge_right = RoomEdge(VERTICAL) + self.edge_down = RoomEdge(HORIZONTAL) + self.room_left = None + self.room_up = None + self.room_right = None + self.room_down = None + self.locations = [] + self.entities = [] + + def __repr__(self): + return f"Room<{self.x} {self.y}>" + + +class Map: + def __init__(self, w, h, tilesets): + self.w = w + self.h = h + self.tilesets = tilesets + self.__rooms = [RoomInfo(x, y) for y in range(h) for x in range(w)] + for x in range(w): + for y in range(h): + room = self.get(x, y) + if x == 0: + room.edge_left = RoomEdge(VERTICAL) + else: + room.edge_left = self.get(x - 1, y).edge_right + if y == 0: + room.edge_up = RoomEdge(HORIZONTAL) + else: + room.edge_up = self.get(x, y - 1).edge_down + if x > 0: + room.room_left = self.get(x - 1, y) + if x < w - 1: + room.room_right = self.get(x + 1, y) + if y > 0: + room.room_up = self.get(x, y - 1) + if y < h - 1: + room.room_down = self.get(x, y + 1) + for x in range(w): + self.get(x, 0).edge_up.set_solid() + self.get(x, h-1).edge_down.set_solid() + for y in range(h): + self.get(0, y).edge_left.set_solid() + self.get(w-1, y).edge_right.set_solid() + + def __iter__(self): + return iter(self.__rooms) + + def get(self, x, y) -> RoomInfo: + assert 0 <= x < self.w and 0 <= y < self.h, f"{x} {y}" + return self.__rooms[x + y * self.w] + + def get_tile(self, x, y): + return self.get(x // 10, y // 8).tiles[(x % 10) + (y % 8) * 10] + + def get_item_pool(self): + item_pool = {} + for room in self.__rooms: + for location in room.locations: + print(room, location.get_item_pool(), location.__class__.__name__) + for k, v in location.get_item_pool().items(): + item_pool[k] = item_pool.get(k, 0) + v + unmapped_count = item_pool.get(None, 0) + del item_pool[None] + for item in PRIMARY_ITEMS: + if item not in item_pool: + item_pool[item] = 1 + unmapped_count -= 1 + while item_pool[POWER_BRACELET] < 2: + item_pool[POWER_BRACELET] = item_pool.get(POWER_BRACELET, 0) + 1 + unmapped_count -= 1 + while item_pool[SHIELD] < 2: + item_pool[SHIELD] = item_pool.get(SHIELD, 0) + 1 + unmapped_count -= 1 + assert unmapped_count >= 0 + + for item in SECONDARY_ITEMS: + if unmapped_count > 0: + item_pool[item] = item_pool.get(item, 0) + 1 + unmapped_count -= 1 + + # Add a heart container per 10 items "spots" left. + heart_piece_count = unmapped_count // 10 + unmapped_count -= heart_piece_count * 4 + item_pool[HEART_PIECE] = item_pool.get(HEART_PIECE, 0) + heart_piece_count * 4 + + # Add the rest as rupees + item_pool[RUPEES_50] = item_pool.get(RUPEES_50, 0) + unmapped_count + return item_pool + + def dump(self): + for y in range(self.h): + for x in range(self.w): + if self.get(x, y).edge_right.is_solid(): + print(" |", end="") + elif self.get(x, y).edge_right.get_open_range(): + print(" ", end="") + else: + print(" ?", end="") + print() + for x in range(self.w): + if self.get(x, y).edge_down.is_solid(): + print("-+", end="") + elif self.get(x, y).edge_down.get_open_range(): + print(" +", end="") + else: + print("?+", end="") + print() + print() + + +class MazeGen: + UP = 0x01 + DOWN = 0x02 + LEFT = 0x04 + RIGHT = 0x08 + + def __init__(self, the_map: Map): + self.map = the_map + self.visited = set() + self.visit(0, 0) + + def visit(self, x, y): + self.visited.add((x, y)) + neighbours = self.get_neighbours(x, y) + while any((x, y) not in self.visited for x, y, d in neighbours): + x, y, d = random.choice(neighbours) + if (x, y) not in self.visited: + if d == self.RIGHT and self.map.get(x, y).edge_left.can_open(): + self.map.get(x, y).edge_left.set_open() + elif d == self.LEFT and self.map.get(x, y).edge_right.can_open(): + self.map.get(x, y).edge_right.set_open() + elif d == self.DOWN and self.map.get(x, y).edge_up.can_open(): + self.map.get(x, y).edge_up.set_open() + elif d == self.UP and self.map.get(x, y).edge_down.can_open(): + self.map.get(x, y).edge_down.set_open() + self.visit(x, y) + + def get_neighbours(self, x, y): + neighbours = [] + if x > 0: + neighbours.append((x - 1, y, self.LEFT)) + if x < self.map.w - 1: + neighbours.append((x + 1, y, self.RIGHT)) + if y > 0: + neighbours.append((x, y - 1, self.UP)) + if y < self.map.h - 1: + neighbours.append((x, y + 1, self.DOWN)) + return neighbours diff --git a/worlds/ladx/LADXR/mapgen/roomgen.py b/worlds/ladx/LADXR/mapgen/roomgen.py new file mode 100644 index 0000000000..189bb25d72 --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/roomgen.py @@ -0,0 +1,78 @@ +from .map import Map +from .roomtype.town import Town +from .roomtype.mountain import Mountain, MountainEgg +from .roomtype.forest import Forest +from .roomtype.base import RoomType +from .roomtype.water import Water, Beach +import random + + +def is_area_clear(the_map: Map, x, y, w, h): + for y0 in range(y, y+h): + for x0 in range(x, x+w): + if 0 <= x0 < the_map.w and 0 <= y0 < the_map.h: + if the_map.get(x0, y0).room_type is not None: + return False + return True + + +def find_random_clear_area(the_map: Map, w, h, *, tries): + for n in range(tries): + x = random.randint(0, the_map.w - w) + y = random.randint(0, the_map.h - h) + if is_area_clear(the_map, x - 1, y - 1, w + 2, h + 2): + return x, y + return None, None + + +def setup_room_types(the_map: Map): + # Always make the rop row mountains. + egg_x = the_map.w // 2 + for x in range(the_map.w): + if x == egg_x: + MountainEgg(the_map.get(x, 0)) + else: + Mountain(the_map.get(x, 0)) + + # Add some beach. + width = the_map.w if random.random() < 0.5 else random.randint(max(2, the_map.w // 4), the_map.w // 2) + beach_x = 0 # current tileset doesn't allow anything else + for x in range(beach_x, beach_x+width): + # Beach(the_map.get(x, the_map.h - 2)) + Beach(the_map.get(x, the_map.h - 1)) + the_map.get(beach_x + width - 1, the_map.h - 1).edge_right.force_solid() + + town_x, town_y = find_random_clear_area(the_map, 2, 2, tries=20) + if town_x is not None: + for y in range(town_y, town_y + 2): + for x in range(town_x, town_x + 2): + Town(the_map.get(x, y)) + + forest_w, forest_h = 2, 2 + if random.random() < 0.5: + forest_w += 1 + else: + forest_h += 1 + forest_x, forest_y = find_random_clear_area(the_map, forest_w, forest_h, tries=20) + if forest_x is None: + forest_w, forest_h = 2, 2 + forest_x, forest_y = find_random_clear_area(the_map, forest_w, forest_h, tries=20) + if forest_x is not None: + for y in range(forest_y, forest_y + forest_h): + for x in range(forest_x, forest_x + forest_w): + Forest(the_map.get(x, y)) + + # for n in range(5): + # water_w, water_h = 2, 1 + # if random.random() < 0.5: + # water_w, water_h = water_h, water_w + # water_x, water_y = find_random_clear_area(the_map, water_w, water_h, tries=20) + # if water_x is not None: + # for y in range(water_y, water_y + water_h): + # for x in range(water_x, water_x + water_w): + # Water(the_map.get(x, y)) + + for y in range(the_map.h): + for x in range(the_map.w): + if the_map.get(x, y).room_type is None: + RoomType(the_map.get(x, y)) diff --git a/worlds/ladx/LADXR/mapgen/roomtype/base.py b/worlds/ladx/LADXR/mapgen/roomtype/base.py new file mode 100644 index 0000000000..b524c4fb23 --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/roomtype/base.py @@ -0,0 +1,54 @@ +from ..tileset import open_tiles + + +def plot_line(x0, y0, x1, y1): + dx = abs(x1 - x0) + sx = 1 if x0 < x1 else -1 + dy = -abs(y1 - y0) + sy = 1 if y0 < y1 else -1 + error = dx + dy + + yield x0, y0 + while True: + if x0 == x1 and y0 == y1: + break + e2 = 2 * error + if e2 >= dy: + error = error + dy + x0 = x0 + sx + yield x0, y0 + if e2 <= dx: + error = error + dx + y0 = y0 + sy + yield x0, y0 + + yield x1, y1 + + +class RoomType: + def __init__(self, room): + self.room = room + room.room_type = self + + def seed(self, wfc, x, y): + open_points = [] + r = self.room.edge_left.get_open_range() + if r: + open_points.append((x + 1, y + (r[0] + r[1]) // 2)) + r = self.room.edge_right.get_open_range() + if r: + open_points.append((x + 8, y + (r[0] + r[1]) // 2)) + r = self.room.edge_up.get_open_range() + if r: + open_points.append((x + (r[0] + r[1]) // 2, y + 1)) + r = self.room.edge_down.get_open_range() + if r: + open_points.append((x + (r[0] + r[1]) // 2, y + 6)) + if len(open_points) < 2: + return + mid_x = sum([x for x, y in open_points]) // len(open_points) + mid_y = sum([y for x, y in open_points]) // len(open_points) + + for x0, y0 in open_points: + for px, py in plot_line(x0, y0, mid_x, mid_y): + wfc.cell_data[(px, py)].init_options.intersection_update(open_tiles) diff --git a/worlds/ladx/LADXR/mapgen/roomtype/forest.py b/worlds/ladx/LADXR/mapgen/roomtype/forest.py new file mode 100644 index 0000000000..25c71eefc8 --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/roomtype/forest.py @@ -0,0 +1,28 @@ +from .base import RoomType +from ..tileset import open_tiles +import random + + +class Forest(RoomType): + def __init__(self, room): + super().__init__(room) + room.tileset_id = "forest" + + def seed(self, wfc, x, y): + if self.room.room_up and isinstance(self.room.room_up.room_type, Forest) and self.room.edge_up.get_open_range() is None: + self.room.edge_up.set_solid() + if self.room.room_left and isinstance(self.room.room_left.room_type, Forest) and self.room.edge_left.get_open_range() is None: + self.room.edge_left.set_solid() + + if self.room.room_up and isinstance(self.room.room_up.room_type, Forest) and random.random() < 0.5: + door_x, door_y = x + 5 + random.randint(-1, 1), y + 3 + random.randint(-1, 1) + wfc.cell_data[(door_x, door_y)].init_options.intersection_update({0xE3}) + self.room.edge_up.set_solid() + if self.room.edge_left.get_open_range() is not None: + for x0 in range(x + 1, door_x): + wfc.cell_data[(x0, door_y + 1)].init_options.intersection_update(open_tiles) + if self.room.edge_right.get_open_range() is not None: + for x0 in range(door_x + 1, x + 10): + wfc.cell_data[(x0, door_y + 1)].init_options.intersection_update(open_tiles) + else: + super().seed(wfc, x, y) diff --git a/worlds/ladx/LADXR/mapgen/roomtype/mountain.py b/worlds/ladx/LADXR/mapgen/roomtype/mountain.py new file mode 100644 index 0000000000..d80d5dc581 --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/roomtype/mountain.py @@ -0,0 +1,38 @@ +from .base import RoomType +from ..locations.entrance import EggEntrance +import random + + +class Mountain(RoomType): + def __init__(self, room): + super().__init__(room) + room.tileset_id = "mountains" + room.edge_left.set_open_min(3) + room.edge_right.set_open_min(3) + + def seed(self, wfc, x, y): + super().seed(wfc, x, y) + if y == 0: + if x == 0: + wfc.cell_data[(0, 1)].init_options.intersection_update({0}) + if x == wfc.w - 10: + wfc.cell_data[(x + 9, 1)].init_options.intersection_update({0}) + wfc.cell_data[(x + random.randint(3, 6), random.randint(0, 1))].init_options.intersection_update({0}) + + +class MountainEgg(RoomType): + def __init__(self, room): + super().__init__(room) + room.tileset_id = "egg" + room.edge_left.force_solid() + room.edge_right.force_solid() + room.edge_down.set_open_min(5) + room.edge_down.set_open_max(6) + + EggEntrance(room, 5, 4) + + def seed(self, wfc, x, y): + super().seed(wfc, x, y) + wfc.cell_data[(x + 2, y + 1)].init_options.intersection_update({0x00}) + wfc.cell_data[(x + 2, y + 2)].init_options.intersection_update({0xEF}) + wfc.cell_data[(x + 5, y + 3)].init_options.intersection_update({0xAA}) diff --git a/worlds/ladx/LADXR/mapgen/roomtype/town.py b/worlds/ladx/LADXR/mapgen/roomtype/town.py new file mode 100644 index 0000000000..2553a13308 --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/roomtype/town.py @@ -0,0 +1,16 @@ +from .base import RoomType +from ..tileset import solid_tiles +import random + + +class Town(RoomType): + def __init__(self, room): + super().__init__(room) + room.tileset_id = "town" + + def seed(self, wfc, x, y): + ex = x + 5 + random.randint(-1, 1) + ey = y + 3 + random.randint(-1, 1) + wfc.cell_data[(ex, ey)].init_options.intersection_update({0xE2}) + wfc.cell_data[(ex - 1, ey - 1)].init_options.intersection_update(solid_tiles) + wfc.cell_data[(ex + 1, ey - 1)].init_options.intersection_update(solid_tiles) diff --git a/worlds/ladx/LADXR/mapgen/roomtype/water.py b/worlds/ladx/LADXR/mapgen/roomtype/water.py new file mode 100644 index 0000000000..e3f4830ecf --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/roomtype/water.py @@ -0,0 +1,30 @@ +from .base import RoomType +import random + + +class Water(RoomType): + def __init__(self, room): + super().__init__(room) + room.tileset_id = "water" + + # def seed(self, wfc, x, y): + # wfc.cell_data[(x + 5 + random.randint(-1, 1), y + 3 + random.randint(-1, 1))].init_options.intersection_update({0x0E}) + + +class Beach(RoomType): + def __init__(self, room): + super().__init__(room) + room.tileset_id = "beach" + if self.room.room_down is None: + self.room.edge_left.set_open_max(4) + self.room.edge_right.set_open_max(4) + self.room.edge_up.set_open_min(4) + self.room.edge_up.set_open_max(6) + + def seed(self, wfc, x, y): + if self.room.room_down is None: + for n in range(1, 9): + wfc.cell_data[(x + n, y + 5)].init_options.intersection_update({0x1E}) + for n in range(1, 9): + wfc.cell_data[(x + n, y + 7)].init_options.intersection_update({0x1F}) + super().seed(wfc, x, y) \ No newline at end of file diff --git a/worlds/ladx/LADXR/mapgen/tileset.py b/worlds/ladx/LADXR/mapgen/tileset.py new file mode 100644 index 0000000000..b634c30223 --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/tileset.py @@ -0,0 +1,253 @@ +from typing import Dict, Set +from ..roomEditor import RoomEditor + + +animated_tiles = {0x0E, 0x1B, 0x1E, 0x1F, 0x44, 0x91, 0xCF, 0xD0, 0xD1, 0xD2, 0xD9, 0xDC, 0xE9, 0xEB, 0xEC, 0xED, 0xEE, 0xEF} +entrance_tiles = {0xE1, 0xE2, 0xE3, 0xBA, 0xC6} + +solid_tiles = set() +open_tiles = set() +walkable_tiles = set() +vertical_edge_tiles = set() +horizontal_edge_tiles = set() + + +class TileInfo: + def __init__(self, key): + self.key = key + self.up = set() + self.right = set() + self.down = set() + self.left = set() + self.up_freq = {} + self.right_freq = {} + self.down_freq = {} + self.left_freq = {} + self.frequency = 0 + + def copy(self): + result = TileInfo(self.key) + result.up = self.up.copy() + result.right = self.right.copy() + result.down = self.down.copy() + result.left = self.left.copy() + result.up_freq = self.up_freq.copy() + result.right_freq = self.right_freq.copy() + result.down_freq = self.down_freq.copy() + result.left_freq = self.left_freq.copy() + result.frequency = self.frequency + return result + + def remove(self, tile_id): + if tile_id in self.up: + self.up.remove(tile_id) + del self.up_freq[tile_id] + if tile_id in self.down: + self.down.remove(tile_id) + del self.down_freq[tile_id] + if tile_id in self.left: + self.left.remove(tile_id) + del self.left_freq[tile_id] + if tile_id in self.right: + self.right.remove(tile_id) + del self.right_freq[tile_id] + + def update(self, other: "TileInfo", tile_filter: Set[int]): + self.frequency += other.frequency + self.up.update(other.up.intersection(tile_filter)) + self.down.update(other.down.intersection(tile_filter)) + self.left.update(other.left.intersection(tile_filter)) + self.right.update(other.right.intersection(tile_filter)) + for k, v in other.up_freq.items(): + if k not in tile_filter: + continue + self.up_freq[k] = self.up_freq.get(k, 0) + v + for k, v in other.down_freq.items(): + if k not in tile_filter: + continue + self.down_freq[k] = self.down_freq.get(k, 0) + v + for k, v in other.left_freq.items(): + if k not in tile_filter: + continue + self.left_freq[k] = self.left_freq.get(k, 0) + v + for k, v in other.down_freq.items(): + if k not in tile_filter: + continue + self.right_freq[k] = self.right_freq.get(k, 0) + v + + def __repr__(self): + return f"<{self.key}>\n U{[f'{n:02x}' for n in self.up]}\n R{[f'{n:02x}' for n in self.right]}\n D{[f'{n:02x}' for n in self.down]}\n L{[f'{n:02x}' for n in self.left]}>" + + +class TileSet: + def __init__(self, *, main_id=None, animation_id=None): + self.main_id = main_id + self.animation_id = animation_id + self.palette_id = None + self.attr_bank = None + self.attr_addr = None + self.tiles: Dict[int, "TileInfo"] = {} + self.all: Set[int] = set() + + def copy(self) -> "TileSet": + result = TileSet(main_id=self.main_id, animation_id=self.animation_id) + for k, v in self.tiles.items(): + result.tiles[k] = v.copy() + result.all = self.all.copy() + return result + + def remove(self, tile_id): + self.all.remove(tile_id) + del self.tiles[tile_id] + for k, v in self.tiles.items(): + v.remove(tile_id) + + # Look at the "other" tileset and merge information about tiles known in this tileset + def learn_from(self, other: "TileSet"): + for key, other_info in other.tiles.items(): + if key not in self.all: + continue + self.tiles[key].update(other_info, self.all) + + def combine(self, other: "TileSet"): + if other.main_id and not self.main_id: + self.main_id = other.main_id + if other.animation_id and not self.animation_id: + self.animation_id = other.animation_id + for key, other_info in other.tiles.items(): + if key not in self.all: + self.tiles[key] = other_info.copy() + else: + self.tiles[key].update(other_info, self.all) + self.all.update(other.all) + + +def loadTileInfo(rom) -> Dict[str, TileSet]: + for n in range(0x100): + physics_flag = rom.banks[8][0x0AD4 + n] + if n == 0xEF: + physics_flag = 0x01 # One of the sky tiles is marked as a pit instead of solid, which messes with the generation of sky + if physics_flag in {0x00, 0x05, 0x06, 0x07}: + open_tiles.add(n) + walkable_tiles.add(n) + vertical_edge_tiles.add(n) + horizontal_edge_tiles.add(n) + elif physics_flag in {0x01, 0x04, 0x60}: + solid_tiles.add(n) + vertical_edge_tiles.add(n) + horizontal_edge_tiles.add(n) + elif physics_flag in {0x08}: # Bridge + open_tiles.add(n) + walkable_tiles.add(n) + elif physics_flag in {0x02}: # Stairs + open_tiles.add(n) + walkable_tiles.add(n) + horizontal_edge_tiles.add(n) + elif physics_flag in {0x03}: # Entrances + open_tiles.add(n) + elif physics_flag in {0x30}: # bushes/rocks + open_tiles.add(n) + elif physics_flag in {0x50}: # pits + open_tiles.add(n) + world_tiles = {} + for ry in range(0, 16): + for rx in range(0, 16): + tileset_id = rom.banks[0x3F][0x3F00 + rx + (ry << 4)] + re = RoomEditor(rom, rx | (ry << 4)) + tiles = re.getTileArray() + for y in range(8): + for x in range(10): + tile_id = tiles[x+y*10] + world_tiles[(rx*10+x, ry*8+y)] = (tile_id, tileset_id, re.animation_id | 0x100) + + # Fix up wrong tiles + world_tiles[(150, 24)] = (0x2A, world_tiles[(150, 24)][1], world_tiles[(150, 24)][2]) # Left of the raft house, a tree has the wrong tile. + + rom_tilesets: Dict[int, TileSet] = {} + for (x, y), (key, tileset_id, animation_id) in world_tiles.items(): + if key in animated_tiles: + if animation_id not in rom_tilesets: + rom_tilesets[animation_id] = TileSet(animation_id=animation_id&0xFF) + tileset = rom_tilesets[animation_id] + else: + if tileset_id not in rom_tilesets: + rom_tilesets[tileset_id] = TileSet(main_id=tileset_id) + tileset = rom_tilesets[tileset_id] + tileset.all.add(key) + if key not in tileset.tiles: + tileset.tiles[key] = TileInfo(key) + ti = tileset.tiles[key] + ti.frequency += 1 + if (x, y - 1) in world_tiles: + tile_id = world_tiles[(x, y - 1)][0] + ti.up.add(tile_id) + ti.up_freq[tile_id] = ti.up_freq.get(tile_id, 0) + 1 + if (x + 1, y) in world_tiles: + tile_id = world_tiles[(x + 1, y)][0] + ti.right.add(tile_id) + ti.right_freq[tile_id] = ti.right_freq.get(tile_id, 0) + 1 + if (x, y + 1) in world_tiles: + tile_id = world_tiles[(x, y + 1)][0] + ti.down.add(tile_id) + ti.down_freq[tile_id] = ti.down_freq.get(tile_id, 0) + 1 + if (x - 1, y) in world_tiles: + tile_id = world_tiles[(x - 1, y)][0] + ti.left.add(tile_id) + ti.left_freq[tile_id] = ti.left_freq.get(tile_id, 0) + 1 + + tilesets = { + "basic": rom_tilesets[0x0F].copy() + } + for key, tileset in rom_tilesets.items(): + tilesets["basic"].learn_from(tileset) + tilesets["mountains"] = rom_tilesets[0x3E].copy() + tilesets["mountains"].combine(rom_tilesets[0x10B]) + tilesets["mountains"].remove(0xB6) # Remove the raft house roof + tilesets["mountains"].remove(0xB7) # Remove the raft house roof + tilesets["mountains"].remove(0x66) # Remove the raft house roof + tilesets["mountains"].learn_from(rom_tilesets[0x1C]) + tilesets["mountains"].learn_from(rom_tilesets[0x3C]) + tilesets["mountains"].learn_from(rom_tilesets[0x30]) + tilesets["mountains"].palette_id = 0x15 + tilesets["mountains"].attr_bank = 0x27 + tilesets["mountains"].attr_addr = 0x5A20 + + tilesets["egg"] = rom_tilesets[0x3C].copy() + tilesets["egg"].combine(tilesets["mountains"]) + tilesets["egg"].palette_id = 0x13 + tilesets["egg"].attr_bank = 0x27 + tilesets["egg"].attr_addr = 0x5620 + + tilesets["forest"] = rom_tilesets[0x20].copy() + tilesets["forest"].palette_id = 0x00 + tilesets["forest"].attr_bank = 0x25 + tilesets["forest"].attr_addr = 0x4000 + + tilesets["town"] = rom_tilesets[0x26].copy() + tilesets["town"].combine(rom_tilesets[0x103]) + tilesets["town"].palette_id = 0x03 + tilesets["town"].attr_bank = 0x25 + tilesets["town"].attr_addr = 0x4C00 + + tilesets["swamp"] = rom_tilesets[0x36].copy() + tilesets["swamp"].combine(rom_tilesets[0x103]) + tilesets["swamp"].palette_id = 0x0E + tilesets["swamp"].attr_bank = 0x22 + tilesets["swamp"].attr_addr = 0x7400 + + tilesets["beach"] = rom_tilesets[0x22].copy() + tilesets["beach"].combine(rom_tilesets[0x102]) + tilesets["beach"].palette_id = 0x01 + tilesets["beach"].attr_bank = 0x22 + tilesets["beach"].attr_addr = 0x5000 + + tilesets["water"] = rom_tilesets[0x3E].copy() + tilesets["water"].combine(rom_tilesets[0x103]) + tilesets["water"].learn_from(tilesets["basic"]) + tilesets["water"].remove(0x7A) + tilesets["water"].remove(0xC8) + tilesets["water"].palette_id = 0x09 + tilesets["water"].attr_bank = 0x22 + tilesets["water"].attr_addr = 0x6400 + + return tilesets diff --git a/worlds/ladx/LADXR/mapgen/util.py b/worlds/ladx/LADXR/mapgen/util.py new file mode 100644 index 0000000000..ab22755b2e --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/util.py @@ -0,0 +1,5 @@ + +def xyrange(w, h): + for y in range(h): + for x in range(w): + yield x, y diff --git a/worlds/ladx/LADXR/mapgen/wfc.py b/worlds/ladx/LADXR/mapgen/wfc.py new file mode 100644 index 0000000000..e40b6af127 --- /dev/null +++ b/worlds/ladx/LADXR/mapgen/wfc.py @@ -0,0 +1,250 @@ +from .tileset import TileSet, solid_tiles, open_tiles, vertical_edge_tiles, horizontal_edge_tiles +from .map import Map +from typing import Set +import random + + +class ContradictionException(Exception): + def __init__(self, x, y): + self.x = x + self.y = y + + +class Cell: + def __init__(self, x, y, tileset: TileSet, options: Set[int]): + self.x = x + self.y = y + self.tileset = tileset + self.init_options = options + self.options = None + self.result = None + + def __set_new_options(self, new_options): + if new_options != self.options: + if self.result is not None: + raise ContradictionException(self.x, self.y) + if not new_options: + raise ContradictionException(self.x, self.y) + self.options = new_options + return True + return False + + def update_options_up(self, cell: "Cell") -> bool: + new_options = set() + for tile in cell.options: + new_options.update(cell.tileset.tiles[tile].up) + new_options.intersection_update(self.options) + if (self.y % 8) == 7: + if cell.options.issubset(solid_tiles): + new_options.intersection_update(solid_tiles) + if cell.options.issubset(open_tiles): + new_options.intersection_update(open_tiles) + return self.__set_new_options(new_options) + + def update_options_right(self, cell: "Cell") -> bool: + new_options = set() + for tile in cell.options: + new_options.update(cell.tileset.tiles[tile].right) + new_options.intersection_update(self.options) + if (self.x % 10) == 0: + if cell.options.issubset(solid_tiles): + new_options.intersection_update(solid_tiles) + if cell.options.issubset(open_tiles): + new_options.intersection_update(open_tiles) + return self.__set_new_options(new_options) + + def update_options_down(self, cell: "Cell") -> bool: + new_options = set() + for tile in cell.options: + new_options.update(cell.tileset.tiles[tile].down) + new_options.intersection_update(self.options) + if (self.y % 8) == 0: + if cell.options.issubset(solid_tiles): + new_options.intersection_update(solid_tiles) + if cell.options.issubset(open_tiles): + new_options.intersection_update(open_tiles) + return self.__set_new_options(new_options) + + def update_options_left(self, cell: "Cell") -> bool: + new_options = set() + for tile in cell.options: + new_options.update(cell.tileset.tiles[tile].left) + new_options.intersection_update(self.options) + if (self.x % 10) == 9: + if cell.options.issubset(solid_tiles): + new_options.intersection_update(solid_tiles) + if cell.options.issubset(open_tiles): + new_options.intersection_update(open_tiles) + return self.__set_new_options(new_options) + + def __repr__(self): + return f"Cell<{self.options}>" + + +class WFCMap: + def __init__(self, the_map: Map, tilesets, *, step_callback=None): + self.cell_data = {} + self.on_step = step_callback + self.w = the_map.w * 10 + self.h = the_map.h * 8 + + for y in range(self.h): + for x in range(self.w): + tileset = tilesets[the_map.get(x//10, y//8).tileset_id] + new_cell = Cell(x, y, tileset, tileset.all.copy()) + self.cell_data[(new_cell.x, new_cell.y)] = new_cell + for y in range(self.h): + self.cell_data[(0, y)].init_options.intersection_update(solid_tiles) + self.cell_data[(self.w-1, y)].init_options.intersection_update(solid_tiles) + for x in range(self.w): + self.cell_data[(x, 0)].init_options.intersection_update(solid_tiles) + self.cell_data[(x, self.h-1)].init_options.intersection_update(solid_tiles) + + for x in range(0, self.w, 10): + for y in range(self.h): + self.cell_data[(x, y)].init_options.intersection_update(vertical_edge_tiles) + for x in range(9, self.w, 10): + for y in range(self.h): + self.cell_data[(x, y)].init_options.intersection_update(vertical_edge_tiles) + for y in range(0, self.h, 8): + for x in range(self.w): + self.cell_data[(x, y)].init_options.intersection_update(horizontal_edge_tiles) + for y in range(7, self.h, 8): + for x in range(self.w): + self.cell_data[(x, y)].init_options.intersection_update(horizontal_edge_tiles) + + for sy in range(the_map.h): + for sx in range(the_map.w): + the_map.get(sx, sy).room_type.seed(self, sx*10, sy*8) + + for sy in range(the_map.h): + for sx in range(the_map.w): + room = the_map.get(sx, sy) + room.edge_left.seed(self, sx * 10, sy * 8) + room.edge_right.seed(self, sx * 10 + 9, sy * 8) + room.edge_up.seed(self, sx * 10, sy * 8) + room.edge_down.seed(self, sx * 10, sy * 8 + 7) + + def initialize(self): + for y in range(self.h): + for x in range(self.w): + cell = self.cell_data[x, y] + cell.options = cell.init_options.copy() + if self.on_step: + self.on_step(self) + propegation_set = set() + for y in range(self.h): + for x in range(self.w): + propegation_set.add((x, y)) + self.propegate(propegation_set) + for y in range(self.h): + for x in range(self.w): + cell = self.cell_data[x, y] + cell.init_options = cell.options.copy() + + def clear(self): + for y in range(self.h): + for x in range(self.w): + cell = self.cell_data[(x, y)] + if cell.result is None: + cell.options = cell.init_options.copy() + + propegation_set = set() + for y in range(self.h): + for x in range(self.w): + cell = self.cell_data[(x, y)] + if cell.result is not None: + propegation_set.add((x, y)) + self.propegate(propegation_set) + + def random_pick(self, cell): + pick_list = list(cell.options) + if not pick_list: + raise ContradictionException(cell.x, cell.y) + freqs = {} + if (cell.x - 1, cell.y) in self.cell_data and len(self.cell_data[(cell.x - 1, cell.y)].options) == 1: + tile_id = next(iter(self.cell_data[(cell.x - 1, cell.y)].options)) + for k, v in self.cell_data[(cell.x - 1, cell.y)].tileset.tiles[tile_id].right_freq.items(): + freqs[k] = freqs.get(k, 0) + v + if (cell.x + 1, cell.y) in self.cell_data and len(self.cell_data[(cell.x + 1, cell.y)].options) == 1: + tile_id = next(iter(self.cell_data[(cell.x + 1, cell.y)].options)) + for k, v in self.cell_data[(cell.x + 1, cell.y)].tileset.tiles[tile_id].left_freq.items(): + freqs[k] = freqs.get(k, 0) + v + if (cell.x, cell.y - 1) in self.cell_data and len(self.cell_data[(cell.x, cell.y - 1)].options) == 1: + tile_id = next(iter(self.cell_data[(cell.x, cell.y - 1)].options)) + for k, v in self.cell_data[(cell.x, cell.y - 1)].tileset.tiles[tile_id].down_freq.items(): + freqs[k] = freqs.get(k, 0) + v + if (cell.x, cell.y + 1) in self.cell_data and len(self.cell_data[(cell.x, cell.y + 1)].options) == 1: + tile_id = next(iter(self.cell_data[(cell.x, cell.y + 1)].options)) + for k, v in self.cell_data[(cell.x, cell.y + 1)].tileset.tiles[tile_id].up_freq.items(): + freqs[k] = freqs.get(k, 0) + v + if freqs: + weights_list = [freqs.get(n, 1) for n in pick_list] + else: + weights_list = [cell.tileset.tiles[n].frequency for n in pick_list] + return random.choices(pick_list, weights_list)[0] + + def build(self, start_x, start_y, w, h): + cell_todo_list = [] + for y in range(start_y, start_y + h): + for x in range(start_x, start_x+w): + cell_todo_list.append(self.cell_data[(x, y)]) + + while cell_todo_list: + cell_todo_list.sort(key=lambda c: len(c.options)) + l0 = len(cell_todo_list[0].options) + idx = 1 + while idx < len(cell_todo_list) and len(cell_todo_list[idx].options) == l0: + idx += 1 + idx = random.randint(0, idx - 1) + cell = cell_todo_list[idx] + if self.on_step: + self.on_step(self, cur=(cell.x, cell.y)) + pick = self.random_pick(cell) + cell_todo_list.pop(idx) + cell.options = {pick} + self.propegate({(cell.x, cell.y)}) + + for y in range(start_y, start_y + h): + for x in range(start_x, start_x + w): + self.cell_data[(x, y)].result = next(iter(self.cell_data[(x, y)].options)) + + def propegate(self, propegation_set): + while propegation_set: + xy = next(iter(propegation_set)) + propegation_set.remove(xy) + + cell = self.cell_data[xy] + if not cell.options: + raise ContradictionException(cell.x, cell.y) + x, y = xy + if (x, y + 1) in self.cell_data and self.cell_data[(x, y + 1)].update_options_down(cell): + propegation_set.add((x, y + 1)) + if (x + 1, y) in self.cell_data and self.cell_data[(x + 1, y)].update_options_right(cell): + propegation_set.add((x + 1, y)) + if (x, y - 1) in self.cell_data and self.cell_data[(x, y - 1)].update_options_up(cell): + propegation_set.add((x, y - 1)) + if (x - 1, y) in self.cell_data and self.cell_data[(x - 1, y)].update_options_left(cell): + propegation_set.add((x - 1, y)) + + def store_tile_data(self, the_map: Map): + for sy in range(the_map.h): + for sx in range(the_map.w): + tiles = [] + for y in range(8): + for x in range(10): + cell = self.cell_data[(x+sx*10, y+sy*8)] + if cell.result is not None: + tiles.append(cell.result) + elif len(cell.options) == 0: + tiles.append(1) + else: + tiles.append(2) + the_map.get(sx, sy).tiles = tiles + + def dump_option_count(self): + for y in range(self.h): + for x in range(self.w): + print(f"{len(self.cell_data[(x, y)].options):2x}", end="") + print() + print() diff --git a/worlds/ladx/LADXR/patches/aesthetics.py b/worlds/ladx/LADXR/patches/aesthetics.py new file mode 100644 index 0000000000..ff8cd5d856 --- /dev/null +++ b/worlds/ladx/LADXR/patches/aesthetics.py @@ -0,0 +1,436 @@ +from ..assembler import ASM +from ..utils import formatText, setReplacementName +from ..roomEditor import RoomEditor +from .. import entityData +import os +import bsdiff4 + +def imageTo2bpp(filename): + import PIL.Image + baseimg = PIL.Image.new('P', (1,1)) + baseimg.putpalette(( + 128, 0, 128, + 0, 0, 0, + 128, 128, 128, + 255, 255, 255, + )) + img = PIL.Image.open(filename) + img = img.quantize(colors=4, palette=baseimg) + print (f"Palette: {img.getpalette()}") + assert (img.size[0] % 8) == 0 + tileheight = 8 if img.size[1] == 8 else 16 + assert (img.size[1] % tileheight) == 0 + + cols = img.size[0] // 8 + rows = img.size[1] // tileheight + result = bytearray(rows * cols * tileheight * 2) + index = 0 + for ty in range(rows): + for tx in range(cols): + for y in range(tileheight): + a = 0 + b = 0 + for x in range(8): + c = img.getpixel((tx * 8 + x, ty * 16 + y)) + if c & 1: + a |= 0x80 >> x + if c & 2: + b |= 0x80 >> x + result[index] = a + result[index+1] = b + index += 2 + return result + + +def updateGraphics(rom, bank, offset, data): + if offset + len(data) > 0x4000: + updateGraphics(rom, bank, offset, data[:0x4000-offset]) + updateGraphics(rom, bank + 1, 0, data[0x4000 - offset:]) + else: + rom.banks[bank][offset:offset+len(data)] = data + if bank < 0x34: + rom.banks[bank-0x20][offset:offset + len(data)] = data + + +def gfxMod(rom, filename): + if os.path.exists(filename + ".names"): + for line in open(filename + ".names", "rt"): + if ":" in line: + k, v = line.strip().split(":", 1) + setReplacementName(k, v) + + ext = os.path.splitext(filename)[1].lower() + if ext == ".bin": + updateGraphics(rom, 0x2C, 0, open(filename, "rb").read()) + elif ext in (".png", ".bmp"): + updateGraphics(rom, 0x2C, 0, imageTo2bpp(filename)) + elif ext == ".bdiff": + updateGraphics(rom, 0x2C, 0, prepatch(rom, 0x2C, 0, filename)) + elif ext == ".json": + import json + data = json.load(open(filename, "rt")) + + for patch in data: + if "gfx" in patch: + updateGraphics(rom, int(patch["bank"], 16), int(patch["offset"], 16), imageTo2bpp(os.path.join(os.path.dirname(filename), patch["gfx"]))) + if "name" in patch: + setReplacementName(patch["item"], patch["name"]) + else: + updateGraphics(rom, 0x2C, 0, imageTo2bpp(filename)) + + +def createGfxImage(rom, filename): + import PIL.Image + bank_count = 8 + img = PIL.Image.new("P", (32 * 8, 32 * 8 * bank_count)) + img.putpalette(( + 128, 0, 128, + 0, 0, 0, + 128, 128, 128, + 255, 255, 255, + )) + for bank_nr in range(bank_count): + bank = rom.banks[0x2C + bank_nr] + for tx in range(32): + for ty in range(16): + for y in range(16): + a = bank[tx * 32 + ty * 32 * 32 + y * 2] + b = bank[tx * 32 + ty * 32 * 32 + y * 2 + 1] + for x in range(8): + c = 0 + if a & (0x80 >> x): + c |= 1 + if b & (0x80 >> x): + c |= 2 + img.putpixel((tx*8+x, bank_nr * 32 * 8 + ty*16+y), c) + img.save(filename) + +def prepatch(rom, bank, offset, filename): + bank_count = 8 + base_sheet = [] + result = [] + for bank_nr in range(bank_count): + base_sheet[0x4000 * bank_nr:0x4000 * (bank_nr + 1) - 1] = rom.banks[0x2C + bank_nr] + with open(filename, "rb") as patch: + file = patch.read() + result = bsdiff4.patch(src_bytes=bytes(base_sheet), patch_bytes=file) + return result + +def noSwordMusic(rom): + # Skip no-sword music override + # Instead of loading the sword level, we put the value 1 in the A register, indicating we have a sword. + rom.patch(2, 0x0151, ASM("ld a, [$DB4E]"), ASM("ld a, $01"), fill_nop=True) + rom.patch(2, 0x3AEF, ASM("ld a, [$DB4E]"), ASM("ld a, $01"), fill_nop=True) + rom.patch(3, 0x0996, ASM("ld a, [$DB4E]"), ASM("ld a, $01"), fill_nop=True) + rom.patch(3, 0x0B35, ASM("ld a, [$DB44]"), ASM("ld a, $01"), fill_nop=True) + + +def removeNagMessages(rom): + # Remove "this object is heavy, bla bla", and other nag messages when touching an object + rom.patch(0x02, 0x32BB, ASM("ld a, [$C14A]"), ASM("ld a, $01"), fill_nop=True) # crystal blocks + rom.patch(0x02, 0x32EC, ASM("ld a, [$C5A6]"), ASM("ld a, $01"), fill_nop=True) # cracked blocks + rom.patch(0x02, 0x32D3, ASM("jr nz, $25"), ASM("jr $25"), fill_nop=True) # stones/pots + rom.patch(0x02, 0x2B88, ASM("jr nz, $0F"), ASM("jr $0F"), fill_nop=True) # ice blocks + + +def removeLowHPBeep(rom): + rom.patch(2, 0x233A, ASM("ld hl, $FFF3\nld [hl], $04"), b"", fill_nop=True) # Remove health beep + + +def slowLowHPBeep(rom): + rom.patch(2, 0x2338, ASM("ld a, $30"), ASM("ld a, $60")) # slow slow hp beep + + +def removeFlashingLights(rom): + # Remove the switching between two backgrounds at mamu, always show the spotlights. + rom.patch(0x00, 0x01EB, ASM("ldh a, [$E7]\nrrca\nand $80"), ASM("ld a, $80"), fill_nop=True) + # Remove flashing colors from shopkeeper killing you after stealing and the mad batter giving items. + rom.patch(0x24, 0x3B77, ASM("push bc"), ASM("ret")) + + +def forceLinksPalette(rom, index): + # This forces the link sprite into a specific palette index ignoring the tunic options. + rom.patch(0, 0x1D8C, + ASM("ld a, [$DC0F]\nand a\njr z, $03\ninc a"), + ASM("ld a, $%02X" % (index)), fill_nop=True) + rom.patch(0, 0x1DD2, + ASM("ld a, [$DC0F]\nand a\njr z, $03\ninc a"), + ASM("ld a, $%02X" % (index)), fill_nop=True) + # Fix the waking up from bed palette + if index == 1: + rom.patch(0x21, 0x33FC, "A222", "FF05") + elif index == 2: + rom.patch(0x21, 0x33FC, "A222", "3F14") + elif index == 3: + rom.patch(0x21, 0x33FC, "A222", "037E") + for n in range(6): + rom.patch(0x05, 0x1261 + n * 2, "00", f"{index:02x}") + + +def fastText(rom): + rom.patch(0x00, 0x24CA, ASM("jp $2485"), ASM("call $2485")) + + +def noText(rom): + for idx in range(len(rom.texts)): + if not isinstance(rom.texts[idx], int) and (idx < 0x217 or idx > 0x21A): + rom.texts[idx] = rom.texts[idx][-1:] + + +def reduceMessageLengths(rom, rnd): + # Into text from Marin. Got to go fast, so less text. (This intro text is very long) + rom.texts[0x01] = formatText(rnd.choice([ + "Let's a go!", + "Remember, sword goes on A!", + "Avoid the heart piece of shame!", + "Marin? No, this is Zelda. Welcome to Hyrule", + "Why are you in my bed?", + "This is not a Mario game!", + "MuffinJets was here...", + "Remember, there are no bugs in LADX", + "#####, #####, you got to wake up!\nDinner is ready.", + "Go find the stepladder", + "Pizza power!", + "Eastmost penninsula is the secret", + "There is no cow level", + "You cannot lift rocks with your bear hands", + "Thank you, daid!", + "There, there now. Just relax. You've been asleep for almost nine hours now." + ])) + + # Reduce length of a bunch of common texts + rom.texts[0xEA] = formatText("You've got a Guardian Acorn!") + rom.texts[0xEB] = rom.texts[0xEA] + rom.texts[0xEC] = rom.texts[0xEA] + rom.texts[0x08] = formatText("You got a Piece of Power!") + rom.texts[0xEF] = formatText("You found a {SEASHELL}!") + rom.texts[0xA7] = formatText("You've got the {COMPASS}!") + + rom.texts[0x07] = formatText("You need the {NIGHTMARE_KEY}!") + rom.texts[0x8C] = formatText("You need a {KEY}!") # keyhole block + + rom.texts[0x09] = formatText("Ahhh... It has the Sleepy {TOADSTOOL}, it does! We'll mix it up something in a jiffy, we will!") + rom.texts[0x0A] = formatText("The last thing I kin remember was bitin' into a big juicy {TOADSTOOL}... Then, I had the darndest dream... I was a raccoon! Yeah, sounds strange, but it sure was fun!") + rom.texts[0x0F] = formatText("You pick the {TOADSTOOL}... As you hold it over your head, a mellow aroma flows into your nostrils.") + rom.texts[0x13] = formatText("You've learned the ^{SONG1}!^ This song will always remain in your heart!") + rom.texts[0x18] = formatText("Will you give me 28 {RUPEES} for my secret?", ask="Give Don't") + rom.texts[0x19] = formatText("How about it? 42 {RUPEES} for my little secret...", ask="Give Don't") + rom.texts[0x1e] = formatText("...You're so cute! I'll give you a 7 {RUPEE} discount!") + rom.texts[0x2d] = formatText("{ARROWS_10}\n10 {RUPEES}!", ask="Buy Don't") + rom.texts[0x32] = formatText("{SHIELD}\n20 {RUPEES}!", ask="Buy Don't") + rom.texts[0x33] = formatText("Ten {BOMB}\n10 {RUPEES}", ask="Buy Don't") + rom.texts[0x3d] = formatText("It's a {SHIELD}! There is space for your name!") + rom.texts[0x42] = formatText("It's 30 {RUPEES}! You can play the game three more times with this!") + rom.texts[0x45] = formatText("How about some fishing, little buddy? I'll only charge you 10 {RUPEES}...", ask="Fish Not Now") + rom.texts[0x4b] = formatText("Wow! Nice Fish! It's a lunker!! I'll give you a 20 {RUPEE} prize! Try again?", ask="Cast Not Now") + rom.texts[0x4e] = formatText("You're short of {RUPEES}? Don't worry about it. You just come back when you have more money, little buddy.") + rom.texts[0x4f] = formatText("You've got a {HEART_PIECE}! Press SELECT on the Subscreen to see.") + rom.texts[0x8e] = formatText("Well, it's an {OCARINA}, but you don't know how to play it...") + rom.texts[0x90] = formatText("You found the {POWER_BRACELET}! At last, you can pick up pots and stones!") + rom.texts[0x91] = formatText("You got your {SHIELD} back! Press the button and repel enemies with it!") + rom.texts[0x93] = formatText("You've got the {HOOKSHOT}! Its chain stretches long when you use it!") + rom.texts[0x94] = formatText("You've got the {MAGIC_ROD}! Now you can burn things! Burn it! Burn, baby burn!") + rom.texts[0x95] = formatText("You've got the {PEGASUS_BOOTS}! If you hold down the Button, you can dash!") + rom.texts[0x96] = formatText("You've got the {OCARINA}! You should learn to play many songs!") + rom.texts[0x97] = formatText("You've got the {FEATHER}! It feels like your body is a lot lighter!") + rom.texts[0x98] = formatText("You've got a {SHOVEL}! Now you can feel the joy of digging!") + rom.texts[0x99] = formatText("You've got some {MAGIC_POWDER}! Try sprinkling it on a variety of things!") + rom.texts[0x9b] = formatText("You found your {SWORD}! It must be yours because it has your name engraved on it!") + rom.texts[0x9c] = formatText("You've got the {FLIPPERS}! If you press the B Button while you swim, you can dive underwater!") + rom.texts[0x9e] = formatText("You've got a new {SWORD}! You should put your name on it right away!") + rom.texts[0x9f] = formatText("You've got a new {SWORD}! You should put your name on it right away!") + rom.texts[0xa0] = formatText("You found the {MEDICINE}! You should apply this and see what happens!") + rom.texts[0xa1] = formatText("You've got the {TAIL_KEY}! Now you can open the Tail Cave gate!") + rom.texts[0xa2] = formatText("You've got the {SLIME_KEY}! Now you can open the gate in Ukuku Prairie!") + rom.texts[0xa3] = formatText("You've got the {ANGLER_KEY}!") + rom.texts[0xa4] = formatText("You've got the {FACE_KEY}!") + rom.texts[0xa5] = formatText("You've got the {BIRD_KEY}!") + rom.texts[0xa6] = formatText("At last, you got a {MAP}! Press the START Button to look at it!") + rom.texts[0xa8] = formatText("You found a {STONE_BEAK}! Let's find the owl statue that belongs to it.") + rom.texts[0xa9] = formatText("You've got the {NIGHTMARE_KEY}! Now you can open the door to the Nightmare's Lair!") + rom.texts[0xaa] = formatText("You got a {KEY}! You can open a locked door.") + rom.texts[0xab] = formatText("You got 20 {RUPEES}! JOY!", center=True) + rom.texts[0xac] = formatText("You got 50 {RUPEES}! Very Nice!", center=True) + rom.texts[0xad] = formatText("You got 100 {RUPEES}! You're Happy!", center=True) + rom.texts[0xae] = formatText("You got 200 {RUPEES}! You're Ecstatic!", center=True) + rom.texts[0xdc] = formatText("Ribbit! Ribbit! I'm Mamu, on vocals! But I don't need to tell you that, do I? Everybody knows me! Want to hang out and listen to us jam? For 300 Rupees, we'll let you listen to a previously unreleased cut! What do you do?", ask="Pay Leave") + rom.texts[0xe8] = formatText("You've found a {GOLD_LEAF}! Press START to see how many you've collected!") + rom.texts[0xed] = formatText("You've got the Mirror Shield! You can now turnback the beams you couldn't block before!") + rom.texts[0xee] = formatText("You've got a more Powerful {POWER_BRACELET}! Now you can almost lift a whale!") + rom.texts[0xf0] = formatText("Want to go on a raft ride for a hundred {RUPEES}?", ask="Yes No Way") + + +def allowColorDungeonSpritesEverywhere(rom): + # Set sprite set numbers $01-$40 to map to the color dungeon sprites + rom.patch(0x00, 0x2E6F, "00", "15") + # Patch the spriteset loading code to load the 4 entries from the normal table instead of skipping this for color dungeon specific exception weirdness + rom.patch(0x00, 0x0DA4, ASM("jr nc, $05"), ASM("jr nc, $41")) + rom.patch(0x00, 0x0DE5, ASM(""" + ldh a, [$F7] + cp $FF + jr nz, $06 + ld a, $01 + ldh [$91], a + jr $40 + """), ASM(""" + jr $0A ; skip over the rest of the code + cp $FF ; check if color dungeon + jp nz, $0DAB + inc d + jp $0DAA + """), fill_nop=True) + # Disable color dungeon specific tile load hacks + rom.patch(0x00, 0x06A7, ASM("jr nz, $22"), ASM("jr $22")) + rom.patch(0x00, 0x2E77, ASM("jr nz, $0B"), ASM("jr $0B")) + + # Finally fill in the sprite data for the color dungeon + for n in range(22): + data = bytearray() + for m in range(4): + idx = rom.banks[0x20][0x06AA + 44 * m + n * 2] + bank = rom.banks[0x20][0x06AA + 44 * m + n * 2 + 1] + if idx == 0 and bank == 0: + v = 0xFF + elif bank == 0x35: + v = idx - 0x40 + elif bank == 0x31: + v = idx + elif bank == 0x2E: + v = idx + 0x40 + else: + assert False, "%02x %02x" % (idx, bank) + data += bytes([v]) + rom.room_sprite_data_indoor[0x200 + n] = data + + # Patch the graphics loading code to use DMA and load all sets that need to be reloaded, not just the first and last + rom.patch(0x00, 0x06FA, 0x07AF, ASM(""" + ;We enter this code with the right bank selected for tile data copy, + ;d = tile row (source addr = (d*$100+$4000)) + ;e = $00 + ;$C197 = index of sprite set to update (target addr = ($8400 + $100 * [$C197])) + ld a, d + add a, $40 + ldh [$51], a + xor a + ldh [$52], a + ldh [$54], a + ld a, [$C197] + add a, $84 + ldh [$53], a + ld a, $0F + ldh [$55], a + + ; See if we need to do anything next + ld a, [$C10E] ; check the 2nd update flag + and a + jr nz, getNext + ldh [$91], a ; no 2nd update flag, so clear primary update flag + ret + getNext: + ld hl, $C197 + inc [hl] + res 2, [hl] + ld a, [$C10D] + cp [hl] + ret nz + xor a ; clear the 2nd update flag when we prepare to update the last spriteset + ld [$C10E], a + ret + """), fill_nop=True) + rom.patch(0x00, 0x0738, "00" * (0x073E - 0x0738), ASM(""" + ; we get here by some color dungeon specific code jumping to this position + ; We still need that color dungeon specific code as it loads background tiles + xor a + ldh [$91], a + ldh [$93], a + ret + """)) + rom.patch(0x00, 0x073E, "00" * (0x07AF - 0x073E), ASM(""" + ;If we get here, only the 2nd flag is filled and the primary is not. So swap those around. + ld a, [$C10D] ;copy the index number + ld [$C197], a + xor a + ld [$C10E], a ; clear the 2nd update flag + inc a + ldh [$91], a ; set the primary update flag + ret + """), fill_nop=True) + + +def updateSpriteData(rom): + # Change the special sprite change exceptions + rom.patch(0x00, 0x0DAD, 0x0DDB, ASM(""" + ; Check for indoor + ld a, d + and a + jr nz, noChange + ldh a, [$F6] ; hMapRoom + cp $C9 + jr nz, sirenRoomEnd + ld a, [$D8C9] ; wOverworldRoomStatus + ROOM_OW_SIREN + and $20 + jr z, noChange + ld hl, $7837 + jp $0DFE +sirenRoomEnd: + ldh a, [$F6] ; hMapRoom + cp $D8 + jr nz, noChange + ld a, [$D8FD] ; wOverworldRoomStatus + ROOM_OW_WALRUS + and $20 + jr z, noChange + ld hl, $783B + jp $0DFE +noChange: + """), fill_nop=True) + rom.patch(0x20, 0x3837, "A4FF8BFF", "A461FF72") + rom.patch(0x20, 0x383B, "A44DFFFF", "A4C5FF70") + + # For each room update the sprite load data based on which entities are in there. + for room_nr in range(0x316): + if room_nr == 0x2FF: + continue + values = [None, None, None, None] + if room_nr == 0x00E: # D7 entrance opening + values[2] = 0xD6 + values[3] = 0xD7 + if 0x211 <= room_nr <= 0x21E: # D7 throwing ball thing. + values[0] = 0x66 + r = RoomEditor(rom, room_nr) + for obj in r.objects: + if obj.type_id == 0xC5 and room_nr < 0x100: # Pushable Gravestone + values[3] = 0x82 + for x, y, entity in r.entities: + sprite_data = entityData.SPRITE_DATA[entity] + if callable(sprite_data): + sprite_data = sprite_data(r) + if sprite_data is None: + continue + for m in range(0, len(sprite_data), 2): + idx, value = sprite_data[m:m+2] + if values[idx] is None: + values[idx] = value + elif isinstance(values[idx], set) and isinstance(value, set): + values[idx] = values[idx].intersection(value) + assert len(values[idx]) > 0 + elif isinstance(values[idx], set) and value in values[idx]: + values[idx] = value + elif isinstance(value, set) and values[idx] in value: + pass + elif values[idx] == value: + pass + else: + assert False, "Room: %03x cannot load graphics for entity: %02x (Index: %d Failed: %s, Active: %s)" % (room_nr, entity, idx, value, values[idx]) + + data = bytearray() + for v in values: + if isinstance(v, set): + v = next(iter(v)) + elif v is None: + v = 0xff + data.append(v) + + if room_nr < 0x100: + rom.room_sprite_data_overworld[room_nr] = data + else: + rom.room_sprite_data_indoor[room_nr - 0x100] = data diff --git a/worlds/ladx/LADXR/patches/bank34.py b/worlds/ladx/LADXR/patches/bank34.py new file mode 100644 index 0000000000..22abd48b39 --- /dev/null +++ b/worlds/ladx/LADXR/patches/bank34.py @@ -0,0 +1,125 @@ +import os +import binascii +from ..assembler import ASM +from ..utils import formatText + +ItemNameLookupTable = 0x0100 +ItemNameLookupSize = 2 +TotalRoomCount = 0x316 + +AnItemText = "an item" +ItemNameStringBufferStart = ItemNameLookupTable + \ + TotalRoomCount * ItemNameLookupSize + + +def addBank34(rom, item_list): + my_path = os.path.dirname(__file__) + rom.patch(0x34, 0x0000, ItemNameLookupTable, ASM(""" + ; Get the pointer in the lookup table, doubled as it's two bytes + ld hl, $2080 + push de + call OffsetPointerByRoomNumber + pop de + add hl, hl + + ldi a, [hl] ; hl = *hl + ld h, [hl] + ld l, a + + ; If there's no data, bail + ld a, l + or h + jp z, SwitchBackTo3E + + ld de, wCustomMessage + ; Copy "Got " to de + ld a, 71 + ld [de], a + inc de + ld a, 111 + ld [de], a + inc de + ld a, 116 + ld [de], a + inc de + ld a, 32 + ld [de], a + inc de + ; Copy in our item name + call MessageCopyString + SwitchBackTo3E: + ; Bail + ld a, $3e ; Set bank number + jp $080C ; switch bank + + ; this should be shared but I got link errors + OffsetPointerByRoomNumber: + ldh a, [$F6] ; map room + ld e, a + ld a, [$DBA5] ; is indoor + ld d, a + ldh a, [$F7] ; mapId + cp $FF + jr nz, .notColorDungeon + + ld d, $03 + jr .notCavesA + + .notColorDungeon: + cp $1A + jr nc, .notCavesA + cp $06 + jr c, .notCavesA + inc d + .notCavesA: + add hl, de + ret + """ + open(os.path.join(my_path, "bank3e.asm/message.asm"), "rt").read(), 0x4000), fill_nop=True) + + nextItemLookup = ItemNameStringBufferStart + nameLookup = { + + } + + name = AnItemText + + def add_or_get_name(name): + nonlocal nextItemLookup + if name in nameLookup: + return nameLookup[name] + if len(name) + 1 + nextItemLookup >= 0x4000: + return nameLookup[AnItemText] + asm = ASM(f'db "{name}", $ff\n') + rom.patch(0x34, nextItemLookup, None, asm) + patch_len = len(binascii.unhexlify(asm)) + nameLookup[name] = nextItemLookup + 0x4000 + nextItemLookup += patch_len + return nameLookup[name] + + item_text_addr = add_or_get_name(AnItemText) + #error_text_addr = add_or_get_name("Please report this check to #bug-reports in the AP discord") + def swap16(x): + assert x <= 0xFFFF + return (x >> 8) | ((x & 0xFF) << 8) + + def to_hex_address(x): + return f"{swap16(x):04x}" + + # Set defaults for every room + for i in range(TotalRoomCount): + rom.patch(0x34, ItemNameLookupTable + i * + ItemNameLookupSize, None, to_hex_address(0)) + + for item in item_list: + if not item.custom_item_name: + continue + assert item.room < TotalRoomCount, item.room + # Item names of exactly 255 characters will cause overwrites to occur in the text box + # assert len(item.custom_item_name) < 0x100 + # Custom text is only 95 bytes long, restrict to 50 + addr = add_or_get_name(item.custom_item_name[:50]) + rom.patch(0x34, ItemNameLookupTable + item.room * + ItemNameLookupSize, None, to_hex_address(addr)) + if item.extra: + rom.patch(0x34, ItemNameLookupTable + item.extra * + ItemNameLookupSize, None, to_hex_address(addr)) \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/bank3e.asm/bowwow.asm b/worlds/ladx/LADXR/patches/bank3e.asm/bowwow.asm new file mode 100644 index 0000000000..3480838e2f --- /dev/null +++ b/worlds/ladx/LADXR/patches/bank3e.asm/bowwow.asm @@ -0,0 +1,303 @@ +CheckIfLoadBowWow: + ; Check has bowwow flag + ld a, [$DB56] + cp $01 + jr nz, .noLoadBowwow + + ldh a, [$F6] ; load map number + cp $22 + jr z, .loadBowwow + cp $23 + jr z, .loadBowwow + cp $24 + jr z, .loadBowwow + cp $32 + jr z, .loadBowwow + cp $33 + jr z, .loadBowwow + cp $34 + jr z, .loadBowwow + +.noLoadBowwow: + ld e, $00 + ret + +.loadBowwow: + ld e, $01 + ret + + +; Special handler for when Bowwow tries to eat an entity. +; Our target entity index is loaded in BC. +BowwowEat: + ; Load the entity type into A + ld hl, $C3A0 ; entity type + add hl, bc + ld a, [hl] + + ; Check if we need special handling for bosses + cp $59 ; Moldorm + jr z, BowwowHurtEnemy + cp $5C ; Genie + jr z, BowwowEatGenie + cp $5B ; SlimeEye + jp z, BowwowEatSlimeEye + cp $65 ; AnglerFish + jr z, BowwowHurtEnemy + cp $5D ; SlimeEel + jp z, BowwowEatSlimeEel + cp $5A ; Facade + jr z, BowwowHurtEnemy + cp $63 ; Eagle + jr z, BowwowHurtEnemy + cp $62 ; Hot head + jp z, BowwowEatHotHead + cp $F9 ; Hardhit beetle + jr z, BowwowHurtEnemy + cp $E6 ; Nightmare (all forms) + jp z, BowwowEatNightmare + + ; Check for special handling for minibosses + cp $87 ; Lanmola + jr z, BowwowHurtEnemy + ; cp $88 ; Armos knight + ; No special handling, just eat him, solves the fight real quick. + cp $81 ; rolling bones + jr z, BowwowHurtEnemy + cp $89 ; Hinox + jr z, BowwowHurtEnemy + cp $8E ; Cue ball + jr z, BowwowHurtEnemy + ;cp $5E ; Gnoma + ;jr z, BowwowHurtEnemy + cp $5F ; Master stalfos + jr z, BowwowHurtEnemy + cp $92 ; Smasher + jp z, BowwowEatSmasher + cp $BC ; Grim Creeper + jp z, BowwowEatGrimCreeper + cp $BE ; Blaino + jr z, BowwowHurtEnemy + cp $F8 ; Giant buzz blob + jr z, BowwowHurtEnemy + cp $F4 ; Avalaunch + jr z, BowwowHurtEnemy + + ; Some enemies + cp $E9 ; Color dungeon shell + jr z, BowwowHurtEnemy + cp $EA ; Color dungeon shell + jr z, BowwowHurtEnemy + cp $EB ; Color dungeon shell + jr z, BowwowHurtEnemy + + ; Play SFX + ld a, $03 + ldh [$F2], a + ; Call normal "destroy entity and drop item" handler + jp $3F50 + +BowwowHurtEnemy: + ; Hurt enemy with damage type zero (sword) + ld a, $00 + ld [$C19E], a + rst $18 + ; Play SFX + ld a, $03 + ldh [$F2], a + ret + +BowwowEatGenie: + ; Get private state to find out if this is a bottle or the genie + ld hl, $C2B0 + add hl, bc + ld a, [hl] + ; Prepare loading state from hl + ld hl, $C290 + add hl, bc + + cp $00 + jr z, .bottle + cp $01 + jr z, .ghost + ret + +.ghost: + ; Get current state + ld a, [hl] + cp $04 ; Flying around without bottle + jr z, BowwowHurtEnemy + ret + +.bottle: + ; Get current state + ld a, [hl] + cp $03 ; Hopping around in bottle + jr z, BowwowHurtEnemy + ret + +BowwowEatSlimeEye: + ; On set privateCountdown2 to $0C to split, when privateState1 is $04 and state is $03 + ld hl, $C290 ; state + add hl, bc + ld a, [hl] + cp $03 + jr nz, .skipSplit + + ld hl, $C2B0 ; private state1 + add hl, bc + ld a, [hl] + cp $04 + jr nz, .skipSplit + + ld hl, $C300 ; private countdown 2 + add hl, bc + ld [hl], $0C + +.skipSplit: + jp BowwowHurtEnemy + +BowwowEatSlimeEel: + ; Get private state to find out if this is the tail or the head + ld hl, $C2B0 + add hl, bc + ld a, [hl] + cp $01 ; not the head, so, skip. + ret nz + + ; Check if we are pulled out of the wall + ld hl, $C290 + add hl, bc + ld a, [hl] + cp $03 ; pulled out of the wall + jr nz, .knockOutOfWall + + ld hl, $D204 + ld a, [hl] + cp $07 + jr nc, .noExtraDamage + inc [hl] +.noExtraDamage: + jp BowwowHurtEnemy + +.knockOutOfWall: + ld [hl], $03 ; set state to $03 + ld hl, $C210 ; Y position + add hl, bc + ld a, [hl] + ld [hl], $60 + cp $48 + jp nc, BowwowHurtEnemy + ld [hl], $30 + jp BowwowHurtEnemy + + +BowwowEatHotHead: + ; Load health of hothead + ld hl, $C360 + add hl, bc + ld a, [hl] + cp $20 + jr c, .lowHp + ld [hl], $20 +.lowHp: + jp BowwowHurtEnemy + +BowwowEatSmasher: + ; Check if this is the ball or the monster + ld hl, $C440 + add hl, bc + ld a, [hl] + and a + ret nz + jp BowwowHurtEnemy + +BowwowEatGrimCreeper: + ; Check if this is the main enemy or the smaller ones. Only kill the small ones + ld hl, $C2B0 + add hl, bc + ld a, [hl] + and a + ret z + jp BowwowHurtEnemy + +BowwowEatNightmare: + ; Check if this is the staircase. + ld hl, $C390 + add hl, bc + ld a, [hl] + cp $02 + ret z + + ; Prepare loading state from hl + ld hl, $C290 + add hl, bc + + ld a, [$D219] ; which form has the nightmare + cp $01 + jr z, .slimeForm + cp $02 + jr z, .agahnimForm + cp $03 ; moldormForm + jp z, BowwowHurtEnemy + cp $04 ; ganon and lanmola + jp z, BowwowHurtEnemy + cp $05 ; dethl + jp z, BowwowHurtEnemy + ; 0 is the intro form + ret + +.slimeForm: + ld a, [hl] + cp $02 + jr z, .canHurtSlime + cp $03 + ret nz + +.canHurtSlime: + ; We need quite some custom handling, normally the nightmare checks very directly if you use powder. + ; No idea why this insta kills the slime form... + ; Change state to hurt state + ld [hl], $07 + ; Set flash count + ld hl, $C420 + add hl, bc + ld [hl], $14 + ; play proper sfx + ld a, $07 + ldh [$F3], a + ld a, $37 + ldh [$F2], a + ; No idea why this is done, but it happens when you use powder on the slime + ld a, $03 + ld [$D220], a + ret + +.agahnimForm: + ld a, [hl] + ; only damage in states 2 to 4 + cp $02 + ret c + cp $04 + ret nc + + ; Decrease health + ld a, [$D220] + inc a + ld [$D220], a + ; If dead, do stuff + cp $04 + jr c, .agahnimNotDeadYet + ld [hl], $07 + ld hl, $C2E0 + add hl, bc + ld [hl], $C0 + ld a, $36 + ldh [$F2], a +.agahnimNotDeadYet: + ld hl, $C420 + add hl, bc + ld [hl], $14 + ld a, $07 + ldh [$F3], a + ret diff --git a/worlds/ladx/LADXR/patches/bank3e.asm/chest.asm b/worlds/ladx/LADXR/patches/bank3e.asm/chest.asm new file mode 100644 index 0000000000..717a2def1d --- /dev/null +++ b/worlds/ladx/LADXR/patches/bank3e.asm/chest.asm @@ -0,0 +1,993 @@ +RenderChestItem: + ldh a, [$F1] ; active sprite + and $80 + jr nz, .renderLargeItem + + ld de, ItemSpriteTable + call $3C77 ; RenderActiveEntitySprite + ret +.renderLargeItem: + ld de, LargeItemSpriteTable + dec d + dec d + call $3BC0 ; RenderActiveEntitySpritePair + + ; If we are an instrument + ldh a, [$F1] + cp $8E + ret c + cp $96 + ret nc + + ; But check if we are not state >3 before that, else the fade-out at the instrument room breaks. + ldh a, [$F0] ; hActiveEntityState + cp $03 + ret nc + + ; Call the color cycling code + xor a + ld [$DC82], a + ld [$DC83], a + ld a, $3e + call $0AD2 + ret + +GiveItemFromChestMultiworld: + call IncreaseCheckCounter + ; Check our "item is for other player" flag + ld hl, $7300 + call OffsetPointerByRoomNumber + ld a, [hl] + ld hl, $0055 + cp [hl] + ret nz + +GiveItemFromChest: + ldh a, [$F1] ; Load active sprite variant + + rst 0 ; JUMP TABLE + dw ChestPowerBracelet; CHEST_POWER_BRACELET + dw ChestShield ; CHEST_SHIELD + dw ChestBow ; CHEST_BOW + dw ChestWithItem ; CHEST_HOOKSHOT + dw ChestWithItem ; CHEST_MAGIC_ROD + dw ChestWithItem ; CHEST_PEGASUS_BOOTS + dw ChestWithItem ; CHEST_OCARINA + dw ChestWithItem ; CHEST_FEATHER + dw ChestWithItem ; CHEST_SHOVEL + dw ChestMagicPowder ; CHEST_MAGIC_POWDER_BAG + dw ChestBomb ; CHEST_BOMB + dw ChestSword ; CHEST_SWORD + dw Flippers ; CHEST_FLIPPERS + dw NoItem ; CHEST_MAGNIFYING_LENS + dw ChestWithItem ; Boomerang (used to be unused) + dw SlimeKey ; ?? right side of your trade quest item + dw Medicine ; CHEST_MEDICINE + dw TailKey ; CHEST_TAIL_KEY + dw AnglerKey ; CHEST_ANGLER_KEY + dw FaceKey ; CHEST_FACE_KEY + dw BirdKey ; CHEST_BIRD_KEY + dw GoldenLeaf ; CHEST_GOLD_LEAF + dw ChestWithCurrentDungeonItem ; CHEST_MAP + dw ChestWithCurrentDungeonItem ; CHEST_COMPASS + dw ChestWithCurrentDungeonItem ; CHEST_STONE_BEAK + dw ChestWithCurrentDungeonItem ; CHEST_NIGHTMARE_KEY + dw ChestWithCurrentDungeonItem ; CHEST_SMALL_KEY + dw AddRupees50 ; CHEST_RUPEES_50 + dw AddRupees20 ; CHEST_RUPEES_20 + dw AddRupees100 ; CHEST_RUPEES_100 + dw AddRupees200 ; CHEST_RUPEES_200 + dw AddRupees500 ; CHEST_RUPEES_500 + dw AddSeashell ; CHEST_SEASHELL + dw NoItem ; CHEST_MESSAGE + dw NoItem ; CHEST_GEL + dw AddKey ; KEY1 + dw AddKey ; KEY2 + dw AddKey ; KEY3 + dw AddKey ; KEY4 + dw AddKey ; KEY5 + dw AddKey ; KEY6 + dw AddKey ; KEY7 + dw AddKey ; KEY8 + dw AddKey ; KEY9 + dw AddMap ; MAP1 + dw AddMap ; MAP2 + dw AddMap ; MAP3 + dw AddMap ; MAP4 + dw AddMap ; MAP5 + dw AddMap ; MAP6 + dw AddMap ; MAP7 + dw AddMap ; MAP8 + dw AddMap ; MAP9 + dw AddCompass ; COMPASS1 + dw AddCompass ; COMPASS2 + dw AddCompass ; COMPASS3 + dw AddCompass ; COMPASS4 + dw AddCompass ; COMPASS5 + dw AddCompass ; COMPASS6 + dw AddCompass ; COMPASS7 + dw AddCompass ; COMPASS8 + dw AddCompass ; COMPASS9 + dw AddStoneBeak ; STONE_BEAK1 + dw AddStoneBeak ; STONE_BEAK2 + dw AddStoneBeak ; STONE_BEAK3 + dw AddStoneBeak ; STONE_BEAK4 + dw AddStoneBeak ; STONE_BEAK5 + dw AddStoneBeak ; STONE_BEAK6 + dw AddStoneBeak ; STONE_BEAK7 + dw AddStoneBeak ; STONE_BEAK8 + dw AddStoneBeak ; STONE_BEAK9 + dw AddNightmareKey ; NIGHTMARE_KEY1 + dw AddNightmareKey ; NIGHTMARE_KEY2 + dw AddNightmareKey ; NIGHTMARE_KEY3 + dw AddNightmareKey ; NIGHTMARE_KEY4 + dw AddNightmareKey ; NIGHTMARE_KEY5 + dw AddNightmareKey ; NIGHTMARE_KEY6 + dw AddNightmareKey ; NIGHTMARE_KEY7 + dw AddNightmareKey ; NIGHTMARE_KEY8 + dw AddNightmareKey ; NIGHTMARE_KEY9 + dw AddToadstool ; Toadstool + dw NoItem ; $51 + dw NoItem ; $52 + dw NoItem ; $53 + dw NoItem ; $54 + dw NoItem ; $55 + dw NoItem ; $56 + dw NoItem ; $57 + dw NoItem ; $58 + dw NoItem ; $59 + dw NoItem ; $5A + dw NoItem ; $5B + dw NoItem ; $5C + dw NoItem ; $5D + dw NoItem ; $5E + dw NoItem ; $5F + dw NoItem ; $60 + dw NoItem ; $61 + dw NoItem ; $62 + dw NoItem ; $63 + dw NoItem ; $64 + dw NoItem ; $65 + dw NoItem ; $66 + dw NoItem ; $67 + dw NoItem ; $68 + dw NoItem ; $69 + dw NoItem ; $6A + dw NoItem ; $6B + dw NoItem ; $6C + dw NoItem ; $6D + dw NoItem ; $6E + dw NoItem ; $6F + dw NoItem ; $70 + dw NoItem ; $71 + dw NoItem ; $72 + dw NoItem ; $73 + dw NoItem ; $74 + dw NoItem ; $75 + dw NoItem ; $76 + dw NoItem ; $77 + dw NoItem ; $78 + dw NoItem ; $79 + dw NoItem ; $7A + dw NoItem ; $7B + dw NoItem ; $7C + dw NoItem ; $7D + dw NoItem ; $7E + dw NoItem ; $7F + dw PieceOfHeart ; Heart piece + dw GiveBowwow + dw Give10Arrows + dw Give1Arrow + dw UpgradeMaxPowder + dw UpgradeMaxBombs + dw UpgradeMaxArrows + dw GiveRedTunic + dw GiveBlueTunic + dw GiveExtraHeart + dw TakeHeart + dw GiveSong1 + dw GiveSong2 + dw GiveSong3 + dw GiveInstrument + dw GiveInstrument + dw GiveInstrument + dw GiveInstrument + dw GiveInstrument + dw GiveInstrument + dw GiveInstrument + dw GiveInstrument + dw GiveRooster + dw GiveTradeItem1 + dw GiveTradeItem2 + dw GiveTradeItem3 + dw GiveTradeItem4 + dw GiveTradeItem5 + dw GiveTradeItem6 + dw GiveTradeItem7 + dw GiveTradeItem8 + dw GiveTradeItem9 + dw GiveTradeItem10 + dw GiveTradeItem11 + dw GiveTradeItem12 + dw GiveTradeItem13 + dw GiveTradeItem14 + +NoItem: + ret + +ChestPowerBracelet: + ld hl, $DB43 ; power bracelet level + jr ChestIncreaseItemLevel + +ChestShield: + ld hl, $DB44 ; shield level + jr ChestIncreaseItemLevel + +ChestSword: + ld hl, $DB4E ; sword level + jr ChestIncreaseItemLevel + +ChestIncreaseItemLevel: + ld a, [hl] + cp $02 + jr z, DoNotIncreaseItemLevel + inc [hl] +DoNotIncreaseItemLevel: + jp ChestWithItem + +ChestBomb: + ld a, [$DB4D] ; bomb count + add a, $10 + daa + ld hl, $DB77 ; max bombs + cp [hl] + jr c, .bombsNotFull + ld a, [hl] +.bombsNotFull: + ld [$DB4D], a + jp ChestWithItem + +ChestBow: + ld a, [$DB45] + cp $20 + jp nc, ChestWithItem + ld a, $20 + ld [$DB45], a + jp ChestWithItem + +ChestMagicPowder: + ; Reset the toadstool state + ld a, $0B + ldh [$A5], a + xor a + ld [$DB4B], a ; has toadstool + + ld a, [$DB4C] ; powder count + add a, $10 + daa + ld hl, $DB76 ; max powder + cp [hl] + jr c, .magicPowderNotFull + ld a, [hl] +.magicPowderNotFull: + ld [$DB4C], a + jp ChestWithItem + + +Flippers: + ld a, $01 + ld [wHasFlippers], a + ret + +Medicine: + ld a, $01 + ld [wHasMedicine], a + ret + +TailKey: + ld a, $01 + ld [$DB11], a + ret + +AnglerKey: + ld a, $01 + ld [$DB12], a + ret + +FaceKey: + ld a, $01 + ld [$DB13], a + ret + +BirdKey: + ld a, $01 + ld [$DB14], a + ret + +SlimeKey: + ld a, $01 + ld [$DB15], a + ret + +GoldenLeaf: + ld hl, wGoldenLeaves + inc [hl] + ret + +AddSeaShell: + ld a, [wSeashellsCount] + inc a + daa + ld [wSeashellsCount], a + ret + +PieceOfHeart: +#IF HARD_MODE + ld a, $FF + ld [$DB93], a +#ENDIF + + ld a, [$DB5C] + inc a + cp $04 + jr z, .FullHeart + ld [$DB5C], a + ret +.FullHeart: + xor a + ld [$DB5C], a + jp GiveExtraHeart + +GiveBowwow: + ld a, $01 + ld [$DB56], a + ret + +ChestInventoryTable: + db $03 ; CHEST_POWER_BRACELET + db $04 ; CHEST_SHIELD + db $05 ; CHEST_BOW + db $06 ; CHEST_HOOKSHOT + db $07 ; CHEST_MAGIC_ROD + db $08 ; CHEST_PEGASUS_BOOTS + db $09 ; CHEST_OCARINA + db $0A ; CHEST_FEATHER + db $0B ; CHEST_SHOVEL + db $0C ; CHEST_MAGIC_POWDER_BAG + db $02 ; CHEST_BOMB + db $01 ; CHEST_SWORD + db $00 ; - (flippers slot) + db $00 ; - (magnifier lens slot) + db $0D ; Boomerang + +ChestWithItem: + ldh a, [$F1] ; Load active sprite variant + ld d, $00 + ld e, a + ld hl, ChestInventoryTable + add hl, de + ld d, [hl] + call $3E6B ; Give Inventory + ret + +ChestWithCurrentDungeonItem: + sub $16 ; a -= CHEST_MAP + ld e, a + ld d, $00 + ld hl, $DBCC ; hasDungeonMap + add hl, de + inc [hl] + call $2802 ; Sync current dungeon items with dungeon specific table + ret + +AddToadstool: + ld d, $0E + call $3E6B ; Give Inventory + ret + +AddKey: + sub $23 ; Make 'A' target dungeon index + ld de, $0004 + jr AddDungeonItem + +AddMap: + sub $2C ; Make 'A' target dungeon index + ld de, $0000 + jr AddDungeonItem + +AddCompass: + sub $35 ; Make 'A' target dungeon index + ld de, $0001 + jr AddDungeonItem + +AddStoneBeak: + sub $3E ; Make 'A' target dungeon index + ld de, $0002 + jr AddDungeonItem + +AddNightmareKey: + sub $47 ; Make 'A' target dungeon index + ld de, $0003 + jr AddDungeonItem + +AddDungeonItem: + cp $08 + jr z, .colorDungeon + ; hl = dungeonitems + type_type + dungeon * 8 + ld hl, $DB16 + add hl, de + push de + ld e, a + add hl, de + add hl, de + add hl, de + add hl, de + add hl, de + pop de + inc [hl] + ; Check if we are in this specific dungeon, and then increase the copied counters as well. + ld hl, $FFF7 ; is current map == target map + cp [hl] + ret nz + ld a, [$DBA5] ; is indoor + and a + ret z + + ld hl, $DBCC + add hl, de + inc [hl] + ret +.colorDungeon: + ; Special case for the color dungeon, which is in a different location in memory. + ld hl, $DDDA + add hl, de + inc [hl] + ldh a, [$F7] ; is current map == color dungeon + cp $ff + ret nz + ld hl, $DBCC + add hl, de + inc [hl] + ret + +AddRupees20: + xor a + ld h, $14 + jr AddRupees + +AddRupees50: + xor a + ld h, $32 + jr AddRupees + +AddRupees100: + xor a + ld h, $64 + jr AddRupees + +AddRupees200: + xor a + ld h, $C8 + jr AddRupees + +AddRupees500: + ld a, $01 + ld h, $F4 + jr AddRupees + +AddRupees: + ld [$DB8F], a + ld a, h + ld [$DB90], a + ld a, $18 + ld [$C3CE], a + ret + +Give1Arrow: + ld a, [$DB45] + inc a + jp FinishGivingArrows + +Give10Arrows: + ld a, [$DB45] + add a, $0A +FinishGivingArrows: + daa + ld [$DB45], a + ld hl, $DB78 + cp [hl] + ret c + ld a, [hl] + ld [$DB45], a + ret + +UpgradeMaxPowder: + ld a, $40 + ld [$DB76], a + ; If we have no powder, we should not increase the current amount, as that would prevent + ; The toadstool from showing up. + ld a, [$DB4C] + and a + ret z + ld a, $40 + ld [$DB4C], a + ret + +UpgradeMaxBombs: + ld a, $60 + ld [$DB77], a + ld [$DB4D], a + ret + +UpgradeMaxArrows: + ld a, $60 + ld [$DB78], a + ld [$DB45], a + ret + +GiveRedTunic: + ld a, $01 + ld [$DC0F], a + ; We use DB6D to store which tunics we have available. + ld a, [wCollectedTunics] + or $01 + ld [wCollectedTunics], a + ret + +GiveBlueTunic: + ld a, $02 + ld [$DC0F], a + ; We use DB6D to store which tunics we have available. + ld a, [wCollectedTunics] + or $02 + ld [wCollectedTunics], a + ret + +GiveExtraHeart: + ; Regen all health + ld hl, $DB93 + ld [hl], $FF + ; Increase max health if health is lower then 14 hearts + ld hl, $DB5B + ld a, $0E + cp [hl] + ret z + inc [hl] + ret + +TakeHeart: + ; First, reduce the max HP + ld hl, $DB5B + ld a, [hl] + cp $01 + ret z + dec a + ld [$DB5B], a + + ; Next, check if we need to reduce our actual HP to keep it below the maximum. + rlca + rlca + rlca + sub $01 + ld hl, $DB5A + cp [hl] + jr nc, .noNeedToReduceHp + ld [hl], a +.noNeedToReduceHp: + ; Finally, give all health back. + ld hl, $DB93 + ld [hl], $FF + ret + +GiveSong1: + ld hl, $DB49 + set 2, [hl] + ld a, $00 + ld [$DB4A], a + ret + +GiveSong2: + ld hl, $DB49 + set 1, [hl] + ld a, $01 + ld [$DB4A], a + ret + +GiveSong3: + ld hl, $DB49 + set 0, [hl] + ld a, $02 + ld [$DB4A], a + ret + +GiveInstrument: + ldh a, [$F1] ; Load active sprite variant + sub $8E + ld d, $00 + ld e, a + ld hl, $db65 ; has instrument table + add hl, de + set 1, [hl] + ret + +GiveRooster: + ld d, $0F + call $3E6B ; Give Inventory (rooster item) + + ;ld a, $01 + ;ld [$DB7B], a ; has rooster + ldh a, [$F9] ; do not spawn rooster in sidescroller + and a + ret z + + ld a, $D5 ; ENTITY_ROOSTER + call $3B86 ; SpawnNewEntity_trampoline + ldh a, [$98] ; LinkX + ld hl, $C200 ; wEntitiesPosXTable + add hl, de + ld [hl], a + ldh a, [$99] ; LinkY + ld hl, $C210 ; wEntitiesPosYTable + add hl, de + ld [hl], a + + ret + +GiveTradeItem1: + ld hl, wTradeSequenceItem + set 0, [hl] + ret +GiveTradeItem2: + ld hl, wTradeSequenceItem + set 1, [hl] + ret +GiveTradeItem3: + ld hl, wTradeSequenceItem + set 2, [hl] + ret +GiveTradeItem4: + ld hl, wTradeSequenceItem + set 3, [hl] + ret +GiveTradeItem5: + ld hl, wTradeSequenceItem + set 4, [hl] + ret +GiveTradeItem6: + ld hl, wTradeSequenceItem + set 5, [hl] + ret +GiveTradeItem7: + ld hl, wTradeSequenceItem + set 6, [hl] + ret +GiveTradeItem8: + ld hl, wTradeSequenceItem + set 7, [hl] + ret +GiveTradeItem9: + ld hl, wTradeSequenceItem2 + set 0, [hl] + ret +GiveTradeItem10: + ld hl, wTradeSequenceItem2 + set 1, [hl] + ret +GiveTradeItem11: + ld hl, wTradeSequenceItem2 + set 2, [hl] + ret +GiveTradeItem12: + ld hl, wTradeSequenceItem2 + set 3, [hl] + ret +GiveTradeItem13: + ld hl, wTradeSequenceItem2 + set 4, [hl] + ret +GiveTradeItem14: + ld hl, wTradeSequenceItem2 + set 5, [hl] + ret + +ItemMessageMultiworld: + ; Check our "item is for other player" flag + ld hl, $7300 + call OffsetPointerByRoomNumber + ld a, [hl] + ld hl, $0055 + cp [hl] + jr nz, ItemMessageForOtherPlayer + +ItemMessage: + ; Fill the custom message slot with this item message. + call BuildItemMessage + ldh a, [$F1] + ld d, $00 + ld e, a + ld hl, ItemMessageTable + add hl, de + ld a, [hl] + cp $90 + jr z, .powerBracelet + cp $3D + jr z, .shield + jp $2385 ; Opendialog in $000-$0FF range + +.powerBracelet: + ; Check the power bracelet level, and give a different message when we get the lv2 bracelet + ld hl, $DB43 ; power bracelet level + bit 1, [hl] + jp z, $2385 ; Opendialog in $000-$0FF range + ld a, $EE + jp $2385 ; Opendialog in $000-$0FF range + +.shield: + ; Check the shield level, and give a different message when we get the lv2 shield + ld hl, $DB44 ; shield level + bit 1, [hl] + jp z, $2385 ; Opendialog in $000-$0FF range + ld a, $ED + jp $2385 ; Opendialog in $000-$0FF range + +ItemMessageForOtherPlayer: + push bc + push hl + push af + call BuildRemoteItemMessage + ld hl, SpaceFor + call MessageCopyString + pop af + call MessageAddPlayerName + pop hl + pop bc + ;dec de + ld a, $C9 + jp $2385 ; Opendialog in $000-$0FF range + +ItemSpriteTable: + db $82, $15 ; CHEST_POWER_BRACELET + db $86, $15 ; CHEST_SHIELD + db $88, $14 ; CHEST_BOW + db $8A, $14 ; CHEST_HOOKSHOT + db $8C, $14 ; CHEST_MAGIC_ROD + db $98, $16 ; CHEST_PEGASUS_BOOTS + db $10, $1F ; CHEST_OCARINA + db $12, $1D ; CHEST_FEATHER + db $96, $17 ; CHEST_SHOVEL + db $0E, $1C ; CHEST_MAGIC_POWDER_BAG + db $80, $15 ; CHEST_BOMB + db $84, $15 ; CHEST_SWORD + db $94, $15 ; CHEST_FLIPPERS + db $9A, $10 ; CHEST_MAGNIFYING_LENS + db $24, $1C ; Boomerang + db $4E, $1C ; Slime key + db $A0, $14 ; CHEST_MEDICINE + db $30, $1C ; CHEST_TAIL_KEY + db $32, $1C ; CHEST_ANGLER_KEY + db $34, $1C ; CHEST_FACE_KEY + db $36, $1C ; CHEST_BIRD_KEY + db $3A, $1C ; CHEST_GOLD_LEAF + db $40, $1C ; CHEST_MAP + db $42, $1D ; CHEST_COMPASS + db $44, $1C ; CHEST_STONE_BEAK + db $46, $1C ; CHEST_NIGHTMARE_KEY + db $4A, $1F ; CHEST_SMALL_KEY + db $A6, $15 ; CHEST_RUPEES_50 (normal blue) + db $38, $19 ; CHEST_RUPEES_20 (red) + db $38, $18 ; CHEST_RUPEES_100 (green) + db $38, $1A ; CHEST_RUPEES_200 (yellow) + db $38, $1A ; CHEST_RUPEES_500 (yellow) + db $9E, $14 ; CHEST_SEASHELL + db $8A, $14 ; CHEST_MESSAGE + db $A0, $14 ; CHEST_GEL + db $4A, $1D ; KEY1 + db $4A, $1D ; KEY2 + db $4A, $1D ; KEY3 + db $4A, $1D ; KEY4 + db $4A, $1D ; KEY5 + db $4A, $1D ; KEY6 + db $4A, $1D ; KEY7 + db $4A, $1D ; KEY8 + db $4A, $1D ; KEY9 + db $40, $1C ; MAP1 + db $40, $1C ; MAP2 + db $40, $1C ; MAP3 + db $40, $1C ; MAP4 + db $40, $1C ; MAP5 + db $40, $1C ; MAP6 + db $40, $1C ; MAP7 + db $40, $1C ; MAP8 + db $40, $1C ; MAP9 + db $42, $1D ; COMPASS1 + db $42, $1D ; COMPASS2 + db $42, $1D ; COMPASS3 + db $42, $1D ; COMPASS4 + db $42, $1D ; COMPASS5 + db $42, $1D ; COMPASS6 + db $42, $1D ; COMPASS7 + db $42, $1D ; COMPASS8 + db $42, $1D ; COMPASS9 + db $44, $1C ; STONE_BEAK1 + db $44, $1C ; STONE_BEAK2 + db $44, $1C ; STONE_BEAK3 + db $44, $1C ; STONE_BEAK4 + db $44, $1C ; STONE_BEAK5 + db $44, $1C ; STONE_BEAK6 + db $44, $1C ; STONE_BEAK7 + db $44, $1C ; STONE_BEAK8 + db $44, $1C ; STONE_BEAK9 + db $46, $1C ; NIGHTMARE_KEY1 + db $46, $1C ; NIGHTMARE_KEY2 + db $46, $1C ; NIGHTMARE_KEY3 + db $46, $1C ; NIGHTMARE_KEY4 + db $46, $1C ; NIGHTMARE_KEY5 + db $46, $1C ; NIGHTMARE_KEY6 + db $46, $1C ; NIGHTMARE_KEY7 + db $46, $1C ; NIGHTMARE_KEY8 + db $46, $1C ; NIGHTMARE_KEY9 + db $4C, $1C ; Toadstool + +LargeItemSpriteTable: + db $AC, $02, $AC, $22 ; heart piece + db $54, $0A, $56, $0A ; bowwow + db $2A, $41, $2A, $61 ; 10 arrows + db $2A, $41, $2A, $61 ; single arrow + db $0E, $1C, $22, $0C ; powder upgrade + db $00, $0D, $22, $0C ; bomb upgrade + db $08, $1C, $22, $0C ; arrow upgrade + db $48, $0A, $48, $2A ; red tunic + db $48, $0B, $48, $2B ; blue tunic + db $2A, $0C, $2A, $2C ; heart container + db $2A, $0F, $2A, $2F ; bad heart container + db $70, $09, $70, $29 ; song 1 + db $72, $0B, $72, $2B ; song 2 + db $74, $08, $74, $28 ; song 3 + db $80, $0E, $82, $0E ; Instrument1 + db $84, $0E, $86, $0E ; Instrument2 + db $88, $0E, $8A, $0E ; Instrument3 + db $8C, $0E, $8E, $0E ; Instrument4 + db $90, $0E, $92, $0E ; Instrument5 + db $94, $0E, $96, $0E ; Instrument6 + db $98, $0E, $9A, $0E ; Instrument7 + db $9C, $0E, $9E, $0E ; Instrument8 + db $A6, $2B, $A4, $2B ; Rooster + db $1A, $0E, $1C, $0E ; TradeItem1 + db $B0, $0C, $B2, $0C ; TradeItem2 + db $B4, $0C, $B6, $0C ; TradeItem3 + db $B8, $0C, $BA, $0C ; TradeItem4 + db $BC, $0C, $BE, $0C ; TradeItem5 + db $C0, $0C, $C2, $0C ; TradeItem6 + db $C4, $0C, $C6, $0C ; TradeItem7 + db $C8, $0C, $CA, $0C ; TradeItem8 + db $CC, $0C, $CE, $0C ; TradeItem9 + db $D0, $0C, $D2, $0C ; TradeItem10 + db $D4, $0D, $D6, $0D ; TradeItem11 + db $D8, $0D, $DA, $0D ; TradeItem12 + db $DC, $0D, $DE, $0D ; TradeItem13 + db $E0, $0D, $E2, $0D ; TradeItem14 + +ItemMessageTable: + db $90, $3D, $89, $93, $94, $95, $96, $97, $98, $99, $9A, $9B, $9C, $9D, $D9, $A2 + db $A0, $A1, $A3, $A4, $A5, $E8, $A6, $A7, $A8, $A9, $AA, $AC, $AB, $AD, $AE, $C9 + db $EF, $BE, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9 + db $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9 + ; $40 + db $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9 + db $0F, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 + db $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 + db $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 + ; $80 + db $4F, $C8, $CA, $CB, $E2, $E3, $E4, $CC, $CD, $2A, $2B, $C9, $C9, $C9, $C9, $C9 + db $C9, $C9, $C9, $C9, $C9, $C9, $B8, $44, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9 + db $C9, $C9, $C9, $C9, $9D + +RenderDroppedKey: + ;TODO: See EntityInitKeyDropPoint for a few special cases to unload. + +RenderHeartPiece: + ; Check if our chest type is already loaded + ld hl, $C2C0 + add hl, bc + ld a, [hl] + and a + jr nz, .droppedKeyTypeLoaded + inc [hl] + + ;Load the chest type from the chest table. + ld hl, $7800 + call OffsetPointerByRoomNumber + + ld a, [hl] + ldh [$F1], a ; set currentEntitySpriteVariant + call $3B0C ; SetEntitySpriteVariant + + and $80 + ld hl, $C340 + add hl, bc + ld a, [hl] + jr z, .singleSprite + ; We potentially need to fix the physics flags table to allocate 2 sprites for us + and $F8 + or $02 + ld [hl], a + jr .droppedKeyTypeLoaded +.singleSprite: + and $F8 + or $01 + ld [hl], a +.droppedKeyTypeLoaded: + jp RenderChestItem + + +OffsetPointerByRoomNumber: + ldh a, [$F6] ; map room + ld e, a + ld a, [$DBA5] ; is indoor + ld d, a + ldh a, [$F7] ; mapId + cp $FF + jr nz, .notColorDungeon + + ld d, $03 + jr .notCavesA + +.notColorDungeon: + cp $1A + jr nc, .notCavesA + cp $06 + jr c, .notCavesA + inc d +.notCavesA: + add hl, de + ret + +GiveItemAndMessageForRoom: + ;Load the chest type from the chest table. + ld hl, $7800 + call OffsetPointerByRoomNumber + ld a, [hl] + ldh [$F1], a + call GiveItemFromChest + jp ItemMessage + +GiveItemAndMessageForRoomMultiworld: + ;Load the chest type from the chest table. + ld hl, $7800 + call OffsetPointerByRoomNumber + ld a, [hl] + ldh [$F1], a + call GiveItemFromChestMultiworld + jp ItemMessageMultiworld + +RenderItemForRoom: + ;Load the chest type from the chest table. + ld hl, $7800 + call OffsetPointerByRoomNumber + ld a, [hl] + ldh [$F1], a + jp RenderChestItem + +; Increase the amount of checks we completed, unless we are on the multichest room. +IncreaseCheckCounter: + ldh a, [$F6] ; map room + cp $F2 + jr nz, .noMultiChest + ld a, [$DBA5] ; is indoor + and a + jr z, .noMultiChest + ldh a, [$F7] ; mapId + cp $0A + ret z + +.noMultiChest: + call $27D0 ; Enable SRAM + ld hl, $B010 +.loop: + ld a, [hl] + and a ; clear carry flag + inc a + daa + ldi [hl], a + ret nc + jr .loop diff --git a/worlds/ladx/LADXR/patches/bank3e.asm/itemnames.asm b/worlds/ladx/LADXR/patches/bank3e.asm/itemnames.asm new file mode 100644 index 0000000000..8495d0898b --- /dev/null +++ b/worlds/ladx/LADXR/patches/bank3e.asm/itemnames.asm @@ -0,0 +1,494 @@ + +BuildRemoteItemMessage: + ld de, wCustomMessage + call CustomItemMessageThreeFour + ld a, $A0 ; low of wCustomMessage + cp e + ret nz + +BuildItemMessage: + ld hl, ItemNamePointers + ldh a, [$F1] + ld d, $00 + ld e, a + add hl, de + add hl, de + ldi a, [hl] + ld h, [hl] + ld l, a + ld de, wCustomMessage + jp MessageCopyString + + ; And then see if the custom item message func wants to override + + ; add hl, de + + +CustomItemMessageThreeFour: + ; the stack _should_ have the address to return to here, so we can just pop it when we're done + ld a, $34 ; Set bank number + ld hl, $4000 ; Set next address + push hl + jp $080C ; switch bank + +FoundItemForOtherPlayerPostfix: + db m" for player X", $ff +GotItemFromOtherPlayerPostfix: + db m" from player X", $ff +SpaceFrom: + db " from ", $ff, $ff +SpaceFor: + db " for ", $ff, $ff +MessagePad: + jr .start ; goto start +.loop: + ld a, $20 ; a = ' ' + ld [de], a ; *de = ' ' + inc de ; de++ + ld a, $ff ; a = 0xFF + ld [de], a ; *de = 0xff +.start: + ld a, e ; a = de & 0xF + and $0F ; a &= 0x0xF + jr nz, .loop ; if a != 0, goto loop + ret + +MessageAddTargetPlayer: + call MessagePad + ld hl, FoundItemForOtherPlayerPostfix + call MessageCopyString + ret + +MessageAddFromPlayerOld: + call MessagePad + ld hl, GotItemFromOtherPlayerPostfix + call MessageCopyString + ret + +; hahaha none of this follows calling conventions +MessageAddPlayerName: + ; call MessagePad + ld h, 0 ; bc = a, hl = a + ld l, a + ld b, 0 + ld c, a + add hl, hl ; 2 + add hl, hl ; 4 + add hl, hl ; 8 + add hl, hl ; 16 + add hl, bc ; 17 + ld bc, MultiNamePointers + add hl, bc ; hl = MultiNamePointers + wLinkGiveItemFrom * 17 + call MessageCopyString + ret + +ItemNamePointers: + dw ItemNamePowerBracelet + dw ItemNameShield + dw ItemNameBow + dw ItemNameHookshot + dw ItemNameMagicRod + dw ItemNamePegasusBoots + dw ItemNameOcarina + dw ItemNameFeather + dw ItemNameShovel + dw ItemNameMagicPowder + dw ItemNameBomb + dw ItemNameSword + dw ItemNameFlippers + dw ItemNameNone + dw ItemNameBoomerang + dw ItemNameSlimeKey + dw ItemNameMedicine + dw ItemNameTailKey + dw ItemNameAnglerKey + dw ItemNameFaceKey + dw ItemNameBirdKey + dw ItemNameGoldLeaf + dw ItemNameMap + dw ItemNameCompass + dw ItemNameStoneBeak + dw ItemNameNightmareKey + dw ItemNameSmallKey + dw ItemNameRupees50 + dw ItemNameRupees20 + dw ItemNameRupees100 + dw ItemNameRupees200 + dw ItemNameRupees500 + dw ItemNameSeashell + dw ItemNameMessage + dw ItemNameGel + dw ItemNameKey1 + dw ItemNameKey2 + dw ItemNameKey3 + dw ItemNameKey4 + dw ItemNameKey5 + dw ItemNameKey6 + dw ItemNameKey7 + dw ItemNameKey8 + dw ItemNameKey9 + dw ItemNameMap1 + dw ItemNameMap2 + dw ItemNameMap3 + dw ItemNameMap4 + dw ItemNameMap5 + dw ItemNameMap6 + dw ItemNameMap7 + dw ItemNameMap8 + dw ItemNameMap9 + dw ItemNameCompass1 + dw ItemNameCompass2 + dw ItemNameCompass3 + dw ItemNameCompass4 + dw ItemNameCompass5 + dw ItemNameCompass6 + dw ItemNameCompass7 + dw ItemNameCompass8 + dw ItemNameCompass9 + dw ItemNameStoneBeak1 + dw ItemNameStoneBeak2 + dw ItemNameStoneBeak3 + dw ItemNameStoneBeak4 + dw ItemNameStoneBeak5 + dw ItemNameStoneBeak6 + dw ItemNameStoneBeak7 + dw ItemNameStoneBeak8 + dw ItemNameStoneBeak9 + dw ItemNameNightmareKey1 + dw ItemNameNightmareKey2 + dw ItemNameNightmareKey3 + dw ItemNameNightmareKey4 + dw ItemNameNightmareKey5 + dw ItemNameNightmareKey6 + dw ItemNameNightmareKey7 + dw ItemNameNightmareKey8 + dw ItemNameNightmareKey9 + dw ItemNameToadstool + dw ItemNameNone ; 0x51 + dw ItemNameNone ; 0x52 + dw ItemNameNone ; 0x53 + dw ItemNameNone ; 0x54 + dw ItemNameNone ; 0x55 + dw ItemNameNone ; 0x56 + dw ItemNameNone ; 0x57 + dw ItemNameNone ; 0x58 + dw ItemNameNone ; 0x59 + dw ItemNameNone ; 0x5a + dw ItemNameNone ; 0x5b + dw ItemNameNone ; 0x5c + dw ItemNameNone ; 0x5d + dw ItemNameNone ; 0x5e + dw ItemNameNone ; 0x5f + dw ItemNameNone ; 0x60 + dw ItemNameNone ; 0x61 + dw ItemNameNone ; 0x62 + dw ItemNameNone ; 0x63 + dw ItemNameNone ; 0x64 + dw ItemNameNone ; 0x65 + dw ItemNameNone ; 0x66 + dw ItemNameNone ; 0x67 + dw ItemNameNone ; 0x68 + dw ItemNameNone ; 0x69 + dw ItemNameNone ; 0x6a + dw ItemNameNone ; 0x6b + dw ItemNameNone ; 0x6c + dw ItemNameNone ; 0x6d + dw ItemNameNone ; 0x6e + dw ItemNameNone ; 0x6f + dw ItemNameNone ; 0x70 + dw ItemNameNone ; 0x71 + dw ItemNameNone ; 0x72 + dw ItemNameNone ; 0x73 + dw ItemNameNone ; 0x74 + dw ItemNameNone ; 0x75 + dw ItemNameNone ; 0x76 + dw ItemNameNone ; 0x77 + dw ItemNameNone ; 0x78 + dw ItemNameNone ; 0x79 + dw ItemNameNone ; 0x7a + dw ItemNameNone ; 0x7b + dw ItemNameNone ; 0x7c + dw ItemNameNone ; 0x7d + dw ItemNameNone ; 0x7e + dw ItemNameNone ; 0x7f + dw ItemNameHeartPiece ; 0x80 + dw ItemNameBowwow + dw ItemName10Arrows + dw ItemNameSingleArrow + dw ItemNamePowderUpgrade + dw ItemNameBombUpgrade + dw ItemNameArrowUpgrade + dw ItemNameRedTunic + dw ItemNameBlueTunic + dw ItemNameHeartContainer + dw ItemNameBadHeartContainer + dw ItemNameSong1 + dw ItemNameSong2 + dw ItemNameSong3 + dw ItemInstrument1 + dw ItemInstrument2 + dw ItemInstrument3 + dw ItemInstrument4 + dw ItemInstrument5 + dw ItemInstrument6 + dw ItemInstrument7 + dw ItemInstrument8 + dw ItemRooster + dw ItemTradeQuest1 + dw ItemTradeQuest2 + dw ItemTradeQuest3 + dw ItemTradeQuest4 + dw ItemTradeQuest5 + dw ItemTradeQuest6 + dw ItemTradeQuest7 + dw ItemTradeQuest8 + dw ItemTradeQuest9 + dw ItemTradeQuest10 + dw ItemTradeQuest11 + dw ItemTradeQuest12 + dw ItemTradeQuest13 + dw ItemTradeQuest14 + +ItemNameNone: + db m"NONE", $ff + +ItemNamePowerBracelet: + db m"Got the {POWER_BRACELET}", $ff +ItemNameShield: + db m"Got a {SHIELD}", $ff +ItemNameBow: + db m"Got the {BOW}", $ff +ItemNameHookshot: + db m"Got the {HOOKSHOT}", $ff +ItemNameMagicRod: + db m"Got the {MAGIC_ROD}", $ff +ItemNamePegasusBoots: + db m"Got the {PEGASUS_BOOTS}", $ff +ItemNameOcarina: + db m"Got the {OCARINA}", $ff +ItemNameFeather: + db m"Got the {FEATHER}", $ff +ItemNameShovel: + db m"Got the {SHOVEL}", $ff +ItemNameMagicPowder: + db m"Got {MAGIC_POWDER}", $ff +ItemNameBomb: + db m"Got {BOMB}", $ff +ItemNameSword: + db m"Got a {SWORD}", $ff +ItemNameFlippers: + db m"Got the {FLIPPERS}", $ff +ItemNameBoomerang: + db m"Got the {BOOMERANG}", $ff +ItemNameSlimeKey: + db m"Got the {SLIME_KEY}", $ff +ItemNameMedicine: + db m"Got some {MEDICINE}", $ff +ItemNameTailKey: + db m"Got the {TAIL_KEY}", $ff +ItemNameAnglerKey: + db m"Got the {ANGLER_KEY}", $ff +ItemNameFaceKey: + db m"Got the {FACE_KEY}", $ff +ItemNameBirdKey: + db m"Got the {BIRD_KEY}", $ff +ItemNameGoldLeaf: + db m"Got the {GOLD_LEAF}", $ff +ItemNameMap: + db m"Got the {MAP}", $ff +ItemNameCompass: + db m"Got the {COMPASS}", $ff +ItemNameStoneBeak: + db m"Got the {STONE_BEAK}", $ff +ItemNameNightmareKey: + db m"Got the {NIGHTMARE_KEY}", $ff +ItemNameSmallKey: + db m"Got a {KEY}", $ff +ItemNameRupees50: + db m"Got 50 {RUPEES}", $ff +ItemNameRupees20: + db m"Got 20 {RUPEES}", $ff +ItemNameRupees100: + db m"Got 100 {RUPEES}", $ff +ItemNameRupees200: + db m"Got 200 {RUPEES}", $ff +ItemNameRupees500: + db m"Got 500 {RUPEES}", $ff +ItemNameSeashell: + db m"Got a {SEASHELL}", $ff +ItemNameGel: + db m"Got a Zol Attack", $ff +ItemNameMessage: + db m"Got ... nothing?", $ff +ItemNameKey1: + db m"Got a {KEY1}", $ff +ItemNameKey2: + db m"Got a {KEY2}", $ff +ItemNameKey3: + db m"Got a {KEY3}", $ff +ItemNameKey4: + db m"Got a {KEY4}", $ff +ItemNameKey5: + db m"Got a {KEY5}", $ff +ItemNameKey6: + db m"Got a {KEY6}", $ff +ItemNameKey7: + db m"Got a {KEY7}", $ff +ItemNameKey8: + db m"Got a {KEY8}", $ff +ItemNameKey9: + db m"Got a {KEY9}", $ff +ItemNameMap1: + db m"Got the {MAP1}", $ff +ItemNameMap2: + db m"Got the {MAP2}", $ff +ItemNameMap3: + db m"Got the {MAP3}", $ff +ItemNameMap4: + db m"Got the {MAP4}", $ff +ItemNameMap5: + db m"Got the {MAP5}", $ff +ItemNameMap6: + db m"Got the {MAP6}", $ff +ItemNameMap7: + db m"Got the {MAP7}", $ff +ItemNameMap8: + db m"Got the {MAP8}", $ff +ItemNameMap9: + db m"Got the {MAP9}", $ff +ItemNameCompass1: + db m"Got the {COMPASS1}", $ff +ItemNameCompass2: + db m"Got the {COMPASS2}", $ff +ItemNameCompass3: + db m"Got the {COMPASS3}", $ff +ItemNameCompass4: + db m"Got the {COMPASS4}", $ff +ItemNameCompass5: + db m"Got the {COMPASS5}", $ff +ItemNameCompass6: + db m"Got the {COMPASS6}", $ff +ItemNameCompass7: + db m"Got the {COMPASS7}", $ff +ItemNameCompass8: + db m"Got the {COMPASS8}", $ff +ItemNameCompass9: + db m"Got the {COMPASS9}", $ff +ItemNameStoneBeak1: + db m"Got the {STONE_BEAK1}", $ff +ItemNameStoneBeak2: + db m"Got the {STONE_BEAK2}", $ff +ItemNameStoneBeak3: + db m"Got the {STONE_BEAK3}", $ff +ItemNameStoneBeak4: + db m"Got the {STONE_BEAK4}", $ff +ItemNameStoneBeak5: + db m"Got the {STONE_BEAK5}", $ff +ItemNameStoneBeak6: + db m"Got the {STONE_BEAK6}", $ff +ItemNameStoneBeak7: + db m"Got the {STONE_BEAK7}", $ff +ItemNameStoneBeak8: + db m"Got the {STONE_BEAK8}", $ff +ItemNameStoneBeak9: + db m"Got the {STONE_BEAK9}", $ff +ItemNameNightmareKey1: + db m"Got the {NIGHTMARE_KEY1}", $ff +ItemNameNightmareKey2: + db m"Got the {NIGHTMARE_KEY2}", $ff +ItemNameNightmareKey3: + db m"Got the {NIGHTMARE_KEY3}", $ff +ItemNameNightmareKey4: + db m"Got the {NIGHTMARE_KEY4}", $ff +ItemNameNightmareKey5: + db m"Got the {NIGHTMARE_KEY5}", $ff +ItemNameNightmareKey6: + db m"Got the {NIGHTMARE_KEY6}", $ff +ItemNameNightmareKey7: + db m"Got the {NIGHTMARE_KEY7}", $ff +ItemNameNightmareKey8: + db m"Got the {NIGHTMARE_KEY8}", $ff +ItemNameNightmareKey9: + db m"Got the {NIGHTMARE_KEY9}", $ff +ItemNameToadstool: + db m"Got the {TOADSTOOL}", $ff + +ItemNameHeartPiece: + db m"Got the {HEART_PIECE}", $ff +ItemNameBowwow: + db m"Got the {BOWWOW}", $ff +ItemName10Arrows: + db m"Got {ARROWS_10}", $ff +ItemNameSingleArrow: + db m"Got the {SINGLE_ARROW}", $ff +ItemNamePowderUpgrade: + db m"Got the {MAX_POWDER_UPGRADE}", $ff +ItemNameBombUpgrade: + db m"Got the {MAX_BOMBS_UPGRADE}", $ff +ItemNameArrowUpgrade: + db m"Got the {MAX_ARROWS_UPGRADE}", $ff +ItemNameRedTunic: + db m"Got the {RED_TUNIC}", $ff +ItemNameBlueTunic: + db m"Got the {BLUE_TUNIC}", $ff +ItemNameHeartContainer: + db m"Got a {HEART_CONTAINER}", $ff +ItemNameBadHeartContainer: + db m"Got the {BAD_HEART_CONTAINER}", $ff +ItemNameSong1: + db m"Got the {SONG1}", $ff +ItemNameSong2: + db m"Got {SONG2}", $ff +ItemNameSong3: + db m"Got {SONG3}", $ff + +ItemInstrument1: + db m"You've got the {INSTRUMENT1}", $ff +ItemInstrument2: + db m"You've got the {INSTRUMENT2}", $ff +ItemInstrument3: + db m"You've got the {INSTRUMENT3}", $ff +ItemInstrument4: + db m"You've got the {INSTRUMENT4}", $ff +ItemInstrument5: + db m"You've got the {INSTRUMENT5}", $ff +ItemInstrument6: + db m"You've got the {INSTRUMENT6}", $ff +ItemInstrument7: + db m"You've got the {INSTRUMENT7}", $ff +ItemInstrument8: + db m"You've got the {INSTRUMENT8}", $ff + +ItemRooster: + db m"You've got the {ROOSTER}", $ff + +ItemTradeQuest1: + db m"You've got the Yoshi Doll", $ff +ItemTradeQuest2: + db m"You've got the Ribbon", $ff +ItemTradeQuest3: + db m"You've got the Dog Food", $ff +ItemTradeQuest4: + db m"You've got the Bananas", $ff +ItemTradeQuest5: + db m"You've got the Stick", $ff +ItemTradeQuest6: + db m"You've got the Honeycomb", $ff +ItemTradeQuest7: + db m"You've got the Pineapple", $ff +ItemTradeQuest8: + db m"You've got the Hibiscus", $ff +ItemTradeQuest9: + db m"You've got the Letter", $ff +ItemTradeQuest10: + db m"You've got the Broom", $ff +ItemTradeQuest11: + db m"You've got the Fishing Hook", $ff +ItemTradeQuest12: + db m"You've got the Necklace", $ff +ItemTradeQuest13: + db m"You've got the Scale", $ff +ItemTradeQuest14: + db m"You've got the Magnifying Lens", $ff + +MultiNamePointers: \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/bank3e.asm/link.asm b/worlds/ladx/LADXR/patches/bank3e.asm/link.asm new file mode 100644 index 0000000000..266dd5fc5b --- /dev/null +++ b/worlds/ladx/LADXR/patches/bank3e.asm/link.asm @@ -0,0 +1,89 @@ +; Handle the serial link cable +#IF HARDWARE_LINK +; FF> = Idle +; D6> = Read: D0><[L] D1><[H] [HL]> +; D9> = Write: D8><[L] D9><[H] DA><[^DATA] DB><[DATA] +; DD> = OrW: D8><[L] D9><[H] DA><[^DATA] DB><[DATA] (used to set flags without requiring a slow read,modify,write race condition) + +handleSerialLink: + ; Check if we got a byte from hardware + ldh a, [$01] + + cp $D6 + jr z, serialReadMem + cp $D9 + jr z, serialWriteMem + cp $DD + jr z, serialOrMem + +finishSerialLink: + ; Do a new idle transfer. + ld a, $E4 + ldh [$01], a + ld a, $81 + ldh [$02], a + ret + +serialReadMem: + ld a, $D0 + call serialTransfer + ld h, a + ld a, $D1 + call serialTransfer + ld l, a + ld a, [hl] + call serialTransfer + jr finishSerialLink + +serialWriteMem: + ld a, $D8 + call serialTransfer + ld h, a + ld a, $D9 + call serialTransfer + ld l, a + ld a, $DA + call serialTransfer + cpl + ld c, a + ld a, $DB + call serialTransfer + cp c + jr nz, finishSerialLink + ld [hl], a + jr finishSerialLink + +serialOrMem: + ld a, $D8 + call serialTransfer + ld h, a + ld a, $D9 + call serialTransfer + ld l, a + ld a, $DA + call serialTransfer + cpl + ld c, a + ld a, $DB + call serialTransfer + cp c + jr nz, finishSerialLink + ld c, a + ld a, [hl] + or c + ld [hl], a + jr finishSerialLink + +; Transfer A to the serial link and wait for it to be done and return the result in A +serialTransfer: + ldh [$01], a + ld a, $81 + ldh [$02], a +.loop: + ldh a, [$02] + and $80 + jr nz, .loop + ldh a, [$01] + ret + +#ENDIF diff --git a/worlds/ladx/LADXR/patches/bank3e.asm/message.asm b/worlds/ladx/LADXR/patches/bank3e.asm/message.asm new file mode 100644 index 0000000000..33062c6e9b --- /dev/null +++ b/worlds/ladx/LADXR/patches/bank3e.asm/message.asm @@ -0,0 +1,16 @@ +MessageCopyString: +.loop: + ldi a, [hl] + ld [de], a + cp $ff + ret z + inc de + jr .loop + +MessageAddSpace: + ld a, $20 + ld [de], a + inc de + ld a, $ff + ld [de], a + ret diff --git a/worlds/ladx/LADXR/patches/bank3e.asm/multiworld.asm b/worlds/ladx/LADXR/patches/bank3e.asm/multiworld.asm new file mode 100644 index 0000000000..d7804cba6b --- /dev/null +++ b/worlds/ladx/LADXR/patches/bank3e.asm/multiworld.asm @@ -0,0 +1,355 @@ +; Handle the multiworld link + +MainLoop: +#IF HARDWARE_LINK + call handleSerialLink +#ENDIF + ; Check if the gameplay is world + ld a, [$DB95] + cp $0B + ret nz + ; Check if the world subtype is the normal one + ld a, [$DB96] + cp $07 + ret nz + ; Check if we are moving between rooms + ld a, [$C124] + and a + ret nz + ; Check if link is in a normal walking/swimming state + ld a, [$C11C] + cp $02 + ret nc + ; Check if a dialog is open + ld a, [$C19F] + and a + ret nz + ; Check if interaction is blocked + ldh a, [$A1] + and a + ret nz + + ld a, [wLinkSpawnDelay] + and a + jr z, .allowSpawn + dec a + ld [wLinkSpawnDelay], a + jr .noSpawn + +.allowSpawn: + ld a, [wZolSpawnCount] + and a + call nz, LinkSpawnSlime + ld a, [wCuccoSpawnCount] + and a + call nz, LinkSpawnCucco + ld a, [wDropBombSpawnCount] + and a + call nz, LinkSpawnBomb +.noSpawn: + + ; Have an item to give? + ld hl, wLinkStatusBits + bit 0, [hl] + ret z + + ; Give an item to the player + ld a, [wLinkGiveItem] + ; if zol: + cp $22 ; zol item + jr z, LinkGiveSlime + ; if special item + cp $F0 + jr nc, HandleSpecialItem + ; tmpChestItem = a + ldh [$F1], a + ; Give the item + call GiveItemFromChest + ; Paste the item text + call BuildItemMessage + ; Paste " from " + ld hl, SpaceFrom + call MessageCopyString + ; Paste the player name + ld a, [wLinkGiveItemFrom] + call MessageAddPlayerName + ld a, $C9 + ; hl = $wLinkStatusBits + ld hl, wLinkStatusBits + ; clear the 0 bit of *hl + res 0, [hl] + ; OpenDialog() + jp $2385 ; Opendialog in $000-$0FF range + +LinkGiveSlime: + ld a, $05 + ld [wZolSpawnCount], a + ld hl, wLinkStatusBits + res 0, [hl] + ret + +HandleSpecialItem: + ld hl, wLinkStatusBits + res 0, [hl] + + and $0F + rst 0 + dw SpecialSlimeStorm + dw SpecialCuccoParty + dw SpecialPieceOfPower + dw SpecialHealth + dw SpecialRandomTeleport + dw .ret + dw .ret + dw .ret + dw .ret + dw .ret + dw .ret + dw .ret + dw .ret + dw .ret + dw .ret + dw .ret +.ret: + ret + +SpecialSlimeStorm: + ld a, $20 + ld [wZolSpawnCount], a + ret +SpecialCuccoParty: + ld a, $20 + ld [wCuccoSpawnCount], a + ret +SpecialPieceOfPower: + ; Give the piece of power and the music + ld a, $01 + ld [$D47C], a + ld a, $27 + ld [$D368], a + ld a, $49 + ldh [$BD], a + ldh [$BF], a + ret +SpecialHealth: + ; Regen all health + ld hl, $DB93 + ld [hl], $FF + ret + +LinkSpawnSlime: + ld a, $1B + ld e, $08 + call $3B98 ; SpawnNewEntity in range + ret c + + ; Place somewhere random + call placeRandom + + ld hl, $C310 + add hl, de + ld [hl], $7F + + ld hl, wZolSpawnCount + dec [hl] + + call $280D + and $03 + ld [wLinkSpawnDelay], a + ret + +LinkSpawnCucco: + ld a, $6C + ld e, $04 + call $3B98 ; SpawnNewEntity in range + ret c + + ; Place where link is at. + ld hl, $C200 + add hl, de + ldh a, [$98] + ld [hl], a + ld hl, $C210 + add hl, de + ldh a, [$99] + ld [hl], a + + ; Set the "hits till cucco killer attack" much lower + ld hl, $C2B0 + add hl, de + ld a, $21 + ld [hl], a + + ld hl, wCuccoSpawnCount + dec [hl] + + call $280D + and $07 + ld [wLinkSpawnDelay], a + ret + +LinkSpawnBomb: + ld a, $02 + ld e, $08 + call $3B98 ; SpawnNewEntity in range + ret c + + call placeRandom + + ld hl, $C310 ; z pos + add hl, de + ld [hl], $4F + + ld hl, $C430 ; wEntitiesOptions1Table + add hl, de + res 0, [hl] + ld hl, $C2E0 ; wEntitiesTransitionCountdownTable + add hl, de + ld [hl], $80 + ld hl, $C440 ; wEntitiesPrivateState4Table + add hl, de + ld [hl], $01 + + ld hl, wDropBombSpawnCount + dec [hl] + + call $280D + and $1F + ld [wLinkSpawnDelay], a + ret + +placeRandom: + ; Place somewhere random + ld hl, $C200 + add hl, de + call $280D ; random number + and $7F + add a, $08 + ld [hl], a + ld hl, $C210 + add hl, de + call $280D ; random number + and $3F + add a, $20 + ld [hl], a + ret + +SpecialRandomTeleport: + xor a + ; Warp data + ld [$D401], a + ld [$D402], a + call $280D ; random number + ld [$D403], a + ld hl, RandomTeleportPositions + ld d, $00 + ld e, a + add hl, de + ld e, [hl] + ld a, e + and $0F + swap a + add a, $08 + ld [$D404], a + ld a, e + and $F0 + add a, $10 + ld [$D405], a + + ldh a, [$98] + swap a + and $0F + ld e, a + ldh a, [$99] + sub $08 + and $F0 + or e + ld [$D416], a ; wWarp0PositionTileIndex + + call $0C7D + ld a, $07 + ld [$DB96], a ; wGameplaySubtype + + ret + +Data_004_7AE5: ; @TODO Palette data + db $33, $62, $1A, $01, $FF, $0F, $FF, $7F + + +Deathlink: + ; Spawn the entity + ld a, $CA ; $7AF3: $3E $CA + call $3B86 ; $7AF5: $CD $86 $3B ;SpawnEntityTrampoline + ld a, $26 ; $7AF8: $3E $26 ; + ldh [$F4], a ; $7AFA: $E0 $F4 ; set noise + ; Set posX = linkX + ldh a, [$98] ; LinkX + ld hl, $C200 ; wEntitiesPosXTable + add hl, de + ld [hl], a + ; set posY = linkY - 54 + ldh a, [$99] ; LinkY + sub a, 54 + ld hl, $C210 ; wEntitiesPosYTable + add hl, de + ld [hl], a + ; wEntitiesPrivateState3Table + ld hl, $C2D0 ; $7B0A: $21 $D0 $C2 + add hl, de ; $7B0D: $19 + ld [hl], $01 ; $7B0E: $36 $01 + ; wEntitiesTransitionCountdownTable + ld hl, $C2E0 ; $7B10: $21 $E0 $C2 + add hl, de ; $7B13: $19 + ld [hl], $C0 ; $7B14: $36 $C0 + ; GetEntityTransitionCountdown + call $0C05 ; $7B16: $CD $05 $0C + ld [hl], $C0 ; $7B19: $36 $C0 + ; IncrementEntityState + call $3B12 ; $7B1B: $CD $12 $3B + + ; Remove medicine + xor a ; $7B1E: $AF + ld [$DB0D], a ; $7B1F: $EA $0D $DB ; ld [wHasMedicine], a + ; Reduce health by a lot + ld a, $FF ; $7B22: $3E $FF + ld [$DB94], a ; $7B24: $EA $94 $DB ; ld [wSubtractHealthBuffer], a + + ld hl, $DC88 ; $7B2C: $21 $88 $DC + ; Set palette + ld de, Data_004_7AE5 ; $7B2F: $11 $E5 $7A + +loop_7B32: + ld a, [de] ; $7B32: $1A + ; ld [hl+], a ; $7B33: $22 + db $22 + inc de ; $7B34: $13 + ld a, l ; $7B35: $7D + and $07 ; $7B36: $E6 $07 + jr nz, loop_7B32 ; $7B38: $20 $F8 + + ld a, $02 ; $7B3A: $3E $02 + ld [$DDD1], a ; $7B3C: $EA $D1 $DD + + ret + +; probalby wants +; ld a, $02 ; $7B40: $3E $02 + ;ldh [hLinkInteractiveMotionBlocked], a + +RandomTeleportPositions: + db $55, $54, $54, $54, $55, $55, $55, $54, $65, $55, $54, $65, $56, $56, $55, $55 + db $55, $45, $65, $54, $55, $55, $55, $55, $55, $55, $55, $58, $43, $57, $55, $55 + db $55, $55, $55, $55, $55, $54, $55, $53, $54, $56, $65, $65, $56, $55, $57, $65 + db $45, $55, $55, $55, $55, $55, $55, $55, $48, $45, $43, $34, $35, $35, $36, $34 + db $65, $55, $55, $54, $54, $54, $55, $54, $56, $65, $55, $55, $55, $55, $54, $54 + db $55, $55, $55, $55, $56, $55, $55, $54, $55, $55, $55, $53, $45, $35, $53, $46 + db $56, $55, $55, $55, $53, $55, $54, $54, $55, $55, $55, $54, $44, $55, $55, $54 + db $55, $55, $45, $55, $55, $54, $45, $45, $63, $55, $65, $55, $45, $45, $44, $54 + db $56, $56, $54, $55, $54, $55, $55, $55, $55, $55, $55, $56, $54, $55, $65, $56 + db $54, $54, $55, $65, $56, $54, $55, $56, $55, $55, $55, $66, $65, $65, $55, $56 + db $65, $55, $55, $75, $55, $55, $55, $54, $55, $55, $65, $57, $55, $54, $53, $45 + db $55, $56, $55, $55, $55, $45, $54, $55, $54, $55, $56, $55, $55, $55, $55, $54 + db $55, $55, $65, $55, $55, $54, $53, $58, $55, $05, $58, $55, $55, $55, $74, $55 + db $55, $55, $55, $55, $46, $55, $55, $56, $55, $55, $55, $54, $55, $45, $55, $55 + db $55, $55, $54, $55, $55, $55, $65, $55, $55, $46, $55, $55, $56, $55, $55, $55 + db $55, $55, $54, $55, $55, $55, $45, $36, $53, $51, $57, $53, $56, $54, $45, $46 diff --git a/worlds/ladx/LADXR/patches/bank3e.asm/owl.asm b/worlds/ladx/LADXR/patches/bank3e.asm/owl.asm new file mode 100644 index 0000000000..35bc53f59e --- /dev/null +++ b/worlds/ladx/LADXR/patches/bank3e.asm/owl.asm @@ -0,0 +1,63 @@ +HandleOwlStatue: + call GetRoomStatusAddressInHL + ld a, [hl] + and $20 + ret nz + ld a, [hl] + or $20 + ld [hl], a + + ld hl, $7B16 + call OffsetPointerByRoomNumber + ld a, [hl] + ldh [$F1], a + call ItemMessage + call GiveItemFromChest + ret + + + +GetRoomStatusAddressInHL: + ld a, [$DBA5] ; isIndoor + ld d, a + ld hl, $D800 + ldh a, [$F6] ; room nr + ld e, a + ldh a, [$F7] ; map nr + cp $FF + jr nz, .notColorDungeon + + ld d, $00 + ld hl, $DDE0 + jr .notIndoorB + +.notColorDungeon: + cp $1A + jr nc, .notIndoorB + + cp $06 + jr c, .notIndoorB + + inc d + +.notIndoorB: + add hl, de + ret + + +RenderOwlStatueItem: + ldh a, [$F6] ; map room + cp $B2 + jr nz, .NotYipYip + ; Add 2 to room to set room pointer to an empty room for trade items + add a, 2 + ldh [$F6], a + call RenderItemForRoom + ldh a, [$F6] ; map room + ; ...and undo it + sub a, 2 + ldh [$F6], a + ret +.NotYipYip: + call RenderItemForRoom + ret diff --git a/worlds/ladx/LADXR/patches/bank3e.py b/worlds/ladx/LADXR/patches/bank3e.py new file mode 100644 index 0000000000..d2b31adf91 --- /dev/null +++ b/worlds/ladx/LADXR/patches/bank3e.py @@ -0,0 +1,225 @@ +import os +import binascii +from ..assembler import ASM +from ..utils import formatText + + +def hasBank3E(rom): + return rom.banks[0x3E][0] != 0x00 + +def generate_name(l, i): + if i < len(l): + name = l[i] + else: + name = f"player {i}" + name = name[:16] + assert(len(name) <= 16) + return 'db "' + name + '"' + ', $ff' * (17 - len(name)) + '\n' + + +# Bank $3E is used for large chunks of custom code. +# Mainly for new chest and dropped items handling. +def addBank3E(rom, seed, player_id, player_name_list): + # No default text for getting the bow, so use an unused slot. + rom.texts[0x89] = formatText("Found the {BOW}!") + rom.texts[0xD9] = formatText("Found the {BOOMERANG}!") # owl text slot reuse + rom.texts[0xBE] = rom.texts[0x111] # owl text slot reuse to get the master skull message in the first dialog group + rom.texts[0xC8] = formatText("Found {BOWWOW}! Which monster put him in a chest? He is a good boi, and waits for you at the Swamp.") + rom.texts[0xC9] = 0xC0A0 # Custom message slot + rom.texts[0xCA] = formatText("Found {ARROWS_10}!") + rom.texts[0xCB] = formatText("Found a {SINGLE_ARROW}... joy?") + + # Create a trampoline to bank 0x3E in bank 0x00. + # There is very little room in bank 0, so we set this up as a single trampoline for multiple possible usages. + # the A register is preserved and can directly be used as a jumptable in page 3E. + # Trampoline at rst 8 + # the A register is preserved and can directly be used as a jumptable in page 3E. + rom.patch(0, 0x0008, "0000000000000000000000000000", ASM(""" + ld h, a + ld a, [$DBAF] + push af + ld a, $3E + call $080C ; switch bank + ld a, h + jp $4000 + """), fill_nop=True) + + # Special trampoline to jump to the damage-entity code, we use this from bowwow to damage instead of eat. + rom.patch(0x00, 0x0018, "000000000000000000000000000000", ASM(""" + ld a, $03 + ld [$2100], a + call $71C0 + ld a, [$DBAF] + ld [$2100], a + ret + """)) + + my_path = os.path.dirname(__file__) + rom.patch(0x3E, 0x0000, 0x2F00, ASM(""" + call MainJumpTable + pop af + jp $080C ; switch bank and return to normal code. + +MainJumpTable: + rst 0 ; JUMP TABLE + dw MainLoop ; 0 + dw RenderChestItem ; 1 + dw GiveItemFromChest ; 2 + dw ItemMessage ; 3 + dw RenderDroppedKey ; 4 + dw RenderHeartPiece ; 5 + dw GiveItemFromChestMultiworld ; 6 + dw CheckIfLoadBowWow ; 7 + dw BowwowEat ; 8 + dw HandleOwlStatue ; 9 + dw ItemMessageMultiworld ; A + dw GiveItemAndMessageForRoom ; B + dw RenderItemForRoom ; C + dw StartGameMarinMessage ; D + dw GiveItemAndMessageForRoomMultiworld ; E + dw RenderOwlStatueItem ; F + dw UpdateInventoryMenu ; 10 + dw LocalOnlyItemAndMessage ; 11 +StartGameMarinMessage: + ; Injection to reset our frame counter + call $27D0 ; Enable SRAM + ld hl, $B000 + xor a + ldi [hl], a ;subsecond counter + ld a, $08 ;(We set the counter to 8 seconds, as it takes 8 seconds before link wakes up and marin talks to him) + ldi [hl], a ;second counter + xor a + ldi [hl], a ;minute counter + ldi [hl], a ;hour counter + + ld hl, $B010 + ldi [hl], a ;check counter low + ldi [hl], a ;check counter high + + ; Show the normal message + ld a, $01 + jp $2385 + +TradeSequenceItemData: + ; tile attributes + db $0D, $0A, $0D, $0D, $0E, $0E, $0D, $0D, $0D, $0E, $09, $0A, $0A, $0D + ; tile index + db $1A, $B0, $B4, $B8, $BC, $C0, $C4, $C8, $CC, $D0, $D4, $D8, $DC, $E0 + +UpdateInventoryMenu: + ld a, [wTradeSequenceItem] + ld hl, wTradeSequenceItem2 + or [hl] + ret z + + ld hl, TradeSequenceItemData + ld a, [$C109] + ld e, a + ld d, $00 + add hl, de + + ; Check if we need to increase the counter + ldh a, [$E7] ; frame counter + and $0F + jr nz, .noInc + ld a, e + inc a + cp 14 + jr nz, .noWrap + xor a +.noWrap: + ld [$C109], a +.noInc: + + ; Check if we have the item + ld b, e + inc b + ld a, $01 + + ld de, wTradeSequenceItem +.shiftLoop: + dec b + jr z, .shiftLoopDone + sla a + jr nz, .shiftLoop + ; switching to second byte + ld de, wTradeSequenceItem2 + ld a, $01 + jr .shiftLoop +.shiftLoopDone: + ld b, a + ld a, [de] + and b + ret z ; skip this item + + ld b, [hl] + push hl + + ; Write the tile attribute data + ld a, $01 + ldh [$4F], a + + ld hl, $9C6E + call WriteToVRAM + inc hl + call WriteToVRAM + ld de, $001F + add hl, de + call WriteToVRAM + inc hl + call WriteToVRAM + + ; Write the tile data + xor a + ldh [$4F], a + + pop hl + ld de, 14 + add hl, de + ld b, [hl] + + ld hl, $9C6E + call WriteToVRAM + inc b + inc b + inc hl + call WriteToVRAM + ld de, $001F + add hl, de + dec b + call WriteToVRAM + inc hl + inc b + inc b + call WriteToVRAM + ret + +WriteToVRAM: + ldh a, [$41] + and $02 + jr nz, WriteToVRAM + ld [hl], b + ret +LocalOnlyItemAndMessage: + call GiveItemFromChest + call ItemMessage + ret + """ + open(os.path.join(my_path, "bank3e.asm/multiworld.asm"), "rt").read() + + open(os.path.join(my_path, "bank3e.asm/link.asm"), "rt").read() + + open(os.path.join(my_path, "bank3e.asm/chest.asm"), "rt").read() + + open(os.path.join(my_path, "bank3e.asm/bowwow.asm"), "rt").read() + + open(os.path.join(my_path, "bank3e.asm/message.asm"), "rt").read() + + open(os.path.join(my_path, "bank3e.asm/itemnames.asm"), "rt").read() + + "".join(generate_name(["The Server"] + player_name_list, i ) for i in range(100)) # allocate + + 'db "another world", $ff\n' + + open(os.path.join(my_path, "bank3e.asm/owl.asm"), "rt").read(), 0x4000), fill_nop=True) + # 3E:3300-3616: Multiworld flags per room (for both chests and dropped keys) + # 3E:3800-3B16: DroppedKey item types + # 3E:3B16-3E2C: Owl statue or trade quest items + + # Put 20 rupees in all owls by default. + rom.patch(0x3E, 0x3B16, "00" * 0x316, "1C" * 0x316) + + + # Prevent the photo album from crashing due to serial interrupts + rom.patch(0x28, 0x00D2, ASM("ld a, $09"), ASM("ld a, $01")) diff --git a/worlds/ladx/LADXR/patches/bank3f.py b/worlds/ladx/LADXR/patches/bank3f.py new file mode 100644 index 0000000000..8c6b86a7f3 --- /dev/null +++ b/worlds/ladx/LADXR/patches/bank3f.py @@ -0,0 +1,386 @@ +from ..assembler import ASM +from .. import utils + + +def addBank3F(rom): + # Bank3F is used to initialize the tile data in VRAM:1 at the start of the rom. + # The normal rom does not use this tile data to maintain GB compatibility. + rom.patch(0, 0x0150, ASM(""" + cp $11 ; is running on Game Boy Color? + jr nz, notGBC + ldh a, [$4d] + and $80 ; do we need to switch the CPU speed? + jr nz, speedSwitchDone + ; switch to GBC speed + ld a, $30 + ldh [$00], a + ld a, $01 + ldh [$4d], a + xor a + ldh [$ff], a + stop + db $00 + + speedSwitchDone: + xor a + ldh [$70], a + ld a, $01 ; isGBC = true + jr Init + + notGBC: + xor a ; isGBC = false + Init: + """), ASM(""" + ; Check if we are a color gameboy, we require a color version now. + cp $11 + jr nz, notGBC + + ; Switch to bank $3F to run our custom initializer + ld a, $3F + ld [$2100], a + call $4000 + ; Switch back to bank 0 after loading our own initializer + ld a, $01 + ld [$2100], a + + ; set a to 1 to indicate GBC + ld a, $01 + jr Init + notGBC: + xor a + Init: + """), fill_nop=True) + + rom.patch(0x3F, 0x0000, None, ASM(""" + ; switch speed + ld a, $30 + ldh [$00], a + ld a, $01 + ldh [$4d], a + xor a + ldh [$ff], a + stop + db $00 + + ; Switch VRAM bank + ld a, $01 + ldh [$4F], a + + call $28CF ; display off + + ; Use the GBC DMA to transfer our tile data + ld a, $68 + ldh [$51], a + ld a, $00 + ldh [$52], a + + ld a, $80 + ldh [$53], a + ld a, $00 + ldh [$54], a + + ld a, $7F + ldh [$55], a + + waitTillTransferDone: + ldh a, [$55] + and $80 + jr z, waitTillTransferDone + + ld a, $70 + ldh [$51], a + ld a, $00 + ldh [$52], a + + ld a, $88 + ldh [$53], a + ld a, $00 + ldh [$54], a + + ld a, $7F + ldh [$55], a + + waitTillTransferDone2: + ldh a, [$55] + and $80 + jr z, waitTillTransferDone2 + + ld a, $68 + ldh [$51], a + ld a, $00 + ldh [$52], a + + ld a, $90 + ldh [$53], a + ld a, $00 + ldh [$54], a + + ld a, $7F + ldh [$55], a + + waitTillTransferDone3: + ldh a, [$55] + and $80 + jr z, waitTillTransferDone3 + + ; Switch VRAM bank back + ld a, $00 + ldh [$4F], a + + ; Switch the display back on, else the later code hangs + ld a, $80 + ldh [$40], a + + speedSwitchDone: + xor a + ldh [$70], a + + ; Check if we are running on a bad emulator + ldh [$02], a + ldh a, [$02] + and $7c + cp $7c + jr nz, badEmu + + ; Enable the timer to run 32 times per second + xor a + ldh [$06], a + ld a, $04 + ldh [$07], a + + ; Set SB to $FF to indicate we have no data from hardware + ld a, $FF + ldh [$01], a + ret +badEmu: + xor a + ldh [$40], a ; switch display off + ; Load some palette + ld a, $80 + ldh [$68], a + xor a + ldh [$69], a + ldh [$69], a + ldh [$69], a + ldh [$69], a + + ; Load a different gfx tile for the first gfx + cpl + ld hl, $8000 + ld c, $10 +.loop: + ldi [hl], a + dec c + jr nz, .loop + + ld a, $01 + ld [$9800], a + ld [$9820], a + ld [$9840], a + ld [$9860], a + ld [$9880], a + + ld [$9801], a + ld [$9841], a + ld [$9881], a + + ld [$9822], a + ld [$9862], a + + ld [$9824], a + ld [$9844], a + ld [$9864], a + ld [$9884], a + + ld [$9805], a + ld [$9845], a + + ld [$9826], a + ld [$9846], a + ld [$9866], a + ld [$9886], a + + ld [$9808], a + ld [$9828], a + ld [$9848], a + ld [$9868], a + ld [$9888], a + + ld [$9809], a + ld [$9889], a + + ld [$982A], a + ld [$984A], a + ld [$986A], a + + ld [$9900], a + ld [$9920], a + ld [$9940], a + ld [$9960], a + ld [$9980], a + + ld [$9901], a + ld [$9941], a + ld [$9981], a + + ld [$9903], a + ld [$9923], a + ld [$9943], a + ld [$9963], a + ld [$9983], a + + ld [$9904], a + ld [$9925], a + ld [$9906], a + + ld [$9907], a + ld [$9927], a + ld [$9947], a + ld [$9967], a + ld [$9987], a + + ld [$9909], a + ld [$9929], a + ld [$9949], a + ld [$9969], a + ld [$9989], a + + ld [$998A], a + + ld [$990B], a + ld [$992B], a + ld [$994B], a + ld [$996B], a + ld [$998B], a + + ; lcd on + ld a, $91 + ldh [$40], a +blockBadEmu: + di + jr blockBadEmu + + """)) + + # Copy all normal item graphics + rom.banks[0x3F][0x2800:0x2B00] = rom.banks[0x2C][0x0800:0x0B00] # main items + rom.banks[0x3F][0x2B00:0x2C00] = rom.banks[0x2C][0x0C00:0x0D00] # overworld key items + rom.banks[0x3F][0x2C00:0x2D00] = rom.banks[0x32][0x3D00:0x3E00] # dungeon key items + # Create ruppee for palettes 0-3 + rom.banks[0x3F][0x2B80:0x2BA0] = rom.banks[0x3F][0x2A60:0x2A80] + for n in range(0x2B80, 0x2BA0, 2): + rom.banks[0x3F][n+1] ^= rom.banks[0x3F][n] + + # Create capacity upgrade arrows + rom.banks[0x3F][0x2A30:0x2A40] = utils.createTileData(""" + 33 + 3113 + 311113 +33311333 + 3113 + 3333 +""") + rom.banks[0x3F][0x2A20:0x2A30] = rom.banks[0x3F][0x2A30:0x2A40] + for n in range(0x2A20, 0x2A40, 2): + rom.banks[0x3F][n] |= rom.banks[0x3F][n + 1] + + # Add the slime key and mushroom which are not in the above sets + rom.banks[0x3F][0x2CC0:0x2D00] = rom.banks[0x2C][0x28C0:0x2900] + # Add tunic sprites as well. + rom.banks[0x3F][0x2C80:0x2CA0] = rom.banks[0x35][0x0F00:0x0F20] + + # Add the bowwow sprites + rom.banks[0x3F][0x2D00:0x2E00] = rom.banks[0x2E][0x2400:0x2500] + + # Zol sprites, so we can have zol anywhere from a chest + rom.banks[0x3F][0x2E00:0x2E60] = rom.banks[0x2E][0x1120:0x1180] + # Patch gel(zol) entity to load sprites from the 2nd bank + rom.patch(0x06, 0x3C09, "5202522254025422" "5200522054005420", "600A602A620A622A" "6008602862086228") + rom.patch(0x07, 0x329B, "FFFFFFFF" "FFFFFFFF" "54005420" "52005220" "56005600", + "FFFFFFFF" "FFFFFFFF" "62086228" "60086028" "64086408") + rom.patch(0x06, 0x3BFA, "56025622", "640A642A"); + + + # Cucco + rom.banks[0x3F][0x2E80:0x2F00] = rom.banks[0x32][0x2500:0x2580] + # Patch the cucco graphics to load from 2nd vram bank + rom.patch(0x05, 0x0514, + "5001" "5201" "5401" "5601" "5221" "5021" "5621" "5421", + "6809" "6A09" "6C09" "6E09" "6A29" "6829" "6E29" "6C29") + # Song symbols + rom.banks[0x3F][0x2F00:0x2F60] = utils.createTileData(""" + + + ... + . .222 + .2.2222 +.22.222. +.22222.3 +.2..22.3 + .33...3 + .33.3.3 + ..233.3 +.22.2333 +.222.233 + .222... + ... +""" + """ + + + .. + .22 + .223 + ..222 + .33.22 + .3..22 + .33.33 + ..23. + ..233. + .22.333 +.22..233 + .. .23 + .. +""" + """ + + + ... + .222. + .2.332 + .23.32 + .233.2 + .222222 +.2222222 +.2..22.2 +.2.3.222 +.22...22 + .2333.. + .23333 + .....""", " .23") + + # Ghost + rom.banks[0x3F][0x2F60:0x2FE0] = rom.banks[0x32][0x1800:0x1880] + + # Instruments + rom.banks[0x3F][0x3000:0x3200] = rom.banks[0x31][0x1000:0x1200] + # Patch the egg song event to use the 2nd vram sprites + rom.patch(0x19, 0x0BAC, + "5006520654065606" + "58065A065C065E06" + "6006620664066606" + "68066A066C066E06", + "800E820E840E860E" + "880E8A0E8C0E8E0E" + "900E920E940E960E" + "980E9A0E9C0E9E0E" + ) + + # Rooster + rom.banks[0x3F][0x3200:0x3300] = rom.banks[0x32][0x1D00:0x1E00] + rom.patch(0x19, 0x19BC, + "42234023" "46234423" "40034203" "44034603" "4C034C23" "4E034E23" "48034823" "4A034A23", + "A22BA02B" "A62BA42B" "A00BA20B" "A40BA60B" "AC0BAC2B" "AE0BAE2B" "A80BA82B" "AA0BAA2B") + # Replace some main item graphics with the rooster + rom.banks[0x2C][0x0900:0x0940] = utils.createTileData(utils.tileDataToString(rom.banks[0x32][0x1D00:0x1D40]), " 321") + + # Trade sequence items + rom.banks[0x3F][0x3300:0x3640] = rom.banks[0x2C][0x0400:0x0740] diff --git a/worlds/ladx/LADXR/patches/bingo.py b/worlds/ladx/LADXR/patches/bingo.py new file mode 100644 index 0000000000..05a48c6980 --- /dev/null +++ b/worlds/ladx/LADXR/patches/bingo.py @@ -0,0 +1,1036 @@ +from ..backgroundEditor import BackgroundEditor +from ..roomEditor import RoomEditor, ObjectWarp +from ..assembler import ASM +from ..locations.constants import * +from ..utils import formatText + +# Few unused rooms that we can use the room status variables for to store data. +UNUSED_ROOMS = [0x15D, 0x17E, 0x17F, 0x1AD] +next_bit_flag_index = 0 + + +def getUnusedBitFlag(): + global next_bit_flag_index + addr = UNUSED_ROOMS[next_bit_flag_index // 8] + 0xD800 + bit_nr = next_bit_flag_index & 0x07 + mask = 1 << bit_nr + next_bit_flag_index += 1 + check_code = checkMemoryMask("$%04x" % (addr), "$%02x" % (mask)) + set_code = "ld hl, $%04x\nset %d, [hl]" % (addr, bit_nr) + return check_code, set_code + + +class Goal: + def __init__(self, description, code, tile_info, *, kill_code=None, group=None, extra_patches=None): + self.description = description + self.code = code + self.tile_info = tile_info + self.kill_code = kill_code + self.group = group + self.extra_patches = extra_patches or [] + + +class TileInfo: + def __init__(self, index1, index2=None, index3=None, index4=None, *, shift4=False, colormap=None, flipH=False): + self.index1 = index1 + self.index2 = index2 if index2 is not None else self.index1 + 1 + self.index3 = index3 + self.index4 = index4 + if self.index3 is None: + self.index3 = self.index1 if flipH else self.index1 + 2 + if self.index4 is None: + self.index4 = self.index2 if flipH else self.index1 + 3 + self.shift4 = shift4 + self.colormap = colormap + self.flipH = flipH + + def getTile(self, rom, idx): + return rom.banks[0x2C + idx // 0x400][(idx % 0x400) * 0x10:((idx % 0x400) + 1) * 0x10] + + def get(self, rom): + data = self.getTile(rom, self.index1) + self.getTile(rom, self.index2) + self.getTile(rom, + self.index3) + self.getTile( + rom, self.index4) + if self.shift4: + a = [] + b = [] + for c in data[0:32]: + a.append(c >> 4) + b.append((c << 4) & 0xFF) + data = bytes(a + b) + if self.flipH: + a = [] + for c in data[32:64]: + d = 0 + for bit in range(8): + if c & (1 << bit): + d |= 0x80 >> bit + a.append(d) + data = data[0:32] + bytes(a) + if self.colormap: + d = [] + for n in range(0, 64, 2): + a = data[n] + b = data[n + 1] + for bit in range(8): + col = 0 + if a & (1 << bit): + col |= 1 + if b & (1 << bit): + col |= 2 + col = self.colormap[col] + a &= ~(1 << bit) + b &= ~(1 << bit) + if col & 1: + a |= 1 << bit + if col & 2: + b |= 1 << bit + d.append(a) + d.append(b) + data = bytes(d) + return data + + +ITEM_TILES = { + BOMB: TileInfo(0x80, shift4=True), + POWER_BRACELET: TileInfo(0x82, shift4=True), + SWORD: TileInfo(0x84, shift4=True), + SHIELD: TileInfo(0x86, shift4=True), + BOW: TileInfo(0x88, shift4=True), + HOOKSHOT: TileInfo(0x8A, shift4=True), + MAGIC_ROD: TileInfo(0x8C, shift4=True), + MAGIC_POWDER: TileInfo(0x8E, shift4=True), + OCARINA: TileInfo(0x4E90, shift4=True), + FEATHER: TileInfo(0x4E92, shift4=True), + FLIPPERS: TileInfo(0x94, shift4=True), + SHOVEL: TileInfo(0x96, shift4=True), + PEGASUS_BOOTS: TileInfo(0x98, shift4=True), + SEASHELL: TileInfo(0x9E, shift4=True), + MEDICINE: TileInfo(0xA0, shift4=True), + BOOMERANG: TileInfo(0xA4, shift4=True), + TOADSTOOL: TileInfo(0x28C, shift4=True), + GOLD_LEAF: TileInfo(0xCA, shift4=True), +} + + +def checkMemoryEqualCode(location, value): + return """ + ld a, [%s] + cp %s + ret + """ % (location, value) + + +def checkMemoryNotZero(*locations): + if len(locations) == 1: + return """ + ld a, [%s] + and a + jp flipZ + """ % (locations[0]) + code = "" + for location in locations: + code += """ + ld a, [%s] + and a + jp z, flipZ + """ % (location) + code += "jp flipZ" + return code + + +def checkMemoryMask(location, mask): + if isinstance(location, tuple): + code = "" + for loc in location: + code += """ + ld a, [%s] + and %s + jp z, clearZ + """ % (loc, mask) + code += "jp setZ" + return code + return """ + ld a, [%s] + and %s + jp flipZ + """ % (location, mask) + + +def checkForSeashellsCode(count): + return """ + ld a, [wSeashellsCount] + cp $%02x + jp nc, setZ + ld a, [$DAE9] + and $10 + jp flipZ + """ % (count) + + +def checkMemoryEqualGreater(location, count): + return """ + ld a, [%s] + cp %s + jp nc, setZ + jp clearZ + """ % (location, count) + + +def InventoryGoal(item, *, memory_location=None, msg=None, group=None): + if memory_location is not None: + code = checkMemoryNotZero(memory_location) + elif item in INVENTORY_MAP: + code = """ + ld hl, $DB00 + ld e, INV_SIZE + + .checkLoop: + ldi a, [hl] + cp $%s + ret z ; item found, return with zero flag set to indicate goal done. + dec e + jr nz, .checkLoop + rra ; clear z flag + ret + """ % (INVENTORY_MAP[item]) + else: + code = """ + rra ; clear z flag + ret + """ + if msg is None: + msg = "Find the {%s}" % (item) + return Goal(msg, code, ITEM_TILES[item], group=group) + + +def KillGoal(description, entity_id, tile_info): + check_code, set_code = getUnusedBitFlag() + return Goal(description, check_code, tile_info, kill_code=""" + cp $%02x + jr nz, skip_%02x + %s + jp done + skip_%02x: + """ % (entity_id, entity_id, set_code, entity_id)) + + +def MonkeyGoal(description, tile_info): + check_code, set_code = getUnusedBitFlag() + return Goal(description, check_code, tile_info, extra_patches=[ + (0x15, 0x36EC, 0x36EF, ASM("jp $7FCE")), + (0x15, 0x3FCE, "00" * 8, ASM(""" + ld [hl], $FA + %s + ret + """ % (set_code))) + ]) + + +def BuzzBlobTalkGoal(description, tile_info): + check_code, set_code = getUnusedBitFlag() + return Goal(description, check_code, tile_info, extra_patches=[ + (0x18, 0x37C9, ASM("call $237C"), ASM("call $7FDE")), + (0x18, 0x3FDE, "00" * 11, ASM(""" + call $237C + ld [hl], $FA + %s + ret + """ % (set_code))) + ]) + + +def KillDethlGoal(description, tile_info): + check_code, set_code = getUnusedBitFlag() + return Goal(description, check_code, tile_info, extra_patches=[ + (0x15, 0x0606, 0x060B, ASM(set_code)), + ]) + + +def FishDaPondGoal(description, tile_info): + check_code, set_code = getUnusedBitFlag() + return Goal(description, check_code, tile_info, extra_patches=[ + (0x04, 0x21F7, 0x21FC, ASM(set_code)), + ]) + + +BINGO_GOALS = [ + InventoryGoal(BOOMERANG), + InventoryGoal(HOOKSHOT), + InventoryGoal(MAGIC_ROD), + InventoryGoal(PEGASUS_BOOTS), + InventoryGoal(FEATHER), + InventoryGoal(POWER_BRACELET), + Goal("Find the L2 {POWER_BRACELET}", checkMemoryEqualCode("$DB43", "2"), TileInfo(0x82, 0x83, 0x06, 0xB2)), + InventoryGoal(FLIPPERS, memory_location="wHasFlippers"), + InventoryGoal(OCARINA), + InventoryGoal(MEDICINE, memory_location="wHasMedicine", msg="Have the {MEDICINE}"), + InventoryGoal(BOW), + InventoryGoal(SHOVEL), + # InventoryGoal(MAGIC_POWDER), + InventoryGoal(TOADSTOOL, msg="Have the {TOADSTOOL}", group="witch"), + Goal("Find the L2 {SHIELD}", checkMemoryEqualCode("$DB44", "2"), TileInfo(0x86, 0x87, 0x06, 0xB2)), + Goal("Find 10 Secret Seashells", checkForSeashellsCode(10), ITEM_TILES[SEASHELL]), + Goal("Find the L2 {SWORD}", checkMemoryEqualCode("$DB4E", "2"), TileInfo(0x84, 0x85, 0x06, 0xB2)), + Goal("Find the {TAIL_KEY}", checkMemoryNotZero("$DB11"), TileInfo(0xC0, shift4=True)), + Goal("Find the {SLIME_KEY}", checkMemoryNotZero("$DB15"), TileInfo(0x28E, shift4=True)), + Goal("Find the {ANGLER_KEY}", checkMemoryNotZero("$DB12"), TileInfo(0xC2, shift4=True)), + Goal("Find the {FACE_KEY}", checkMemoryNotZero("$DB13"), TileInfo(0xC4, shift4=True)), + Goal("Find the {BIRD_KEY}", checkMemoryNotZero("$DB14"), TileInfo(0xC6, shift4=True)), + # {"description": "Marin's Cucco Killing Text"}, + # {"description": "Pick up Crane Game Owner"}, + BuzzBlobTalkGoal("Talk to a buzz blob", TileInfo(0x179C, colormap=[2, 3, 1, 0])), + # {"description": "Moblin King"}, + Goal("Turtle Rock Entrance Boss", checkMemoryMask("$D810", "$20"), + TileInfo(0x1413, flipH=True, colormap=[2, 3, 1, 0])), + Goal("Kill Master Stalfos", checkMemoryMask("$D980", "$10"), TileInfo(0x1622, colormap=[2, 3, 1, 0])), + # {"description": "Gohma"}, + # {"description": "Grim Creeper"}, + # {"description": "Blaino"}, + KillDethlGoal("Kill Dethl", TileInfo(0x1B38, colormap=[2, 3, 1, 0])), + # {"description": "Rooster"}, + # {"description": "Marin"}, + # {"description": "Bow-wow"}, + # {"description": "Return Bow-wow"}, + # {"description": "8 Heart Pieces"}, + # {"description": "12 Heart Pieces"}, + Goal("{BOMB} upgrade", checkMemoryEqualCode("$DB77", "$60"), TileInfo(0x80, 0x81, 0x06, 0xA3)), + Goal("Arrow upgrade", checkMemoryEqualCode("$DB78", "$60"), TileInfo(0x88, 0x89, 0x06, 0xA3)), + Goal("{MAGIC_POWDER} upgrade", checkMemoryEqualCode("$DB76", "$40"), TileInfo(0x8E, 0x8F, 0x06, 0xA3)), + # {"description": "Steal From Shop 5 Times"}, + KillGoal("Kill the giant ghini", 0x11, TileInfo(0x08A6, colormap=[2, 3, 1, 0])), + Goal("Got the Ballad of the Wind Fish", checkMemoryMask("$DB49", "4"), + TileInfo(0x298, flipH=True, colormap=[2, 3, 1, 0])), + Goal("Got the Manbo's Mambo", checkMemoryMask("$DB49", "2"), TileInfo(0x29A, flipH=True, colormap=[2, 3, 1, 0])), + Goal("Got the Frog's Song of Soul", checkMemoryMask("$DB49", "1"), + TileInfo(0x29C, flipH=True, colormap=[2, 3, 1, 0])), + Goal("Map and Compass in Tail Cave", checkMemoryNotZero("$DB16", "$DB17"), TileInfo(0x1BD0, index4=0xB1)), + Goal("Map and Compass in Bottle Grotto", checkMemoryNotZero("$DB1B", "$DB1C"), TileInfo(0x1BD0, index4=0xB2)), + Goal("Map and Compass in Key Cavern", checkMemoryNotZero("$DB20", "$DB21"), TileInfo(0x1BD0, index4=0xB3)), + Goal("Map and Compass in Angler's Tunnel", checkMemoryNotZero("$DB25", "$DB26"), TileInfo(0x1BD0, index4=0xB4)), + Goal("Map and Compass in Catfish's Maw", checkMemoryNotZero("$DB2A", "$DB2B"), TileInfo(0x1BD0, index4=0xB5)), + Goal("Map and Compass in Face Shrine", checkMemoryNotZero("$DB2F", "$DB30"), TileInfo(0x1BD0, index4=0xB6)), + Goal("Map and Compass in Eagle's Tower", checkMemoryNotZero("$DB34", "$DB35"), TileInfo(0x1BD0, index4=0xB7)), + Goal("Map and Compass in Turtle Rock", checkMemoryNotZero("$DB39", "$DB3A"), TileInfo(0x1BD0, index4=0xB8)), + Goal("Map and Compass in Color Dungeon", checkMemoryNotZero("$DDDA", "$DDDB"), TileInfo(0x1BD0, index4=0xB0)), + # {"description": "Talk to all Owl Statues in Tail Cave"}, + # {"description": "Talk to all Owl Statues in Bottle Grotto"}, + # {"description": "Talk to all Owl Statues in Key Cavern"}, + # {"description": "Talk to all Owl Statues in Angler's Tunnel"}, + # {"description": "Talk to all Owl Statues in Catfish's Maw"}, + # {"description": "Talk to all Owl Statues in Face Shrine"}, + # {"description": "Talk to all Owl Statues in Eagle's Tower"}, + # {"description": "Talk to all Owl Statues in Turtle Rock"}, + # {"description": "Talk to all Owl Statues in Color Dungeon"}, + # {"description": "Defeat 2 Sets of 3-Of-A-Kind (D1, D7)"}, + # {"description": "Stand 6 HorseHeads in D6"}, + Goal("Find the 5 Golden Leaves", checkMemoryEqualGreater("wGoldenLeaves", "5"), ITEM_TILES[GOLD_LEAF]), + # {"description": "Defeat Mad Bomber (outside Kanalet Castle)"}, + # {"description": "Totaka's Song in Richard's Villa"}, + Goal("Get the Yoshi Doll", checkMemoryMask("$DAA0", "$20"), TileInfo(0x9A)), + # {"description": "Save Papahl on the mountain"}, + Goal("Give the banana to Kiki", checkMemoryMask("$D87B", "$20"), TileInfo(0x1670, colormap=[2, 3, 1, 0])), + Goal("Have 99 or less rupees", checkMemoryEqualCode("$DB5D", "0"), TileInfo(0xA6, 0xA7, shift4=True), group="rupees"), + Goal("Have 900 or more rupees", checkMemoryEqualGreater("$DB5D", "9"), TileInfo(0xA6, 0xA7, 0xA6, 0xA7), group="rupees"), + MonkeyGoal("Bonk the Beach Monkey", TileInfo(0x1946, colormap=[2, 3, 1, 0])), + # {"description": "Kill an enemy after transforming"}, + + Goal("Got the Red Tunic", checkMemoryMask("wCollectedTunics", "1"), + TileInfo(0x2400, 0x0D11, 0x2400, 0x2401, flipH=True, colormap=[2, 3, 1, 0])), + Goal("Got the Blue Tunic", checkMemoryMask("wCollectedTunics", "2"), + TileInfo(0x2400, 0x0D01, 0x2400, 0x2401, flipH=True, colormap=[2, 3, 1, 0])), + Goal("Buy the first shop item", checkMemoryMask("$DAA1", "$10"), TileInfo(0x0880, colormap=[2, 3, 1, 0])), + Goal("Buy the second shop item", checkMemoryMask("$DAA1", "$20"), TileInfo(0x0880, colormap=[2, 3, 1, 0])), + Goal("Find the {INSTRUMENT1}", checkMemoryMask("$DB65", "2"), TileInfo(0x1500, colormap=[2, 3, 1, 0])), + Goal("Find the {INSTRUMENT2}", checkMemoryMask("$DB66", "2"), TileInfo(0x1504, colormap=[2, 3, 1, 0])), + Goal("Find the {INSTRUMENT3}", checkMemoryMask("$DB67", "2"), TileInfo(0x1508, colormap=[2, 3, 1, 0])), + Goal("Find the {INSTRUMENT4}", checkMemoryMask("$DB68", "2"), TileInfo(0x150C, colormap=[2, 3, 1, 0])), + Goal("Find the {INSTRUMENT5}", checkMemoryMask("$DB69", "2"), TileInfo(0x1510, colormap=[2, 3, 1, 0])), + Goal("Find the {INSTRUMENT6}", checkMemoryMask("$DB6A", "2"), TileInfo(0x1514, colormap=[2, 3, 1, 0])), + Goal("Find the {INSTRUMENT7}", checkMemoryMask("$DB6B", "2"), TileInfo(0x1518, colormap=[2, 3, 1, 0])), + Goal("Find the {INSTRUMENT8}", checkMemoryMask("$DB6C", "2"), TileInfo(0x151C, colormap=[2, 3, 1, 0])), + # {"description": "Moldorm", "group": "d1"}, + # {"description": "Genie in a Bottle", "group": "d2"}, + # {"description": "Slime Eyes", "group": "d3"}, + # {"description": "Angler Fish", "group": "d4"}, + # {"description": "Slime Eel", "group": "d5"}, + # {"description": "Facade", "group": "d6"}, + # {"description": "Evil Eagle", "group": "d7"}, + # {"description": "Hot Head", "group": "d8"}, + # {"description": "2 Followers at the same time", "group": "multifollower"}, + # {"description": "3 Followers at the same time", "group": "multifollower"}, + Goal("Visit the 4 Fountain Fairies", checkMemoryMask(("$D853", "$D9AC", "$D9F3", "$D9FB"), "$80"), + TileInfo(0x20, shift4=True, colormap=[2, 3, 1, 0])), + Goal("Have at least 8 Heart Containers", checkMemoryEqualGreater("$DB5B", "8"), TileInfo(0xAA, flipH=True), group="Health"), + Goal("Have at least 9 Heart Containers", checkMemoryEqualGreater("$DB5B", "9"), TileInfo(0xAA, flipH=True), group="Health"), + Goal("Have at least 10 Heart Containers", checkMemoryEqualGreater("$DB5B", "10"), TileInfo(0xAA, flipH=True), group="Health"), + Goal("Got photo 1: Here Stands A Brave Man", checkMemoryMask("$DC0C", "$01"), TileInfo(0x3008, 0x0D0F, colormap=[2, 3, 1, 0])), + Goal("Got photo 2: Looking over the sea with Marin", checkMemoryMask("$DC0C", "$02"), TileInfo(0x08F0, 0x0D0F, colormap=[2, 3, 1, 0])), + Goal("Got photo 3: Heads up!", checkMemoryMask("$DC0C", "$04"), TileInfo(0x0E6A, 0x0D0F, 0x0E6B, 0x0E7B)), + Goal("Got photo 4: Say Mushroom!", checkMemoryMask("$DC0C", "$08"), TileInfo(0x0E60, 0x0D0F, 0x0E61, 0x0E71)), + Goal("Got photo 5: Ulrira's Secret!", checkMemoryMask("$DC0C", "$10"), + TileInfo(0x1461, 0x1464, 0x1463, 0x0D0F, colormap=[2, 3, 1, 0])), + Goal("Got photo 6: Playing with Bowwow!", checkMemoryMask("$DC0C", "$20"), + TileInfo(0x1A42, 0x0D0F, 0x1A42, 0x1A43, flipH=True, colormap=[2, 3, 1, 0])), + Goal("Got photo 7: Thief!", checkMemoryMask("$DC0C", "$40"), TileInfo(0x0880, 0x0D0F, colormap=[2, 3, 1, 0])), + Goal("Got photo 8: Be more careful next time!", checkMemoryMask("$DC0C", "$80"), TileInfo(0x14E0, index4=0x0D0F, colormap=[2, 3, 1, 0])), + Goal("Got photo 9: I Found Zora!", checkMemoryMask("$DC0D", "$01"), + TileInfo(0x1906, 0x0D0F, 0x1906, 0x1907, flipH=True, colormap=[2, 3, 1, 0])), + Goal("Got photo 10: Richard at Kanalet Castle", checkMemoryMask("$DC0D", "$02"), TileInfo(0x15B0, 0x0D0F, colormap=[2, 3, 1, 0])), + Goal("Got photo 11: Ghost", checkMemoryMask("$DC0D", "$04"), TileInfo(0x1980, 0x0D0F, colormap=[2, 3, 1, 0])), + Goal("Got photo 12: Close Call", checkMemoryMask("$DC0D", "$08"), TileInfo(0x0FED, 0x0D0F, 0x0FED, 0x0FFD)), + # {"description": "Collect 4 Pictures", "group": "pics"}, + # {"description": "Collect 5 Pictures", "group": "pics"}, + # {"description": "Collect 6 Pictures", "group": "pics"}, + Goal("Open the 4 Overworld Warp Holes", checkMemoryMask(("$D801", "$D82C", "$D895", "$D8EC"), "$80"), + TileInfo(0x3E, 0x3E, 0x3E, 0x3E, colormap=[2, 1, 3, 0])), + Goal("Finish the Raft Minigame", checkMemoryMask("$D87F", "$80"), TileInfo(0x087C, flipH=True, colormap=[2, 3, 1, 0])), + Goal("Kill the Ball and Chain Trooper", checkMemoryMask("$DAC6", "$10"), TileInfo(0x09A4, colormap=[2, 3, 1, 0])), + Goal("Destroy all Pillars with the Ball", checkMemoryMask(("$DA14", "$DA15", "$DA18", "$DA19"), "$20"), + TileInfo(0x166C, flipH=True)), + FishDaPondGoal("Fish the pond empty", TileInfo(0x0A00, colormap=[2, 3, 1, 0])), + KillGoal("Kill the Anti-Kirby", 0x91, TileInfo(0x1550, colormap=[2, 3, 1, 0])), + KillGoal("Kill a Rolling Bones", 0x81, TileInfo(0x0AB6, colormap=[2, 3, 1, 0])), + KillGoal("Kill a Hinox", 0x89, TileInfo(0x1542, colormap=[2, 3, 1, 0])), + KillGoal("Kill a Stone Hinox", 0xF4, TileInfo(0x2482, colormap=[2, 3, 1, 0])), + KillGoal("Kill a Dodongo", 0x60, TileInfo(0x0AA0, flipH=True, colormap=[2, 3, 1, 0])), + KillGoal("Kill a Cue Ball", 0x8E, TileInfo(0x1566, flipH=True, colormap=[2, 3, 1, 0])), + KillGoal("Kill a Smasher", 0x92, TileInfo(0x1576, colormap=[2, 3, 1, 0])), + + Goal("Save Marin on the Mountain Bridge", checkMemoryMask("$D808", "$10"), TileInfo(0x1A6C, colormap=[2, 3, 1, 0])), + Goal("Save Raccoon Tarin", checkMemoryMask("$D851", "$10"), TileInfo(0x1888, colormap=[2, 3, 1, 0])), + Goal("Trade the {TOADSTOOL} with the witch", checkMemoryMask("$DAA2", "$20"), TileInfo(0x0A30, colormap=[2, 3, 1, 0]), group="witch"), +] + + +def randomizeGoals(rnd, options): + goals = BINGO_GOALS.copy() + rnd.shuffle(goals) + has_group = set() + for n in range(len(goals)): + if goals[n].group: + if goals[n].group in has_group: + goals[n] = None + else: + has_group.add(goals[n].group) + goals = [goal for goal in goals if goal is not None] + return goals[:25] + + +def setBingoGoal(rom, goals, mode): + assert len(goals) == 25 + + for goal in goals: + for bank, addr, current, target in goal.extra_patches: + rom.patch(bank, addr, current, target) + + # Setup the bingo card visuals + be = BackgroundEditor(rom, 0x15) + ba = BackgroundEditor(rom, 0x15, attributes=True) + for y in range(18): + for x in range(20): + be.tiles[0x9800 + x + y * 0x20] = (x + (y & 2)) % 4 | ((y % 2) * 4) + ba.tiles[0x9800 + x + y * 0x20] = 0x01 + + for y in range(5): + for x in range(5): + idx = x + y * 5 + be.tiles[0x9843 + x * 3 + y * 3 * 0x20] = 8 + idx * 4 + be.tiles[0x9844 + x * 3 + y * 3 * 0x20] = 10 + idx * 4 + be.tiles[0x9863 + x * 3 + y * 3 * 0x20] = 9 + idx * 4 + be.tiles[0x9864 + x * 3 + y * 3 * 0x20] = 11 + idx * 4 + + ba.tiles[0x9843 + x * 3 + y * 3 * 0x20] = 0x03 + ba.tiles[0x9844 + x * 3 + y * 3 * 0x20] = 0x03 + ba.tiles[0x9863 + x * 3 + y * 3 * 0x20] = 0x03 + ba.tiles[0x9864 + x * 3 + y * 3 * 0x20] = 0x03 + be.store(rom) + ba.store(rom) + + tiles = rom.banks[0x30][0x3000:0x3040] + rom.banks[0x30][0x3100:0x3140] + for goal in goals: + tiles += goal.tile_info.get(rom) + rom.banks[0x30][0x3000:0x3000 + len(tiles)] = tiles + + # Patch the mural palette to have more useful entries for us + rom.patch(0x21, 0x36EE, ASM("dw $7C00, $7C00, $7C00, $7C00"), ASM("dw $7FFF, $56b5, $294a, $0000")) + rom.patch(0x21, 0x36F6, ASM("dw $7C00, $7C00, $7C00, $7C00"), ASM("dw $43f0, $32ac, $1946, $0000")) + + # Patch the face shrine mural handler stage 4, we want to jump to bank 0x0C, which normally contains + # DMG graphics, but gives us a lot of room for our own code and graphics. + rom.patch(0x01, 0x2B81, 0x2B99, ASM(""" + ld a, $0D + ld hl, $4000 + push hl + jp $080C ; switch bank + """), fill_nop=True) + # Fix that the mural is always centered on screen, instead offset it properly + rom.patch(0x18, 0x1E3D, ASM("add hl, bc\nld [hl], $50"), ASM("call $7FD6")) + rom.patch(0x18, 0x3FD6, "00" * 8, ASM(""" + add hl, bc + ld a, [hl] + and $F0 + add a, $0F + ld [hl], a + ret + """)) + + # In stage 5, just exit the mural without waiting for a button press. + rom.patch(0x01, 0x2B9E, ASM("jr z, $07"), "", fill_nop=True) + + # Our custom stage 4 + rom.patch(0x0D, 0x0000, 0x3000, ASM(""" +wState := $C3C4 ; Our internal state, guaranteed to be 0 on the first entry. +wCursorX := $D100 +wCursorY := $D101 + + call mainHandler + ; Make sure we return with bank 1 active. + ld a, $01 + jp $080C ; switch bank + +mainHandler: + ld a, [wState] + rst 0 + dw init + dw checkGoalDone + dw chooseSquare + dw waitDialogDone + dw finishGame + +init: + xor a + ld [wCursorX], a + ld [wCursorY], a + inc a + ld [wState], a + di + ldh [$4F], a ; 2nd vram bank + + ld hl, $9843 + ld c, 25 + ld b, $00 +.checkDoneLoop: + push bc + push hl + ld a, b + call goalCheck + pop hl + jr nz, .notDone +.statWait1: + ldh a, [$41] ;STAT + and $02 + jr nz, .statWait1 + ld a, $04 + ldi [hl], a + ldd [hl], a + + ld bc, $0020 + add hl, bc +.statWait2: + ldh a, [$41] ;STAT + and $02 + jr nz, .statWait2 + ld a, $04 + ldi [hl], a + ldd [hl], a + + ld bc, $FFE0 + add hl, bc +.notDone: + inc hl + inc hl + inc hl + ld a, l + and $1F + cp $12 + jr nz, .noRowSkip + ld bc, $0060 - 5*3 + add hl, bc +.noRowSkip: + pop bc + inc b + dec c + jr nz, .checkDoneLoop + + xor a + ldh [$4F], a ; 1st vram bank + ei + +checkGoalDone: + ld a, $02 + ld [wState], a + ; Check if the egg event is already triggered + ld a, [$D806] + and $10 + ret nz + + call checkAnyGoal + ret nz + + ; Goal done, give a message and goto state to finish the game + ld a, $04 + ld [wState], a + ld a, $E7 + call $2385 ; open dialog + ret + +chooseSquare: + ld hl, $C000 ; oam buffer + + ; Draw the cursor + ld a, [wCursorY] + call multiA24 + add a, $27 + ldi [hl], a + ld a, [wCursorX] + call multiA24 + add a, $24 + ldi [hl], a + ld a, $A2 + ldi [hl], a + ld a, $02 + ldi [hl], a + + ldh a, [$CC] ; button presses + bit 0, a + jr nz, .right + bit 1, a + jr nz, .left + bit 2, a + jr nz, .up + bit 3, a + jr nz, .down + bit 4, a + jr nz, .showText + bit 5, a + jr nz, exitMural + bit 7, a + jr nz, exitMural + ret + +.right: + ld a, [wCursorX] + cp $04 + ret z + inc a + ld [wCursorX], a + ret + +.left: + ld a, [wCursorX] + and a + ret z + dec a + ld [wCursorX], a + ret + +.down: + ld a, [wCursorY] + cp $04 + ret z + inc a + ld [wCursorY], a + ret + +.up: + ld a, [wCursorY] + and a + ret z + dec a + ld [wCursorY], a + ret + +.showText: + ld a, [wCursorY] + ld c, a + add a, a + add a, a + add a, c + ld c, a + ld a, [wCursorX] + add a, c + add a, a + ld l, a + ld h, $00 + ld de, messageTable + add hl, de + ldi a, [hl] + ld h, [hl] + ld l, a + ld de, wCustomMessage +.copyLoop: + ldi a, [hl] + ld [de], a + inc de + inc a + jr nz, .copyLoop + + ld a, $C9 + call $2385 ; open dialog + ld a, $03 + ld [wState], a + ret + +waitDialogDone: + ld a, [$C19F] ; dialog state + and a + ret nz + ld a, $02 ; choose square + ld [wState], a + ret + +finishGame: + ld a, [$C19F] ; dialog state + and a + ret nz + + ldh a, [$CC] ; button presses + and a + ret z + + ; Goto "credits" + xor a + ld [$DB96], a + inc a + ld [$DB95], a + ret + +exitMural: + ld hl, $DB96 ;gameplay subtype + inc [hl] + ret + +multiA24: + ld c, a + add a, a + add a, c + add a, a + add a, a + add a, a + ret + +flipZ: + jr nz, setZ +clearZ: + rra + ret +setZ: + cp a + ret + +checkAnyGoal: +#IF {mode} + call goalcheck_0 + ret nz + call goalcheck_1 + ret nz + call goalcheck_2 + ret nz + call goalcheck_3 + ret nz + call goalcheck_4 + ret nz + call goalcheck_5 + ret nz + call goalcheck_6 + ret nz + call goalcheck_7 + ret nz + call goalcheck_8 + ret nz + call goalcheck_9 + ret nz + call goalcheck_10 + ret nz + call goalcheck_11 + ret nz + call goalcheck_12 + ret nz + call goalcheck_13 + ret nz + call goalcheck_14 + ret nz + call goalcheck_15 + ret nz + call goalcheck_16 + ret nz + call goalcheck_17 + ret nz + call goalcheck_18 + ret nz + call goalcheck_19 + ret nz + call goalcheck_20 + ret nz + call goalcheck_21 + ret nz + call goalcheck_22 + ret nz + call goalcheck_23 + ret nz + call goalcheck_24 + ret +#ELSE + call checkGoalRow1 + ret z + call checkGoalRow2 + ret z + call checkGoalRow3 + ret z + call checkGoalRow4 + ret z + call checkGoalRow5 + ret z + call checkGoalCol1 + ret z + call checkGoalCol2 + ret z + call checkGoalCol3 + ret z + call checkGoalCol4 + ret z + call checkGoalCol5 + ret z + call checkGoalDiagonal0 + ret z + call checkGoalDiagonal1 + ret + +checkGoalRow1: + call goalcheck_0 + ret nz + call goalcheck_1 + ret nz + call goalcheck_2 + ret nz + call goalcheck_3 + ret nz + call goalcheck_4 + ret + +checkGoalRow2: + call goalcheck_5 + ret nz + call goalcheck_6 + ret nz + call goalcheck_7 + ret nz + call goalcheck_8 + ret nz + call goalcheck_9 + ret + +checkGoalRow3: + call goalcheck_10 + ret nz + call goalcheck_11 + ret nz + call goalcheck_12 + ret nz + call goalcheck_13 + ret nz + call goalcheck_14 + ret + +checkGoalRow4: + call goalcheck_15 + ret nz + call goalcheck_16 + ret nz + call goalcheck_17 + ret nz + call goalcheck_18 + ret nz + call goalcheck_19 + ret + +checkGoalRow5: + call goalcheck_20 + ret nz + call goalcheck_21 + ret nz + call goalcheck_22 + ret nz + call goalcheck_23 + ret nz + call goalcheck_24 + ret + +checkGoalCol1: + call goalcheck_0 + ret nz + call goalcheck_5 + ret nz + call goalcheck_10 + ret nz + call goalcheck_15 + ret nz + call goalcheck_20 + ret + +checkGoalCol2: + call goalcheck_1 + ret nz + call goalcheck_6 + ret nz + call goalcheck_11 + ret nz + call goalcheck_16 + ret nz + call goalcheck_21 + ret + +checkGoalCol3: + call goalcheck_2 + ret nz + call goalcheck_7 + ret nz + call goalcheck_12 + ret nz + call goalcheck_17 + ret nz + call goalcheck_22 + ret + +checkGoalCol4: + call goalcheck_3 + ret nz + call goalcheck_8 + ret nz + call goalcheck_13 + ret nz + call goalcheck_18 + ret nz + call goalcheck_23 + ret + +checkGoalCol5: + call goalcheck_4 + ret nz + call goalcheck_9 + ret nz + call goalcheck_14 + ret nz + call goalcheck_19 + ret nz + call goalcheck_24 + ret + +checkGoalDiagonal0: + call goalcheck_0 + ret nz + call goalcheck_6 + ret nz + call goalcheck_12 + ret nz + call goalcheck_18 + ret nz + call goalcheck_24 + ret + +checkGoalDiagonal1: + call goalcheck_4 + ret nz + call goalcheck_8 + ret nz + call goalcheck_12 + ret nz + call goalcheck_16 + ret nz + call goalcheck_20 + ret +#ENDIF + +messageTable: +""".format(mode=1 if mode == "bingo-full" else 0) + + "\n".join(["dw message_%d" % (n) for n in range(25)]) + "\n" + + "\n".join(["message_%d:\n db m\"%s\"" % (n, goal.description) for n, goal in + enumerate(goals)]) + "\n" + + """ + goalCheck: + rst 0 + """ + + "\n".join(["dw goalcheck_%d" % (n) for n in range(25)]) + "\n" + + "\n".join(["goalcheck_%d:\n %s\n" % (n, goal.code) for n, goal in + enumerate(goals)]) + "\n", 0x4000), fill_nop=True) + rom.texts[0xE7] = formatText("BINGO!\nPress any button to finish.") + + # Patch the game to call a bit of our code when an enemy is killed by patching into the drop item handling + rom.patch(0x00, 0x3F50, ASM("ld a, $03\nld [$C113], a\nld [$2100], a\ncall $55CF"), ASM(""" + ld a, $0D + ld [$C113], a + ld [$2100], a + call $7000 + """)) + rom.patch(0x0D, 0x3000, 0x4000, ASM(""" + ldh a, [$EB] ; active entity + """ + "\n".join([goal.kill_code for goal in goals if goal.kill_code is not None]) + """ +done: ; Return to normal item drop handler + ld a, $03 ;normal drop item handler bank + ld hl, $55CF ;normal drop item handler address + push hl + jp $080F ; switch bank + """, 0x7000), fill_nop=True) + + # Patch Dethl to warp you outside + rom.patch(0x15, 0x0682, 0x069B, ASM(""" + ld a, $0B + ld [$DB95], a + call $0C7D + + ld a, $07 + ld [$DB96], a + """), fill_nop=True) + re = RoomEditor(rom, 0x274) + re.objects += [ObjectWarp(0, 0, 0x06, 0x58, 0x40)] * 4 + re.store(rom) + # Patch the egg to be always open + rom.patch(0x00, 0x31f5, ASM("ld a, [$D806]\nand $10\njr z, $25"), ASM(""), fill_nop=True) + rom.patch(0x20, 0x2dea, ASM("ld a, [$D806]\nand $10\njr z, $29"), ASM(""), fill_nop=True) + + # Patch unused entity 4C into our bingo board. + rom.patch(0x03, 0x004C, "41", "82") + rom.patch(0x03, 0x0147, "00", "98") + rom.patch(0x20, 0x00e4, "000000", ASM("dw $5e1b\ndb $18")) + + # Add graphics for our bingo board to 2nd WRAM bank. + rom.banks[0x3F][0x3700:0x3780] = rom.banks[0x32][0x1500:0x1580] + rom.banks[0x3F][0x3728:0x373A] = b'\x55\xAA\x00\xFF\x55\xAA\x00\xFF\x55\xAA\x00\xFF\x55\xAA\x00\xFF\x00\xFF' + rom.banks[0x3F][0x3748:0x375A] = b'\x55\xAA\x00\xFF\x55\xAA\x00\xFF\x55\xAA\x00\xFF\x55\xAA\x00\xFF\x00\xFF' + rom.patch(0x18, 0x1E0B, + "00F85003" + "00005203" + "00085403" + "00105603", + "00F8F00B" + "0000F20B" + "0008F40B" + "0010F60B") + + # Add the bingo board to marins house + re = RoomEditor(rom, 0x2A3) + re.entities.append((2, 0, 0x4C)) + re.store(rom) + + # Add the bingo board to the room before the egg + re = RoomEditor(rom, 0x016) + re.removeObject(4, 5) + re.entities.append((3, 4, 0x4C)) + re.updateOverlay() + re.store(rom) + + # Remove the egg event from the egg room (no bomb triggers for you!) + re = RoomEditor(rom, 0x006) + re.entities = [] + re.store(rom) + + rom.texts[0xCF] = formatText(""" + Bingo! + Young lad, I mean... #####, the hero! + You have bingo! + You have proven your wisdom, courage and power! + ... ... ... ... + As part of the Wind Fish's spirit, I am the guardian of his dream world... + But one day, we decided to have a bingo game. + Then you, #####, came to win the bingo... + Thank you, #####... + My work is done... + The Wind Fish will wake soon. + Good bye...Bingo! + """) + rom.texts[0xCE] = rom.texts[0xCF] diff --git a/worlds/ladx/LADXR/patches/bomb.py b/worlds/ladx/LADXR/patches/bomb.py new file mode 100644 index 0000000000..4d9b2891d4 --- /dev/null +++ b/worlds/ladx/LADXR/patches/bomb.py @@ -0,0 +1,20 @@ +from ..assembler import ASM + + +def onlyDropBombsWhenHaveBombs(rom): + rom.patch(0x03, 0x1FC5, ASM("call $608C"), ASM("call $50B2")) + # We use some of the unused chest code space here to remove the bomb if you do not have bombs in your inventory. + rom.patch(0x03, 0x10B2, 0x112A, ASM(""" + ld e, INV_SIZE + ld hl, $DB00 + ld a, $02 +loop: + cp [hl] + jr z, resume + dec e + inc hl + jr nz, loop + jp $3F8D ; unload entity +resume: + jp $608C + """), fill_nop=True) \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/bowwow.py b/worlds/ladx/LADXR/patches/bowwow.py new file mode 100644 index 0000000000..479f360514 --- /dev/null +++ b/worlds/ladx/LADXR/patches/bowwow.py @@ -0,0 +1,207 @@ +from ..assembler import ASM +from ..roomEditor import RoomEditor + + +def fixBowwow(rom, everywhere=False): + ### BowWow patches + rom.patch(0x03, 0x1E0E, ASM("ld [$DB56], a"), "", fill_nop=True) # Do not mark BowWow as kidnapped after we complete dungeon 1. + rom.patch(0x15, 0x06B6, ASM("ld a, [$DB56]\ncp $80"), ASM("xor a"), fill_nop=True) # always load the moblin boss + rom.patch(0x03, 0x182D, ASM("ld a, [$DB56]\ncp $80"), ASM("ld a, [$DAE2]\nand $10")) # load the cave moblins if the chest is not opened + rom.patch(0x07, 0x3947, ASM("ld a, [$DB56]\ncp $80"), ASM("ld a, [$DAE2]\nand $10")) # load the cave moblin with sword if the chest is not opened + + # Modify the moblin cave to contain a chest at the end, which contains bowwow + re = RoomEditor(rom, 0x2E2) + re.removeEntities(0x6D) + re.changeObject(8, 3, 0xA0) + re.store(rom) + # Place bowwow in the chest table + rom.banks[0x14][0x560 + 0x2E2] = 0x81 + + # Patch bowwow follower sprite to be used from 2nd vram bank + rom.patch(0x05, 0x001C, + b"40034023" + b"42034223" + b"44034603" + b"48034A03" + b"46234423" + b"4A234823" + b"4C034C23", + b"500B502B" + b"520B522B" + b"540B560B" + b"580B5A0B" + b"562B542B" + b"5A2B582B" + b"5C0B5C2B") + # Patch to use the chain sprite from second vram bank (however, the chain bugs out various things) + rom.patch(0x05, 0x0282, + ASM("ld a, $4E\njr nz, $02\nld a, $7E\nld [de], a\ninc de\nld a, $00"), + ASM("ld a, $5E\nld [de], a\ninc de\nld a, $08"), fill_nop=True) + # Never load the bowwow tiles in the first VRAM bank, as we do not need them. + rom.patch(0x00, 0x2EB0, ASM("ld a, [$DB56]\ncp $01\nld a, $A4\njr z, $18"), "", fill_nop=True) + + # Patch the location where bowwow stores chain X/Y positions so it does not conflict with a lot of other things + rom.patch(0x05, 0x00BE, ASM("ld hl, $D100"), ASM("ld hl, $D180")) + rom.patch(0x05, 0x0275, ASM("ld hl, $D100"), ASM("ld hl, $D180")) + rom.patch(0x05, 0x03AD, ASM("ld [$D100], a"), ASM("ld [$D180], a")) + rom.patch(0x05, 0x03BD, ASM("ld de, $D100"), ASM("ld de, $D180")) + rom.patch(0x05, 0x049F, ASM("ld hl, $D100"), ASM("ld hl, $D180")) + rom.patch(0x05, 0x04C2, ASM("ld a, [$D100]"), ASM("ld a, [$D180]")) + rom.patch(0x05, 0x03C0, ASM("ld hl, $D101"), ASM("ld hl, $D181")) + rom.patch(0x05, 0x0418, ASM("ld [$D106], a"), ASM("ld [$D186], a")) + rom.patch(0x05, 0x0423, ASM("ld de, $D106"), ASM("ld de, $D186")) + rom.patch(0x05, 0x0426, ASM("ld hl, $D105"), ASM("ld hl, $D185")) + + rom.patch(0x19, 0x3A4E, ASM("ld hl, $D100"), ASM("ld hl, $D180")) + rom.patch(0x19, 0x3A5A, ASM("ld hl, $D110"), ASM("ld hl, $D190")) + + rom.patch(0x05, 0x00D9, ASM("ld hl, $D110"), ASM("ld hl, $D190")) + rom.patch(0x05, 0x026E, ASM("ld hl, $D110"), ASM("ld hl, $D190")) + rom.patch(0x05, 0x03BA, ASM("ld [$D110], a"), ASM("ld [$D190], a")) + rom.patch(0x05, 0x03DD, ASM("ld de, $D110"), ASM("ld de, $D190")) + rom.patch(0x05, 0x0480, ASM("ld hl, $D110"), ASM("ld hl, $D190")) + rom.patch(0x05, 0x04B5, ASM("ld a, [$D110]"), ASM("ld a, [$D190]")) + rom.patch(0x05, 0x03E0, ASM("ld hl, $D111"), ASM("ld hl, $D191")) + rom.patch(0x05, 0x0420, ASM("ld [$D116], a"), ASM("ld [$D196], a")) + rom.patch(0x05, 0x044d, ASM("ld de, $D116"), ASM("ld de, $D196")) + rom.patch(0x05, 0x0450, ASM("ld hl, $D115"), ASM("ld hl, $D195")) + + rom.patch(0x05, 0x0039, ASM("ld [$D154], a"), "", fill_nop=True) # normally this stores the index to bowwow, for the kiki fight + rom.patch(0x05, 0x013C, ASM("ld [$D150], a"), ASM("ld [$D197], a")) + rom.patch(0x05, 0x0144, ASM("ld [$D151], a"), ASM("ld [$D198], a")) + rom.patch(0x05, 0x02F9, ASM("ld [$D152], a"), ASM("ld [$D199], a")) + rom.patch(0x05, 0x0335, ASM("ld a, [$D152]"), ASM("ld a, [$D199]")) + rom.patch(0x05, 0x0485, ASM("ld a, [$D151]"), ASM("ld a, [$D198]")) + rom.patch(0x05, 0x04A4, ASM("ld a, [$D150]"), ASM("ld a, [$D197]")) + + # Patch the bowwow create code to call our custom check of we are in swamp function. + if everywhere: + # Load followers in dungeons, caves, etc + rom.patch(0x01, 0x1FC1, ASM("ret z"), "", fill_nop=True) + rom.patch(0x01, 0x1FC4, ASM("ret z"), "", fill_nop=True) + rom.patch(0x01, 0x1FC7, ASM("ret z"), "", fill_nop=True) + rom.patch(0x01, 0x1FCA, ASM("ret c"), "", fill_nop=True) # dungeon + # rom.patch(0x01, 0x1FBC, ASM("ret nz"), "", fill_nop=True) # sidescroller: TOFIX this breaks fishing minigame reward + else: + # Patch the bowwow create code to call our custom check of we are in swamp function. + rom.patch(0x01, 0x211F, ASM("ldh a, [$F6]\ncp $A7\nret z\nld a, [$DB56]\ncp $01\njr nz, $36"), ASM(""" + ld a, $07 + rst 8 + ld a, e + and a + ret z + """), fill_nop=True) + # Patch bowwow to not stay around when we move from map to map + rom.patch(0x05, 0x0049, 0x0054, ASM(""" + cp [hl] + jr z, Continue + ld hl, $C280 + add hl, bc + ld [hl], b + ret +Continue: + """), fill_nop=True) + + # Patch madam meow meow to not take bowwow + rom.patch(0x06, 0x1BD7, ASM("ld a, [$DB66]\nand $02"), ASM("ld a, $00\nand $02"), fill_nop=True) + + # Patch kiki not to react to bowwow, as bowwow is not with link at this map + rom.patch(0x07, 0x18A8, ASM("ld a, [$DB56]\ncp $01"), ASM("ld a, $00\ncp $01"), fill_nop=True) + + # Patch the color dungeon entrance not to check for bowwow + rom.patch(0x02, 0x340D, ASM("ld hl, $DB56\nor [hl]"), "", fill_nop=True) + + # Patch richard to ignore bowwow + rom.patch(0x06, 0x006C, ASM("ld a, [$DB56]"), ASM("xor a"), fill_nop=True) + + # Patch to modify how bowwow eats enemies, normally it just unloads them, but we call our handler in bank 3E + rom.patch(0x05, 0x03A0, 0x03A8, ASM(""" + push bc + ld b, d + ld c, e + ld a, $08 + rst 8 + pop bc + ret + """), fill_nop=True) + rom.patch(0x05, 0x0387, ASM("ld a, $03\nldh [$F2], a"), "", fill_nop=True) # remove the default chomp sfx + + # Various enemies + rom.banks[0x14][0x1218 + 0xC5] = 0x01 # Urchin + rom.banks[0x14][0x1218 + 0x93] = 0x01 # MadBomber + rom.banks[0x14][0x1218 + 0x51] = 0x01 # Swinging ball&chain golden leaf enemy + rom.banks[0x14][0x1218 + 0xF2] = 0x01 # Color dungeon flying hopper + rom.banks[0x14][0x1218 + 0xF3] = 0x01 # Color dungeon hopper + rom.banks[0x14][0x1218 + 0xE9] = 0x01 # Color dungeon shell + rom.banks[0x14][0x1218 + 0xEA] = 0x01 # Color dungeon shell + rom.banks[0x14][0x1218 + 0xEB] = 0x01 # Color dungeon shell + rom.banks[0x14][0x1218 + 0xEC] = 0x01 # Color dungeon thing + rom.banks[0x14][0x1218 + 0xED] = 0x01 # Color dungeon thing + rom.banks[0x14][0x1218 + 0xEE] = 0x01 # Color dungeon thing + rom.banks[0x14][0x1218 + 0x87] = 0x01 # Lanmola (for D4 key) + rom.banks[0x14][0x1218 + 0x88] = 0x01 # Armos knight (for D6 key) + rom.banks[0x14][0x1218 + 0x16] = 0x01 # Spark + rom.banks[0x14][0x1218 + 0x17] = 0x01 # Spark + rom.banks[0x14][0x1218 + 0x2C] = 0x01 # Spiked beetle + rom.banks[0x14][0x1218 + 0x90] = 0x01 # Three of a kind (screw these guys) + rom.banks[0x14][0x1218 + 0x18] = 0x01 # Pols voice + rom.banks[0x14][0x1218 + 0x50] = 0x01 # Boo buddy + rom.banks[0x14][0x1218 + 0xA2] = 0x01 # Pirana plant + rom.banks[0x14][0x1218 + 0x52] = 0x01 # Tractor device + rom.banks[0x14][0x1218 + 0x53] = 0x01 # Tractor device (D3) + rom.banks[0x14][0x1218 + 0x55] = 0x01 # Bounding bombite + rom.banks[0x14][0x1218 + 0x56] = 0x01 # Timer bombite + rom.banks[0x14][0x1218 + 0x57] = 0x01 # Pairod + rom.banks[0x14][0x1218 + 0x15] = 0x01 # Antifairy + rom.banks[0x14][0x1218 + 0xA0] = 0x01 # Peahat + rom.banks[0x14][0x1218 + 0x9C] = 0x01 # Star + rom.banks[0x14][0x1218 + 0xA1] = 0x01 # Snake + rom.banks[0x14][0x1218 + 0xBD] = 0x01 # Vire + rom.banks[0x14][0x1218 + 0xE4] = 0x01 # Moblin boss + + # Bosses + rom.banks[0x14][0x1218 + 0x59] = 0x01 # Moldorm + rom.banks[0x14][0x1218 + 0x5C] = 0x01 # Genie + rom.banks[0x14][0x1218 + 0x5B] = 0x01 # Slime Eye + rom.patch(0x04, 0x0AC4, ASM("ld [hl], $28"), ASM("ld [hl], $FF")) # give more time before slimeeye unsplits + rom.patch(0x04, 0x0B05, ASM("ld [hl], $50"), ASM("ld [hl], $FF")) # give more time before slimeeye unsplits + rom.banks[0x14][0x1218 + 0x65] = 0x01 # Angler fish + rom.banks[0x14][0x1218 + 0x5D] = 0x01 # Slime eel + rom.banks[0x14][0x1218 + 0x5A] = 0x01 # Facade + rom.banks[0x14][0x1218 + 0x63] = 0x01 # Eagle + rom.banks[0x14][0x1218 + 0x62] = 0x01 # Hot head + rom.banks[0x14][0x1218 + 0xF9] = 0x01 # Hardhit beetle + rom.banks[0x14][0x1218 + 0xE6] = 0x01 # Nightmare + + # Minibosses + rom.banks[0x14][0x1218 + 0x81] = 0x01 # Rolling bones + rom.banks[0x14][0x1218 + 0x89] = 0x01 # Hinox + rom.banks[0x14][0x1218 + 0x8E] = 0x01 # Cue ball + rom.banks[0x14][0x1218 + 0x5E] = 0x01 # Gnoma + rom.banks[0x14][0x1218 + 0x5F] = 0x01 # Master stalfos + rom.banks[0x14][0x1218 + 0x92] = 0x01 # Smasher + rom.banks[0x14][0x1218 + 0xBC] = 0x01 # Grim creeper + rom.banks[0x14][0x1218 + 0xBE] = 0x01 # Blaino + rom.banks[0x14][0x1218 + 0xF8] = 0x01 # Giant buzz blob + rom.banks[0x14][0x1218 + 0xF4] = 0x01 # Avalaunch + + # NPCs + rom.banks[0x14][0x1218 + 0x6F] = 0x01 # Dog + rom.banks[0x14][0x1218 + 0x6E] = 0x01 # Butterfly + rom.banks[0x14][0x1218 + 0x6C] = 0x01 # Cucco + rom.banks[0x14][0x1218 + 0x70] = 0x01 # Kid + rom.banks[0x14][0x1218 + 0x71] = 0x01 # Kid + rom.banks[0x14][0x1218 + 0x72] = 0x01 # Kid + rom.banks[0x14][0x1218 + 0x73] = 0x01 # Kid + rom.banks[0x14][0x1218 + 0xD0] = 0x01 # Animal + rom.banks[0x14][0x1218 + 0xD1] = 0x01 # Animal + rom.banks[0x14][0x1218 + 0xD2] = 0x01 # Animal + rom.banks[0x14][0x1218 + 0xD3] = 0x01 # Animal + + +def bowwowMapPatches(rom): + # Remove all the cystal things that can only be destroyed with a sword. + for n in range(0x100, 0x2FF): + re = RoomEditor(rom, n) + re.objects = list(filter(lambda obj: obj.type_id != 0xDD, re.objects)) + re.store(rom) diff --git a/worlds/ladx/LADXR/patches/chest.py b/worlds/ladx/LADXR/patches/chest.py new file mode 100644 index 0000000000..c092fc75f5 --- /dev/null +++ b/worlds/ladx/LADXR/patches/chest.py @@ -0,0 +1,59 @@ +from ..assembler import ASM +from ..utils import formatText +from ..locations.constants import CHEST_ITEMS + + +def fixChests(rom): + # Patch the chest code, so it can give a lvl1 sword. + # Normally, there is some code related to the owl event when getting the tail key, + # as we patched out the owl. We use it to jump to our custom code in bank $3E to handle getting the item + rom.patch(0x03, 0x109C, ASM(""" + cp $11 ; if not tail key, skip + jr nz, end + push af + ld a, [$C501] + ld e, a + ld hl, $C2F0 + add hl, de + ld [hl], $38 + pop af + end: + ld e, a + cp $21 ; if is message chest or higher number, next instruction is to skip giving things. + """), ASM(""" + ld a, $06 ; GiveItemMultiworld + rst 8 + + and a ; clear the carry flag to always skip giving stuff. + """), fill_nop=True) + + # Instead of the normal logic to on which sprite data to show, we jump to our custom code in bank 3E. + rom.patch(0x07, 0x3C36, None, ASM(""" + ld a, $01 + rst 8 + jp $7C5E + """), fill_nop=True) + + # Instead of the normal logic of showing the proper dialog, we jump to our custom code in bank 3E. + rom.patch(0x07, 0x3C9C, None, ASM(""" + ld a, $0A ; showItemMessageMultiworld + rst 8 + jp $7CE9 + """)) + + # Sound to play is normally loaded from a table, which is no longer big enough. So always use the same sound. + rom.patch(0x07, 0x3C81, ASM(""" + add hl, de + ld a, [hl] + """), ASM("ld a, $01"), fill_nop=True) + + # Always spawn seashells even if you have the L2 sword + rom.patch(0x14, 0x192F, ASM("ld a, $1C"), ASM("ld a, $20")) + + rom.texts[0x9A] = formatText("You found 10 {BOMB}!") + + +def setMultiChest(rom, option): + room = 0x2F2 + addr = room + 0x560 + rom.banks[0x14][addr] = CHEST_ITEMS[option] diff --git a/worlds/ladx/LADXR/patches/core.py b/worlds/ladx/LADXR/patches/core.py new file mode 100644 index 0000000000..a202e661f9 --- /dev/null +++ b/worlds/ladx/LADXR/patches/core.py @@ -0,0 +1,539 @@ +from ..assembler import ASM +from ..entranceInfo import ENTRANCE_INFO +from ..roomEditor import RoomEditor, ObjectWarp, ObjectHorizontal +from ..backgroundEditor import BackgroundEditor +from .. import utils + + +def bugfixWrittingWrongRoomStatus(rom): + # The normal rom contains a pretty nasty bug where door closing triggers in D7/D8 can effect doors in + # dungeons D1-D6. This fix should prevent this. + rom.patch(0x02, 0x1D21, 0x1D3C, ASM("call $5B9F"), fill_nop=True) + +def fixEggDeathClearingItems(rom): + rom.patch(0x01, 0x1E79, ASM("cp $0A"), ASM("cp $08")) + +def fixWrongWarp(rom): + rom.patch(0x00, 0x18CE, ASM("cp $04"), ASM("cp $03")) + re = RoomEditor(rom, 0x2b) + for x in range(10): + re.removeObject(x, 7) + re.objects.append(ObjectHorizontal(0, 7, 0x2C, 10)) + while len(re.getWarps()) < 4: + re.objects.append(ObjectWarp(1, 3, 0x7a, 80, 124)) + re.store(rom) + +def bugfixBossroomTopPush(rom): + rom.patch(0x14, 0x14D9, ASM(""" + ldh a, [$99] + dec a + ldh [$99], a + """), ASM(""" + jp $7F80 + """), fill_nop=True) + rom.patch(0x14, 0x3F80, "00" * 0x80, ASM(""" + ldh a, [$99] + cp $50 + jr nc, up +down: + inc a + ldh [$99], a + jp $54DE +up: + dec a + ldh [$99], a + jp $54DE + """), fill_nop=True) + +def bugfixPowderBagSprite(rom): + rom.patch(0x03, 0x2055, "8E16", "0E1E") + +def easyColorDungeonAccess(rom): + re = RoomEditor(rom, 0x312) + re.entities = [(3, 1, 246), (6, 1, 247)] + re.store(rom) + +def removeGhost(rom): + ## Ghost patch + # Do not have the ghost follow you after dungeon 4 + rom.patch(0x03, 0x1E1B, ASM("LD [$DB79], A"), "", fill_nop=True) + +def alwaysAllowSecretBook(rom): + rom.patch(0x15, 0x3F23, ASM("ld a, [$DB0E]\ncp $0E"), ASM("xor a\ncp $00"), fill_nop=True) + rom.patch(0x15, 0x3F2A, 0x3F30, "", fill_nop=True) + +def cleanup(rom): + # Remove unused rooms to make some space in the rom + re = RoomEditor(rom, 0x2C4) + re.objects = [] + re.entities = [] + re.store(rom, 0x2C4) + re.store(rom, 0x2D4) + re.store(rom, 0x277) + re.store(rom, 0x278) + re.store(rom, 0x279) + re.store(rom, 0x1ED) + re.store(rom, 0x1FC) # Beta room + + rom.texts[0x02B] = b'' # unused text + + +def disablePhotoPrint(rom): + rom.patch(0x28, 0x07CC, ASM("ldh [$01], a\nldh [$02], a"), "", fill_nop=True) # do not reset the serial link + rom.patch(0x28, 0x0483, ASM("ld a, $13"), ASM("jr $EA", 0x4483)) # Do not print on A press, but jump to cancel + rom.patch(0x28, 0x0492, ASM("ld hl, $4439"), ASM("ret"), fill_nop=True) # Do not show the print/cancel overlay + +def fixMarinFollower(rom): + # Allow opening of D0 with marin + rom.patch(0x02, 0x3402, ASM("ld a, [$DB73]"), ASM("xor a"), fill_nop=True) + # Instead of uselessly checking for sidescroller rooms for follower spawns, check for color dungeon instead + rom.patch(0x01, 0x1FCB, 0x1FD3, ASM("cp $FF\nret z"), fill_nop=True) + # Do not load marin graphics in color dungeon + rom.patch(0x00, 0x2EA6, 0x2EB0, ASM("cp $FF\njp $2ED3"), fill_nop=True) + # Fix marin on taltal bridge causing a lockup if you have marin with you + # This changes the location where the index to the marin entity is stored from it's normal location + # To the memory normal reserved for progress on the egg maze (which is reset to 0 on a warp) + rom.patch(0x18, 0x1EF7, ASM("ld [$C50F], a"), ASM("ld [$C5AA], a")) + rom.patch(0x18, 0x2126, ASM("ld a, [$C50F]"), ASM("ld a, [$C5AA]")) + rom.patch(0x18, 0x2139, ASM("ld a, [$C50F]"), ASM("ld a, [$C5AA]")) + rom.patch(0x18, 0x214F, ASM("ld a, [$C50F]"), ASM("ld a, [$C5AA]")) + rom.patch(0x18, 0x2166, ASM("ld a, [$C50F]"), ASM("ld a, [$C5AA]")) + +def quickswap(rom, button): + rom.patch(0x00, 0x1094, ASM("jr c, $49"), ASM("jr nz, $49")) # prevent agressive key repeat + rom.patch(0x00, 0x10BC, # Patch the open minimap code to swap the your items instead + ASM("xor a\nld [$C16B], a\nld [$C16C], a\nld [$DB96], a\nld a, $07\nld [$DB95], a"), ASM(""" + ld a, [$DB%02X] + ld e, a + ld a, [$DB%02X] + ld [$DB%02X], a + ld a, e + ld [$DB%02X], a + ret + """ % (button, button + 2, button, button + 2))) + +def injectMainLoop(rom): + rom.patch(0x00, 0x0346, ASM(""" + ldh a, [$FE] + and a + jr z, $08 + """), ASM(""" + ; Call the mainloop handler + xor a + rst 8 + """), fill_nop=True) + +def warpHome(rom): + # Patch the S&Q menu to allow 3 options + rom.patch(0x01, 0x012A, 0x0150, ASM(""" + ld hl, $C13F + call $6BA8 ; make sound on keypress + ldh a, [$CC] ; load joystick status + and $04 ; if up + jr z, noUp + dec [hl] +noUp: + ldh a, [$CC] ; load joystick status + and $08 ; if down + jr z, noDown + inc [hl] +noDown: + + ld a, [hl] + cp $ff + jr nz, noWrapUp + ld a, $02 +noWrapUp: + cp $03 + jr nz, noWrapDown + xor a +noWrapDown: + ld [hl], a + jp $7E02 + """), fill_nop=True) + rom.patch(0x01, 0x3E02, 0x3E20, ASM(""" + swap a + add a, $48 + ld hl, $C018 + ldi [hl], a + ld a, $24 + ldi [hl], a + ld a, $BE + ldi [hl], a + ld [hl], $00 + ret + """), fill_nop=True) + + rom.patch(0x01, 0x00B7, ASM(""" + ld a, [$C13F] + cp $01 + jr z, $3B + """), ASM(""" + ld a, [$C13F] + jp $7E20 + """), fill_nop=True) + + re = RoomEditor(rom, 0x2a3) + warp = re.getWarps()[0] + + type = 0x00 + map = 0x00 + room = warp.room + x = warp.target_x + y = warp.target_y + + one_way = [ + 'd0', + 'd1', + 'd3', + 'd4', + 'd6', + 'd8', + 'animal_cave', + 'right_fairy', + 'rooster_grave', + 'prairie_left_cave2', + 'prairie_left_fairy', + 'armos_fairy', + 'boomerang_cave', + 'madbatter_taltal', + 'forest_madbatter', + ] + + one_way = {ENTRANCE_INFO[x].room for x in one_way} + + if warp.room in one_way: + # we're starting at a one way exit room + # warp indoors to avoid soft locks + type = 0x01 + map = 0x10 + room = 0xa3 + x = 0x50 + y = 0x7f + + rom.patch(0x01, 0x3E20, 0x3E6B, ASM(""" + ; First, handle save & quit + cp $01 + jp z, $40F9 + and a + jp z, $40BE ; return to normal "return to game" handling + + ld a, [$C509] ; Check if we have an item in the shop + and a + jp nz, $40BE ; return to normal "return to game" handling + + ld a, $0B + ld [$DB95], a + call $0C7D + + ; Replace warp0 tile data, and put link on that tile. + ld a, $%02x ; Type + ld [$D401], a + ld a, $%02x ; Map + ld [$D402], a + ld a, $%02x ; Room + ld [$D403], a + ld a, $%02x ; X + ld [$D404], a + ld a, $%02x ; Y + ld [$D405], a + + ldh a, [$98] + swap a + and $0F + ld e, a + ldh a, [$99] + sub $08 + and $F0 + or e + ld [$D416], a + + ld a, $07 + ld [$DB96], a + ret + jp $40BE ; return to normal "return to game" handling + """ % (type, map, room, x, y)), fill_nop=True) + + # Patch the RAM clear not to delete our custom dialog when we screen transition + rom.patch(0x01, 0x042C, "C629", "6B7E") + rom.patch(0x01, 0x3E6B, 0x3FFF, ASM(""" + ld bc, $A0 + call $29DC + ld bc, $1200 + ld hl, $C100 + call $29DF + ret + """), fill_nop=True) + # Patch the S&Q screen to have 3 options. + be = BackgroundEditor(rom, 0x0D) + for n in range(2, 18): + be.tiles[0x99C0 + n] = be.tiles[0x9980 + n] + be.tiles[0x99A0 + n] = be.tiles[0x9960 + n] + be.tiles[0x9980 + n] = be.tiles[0x9940 + n] + be.tiles[0x9960 + n] = be.tiles[0x98e0 + n] + be.tiles[0x9960 + 10] = 0xCE + be.tiles[0x9960 + 11] = 0xCF + be.tiles[0x9960 + 12] = 0xC4 + be.tiles[0x9960 + 13] = 0x7F + be.tiles[0x9960 + 14] = 0x7F + be.store(rom) + + sprite_data = [ + 0b00000000, + 0b01000100, + 0b01000101, + 0b01000101, + 0b01111101, + 0b01000101, + 0b01000101, + 0b01000100, + + 0b00000000, + 0b11100100, + 0b00010110, + 0b00010101, + 0b00010100, + 0b00010100, + 0b00010100, + 0b11100100, + ] + for n in range(32): + rom.banks[0x0F][0x08E0 + n] = sprite_data[n // 2] + + +def addFrameCounter(rom, check_count): + # Patch marin giving the start the game to jump to a custom handler + rom.patch(0x05, 0x1299, ASM("ld a, $01\ncall $2385"), ASM("push hl\nld a, $0D\nrst 8\npop hl"), fill_nop=True) + + # Add code that needs to be called every frame to tick our ingame time counter. + rom.patch(0x00, 0x0091, "00" * (0x100 - 0x91), ASM(""" + ld a, [$DB95] ;Get the gameplay type + dec a ; and if it was 1 + ret z ; we are at the credits and the counter should stop. + + ; Check if the timer expired + ld hl, $FF0F + bit 2, [hl] + ret z + res 2, [hl] + + ; Increase the "subsecond" counter, and continue if it "overflows" + call $27D0 ; Enable SRAM + ld hl, $B000 + ld a, [hl] + inc a + cp $20 + ld [hl], a + ret nz + xor a + ldi [hl], a + + ; Increase the seconds counter/minutes/hours counter +increaseSecMinHours: + ld a, [hl] + inc a + daa + ld [hl], a + cp $60 + ret nz + xor a + ldi [hl], a + jr increaseSecMinHours + """), fill_nop=True) + # Replace a cgb check with the call to our counter code. + rom.patch(0x00, 0x0367, ASM("ld a, $0C\ncall $0B0B"), ASM("call $0091\nld a, $2C")) + + # Do not switch to 8x8 sprite mode + rom.patch(0x17, 0x2E9E, ASM("res 2, [hl]"), "", fill_nop=True) + # We need to completely reorder link sitting on the raft to work with 16x8 sprites. + sprites = rom.banks[0x38][0x1600:0x1800] + sprites[0x1F0:0x200] = b'\x00' * 16 + for index, position in enumerate( + (0, 0x1F, + 1, 0x1F, 2, 0x1F, + 7, 8, + 3, 9, 4, 10, 5, 11, 6, 12, + 3, 13, 4, 14, 5, 15, 6, 16, + 3, 17, 4, 18, 5, 19, 6, 20, + )): + rom.banks[0x38][0x1600+index*0x10:0x1610+index*0x10] = sprites[position*0x10:0x10+position*0x10] + rom.patch(0x27, 0x376E, 0x3776, "00046601", fill_nop=True) + rom.patch(0x27, 0x384E, ASM("ld c, $08"), ASM("ld c, $04")) + rom.patch(0x27, 0x3776, 0x3826, + "FA046002" + "0208640402006204" + "0A106E030A086C030A006A030AF86803" + + "FA046002" + "0208640402006204" + "0A1076030A0874030A0072030AF87003" + + "FA046002" + "0208640402006204" + "0A107E030A087C030A007A030AF87803" + , fill_nop=True) + rom.patch(0x27, 0x382E, ASM("ld a, $6C"), ASM("ld a, $80")) # OAM start position + rom.patch(0x27, 0x384E, ASM("ld c, $08"), ASM("ld c, $04")) # Amount of overlay OAM data + rom.patch(0x27, 0x3826, 0x382E, ASM("dw $7776, $7792, $77AE, $7792")) # pointers to animation + rom.patch(0x27, 0x3846, ASM("ld c, $2C"), ASM("ld c, $1C")) # Amount of OAM data + + # TODO: fix flying windfish + # Upper line of credits roll into "TIME" + rom.patch(0x17, 0x069D, 0x0713, ASM(""" + ld hl, OAMData + ld de, $C000 ; OAM Buffer + ld bc, $0048 + call $2914 + ret +OAMData: + db $20, $18, $34, $00 ;T + db $20, $20, $20, $00 ;I + db $20, $28, $28, $00 ;M + db $20, $30, $18, $00 ;E + + db $20, $70, $16, $00 ;D + db $20, $78, $18, $00 ;E + db $20, $80, $10, $00 ;A + db $20, $88, $34, $00 ;T + db $20, $90, $1E, $00 ;H + + db $50, $18, $14, $00 ;C + db $50, $20, $1E, $00 ;H + db $50, $28, $18, $00 ;E + db $50, $30, $14, $00 ;C + db $50, $38, $24, $00 ;K + db $50, $40, $32, $00 ;S + + db $68, $38, $%02x, $00 ;0 + db $68, $40, $%02x, $00 ;0 + db $68, $48, $%02x, $00 ;0 + + """ % ((((check_count // 100) % 10) * 2) | 0x40, (((check_count // 10) % 10) * 2) | 0x40, ((check_count % 10) * 2) | 0x40), 0x469D), fill_nop=True) + # Lower line of credits roll into XX XX XX + rom.patch(0x17, 0x0784, 0x082D, ASM(""" + ld hl, OAMData + ld de, $C048 ; OAM Buffer + ld bc, $0038 + call $2914 + + call $27D0 ; Enable SRAM + ld hl, $C04A + ld a, [$B003] ; hours + call updateOAM + ld a, [$B002] ; minutes + call updateOAM + ld a, [$B001] ; seconds + call updateOAM + + ld a, [$DB58] ; death count high + call updateOAM + ld a, [$DB57] ; death count low + call updateOAM + + ld a, [$B011] ; check count high + call updateOAM + ld a, [$B010] ; check count low + call updateOAM + ret + +updateOAM: + ld de, $0004 + ld b, a + swap a + and $0F + add a, a + or $40 + ld [hl], a + add hl, de + + ld a, b + and $0F + add a, a + or $40 + ld [hl], a + add hl, de + ret +OAMData: + db $38, $18, $40, $00 ;0 (10 hours) + db $38, $20, $40, $00 ;0 (1 hours) + db $38, $30, $40, $00 ;0 (10 minutes) + db $38, $38, $40, $00 ;0 (1 minutes) + db $38, $48, $40, $00 ;0 (10 seconds) + db $38, $50, $40, $00 ;0 (1 seconds) + + db $00, $00, $40, $00 ;0 (1000 death) + db $38, $80, $40, $00 ;0 (100 death) + + db $38, $88, $40, $00 ;0 (10 death) + db $38, $90, $40, $00 ;0 (1 death) + + ; checks + db $00, $00, $40, $00 ;0 + db $68, $18, $40, $00 ;0 + db $68, $20, $40, $00 ;0 + db $68, $28, $40, $00 ;0 + + """, 0x4784), fill_nop=True) + + # Grab the "mostly" complete A-Z font + sprites = rom.banks[0x38][0x1100:0x1400] + for index, position in enumerate(( + 0x10, 0x20, # A + 0x11, 0x21, # B + 0x12, 0x12 | 0x100, # C + 0x13, 0x23, # D + 0x14, 0x24, # E + 0x14, 0x25, # F + 0x12, 0x22, # G + 0x20 | 0x100, 0x26, # H + 0x17, 0x17 | 0x100, # I + 0x28, 0x28, # J + 0x19, 0x29, # K + 0x06, 0x07, # L + 0x1A, 0x2A, # M + 0x1B, 0x2B, # N + 0x00, 0x00, # O? + 0x00, 0x00, # P? + #0x00, 0x00, # Q? + 0x11, 0x18, # R + 0x1C, 0x2C, # S + 0x1D, 0x2D, # T + 0x26, 0x10, # U + 0x00, 0x00, # V? + 0x1E, 0x2E, # W + #0x00, 0x00, # X? + #0x00, 0x00, # Y? + 0x27, 0x27, # Z + )): + sprite = sprites[(position&0xFF)*0x10:0x10+(position&0xFF)*0x10] + if position & 0x100: + for n in range(4): + sprite[n * 2], sprite[14 - n * 2] = sprite[14 - n * 2], sprite[n * 2] + sprite[n * 2 + 1], sprite[15 - n * 2] = sprite[15 - n * 2], sprite[n * 2 + 1] + rom.banks[0x38][0x1100+index*0x10:0x1110+index*0x10] = sprite + + + # Number graphics change for the end + tile_graphics = """ +........ ........ ........ ........ ........ ........ ........ ........ ........ ........ +.111111. ..1111.. .111111. .111111. ..11111. 11111111 .111111. 11111111 .111111. .111111. +11333311 .11331.. 11333311 11333311 .113331. 13333331 11333311 13333331 11333311 11333311 +13311331 113331.. 13311331 13311331 1133331. 13311111 13311331 11111331 13311331 13311331 +13311331 133331.. 13311331 11111331 1331331. 1331.... 13311331 ...11331 13311331 13311331 +13311331 133331.. 11111331 ....1331 1331331. 1331.... 13311111 ...13311 13311331 13311331 +13311331 111331.. ...13311 .1111331 1331331. 1331111. 1331.... ..11331. 13311331 13311331 +13311331 ..1331.. ..11331. .1333331 13313311 13333311 1331111. ..13311. 11333311 11333331 +13311331 ..1331.. ..13311. .1111331 13333331 13311331 13333311 .11331.. 13311331 .1111331 +13311331 ..1331.. .11331.. ....1331 11113311 11111331 13311331 .13311.. 13311331 ....1331 +13311331 ..1331.. .13311.. ....1331 ...1331. ....1331 13311331 11331... 13311331 ....1331 +13311331 ..1331.. 11331... 11111331 ...1331. 11111331 13311331 13311... 13311331 11111331 +13311331 ..1331.. 13311111 13311331 ...1331. 13311331 13311331 1331.... 13311331 13311331 +11333311 ..1331.. 13333331 11333311 ...1331. 11333311 11333311 1331.... 11333311 11333311 +.111111. ..1111.. 11111111 .111111. ...1111. .111111. .111111. 1111.... .111111. .111111. +........ ........ ........ ........ ........ ........ ........ ........ ........ ........ +""".strip() + for n in range(10): + gfx_high = "\n".join([line.split(" ")[n] for line in tile_graphics.split("\n")[:8]]) + gfx_low = "\n".join([line.split(" ")[n] for line in tile_graphics.split("\n")[8:]]) + rom.banks[0x38][0x1400+n*0x20:0x1410+n*0x20] = utils.createTileData(gfx_high) + rom.banks[0x38][0x1410+n*0x20:0x1420+n*0x20] = utils.createTileData(gfx_low) diff --git a/worlds/ladx/LADXR/patches/desert.py b/worlds/ladx/LADXR/patches/desert.py new file mode 100644 index 0000000000..e3f008661c --- /dev/null +++ b/worlds/ladx/LADXR/patches/desert.py @@ -0,0 +1,7 @@ +from ..roomEditor import RoomEditor + + +def desertAccess(rom): + re = RoomEditor(rom, 0x0FD) + re.entities = [(6, 2, 0xC4)] + re.store(rom) diff --git a/worlds/ladx/LADXR/patches/droppedKey.py b/worlds/ladx/LADXR/patches/droppedKey.py new file mode 100644 index 0000000000..d24b8b76c7 --- /dev/null +++ b/worlds/ladx/LADXR/patches/droppedKey.py @@ -0,0 +1,134 @@ +from ..assembler import ASM + + +def fixDroppedKey(rom): + # Patch the rendering code to use the dropped key rendering code. + rom.patch(0x03, 0x1C99, None, ASM(""" + ld a, $04 + rst 8 + jp $5CA6 + """)) + + # Patch the key pickup code to use the chest pickup code. + rom.patch(0x03, 0x248F, None, ASM(""" + ldh a, [$F6] ; load room nr + cp $7C ; L4 Side-view room where the key drops + jr nz, notSpecialSideView + + ld hl, $D969 ; status of the room above the side-view where the key drops in dungeon 4 + set 4, [hl] +notSpecialSideView: + call $512A ; mark room as done + + ; Handle item effect + ld a, $06 ; giveItemMultiworld + rst 8 + + ldh a, [$F1] ; Load active sprite variant to see if this is just a normal small key + cp $1A + jr z, isAKey + + ;Show message (if not a key) + ld a, $0A ; showMessageMultiworld + rst 8 +isAKey: + ret + """)) + rom.patch(0x03, 0x24B7, "3E", "3E") # sanity check + + # Mark all dropped keys as keys by default. + for n in range(0x316): + rom.banks[0x3E][0x3800 + n] = 0x1A + # Set the proper angler key by default + rom.banks[0x3E][0x3800 + 0x0CE] = 0x12 + rom.banks[0x3E][0x3800 + 0x1F8] = 0x12 + # Set the proper bird key by default + rom.banks[0x3E][0x3800 + 0x27A] = 0x14 + # Set the proper face key by default + rom.banks[0x3E][0x3800 + 0x27F] = 0x13 + + # Set the proper hookshot key by default + rom.banks[0x3E][0x3800 + 0x180] = 0x03 + + # Set the proper golden leaves + rom.banks[0x3E][0x3800 + 0x058] = 0x15 + rom.banks[0x3E][0x3800 + 0x05a] = 0x15 + rom.banks[0x3E][0x3800 + 0x2d2] = 0x15 + rom.banks[0x3E][0x3800 + 0x2c5] = 0x15 + rom.banks[0x3E][0x3800 + 0x2c6] = 0x15 + + # Set the slime key drop. + rom.banks[0x3E][0x3800 + 0x0C6] = 0x0F + + # Set the heart pieces + rom.banks[0x3E][0x3800 + 0x000] = 0x80 + rom.banks[0x3E][0x3800 + 0x2A4] = 0x80 + rom.banks[0x3E][0x3800 + 0x2B1] = 0x80 # fishing game, unused + rom.banks[0x3E][0x3800 + 0x044] = 0x80 + rom.banks[0x3E][0x3800 + 0x2AB] = 0x80 + rom.banks[0x3E][0x3800 + 0x2DF] = 0x80 + rom.banks[0x3E][0x3800 + 0x2E5] = 0x80 + rom.banks[0x3E][0x3800 + 0x078] = 0x80 + rom.banks[0x3E][0x3800 + 0x2E6] = 0x80 + rom.banks[0x3E][0x3800 + 0x1E8] = 0x80 + rom.banks[0x3E][0x3800 + 0x1F2] = 0x80 + rom.banks[0x3E][0x3800 + 0x2BA] = 0x80 + + # Set the seashells + rom.banks[0x3E][0x3800 + 0x0A3] = 0x20 + rom.banks[0x3E][0x3800 + 0x2B2] = 0x20 + rom.banks[0x3E][0x3800 + 0x0A5] = 0x20 + rom.banks[0x3E][0x3800 + 0x0A6] = 0x20 + rom.banks[0x3E][0x3800 + 0x08B] = 0x20 + rom.banks[0x3E][0x3800 + 0x074] = 0x20 + rom.banks[0x3E][0x3800 + 0x0A4] = 0x20 + rom.banks[0x3E][0x3800 + 0x0D2] = 0x20 + rom.banks[0x3E][0x3800 + 0x0E9] = 0x20 + rom.banks[0x3E][0x3800 + 0x0B9] = 0x20 + rom.banks[0x3E][0x3800 + 0x0F8] = 0x20 + rom.banks[0x3E][0x3800 + 0x0A8] = 0x20 + rom.banks[0x3E][0x3800 + 0x0FF] = 0x20 + rom.banks[0x3E][0x3800 + 0x1E3] = 0x20 + rom.banks[0x3E][0x3800 + 0x0DA] = 0x20 + rom.banks[0x3E][0x3800 + 0x00C] = 0x20 + + # Set heart containers + rom.banks[0x3E][0x3800 + 0x106] = 0x89 + rom.banks[0x3E][0x3800 + 0x12B] = 0x89 + rom.banks[0x3E][0x3800 + 0x15A] = 0x89 + rom.banks[0x3E][0x3800 + 0x1FF] = 0x89 + rom.banks[0x3E][0x3800 + 0x185] = 0x89 + rom.banks[0x3E][0x3800 + 0x1BC] = 0x89 + rom.banks[0x3E][0x3800 + 0x2E8] = 0x89 + rom.banks[0x3E][0x3800 + 0x234] = 0x89 + + # Toadstool + rom.banks[0x3E][0x3800 + 0x050] = 0x50 + # Sword on beach + rom.banks[0x3E][0x3800 + 0x0F2] = 0x0B + # Sword upgrade + rom.banks[0x3E][0x3800 + 0x2E9] = 0x0B + + # Songs + rom.banks[0x3E][0x3800 + 0x092] = 0x8B # song 1 + rom.banks[0x3E][0x3800 + 0x0DC] = 0x8B # song 1 + rom.banks[0x3E][0x3800 + 0x2FD] = 0x8C # song 2 + rom.banks[0x3E][0x3800 + 0x2FB] = 0x8D # song 3 + + # Instruments + rom.banks[0x3E][0x3800 + 0x102] = 0x8E + rom.banks[0x3E][0x3800 + 0x12a] = 0x8F + rom.banks[0x3E][0x3800 + 0x159] = 0x90 + rom.banks[0x3E][0x3800 + 0x162] = 0x91 + rom.banks[0x3E][0x3800 + 0x182] = 0x92 + rom.banks[0x3E][0x3800 + 0x1b5] = 0x93 + rom.banks[0x3E][0x3800 + 0x22c] = 0x94 + rom.banks[0x3E][0x3800 + 0x230] = 0x95 + + # Start item + rom.banks[0x3E][0x3800 + 0x2a3] = 0x01 + + # Master stalfos overkill drops + rom.banks[0x3E][0x3800 + 0x195] = 0x1A + rom.banks[0x3E][0x3800 + 0x192] = 0x1A + rom.banks[0x3E][0x3800 + 0x184] = 0x1A diff --git a/worlds/ladx/LADXR/patches/dungeon.py b/worlds/ladx/LADXR/patches/dungeon.py new file mode 100644 index 0000000000..f99d99fe49 --- /dev/null +++ b/worlds/ladx/LADXR/patches/dungeon.py @@ -0,0 +1,129 @@ +from ..roomEditor import RoomEditor, Object, ObjectHorizontal + + +KEY_DOORS = { + 0xEC: 0xF4, + 0xED: 0xF5, + 0xEE: 0xF6, + 0xEF: 0xF7, + 0xF8: 0xF4, +} + +def removeKeyDoors(rom): + for n in range(0x100, 0x316): + if n == 0x2FF: + continue + update = False + re = RoomEditor(rom, n) + for obj in re.objects: + if obj.type_id in KEY_DOORS: + obj.type_id = KEY_DOORS[obj.type_id] + update = True + if obj.type_id == 0xDE: # Keyblocks + obj.type_id = re.floor_object & 0x0F + update = True + if update: + re.store(rom) + + +def patchNoDungeons(rom): + def setMinimap(dungeon_nr, x, y, room): + for n in range(64): + if rom.banks[0x14][0x0220 + 64 * dungeon_nr + n] == room: + rom.banks[0x14][0x0220 + 64 * dungeon_nr + n] = 0xFF + rom.banks[0x14][0x0220 + 64 * dungeon_nr + x + y * 8] = room + #D1 + setMinimap(0, 3, 6, 0x06) + setMinimap(0, 3, 5, 0x02) + re = RoomEditor(rom, 0x117) + for n in range(1, 7): + re.removeObject(n, 0) + re.removeObject(0, n) + re.removeObject(9, n) + re.objects += [Object(4, 0, 0xf0)] + re.store(rom) + re = RoomEditor(rom, 0x11A) + re.getWarps()[0].room = 0x117 + re.store(rom) + re = RoomEditor(rom, 0x11B) + re.getWarps()[0].room = 0x117 + re.store(rom) + + #D2 + setMinimap(1, 2, 6, 0x2B) + setMinimap(1, 1, 6, 0x2A) + re = RoomEditor(rom, 0x136) + for n in range(1, 7): + re.removeObject(n, 0) + re.objects += [Object(4, 0, 0xf0)] + re.store(rom) + + #D3 + setMinimap(2, 1, 6, 0x5A) + setMinimap(2, 1, 5, 0x59) + re = RoomEditor(rom, 0x152) + for n in range(2, 7): + re.removeObject(9, n) + re.store(rom) + + #D4 + setMinimap(3, 3, 6, 0x66) + setMinimap(3, 3, 5, 0x62) + re = RoomEditor(rom, 0x17A) + for n in range(3, 7): + re.removeObject(n, 0) + re.objects += [Object(4, 0, 0xf0)] + re.store(rom) + + #D5 + setMinimap(4, 7, 6, 0x85) + setMinimap(4, 7, 5, 0x82) + re = RoomEditor(rom, 0x1A1) + for n in range(3, 8): + re.removeObject(n, 0) + re.removeObject(0, n) + for n in range(4, 6): + re.removeObject(n, 1) + re.removeObject(n, 2) + re.objects += [Object(4, 0, 0xf0)] + re.store(rom) + + #D6 + setMinimap(5, 3, 6, 0xBC) + setMinimap(5, 3, 5, 0xB5) + re = RoomEditor(rom, 0x1D4) + for n in range(2, 8): + re.removeObject(0, n) + re.removeObject(9, n) + re.objects += [Object(4, 0, 0xf0)] + re.store(rom) + + #D7 + setMinimap(6, 1, 6, 0x2E) + setMinimap(6, 1, 5, 0x2C) + re = RoomEditor(rom, 0x20E) + for n in range(1, 8): + re.removeObject(0, n) + re.removeObject(9, n) + re.objects += [Object(3, 0, 0x29), ObjectHorizontal(4, 0, 0x0D, 2), Object(6, 0, 0x2A)] + re.store(rom) + re = RoomEditor(rom, 0x22E) + re.objects = [Object(4, 0, 0xf0), Object(3, 7, 0x2B), ObjectHorizontal(4, 7, 0x0D, 2), Object(6, 7, 0x2C), Object(1, 0, 0xA8)] + re.getWarps() + re.floor_object = 13 + re.store(rom) + re = RoomEditor(rom, 0x22C) + re.removeObject(0, 7) + re.removeObject(2, 7) + re.objects.append(ObjectHorizontal(0, 7, 0x03, 3)) + re.store(rom) + + #D8 + setMinimap(7, 3, 6, 0x34) + setMinimap(7, 3, 5, 0x30) + re = RoomEditor(rom, 0x25D) + re.objects += [Object(3, 0, 0x25), Object(4, 0, 0xf0), Object(6, 0, 0x26)] + re.store(rom) + + #D0 + setMinimap(11, 2, 6, 0x00) + setMinimap(11, 3, 6, 0x01) diff --git a/worlds/ladx/LADXR/patches/endscreen.py b/worlds/ladx/LADXR/patches/endscreen.py new file mode 100644 index 0000000000..843120f1c0 --- /dev/null +++ b/worlds/ladx/LADXR/patches/endscreen.py @@ -0,0 +1,139 @@ +from ..assembler import ASM +import os + + +def updateEndScreen(rom): + # Call our custom data loader in bank 3F + rom.patch(0x00, 0x391D, ASM(""" + ld a, $20 + ld [$2100], a + jp $7de6 + """), ASM(""" + ld a, $3F + ld [$2100], a + jp $4200 + """)) + rom.patch(0x17, 0x2FCE, "B170", "D070") # Ignore the final tile data load + + rom.patch(0x3F, 0x0200, None, ASM(""" + ; Disable LCD + xor a + ldh [$40], a + + ld hl, $8000 + ld de, $5000 +copyLoop: + ld a, [de] + inc de + ldi [hl], a + bit 4, h + jr z, copyLoop + + ld a, $01 + ldh [$4F], a + + ld hl, $8000 + ld de, $6000 +copyLoop2: + ld a, [de] + inc de + ldi [hl], a + bit 4, h + jr z, copyLoop2 + + ld hl, $9800 + ld de, $0190 +clearLoop1: + xor a + ldi [hl], a + dec de + ld a, d + or e + jr nz, clearLoop1 + + ld de, $0190 +clearLoop2: + ld a, $08 + ldi [hl], a + dec de + ld a, d + or e + jr nz, clearLoop2 + + xor a + ldh [$4F], a + + + ld hl, $9800 + ld de, $000C + xor a +loadLoop1: + ldi [hl], a + ld b, a + ld a, l + and $1F + cp $14 + jr c, .noLineSkip + add hl, de +.noLineSkip: + ld a, b + inc a + jr nz, loadLoop1 + +loadLoop2: + ldi [hl], a + ld b, a + ld a, l + and $1F + cp $14 + jr c, .noLineSkip + add hl, de +.noLineSkip: + ld a, b + inc a + jr nz, loadLoop2 + + ; Load palette + ld hl, $DC10 + ld a, $00 + ldi [hl], a + ld a, $00 + ldi [hl], a + + ld a, $ad + ldi [hl], a + ld a, $35 + ldi [hl], a + + ld a, $94 + ldi [hl], a + ld a, $52 + ldi [hl], a + + ld a, $FF + ldi [hl], a + ld a, $7F + ldi [hl], a + + ld a, $00 + ld [$DDD3], a + ld a, $04 + ld [$DDD4], a + ld a, $81 + ld [$DDD1], a + + ; Enable LCD + ld a, $91 + ldh [$40], a + ld [$d6fd], a + + xor a + ldh [$96], a + ldh [$97], a + ret + """)) + + addr = 0x1000 + for c in open(os.path.join(os.path.dirname(__file__), "nyan.bin"), "rb").read(): + rom.banks[0x3F][addr] = c + addr += 1 diff --git a/worlds/ladx/LADXR/patches/enemies.py b/worlds/ladx/LADXR/patches/enemies.py new file mode 100644 index 0000000000..f5e1df1313 --- /dev/null +++ b/worlds/ladx/LADXR/patches/enemies.py @@ -0,0 +1,462 @@ +from ..roomEditor import RoomEditor, Object, ObjectWarp, ObjectHorizontal +from ..assembler import ASM +from ..locations import constants +from typing import List + + +# Room containing the boss +BOSS_ROOMS = [ + 0x106, + 0x12b, + 0x15a, + 0x166, + 0x185, + 0x1bc, + 0x223, # Note: unused room normally + 0x234, + 0x300, +] +BOSS_ENTITIES = [ + (3, 2, 0x59), + (4, 2, 0x5C), + (4, 3, 0x5B), + None, + (4, 3, 0x5D), + (4, 3, 0x5A), + None, + (4, 3, 0x62), + (5, 2, 0xF9), +] +MINIBOSS_ENTITIES = { + "ROLLING_BONES": [(8, 3, 0x81), (6, 3, 0x82)], + "HINOX": [(5, 2, 0x89)], + "DODONGO": [(3, 2, 0x60), (5, 2, 0x60)], + "CUE_BALL": [(1, 1, 0x8e)], + "GHOMA": [(2, 1, 0x5e), (2, 4, 0x5e)], + "SMASHER": [(5, 2, 0x92)], + "GRIM_CREEPER": [(4, 0, 0xbc)], + "BLAINO": [(5, 3, 0xbe)], + "AVALAUNCH": [(5, 1, 0xf4)], + "GIANT_BUZZ_BLOB": [(4, 2, 0xf8)], + "MOBLIN_KING": [(5, 5, 0xe4)], + "ARMOS_KNIGHT": [(4, 3, 0x88)], +} +MINIBOSS_ROOMS = { + 0: 0x111, 1: 0x128, 2: 0x145, 3: 0x164, 4: 0x193, 5: 0x1C5, 6: 0x228, 7: 0x23F, + "c1": 0x30C, "c2": 0x303, + "moblin_cave": 0x2E1, + "armos_temple": 0x27F, +} + + +def fixArmosKnightAsMiniboss(rom): + # Make the armos temple room with armos knight drop a ceiling key on kill. + # This makes the door always open, but that's fine. + rom.patch(0x14, 0x017F, "21", "81") + + # Do not change the drop from Armos knight into a ceiling key. + rom.patch(0x06, 0x12E8, ASM("ld [hl], $30"), "", fill_nop=True) + + +def getBossRoomStatusFlagLocation(dungeon_nr): + if BOSS_ROOMS[dungeon_nr] >= 0x300: + return 0xDDE0 - 0x300 + BOSS_ROOMS[dungeon_nr] + return 0xD800 + BOSS_ROOMS[dungeon_nr] + + +def fixDungeonItem(item_chest_id, dungeon_nr): + if item_chest_id == constants.CHEST_ITEMS[constants.MAP]: + return constants.CHEST_ITEMS["MAP%d" % (dungeon_nr + 1)] + if item_chest_id == constants.CHEST_ITEMS[constants.COMPASS]: + return constants.CHEST_ITEMS["COMPASS%d" % (dungeon_nr + 1)] + if item_chest_id == constants.CHEST_ITEMS[constants.KEY]: + return constants.CHEST_ITEMS["KEY%d" % (dungeon_nr + 1)] + if item_chest_id == constants.CHEST_ITEMS[constants.NIGHTMARE_KEY]: + return constants.CHEST_ITEMS["NIGHTMARE_KEY%d" % (dungeon_nr + 1)] + if item_chest_id == constants.CHEST_ITEMS[constants.STONE_BEAK]: + return constants.CHEST_ITEMS["STONE_BEAK%d" % (dungeon_nr + 1)] + return item_chest_id + + +def getCleanBossRoom(rom, dungeon_nr): + re = RoomEditor(rom, BOSS_ROOMS[dungeon_nr]) + new_objects = [] + for obj in re.objects: + if isinstance(obj, ObjectWarp): + continue + if obj.type_id == 0xBE: # Remove staircases + continue + if obj.type_id == 0x06: # Remove lava + continue + if obj.type_id == 0x1c: # Change D1 pits into normal pits + obj.type_id = 0x01 + if obj.type_id == 0x1e: # Change D1 pits into normal pits + obj.type_id = 0xaf + if obj.type_id == 0x1f: # Change D1 pits into normal pits + obj.type_id = 0xb0 + if obj.type_id == 0xF5: # Change open doors into closing doors. + obj.type_id = 0xF1 + new_objects.append(obj) + + + # Make D4 room a valid fighting room by removing most content. + if dungeon_nr == 3: + new_objects = new_objects[:2] + [Object(1, 1, 0xAC), Object(8, 1, 0xAC), Object(1, 6, 0xAC), Object(8, 6, 0xAC)] + + # D7 has an empty room we use for most bosses, but it needs some adjustments. + if dungeon_nr == 6: + # Move around the unused and instrument room. + rom.banks[0x14][0x03a0 + 6 + 1 * 8] = 0x00 + rom.banks[0x14][0x03a0 + 7 + 2 * 8] = 0x2C + rom.banks[0x14][0x03a0 + 7 + 3 * 8] = 0x23 + rom.banks[0x14][0x03a0 + 6 + 5 * 8] = 0x00 + + rom.banks[0x14][0x0520 + 7 + 2 * 8] = 0x2C + rom.banks[0x14][0x0520 + 7 + 3 * 8] = 0x23 + rom.banks[0x14][0x0520 + 6 + 5 * 8] = 0x00 + + re.floor_object &= 0x0F + new_objects += [ + Object(4, 0, 0xF0), + Object(1, 6, 0xBE), + ObjectWarp(1, dungeon_nr, 0x22E, 24, 16) + ] + + # Set the stairs towards the eagle tower top to our new room. + r = RoomEditor(rom, 0x22E) + r.objects[-1] = ObjectWarp(1, dungeon_nr, re.room, 24, 112) + r.store(rom) + + # Remove the normal door to the instrument room + r = RoomEditor(rom, 0x22e) + r.removeObject(4, 0) + r.store(rom) + rom.banks[0x14][0x22e - 0x100] = 0x00 + + r = RoomEditor(rom, 0x22c) + r.changeObject(0, 7, 0x03) + r.changeObject(2, 7, 0x03) + r.store(rom) + + re.objects = new_objects + re.entities = [] + return re + + +def changeBosses(rom, mapping: List[int]): + # Fix the color dungeon not properly warping to room 0 with the boss. + for addr in range(0x04E0, 0x04E0 + 64): + if rom.banks[0x14][addr] == 0x00 and addr not in {0x04E0 + 1 + 3 * 8, 0x04E0 + 2 + 6 * 8}: + rom.banks[0x14][addr] = 0xFF + # Fix the genie death not really liking pits/water. + rom.patch(0x04, 0x0521, ASM("ld [hl], $81"), ASM("ld [hl], $91")) + + # For the sidescroll bosses, we need to update this check to be the evil eagle dungeon. + # But if evil eagle is not there we still need to remove this check to make angler fish work in D7 + dungeon_nr = mapping.index(6) if 6 in mapping else 0xFE + rom.patch(0x02, 0x1FC8, ASM("cp $06"), ASM("cp $%02x" % (dungeon_nr if dungeon_nr < 8 else 0xff))) + + for dungeon_nr in range(9): + target = mapping[dungeon_nr] + if target == dungeon_nr: + continue + + if target == 3: # D4 fish boss + # If dungeon_nr == 6: use normal eagle door towards fish. + if dungeon_nr == 6: + # Add the staircase to the boss, and fix the warp back. + re = RoomEditor(rom, 0x22E) + for obj in re.objects: + if isinstance(obj, ObjectWarp): + obj.type_id = 2 + obj.map_nr = 3 + obj.room = 0x1EF + obj.target_x = 24 + obj.target_y = 16 + re.store(rom) + re = RoomEditor(rom, 0x1EF) + re.objects[-1] = ObjectWarp(1, dungeon_nr if dungeon_nr < 8 else 0xff, 0x22E, 24, 16) + re.store(rom) + else: + # Set the proper room event flags + rom.banks[0x14][BOSS_ROOMS[dungeon_nr] - 0x100] = 0x2A + + # Add the staircase to the boss, and fix the warp back. + re = getCleanBossRoom(rom, dungeon_nr) + re.objects += [Object(4, 4, 0xBE), ObjectWarp(2, 3, 0x1EF, 24, 16)] + re.store(rom) + re = RoomEditor(rom, 0x1EF) + re.objects[-1] = ObjectWarp(1, dungeon_nr if dungeon_nr < 8 else 0xff, BOSS_ROOMS[dungeon_nr], 72, 80) + re.store(rom) + + # Patch the fish heart container to open up the right room. + if dungeon_nr == 6: + rom.patch(0x03, 0x1A0F, ASM("ld hl, $D966"), ASM("ld hl, $%04x" % (0xD800 + 0x22E))) + else: + rom.patch(0x03, 0x1A0F, ASM("ld hl, $D966"), ASM("ld hl, $%04x" % (getBossRoomStatusFlagLocation(dungeon_nr)))) + + # Patch the proper item towards the D4 boss + rom.banks[0x3E][0x3800 + 0x01ff] = fixDungeonItem(rom.banks[0x3E][0x3800 + BOSS_ROOMS[dungeon_nr]], dungeon_nr) + rom.banks[0x3E][0x3300 + 0x01ff] = fixDungeonItem(rom.banks[0x3E][0x3300 + BOSS_ROOMS[dungeon_nr]], dungeon_nr) + elif target == 6: # Evil eagle + rom.banks[0x14][BOSS_ROOMS[dungeon_nr] - 0x100] = 0x2A + + # Patch the eagle heart container to open up the right room. + rom.patch(0x03, 0x1A04, ASM("ld hl, $DA2E"), ASM("ld hl, $%04x" % (getBossRoomStatusFlagLocation(dungeon_nr)))) + + # Add the staircase to the boss, and fix the warp back. + re = getCleanBossRoom(rom, dungeon_nr) + re.objects += [Object(4, 4, 0xBE), ObjectWarp(2, 6, 0x2F8, 72, 80)] + re.store(rom) + re = RoomEditor(rom, 0x2F8) + re.objects[-1] = ObjectWarp(1, dungeon_nr if dungeon_nr < 8 else 0xff, BOSS_ROOMS[dungeon_nr], 72, 80) + re.store(rom) + + # Patch the proper item towards the D7 boss + rom.banks[0x3E][0x3800 + 0x02E8] = fixDungeonItem(rom.banks[0x3E][0x3800 + BOSS_ROOMS[dungeon_nr]], dungeon_nr) + rom.banks[0x3E][0x3300 + 0x02E8] = fixDungeonItem(rom.banks[0x3E][0x3300 + BOSS_ROOMS[dungeon_nr]], dungeon_nr) + else: + rom.banks[0x14][BOSS_ROOMS[dungeon_nr] - 0x100] = 0x21 + re = getCleanBossRoom(rom, dungeon_nr) + re.entities = [BOSS_ENTITIES[target]] + + if target == 4: + # For slime eel, we need to setup the right wall tiles. + rom.banks[0x20][0x2EB3 + BOSS_ROOMS[dungeon_nr] - 0x100] = 0x06 + if target == 5: + # Patch facade so he doesn't use the spinning tiles, which is a problem for the sprites. + rom.patch(0x04, 0x121D, ASM("cp $14"), ASM("cp $00")) + rom.patch(0x04, 0x1226, ASM("cp $04"), ASM("cp $00")) + rom.patch(0x04, 0x127F, ASM("cp $14"), ASM("cp $00")) + if target == 7: + pass + # For hot head, add some lava (causes graphical glitches) + # re.animation_id = 0x06 + # re.objects += [ + # ObjectHorizontal(3, 2, 0x06, 4), + # ObjectHorizontal(2, 3, 0x06, 6), + # ObjectHorizontal(2, 4, 0x06, 6), + # ObjectHorizontal(3, 5, 0x06, 4), + # ] + + re.store(rom) + + +def readBossMapping(rom): + mapping = [] + for dungeon_nr in range(9): + r = RoomEditor(rom, BOSS_ROOMS[dungeon_nr]) + if r.entities: + mapping.append(BOSS_ENTITIES.index(r.entities[0])) + elif isinstance(r.objects[-1], ObjectWarp) and r.objects[-1].room == 0x1ef: + mapping.append(3) + elif isinstance(r.objects[-1], ObjectWarp) and r.objects[-1].room == 0x2f8: + mapping.append(6) + else: + mapping.append(dungeon_nr) + return mapping + + +def changeMiniBosses(rom, mapping): + # Fix avalaunch not working when entering a room from the left or right + rom.patch(0x03, 0x0BE0, ASM(""" + ld [hl], $50 + ld hl, $C2D0 + add hl, bc + ld [hl], $00 + jp $4B56 + """), ASM(""" + ld a, [hl] + sub $08 + ld [hl], a + ld hl, $C2D0 + add hl, bc + ld [hl], b ; b is always zero here + ret + """), fill_nop=True) + # Fix avalaunch waiting until the room event is done (and not all rooms have a room event on enter) + rom.patch(0x36, 0x1C14, ASM("ret z"), "", fill_nop=True) + # Fix giant buzz blob waiting until the room event is done (and not all rooms have a room event on enter) + rom.patch(0x36, 0x153B, ASM("ret z"), "", fill_nop=True) + + # Remove the powder fairy from giant buzz blob + rom.patch(0x36, 0x14F7, ASM("jr nz, $05"), ASM("jr $05")) + + # Do not allow the force barrier in D3 dodongo room + rom.patch(0x14, 0x14AC, 0x14B5, ASM("jp $7FE0"), fill_nop=True) + rom.patch(0x14, 0x3FE0, "00" * 0x20, ASM(""" + ld a, [$C124] ; room transition + ld hl, $C17B + or [hl] + ret nz + ldh a, [$F6] ; room + cp $45 ; check for D3 dodogo room + ret z + cp $7F ; check for armos temple room + ret z + jp $54B5 + """), fill_nop=True) + + # Patch smasher to spawn the ball closer, so it doesn't spawn on the wall in the armos temple + rom.patch(0x06, 0x0533, ASM("add a, $30"), ASM("add a, $20")) + + for target, name in mapping.items(): + re = RoomEditor(rom, MINIBOSS_ROOMS[target]) + re.entities = [e for e in re.entities if e[2] == 0x61] # Only keep warp, if available + re.entities += MINIBOSS_ENTITIES[name] + + if re.room == 0x228 and name != "GRIM_CREEPER": + for x in range(3, 7): + for y in range(0, 3): + re.removeObject(x, y) + + if name == "CUE_BALL": + re.objects += [ + Object(3, 3, 0x2c), + ObjectHorizontal(4, 3, 0x22, 2), + Object(6, 3, 0x2b), + Object(3, 4, 0x2a), + ObjectHorizontal(4, 4, 0x21, 2), + Object(6, 4, 0x29), + ] + if name == "BLAINO": + # BLAINO needs a warp object to hit you to the entrance of the dungeon. + if len(re.getWarps()) < 1: + # Default to start house. + target = (0x10, 0x2A3, 0x50, 0x7c) + if 0x100 <= re.room < 0x11D: #D1 + target = (0, 0x117, 80, 80) + elif 0x11D <= re.room < 0x140: #D2 + target = (1, 0x136, 80, 80) + elif 0x140 <= re.room < 0x15D: #D3 + target = (2, 0x152, 80, 80) + elif 0x15D <= re.room < 0x180: #D4 + target = (3, 0x174, 80, 80) + elif 0x180 <= re.room < 0x1AC: #D5 + target = (4, 0x1A1, 80, 80) + elif 0x1B0 <= re.room < 0x1DE: #D6 + target = (5, 0x1D4, 80, 80) + elif 0x200 <= re.room < 0x22D: #D7 + target = (6, 0x20E, 80, 80) + elif 0x22D <= re.room < 0x26C: #D8 + target = (7, 0x25D, 80, 80) + elif re.room >= 0x300: #D0 + target = (0xFF, 0x312, 80, 80) + elif re.room == 0x2E1: #Moblin cave + target = (0x15, 0x2F0, 0x50, 0x7C) + elif re.room == 0x27F: #Armos temple + target = (0x16, 0x28F, 0x50, 0x7C) + re.objects.append(ObjectWarp(1, *target)) + if name == "DODONGO": + # Remove breaking floor tiles from the room. + re.objects = [obj for obj in re.objects if obj.type_id != 0xDF] + if name == "ROLLING_BONES" and target == 2: + # Make rolling bones pass trough walls so it does not get stuck here. + rom.patch(0x03, 0x02F1 + 0x81, "84", "95") + re.store(rom) + + +def readMiniBossMapping(rom): + mapping = {} + for key, room in MINIBOSS_ROOMS.items(): + r = RoomEditor(rom, room) + for me_key, me_data in MINIBOSS_ENTITIES.items(): + if me_data[-1][2] == r.entities[-1][2]: + mapping[key] = me_key + return mapping + + +def doubleTrouble(rom): + for n in range(0x316): + if n == 0x2FF: + continue + re = RoomEditor(rom, n) + # Bosses + if re.hasEntity(0x59): # Moldorm (TODO; double heart container drop) + re.removeEntities(0x59) + re.entities += [(3, 2, 0x59), (4, 2, 0x59)] + re.store(rom) + if re.hasEntity(0x5C): # Ghini + re.removeEntities(0x5C) + re.entities += [(3, 2, 0x5C), (4, 2, 0x5C)] + re.store(rom) + if re.hasEntity(0x5B): # slime eye + re.removeEntities(0x5B) + re.entities += [(3, 2, 0x5B), (6, 2, 0x5B)] + re.store(rom) + if re.hasEntity(0x65): # angler fish + re.removeEntities(0x65) + re.entities += [(6, 2, 0x65), (6, 5, 0x65)] + re.store(rom) + # Slime eel bugs out on death if duplicated. + # if re.hasEntity(0x5D): # slime eel + # re.removeEntities(0x5D) + # re.entities += [(6, 2, 0x5D), (6, 5, 0x5D)] + # re.store(rom) + if re.hasEntity(0x5A): # facade (TODO: Drops two hearts, shared health?) + re.removeEntities(0x5A) + re.entities += [(2, 3, 0x5A), (6, 3, 0x5A)] + re.store(rom) + # Evil eagle causes a crash, and messes up the intro sequence and generally is just a mess if I spawn multiple + # if re.hasEntity(0x63): # evil eagle + # re.removeEntities(0x63) + # re.entities += [(3, 4, 0x63), (2, 4, 0x63)] + # re.store(rom) + # # Remove that links movement is blocked + # rom.patch(0x05, 0x2258, ASM("ldh [$A1], a"), "0000") + # rom.patch(0x05, 0x1AE3, ASM("ldh [$A1], a"), "0000") + # rom.patch(0x05, 0x1C5D, ASM("ldh [$A1], a"), "0000") + # rom.patch(0x05, 0x1C8D, ASM("ldh [$A1], a"), "0000") + # rom.patch(0x05, 0x1CAF, ASM("ldh [$A1], a"), "0000") + if re.hasEntity(0x62): # hot head (TODO: Drops thwo hearts) + re.removeEntities(0x62) + re.entities += [(2, 2, 0x62), (4, 4, 0x62)] + re.store(rom) + if re.hasEntity(0xF9): # hardhit beetle + re.removeEntities(0xF9) + re.entities += [(2, 2, 0xF9), (5, 4, 0xF9)] + re.store(rom) + # Minibosses + if re.hasEntity(0x89): + re.removeEntities(0x89) + re.entities += [(2, 3, 0x89), (6, 3, 0x89)] + re.store(rom) + if re.hasEntity(0x81): + re.removeEntities(0x81) + re.entities += [(2, 3, 0x81), (6, 3, 0x81)] + re.store(rom) + if re.hasEntity(0x60): + dodongo = [e for e in re.entities if e[2] == 0x60] + x = (dodongo[0][0] + dodongo[1][0]) // 2 + y = (dodongo[0][1] + dodongo[1][1]) // 2 + re.entities += [(x, y, 0x60)] + re.store(rom) + if re.hasEntity(0x8e): + re.removeEntities(0x8e) + re.entities += [(1, 1, 0x8e), (7, 1, 0x8e)] + re.store(rom) + if re.hasEntity(0x92): + re.removeEntities(0x92) + re.entities += [(2, 3, 0x92), (4, 3, 0x92)] + re.store(rom) + if re.hasEntity(0xf4): + re.removeEntities(0xf4) + re.entities += [(2, 1, 0xf4), (6, 1, 0xf4)] + re.store(rom) + if re.hasEntity(0xf8): + re.removeEntities(0xf8) + re.entities += [(2, 2, 0xf8), (6, 2, 0xf8)] + re.store(rom) + if re.hasEntity(0xe4): + re.removeEntities(0xe4) + re.entities += [(5, 2, 0xe4), (5, 5, 0xe4)] + re.store(rom) + + if re.hasEntity(0x88): # Armos knight (TODO: double item drop) + re.removeEntities(0x88) + re.entities += [(3, 3, 0x88), (6, 3, 0x88)] + re.store(rom) + if re.hasEntity(0x87): # Lanmola (TODO: killing one drops the item, and marks as done) + re.removeEntities(0x87) + re.entities += [(2, 2, 0x87), (1, 1, 0x87)] + re.store(rom) diff --git a/worlds/ladx/LADXR/patches/entrances.py b/worlds/ladx/LADXR/patches/entrances.py new file mode 100644 index 0000000000..82a09edf58 --- /dev/null +++ b/worlds/ladx/LADXR/patches/entrances.py @@ -0,0 +1,58 @@ +from ..roomEditor import RoomEditor, ObjectWarp +from ..worldSetup import ENTRANCE_INFO + + +def changeEntrances(rom, mapping): + warp_to_indoor = {} + warp_to_outdoor = {} + for key in mapping.keys(): + info = ENTRANCE_INFO[key] + re = RoomEditor(rom, info.alt_room if info.alt_room is not None else info.room) + warp = re.getWarps()[info.index if info.index not in (None, "all") else 0] + warp_to_indoor[key] = warp + assert info.target == warp.room, "%s != %03x" % (key, warp.room) + + re = RoomEditor(rom, warp.room) + for warp in re.getWarps(): + if warp.room == info.room: + warp_to_outdoor[key] = warp + assert key in warp_to_outdoor, "Missing warp to outdoor on %s" % (key) + + # First collect all the changes we need to do per room + changes_per_room = {} + def addChange(source_room, target_room, new_warp): + if source_room not in changes_per_room: + changes_per_room[source_room] = {} + changes_per_room[source_room][target_room] = new_warp + for key, target in mapping.items(): + if key == target: + continue + info = ENTRANCE_INFO[key] + # Change the entrance to point to the new indoor room + addChange(info.room, warp_to_indoor[key].room, warp_to_indoor[target]) + if info.alt_room: + addChange(info.alt_room, warp_to_indoor[key].room, warp_to_indoor[target]) + + # Change the exit to point to the right outside + addChange(warp_to_indoor[target].room, ENTRANCE_INFO[target].room, warp_to_outdoor[key]) + if ENTRANCE_INFO[target].instrument_room is not None: + addChange(ENTRANCE_INFO[target].instrument_room, ENTRANCE_INFO[target].room, warp_to_outdoor[key]) + + # Finally apply the changes, we need to do this once per room to prevent A->B->C issues. + for room, changes in changes_per_room.items(): + re = RoomEditor(rom, room) + for idx, obj in enumerate(re.objects): + if isinstance(obj, ObjectWarp) and obj.room in changes: + re.objects[idx] = changes[obj.room].copy() + re.store(rom) + + +def readEntrances(rom): + result = {} + for key, info in ENTRANCE_INFO.items(): + re = RoomEditor(rom, info.alt_room if info.alt_room is not None else info.room) + warp = re.getWarps()[info.index if info.index not in (None, "all") else 0] + for other_key, other_info in ENTRANCE_INFO.items(): + if warp.room == other_info.target: + result[key] = other_key + return result diff --git a/worlds/ladx/LADXR/patches/fishingMinigame.py b/worlds/ladx/LADXR/patches/fishingMinigame.py new file mode 100644 index 0000000000..a0c079a6e2 --- /dev/null +++ b/worlds/ladx/LADXR/patches/fishingMinigame.py @@ -0,0 +1,19 @@ +from ..assembler import ASM +from ..roomEditor import RoomEditor + + +def updateFinishingMinigame(rom): + rom.patch(0x04, 0x26BE, 0x26DF, ASM(""" + ld a, $0E ; GiveItemAndMessageForRoomMultiworld + rst 8 + + ; Mark selection as stopping minigame, as we are not asking a question. + ld a, $01 + ld [$C177], a + + ; Check if we got rupees from the item skip getting rupees from the fish. + ld a, [$DB90] + ld hl, $DB8F + or [hl] + jp nz, $66FE + """), fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/goal.py b/worlds/ladx/LADXR/patches/goal.py new file mode 100644 index 0000000000..cb932aa1d9 --- /dev/null +++ b/worlds/ladx/LADXR/patches/goal.py @@ -0,0 +1,317 @@ +from ..assembler import ASM +from ..roomEditor import RoomEditor, Object, ObjectVertical, ObjectHorizontal, ObjectWarp +from ..utils import formatText + + +def setRequiredInstrumentCount(rom, count): + rom.texts[0x1A3] = formatText("You need %d instruments" % (count)) + if count >= 8: + return + if count < 0: + rom.patch(0x00, 0x31f5, ASM("ld a, [$D806]\nand $10\njr z, $25"), ASM(""), fill_nop=True) + rom.patch(0x20, 0x2dea, ASM("ld a, [$D806]\nand $10\njr z, $29"), ASM(""), fill_nop=True) + count = 0 + + # TODO: Music bugs out at the end, unless you have all instruments. + rom.patch(0x19, 0x0B79, None, "0000") # always spawn all instruments, we need the last one as that handles opening the egg. + rom.patch(0x19, 0x0BF4, ASM("jp $3BC0"), ASM("jp $7FE0")) # instead of rendering the instrument, jump to the code below. + rom.patch(0x19, 0x0BFE, ASM(""" + ; Normal check fo all instruments + ld e, $08 + ld hl, $DB65 + loop: + ldi a, [hl] + and $02 + jr z, $12 + dec e + jr nz, loop + """), ASM(""" + jp $7F2B ; jump to the end of the bank, where there is some space for code. + """), fill_nop=True) + # Add some code at the end of the bank, as we do not have enough space to do this "in place" + rom.patch(0x19, 0x3F2B, "0000000000000000000000000000000000000000000000000000", ASM(""" + ld d, $00 + ld e, $08 + ld hl, $DB65 ; start of has instrument memory +loop: + ld a, [hl] + and $02 + jr z, noinc + inc d +noinc: + inc hl + dec e + jr nz, loop + ld a, d + cp $%02x ; check if we have a minimal of this amount of instruments. + jp c, $4C1A ; not enough instruments + jp $4C0B ; enough instruments + """ % (count)), fill_nop=True) + rom.patch(0x19, 0x3FE0, "0000000000000000000000000000000000000000000000000000", ASM(""" + ; Entry point of render code + ld hl, $DB65 ; table of having instruments + push bc + ldh a, [$F1] + ld c, a + add hl, bc + pop bc + ld a, [hl] + and $02 ; check if we have this instrument + ret z + jp $3BC0 ; jump to render code + """), fill_nop=True) + + +def setSeashellGoal(rom, count): + rom.texts[0x1A3] = formatText("You need %d {SEASHELL}s" % (count)) + + # Remove the seashell mansion handler (as it will take your seashells) but put a heartpiece instead + re = RoomEditor(rom, 0x2E9) + re.entities = [(4, 4, 0x35)] + re.store(rom) + + rom.patch(0x19, 0x0ACB, 0x0C21, ASM(""" + ldh a, [$F8] ; room status + and $10 + ret nz + ldh a, [$F0] ; active entity state + rst 0 + dw state0, state1, state2, state3, state4 + +state0: + ld a, [$C124] ; room transition state + and a + ret nz + ldh a, [$99] ; link position Y + cp $70 + ret nc + jp $3B12 ; increase entity state + +state1: + call $0C05 ; get entity transition countdown + jr nz, renderShells + ld [hl], $10 + call renderShells + + ld hl, $C2B0 ; private state 1 table + add hl, bc + ld a, [wSeashellsCount] + cp [hl] + jp z, $3B12 ; increase entity state + ld a, [hl] ; increase the amount of compared shells + inc a + daa + ld [hl], a + ld hl, $C2C0 ; private state 2 table + add hl, bc + inc [hl] ; increase amount of displayed shells + ld a, $2B + ldh [$F4], a ; SFX + ret + +state2: + ld a, [wSeashellsCount] + cp $%02d + jr c, renderShells + ; got enough shells + call $3B12 ; increase entity state + call $0C05 ; get entity transition countdown + ld [hl], $40 + jp renderShells + +state3: + ld a, $23 + ldh [$F2], a ; SFX: Dungeon opened + ld hl, $D806 ; egg room status + set 4, [hl] + ld a, [hl] + ldh [$F8], a ; current room status + call $3B12 ; increase entity state + + ld a, $00 + jp $4C2E + +state4: + ret + +renderShells: + ld hl, $C2C0 ; private state 2 table + add hl, bc + ld a, [hl] + cp $14 + jr c, .noMax + ld a, $14 +.noMax: + and a + ret z + ld c, a + ld hl, spriteRect + call $3CE6 ; RenderActiveEntitySpritesRect + ret + +spriteRect: + db $10, $1E, $1E, $0C + db $10, $2A, $1E, $0C + db $10, $36, $1E, $0C + db $10, $42, $1E, $0C + db $10, $4E, $1E, $0C + + db $10, $5A, $1E, $0C + db $10, $66, $1E, $0C + db $10, $72, $1E, $0C + db $10, $7E, $1E, $0C + db $10, $8A, $1E, $0C + + db $24, $1E, $1E, $0C + db $24, $2A, $1E, $0C + db $24, $36, $1E, $0C + db $24, $42, $1E, $0C + db $24, $4E, $1E, $0C + + db $24, $5A, $1E, $0C + db $24, $66, $1E, $0C + db $24, $72, $1E, $0C + db $24, $7E, $1E, $0C + db $24, $8A, $1E, $0C + """ % (count), 0x4ACB), fill_nop=True) + + +def setRaftGoal(rom): + rom.texts[0x1A3] = formatText("Just sail away.") + + # Remove the egg and egg event handler. + re = RoomEditor(rom, 0x006) + for x in range(4, 7): + for y in range(0, 4): + re.removeObject(x, y) + re.objects.append(ObjectHorizontal(4, 1, 0x4d, 3)) + re.objects.append(ObjectHorizontal(4, 2, 0x03, 3)) + re.objects.append(ObjectHorizontal(4, 3, 0x03, 3)) + re.entities = [] + re.updateOverlay() + re.store(rom) + + re = RoomEditor(rom, 0x08D) + re.objects[6].count = 4 + re.objects[7].x += 2 + re.objects[7].type_id = 0x2B + re.objects[8].x += 2 + re.objects[8].count = 2 + re.objects[9].x += 1 + re.objects[11] = ObjectVertical(7, 5, 0x37, 2) + re.objects[12].x -= 1 + re.objects[13].x -= 1 + re.objects[14].x -= 1 + re.objects[14].type_id = 0x34 + re.objects[17].x += 3 + re.objects[17].count -= 3 + re.updateOverlay() + re.overlay[7 + 60] = 0x33 + re.store(rom) + + re = RoomEditor(rom, 0x0E9) + re.objects[30].count = 1 + re.objects[30].x += 2 + re.overlay[7 + 70] = 0x0E + re.overlay[8 + 70] = 0x0E + re.store(rom) + re = RoomEditor(rom, 0x0F9) + re.objects = [ + ObjectHorizontal(4, 0, 0x0E, 6), + ObjectVertical(9, 0, 0xCA, 8), + ObjectVertical(8, 0, 0x0E, 8), + + Object(3, 0, 0x38), + Object(3, 1, 0x32), + ObjectHorizontal(4, 1, 0x2C, 3), + Object(7, 1, 0x2D), + ObjectVertical(7, 2, 0x38, 5), + Object(7, 7, 0x34), + ObjectHorizontal(0, 7, 0x2F, 7), + + ObjectVertical(2, 3, 0xE8, 4), + ObjectVertical(3, 2, 0xE8, 5), + ObjectVertical(4, 2, 0xE8, 2), + + ObjectVertical(4, 4, 0x5C, 3), + ObjectVertical(5, 2, 0x5C, 5), + ObjectVertical(6, 2, 0x5C, 5), + + Object(6, 4, 0xC6), + ObjectWarp(1, 0x1F, 0xF6, 136, 112) + ] + re.updateOverlay(True) + re.entities.append((0, 0, 0x41)) + re.store(rom) + re = RoomEditor(rom, 0x1F6) + re.objects[-1].target_x -= 16 + re.store(rom) + + # Fix the raft graphics (this overrides some unused graphic tiles) + rom.banks[0x31][0x21C0:0x2200] = rom.banks[0x2E][0x07C0:0x0800] + + # Patch the owl entity to run our custom end handling. + rom.patch(0x06, 0x27F5, 0x2A77, ASM(""" + ld a, [$DB95] + cp $0B + ret nz + ; If map is not fully loaded, return + ld a, [$C124] + and a + ret nz + ; Check if we are moving off the bottom of the map + ldh a, [$99] + cp $7D + ret c + ; Move link back so it does not move off the map + ld a, $7D + ldh [$99], a + + xor a + ld e, a + ld d, a + +raftSearchLoop: + ld hl, $C280 + add hl, de + ld a, [hl] + and a + jr z, .skipEntity + + ld hl, $C3A0 + add hl, de + ld a, [hl] + cp $6A + jr nz, .skipEntity + + ; Raft found, check if near the bottom of the screen. + ld hl, $C210 + add hl, de + ld a, [hl] + cp $70 + jr nc, raftOffWorld + +.skipEntity: + inc e + ld a, e + cp $10 + jr nz, raftSearchLoop + ret + +raftOffWorld: + ; Switch to the end credits + ld a, $01 + ld [$DB95], a + ld a, $00 + ld [$DB96], a + ret + """), fill_nop=True) + + # We need to run quickly trough part of the credits, or else it bugs out + # Skip the whole windfish part. + rom.patch(0x17, 0x0D39, None, ASM("ld a, $18\nld [$D00E], a\nret")) + # And skip the zoomed out laying on the log + rom.patch(0x17, 0x20ED, None, ASM("ld a, $00")) + # Finally skip some waking up on the log. + rom.patch(0x17, 0x23BC, None, ASM("jp $4CD9")) + rom.patch(0x17, 0x2476, None, ASM("jp $4CD9")) diff --git a/worlds/ladx/LADXR/patches/goldenLeaf.py b/worlds/ladx/LADXR/patches/goldenLeaf.py new file mode 100644 index 0000000000..87cefae0f6 --- /dev/null +++ b/worlds/ladx/LADXR/patches/goldenLeaf.py @@ -0,0 +1,34 @@ +from ..assembler import ASM + + +def fixGoldenLeaf(rom): + # Patch the golden leaf code so it jumps to the dropped key handling in bank 3E + rom.patch(3, 0x2007, ASM(""" + ld de, $5FFB + call $3C77 ; RenderActiveEntitySprite + """), ASM(""" + ld a, $04 + rst 8 + """), fill_nop=True) + rom.patch(3, 0x2018, None, ASM(""" + ld a, $06 ; giveItemMultiworld + rst 8 + jp $602F + """)) + rom.patch(3, 0x2037, None, ASM(""" + ld a, $0a ; showMessageMultiworld + rst 8 + jp $604B + """)) + + # Patch all over the place to move the golden leafs to a different memory location. + # We use $DB6D (dungeon 9 status), but we could also use $DB7A (which is only used by the ghost) + rom.patch(0x00, 0x2D17, ASM("ld a, [$DB15]"), ASM("ld a, $06"), fill_nop=True) # Always load the slime tiles + rom.patch(0x02, 0x3005, ASM("cp $06"), ASM("cp $01"), fill_nop=True) # Instead of checking for 6 leaves a the keyhole, just check for the key + rom.patch(0x20, 0x1AD1, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # For the status screen, load the number of leafs from the proper memory + rom.patch(0x03, 0x0980, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # If leaves >= 6 move richard + rom.patch(0x06, 0x0059, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # If leaves >= 6 move richard + rom.patch(0x06, 0x007D, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # Richard message if no leaves + rom.patch(0x06, 0x00B8, ASM("ld [$DB15], a"), ASM("ld [wGoldenLeaves], a")) # Stores FF in the leaf counter if we opened the path + # 6:40EE uses leaves == 6 to check if we have collected the key, but only to change the message. + # rom.patch(0x06, 0x2AEF, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # Telephone message handler diff --git a/worlds/ladx/LADXR/patches/hardMode.py b/worlds/ladx/LADXR/patches/hardMode.py new file mode 100644 index 0000000000..3ecceda919 --- /dev/null +++ b/worlds/ladx/LADXR/patches/hardMode.py @@ -0,0 +1,64 @@ +from ..assembler import ASM + + +def oracleMode(rom): + # Reduce iframes + rom.patch(0x03, 0x2DB2, ASM("ld a, $50"), ASM("ld a, $20")) + + # Make bomb explosions damage you. + rom.patch(0x03, 0x2618, ASM(""" + ld hl, $C440 + add hl, bc + ld a, [hl] + and a + jr nz, $05 + """), ASM(""" + call $6625 + """), fill_nop=True) + # Reduce bomb blast push back on link + rom.patch(0x03, 0x2643, ASM("sla [hl]"), ASM("sra [hl]"), fill_nop=True) + rom.patch(0x03, 0x2648, ASM("sla [hl]"), ASM("sra [hl]"), fill_nop=True) + + # Never spawn a piece of power or acorn + rom.patch(0x03, 0x1608, ASM("jr nz, $05"), ASM("jr $05")) + rom.patch(0x03, 0x1642, ASM("jr nz, $04"), ASM("jr $04")) + + # Let hearts only recover half a container instead of a full one. + rom.patch(0x03, 0x24B7, ASM("ld a, $08"), ASM("ld a, $04")) + # Don't randomly drop fairies from enemies, drop a rupee instead + rom.patch(0x03, 0x15C7, "2E2D382F2E2D3837", "2E2D382E2E2D3837") + + # Make dropping in water without flippers damage you. + rom.patch(0x02, 0x3722, ASM("ldh a, [$AF]"), ASM("ld a, $06")) + + +def heroMode(rom): + # Don't randomly drop fairies and hearts from enemies, drop a rupee instead + rom.patch(0x03, 0x159D, + "2E2E2D2D372DFFFF2F37382E2F2F", + "2E2EFFFF37FFFFFFFF37382EFFFF") + rom.patch(0x03, 0x15C7, + "2E2D382F2E2D3837", + "2E2E382E2E2E3837") + rom.patch(0x00, 0x168F, ASM("ld a, $2D"), "", fill_nop=True) + rom.patch(0x02, 0x0CDB, ASM("ld a, $2D"), "", fill_nop=True) + # Double damage + rom.patch(0x03, 0x2DAB, + ASM("ld a, [$DB94]\nadd a, e\nld [$DB94], a"), + ASM("ld hl, $DB94\nld a, [hl]\nadd a, e\nadd a, e\nld [hl], a")) + rom.patch(0x02, 0x11B2, ASM("add a, $04"), ASM("add a, $08")) + rom.patch(0x02, 0x127E, ASM("add a, $04"), ASM("add a, $08")) + rom.patch(0x02, 0x291C, ASM("add a, $04"), ASM("add a, $08")) + rom.patch(0x02, 0x362B, ASM("add a, $04"), ASM("add a, $08")) + rom.patch(0x06, 0x041C, ASM("ld a, $02"), ASM("ld a, $04")) + rom.patch(0x15, 0x09B8, ASM("add a, $08"), ASM("add a, $10")) + rom.patch(0x15, 0x32FD, ASM("ld a, $08"), ASM("ld a, $10")) + rom.patch(0x18, 0x370E, ASM("ld a, $08"), ASM("ld a, $10")) + rom.patch(0x07, 0x3103, ASM("ld a, $08"), ASM("ld a, $10")) + rom.patch(0x06, 0x1166, ASM("ld a, $08"), ASM("ld a, $10")) + + + + +def oneHitKO(rom): + rom.patch(0x02, 0x238C, ASM("ld [$DB94], a"), "", fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/health.py b/worlds/ladx/LADXR/patches/health.py new file mode 100644 index 0000000000..7488e6280a --- /dev/null +++ b/worlds/ladx/LADXR/patches/health.py @@ -0,0 +1,33 @@ +from ..assembler import ASM +from ..utils import formatText + + +def setStartHealth(rom, amount): + rom.patch(0x01, 0x0B1C, ASM("ld [hl], $03"), ASM("ld [hl], $%02X" % (amount))) # max health of new save + rom.patch(0x01, 0x0B14, ASM("ld [hl], $18"), ASM("ld [hl], $%02X" % (amount * 8))) # current health of new save + + +def upgradeHealthContainers(rom): + # Reuse 2 unused shop messages for the heart containers. + rom.texts[0x2A] = formatText("You found a {HEART_CONTAINER}!") + rom.texts[0x2B] = formatText("You lost a heart!") + + rom.patch(0x03, 0x19DC, ASM(""" + ld de, $59D8 + call $3BC0 + """), ASM(""" + ld a, $05 ; renderHeartPiece + rst 8 + """), fill_nop=True) + rom.patch(0x03, 0x19F0, ASM(""" + ld hl, $DB5B + inc [hl] + ld hl, $DB93 + ld [hl], $FF + """), ASM(""" + ld a, $06 ; giveItemMultiworld + rst 8 + ld a, $0A ; messageForItemMultiworld + rst 8 +skip: + """), fill_nop=True) # add heart->remove heart on heart container diff --git a/worlds/ladx/LADXR/patches/heartPiece.py b/worlds/ladx/LADXR/patches/heartPiece.py new file mode 100644 index 0000000000..4147c8fe95 --- /dev/null +++ b/worlds/ladx/LADXR/patches/heartPiece.py @@ -0,0 +1,42 @@ +from ..assembler import ASM + + +def fixHeartPiece(rom): + # Patch all locations where the piece of heart is rendered. + rom.patch(0x03, 0x1b52, ASM("ld de, $5A4D\ncall $3BC0"), ASM("ld a, $04\nrst 8"), fill_nop=True) # state 0 + + # Write custom code in the first state handler, this overwrites all state handlers + # Till state 5. + rom.patch(0x03, 0x1A74, 0x1A98, ASM(""" + ; Render sprite + ld a, $05 + rst 8 + + ; Handle item effect + ld a, $06 ; giveItemMultiworld + rst 8 + + ;Show message + ld a, $0A ; showMessageMultiworld + rst 8 + + ; Switch to state 5 + ld hl, $C290; stateTable + add hl, bc + ld [hl], $05 + ret + """), fill_nop=True) + # Insert a state 5 handler + rom.patch(0x03, 0x1A98, 0x1B17, ASM(""" + ; Render sprite + ld a, $05 + rst 8 + + ld a, [$C19F] ; dialog state + and a + ret nz + + call $512A ; mark room as done + call $3F8D ; unload entity + ret + """), fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/instrument.py b/worlds/ladx/LADXR/patches/instrument.py new file mode 100644 index 0000000000..9e1cfecc40 --- /dev/null +++ b/worlds/ladx/LADXR/patches/instrument.py @@ -0,0 +1,24 @@ +from ..assembler import ASM + + +def fixInstruments(rom): + rom.patch(0x03, 0x1EA9, 0x1EAE, "", fill_nop=True) + rom.patch(0x03, 0x1EB9, 0x1EC8, ASM(""" + ; Render sprite + ld a, $05 + rst 8 + """), fill_nop=True) + + # Patch the message and instrument giving code + rom.patch(0x03, 0x1EE3, 0x1EF6, ASM(""" + ; Handle item effect + ld a, $06 ; giveItemMultiworld + rst 8 + + ;Show message + ld a, $0A ; showMessageMultiworld + rst 8 + """), fill_nop=True) + + # Color cycle palette 7 instead of 1 + rom.patch(0x36, 0x30F0, ASM("ld de, $DC5C"), ASM("ld de, $DC84")) diff --git a/worlds/ladx/LADXR/patches/inventory.py b/worlds/ladx/LADXR/patches/inventory.py new file mode 100644 index 0000000000..c3ca96e01b --- /dev/null +++ b/worlds/ladx/LADXR/patches/inventory.py @@ -0,0 +1,421 @@ +from ..assembler import ASM +from ..backgroundEditor import BackgroundEditor + + +def selectToSwitchSongs(rom): + # Do not ignore left/right keys when ocarina is selected + rom.patch(0x20, 0x1F18, ASM("and a"), ASM("xor a")) + # Change the keys which switch the ocarina song to select and no key. + rom.patch(0x20, 0x21A9, ASM("and $01"), ASM("and $40")) + rom.patch(0x20, 0x21C7, ASM("and $02"), ASM("and $00")) + +def songSelectAfterOcarinaSelect(rom): + rom.patch(0x20, 0x2002, ASM("ld [$DB00], a"), ASM("call $5F96")) + rom.patch(0x20, 0x1FE0, ASM("ld [$DB01], a"), ASM("call $5F9B")) + # Remove the code that opens the ocerina on cursor movement, but use it to insert code + # for opening the menu on item select + rom.patch(0x20, 0x1F93, 0x1FB2, ASM(""" + jp $5FB2 + itemToB: + ld [$DB00], a + jr checkForOcarina + itemToA: + ld [$DB01], a + checkForOcarina: + cp $09 + jp nz, $6010 + ld a, [$DB49] + and a + ret z + ld a, $08 + ldh [$90], a ; load ocarina song select graphics + ;ld a, $10 + ;ld [$C1B8], a ; shows the opening animation + ld a, $01 + ld [$C1B5], a + ret + """), fill_nop=True) + # More code that opens the menu, use this to close the menu + rom.patch(0x20, 0x200D, 0x2027, ASM(""" + jp $6027 + closeOcarinaMenu: + ld a, [$C1B5] + and a + ret z + xor a + ld [$C1B5], a + ld a, $10 + ld [$C1B9], a ; shows the closing animation + ret + """), fill_nop=True) + rom.patch(0x20, 0x2027, 0x2036, "", fill_nop=True) # Code that closes the ocarina menu on item select + + rom.patch(0x20, 0x22A2, ASM(""" + ld a, [$C159] + inc a + ld [$C159], a + and $10 + jr nz, $30 + """), ASM(""" + ld a, [$C1B5] + and a + ret nz + ldh a, [$E7] ; frame counter + and $10 + ret nz + """), fill_nop=True) + +def moreSlots(rom): + #Move flippers, medicine, trade item and seashells to DB3E+ + rom.patch(0x02, 0x292B, ASM("ld a, [$DB0C]"), ASM("ld a, [$DB3E]")) + #rom.patch(0x02, 0x2E8F, ASM("ld a, [$DB0C]"), ASM("ld a, [$DB3E]")) + rom.patch(0x02, 0x3713, ASM("ld a, [$DB0C]"), ASM("ld a, [$DB3E]")) + rom.patch(0x20, 0x1A23, ASM("ld de, $DB0C"), ASM("ld de, $DB3E")) + rom.patch(0x02, 0x23a3, ASM("ld a, [$DB0D]"), ASM("ld a, [$DB3F]")) + rom.patch(0x02, 0x23d7, ASM("ld a, [$DB0D]"), ASM("ld a, [$DB3F]")) + rom.patch(0x02, 0x23aa, ASM("ld [$DB0D], a"), ASM("ld [$DB3F], a")) + rom.patch(0x04, 0x3b1f, ASM("ld [$DB0D], a"), ASM("ld [$DB3F], a")) + rom.patch(0x06, 0x1f58, ASM("ld a, [$DB0D]"), ASM("ld a, [$DB3F]")) + rom.patch(0x06, 0x1ff5, ASM("ld hl, $DB0D"), ASM("ld hl, $DB3F")) + rom.patch(0x07, 0x3c33, ASM("ld [$DB0D], a"), ASM("ld [$DB3F], a")) + rom.patch(0x00, 0x1e01, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x00, 0x2d21, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x00, 0x3199, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x03, 0x0ae6, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x03, 0x0b6d, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x03, 0x0f68, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x04, 0x2faa, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x04, 0x3502, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x04, 0x3624, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x05, 0x0bff, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x05, 0x0d20, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x05, 0x0db1, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x05, 0x0dd5, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x05, 0x0e8e, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x05, 0x11ce, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x06, 0x1a2c, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x06, 0x1a7c, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x06, 0x1ab1, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x06, 0x2214, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x06, 0x223e, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x02f8, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x04bf, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x057f, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x07, 0x0797, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x0856, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x07, 0x0a21, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x0a33, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x0a58, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x0a81, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x0acf, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x0af9, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x0b31, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x07, 0x0bcc, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x0c23, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x0c3c, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x0c60, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x07, 0x0d73, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x07, 0x1549, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x155d, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x159f, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x18e6, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x07, 0x19ce, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + #rom.patch(0x15, 0x3F23, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0966, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0972, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x18, 0x09f3, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0bf1, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0c2c, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0c6d, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x18, 0x0c8b, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0ce4, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x18, 0x0d3c, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0d4a, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0d95, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0da3, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0de4, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x18, 0x0e7a, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0e91, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x18, 0x0eb6, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x18, 0x219e, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x19, 0x05ec, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x19, 0x2d54, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x19, 0x2df2, ASM("ld [$DB0E], a"), ASM("ld [$DB40], a")) + rom.patch(0x19, 0x2ef1, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x19, 0x2f95, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x20, 0x1b04, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x20, 0x1e42, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x36, 0x0948, ASM("ld a, [$DB0E]"), ASM("ld a, [$DB40]")) + rom.patch(0x19, 0x31Ca, ASM("ld a, [$DB0F]"), ASM("ld a, [$DB41]")) + rom.patch(0x19, 0x3215, ASM("ld a, [$DB0F]"), ASM("ld a, [$DB41]")) + rom.patch(0x19, 0x32a2, ASM("ld a, [$DB0F]"), ASM("ld a, [$DB41]")) + rom.patch(0x19, 0x3700, ASM("ld [$DB0F], a"), ASM("ld [$DB41], a")) + rom.patch(0x19, 0x38b3, ASM("ld a, [$DB0F]"), ASM("ld a, [$DB41]")) + rom.patch(0x19, 0x38c3, ASM("ld [$DB0F], a"), ASM("ld [$DB41], a")) + rom.patch(0x20, 0x1a83, ASM("ld a, [$DB0F]"), ASM("ld a, [$DB41]")) + + # Fix the whole inventory rendering, this needs to extend a few tables with more entries so it moves tables + # to the end of the bank as well. + rom.patch(0x20, 0x3E53, "00" * 32, + "9C019C06" + "9C619C65" + "9CA19CA5" + "9CE19CE5" + "9D219D25" + "9D619D65" + "9DA19DA5" + "9DE19DE5") # New table with tile addresses for all slots + rom.patch(0x20, 0x1CC7, ASM("ld hl, $5C84"), ASM("ld hl, $7E53")) # use the new table + rom.patch(0x20, 0x1BCC, ASM("ld hl, $5C84"), ASM("ld hl, $7E53")) # use the new table + rom.patch(0x20, 0x1CF0, ASM("ld hl, $5C84"), ASM("ld hl, $7E53")) # use the new table + + # sprite positions for inventory cursor, new table, placed at the end of the bank + rom.patch(0x20, 0x3E90, "00" * 16, "28283838484858586868787888889898") + rom.patch(0x20, 0x22b3, ASM("ld hl, $6298"), ASM("ld hl, $7E90")) + rom.patch(0x20, 0x2298, "28284040", "08280828") # Extend the sprite X positions for the inventory table + + # Piece of power overlay positions + rom.patch(0x20, 0x233A, + "1038103010301030103010300E0E2626", + "10381030103010301030103010301030") + rom.patch(0x20, 0x3E73, "00" * 16, + "0E0E2626363646465656666676768686") + rom.patch(0x20, 0x2377, ASM("ld hl, $6346"), ASM("ld hl, $7E73")) + + # Allow selecting the 4 extra slots. + rom.patch(0x20, 0x1F33, ASM("ld a, $09"), ASM("ld a, $0D")) + rom.patch(0x20, 0x1F54, ASM("ld a, $09"), ASM("ld a, $0D")) + rom.patch(0x20, 0x1F2A, ASM("cp $0A"), ASM("cp $0E")) + rom.patch(0x20, 0x1F4B, ASM("cp $0A"), ASM("cp $0E")) + rom.patch(0x02, 0x217E, ASM("ld a, $0B"), ASM("ld a, $0F")) + + # Patch all the locations that iterate over inventory to check the extra slots + rom.patch(0x02, 0x33FC, ASM("cp $0C"), ASM("cp $10")) + rom.patch(0x03, 0x2475, ASM("ld e, $0C"), ASM("ld e, $10")) + rom.patch(0x03, 0x248a, ASM("cp $0C"), ASM("cp $10")) + rom.patch(0x04, 0x3849, ASM("ld c, $0B"), ASM("ld c, $0F")) + rom.patch(0x04, 0x3862, ASM("ld c, $0B"), ASM("ld c, $0F")) + rom.patch(0x04, 0x39C2, ASM("ld d, $0C"), ASM("ld d, $10")) + rom.patch(0x04, 0x39E0, ASM("ld d, $0C"), ASM("ld d, $10")) + rom.patch(0x04, 0x39FE, ASM("ld d, $0C"), ASM("ld d, $10")) + rom.patch(0x05, 0x0F95, ASM("ld e, $0B"), ASM("ld e, $0F")) + rom.patch(0x05, 0x0FD1, ASM("ld c, $0B"), ASM("ld c, $0F")) + rom.patch(0x05, 0x1324, ASM("ld e, $0C"), ASM("ld e, $10")) + rom.patch(0x05, 0x1339, ASM("cp $0C"), ASM("cp $10")) + rom.patch(0x18, 0x005A, ASM("ld e, $0B"), ASM("ld e, $0F")) + rom.patch(0x18, 0x0571, ASM("ld e, $0B"), ASM("ld e, $0F")) + rom.patch(0x19, 0x0703, ASM("cp $0C"), ASM("cp $10")) + rom.patch(0x20, 0x235C, ASM("ld d, $0C"), ASM("ld d, $10")) + rom.patch(0x36, 0x31B8, ASM("ld e, $0C"), ASM("ld e, $10")) + + ## Patch the toadstool as a different item + rom.patch(0x20, 0x1C84, "9C019C" "069C61", "4C7F7F" "4D7F7F") # Which tiles are used for the toadstool + rom.patch(0x20, 0x1C8A, "9C659C" "C19CC5", "90927F" "91937F") # Which tiles are used for the rooster + rom.patch(0x20, 0x1C6C, "927F7F" "937F7F", "127F7F" "137F7F") # Which tiles are used for the feather (to make space for rooster) + rom.patch(0x20, 0x1C66, "907F7F" "917F7F", "107F7F" "117F7F") # Which tiles are used for the ocarina (to make space for rooster) + + # Move the inventory tile numbers to a higher address, so there is space for the table above it. + rom.banks[0x20][0x1C34:0x1C94] = rom.banks[0x20][0x1C30:0x1C90] + rom.patch(0x20, 0x1CDB, ASM("ld hl, $5C30"), ASM("ld hl, $5C34")) + rom.patch(0x20, 0x1D0D, ASM("ld hl, $5C33"), ASM("ld hl, $5C37")) + rom.patch(0x20, 0x1C30, "7F7F", "0A0B") # Toadstool tile attributes + rom.patch(0x20, 0x1C32, "7F7F", "0101") # Rooster tile attributes + rom.patch(0x20, 0x1C28, "0303", "0B0B") # Feather tile attributes (due to rooster) + rom.patch(0x20, 0x1C26, "0202", "0A0A") # Ocarina tile attributes (due to rooster) + + # Allow usage of the toadstool (replace the whole manual jump table with an rst 0 jumptable + rom.patch(0x00, 0x129D, 0x12D8, ASM(""" + rst 0 ; jump table + dw $12ED ; no item + dw $1528 ; Sword + dw $135A ; Bomb + dw $1382 ; Bracelet + dw $12EE ; Shield + dw $13BD ; Bow + dw $1319 ; Hookshot + dw $12D8 ; Magic rod + dw $12ED ; Boots (no action) + dw $41FC ; Ocarina + dw $14CB ; Feather + dw $12F8 ; Shovel + dw $148D ; Magic powder + dw $1383 ; Boomerang + dw $1498 ; Toadstool + dw RoosterUse ; Rooster +RoosterUse: + ld a, $01 + ld [$DB7B], a ; has rooster + call $3958 ; spawn followers + xor a + ld [$DB7B], a ; has rooster + ret + """, 0x129D), fill_nop=True) + # Fix the graphics of the toadstool hold over your head + rom.patch(0x02, 0x121E, ASM("ld e, $8E"), ASM("ld e, $4C")) + rom.patch(0x02, 0x1241, ASM("ld a, $14"), ASM("ld a, $1C")) + + # Do not remove powder when it is used up. + rom.patch(0x20, 0x0C59, ASM("jr nz, $12"), ASM("jr $12")) + + # Patch the toadstool entity code to give the proper item, and not set the has-toadstool flag. + rom.patch(0x03, 0x1D6F, ASM(""" + ld a, $0A + ldh [$A5], a + ld d, $0C + call $6472 + ld a, $01 + ld [$DB4B], a + """), ASM(""" + ld d, $0E + call $6472 + """), fill_nop=True) + + # Patch the debug save game so it does not give a bunch of swords + rom.patch(0x01, 0x0673, "01010100", "0D0E0F00") + + # Patch the witch to use the new toadstool instead of the old flag + rom.patch(0x05, 0x081A, ASM("ld a, [$DB4B]"), ASM("ld a, $01"), fill_nop=True) + rom.patch(0x05, 0x082A, ASM("cp $0C"), ASM("cp $0E")) + rom.patch(0x05, 0x083E, ASM("cp $0C"), ASM("cp $0E")) + + +def advancedInventorySubscreen(rom): + # Instrument positions + rom.patch(0x01, 0x2BCF, + "0F51B1EFECAA4A0C", + "090C0F12494C4F52") + + be = BackgroundEditor(rom, 2) + be.tiles[0x9DA9] = 0x4A + be.tiles[0x9DC9] = 0x4B + for x in range(1, 10): + be.tiles[0x9DE9 + x] = 0xB0 + (x % 9) + be.tiles[0x9DE9] = 0xBA + be.store(rom) + be = BackgroundEditor(rom, 2, attributes=True) + + # Remove all attributes out of range. + for y in range(0x9C00, 0x9E40, 0x20): + for x in range(0x14, 0x20): + del be.tiles[x + y] + for n in range(0x9E40, 0xA020): + del be.tiles[n] + + # Remove palette of instruments + for y in range(0x9D00, 0x9E20, 0x20): + for x in range(0x00, 0x14): + be.tiles[x + y] = 0x01 + # And place it at the proper location + for y in range(0x9D00, 0x9D80, 0x20): + for x in range(0x09, 0x14): + be.tiles[x + y] = 0x07 + + # Key from 2nd vram bank + be.tiles[0x9DA9] = 0x09 + be.tiles[0x9DC9] = 0x09 + # Nightmare heads from 2nd vram bank with proper palette + for n in range(1, 10): + be.tiles[0x9DA9 + n] = 0x0E + + be.store(rom) + + rom.patch(0x20, 0x19D3, ASM("ld bc, $5994\nld e, $33"), ASM("ld bc, $7E08\nld e, $%02x" % (0x33 + 24))) + rom.banks[0x20][0x3E08:0x3E08+0x33] = rom.banks[0x20][0x1994:0x1994+0x33] + rom.patch(0x20, 0x3E08+0x32, "00" * 25, "9DAA08464646464646464646" "9DCA08B0B0B0B0B0B0B0B0B0" "00") + + # instead of doing an GBC specific check, jump to our custom handling + rom.patch(0x20, 0x19DE, ASM("ldh a, [$FE]\nand a\njr z, $40"), ASM("call $7F00"), fill_nop=True) + + rom.patch(0x20, 0x3F00, "00" * 0x100, ASM(""" + ld a, [$DBA5] ; isIndoor + and a + jr z, RenderKeysCounts + ldh a, [$F7] ; mapNr + cp $FF + jr z, RenderDungeonFix + cp $06 + jr z, D7RenderDungeonFix + cp $08 + jr c, RenderDungeonFix + +RenderKeysCounts: + ; Check if we have each nightmare key, and else null out the rendered tile + ld hl, $D636 + ld de, $DB19 + ld c, $08 +NKeyLoop: + ld a, [de] + and a + jr nz, .hasNKey + ld a, $7F + ld [hl], a +.hasNKey: + inc hl + inc de + inc de + inc de + inc de + inc de + dec c + jr nz, NKeyLoop + + ld a, [$DDDD] + and a + jr nz, .hasCNKey + ld a, $7F + ld [hl], a +.hasCNKey: + + ; Check the small key count for each dungeon and increase the tile to match the number + ld hl, $D642 + ld de, $DB1A + ld c, $08 +KeyLoop: + ld a, [de] + add a, $B0 + ld [hl], a + inc hl + inc de + inc de + inc de + inc de + inc de + dec c + jr nz, KeyLoop + + ld a, [$DDDE] + add a, $B0 + ld [hl], a + ret + +D7RenderDungeonFix: + ld de, D7DungeonFix + ld c, $11 + jr RenderDungeonFixGo + +RenderDungeonFix: + ld de, DungeonFix + ld c, $0D +RenderDungeonFixGo: + ld hl, $D633 +.copyLoop: + ld a, [de] + inc de + ldi [hl], a + dec c + jr nz, .copyLoop + ret + +DungeonFix: + db $9D, $09, $C7, $7F + db $9D, $0A, $C7, $7F + db $9D, $13, $C3, $7F + db $00 +D7DungeonFix: + db $9D, $09, $C7, $7F + db $9D, $0A, $C7, $7F + db $9D, $6B, $48, $7F + db $9D, $0F, $C7, $7F + db $00 + + """, 0x7F00), fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/madBatter.py b/worlds/ladx/LADXR/patches/madBatter.py new file mode 100644 index 0000000000..601c5ac51e --- /dev/null +++ b/worlds/ladx/LADXR/patches/madBatter.py @@ -0,0 +1,42 @@ +from ..assembler import ASM +from ..utils import formatText + + +def upgradeMadBatter(rom): + # Normally the madbatter won't do anything if you have full capacity. Remove that check. + rom.patch(0x18, 0x0F05, 0x0F1D, "", fill_nop=True) + # Remove the code that finds which upgrade to apply, + rom.patch(0x18, 0x0F9E, 0x0FC4, "", fill_nop=True) + rom.patch(0x18, 0x0FD2, 0x0FD8, "", fill_nop=True) + + # Finally, at the last step, give the item and the item message. + rom.patch(0x18, 0x1016, 0x101B, "", fill_nop=True) + rom.patch(0x18, 0x101E, 0x1051, ASM(""" + ; Mad batter rooms are E0,E1 and E2, load the item type from a table in the rom + ; which only has 3 entries, and store it where bank 3E wants it. + ldh a, [$F6] ; current room + and $0F + ld d, $00 + ld e, a + ld hl, $4F90 + add hl, de + ld a, [hl] + ldh [$F1], a + + ; Give item + ld a, $06 ; giveItemMultiworld + rst 8 + ; Message + ld a, $0A ; showMessageMultiworld + rst 8 + ; Force the dialog at the bottom + ld a, [$C19F] + or $80 + ld [$C19F], a + """), fill_nop=True) + # Setup the default items + rom.patch(0x18, 0x0F90, "406060", "848586") + + rom.texts[0xE2] = formatText("You can now carry more Magic Powder!") + rom.texts[0xE3] = formatText("You can now carry more Bombs!") + rom.texts[0xE4] = formatText("You can now carry more Arrows!") diff --git a/worlds/ladx/LADXR/patches/maptweaks.py b/worlds/ladx/LADXR/patches/maptweaks.py new file mode 100644 index 0000000000..c25dd83dca --- /dev/null +++ b/worlds/ladx/LADXR/patches/maptweaks.py @@ -0,0 +1,27 @@ +from ..roomEditor import RoomEditor, ObjectWarp, ObjectVertical + + +def tweakMap(rom): + # 5 holes at the castle, reduces to 3 + re = RoomEditor(rom, 0x078) + re.objects[-1].count = 3 + re.overlay[7 + 6 * 10] = re.overlay[9 + 6 * 10] + re.overlay[8 + 6 * 10] = re.overlay[9 + 6 * 10] + re.store(rom) + + +def addBetaRoom(rom): + re = RoomEditor(rom, 0x1FC) + re.objects[-1].target_y -= 0x10 + re.store(rom) + re = RoomEditor(rom, 0x038) + re.changeObject(5, 1, 0xE1) + re.removeObject(0, 0) + re.removeObject(0, 1) + re.removeObject(0, 2) + re.removeObject(6, 1) + re.objects.append(ObjectVertical(0, 0, 0x38, 3)) + re.objects.append(ObjectWarp(1, 0x1F, 0x1FC, 0x50, 0x7C)) + re.store(rom) + + rom.room_sprite_data_indoor[0x0FC] = rom.room_sprite_data_indoor[0x1A1] diff --git a/worlds/ladx/LADXR/patches/multiworld.py b/worlds/ladx/LADXR/patches/multiworld.py new file mode 100644 index 0000000000..e41dacf35b --- /dev/null +++ b/worlds/ladx/LADXR/patches/multiworld.py @@ -0,0 +1,308 @@ +from ..assembler import ASM +from ..roomEditor import RoomEditor, ObjectHorizontal, ObjectVertical, Object +from .. import entityData + + +def addMultiworldShop(rom, this_player, player_count): + # Make a copy of the shop into GrandpaUlrira house + re = RoomEditor(rom, 0x2A9) + re.objects = [ + ObjectHorizontal(1,1, 0x00, 8), + ObjectHorizontal(1,2, 0x00, 8), + ObjectHorizontal(1,3, 0xCD, 8), + Object(2, 0, 0xC7), + Object(7, 0, 0xC7), + Object(7, 7, 0xFD), + ] + re.getWarps() + re.entities = [(0, 6, 0xD4)] + for n in range(player_count): + if n != this_player: + re.entities.append((n + 1, 6, 0xD4)) + re.animation_id = 0x04 + re.floor_object = 0x0D + re.store(rom) + # Fix the tileset + rom.banks[0x20][0x2EB3 + 0x2A9 - 0x100] = rom.banks[0x20][0x2EB3 + 0x2A1 - 0x100] + + re = RoomEditor(rom, 0x0B1) + re.getWarps()[0].target_x = 128 + re.store(rom) + + # Load the shopkeeper sprites + entityData.SPRITE_DATA[0xD4] = entityData.SPRITE_DATA[0x4D] + rom.patch(0x03, 0x01CF, "00", "98") # Fix the hitbox of the ghost to be 16x16 + + # Patch Ghost to work as a multiworld shop + rom.patch(0x19, 0x1E18, 0x20B0, ASM(""" + ld a, $01 + ld [$C50A], a ; this stops link from using items + + ldh a, [$EE] ; X + cp $08 + ; Jump to other code which is placed on the old owl code. As we do not have enough space here. + jp z, shopItemsHandler + +;Draw shopkeeper + ld de, OwnerSpriteData + call $3BC0 ; render sprite pair + ldh a, [$E7] ; frame counter + swap a + and $01 + call $3B0C ; set sprite variant + + ldh a, [$F0] + and a + jr nz, checkTalkingResult + + call $7CA2 ; prevent link from moving into the sprite + call $7CF0 ; check if talking to NPC + call c, talkHandler ; talk handling + ret + +checkTalkingResult: + ld a, [$C19F] + and a + ret nz ; still taking + call $3B12 ; increase entity state + ld [hl], $00 + ld a, [$C177] ; dialog selection + and a + ret nz + jp TalkResultHandler + +OwnerSpriteData: + ;db $60, $03, $62, $03, $62, $23, $60, $23 ; down + db $64, $03, $66, $03, $66, $23, $64, $23 ; up + ;db $68, $03, $6A, $03, $6C, $03, $6E, $03 ; left + ;db $6A, $23, $68, $23, $6E, $23, $6C, $23 ; right + +shopItemsHandler: +; Render the shop items + ld h, $00 +loop: + ; First load links position to render the item at + ldh a, [$98] ; LinkX + ldh [$EE], a ; X + ldh a, [$99] ; LinkY + sub $0E + ldh [$EC], a ; Y + ; Check if this is the item we have picked up + ld a, [$C509] ; picked up item in shop + dec a + cp h + jr z, .renderCarry + + ld a, h + swap a + add a, $20 + ldh [$EE], a ; X + ld a, $30 + ldh [$EC], a ; Y +.renderCarry: + ld a, h + push hl + ldh [$F1], a ; variant + cp $03 + jr nc, .singleSprite + ld de, ItemsDualSpriteData + call $3BC0 ; render sprite pair + jr .renderDone +.singleSprite: + ld de, ItemsSingleSpriteData + call $3C77 ; render sprite +.renderDone: + + pop hl +.skipItem: + inc h + ld a, $07 + cp h + jr nz, loop + +; check if we want to pickup or drop an item + ldh a, [$CC] + and $30 ; A or B button + call nz, checkForPickup + +; check if we have an item + ld a, [$C509] ; carry item + and a + ret z + + ; Set that link has picked something up + ld a, $01 + ld [$C15C], a + call $0CAF ; reset spin attack... + + ; Check if we are trying to exit the shop and so drop our item. + ldh a, [$99] + cp $78 + ret c + xor a + ld [$C509], a + + ret + +checkForPickup: + ldh a, [$9E] ; direction + cp $02 + ret nz + ldh a, [$99] ; LinkY + cp $48 + ret nc + + ld a, $13 + ldh [$F2], a ; play SFX + + ld a, [$C509] ; picked up shop item + and a + jr nz, .drop + + ldh a, [$98] ; LinkX + sub $08 + swap a + and $07 + ld [$C509], a ; picked up shop item + ret +.drop: + xor a + ld [$C509], a + ret + +ItemsDualSpriteData: + db $60, $08, $60, $28 ; zol + db $68, $09 ; chicken (left) +ItemsSingleSpriteData: ; (first 3 entries are still dual sprites) + db $6A, $09 ; chicken (right) + db $14, $02, $14, $22 ; piece of power +;Real single sprite data starts here + db $00, $0F ; bomb + db $38, $0A ; rupees + db $20, $0C ; medicine + db $28, $0C ; heart + +;------------------------------------trying to buy something starts here +talkHandler: + ld a, [$C509] ; carry item + add a, a + ret z ; check if we have something to buy + sub $02 + + ld hl, itemNames + ld e, a + ld d, b ; b=0 + add hl, de + ld e, [hl] + inc hl + ld d, [hl] + + ld hl, wCustomMessage + call appendString + dec hl + call padString + ld de, postMessage + call appendString + dec hl + ld a, $fe + ld [hl], a + ld de, $FFEF + add hl, de + ldh a, [$EE] + swap a + and $0F + add a, $30 + ld [hl], a + ld a, $C9 + call $2385 ; open dialog + call $3B12 ; increase entity state + ret + +appendString: + ld a, [de] + inc de + and a + ret z + ldi [hl], a + jr appendString + +padString: + ld a, l + and $0F + ret z + ld a, $20 + ldi [hl], a + jr padString + +itemNames: + dw itemZol + dw itemChicken + dw itemPieceOfPower + dw itemBombs + dw itemRupees + dw itemMedicine + dw itemHealth + +postMessage: + db "For player X? Yes No ", $00 + +itemZol: + db m"Slime storm|100 {RUPEES}", $00 +itemChicken: + db m"Coccu party|50 {RUPEES}", $00 +itemPieceOfPower: + db m"Piece of Power|50 {RUPEES}", $00 +itemBombs: + db m"10 Bombs|50 {RUPEES}", $00 +itemRupees: + db m"100 {RUPEES}|200 {RUPEES}", $00 +itemMedicine: + db m"Medicine|100 {RUPEES}", $00 +itemHealth: + db m"Health refill|10 {RUPEES}", $00 + +TalkResultHandler: + ld hl, ItemPriceTableBCD + ld a, [$C509] + dec a + add a, a + ld c, a ; b=0 + add hl, bc + ldi a, [hl] + ld d, [hl] + ld e, a + ld a, [$DB5D] + cp d + ret c + jr nz, .highEnough + ld a, [$DB5E] + cp e + ret c +.highEnough: + ; Got enough money, take it. + ld hl, ItemPriceTableDEC + ld a, [$C509] + dec a + ld c, a ; b=0 + add hl, bc + ld a, [hl] + ld [$DB92], a ; set substract buffer + + ; Set the item to send + ld hl, $DDFE + ld a, [$C509] ; currently picked up item + ldi [hl], a + ldh a, [$EE] ; X position of NPC + ldi [hl], a + ld hl, $DDF7 + set 2, [hl] + + ; No longer picked up item + xor a + ld [$C509], a + ret + +ItemPriceTableBCD: + dw $0100, $0050, $0050, $0050, $0200, $0100, $0010 +ItemPriceTableDEC: + db $64, $32, $32, $32, $C8, $64, $0A + """, 0x5E18), fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/music.py b/worlds/ladx/LADXR/patches/music.py new file mode 100644 index 0000000000..f7478a80c5 --- /dev/null +++ b/worlds/ladx/LADXR/patches/music.py @@ -0,0 +1,27 @@ +from ..assembler import ASM + + +_LOOPING_MUSIC = (1, 2, 3, 4, 5, 6, 7, 8, 9, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1C, 0x1D, 0x1F, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x2F, 0x31, 0x32, 0x33, 0x37, + 0x39, 0x3A, 0x3C, 0x3E, 0x40, 0x48, 0x49, 0x4A, 0x4B, 0x4E, 0x50, 0x53, 0x54, 0x55, 0x57, 0x58, 0x59, + 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, 0x60, 0x61) + + +def randomizeMusic(rom, rnd): + # Randomize overworld + for x in range(0, 16, 2): + for y in range(0, 16, 2): + idx = x + y * 16 + result = rnd.choice(_LOOPING_MUSIC) + rom.banks[0x02][idx] = result + rom.banks[0x02][idx+1] = result + rom.banks[0x02][idx+16] = result + rom.banks[0x02][idx+17] = result + # Random music in dungeons/caves + for n in range(0x20): + rom.banks[0x02][0x100 + n] = rnd.choice(_LOOPING_MUSIC) + + +def noMusic(rom): + rom.patch(0x1B, 0x001E, ASM("ld hl, $D368\nldi a, [hl]"), ASM("xor a"), fill_nop=True) + rom.patch(0x1E, 0x001E, ASM("ld hl, $D368\nldi a, [hl]"), ASM("xor a"), fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/nyan.bin b/worlds/ladx/LADXR/patches/nyan.bin new file mode 100644 index 0000000000000000000000000000000000000000..65f1772e7da1089ba3514b084e0e973df8e28e76 GIT binary patch literal 5760 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 F{{TG-AJG5+ literal 0 HcmV?d00001 diff --git a/worlds/ladx/LADXR/patches/overworld.py b/worlds/ladx/LADXR/patches/overworld.py new file mode 100644 index 0000000000..04668efca0 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld.py @@ -0,0 +1,224 @@ +from ..assembler import ASM +from ..roomEditor import RoomEditor, ObjectWarp, Object, WARP_TYPE_IDS +from .. import entityData +import os +import json + + +def patchOverworldTilesets(rom): + rom.patch(0x00, 0x0D5B, 0x0D79, ASM(""" + ; Instead of loading tileset info from a small 8x8 table, load it from a 16x16 table to give + ; full control. + ; A=MapRoom + ld hl, $2100 + ld [hl], $3F + ld d, $00 + ld e, a + ld hl, $7F00 + add hl, de + ldh a, [$94] ; We need to load the currently loaded tileset in E to compare it + ld e, a + ld a, [hl] + ld hl, $2100 + ld [hl], $20 + """), fill_nop=True) + # Remove the camera shop exception + rom.patch(0x00, 0x0D80, 0x0D8B, "", fill_nop=True) + + for x in range(16): + for y in range(16): + rom.banks[0x3F][0x3F00+x+y*16] = rom.banks[0x20][0x2E73 + (x // 2) + (y // 2) * 8] + rom.banks[0x3F][0x3F07] = rom.banks[0x3F][0x3F08] # Fix the room next to the egg + rom.banks[0x3F][0x3F17] = rom.banks[0x3F][0x3F08] # Fix the room next to the egg + rom.banks[0x3F][0x3F3A] = 0x0F # room below mambo cave + rom.banks[0x3F][0x3F3B] = 0x0F # room below D4 + rom.banks[0x3F][0x3F4B] = 0x0F # room next to castle + rom.banks[0x3F][0x3F5B] = 0x0F # room next to castle + # Fix the rooms around the camera shop + rom.banks[0x3F][0x3F26] = 0x0F + rom.banks[0x3F][0x3F27] = 0x0F + rom.banks[0x3F][0x3F36] = 0x0F + + +def createDungeonOnlyOverworld(rom): + # Skip the whole egg maze. + rom.patch(0x14, 0x0453, "75", "73") + + instrument_rooms = [0x102, 0x12A, 0x159, 0x162, 0x182, 0x1B5, 0x22C, 0x230, 0x301] + path = os.path.dirname(__file__) + + # Start with clearing all the maps, because this just generates a bunch of room in the rom. + for n in range(0x100): + re = RoomEditor(rom, n) + re.entities = [] + re.objects = [] + if os.path.exists("%s/overworld/dive/%02X.json" % (path, n)): + re.loadFromJson("%s/overworld/dive/%02X.json" % (path, n)) + entrances = list(filter(lambda obj: obj.type_id in WARP_TYPE_IDS, re.objects)) + for obj in re.objects: + if isinstance(obj, ObjectWarp) and entrances: + e = entrances.pop(0) + + other = RoomEditor(rom, obj.room) + for o in other.objects: + if isinstance(o, ObjectWarp) and o.warp_type == 0: + o.room = n + o.target_x = e.x * 16 + 8 + o.target_y = e.y * 16 + 16 + other.store(rom) + + if obj.room == 0x1F5: + # Patch the boomang guy exit + other = RoomEditor(rom, "Alt1F5") + other.getWarps()[0].room = n + other.getWarps()[0].target_x = e.x * 16 + 8 + other.getWarps()[0].target_y = e.y * 16 + 16 + other.store(rom) + + if obj.warp_type == 1 and (obj.map_nr < 8 or obj.map_nr == 0xFF) and obj.room not in (0x1B0, 0x23A, 0x23D): + other = RoomEditor(rom, instrument_rooms[min(8, obj.map_nr)]) + for o in other.objects: + if isinstance(o, ObjectWarp) and o.warp_type == 0: + o.room = n + o.target_x = e.x * 16 + 8 + o.target_y = e.y * 16 + 16 + other.store(rom) + re.store(rom) + + +def exportOverworld(rom): + import PIL.Image + + path = os.path.dirname(__file__) + for room_index in list(range(0x100)) + ["Alt06", "Alt0E", "Alt1B", "Alt2B", "Alt79", "Alt8C"]: + room = RoomEditor(rom, room_index) + if isinstance(room_index, int): + room_nr = room_index + else: + room_nr = int(room_index[3:], 16) + tileset_index = rom.banks[0x3F][0x3F00 + room_nr] + attributedata_bank = rom.banks[0x1A][0x2476 + room_nr] + attributedata_addr = rom.banks[0x1A][0x1E76 + room_nr * 2] + attributedata_addr |= rom.banks[0x1A][0x1E76 + room_nr * 2 + 1] << 8 + attributedata_addr -= 0x4000 + + metatile_info = rom.banks[0x1A][0x2B1D:0x2B1D + 0x400] + attrtile_info = rom.banks[attributedata_bank][attributedata_addr:attributedata_addr+0x400] + + palette_index = rom.banks[0x21][0x02EF + room_nr] + palette_addr = rom.banks[0x21][0x02B1 + palette_index * 2] + palette_addr |= rom.banks[0x21][0x02B1 + palette_index * 2 + 1] << 8 + palette_addr -= 0x4000 + + hidden_warp_tiles = [] + for obj in room.objects: + if obj.type_id in WARP_TYPE_IDS and room.overlay[obj.x + obj.y * 10] != obj.type_id: + if obj.type_id != 0xE1 or room.overlay[obj.x + obj.y * 10] != 0x53: # Ignore the waterfall 'caves' + hidden_warp_tiles.append(obj) + if obj.type_id == 0xC5 and room_nr < 0x100 and room.overlay[obj.x + obj.y * 10] == 0xC4: + # Pushable gravestones have the wrong overlay by default + room.overlay[obj.x + obj.y * 10] = 0xC5 + if obj.type_id == 0xDC and room_nr < 0x100: + # Flowers above the rooster windmill need a different tile + hidden_warp_tiles.append(obj) + + image_filename = "tiles_%02x_%02x_%02x_%02x_%04x.png" % (tileset_index, room.animation_id, palette_index, attributedata_bank, attributedata_addr) + data = { + "width": 10, "height": 8, + "type": "map", "renderorder": "right-down", "tiledversion": "1.4.3", "version": 1.4, + "tilewidth": 16, "tileheight": 16, "orientation": "orthogonal", + "tilesets": [ + { + "columns": 16, "firstgid": 1, + "image": image_filename, "imageheight": 256, "imagewidth": 256, + "margin": 0, "name": "main", "spacing": 0, + "tilecount": 256, "tileheight": 16, "tilewidth": 16 + } + ], + "layers": [{ + "data": [n+1 for n in room.overlay], + "width": 10, "height": 8, + "id": 1, "name": "Tiles", "type": "tilelayer", "visible": True, "opacity": 1, "x": 0, "y": 0, + }, { + "id": 2, "name": "EntityLayer", "type": "objectgroup", "visible": True, "opacity": 1, "x": 0, "y": 0, + "objects": [ + {"width": 16, "height": 16, "x": entity[0] * 16, "y": entity[1] * 16, "name": entityData.NAME[entity[2]], "type": "entity"} for entity in room.entities + ] + [ + {"width": 8, "height": 8, "x": 0, "y": idx * 8, "name": "%x:%02x:%03x:%02x:%02x" % (obj.warp_type, obj.map_nr, obj.room, obj.target_x, obj.target_y), "type": "warp"} for idx, obj in enumerate(room.getWarps()) if isinstance(obj, ObjectWarp) + ] + [ + {"width": 16, "height": 16, "x": obj.x * 16, "y": obj.y * 16, "name": "%02X" % (obj.type_id), "type": "hidden_tile"} for obj in hidden_warp_tiles + ], + }], + "properties": [ + {"name": "tileset", "type": "string", "value": "%02X" % (tileset_index)}, + {"name": "animationset", "type": "string", "value": "%02X" % (room.animation_id)}, + {"name": "attribset", "type": "string", "value": "%02X:%04X" % (attributedata_bank, attributedata_addr)}, + {"name": "palette", "type": "string", "value": "%02X" % (palette_index)}, + ] + } + if isinstance(room_index, str): + json.dump(data, open("%s/overworld/export/%s.json" % (path, room_index), "wt")) + else: + json.dump(data, open("%s/overworld/export/%02X.json" % (path, room_index), "wt")) + + if not os.path.exists("%s/overworld/export/%s" % (path, image_filename)): + tilemap = rom.banks[0x2F][tileset_index*0x100:tileset_index*0x100+0x200] + tilemap += rom.banks[0x2C][0x1200:0x1800] + tilemap += rom.banks[0x2C][0x0800:0x1000] + anim_addr = {2: 0x2B00, 3: 0x2C00, 4: 0x2D00, 5: 0x2E00, 6: 0x2F00, 7: 0x2D00, 8: 0x3000, 9: 0x3100, 10: 0x3200, 11: 0x2A00, 12: 0x3300, 13: 0x3500, 14: 0x3600, 15: 0x3400, 16: 0x3700}.get(room.animation_id, 0x0000) + tilemap[0x6C0:0x700] = rom.banks[0x2C][anim_addr:anim_addr + 0x40] + + palette = [] + for n in range(8*4): + p0 = rom.banks[0x21][palette_addr] + p1 = rom.banks[0x21][palette_addr + 1] + pal = p0 | p1 << 8 + palette_addr += 2 + r = (pal & 0x1F) << 3 + g = ((pal >> 5) & 0x1F) << 3 + b = ((pal >> 10) & 0x1F) << 3 + palette += [r, g, b] + + img = PIL.Image.new("P", (16*16, 16*16)) + img.putpalette(palette) + def drawTile(x, y, index, attr): + for py in range(8): + a = tilemap[index * 16 + py * 2] + b = tilemap[index * 16 + py * 2 + 1] + if attr & 0x40: + a = tilemap[index * 16 + 14 - py * 2] + b = tilemap[index * 16 + 15 - py * 2] + for px in range(8): + bit = 0x80 >> px + if attr & 0x20: + bit = 0x01 << px + c = (attr & 7) << 2 + if a & bit: + c |= 1 + if b & bit: + c |= 2 + img.putpixel((x+px, y+py), c) + for x in range(16): + for y in range(16): + idx = x+y*16 + metatiles = metatile_info[idx*4:idx*4+4] + attrtiles = attrtile_info[idx*4:idx*4+4] + drawTile(x * 16 + 0, y * 16 + 0, metatiles[0], attrtiles[0]) + drawTile(x * 16 + 8, y * 16 + 0, metatiles[1], attrtiles[1]) + drawTile(x * 16 + 0, y * 16 + 8, metatiles[2], attrtiles[2]) + drawTile(x * 16 + 8, y * 16 + 8, metatiles[3], attrtiles[3]) + img.save("%s/overworld/export/%s" % (path, image_filename)) + + world = { + "maps": [ + {"fileName": "%02X.json" % (n), "height": 128, "width": 160, "x": (n & 0x0F) * 160, "y": (n >> 4) * 128} + for n in range(0x100) + ], + "onlyShowAdjacentMaps": False, + "type": "world" + } + json.dump(world, open("%s/overworld/export/world.world" % (path), "wt")) + + +def isNormalOverworld(rom): + return len(RoomEditor(rom, 0x010).getWarps()) > 0 diff --git a/worlds/ladx/LADXR/patches/overworld/dive/00.json b/worlds/ladx/LADXR/patches/overworld/dive/00.json new file mode 100644 index 0000000000..fd16fa6675 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/00.json @@ -0,0 +1,124 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 125, 126, 1, 129, 78, 78, 78, 130, 1, 125, 240, 240, 240, 56, 11, 11, 11, 57, 240, 240, 2, 2, 30, 47, 73, 225, 74, 79, 94, 2, 2, 2, 56, 58, 226, 225, 59, 60, 57, 2, 2, 2, 56, 10, 10, 10, 10, 10, 123, 123, 2, 2, 56, 10, 10, 10, 10, 10, 57, 2, 2, 2, 47, 48, 48, 48, 48, 48, 79, 2], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":16, + "id":1, + "name":"HEART_PIECE", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":64, + "y":32 + }, + { + "height":16, + "id":2, + "name":"CROW", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":96, + "y":32 + }, + { + "height":16, + "id":3, + "name":"MINI_MOLDORM", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":112, + "y":96 + }, + { + "height":8, + "id":4, + "name":"1:07:23a:58:10", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":5, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"0B" + }, + { + "name":"attribset", + "type":"string", + "value":"25:3400" + }, + { + "name":"palette", + "type":"string", + "value":"0F" + }, + { + "name":"tileset", + "type":"string", + "value":"1C" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_1c_0b_0f_25_3400.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/01.json b/worlds/ladx/LADXR/patches/overworld/dive/01.json new file mode 100644 index 0000000000..0441d875a1 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/01.json @@ -0,0 +1,102 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[125, 126, 1, 1, 1, 1, 1, 1, 1, 125, 29, 29, 126, 1, 1, 129, 78, 130, 125, 29, 240, 240, 240, 240, 240, 56, 4, 57, 240, 240, 2, 2, 30, 81, 81, 47, 48, 79, 94, 2, 2, 2, 56, 4, 4, 206, 226, 216, 57, 2, 123, 123, 123, 11, 4, 4, 4, 4, 57, 2, 2, 2, 56, 11, 11, 11, 11, 11, 57, 2, 2, 2, 47, 48, 48, 48, 48, 48, 79, 2], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":16, + "id":1, + "name":"CROW", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":32, + "y":80 + }, + { + "height":8, + "id":2, + "name":"1:07:23d:58:10", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":3, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"0B" + }, + { + "name":"attribset", + "type":"string", + "value":"25:3400" + }, + { + "name":"palette", + "type":"string", + "value":"0F" + }, + { + "name":"tileset", + "type":"string", + "value":"1C" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_1c_0b_0f_25_3400.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/06.json b/worlds/ladx/LADXR/patches/overworld/dive/06.json new file mode 100644 index 0000000000..405a7aa73c --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/06.json @@ -0,0 +1,113 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[1, 1, 1, 1, 6, 7, 8, 1, 1, 1, 125, 126, 1, 129, 100, 101, 102, 130, 125, 126, 240, 240, 240, 56, 114, 29, 128, 57, 240, 240, 230, 230, 30, 56, 170, 171, 192, 57, 94, 230, 230, 230, 56, 47, 73, 225, 74, 79, 57, 230, 230, 230, 56, 63, 59, 225, 59, 64, 57, 230, 230, 30, 47, 48, 73, 225, 74, 48, 79, 94, 230, 56, 63, 59, 59, 225, 59, 59, 64, 57], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":16, + "id":1, + "name":"EGG_SONG_EVENT", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":0, + "y":0 + }, + { + "height":8, + "id":2, + "name":"1:08:270:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }, + { + "height":16, + "id":3, + "name":"E1", + "rotation":0, + "type":"hidden_tile", + "visible":true, + "width":16, + "x":80, + "y":48 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":4, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"0B" + }, + { + "name":"attribset", + "type":"string", + "value":"27:1620" + }, + { + "name":"palette", + "type":"string", + "value":"13" + }, + { + "name":"tileset", + "type":"string", + "value":"3C" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_3c_0b_13_27_1620.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/16.json b/worlds/ladx/LADXR/patches/overworld/dive/16.json new file mode 100644 index 0000000000..25538084ca --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/16.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[30, 47, 48, 48, 73, 225, 48, 48, 48, 79, 56, 63, 59, 59, 59, 225, 59, 59, 59, 64, 56, 58, 59, 59, 59, 225, 59, 59, 59, 60, 47, 48, 48, 48, 73, 225, 74, 48, 48, 48, 58, 59, 226, 59, 59, 225, 59, 59, 59, 59, 201, 213, 4, 4, 4, 4, 4, 4, 4, 201, 201, 4, 4, 4, 4, 4, 4, 4, 4, 201, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"0:00:082:48:30", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"0B" + }, + { + "name":"attribset", + "type":"string", + "value":"27:1620" + }, + { + "name":"palette", + "type":"string", + "value":"13" + }, + { + "name":"tileset", + "type":"string", + "value":"3C" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_3c_0b_13_27_1620.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/62.json b/worlds/ladx/LADXR/patches/overworld/dive/62.json new file mode 100644 index 0000000000..26d6c9de6c --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/62.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[2, 2, 2, 115, 117, 117, 117, 116, 2, 2, 2, 2, 2, 115, 118, 215, 119, 116, 2, 2, 2, 2, 30, 115, 117, 226, 117, 116, 94, 2, 2, 2, 56, 183, 117, 120, 117, 184, 57, 2, 2, 2, 56, 4, 4, 4, 4, 4, 57, 2, 2, 2, 56, 4, 4, 4, 4, 4, 57, 2, 2, 2, 47, 48, 73, 225, 74, 48, 79, 2, 2, 2, 63, 59, 59, 225, 59, 59, 64, 2], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:06:20e:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"00" + }, + { + "name":"attribset", + "type":"string", + "value":"27:1E40" + }, + { + "name":"palette", + "type":"string", + "value":"16" + }, + { + "name":"tileset", + "type":"string", + "value":"30" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_30_00_16_27_1e40.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/6C.json b/worlds/ladx/LADXR/patches/overworld/dive/6C.json new file mode 100644 index 0000000000..79b3f96174 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/6C.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 44, 45, 45, 46, 15, 15, 15, 15, 15, 15, 56, 161, 199, 57, 15, 15, 15, 15, 15, 15, 56, 5, 5, 57, 15, 15, 15, 15, 15, 15, 52, 48, 48, 53, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:05:1b0:78:10", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"03" + }, + { + "name":"attribset", + "type":"string", + "value":"22:0000" + }, + { + "name":"palette", + "type":"string", + "value":"16" + }, + { + "name":"tileset", + "type":"string", + "value":"0F" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_0f_03_16_22_0000.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/71.json b/worlds/ladx/LADXR/patches/overworld/dive/71.json new file mode 100644 index 0000000000..52910db6c4 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/71.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[3, 56, 63, 59, 59, 59, 59, 59, 64, 48, 3, 56, 58, 183, 59, 226, 59, 183, 60, 10, 3, 56, 33, 184, 10, 10, 10, 184, 10, 10, 3, 56, 33, 10, 10, 10, 10, 10, 10, 4, 3, 56, 201, 10, 10, 10, 10, 10, 4, 4, 3, 56, 201, 201, 10, 10, 10, 10, 10, 4, 3, 47, 48, 48, 48, 48, 48, 48, 48, 48, 3, 63, 59, 59, 59, 59, 59, 59, 59, 59], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:07:25d:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"00" + }, + { + "name":"attribset", + "type":"string", + "value":"25:3400" + }, + { + "name":"palette", + "type":"string", + "value":"19" + }, + { + "name":"tileset", + "type":"string", + "value":"1C" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_1c_00_19_25_3400.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/72.json b/worlds/ladx/LADXR/patches/overworld/dive/72.json new file mode 100644 index 0000000000..aa79f1a655 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/72.json @@ -0,0 +1,80 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[48, 54, 58, 59, 59, 225, 59, 59, 60, 46, 4, 4, 4, 4, 4, 4, 4, 4, 4, 57, 4, 4, 4, 4, 4, 93, 93, 93, 4, 77, 4, 4, 12, 12, 4, 93, 93, 93, 93, 4, 4, 4, 12, 4, 4, 4, 93, 93, 4, 4, 4, 4, 12, 4, 4, 4, 4, 4, 4, 4, 48, 73, 225, 74, 48, 73, 75, 74, 48, 48, 59, 59, 225, 59, 59, 59, 59, 59, 59, 59], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":1, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"00" + }, + { + "name":"attribset", + "type":"string", + "value":"22:0000" + }, + { + "name":"palette", + "type":"string", + "value":"16" + }, + { + "name":"tileset", + "type":"string", + "value":"0F" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_0f_00_16_22_0000.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/73.json b/worlds/ladx/LADXR/patches/overworld/dive/73.json new file mode 100644 index 0000000000..b762b0ce87 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/73.json @@ -0,0 +1,113 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[2, 2, 2, 180, 180, 180, 180, 180, 2, 2, 2, 2, 44, 180, 180, 180, 180, 180, 46, 2, 81, 81, 76, 174, 178, 232, 174, 178, 57, 2, 4, 4, 4, 175, 179, 228, 175, 179, 57, 2, 4, 4, 4, 4, 4, 4, 4, 4, 57, 2, 4, 4, 4, 4, 4, 4, 4, 4, 57, 2, 48, 48, 48, 48, 48, 48, 48, 48, 53, 2, 59, 59, 59, 59, 59, 59, 59, 59, 64, 2], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":16, + "id":1, + "name":"ARMOS_STATUE", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":64, + "y":64 + }, + { + "height":16, + "id":2, + "name":"ARMOS_STATUE", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":96, + "y":64 + }, + { + "height":8, + "id":3, + "name":"1:05:1d4:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":4, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"00" + }, + { + "name":"attribset", + "type":"string", + "value":"25:3C00" + }, + { + "name":"palette", + "type":"string", + "value":"1C" + }, + { + "name":"tileset", + "type":"string", + "value":"2A" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_2a_00_1c_25_3c00.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/81.json b/worlds/ladx/LADXR/patches/overworld/dive/81.json new file mode 100644 index 0000000000..e85673835c --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/81.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[11, 57, 63, 59, 214, 215, 216, 59, 64, 63, 11, 57, 58, 59, 206, 226, 207, 59, 60, 63, 11, 57, 15, 15, 15, 12, 15, 15, 15, 58, 11, 57, 15, 15, 15, 12, 15, 15, 15, 5, 11, 57, 15, 15, 15, 12, 15, 15, 5, 5, 55, 53, 15, 15, 15, 12, 12, 12, 5, 5, 38, 39, 10, 15, 15, 15, 15, 15, 15, 38, 40, 42, 39, 38, 39, 38, 39, 38, 39, 40], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:03:17a:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"03" + }, + { + "name":"attribset", + "type":"string", + "value":"27:2640" + }, + { + "name":"palette", + "type":"string", + "value":"01" + }, + { + "name":"tileset", + "type":"string", + "value":"34" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_34_03_01_27_2640.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/82.json b/worlds/ladx/LADXR/patches/overworld/dive/82.json new file mode 100644 index 0000000000..ad62948706 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/82.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[59, 59, 225, 59, 59, 59, 59, 59, 59, 59, 59, 59, 225, 59, 59, 59, 59, 59, 59, 59, 59, 59, 225, 59, 187, 59, 59, 59, 59, 59, 5, 5, 12, 10, 93, 10, 5, 5, 5, 5, 5, 5, 12, 5, 10, 5, 11, 11, 11, 5, 5, 5, 12, 5, 5, 11, 93, 93, 93, 11, 39, 5, 5, 5, 5, 11, 93, 93, 93, 11, 41, 5, 5, 5, 5, 5, 11, 11, 11, 62], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"0:00:016:28:50", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"00" + }, + { + "name":"attribset", + "type":"string", + "value":"25:0C00" + }, + { + "name":"palette", + "type":"string", + "value":"01" + }, + { + "name":"tileset", + "type":"string", + "value":"0F" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_0f_00_01_25_0c00.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/83.json b/worlds/ladx/LADXR/patches/overworld/dive/83.json new file mode 100644 index 0000000000..a97454979e --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/83.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[59, 64, 38, 39, 72, 59, 59, 59, 59, 60, 59, 64, 40, 41, 57, 183, 184, 103, 82, 82, 59, 60, 212, 212, 57, 104, 228, 105, 82, 203, 5, 5, 5, 5, 57, 15, 15, 15, 15, 203, 5, 5, 62, 225, 53, 15, 15, 15, 15, 203, 5, 5, 57, 15, 15, 15, 15, 15, 82, 203, 5, 33, 57, 15, 15, 15, 15, 82, 82, 203, 48, 61, 51, 45, 45, 45, 45, 45, 45, 45], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:04:1a1:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"03" + }, + { + "name":"attribset", + "type":"string", + "value":"22:2400" + }, + { + "name":"palette", + "type":"string", + "value":"09" + }, + { + "name":"tileset", + "type":"string", + "value":"3A" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_3a_03_09_22_2400.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/91.json b/worlds/ladx/LADXR/patches/overworld/dive/91.json new file mode 100644 index 0000000000..1c9fe427d3 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/91.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[43, 42, 63, 59, 59, 59, 59, 59, 64, 42, 42, 43, 63, 59, 183, 216, 183, 59, 64, 40, 43, 41, 58, 183, 184, 226, 184, 183, 60, 11, 41, 82, 82, 28, 28, 28, 28, 82, 19, 5, 82, 28, 28, 28, 28, 28, 28, 27, 23, 5, 82, 82, 28, 28, 82, 82, 28, 19, 33, 5, 82, 82, 82, 28, 28, 28, 28, 19, 33, 5, 38, 39, 38, 39, 38, 39, 38, 39, 38, 39], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:01:136:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"03" + }, + { + "name":"attribset", + "type":"string", + "value":"22:3400" + }, + { + "name":"palette", + "type":"string", + "value":"0E" + }, + { + "name":"tileset", + "type":"string", + "value":"36" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_36_03_0e_22_3400.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/92.json b/worlds/ladx/LADXR/patches/overworld/dive/92.json new file mode 100644 index 0000000000..1f78b1bebe --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/92.json @@ -0,0 +1,102 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[39, 5, 5, 5, 5, 5, 5, 5, 5, 51, 41, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 11, 11, 11, 5, 38, 70, 39, 5, 5, 5, 83, 83, 83, 11, 40, 226, 41, 5, 5, 5, 92, 227, 92, 11, 11, 93, 11, 5, 5, 5, 5, 5, 5, 11, 11, 11, 5, 10, 5, 5, 5, 5, 5, 5, 5, 5, 10, 10, 38, 39, 5, 5, 5, 5, 5, 5, 10, 111], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:10:2cb:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }, + { + "height":8, + "id":2, + "name":"1:0e:2a1:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":8 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":3, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"03" + }, + { + "name":"attribset", + "type":"string", + "value":"22:0000" + }, + { + "name":"palette", + "type":"string", + "value":"0E" + }, + { + "name":"tileset", + "type":"string", + "value":"0F" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_0f_03_0e_22_0000.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/93.json b/worlds/ladx/LADXR/patches/overworld/dive/93.json new file mode 100644 index 0000000000..c9ac830b6d --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/93.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[45, 50, 63, 59, 59, 59, 59, 59, 64, 63, 5, 5, 58, 107, 109, 109, 109, 107, 60, 63, 5, 5, 183, 108, 99, 228, 99, 108, 183, 63, 5, 5, 184, 18, 28, 28, 28, 19, 184, 63, 5, 5, 5, 22, 17, 17, 17, 23, 5, 63, 10, 5, 10, 183, 5, 5, 5, 183, 5, 58, 10, 10, 10, 184, 5, 5, 5, 184, 5, 38, 111, 38, 39, 38, 39, 38, 39, 38, 39, 40], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:02:152:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"03" + }, + { + "name":"attribset", + "type":"string", + "value":"22:1800" + }, + { + "name":"palette", + "type":"string", + "value":"01" + }, + { + "name":"tileset", + "type":"string", + "value":"2E" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_2e_03_01_22_1800.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/A1.json b/worlds/ladx/LADXR/patches/overworld/dive/A1.json new file mode 100644 index 0000000000..8ed9e355bb --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/A1.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[43, 42, 44, 45, 45, 45, 46, 42, 43, 42, 42, 43, 47, 48, 48, 48, 79, 40, 41, 40, 43, 41, 58, 99, 228, 99, 60, 11, 11, 5, 41, 11, 11, 10, 10, 10, 10, 10, 10, 10, 111, 11, 183, 10, 10, 10, 183, 10, 10, 10, 111, 5, 184, 10, 10, 10, 184, 10, 5, 5, 111, 5, 5, 10, 10, 10, 10, 5, 5, 5, 48, 48, 73, 75, 74, 48, 48, 48, 48, 48], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:00:117:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"00" + }, + { + "name":"attribset", + "type":"string", + "value":"22:0C00" + }, + { + "name":"palette", + "type":"string", + "value":"01" + }, + { + "name":"tileset", + "type":"string", + "value":"24" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_24_00_01_22_0c00.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/A2.json b/worlds/ladx/LADXR/patches/overworld/dive/A2.json new file mode 100644 index 0000000000..540f5c97b2 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/A2.json @@ -0,0 +1,113 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[43, 41, 5, 5, 5, 5, 5, 10, 10, 111, 41, 5, 5, 5, 5, 5, 5, 5, 5, 10, 5, 5, 38, 39, 93, 93, 93, 38, 39, 11, 10, 5, 40, 41, 83, 83, 83, 40, 41, 11, 10, 5, 11, 11, 92, 227, 92, 11, 11, 11, 5, 5, 11, 11, 11, 12, 11, 11, 11, 11, 5, 5, 5, 11, 12, 12, 5, 5, 5, 11, 61, 4, 4, 4, 12, 4, 4, 4, 4, 62], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":16, + "id":1, + "name":"BUTTERFLY", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":32, + "y":48 + }, + { + "height":16, + "id":2, + "name":"BUTTERFLY", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":112, + "y":80 + }, + { + "height":8, + "id":3, + "name":"1:10:2a3:50:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":4, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"00" + }, + { + "name":"attribset", + "type":"string", + "value":"22:0000" + }, + { + "name":"palette", + "type":"string", + "value":"01" + }, + { + "name":"tileset", + "type":"string", + "value":"0F" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_0f_00_01_22_0000.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/A3.json b/worlds/ladx/LADXR/patches/overworld/dive/A3.json new file mode 100644 index 0000000000..10eebc4c6f --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/A3.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[111, 82, 82, 82, 82, 82, 82, 82, 82, 82, 11, 11, 11, 5, 5, 5, 5, 11, 11, 82, 11, 183, 184, 10, 183, 201, 184, 5, 11, 82, 11, 206, 207, 10, 206, 226, 207, 5, 11, 82, 11, 5, 5, 11, 11, 11, 11, 11, 11, 82, 11, 11, 11, 5, 197, 10, 197, 11, 11, 82, 11, 5, 11, 11, 11, 11, 5, 5, 11, 82, 48, 48, 48, 48, 48, 48, 48, 48, 48, 61], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:ff:312:50:5c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"00" + }, + { + "name":"attribset", + "type":"string", + "value":"25:2800" + }, + { + "name":"palette", + "type":"string", + "value":"01" + }, + { + "name":"tileset", + "type":"string", + "value":"38" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_38_00_01_25_2800.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/B0.json b/worlds/ladx/LADXR/patches/overworld/dive/B0.json new file mode 100644 index 0000000000..9203a9ec03 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/B0.json @@ -0,0 +1,80 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[56, 4, 4, 4, 4, 4, 4, 4, 4, 57, 56, 183, 184, 4, 4, 4, 4, 62, 48, 53, 56, 206, 207, 4, 4, 4, 4, 57, 183, 184, 56, 161, 93, 4, 4, 4, 4, 57, 206, 207, 56, 93, 93, 62, 73, 75, 74, 79, 183, 184, 56, 93, 4, 57, 59, 59, 59, 60, 206, 207, 47, 48, 48, 79, 31, 31, 31, 31, 31, 31, 58, 59, 59, 60, 32, 32, 32, 32, 32, 32], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":1, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"02" + }, + { + "name":"attribset", + "type":"string", + "value":"22:1000" + }, + { + "name":"palette", + "type":"string", + "value":"01" + }, + { + "name":"tileset", + "type":"string", + "value":"22" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_22_02_01_22_1000.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/B1.json b/worlds/ladx/LADXR/patches/overworld/dive/B1.json new file mode 100644 index 0000000000..9ec04a19cd --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/B1.json @@ -0,0 +1,102 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[44, 46, 44, 45, 45, 45, 45, 46, 44, 46, 52, 53, 52, 48, 227, 48, 48, 53, 52, 53, 183, 184, 9, 9, 9, 9, 201, 183, 184, 9, 206, 207, 9, 9, 9, 9, 9, 206, 207, 9, 183, 184, 201, 9, 9, 9, 9, 183, 184, 9, 206, 207, 9, 9, 9, 9, 9, 206, 207, 9, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":16, + "id":1, + "name":"MARIN", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":48, + "y":48 + }, + { + "height":8, + "id":2, + "name":"1:1f:1e1:88:50", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":3, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"02" + }, + { + "name":"attribset", + "type":"string", + "value":"22:1000" + }, + { + "name":"palette", + "type":"string", + "value":"01" + }, + { + "name":"tileset", + "type":"string", + "value":"22" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_22_02_01_22_1000.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/B2.json b/worlds/ladx/LADXR/patches/overworld/dive/B2.json new file mode 100644 index 0000000000..b16f6fe221 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/B2.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[56, 4, 4, 4, 12, 4, 4, 4, 4, 57, 52, 54, 4, 4, 4, 9, 9, 9, 55, 53, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 183, 184, 9, 9, 9, 9, 9, 37, 9, 9, 206, 207, 36, 9, 9, 9, 9, 9, 9, 9, 9, 36, 9, 9, 9, 9, 9, 9, 9, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":16, + "id":1, + "name":"HEART_PIECE", + "rotation":0, + "type":"entity", + "visible":true, + "width":16, + "x":80, + "y":80 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"02" + }, + { + "name":"attribset", + "type":"string", + "value":"22:1000" + }, + { + "name":"palette", + "type":"string", + "value":"01" + }, + { + "name":"tileset", + "type":"string", + "value":"22" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_22_02_01_22_1000.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/overworld/dive/B3.json b/worlds/ladx/LADXR/patches/overworld/dive/B3.json new file mode 100644 index 0000000000..608aed75d3 --- /dev/null +++ b/worlds/ladx/LADXR/patches/overworld/dive/B3.json @@ -0,0 +1,91 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":8, + "infinite":false, + "layers":[ + { + "data":[63, 59, 59, 63, 59, 59, 59, 64, 59, 56, 58, 59, 59, 63, 59, 59, 59, 64, 59, 56, 9, 9, 9, 58, 59, 187, 59, 60, 9, 56, 9, 9, 36, 9, 9, 36, 9, 9, 9, 56, 9, 9, 9, 9, 9, 9, 9, 9, 9, 56, 9, 9, 9, 9, 36, 9, 9, 9, 37, 56, 31, 31, 31, 31, 31, 31, 31, 31, 31, 52, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32], + "height":8, + "id":1, + "name":"Tiles", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"EntityLayer", + "objects":[ + { + "height":8, + "id":1, + "name":"1:1f:1f5:48:7c", + "rotation":0, + "type":"warp", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":1, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"animationset", + "type":"string", + "value":"02" + }, + { + "name":"attribset", + "type":"string", + "value":"22:1000" + }, + { + "name":"palette", + "type":"string", + "value":"01" + }, + { + "name":"tileset", + "type":"string", + "value":"22" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":16, + "tilesets":[ + { + "columns":16, + "firstgid":1, + "image":"tiles_22_02_01_22_1000.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"main", + "spacing":0, + "tilecount":256, + "tileheight":16, + "tilewidth":16 + }], + "tilewidth":16, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/owl.py b/worlds/ladx/LADXR/patches/owl.py new file mode 100644 index 0000000000..b22386a6cb --- /dev/null +++ b/worlds/ladx/LADXR/patches/owl.py @@ -0,0 +1,144 @@ +from ..roomEditor import RoomEditor +from ..assembler import ASM +from ..utils import formatText + + +def removeOwlEvents(rom): + # Remove all the owl events from the entity tables. + for room in range(0x100): + re = RoomEditor(rom, room) + if re.hasEntity(0x41): + re.removeEntities(0x41) + re.store(rom) + # Clear texts used by the owl. Potentially reused somewhere o else. + rom.texts[0x0D9] = b'\xff' # used by boomerang + # 1 Used by empty chest (master stalfos message) + # 8 unused (0x0C0-0x0C7) + # 1 used by bowwow in chest + # 1 used by item for other player message + # 2 used by arrow chest messages + # 2 used by tunics + for idx in range(0x0BE, 0x0CE): + rom.texts[idx] = b'\xff' + + + # Patch the owl entity into a ghost to allow refill of powder/bombs/arrows + rom.texts[0xC0] = formatText("Everybody hates me, so I give away free things in the hope people will love me. Want something?", ask="Okay No") + rom.texts[0xC1] = formatText("Good for you.") + rom.patch(0x06, 0x27F5, 0x2A77, ASM(""" + ; Check if we have powder or bombs. + ld e, INV_SIZE + ld hl, $DB00 +loop: + ldi a, [hl] + cp $02 ; bombs + jr z, hasProperItem + cp $0C ; powder + jr z, hasProperItem + cp $05 ; bow + jr z, hasProperItem + dec e + jr nz, loop + ret +hasProperItem: + + ; Render ghost + ld de, sprite + call $3BC0 + + call $64C6 ; check if game is busy (pops this stack frame if busy) + + ldh a, [$E7] ; frame counter + swap a + and $01 + call $3B0C ; set entity sprite variant + call $641A ; check collision + ldh a, [$F0] ;entity state + rst 0 + dw waitForTalk + dw talking + +waitForTalk: + call $645D ; check if talked to + ret nc + ld a, $C0 + call $2385 ; open dialog + call $3B12 ; increase entity state + ret + +talking: + ; Check if we are still talking + ld a, [$C19F] + and a + ret nz + call $3B12 ; increase entity state + ld [hl], $00 ; set to state 0 + ld a, [$C177] ; get which option we selected + and a + ret nz + + ; Give powder + ld a, [$DB4C] + cp $10 + jr nc, doNotGivePowder + ld a, $10 + ld [$DB4C], a +doNotGivePowder: + + ld a, [$DB4D] + cp $10 + jr nc, doNotGiveBombs + ld a, $10 + ld [$DB4D], a +doNotGiveBombs: + + ld a, [$DB45] + cp $10 + jr nc, doNotGiveArrows + ld a, $10 + ld [$DB45], a +doNotGiveArrows: + + ld a, $C1 + call $2385 ; open dialog + ret + +sprite: + db $76, $09, $78, $09, $7A, $09, $7C, $09 +""", 0x67F5), fill_nop=True) + rom.patch(0x20, 0x0322 + 0x41 * 2, "734A", "564B") # Remove the owl init handler + + re = RoomEditor(rom, 0x2A3) + re.entities.append((7, 6, 0x41)) + re.store(rom) + + +def upgradeDungeonOwlStatues(rom): + # Call our custom handler after the check for the stone beak + rom.patch(0x18, 0x1EA2, ASM("ldh a, [$F7]\ncp $FF\njr nz, $05"), ASM("ld a, $09\nrst 8\nret"), fill_nop=True) + +def upgradeOverworldOwlStatues(rom): + # Replace the code that handles signs/owl statues on the overworld + # This removes a "have marin with you" special case to make some room for our custom owl handling. + rom.patch(0x00, 0x201A, ASM(""" + cp $6F + jr z, $2B + cp $D4 + jr z, $27 + ld a, [$DB73] + and a + jr z, $08 + ld a, $78 + call $237C + jp $20CF + """), ASM(""" + cp $D4 + jr z, $2B + cp $6F + jr nz, skip + + ld a, $09 + rst 8 + jp $20CF +skip: + """), fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/phone.py b/worlds/ladx/LADXR/patches/phone.py new file mode 100644 index 0000000000..f38745606c --- /dev/null +++ b/worlds/ladx/LADXR/patches/phone.py @@ -0,0 +1,60 @@ +from ..assembler import ASM + + +def patchPhone(rom): + rom.texts[0x141] = b"" + rom.texts[0x142] = b"" + rom.texts[0x143] = b"" + rom.texts[0x144] = b"" + rom.texts[0x145] = b"" + rom.texts[0x146] = b"" + rom.texts[0x147] = b"" + rom.texts[0x148] = b"" + rom.texts[0x149] = b"" + rom.texts[0x14A] = b"" + rom.texts[0x14B] = b"" + rom.texts[0x14C] = b"" + rom.texts[0x14D] = b"" + rom.texts[0x14E] = b"" + rom.texts[0x14F] = b"" + rom.texts[0x16E] = b"" + rom.texts[0x1FD] = b"" + rom.texts[0x228] = b"" + rom.texts[0x229] = b"" + rom.texts[0x22A] = b"" + rom.texts[0x240] = b"" + rom.texts[0x241] = b"" + rom.texts[0x242] = b"" + rom.texts[0x243] = b"" + rom.texts[0x244] = b"" + rom.texts[0x245] = b"" + rom.texts[0x247] = b"" + rom.texts[0x248] = b"" + rom.patch(0x06, 0x2A8F, 0x2BBC, ASM(""" + ; We use $DB6D to store which tunics we have. This is normally the Dungeon9 instrument, which does not exist. + ld a, [$DC0F] + ld hl, wCollectedTunics + inc a + + cp $01 + jr nz, notTunic1 + bit 0, [HL] + jr nz, notTunic1 + inc a +notTunic1: + + cp $02 + jr nz, notTunic2 + bit 1, [HL] + jr nz, notTunic2 + inc a +notTunic2: + + cp $03 + jr nz, noWrap + xor a +noWrap: + + ld [$DC0F], a + ret + """), fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/photographer.py b/worlds/ladx/LADXR/patches/photographer.py new file mode 100644 index 0000000000..2b377c4d89 --- /dev/null +++ b/worlds/ladx/LADXR/patches/photographer.py @@ -0,0 +1,19 @@ +from ..assembler import ASM + + +def fixPhotographer(rom): + # Allow richard photo without slime key + rom.patch(0x36, 0x3234, ASM("jr nz, $52"), "", fill_nop=True) + rom.patch(0x36, 0x3240, ASM("jr z, $46"), "", fill_nop=True) + # Allow richard photo when castle is opened + rom.patch(0x36, 0x31FF, ASM("jp nz, $7288"), "", fill_nop=True) + # Allow photographer with bowwow saved + rom.patch(0x36, 0x0398, ASM("or [hl]"), "", fill_nop=True) + rom.patch(0x36, 0x3183, ASM("ret nz"), "", fill_nop=True) + rom.patch(0x36, 0x31CB, ASM("jp nz, $7288"), "", fill_nop=True) + rom.patch(0x36, 0x03DC, ASM("and $7F"), ASM("and $00")) + # Allow bowwow photo with follower + rom.patch(0x36, 0x31DA, ASM("jp nz, $7288"), "", fill_nop=True) + # Allow bridge photo with follower + rom.patch(0x36, 0x004D, ASM("call nz, $3F8D"), "", fill_nop=True) + rom.patch(0x36, 0x006D, ASM("ret nz"), "", fill_nop=True) # Checks if any entity is alive diff --git a/worlds/ladx/LADXR/patches/reduceRNG.py b/worlds/ladx/LADXR/patches/reduceRNG.py new file mode 100644 index 0000000000..cfeb4da6e4 --- /dev/null +++ b/worlds/ladx/LADXR/patches/reduceRNG.py @@ -0,0 +1,9 @@ +from ..assembler import ASM + + +def slowdownThreeOfAKind(rom): + rom.patch(0x06, 0x096B, ASM("ldh a, [$E7]\nand $0F"), ASM("ldh a, [$E7]\nand $3F")) + + +def fixHorseHeads(rom): + rom.patch(0x07, 0x3653, "00010400", "00010000") diff --git a/worlds/ladx/LADXR/patches/rooster.py b/worlds/ladx/LADXR/patches/rooster.py new file mode 100644 index 0000000000..c8bd831c88 --- /dev/null +++ b/worlds/ladx/LADXR/patches/rooster.py @@ -0,0 +1,40 @@ +from ..assembler import ASM +from ..utils import formatText + + +def patchRooster(rom): + # Do not give the rooster + rom.patch(0x19, 0x0E9D, ASM("ld [$DB7B], a"), "", fill_nop=True) + + # Do not load the rooster sprites + rom.patch(0x00, 0x2EC7, ASM("jr nz, $08"), "", fill_nop=True) + + # Draw the found item + rom.patch(0x19, 0x0E4A, ASM("ld hl, $4E37\nld c, $03\ncall $3CE6"), ASM("ld a, $0C\nrst $08"), fill_nop=True) + rom.patch(0x19, 0x0E7B, ASM("ld hl, $4E37\nld c, $03\ncall $3CE6"), ASM("ld a, $0C\nrst $08"), fill_nop=True) + # Give the item and message + rom.patch(0x19, 0x0E69, ASM("ld a, $6D\ncall $2373"), ASM("ld a, $0E\nrst $08"), fill_nop=True) + + # Reuse unused evil eagle text slot for rooster message + rom.texts[0x0B8] = formatText("Got the {ROOSTER}!") + + # Allow rooster pickup with special rooster item + rom.patch(0x19, 0x1ABC, ASM("cp $03"), ASM("cp $0F")) + rom.patch(0x19, 0x1AAE, ASM("cp $03"), ASM("cp $0F")) + + # Ignore the has-rooster flag in the rooster entity (do not despawn) + rom.patch(0x19, 0x19E0, ASM("jp z, $7E61"), "", fill_nop=True) + + # If we are spawning the rooster, and the rooster is already existing, do not do anything, instead of despawning the rooster. + rom.patch(0x01, 0x1FEF, ASM("ld [hl], d"), ASM("ret")) + # Allow rooster to unload when changing rooms + rom.patch(0x19, 0x19E9, ASM("ld [hl], a"), "", fill_nop=True) + + # Do not take away the rooster after D7 + rom.patch(0x03, 0x1E25, ASM("ld [$DB7B], a"), "", fill_nop=True) + + # Patch the color dungeon entrance not to check for rooster + rom.patch(0x02, 0x3409, ASM("ld hl, $DB7B\nor [hl]"), "", fill_nop=True) + + # Spawn marin at taltal even with rooster + rom.patch(0x18, 0x1EE3, ASM("jp nz, $7F08"), "", fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/save.py b/worlds/ladx/LADXR/patches/save.py new file mode 100644 index 0000000000..c55d6314a4 --- /dev/null +++ b/worlds/ladx/LADXR/patches/save.py @@ -0,0 +1,54 @@ +from ..assembler import ASM +from ..backgroundEditor import BackgroundEditor + + +def singleSaveSlot(rom): + # Do not validate/erase slots 2 and 3 at rom start + rom.patch(0x01, 0x06B3, ASM("call $4794"), "", fill_nop=True) + rom.patch(0x01, 0x06B9, ASM("call $4794"), "", fill_nop=True) + + # Patch the code that checks if files have proper filenames to skip file 2/3 + rom.patch(0x01, 0x1DD9, ASM("ld b, $02"), ASM("ret"), fill_nop=True) + + # Remove the part that writes death counters for save2/3 on the file select screen + rom.patch(0x01, 0x0821, 0x084B, "", fill_nop=True) + # Remove the call that updates the hearts for save2 + rom.patch(0x01, 0x0800, ASM("call $4DBE"), "", fill_nop=True) + # Remove the call that updates the hearts for save3 + rom.patch(0x01, 0x0806, ASM("call $4DD6"), "", fill_nop=True) + + # Remove the call that updates the names for save2 and save3 + rom.patch(0x01, 0x0D70, ASM("call $4D94\ncall $4D9D"), "", fill_nop=True) + + # Remove the 2/3 slots from the screen and remove the copy text + be = BackgroundEditor(rom, 0x03) + del be.tiles[0x9924] + del be.tiles[0x9984] + be.store(rom) + be = BackgroundEditor(rom, 0x04) + del be.tiles[0x9924] + del be.tiles[0x9984] + for n in range(0x99ED, 0x99F1): + del be.tiles[n] + be.store(rom) + + # Do not do left/right for erase/copy selection. + rom.patch(0x01, 0x092B, ASM("jr z, $0B"), ASM("jr $0B")) + # Only switch between players + rom.patch(0x01, 0x08FA, 0x091D, ASM(""" + ld a, [$DBA7] + and a + ld a, [$DBA6] + jr z, skip + xor $03 +skip: + """), fill_nop=True) + + # On the erase screen, only switch between save 1 and return + rom.patch(0x01, 0x0E12, ASM("inc a\nand $03"), ASM("xor $03"), fill_nop=True) + rom.patch(0x01, 0x0E21, ASM("dec a\ncp $ff\njr nz, $02\nld a,$03"), ASM("xor $03"), fill_nop=True) + + be = BackgroundEditor(rom, 0x06) + del be.tiles[0x9924] + del be.tiles[0x9984] + be.store(rom) diff --git a/worlds/ladx/LADXR/patches/seashell.py b/worlds/ladx/LADXR/patches/seashell.py new file mode 100644 index 0000000000..7e53516d5e --- /dev/null +++ b/worlds/ladx/LADXR/patches/seashell.py @@ -0,0 +1,64 @@ +from ..assembler import ASM + + +def fixSeashell(rom): + # Do not unload if we have the lvl2 sword. + rom.patch(0x03, 0x1FD3, ASM("ld a, [$DB4E]\ncp $02\njp nc, $3F8D"), "", fill_nop=True) + # Do not unload in the ghost house + rom.patch(0x03, 0x1FE8, ASM("ldh a, [$F8]\nand $40\njp z, $3F8D"), "", fill_nop=True) + + # Call our special rendering code + rom.patch(0x03, 0x1FF2, ASM("ld de, $5FD1\ncall $3C77"), ASM("ld a, $05\nrst 8"), fill_nop=True) + + # Call our special handlers for messages and pickup + rom.patch(0x03, 0x2368, 0x237C, ASM(""" + ld a, $0A ; showMessageMultiworld + rst 8 + ld a, $06 ; giveItemMultiworld + rst 8 + call $512A + ret + """), fill_nop=True) + + +def upgradeMansion(rom): + rom.patch(0x19, 0x38EC, ASM(""" + ld hl, $78DC + jr $03 + """), "", fill_nop=True) + rom.patch(0x19, 0x38F1, ASM(""" + ld hl, $78CC + ld c, $04 + call $3CE6 + """), ASM(""" + ld a, $0C + rst 8 + """), fill_nop=True) + rom.patch(0x19, 0x3718, ASM("sub $13"), ASM("sub $0D")) + rom.patch(0x19, 0x3697, ASM(""" + cp $70 + jr c, $15 + ld [hl], $70 + """), ASM(""" + cp $73 + jr c, $15 + ld [hl], $73 + """)) + rom.patch(0x19, 0x36F5, ASM(""" + ld a, $02 + ld [$DB4E], a + """), ASM(""" + ld a, $0E ; give item and message for current room multiworld + rst 8 + """), fill_nop=True) + rom.patch(0x19, 0x36E6, ASM(""" + ld a, $9F + call $2385 + """), "", fill_nop=True) + rom.patch(0x19, 0x31E8, ASM(""" + ld a, [$DB4E] + and $02 + """), ASM(""" + ld a, [$DAE9] + and $10 + """)) diff --git a/worlds/ladx/LADXR/patches/shop.py b/worlds/ladx/LADXR/patches/shop.py new file mode 100644 index 0000000000..197fe09b18 --- /dev/null +++ b/worlds/ladx/LADXR/patches/shop.py @@ -0,0 +1,152 @@ +from ..assembler import ASM + + +def fixShop(rom): + # Move shield visuals to the 2nd slot, and arrow to 3th slot + rom.patch(0x04, 0x3732 + 22, "986A027FB2B098AC01BAB1", "9867027FB2B098A801BAB1") + rom.patch(0x04, 0x3732 + 55, "986302B1B07F98A4010A09", "986B02B1B07F98AC010A09") + + # Just use a fixed location in memory to store which inventory we give. + rom.patch(0x04, 0x37C5, "0708", "0802") + + # Patch the code that decides which shop to show. + rom.patch(0x04, 0x3839, 0x388E, ASM(""" + push bc + jr skipSubRoutine + +checkInventory: + ld hl, $DB00 ; inventory + ld c, INV_SIZE +loop: + cp [hl] + ret z + inc hl + dec c + jr nz, loop + and a + ret + +skipSubRoutine: + ; Set the shop table to all nothing. + ld hl, $C505 + xor a + ldi [hl], a + ldi [hl], a + ldi [hl], a + ldi [hl], a + ld de, $C505 + + ; Check if we want to load a key item into the shop. + ldh a, [$F8] + bit 4, a + jr nz, checkForSecondKeyItem + ld a, $01 + ld [de], a + jr checkForShield +checkForSecondKeyItem: + bit 5, a + jr nz, checkForShield + ld a, $05 + ld [de], a + +checkForShield: + inc de + ; Check if we have the shield or the bow to see if we need to remove certain entries from the shop + ld a, [$DB44] + and a + jr z, hasNoShieldLevel + ld a, $03 + ld [de], a ; Add shield buy option +hasNoShieldLevel: + + inc de + ld a, $05 + call checkInventory + jr nz, hasNoBow + ld a, $06 + ld [de], a ; Add arrow buy option +hasNoBow: + + inc de + ld a, $02 + call checkInventory + jr nz, hasNoBombs + ld a, $04 + ld [de], a ; Add bomb buy option +hasNoBombs: + + pop bc + call $3B12 ; increase entity state + """, 0x7839), fill_nop=True) + + # We do not have enough room at the shovel/bow buy entry to handle this + # So jump to a bit where we have some more space to work, as there is some dead code in the shop. + rom.patch(0x04, 0x3AA9, 0x3AAE, ASM("jp $7AC3"), fill_nop=True) + + # Patch over the "you stole it" dialog + rom.patch(0x00, 0x1A1C, 0x1A21, ASM("""ld a, $C9 + call $2385"""), fill_nop=True) + rom.patch(0x04, 0x3AC3, 0x3AD8, ASM(""" + ; No room override needed, we're in the proper room + ; Call our chest item giving code. + ld a, $0E + rst 8 + ; Update the room status to mark first item as bought + ld hl, $DAA1 + ld a, [hl] + or $10 + ld [hl], a + ret + """), fill_nop=True) + rom.patch(0x04, 0x3A73, 0x3A7E, ASM("jp $7A91"), fill_nop=True) + rom.patch(0x04, 0x3A91, 0x3AA9, ASM(""" + ; Override the room - luckily nothing will go wrong here if we leave it as is + ld a, $A7 + ldh [$F6], a + ; Call our chest item giving code. + ld a, $0E + rst 8 + ; Update the room status to mark second item as bought + ld hl, $DAA1 + ld a, [hl] + or $20 + ld [hl], a + ret + """), fill_nop=True) + + # Patch shop item graphics rendering to use some new code at the end of the bank. + rom.patch(0x04, 0x3B91, 0x3BAC, ASM(""" + call $7FD0 + """), fill_nop=True) + rom.patch(0x04, 0x3BD3, 0x3BE3, ASM(""" + jp $7FD0 + """), fill_nop=True) + rom.patch(0x04, 0x3FD0, "00" * 42, ASM(""" + ; Check if first key item + and a + jr nz, notShovel + ld a, [$77C5] + ldh [$F1], a + ld a, $01 + rst 8 + ret +notShovel: + cp $04 + jr nz, notBow + ld a, [$77C6] + ldh [$F1], a + ld a, $01 + rst 8 + ret +notBow: + cp $05 + jr nz, notArrows + ; Load arrow graphics and render then as a dual sprite + ld de, $7B58 + call $3BC0 + ret +notArrows: + ; Load the normal graphics + ld de, $7B5A + jp $3C77 + """), fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/softlock.py b/worlds/ladx/LADXR/patches/softlock.py new file mode 100644 index 0000000000..15a9119538 --- /dev/null +++ b/worlds/ladx/LADXR/patches/softlock.py @@ -0,0 +1,93 @@ +from ..roomEditor import RoomEditor, Object +from ..assembler import ASM + + +def fixAll(rom): + # Prevent soft locking in the first mountain cave if we do not have a feather + re = RoomEditor(rom, 0x2B7) + re.removeObject(3, 3) + re.store(rom) + + # Prevent getting stuck in the sidescroll room in the beginning of dungeon 5 + re = RoomEditor(rom, 0x1A9) + re.objects[6].count = 7 + re.store(rom) + + # Cave that allows you to escape from D4 without flippers, make it no longer require a feather + re = RoomEditor(rom, 0x1EA) + re.objects[9].count = 8 + re.removeObject(5, 4) + re.moveObject(4, 4, 7, 5) + re.store(rom) + + # D3 west side room requires feather to get the key. But feather is not required to unlock the door, potentially softlocking you. + re = RoomEditor(rom, 0x155) + re.changeObject(4, 1, 0xcf) + re.changeObject(4, 6, 0xd0) + re.store(rom) + + # D3 boots room requires boots to escape + re = RoomEditor(rom, 0x146) + re.removeObject(5, 6) + re.store(rom) + + allowRaftGameWithoutFlippers(rom) + # We cannot access thes holes in logic: + # removeBirdKeyHoleDrop(rom) + fixDoghouse(rom) + flameThrowerShieldRequirement(rom) + fixLessThen3MaxHealth(rom) + +def fixDoghouse(rom): + # Fix entering the dog house from the back, and ending up out of bounds. + re = RoomEditor(rom, 0x0A1) + re.objects.append(Object(6, 2, 0x0E2)) + re.objects.append(re.objects[20]) # Move the flower patch after the warp entry definition so it overrules the tile + re.objects.append(re.objects[3]) + + re.objects.pop(22) + re.objects.pop(21) + re.objects.pop(20) # Remove the flower patch at the normal entry index + re.objects.pop(11) # Duplicate object, we can just remove it, gives room for our custom entry door + re.store(rom) + +def allowRaftGameWithoutFlippers(rom): + # Allow jumping down the waterfall in the raft game without the flippers. + rom.patch(0x02, 0x2E8F, ASM("ld a, [$DB0C]"), ASM("ld a, $01"), fill_nop=True) + # Change the room that goes back up to the raft game from the bottom, so we no longer need flippers + re = RoomEditor(rom, 0x1F7) + re.changeObject(3, 2, 0x1B) + re.changeObject(2, 3, 0x1B) + re.changeObject(3, 4, 0x1B) + re.changeObject(4, 5, 0x1B) + re.changeObject(6, 6, 0x1B) + re.store(rom) + +def removeBirdKeyHoleDrop(rom): + # Prevent the cave with the bird key from dropping you in the water + # (if you do not have flippers this would softlock you) + rom.patch(0x02, 0x1176, ASM(""" + ldh a, [$F7] + cp $0A + jr nz, $30 + """), ASM(""" + nop + nop + nop + nop + jr $30 + """)) + # Remove the hole that drops you all the way from dungeon7 entrance to the water in the cave + re = RoomEditor(rom, 0x01E) + re.removeObject(5, 4) + re.store(rom) + +def flameThrowerShieldRequirement(rom): + # if you somehow get a lvl3 shield or higher, it no longer works against the flamethrower, easy fix. + rom.patch(0x03, 0x2EBA, + ASM("ld a, [$DB44]\ncp $02\nret nz"), # if not shield level 2 + ASM("ld a, [$DB44]\ncp $02\nret c")) # if not shield level 2 or higher + +def fixLessThen3MaxHealth(rom): + # The table that starts your start HP when you die is not working for less then 3 HP, and locks the game. + rom.patch(0x01, 0x1295, "18181818", "08081018") diff --git a/worlds/ladx/LADXR/patches/songs.py b/worlds/ladx/LADXR/patches/songs.py new file mode 100644 index 0000000000..59ca01c4c8 --- /dev/null +++ b/worlds/ladx/LADXR/patches/songs.py @@ -0,0 +1,159 @@ +from ..assembler import ASM + + +def upgradeMarin(rom): + # Show marin outside, even without a sword. + rom.patch(0x05, 0x0E78, ASM("ld a, [$DB4E]"), ASM("ld a, $01"), fill_nop=True) + # Make marin ignore the fact that you did not save the tarin yet, and allowing getting her song + rom.patch(0x05, 0x0E87, ASM("ld a, [$D808]"), ASM("ld a, $10"), fill_nop=True) + rom.patch(0x05, 0x0F73, ASM("ld a, [$D808]"), ASM("ld a, $10"), fill_nop=True) + rom.patch(0x05, 0x0FB0, ASM("ld a, [$DB48]"), ASM("ld a, $01"), fill_nop=True) + # Show marin in the animal village + rom.patch(0x03, 0x0A86, ASM("ld a, [$DB74]"), ASM("ld a, $01"), fill_nop=True) + rom.patch(0x05, 0x3F2E, ASM("ld a, [$DB74]"), ASM("ld a, $01"), fill_nop=True) # animal d0 + rom.patch(0x15, 0x3F96, ASM("ld a, [$DB74]"), ASM("ld a, $01"), fill_nop=True) # animal d1 + rom.patch(0x18, 0x11B0, ASM("ld a, [$DB74]"), ASM("ld a, $01"), fill_nop=True) # animal d2 + + # Instead of checking if we have the ballad, check if we have a specific room flag set + rom.patch(0x05, 0x0F89, ASM(""" + ld a, [$DB49] + and $04 + """), ASM(""" + ld a, [$D892] + and $10 + """), fill_nop=True) + rom.patch(0x05, 0x0FDF, ASM(""" + ld a, [$DB49] + and $04 + """), ASM(""" + ld a, [$D892] + and $10 + """), fill_nop=True) + rom.patch(0x05, 0x1042, ASM(""" + ld a, [$DB49] + and $04 + """), ASM(""" + ld a, [$D892] + and $10 + """), fill_nop=True) + + # Patch that we call our specific handler instead of giving the song + rom.patch(0x05, 0x1170, ASM(""" + ld hl, $DB49 + set 2, [hl] + xor a + ld [$DB4A], a + """), ASM(""" + ; Mark Marin as done. + ld a, [$D892] + or $10 + ld [$D892], a + """), fill_nop=True) + + + # Show the right item instead of the ocerina + rom.patch(0x05, 0x11B3, ASM(""" + ld de, $515F + xor a + ldh [$F1], a + jp $3C77 + """), ASM(""" + ld a, $0C + rst 8 + ret + """), fill_nop=True) + + # Patch the message that tells we got the song, to give the item and show the right message + rom.patch(0x05, 0x119C, ASM(""" + ld a, $13 + call $2385 + """), ASM(""" + ld a, $0E + rst 8 + """), fill_nop=True) + + +def upgradeManbo(rom): + # Instead of checking if we have the song, check if we have a specific room flag set + rom.patch(0x18, 0x0536, ASM(""" + ld a, [$DB49] + and $02 + """), ASM(""" + ld a, [$DAFD] + and $20 + """), fill_nop=True) + + # Show the right item instead of the ocerina + rom.patch(0x18, 0x0786, ASM(""" + ld de, $474D + xor a + ldh [$F1], a + jp $3C77 + """), ASM(""" + ld a, $0C + rst 8 + ret + """), fill_nop=True) + + # Patch to replace song giving to give the right item + rom.patch(0x18, 0x0757, ASM(""" + ld a, $01 + ld [$DB4A], a + ld hl, $DB49 + set 1, [hl] + """), ASM(""" + ; Mark Manbo as done. + ld hl, $DAFD + set 5, [hl] + ; Show item message and give item + ld a, $0E + rst 8 + """), fill_nop=True) + # Remove the normal "got song message") + rom.patch(0x18, 0x076F, 0x0774, "", fill_nop=True) + +def upgradeMamu(rom): + # Always allow the sign maze instead of only allowing the sign maze if you do not have song3 + rom.patch(0x00, 0x2057, ASM("ld a, [$DB49]"), ASM("ld a, $00"), fill_nop=True) + + # Patch the condition at which Mamu gives you the option to listen to him + rom.patch(0x18, 0x0031, ASM(""" + ld a, [$DB49] + and $01 + """), ASM(""" + ld a, [$DAFB] ; load room flag of the Mamu room + and $10 + """), fill_nop=True) + + # Show the right item instead of the ocerina + rom.patch(0x18, 0x0299, ASM(""" + ld de, $474D + xor a + ldh [$F1], a + call $3C77 + """), ASM(""" + ld a, $0C + rst 8 + """), fill_nop=True) + + # Patch given an item + rom.patch(0x18, 0x0270, ASM(""" + ld a, $02 + ld [$DB4A], a + ld hl, $DB49 + set 0, [hl] + """), ASM(""" + ; Set the room complete flag. + ld hl, $DAFB + set 4, [hl] + """), fill_nop=True) + + # Patch to show the right message for the item + rom.patch(0x18, 0x0282, ASM(""" + ld a, $DF + call $4087 + """), ASM(""" + ; Give item and message for room. + ld a, $0E + rst 8 + """), fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/tarin.py b/worlds/ladx/LADXR/patches/tarin.py new file mode 100644 index 0000000000..d84935634e --- /dev/null +++ b/worlds/ladx/LADXR/patches/tarin.py @@ -0,0 +1,51 @@ +from ..assembler import ASM +from ..utils import formatText + + +def updateTarin(rom): + # Do not give the shield. + rom.patch(0x05, 0x0CD0, ASM(""" + ld d, $04 + call $5321 + ld a, $01 + ld [$DB44], a + """), "", fill_nop=True) + + # Instead of showing the usual "your shield back" message, give the proper message and give the item. + rom.patch(0x05, 0x0CDE, ASM(""" + ld a, $91 + call $2385 + """), ASM(""" + ld a, $0B ; GiveItemAndMessageForRoom + rst 8 + """), fill_nop=True) + + rom.patch(0x05, 0x0CF0, ASM(""" + xor a + ldh [$F1], a + ld de, $4CC6 + call $3C77 + """), ASM(""" + ld a, $0C ; RenderItemForRoom + rst 8 + xor a + ldh [$F1], a + """), fill_nop=True) + + # Set the room status to finished. (replaces a GBC check) + rom.patch(0x05, 0x0CAB, 0x0CB0, ASM(""" + ld a, $20 + call $36C4 + """), fill_nop=True) + + # Instead of checking for the shield level to put you in the bed, check the room flag. + rom.patch(0x05, 0x1202, ASM("ld a, [$DB44]\nand a"), ASM("ldh a, [$F8]\nand $20")) + rom.patch(0x05, 0x0C6D, ASM("ld a, [$DB44]\nand a"), ASM("ldh a, [$F8]\nand $20")) + + # If the starting item is picked up, load the right palette when entering the room + rom.patch(0x21, 0x0176, ASM("ld a, [$DB48]\ncp $01"), ASM("ld a, [$DAA3]\ncp $A1"), fill_nop=True) + rom.patch(0x05, 0x0C94, "FF473152C5280000", "FD2ED911CE100000") + rom.patch(0x05, 0x0CB0, ASM("ld hl, $DC88"), ASM("ld hl, $DC80")) + + # Patch the text that Tarin uses to give your shield back. + rom.texts[0x54] = formatText("#####, it is dangerous to go alone!\nTake this!") diff --git a/worlds/ladx/LADXR/patches/titleScreen.py b/worlds/ladx/LADXR/patches/titleScreen.py new file mode 100644 index 0000000000..52adba6813 --- /dev/null +++ b/worlds/ladx/LADXR/patches/titleScreen.py @@ -0,0 +1,89 @@ +from ..backgroundEditor import BackgroundEditor +import subprocess +import binascii + + +CHAR_MAP = {'z': 0x3E, '-': 0x3F, '.': 0x39, ':': 0x42, '?': 0x3C, '!': 0x3D} + + +def _encode(s): + result = bytearray() + for char in s: + if ord("A") <= ord(char) <= ord("Z"): + result.append(ord(char) - ord("A")) + elif ord("a") <= ord(char) <= ord("y"): + result.append(ord(char) - ord("a") + 26) + elif ord("0") <= ord(char) <= ord("9"): + result.append(ord(char) - ord("0") + 0x70) + else: + result.append(CHAR_MAP.get(char, 0x7E)) + return result + + +def setRomInfo(rom, seed, settings, player_name, player_id): + #try: + # version = subprocess.run(['git', 'describe', '--tags', '--dirty=-D'], stdout=subprocess.PIPE).stdout.strip().decode("ascii", "replace") + #except: + # version = "" + + try: + seednr = int(seed, 16) + except: + import hashlib + seednr = int(hashlib.md5(seed.encode('ascii', 'replace')).hexdigest(), 16) + + if settings.race: + seed = "Race" + if isinstance(settings.race, str): + seed += " " + settings.race + rom.patch(0x00, 0x07, "00", "01") + else: + rom.patch(0x00, 0x07, "00", "52") + + line_1_hex = _encode(seed) + #line_2_hex = _encode(seed[16:]) + BASE_DRAWING_AREA = 0x98a0 + LINE_WIDTH = 0x20 + player_id_text = f"Player {player_id}:" + for n in (3, 4): + be = BackgroundEditor(rom, n) + ba = BackgroundEditor(rom, n, attributes=True) + + for n, v in enumerate(_encode(player_id_text)): + be.tiles[BASE_DRAWING_AREA + LINE_WIDTH * 5 + 2 + n] = v + ba.tiles[BASE_DRAWING_AREA + LINE_WIDTH * 5 + 2 + n] = 0x00 + for n, v in enumerate(_encode(player_name)): + be.tiles[BASE_DRAWING_AREA + LINE_WIDTH * 6 + 0x13 - len(player_name) + n] = v + ba.tiles[BASE_DRAWING_AREA + LINE_WIDTH * 6 + 0x13 - len(player_name) + n] = 0x00 + for n, v in enumerate(line_1_hex): + be.tiles[0x9a20 + n] = v + ba.tiles[0x9a20 + n] = 0x00 + + for n in range(0x09, 0x14): + be.tiles[0x9820 + n] = 0x7F + be.tiles[0x9840 + n] = 0xA0 + (n % 2) + be.tiles[0x9860 + n] = 0xA2 + sn = seednr + for n in range(0x0A, 0x14): + tilenr = sn % 30 + sn //= 30 + if tilenr > 12: + tilenr += 2 + if tilenr > 16: + tilenr += 1 + if tilenr > 19: + tilenr += 3 + if tilenr > 27: + tilenr += 1 + if tilenr > 29: + tilenr += 2 + if tilenr > 35: + tilenr += 1 + be.tiles[0x9800 + n] = tilenr * 2 + be.tiles[0x9820 + n] = tilenr * 2 + 1 + pal = sn % 8 + sn //= 8 + ba.tiles[0x9800 + n] = 0x08 | pal + ba.tiles[0x9820 + n] = 0x08 | pal + be.store(rom) + ba.store(rom) diff --git a/worlds/ladx/LADXR/patches/tradeSequence.py b/worlds/ladx/LADXR/patches/tradeSequence.py new file mode 100644 index 0000000000..5b608977f2 --- /dev/null +++ b/worlds/ladx/LADXR/patches/tradeSequence.py @@ -0,0 +1,355 @@ +from ..assembler import ASM + + +def patchTradeSequence(rom, boomerang_option): + patchTrendy(rom) + patchPapahlsWife(rom) + patchYipYip(rom) + patchBananasSchule(rom) + patchKiki(rom) + patchTarin(rom) + patchBear(rom) + patchPapahl(rom) + patchGoatMrWrite(rom) + patchGrandmaUlrira(rom) + patchFisherman(rom) + patchMermaid(rom) + patchMermaidStatue(rom) + patchSharedCode(rom) + patchVarious(rom, boomerang_option) + patchInventoryMenu(rom) + + +def patchTrendy(rom): + # Trendy game yoshi + rom.patch(0x04, 0x3502, 0x350F, ASM(""" + ldh a, [$F8] ; room status + and a, $20 + jp nz, $6D7A ; clear entity + ; Render sprite + ld a, $0F + rst 8 + ; Reset the sprite variant, else the code gets confused + xor a + ldh [$F1], a ; sprite variant + """), fill_nop=True) + rom.patch(0x04, 0x2E80, ASM("ldh a, [$F8]"), ASM("ld a, $10")) # Prevent marin cutscene from triggering, as that locks the game now. + rom.patch(0x04, 0x3622, 0x3627, "", fill_nop=True) # Dont set the trade item + + +def patchPapahlsWife(rom): + # Rewrite how the first dialog is generated. + rom.patch(0x18, 0x0E7A, 0x0EA8, ASM(""" + ldh a, [$F8] ; room status + and a, $20 + jr nz, tradeDone + + ld a, [wTradeSequenceItem] + and $01 + jr nz, requestTrade + + ld a, $2A ; Dialog about wanting a yoshi doll + jp $2373 ; OpenDialogInTable1 +tradeDone: + ld a, $2C ; Dialog about kids, after trade is done + jp $2373 ; OpenDialogInTable1 +requestTrade: + ld a, $2B ; Dialog about kids, after trade is done + call $3B12; IncrementEntityState + jp $2373 ; OpenDialogInTable1 + """), fill_nop=True) + rom.patch(0x18, 0x0EB4, 0x0EBD, ASM("ld hl, wTradeSequenceItem\nres 0, [hl]"), fill_nop=True) # Take the trade item + + +def patchYipYip(rom): + # Change how the decision is made to draw yipyip with a ribbon + rom.patch(0x06, 0x1A2C, 0x1A36, ASM(""" + ldh a, [$F8] ; room status + and $20 + jr z, tradeNotDone + ld de, $59C8 ; yipyip with ribbon +tradeNotDone: + """), fill_nop=True) + # Check if we have the ribbon + rom.patch(0x06, 0x1A7C, 0x1A83, ASM(""" + ld a, [wTradeSequenceItem] + and $02 + jr z, $07 + """), fill_nop=True) + rom.patch(0x06, 0x1AAF, 0x1AB8, ASM("ld hl, wTradeSequenceItem\nres 1, [hl]"), fill_nop=True) # Take the trade item + + +def patchBananasSchule(rom): + # Change how to check if we have the right trade item + rom.patch(0x19, 0x2D54, 0x2D5B, ASM(""" + ld a, [wTradeSequenceItem] + and $04 + jr z, $08 + """), fill_nop=True) + rom.patch(0x19, 0x2DF0, 0x2DF9, ASM("ld hl, wTradeSequenceItem\nres 2, [hl]"), fill_nop=True) # Take the trade item + # Change how the decision is made to render less bananas + rom.patch(0x19, 0x2EF1, 0x2EFA, ASM(""" + ldh a, [$F8] + and $20 + jr z, skip + dec c + dec c +skip: """), fill_nop=True) + + # Part of the same entity code, but this is the painter, which changes the dialog depending on mermaid scale or magnifier + rom.patch(0x19, 0x2F95, 0x2F9C, ASM(""" + ld a, [wTradeSequenceItem2] + and $10 ; Check for mermaid scale + jr z, $04 + """)) + rom.patch(0x19, 0x2FA0, 0x2FA4, ASM(""" + and $20 ; Check for magnifier + jr z, $07 + """)) + rom.patch(0x19, 0x2CE3, "9A159C15", "B41DB61D") # Properly draw the dog food + + +def patchKiki(rom): + rom.patch(0x07, 0x18E6, 0x18ED, ASM(""" + ld a, [wTradeSequenceItem] + and $08 ; check for banana + jr z, $08 + """)) + rom.patch(0x07, 0x19AF, 0x19B4, "", fill_nop=True) # Do not change trading item memory + rom.patch(0x07, 0x19CC, 0x19D5, ASM("ld hl, wTradeSequenceItem\nres 3, [hl]"), fill_nop=True) # Take the trade item + rom.patch(0x07, 0x194D, "9A179C17", "B81FBA1F") # Properly draw the banana above kiki + + +def patchTarin(rom): + rom.patch(0x07, 0x0EC5, 0x0ECA, ASM(""" + ld a, [wTradeSequenceItem] + and $10 ; check for stick + """)) + rom.patch(0x07, 0x0F30, 0x0F33, "", fill_nop=True) # Take the trade item + # Honeycomb, change how we detect that it should fall on entering the room + rom.patch(0x07, 0x0CCC, 0x0CD3, ASM(""" + ld a, [$D887] + and $40 + jr z, $14 + """)) + # Something about tarin changing messages or not showing up depending on the trade sequence + rom.patch(0x05, 0x0BFF, 0x0C07, "", fill_nop=True) # Just ignore the trade sequence + rom.patch(0x05, 0x0D20, 0x0D27, "", fill_nop=True) # Just ignore the trade sequence + rom.patch(0x05, 0x0DAF, 0x0DB8, "", fill_nop=True) # Tarin giving bananas? + + rom.patch(0x07, 0x0D6D, 0x0D7A, ASM("ld hl, wTradeSequenceItem\nres 4, [hl]"), fill_nop=True) # Take the trade item + + +def patchBear(rom): + # Change the trade item check + rom.patch(0x07, 0x0BCC, 0x0BD3, ASM(""" + ld a, [wTradeSequenceItem] + and $20 ; check for honeycomb + jr z, $0E + """)) + rom.patch(0x07, 0x0C21, ASM("jr nz, $22"), "", fill_nop=True) + rom.patch(0x07, 0x0C23, 0x0C2A, ASM(""" + ld a, [wTradeSequenceItem] + and $20 ; check for honeycomb + jr z, $08 + """)) + + rom.patch(0x07, 0x0C3C, 0x0C43, ASM(""" + nop + nop + nop + nop + nop + jr $02 + """)) + rom.patch(0x07, 0x0C5E, 0x0C67, ASM("ld hl, wTradeSequenceItem\nres 5, [hl]"), fill_nop=True) # Take the trade item + + +def patchPapahl(rom): + rom.patch(0x07, 0x0A21, 0x0A30, ASM("call $7EA4"), fill_nop=True) # Never show indoor papahl + # Render the bag condition + rom.patch(0x07, 0x0A81, 0x0A88, ASM(""" + ldh a, [$F8] ; current room status + and $20 + nop + jr nz, $18 + """)) + # Check for the right item + rom.patch(0x07, 0x0ACF, 0x0AD4, ASM(""" + ld a, [wTradeSequenceItem] + and $40 ; pineapple + """)) + rom.patch(0x07, 0x0AD6, ASM("jr z, $02"), ASM("jr nz, $02")) + + rom.patch(0x07, 0x0AF9, 0x0B00, ASM(""" + ld a, [wTradeSequenceItem] + and $40 ; pineapple + jr z, $0E + """)) + rom.patch(0x07, 0x0B2F, 0x0B38, ASM("ld hl, wTradeSequenceItem\nres 6, [hl]"), fill_nop=True) # Take the trade item + + +def patchGoatMrWrite(rom): # The goat and mrwrite are the same entity + rom.patch(0x18, 0x0BF1, 0x0BF8, ASM(""" + ldh a, [$F8] + and $20 + nop + jr nz, $03 + """)) # Check if we made the trade with the goat + rom.patch(0x18, 0x0C2C, 0x0C33, ASM(""" + ld a, [wTradeSequenceItem] + and $80 ; hibiscus + jr z, $08 + """)) # Check if we have the hibiscus + rom.patch(0x18, 0x0C3D, 0x0C41, "", fill_nop=True) + rom.patch(0x18, 0x0C6B, 0x0C74, ASM("ld hl, wTradeSequenceItem\nres 7, [hl]"), fill_nop=True) # Take the trade item for the goat + + rom.patch(0x18, 0x0C8B, 0x0C92, ASM(""" + ld a, [wTradeSequenceItem2] + and $01 ; letter + jr z, $08 + """)) # Check if we have the letter + rom.patch(0x18, 0x0C9C, 0x0CA0, "", fill_nop=True) + rom.patch(0x18, 0x0CE2, 0x0CEB, ASM("ld hl, wTradeSequenceItem2\nres 0, [hl]"), fill_nop=True) # Take the trade item for mrwrite + + +def patchGrandmaUlrira(rom): + rom.patch(0x18, 0x0D2C, ASM("jr z, $02"), "", fill_nop=True) # Always show up in animal village + rom.patch(0x18, 0x0D3C, 0x0D51, ASM(""" + ldh a, [$F8] + and $20 + jp nz, $4D58 + """), fill_nop=True) + rom.patch(0x18, 0x0D95, 0x0D9A, "", fill_nop=True) + rom.patch(0x18, 0x0D9C, 0x0DA0, "", fill_nop=True) + rom.patch(0x18, 0x0DA3, 0x0DAA, ASM(""" + ld a, [wTradeSequenceItem2] + and $02 ; broom + jr z, $0B + """)) + rom.patch(0x18, 0x0DC4, 0x0DC7, "", fill_nop=True) + rom.patch(0x18, 0x0DE2, 0x0DEB, ASM("ld hl, wTradeSequenceItem2\nres 1, [hl]"), fill_nop=True) # Take the trade item + rom.patch(0x18, 0x0E1D, 0x0E20, "", fill_nop=True) + rom.patch(0x18, 0x0D13, "9A149C14", "D01CD21C") + + +def patchFisherman(rom): + # Not sure what this first check is for + rom.patch(0x07, 0x02F8, 0x0300, ASM(""" + """), fill_nop=True) + # Check for the hook + rom.patch(0x07, 0x04BF, 0x04C6, ASM(""" + ld a, [wTradeSequenceItem2] + and $04 ; hook + jr z, $08 + """)) + rom.patch(0x07, 0x04F3, 0x04F6, "", fill_nop=True) + rom.patch(0x07, 0x057D, 0x0586, ASM("ld hl, wTradeSequenceItem2\nres 2, [hl]"), fill_nop=True) # Take the trade item + rom.patch(0x04, 0x1F88, 0x1F8B, "", fill_nop=True) + + +def patchMermaid(rom): + # Check for the right trade item + rom.patch(0x07, 0x0797, 0x079E, ASM(""" + ld a, [wTradeSequenceItem2] + and $08 ; necklace + jr z, $0B + """)) + rom.patch(0x07, 0x0854, 0x085B, ASM("ld hl, wTradeSequenceItem2\nres 3, [hl]"), fill_nop=True) # Take the trade item + + +def patchMermaidStatue(rom): + rom.patch(0x18, 0x095D, 0x0962, "", fill_nop=True) + rom.patch(0x18, 0x0966, 0x097A, ASM(""" + ld a, [wTradeSequenceItem2] + and $10 ; scale + ret z + ldh a, [$F8] + and $20 + ret nz + """), fill_nop=True) + + +def patchSharedCode(rom): + # Trade item render code override. + rom.patch(0x07, 0x1535, 0x1575, ASM(""" + ldh a, [$F9] + and a + jr z, notSideScroll + + ldh a, [$EC]; hActiveEntityVisualPosY + add a, $02 + ldh [$EC], a +notSideScroll: + ; Render sprite + ld a, $0F + rst 8 + """), fill_nop=True) + # Trade item message code + # rom.patch(0x07, 0x159F, 0x15B9, ASM(""" + # ld a, $09 ; give message and item (from alt item table) + # rst 8 + # """), fill_nop=True) + rom.patch(0x07, 0x159F, 0x15B9, ASM(""" + ldh a, [$F6] ; map room + cp $B2 + jr nz, NotYipYip + add a, 2 ; Add 2 to room to set room pointer to an empty room for trade items + ldh [$F6], a + ld a, $0e ; giveItemMultiworld + rst 8 + ldh a, [$F6] ; map room + sub a, 2 ; ...and undo it + ldh [$F6], a + jr Done + NotYipYip: + ld a, $0e ; giveItemMultiworld + rst 8 + Done: + """), fill_nop=True) + + + # Prevent changing the 2nd trade item memory + rom.patch(0x07, 0x15BD, 0x15C1, ASM(""" + call $7F7F + xor a ; we need to exit with A=00 + """), fill_nop=True) + rom.patch(0x07, 0x3F7F, "00" * 7, ASM("ldh a, [$F8]\nor $20\nldh [$F8], a\nret")) + + +def patchVarious(rom, boomerang_option): + # Make the zora photo work with the magnifier + rom.patch(0x18, 0x09F3, 0x0A02, ASM(""" + ld a, [wTradeSequenceItem2] + and $20 ; MAGNIFYING_GLASS + jp z, $7F08 ; ClearEntityStatusBank18 + """), fill_nop=True) + rom.patch(0x03, 0x0B6D, 0x0B75, ASM(""" + ld a, [wTradeSequenceItem2] + and $20 ; MAGNIFYING_GLASS + jp z, $3F8D ; UnloadEntity + """), fill_nop=True) + # Mimic invisibility + rom.patch(0x18, 0x2AC8, 0x2ACE, "", fill_nop=True) + # Ignore trade quest state for marin at beach + rom.patch(0x18, 0x219E, 0x21A6, "", fill_nop=True) + # Shift the magnifier 8 pixels + rom.patch(0x03, 0x0F68, 0x0F6F, ASM(""" + ldh a, [$F6] ; map room + cp $97 ; check if we are in the maginfier room + jp z, $4F83 + """), fill_nop=True) + # Something with the photographer + rom.patch(0x36, 0x0948, 0x0950, "", fill_nop=True) + + if boomerang_option not in {'trade', 'gift'}: # Boomerang cave is not patched, so adjust it + rom.patch(0x19, 0x05EC, ASM("ld a, [wTradeSequenceItem]\ncp $0E\njp nz, $7E61"), ASM("ld a, [wTradeSequenceItem2]\nand $20\njp z, $7E61")) # show the guy + rom.patch(0x00, 0x3199, ASM("ld a, [wTradeSequenceItem]\ncp $0E\njr nz, $06"), ASM("ld a, [wTradeSequenceItem2]\nand $20\njr z, $06")) # load the proper room layout + rom.patch(0x19, 0x05F4, 0x05FB, "", fill_nop=True) + + +def patchInventoryMenu(rom): + # Never draw the trade item the normal way + rom.patch(0x20, 0x1A2E, ASM("ld a, [wTradeSequenceItem2]\nand a\njr nz, $23"), ASM("jp $5A57"), fill_nop=True) + + rom.patch(0x20, 0x1EB5, ASM("ldh a, [$FE]\nand a\njr z, $34"), ASM("ld a, $10\nrst 8"), fill_nop=True) diff --git a/worlds/ladx/LADXR/patches/trendy.py b/worlds/ladx/LADXR/patches/trendy.py new file mode 100644 index 0000000000..21118274fc --- /dev/null +++ b/worlds/ladx/LADXR/patches/trendy.py @@ -0,0 +1,3 @@ + +def fixTrendy(rom): + rom.patch(0x04, 0x2F29, "04", "02") # Patch the trendy game shield to be a ruppee diff --git a/worlds/ladx/LADXR/patches/tunicFairy.py b/worlds/ladx/LADXR/patches/tunicFairy.py new file mode 100644 index 0000000000..d61f634087 --- /dev/null +++ b/worlds/ladx/LADXR/patches/tunicFairy.py @@ -0,0 +1,45 @@ +from ..utils import formatText +from ..assembler import ASM + + +def upgradeTunicFairy(rom): + rom.texts[0x268] = formatText("Welcome, #####. I admire you for coming this far.") + rom.texts[0x0CC] = formatText("Got the {RED_TUNIC}! You can change Tunics at the phone booths.") + rom.texts[0x0CD] = formatText("Got the {BLUE_TUNIC}! You can change Tunics at the phone booths.") + + rom.patch(0x36, 0x111C, 0x1133, ASM(""" + call $3B12 + ld a, [$DDE1] + and $10 + jr z, giveItems + ld [hl], $09 + ret + +giveItems: + ld a, [$DDE1] + or $10 + ld [$DDE1], a + """), fill_nop=True) + rom.patch(0x36, 0x1139, 0x1144, ASM(""" + ld a, $04 + ldh [$F6], a + ld a, $0E + rst 8 + """), fill_nop=True) + + rom.patch(0x36, 0x1162, 0x1192, ASM(""" + ld a, $01 + ldh [$F6], a + ld a, $0E + rst 8 + """), fill_nop=True) + + rom.patch(0x36, 0x119D, 0x11A2, "", fill_nop=True) + rom.patch(0x36, 0x11B5, 0x11BE, ASM(""" + ; Skip to the end ignoring all the tunic giving animation. + call $3B12 + ld [hl], $09 + """), fill_nop=True) + + rom.banks[0x36][0x11BF] = 0x87 + rom.banks[0x36][0x11C0] = 0x88 diff --git a/worlds/ladx/LADXR/patches/weapons.py b/worlds/ladx/LADXR/patches/weapons.py new file mode 100644 index 0000000000..9c949934c9 --- /dev/null +++ b/worlds/ladx/LADXR/patches/weapons.py @@ -0,0 +1,64 @@ +from ..assembler import ASM +from ..roomEditor import RoomEditor + + +def patchSuperWeapons(rom): + # Feather jump height + rom.patch(0x00, 0x1508, ASM("ld a, $20"), ASM("ld a, $2C")) + # Boots charge speed + rom.patch(0x00, 0x1731, ASM("cp $20"), ASM("cp $01")) + # Power bracelet pickup speed + rom.patch(0x00, 0x2121, ASM("ld e, $08"), ASM("ld e, $01")) + # Throwing speed (of pickups and bombs) + rom.patch(0x14, 0x1313, "30D0000018E80000", "60A0000040C00000") + rom.patch(0x14, 0x1323, "0000D0300000E818", "0000A0600000C040") + + # Allow as many bombs to be placed as you want! + rom.patch(0x00, 0x135F, ASM("ret nc"), "", fill_nop=True) + + # Maximum amount of arrows in the air + rom.patch(0x00, 0x13C5, ASM("cp $02"), ASM("cp $05")) + # Delay between arrow shots + rom.patch(0x00, 0x13C9, ASM("ld a, $10"), ASM("ld a, $01")) + + # Maximum amount of firerod fires + rom.patch(0x00, 0x12E4, ASM("cp $02"), ASM("cp $05")) + + # Projectile speed (arrows, firerod) + rom.patch(0x00, 0x13AD, + "30D0000040C00000" "0000D0300000C040", + "60A0000060A00000" "0000A0600000A060") + + # Hookshot shoot speed + rom.patch(0x02, 0x024C, + "30D00000" "0000D030", + "60A00000" "0000A060") + # Hookshot retract speed + rom.patch(0x18, 0x3C41, ASM("ld a, $30"), ASM("ld a, $60")) + # Hookshot pull speed + rom.patch(0x18, 0x3C21, ASM("ld a, $30"), ASM("ld a, $60")) + + # Super shovel, always price! + rom.patch(0x02, 0x0CC6, ASM("jr nz, $57"), "", fill_nop=True) + + # Unlimited boomerangs! + rom.patch(0x00, 0x1387, ASM("ret nz"), "", fill_nop=True) + + # Increase shield push power + rom.patch(0x03, 0x2FC5, ASM("ld a, $08"), ASM("ld a, $10")) + rom.patch(0x03, 0x2FCA, ASM("ld a, $20"), ASM("ld a, $40")) + # Decrease link pushback of shield + rom.patch(0x03, 0x2FB9, ASM("ld a, $12"), ASM("ld a, $04")) + rom.patch(0x03, 0x2F9A, ASM("ld a, $0C"), ASM("ld a, $03")) + + # Super charge the ocarina + rom.patch(0x02, 0x0AD8, ASM("cp $38"), ASM("cp $08")) + rom.patch(0x02, 0x0B05, ASM("cp $14"), ASM("cp $04")) + + re = RoomEditor(rom, 0x23D) + tiles = re.getTileArray() + tiles[11] = 0x0D + tiles[12] = 0xA7 + tiles[22] = 0x98 + re.buildObjectList(tiles) + re.store(rom) \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/witch.py b/worlds/ladx/LADXR/patches/witch.py new file mode 100644 index 0000000000..d87190eedd --- /dev/null +++ b/worlds/ladx/LADXR/patches/witch.py @@ -0,0 +1,58 @@ +from ..assembler import ASM +from ..roomEditor import RoomEditor + + +def updateWitch(rom): + # Add a heartpiece at the toadstool, the item patches turn this into a 1 time toadstool item + # Or depending on flags, in something else. + re = RoomEditor(rom, 0x050) + re.addEntity(2, 3, 0x35) + re.store(rom) + + # Change what happens when you trade the toadstool with the witch + # Note that the 2nd byte of this code gets patched with the item to give from the witch. + rom.patch(0x05, 0x08D4, 0x08F0, ASM(""" + ; Get the room flags and mark the witch as done. + ld hl, $DAA2 + ld a, [hl] + and $30 + set 4, [hl] + set 5, [hl] + jr z, item +powder: + ld e, $09 ; give powder every time after the first time. + ld a, e + ldh [$F1], a + ld a, $11 + rst 8 + jp $48F0 +item: + ld a, $0E + rst 8 + """), fill_nop=True) + + # Patch the toadstool to unload when you haven't delivered something to the witch yet. + rom.patch(0x03, 0x1D4B, ASM(""" + ld hl, $DB4B + ld a, [$DB4C] + or [hl] + jp nz, $3F8D + """), ASM(""" + ld a, [$DAA2] + and $20 + jp z, $3F8D + """), fill_nop=True) + + # Patch what happens when we pickup the toadstool, call our chest code to give a toadstool. + rom.patch(0x03, 0x1D6F, 0x1D7D, ASM(""" + ld a, $50 + ldh [$F1], a + ld a, $02 ; give item + rst 8 + + ld hl, $DAA2 + res 5, [hl] + """), fill_nop=True) + +def witchIsPatched(rom): + return sum(rom.banks[0x05][0x08D4:0x08F0]) != 0x0DC2 diff --git a/worlds/ladx/LADXR/plan.py b/worlds/ladx/LADXR/plan.py new file mode 100644 index 0000000000..df1908f433 --- /dev/null +++ b/worlds/ladx/LADXR/plan.py @@ -0,0 +1,38 @@ + + +# Helper class to read and store planomizer data +class Plan: + def __init__(self, filename): + self.forced_items = {} + self.item_pool = {} + item_group = {} + + for line in open(filename, "rt"): + line = line.strip() + if ";" in line: + line = line[:line.find(";")] + if "#" in line: + line = line[:line.find("#")] + if ":" not in line: + continue + entry_type, params = map(str.strip, line.upper().split(":", 1)) + + if entry_type == "LOCATION" and ":" in params: + location, item = map(str.strip, params.split(":", 1)) + if item == "": + continue + if item.startswith("[") and item.endswith("]"): + item = item_group[item[1:-1]] + if "," in item: + item = list(map(str.strip, item.split(","))) + self.forced_items[location] = item + elif entry_type == "POOL" and ":" in params: + item, count = map(str.strip, params.split(":", 1)) + self.item_pool[item] = self.item_pool.get(item, 0) + int(count) + elif entry_type == "GROUP" and ":" in params: + name, item = map(str.strip, params.split(":", 1)) + if item == "": + continue + if "," in item: + item = list(map(str.strip, item.split(","))) + item_group[name] = item diff --git a/worlds/ladx/LADXR/pointerTable.py b/worlds/ladx/LADXR/pointerTable.py new file mode 100644 index 0000000000..6b56b6ff44 --- /dev/null +++ b/worlds/ladx/LADXR/pointerTable.py @@ -0,0 +1,207 @@ +import copy +import struct + + +class PointerTable: + END_OF_DATA = (0xff, ) + + """ + Class to manage a list of pointers to data objects + Can rewrite the rom to modify the data objects and still keep the pointers intact. + """ + def __init__(self, rom, info): + assert "count" in info + assert "pointers_bank" in info + assert "pointers_addr" in info + assert ("banks_bank" in info and "banks_addr" in info) or ("data_bank" in info) + self.__info = info + + self.__data = [] + self.__alt_data = {} + self.__banks = [] + self.__storage = [] + + count = info["count"] + addr = info["pointers_addr"] + pointers_bank = rom.banks[info["pointers_bank"]] + if "data_addr" in info: + pointers_raw = [] + for n in range(count): + pointers_raw.append(info["data_addr"] + 0x4000 + pointers_bank[addr + n] * info["data_size"]) + else: + pointers_raw = struct.unpack("<" + "H" * count, pointers_bank[addr:addr+count*2]) + if "data_bank" in info: + banks = [info["data_bank"]] * count + else: + addr = info["banks_addr"] + banks = rom.banks[info["banks_bank"]][addr:addr+count] + + if "alt_pointers" in self.__info: + for key, (bank, addr) in self.__info["alt_pointers"].items(): + pointer = struct.unpack("= len(s) and st["bank"] == bank: + my_storage = st + break + assert my_storage is not None, "Not enough room in storage... %s" % (storage) + + pointer = my_storage["start"] + my_storage["start"] = pointer + len(s) + rom.banks[bank][pointer:pointer + len(s)] = s + + rom.banks[ptr_bank][ptr_addr] = pointer & 0xFF + rom.banks[ptr_bank][ptr_addr + 1] = (pointer >> 8) | 0x40 + + for n, s in enumerate(self.__data): + if isinstance(s, int): + pointer = s + else: + s = bytes(s) + bank = self.__banks[n] + if s in done[bank]: + pointer = done[bank][s] + assert rom.banks[bank][pointer:pointer+len(s)] == s + else: + my_storage = None + for st in storage: + if st["end"] - st["start"] >= len(s) and st["bank"] == bank: + my_storage = st + break + assert my_storage is not None, "Not enough room in storage... %d/%d %s id:%x(%d) bank:%d" % (n, len(self.__data), storage, n, n, bank) + + pointer = my_storage["start"] + my_storage["start"] = pointer + len(s) + rom.banks[bank][pointer:pointer+len(s)] = s + + if "data_size" not in self.__info: + # aggressive de-duplication. + for skip in range(len(s)): + done[bank][s[skip:]] = pointer + skip + done[bank][s] = pointer + + if "data_addr" in self.__info: + offset = pointer - self.__info["data_addr"] + if "data_size" in self.__info: + assert offset % self.__info["data_size"] == 0 + offset //= self.__info["data_size"] + rom.banks[pointers_bank][pointers_addr + n] = offset + else: + rom.banks[pointers_bank][pointers_addr+n*2] = pointer & 0xff + rom.banks[pointers_bank][pointers_addr+n*2+1] = ((pointer >> 8) & 0xff) | 0x40 + + space_left = sum(map(lambda n: n["end"] - n["start"], storage)) + # print(self.__class__.__name__, "Space left:", space_left) + return storage + + def _readData(self, rom, bank_nr, pointer): + bank = rom.banks[bank_nr] + start = pointer + if "data_size" in self.__info: + pointer += self.__info["data_size"] + else: + while bank[pointer] not in self.END_OF_DATA: + pointer += 1 + pointer += 1 + self._addStorage(bank_nr, start, pointer) + return bank[start:pointer] + + def _addStorage(self, bank, start, end): + for n, data in enumerate(self.__storage): + if data["bank"] == bank: + if data["start"] == end: + data["start"] = start + return + if data["end"] == start: + data["end"] = end + return + if data["start"] <= start and data["end"] >= end: + return + self.__storage.append({"bank": bank, "start": start, "end": end}) + + def __mergeStorage(self): + for n in range(len(self.__storage)): + n_end = self.__storage[n]["end"] + n_start = self.__storage[n]["start"] + for m in range(len(self.__storage)): + if m == n or self.__storage[n]["bank"] != self.__storage[m]["bank"]: + continue + m_end = self.__storage[m]["end"] + m_start = self.__storage[m]["start"] + if m_start - 1 <= n_end <= m_end: + self.__storage[n]["start"] = min(self.__storage[n]["start"], self.__storage[m]["start"]) + self.__storage[n]["end"] = self.__storage[m]["end"] + self.__storage.pop(m) + return True + return False + + def addStorage(self, extra_storage): + for data in extra_storage: + self._addStorage(data["bank"], data["start"], data["end"]) + while self.__mergeStorage(): + pass + self.__storage.sort(key=lambda n: n["start"]) + + def adjustDataStart(self, new_start): + self.__info["data_addr"] = new_start \ No newline at end of file diff --git a/worlds/ladx/LADXR/rom.py b/worlds/ladx/LADXR/rom.py new file mode 100644 index 0000000000..e7ff2f244d --- /dev/null +++ b/worlds/ladx/LADXR/rom.py @@ -0,0 +1,75 @@ +import binascii + +b2h = binascii.hexlify +h2b = binascii.unhexlify + + +class ROM: + def __init__(self, filename): + data = open(filename, "rb").read() + #assert len(data) == 1024 * 1024 + self.banks = [] + for n in range(0x40): + self.banks.append(bytearray(data[n*0x4000:(n+1)*0x4000])) + + def patch(self, bank_nr, addr, old, new, *, fill_nop=False): + new = h2b(new) + bank = self.banks[bank_nr] + if old is not None: + if isinstance(old, int): + old = bank[addr:old] + else: + old = h2b(old) + if fill_nop: + assert len(old) >= len(new), "Length mismatch: %d != %d (%s != %s)" % (len(old), len(new), b2h(old), b2h(new)) + new += b'\x00' * (len(old) - len(new)) + else: + assert len(old) == len(new), "Length mismatch: %d != %d (%s != %s)" % (len(old), len(new), b2h(old), b2h(new)) + assert addr >= 0 and addr + len(old) <= 16*1024 + if bank[addr:addr+len(old)] != old: + if bank[addr:addr + len(old)] == new: + # Patch is already applied. + return + loc = bank.find(old) + while loc > -1: + print("Possible at:", hex(loc)) + loc = bank.find(old, loc+1) + assert False, "Patch mismatch:\n%s !=\n%s at 0x%04x" % (b2h(bank[addr:addr+len(old)]), b2h(old), addr) + bank[addr:addr+len(new)] = new + assert len(bank) == 0x4000 + + def fixHeader(self, *, name=None): + if name is not None: + name = name.encode("utf-8") + name = (name + (b"\x00" * 15))[:15] + self.banks[0][0x134:0x143] = name + + checksum = 0 + for c in self.banks[0][0x134:0x14D]: + checksum -= c + 1 + self.banks[0][0x14D] = checksum & 0xFF + + # zero out the checksum before calculating it. + self.banks[0][0x14E] = 0 + self.banks[0][0x14F] = 0 + checksum = 0 + for bank in self.banks: + checksum = (checksum + sum(bank)) & 0xFFFF + self.banks[0][0x14E] = checksum >> 8 + self.banks[0][0x14F] = checksum & 0xFF + + def save(self, file, *, name=None): + # don't pass the name to fixHeader + self.fixHeader() + if isinstance(file, str): + f = open(file, "wb") + for bank in self.banks: + f.write(bank) + f.close() + print("Saved:", file) + else: + for bank in self.banks: + file.write(bank) + + def readHexSeed(self): + return self.banks[0x3E][0x2F00:0x2F10].hex().upper() diff --git a/worlds/ladx/LADXR/romTables.py b/worlds/ladx/LADXR/romTables.py new file mode 100644 index 0000000000..fbabe7595f --- /dev/null +++ b/worlds/ladx/LADXR/romTables.py @@ -0,0 +1,219 @@ +from .rom import ROM +from .pointerTable import PointerTable +from .assembler import ASM + + +class Texts(PointerTable): + END_OF_DATA = (0xfe, 0xff) + + def __init__(self, rom): + super().__init__(rom, { + "count": 0x2B0, + "pointers_addr": 1, + "pointers_bank": 0x1C, + "banks_addr": 0x741, + "banks_bank": 0x1C, + }) + + +class Entities(PointerTable): + def __init__(self, rom): + super().__init__(rom, { + "count": 0x320, + "pointers_addr": 0, + "pointers_bank": 0x16, + "data_bank": 0x16, + }) + +class RoomsTable(PointerTable): + HEADER = 2 + + def _readData(self, rom, bank_nr, pointer): + bank = rom.banks[bank_nr] + start = pointer + pointer += self.HEADER + while bank[pointer] != 0xFE: + obj_type = (bank[pointer] & 0xF0) + if obj_type == 0xE0: + pointer += 5 + elif obj_type == 0xC0 or obj_type == 0x80: + pointer += 3 + else: + pointer += 2 + pointer += 1 + self._addStorage(bank_nr, start, pointer) + return bank[start:pointer] + + +class RoomsOverworldTop(RoomsTable): + def __init__(self, rom): + super().__init__(rom, { + "count": 0x080, + "pointers_addr": 0x000, + "pointers_bank": 0x09, + "data_bank": 0x09, + "alt_pointers": { + "Alt06": (0x00, 0x31FD), + "Alt0E": (0x00, 0x31CD), + "Alt1B": (0x00, 0x320D), + "Alt2B": (0x00, 0x321D), + "Alt79": (0x00, 0x31ED), + } + }) + + +class RoomsOverworldBottom(RoomsTable): + def __init__(self, rom): + super().__init__(rom, { + "count": 0x080, + "pointers_addr": 0x100, + "pointers_bank": 0x09, + "data_bank": 0x1A, + "alt_pointers": { + "Alt8C": (0x00, 0x31DD), + } + }) + + +class RoomsIndoorA(RoomsTable): + # TODO: The color dungeon tables are in the same bank, but the pointer table is after the room data. + def __init__(self, rom): + super().__init__(rom, { + "count": 0x100, + "pointers_addr": 0x000, + "pointers_bank": 0x0A, + "data_bank": 0x0A, + "alt_pointers": { + "Alt1F5": (0x00, 0x31A1), + } + }) + + +class RoomsIndoorB(RoomsTable): + # Most likely, this table can be expanded all the way to the end of the bank, + # giving a few 100 extra bytes to work with. + def __init__(self, rom): + super().__init__(rom, { + "count": 0x0FF, + "pointers_addr": 0x000, + "pointers_bank": 0x0B, + "data_bank": 0x0B, + }) + + +class RoomsColorDungeon(RoomsTable): + def __init__(self, rom): + super().__init__(rom, { + "count": 0x016, + "pointers_addr": 0x3B77, + "pointers_bank": 0x0A, + "data_bank": 0x0A, + "expand_to_end_of_bank": True + }) + + +class BackgroundTable(PointerTable): + def _readData(self, rom, bank_nr, pointer): + bank = rom.banks[bank_nr] + start = pointer + while bank[pointer] != 0x00: + addr = bank[pointer] << 8 | bank[pointer + 1] + amount = (bank[pointer + 2] & 0x3F) + 1 + repeat = (bank[pointer + 2] & 0x40) == 0x40 + vertical = (bank[pointer + 2] & 0x80) == 0x80 + pointer += 3 + if not repeat: + pointer += amount + if repeat: + pointer += 1 + pointer += 1 + self._addStorage(bank_nr, start, pointer) + return bank[start:pointer] + + +class BackgroundTilesTable(BackgroundTable): + def __init__(self, rom): + super().__init__(rom, { + "count": 0x26, + "pointers_addr": 0x052B, + "pointers_bank": 0x20, + "data_bank": 0x08, + "expand_to_end_of_bank": True + }) + + +class BackgroundAttributeTable(BackgroundTable): + def __init__(self, rom): + super().__init__(rom, { + "count": 0x26, + "pointers_addr": 0x1C4B, + "pointers_bank": 0x24, + "data_bank": 0x24, + "expand_to_end_of_bank": True + }) + + +class OverworldRoomSpriteData(PointerTable): + def __init__(self, rom): + super().__init__(rom, { + "count": 0x100, + "pointers_addr": 0x30D3, + "pointers_bank": 0x20, + "data_bank": 0x20, + "data_addr": 0x33F3, + "data_size": 4, + "claim_storage_gaps": True, + }) + + +class IndoorRoomSpriteData(PointerTable): + def __init__(self, rom): + super().__init__(rom, { + "count": 0x220, + "pointers_addr": 0x31D3, + "pointers_bank": 0x20, + "data_bank": 0x20, + "data_addr": 0x363B, + "data_size": 4, + "claim_storage_gaps": True, + }) + + +class ROMWithTables(ROM): + def __init__(self, filename): + super().__init__(filename) + + # Ability to patch any text in the game with different text + self.texts = Texts(self) + # Ability to modify rooms + self.entities = Entities(self) + self.rooms_overworld_top = RoomsOverworldTop(self) + self.rooms_overworld_bottom = RoomsOverworldBottom(self) + self.rooms_indoor_a = RoomsIndoorA(self) + self.rooms_indoor_b = RoomsIndoorB(self) + self.rooms_color_dungeon = RoomsColorDungeon(self) + self.room_sprite_data_overworld = OverworldRoomSpriteData(self) + self.room_sprite_data_indoor = IndoorRoomSpriteData(self) + + # Backgrounds for things like the title screen. + self.background_tiles = BackgroundTilesTable(self) + self.background_attributes = BackgroundAttributeTable(self) + + self.itemNames = {} + + def save(self, filename, *, name=None): + self.texts.store(self) + self.entities.store(self) + self.rooms_overworld_top.store(self) + self.rooms_overworld_bottom.store(self) + self.rooms_indoor_a.store(self) + self.rooms_indoor_b.store(self) + self.rooms_color_dungeon.store(self) + leftover_storage = self.room_sprite_data_overworld.store(self) + self.room_sprite_data_indoor.addStorage(leftover_storage) + self.patch(0x00, 0x0DFA, ASM("ld hl, $763B"), ASM("ld hl, $%04x" % (leftover_storage[0]["start"] | 0x4000))) + self.room_sprite_data_indoor.adjustDataStart(leftover_storage[0]["start"]) + self.room_sprite_data_indoor.store(self) + self.background_tiles.store(self) + self.background_attributes.store(self) + super().save(filename, name=name) diff --git a/worlds/ladx/LADXR/roomEditor.py b/worlds/ladx/LADXR/roomEditor.py new file mode 100644 index 0000000000..c6cc631363 --- /dev/null +++ b/worlds/ladx/LADXR/roomEditor.py @@ -0,0 +1,584 @@ +import json +from . import entityData + + +WARP_TYPE_IDS = {0xE1, 0xE2, 0xE3, 0xBA, 0xA8, 0xBE, 0xCB, 0xC2, 0xC6} +ALT_ROOM_OVERLAYS = {"Alt06": 0x1040, "Alt0E": 0x1090, "Alt1B": 0x10E0, "Alt2B": 0x1130, "Alt79": 0x1180, "Alt8C": 0x11D0} + + +class RoomEditor: + def __init__(self, rom, room=None): + assert room is not None + self.room = room + self.entities = [] + self.objects = [] + self.tileset_index = None + self.palette_index = None + self.attribset = None + + if isinstance(room, int): + entities_raw = rom.entities[room] + idx = 0 + while entities_raw[idx] != 0xFF: + x = entities_raw[idx] & 0x0F + y = entities_raw[idx] >> 4 + id = entities_raw[idx + 1] + self.entities.append((x, y, id)) + idx += 2 + assert idx == len(entities_raw) - 1 + + if isinstance(room, str): + if room in rom.rooms_overworld_top: + objects_raw = rom.rooms_overworld_top[room] + elif room in rom.rooms_overworld_bottom: + objects_raw = rom.rooms_overworld_bottom[room] + elif room in rom.rooms_indoor_a: + objects_raw = rom.rooms_indoor_a[room] + else: + assert False, "Failed to find alt room: %s" % (room) + else: + if room < 0x080: + objects_raw = rom.rooms_overworld_top[room] + elif room < 0x100: + objects_raw = rom.rooms_overworld_bottom[room - 0x80] + elif room < 0x200: + objects_raw = rom.rooms_indoor_a[room - 0x100] + elif room < 0x300: + objects_raw = rom.rooms_indoor_b[room - 0x200] + else: + objects_raw = rom.rooms_color_dungeon[room - 0x300] + + self.animation_id = objects_raw[0] + self.floor_object = objects_raw[1] + idx = 2 + while objects_raw[idx] != 0xFE: + x = objects_raw[idx] & 0x0F + y = objects_raw[idx] >> 4 + if y == 0x08: # horizontal + count = x + x = objects_raw[idx + 1] & 0x0F + y = objects_raw[idx + 1] >> 4 + self.objects.append(ObjectHorizontal(x, y, objects_raw[idx + 2], count)) + idx += 3 + elif y == 0x0C: # vertical + count = x + x = objects_raw[idx + 1] & 0x0F + y = objects_raw[idx + 1] >> 4 + self.objects.append(ObjectVertical(x, y, objects_raw[idx + 2], count)) + idx += 3 + elif y == 0x0E: # warp + self.objects.append(ObjectWarp(objects_raw[idx] & 0x0F, objects_raw[idx + 1], objects_raw[idx + 2], objects_raw[idx + 3], objects_raw[idx + 4])) + idx += 5 + else: + self.objects.append(Object(x, y, objects_raw[idx + 1])) + idx += 2 + if room is not None: + assert idx == len(objects_raw) - 1 + + if isinstance(room, int) and room < 0x0CC: + self.overlay = rom.banks[0x26][room * 80:room * 80+80] + elif isinstance(room, int) and room < 0x100: + self.overlay = rom.banks[0x27][(room - 0xCC) * 80:(room - 0xCC) * 80 + 80] + elif room in ALT_ROOM_OVERLAYS: + self.overlay = rom.banks[0x27][ALT_ROOM_OVERLAYS[room]:ALT_ROOM_OVERLAYS[room] + 80] + else: + self.overlay = None + + def store(self, rom, new_room_nr=None): + if new_room_nr is None: + new_room_nr = self.room + objects_raw = bytearray([self.animation_id, self.floor_object]) + for obj in self.objects: + objects_raw += obj.export() + objects_raw += bytearray([0xFE]) + + if isinstance(new_room_nr, str): + if new_room_nr in rom.rooms_overworld_top: + rom.rooms_overworld_top[new_room_nr] = objects_raw + elif new_room_nr in rom.rooms_overworld_bottom: + rom.rooms_overworld_bottom[new_room_nr] = objects_raw + elif new_room_nr in rom.rooms_indoor_a: + rom.rooms_indoor_a[new_room_nr] = objects_raw + else: + assert False, "Failed to find alt room: %s" % (new_room_nr) + elif new_room_nr < 0x080: + rom.rooms_overworld_top[new_room_nr] = objects_raw + elif new_room_nr < 0x100: + rom.rooms_overworld_bottom[new_room_nr - 0x80] = objects_raw + elif new_room_nr < 0x200: + rom.rooms_indoor_a[new_room_nr - 0x100] = objects_raw + elif new_room_nr < 0x300: + rom.rooms_indoor_b[new_room_nr - 0x200] = objects_raw + else: + rom.rooms_color_dungeon[new_room_nr - 0x300] = objects_raw + + if isinstance(new_room_nr, int) and new_room_nr < 0x100: + if self.tileset_index is not None: + rom.banks[0x3F][0x3F00 + new_room_nr] = self.tileset_index & 0xFF + if self.attribset is not None: + # With a tileset, comes metatile gbc data that we need to store a proper bank+pointer. + rom.banks[0x1A][0x2476 + new_room_nr] = self.attribset[0] + rom.banks[0x1A][0x1E76 + new_room_nr*2] = self.attribset[1] & 0xFF + rom.banks[0x1A][0x1E76 + new_room_nr*2+1] = self.attribset[1] >> 8 + if self.palette_index is not None: + rom.banks[0x21][0x02ef + new_room_nr] = self.palette_index + + if isinstance(new_room_nr, int): + entities_raw = bytearray() + for entity in self.entities: + entities_raw += bytearray([entity[0] | entity[1] << 4, entity[2]]) + entities_raw += bytearray([0xFF]) + rom.entities[new_room_nr] = entities_raw + + if new_room_nr < 0x0CC: + rom.banks[0x26][new_room_nr * 80:new_room_nr * 80 + 80] = self.overlay + elif new_room_nr < 0x100: + rom.banks[0x27][(new_room_nr - 0xCC) * 80:(new_room_nr - 0xCC) * 80 + 80] = self.overlay + elif new_room_nr in ALT_ROOM_OVERLAYS: + rom.banks[0x27][ALT_ROOM_OVERLAYS[new_room_nr]:ALT_ROOM_OVERLAYS[new_room_nr] + 80] = self.overlay + + def addEntity(self, x, y, type_id): + self.entities.append((x, y, type_id)) + + def removeEntities(self, type_id): + self.entities = list(filter(lambda e: e[2] != type_id, self.entities)) + + def hasEntity(self, type_id): + return any(map(lambda e: e[2] == type_id, self.entities)) + + def changeObject(self, x, y, new_type): + for obj in self.objects: + if obj.x == x and obj.y == y: + obj.type_id = new_type + if self.overlay is not None: + self.overlay[x + y * 10] = new_type + + def removeObject(self, x, y): + self.objects = list(filter(lambda obj: obj.x != x or obj.y != y, self.objects)) + + def moveObject(self, x, y, new_x, new_y): + for obj in self.objects: + if obj.x == x and obj.y == y: + if self.overlay is not None: + self.overlay[x + y * 10] = self.floor_object + self.overlay[new_x + new_y * 10] = obj.type_id + obj.x = new_x + obj.y = new_y + + def getWarps(self): + return list(filter(lambda obj: isinstance(obj, ObjectWarp), self.objects)) + + def updateOverlay(self, preserve_floor=False): + if self.overlay is None: + return + if not preserve_floor: + for n in range(80): + self.overlay[n] = self.floor_object + for obj in self.objects: + if isinstance(obj, ObjectHorizontal): + for n in range(obj.count): + self.overlay[obj.x + n + obj.y * 10] = obj.type_id + elif isinstance(obj, ObjectVertical): + for n in range(obj.count): + self.overlay[obj.x + n * 10 + obj.y * 10] = obj.type_id + elif not isinstance(obj, ObjectWarp): + self.overlay[obj.x + obj.y * 10] = obj.type_id + + def loadFromJson(self, filename): + self.objects = [] + self.entities = [] + self.animation_id = 0 + self.tileset_index = 0x0F + self.palette_index = 0x01 + + data = json.load(open(filename)) + + for prop in data.get("properties", []): + if prop["name"] == "palette": + self.palette_index = int(prop["value"], 16) + elif prop["name"] == "tileset": + self.tileset_index = int(prop["value"], 16) + elif prop["name"] == "animationset": + self.animation_id = int(prop["value"], 16) + elif prop["name"] == "attribset": + bank, _, addr = prop["value"].partition(":") + self.attribset = (int(bank, 16), int(addr, 16) + 0x4000) + + tiles = [0] * 80 + for layer in data["layers"]: + if "data" in layer: + for n in range(80): + if layer["data"][n] > 0: + tiles[n] = (layer["data"][n] - 1) & 0xFF + if "objects" in layer: + for obj in layer["objects"]: + x = int((obj["x"] + obj["width"] / 2) // 16) + y = int((obj["y"] + obj["height"] / 2) // 16) + if obj["type"] == "warp": + warp_type, map_nr, room, x, y = obj["name"].split(":") + self.objects.append(ObjectWarp(int(warp_type), int(map_nr, 16), int(room, 16) & 0xFF, int(x, 16), int(y, 16))) + elif obj["type"] == "entity": + type_id = entityData.NAME.index(obj["name"]) + self.addEntity(x, y, type_id) + elif obj["type"] == "hidden_tile": + self.objects.append(Object(x, y, int(obj["name"], 16))) + self.buildObjectList(tiles, reduce_size=True) + return data + + def getTileArray(self): + if self.room < 0x100: + tiles = [self.floor_object] * 80 + else: + tiles = [self.floor_object & 0x0F] * 80 + def objHSize(type_id): + if type_id == 0xF5: + return 2 + return 1 + def objVSize(type_id): + if type_id == 0xF5: + return 2 + return 1 + def getObject(x, y): + x, y = (x & 15), (y & 15) + if x < 10 and y < 8: + return tiles[x + y * 10] + return 0 + if self.room < 0x100: + def placeObject(x, y, type_id): + if type_id == 0xF5: + if getObject(x, y) in (0x1B, 0x28, 0x29, 0x83, 0x90): + placeObject(x, y, 0x29) + else: + placeObject(x, y, 0x25) + if getObject(x + 1, y) in (0x1B, 0x27, 0x82, 0x86, 0x8A, 0x90, 0x2A): + placeObject(x + 1, y, 0x2A) + else: + placeObject(x + 1, y, 0x26) + if getObject(x, y + 1) in (0x26, 0x2A): + placeObject(x, y + 1, 0x2A) + elif getObject(x, y + 1) == 0x90: + placeObject(x, y + 1, 0x82) + else: + placeObject(x, y + 1, 0x27) + if getObject(x + 1, y + 1) in (0x25, 0x29): + placeObject(x + 1, y + 1, 0x29) + elif getObject(x + 1, y + 1) == 0x90: + placeObject(x + 1, y + 1, 0x83) + else: + placeObject(x + 1, y + 1, 0x28) + elif type_id == 0xF6: # two door house + placeObject(x + 0, y, 0x55) + placeObject(x + 1, y, 0x5A) + placeObject(x + 2, y, 0x5A) + placeObject(x + 3, y, 0x5A) + placeObject(x + 4, y, 0x56) + placeObject(x + 0, y + 1, 0x57) + placeObject(x + 1, y + 1, 0x59) + placeObject(x + 2, y + 1, 0x59) + placeObject(x + 3, y + 1, 0x59) + placeObject(x + 4, y + 1, 0x58) + placeObject(x + 0, y + 2, 0x5B) + placeObject(x + 1, y + 2, 0xE2) + placeObject(x + 2, y + 2, 0x5B) + placeObject(x + 3, y + 2, 0xE2) + placeObject(x + 4, y + 2, 0x5B) + elif type_id == 0xF7: # large house + placeObject(x + 0, y, 0x55) + placeObject(x + 1, y, 0x5A) + placeObject(x + 2, y, 0x56) + placeObject(x + 0, y + 1, 0x57) + placeObject(x + 1, y + 1, 0x59) + placeObject(x + 2, y + 1, 0x58) + placeObject(x + 0, y + 2, 0x5B) + placeObject(x + 1, y + 2, 0xE2) + placeObject(x + 2, y + 2, 0x5B) + elif type_id == 0xF8: # catfish + placeObject(x + 0, y, 0xB6) + placeObject(x + 1, y, 0xB7) + placeObject(x + 2, y, 0x66) + placeObject(x + 0, y + 1, 0x67) + placeObject(x + 1, y + 1, 0xE3) + placeObject(x + 2, y + 1, 0x68) + elif type_id == 0xF9: # palace door + placeObject(x + 0, y, 0xA4) + placeObject(x + 1, y, 0xA5) + placeObject(x + 2, y, 0xA6) + placeObject(x + 0, y + 1, 0xA7) + placeObject(x + 1, y + 1, 0xE3) + placeObject(x + 2, y + 1, 0xA8) + elif type_id == 0xFA: # stone pig head + placeObject(x + 0, y, 0xBB) + placeObject(x + 1, y, 0xBC) + placeObject(x + 0, y + 1, 0xBD) + placeObject(x + 1, y + 1, 0xBE) + elif type_id == 0xFB: # palmtree + if x == 15: + placeObject(x + 1, y + 1, 0xB7) + placeObject(x + 1, y + 2, 0xCE) + else: + placeObject(x + 0, y, 0xB6) + placeObject(x + 0, y + 1, 0xCD) + placeObject(x + 1, y + 0, 0xB7) + placeObject(x + 1, y + 1, 0xCE) + elif type_id == 0xFC: # square "hill with hole" (seen near lvl4 entrance) + placeObject(x + 0, y, 0x2B) + placeObject(x + 1, y, 0x2C) + placeObject(x + 2, y, 0x2D) + placeObject(x + 0, y + 1, 0x37) + placeObject(x + 1, y + 1, 0xE8) + placeObject(x + 2, y + 1, 0x38) + placeObject(x - 1, y + 2, 0x0A) + placeObject(x + 0, y + 2, 0x33) + placeObject(x + 1, y + 2, 0x2F) + placeObject(x + 2, y + 2, 0x34) + placeObject(x + 0, y + 3, 0x0A) + placeObject(x + 1, y + 3, 0x0A) + placeObject(x + 2, y + 3, 0x0A) + placeObject(x + 3, y + 3, 0x0A) + elif type_id == 0xFD: # small house + placeObject(x + 0, y, 0x52) + placeObject(x + 1, y, 0x52) + placeObject(x + 2, y, 0x52) + placeObject(x + 0, y + 1, 0x5B) + placeObject(x + 1, y + 1, 0xE2) + placeObject(x + 2, y + 1, 0x5B) + else: + x, y = (x & 15), (y & 15) + if x < 10 and y < 8: + tiles[x + y * 10] = type_id + else: + def placeObject(x, y, type_id): + x, y = (x & 15), (y & 15) + if type_id == 0xEC: # key door + placeObject(x, y, 0x2D) + placeObject(x + 1, y, 0x2E) + elif type_id == 0xED: + placeObject(x, y, 0x2F) + placeObject(x + 1, y, 0x30) + elif type_id == 0xEE: + placeObject(x, y, 0x31) + placeObject(x, y + 1, 0x32) + elif type_id == 0xEF: + placeObject(x, y, 0x33) + placeObject(x, y + 1, 0x34) + elif type_id == 0xF0: # closed door + placeObject(x, y, 0x35) + placeObject(x + 1, y, 0x36) + elif type_id == 0xF1: + placeObject(x, y, 0x37) + placeObject(x + 1, y, 0x38) + elif type_id == 0xF2: + placeObject(x, y, 0x39) + placeObject(x, y + 1, 0x3A) + elif type_id == 0xF3: + placeObject(x, y, 0x3B) + placeObject(x, y + 1, 0x3C) + elif type_id == 0xF4: # open door + placeObject(x, y, 0x43) + placeObject(x + 1, y, 0x44) + elif type_id == 0xF5: + placeObject(x, y, 0x8C) + placeObject(x + 1, y, 0x08) + elif type_id == 0xF6: + placeObject(x, y, 0x09) + placeObject(x, y + 1, 0x0A) + elif type_id == 0xF7: + placeObject(x, y, 0x0B) + placeObject(x, y + 1, 0x0C) + elif type_id == 0xF8: # boss door + placeObject(x, y, 0xA4) + placeObject(x + 1, y, 0xA5) + elif type_id == 0xF9: # stairs door + placeObject(x, y, 0xAF) + placeObject(x + 1, y, 0xB0) + elif type_id == 0xFA: # flipwall + placeObject(x, y, 0xB1) + placeObject(x + 1, y, 0xB2) + elif type_id == 0xFB: # one way arrow + placeObject(x, y, 0x45) + placeObject(x + 1, y, 0x46) + elif type_id == 0xFC: # entrance + placeObject(x + 0, y, 0xB3) + placeObject(x + 1, y, 0xB4) + placeObject(x + 2, y, 0xB4) + placeObject(x + 3, y, 0xB5) + placeObject(x + 0, y + 1, 0xB6) + placeObject(x + 1, y + 1, 0xB7) + placeObject(x + 2, y + 1, 0xB8) + placeObject(x + 3, y + 1, 0xB9) + placeObject(x + 0, y + 2, 0xBA) + placeObject(x + 1, y + 2, 0xBB) + placeObject(x + 2, y + 2, 0xBC) + placeObject(x + 3, y + 2, 0xBD) + elif type_id == 0xFD: # entrance + placeObject(x, y, 0xC1) + placeObject(x + 1, y, 0xC2) + else: + if x < 10 and y < 8: + tiles[x + y * 10] = type_id + + def addWalls(flags): + for x in range(0, 10): + if flags & 0b0010: + placeObject(x, 0, 0x21) + if flags & 0b0001: + placeObject(x, 7, 0x22) + for y in range(0, 8): + if flags & 0b1000: + placeObject(0, y, 0x23) + if flags & 0b0100: + placeObject(9, y, 0x24) + if flags & 0b1000 and flags & 0b0010: + placeObject(0, 0, 0x25) + if flags & 0b0100 and flags & 0b0010: + placeObject(9, 0, 0x26) + if flags & 0b1000 and flags & 0b0001: + placeObject(0, 7, 0x27) + if flags & 0b0100 and flags & 0b0001: + placeObject(9, 7, 0x28) + + if self.floor_object & 0xF0 == 0x00: + addWalls(0b1111) + if self.floor_object & 0xF0 == 0x10: + addWalls(0b1101) + if self.floor_object & 0xF0 == 0x20: + addWalls(0b1011) + if self.floor_object & 0xF0 == 0x30: + addWalls(0b1110) + if self.floor_object & 0xF0 == 0x40: + addWalls(0b0111) + if self.floor_object & 0xF0 == 0x50: + addWalls(0b1001) + if self.floor_object & 0xF0 == 0x60: + addWalls(0b0101) + if self.floor_object & 0xF0 == 0x70: + addWalls(0b0110) + if self.floor_object & 0xF0 == 0x80: + addWalls(0b1010) + for obj in self.objects: + if isinstance(obj, ObjectWarp): + pass + elif isinstance(obj, ObjectHorizontal): + for n in range(0, obj.count): + placeObject(obj.x + n * objHSize(obj.type_id), obj.y, obj.type_id) + elif isinstance(obj, ObjectVertical): + for n in range(0, obj.count): + placeObject(obj.x, obj.y + n * objVSize(obj.type_id), obj.type_id) + else: + placeObject(obj.x, obj.y, obj.type_id) + return tiles + + def buildObjectList(self, tiles, *, reduce_size=False): + self.objects = [obj for obj in self.objects if isinstance(obj, ObjectWarp)] + tiles = tiles.copy() + if self.overlay: + for n in range(80): + self.overlay[n] = tiles[n] + if reduce_size: + if tiles[n] in {0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, + 0x33, 0x34, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, + 0x48, 0x49, 0x4B, 0x4C, 0x4E, + 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F}: + tiles[n] = 0x3A # Solid tiles + if tiles[n] in {0x08, 0x09, 0x0C, 0x44, + 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF}: + tiles[n] = 0x04 # Open tiles + + is_overworld = isinstance(self.room, str) or self.room < 0x100 + counts = {} + for n in tiles: + if n < 0x0F or is_overworld: + counts[n] = counts.get(n, 0) + 1 + self.floor_object = max(counts, key=counts.get) + for y in range(8) if is_overworld else range(1, 7): + for x in range(10) if is_overworld else range(1, 9): + if tiles[x + y * 10] == self.floor_object: + tiles[x + y * 10] = -1 + for y in range(8): + for x in range(10): + obj = tiles[x + y * 10] + if obj == -1: + continue + w = 1 + h = 1 + while x + w < 10 and tiles[x + w + y * 10] == obj: + w += 1 + while y + h < 8 and tiles[x + (y + h) * 10] == obj: + h += 1 + if obj in {0xE1, 0xE2, 0xE3, 0xBA, 0xC6}: # Entrances should never be horizontal/vertical lists + w = 1 + h = 1 + if w > h: + for n in range(w): + tiles[x + n + y * 10] = -1 + self.objects.append(ObjectHorizontal(x, y, obj, w)) + elif h > 1: + for n in range(h): + tiles[x + (y + n) * 10] = -1 + self.objects.append(ObjectVertical(x, y, obj, h)) + else: + self.objects.append(Object(x, y, obj)) + + +class Object: + def __init__(self, x, y, type_id): + self.x = x + self.y = y + self.type_id = type_id + + def export(self): + return bytearray([self.x | (self.y << 4), self.type_id]) + + def __repr__(self): + return "%s:%d,%d:%02X" % (self.__class__.__name__, self.x, self.y, self.type_id) + + +class ObjectHorizontal(Object): + def __init__(self, x, y, type_id, count): + super().__init__(x, y, type_id) + self.count = count + + def export(self): + return bytearray([0x80 | self.count, self.x | (self.y << 4), self.type_id]) + + def __repr__(self): + return "%s:%d,%d:%02Xx%d" % (self.__class__.__name__, self.x, self.y, self.type_id, self.count) + + +class ObjectVertical(Object): + def __init__(self, x, y, type_id, count): + super().__init__(x, y, type_id) + self.count = count + + def export(self): + return bytearray([0xC0 | self.count, self.x | (self.y << 4), self.type_id]) + + def __repr__(self): + return "%s:%d,%d:%02Xx%d" % (self.__class__.__name__, self.x, self.y, self.type_id, self.count) + + +class ObjectWarp(Object): + def __init__(self, warp_type, map_nr, room_nr, target_x, target_y): + super().__init__(None, None, None) + if warp_type > 0: + # indoor map + if map_nr == 0xff: + room_nr += 0x300 # color dungeon + elif 0x06 <= map_nr < 0x1A: + room_nr += 0x200 # indoor B + else: + room_nr += 0x100 # indoor A + self.warp_type = warp_type + self.room = room_nr + self.map_nr = map_nr + self.target_x = target_x + self.target_y = target_y + + def export(self): + return bytearray([0xE0 | self.warp_type, self.map_nr, self.room & 0xFF, self.target_x, self.target_y]) + + def copy(self): + return ObjectWarp(self.warp_type, self.map_nr, self.room & 0xFF, self.target_x, self.target_y) + + def __repr__(self): + return "%s:%d:%03x:%02x:%d,%d" % (self.__class__.__name__, self.warp_type, self.room, self.map_nr, self.target_x, self.target_y) diff --git a/worlds/ladx/LADXR/settings.py b/worlds/ladx/LADXR/settings.py new file mode 100644 index 0000000000..d52e8fe45d --- /dev/null +++ b/worlds/ladx/LADXR/settings.py @@ -0,0 +1,321 @@ +from typing import List, Tuple, Optional, Union +import os + + +class Setting: + def __init__(self, key: str, + category: str, short_key: str, label: str, *, + description: str, multiworld: bool = True, aesthetic: bool = False, options: Optional[List[Tuple[str, str, str]]] = None, + default: Optional[Union[bool, float, str]] = None, placeholder: Optional[str] = None): + if options: + assert default in [option_key for option_key, option_short, option_label in options], f"{default} not in {options}" + short_options = set() + for option_key, option_short, option_label in options: + assert option_short != "" or option_key == default, f"No short option for non default {label}:{option_key}" + assert option_short not in short_options, "Duplicate short option value..." + short_options.add(option_short) + + self.key = key + self.category = category + self.short_key = short_key + self.label = label + self.description = description + self.multiworld = multiworld + self.aesthetic = aesthetic + self.options = options + self.default = default + self.placeholder = placeholder + + self.value = default + + def set(self, value): + if isinstance(self.default, bool): + if not isinstance(value, bool): + value = bool(int(value)) + elif not isinstance(value, type(self.default)): + try: + value = type(self.default)(value) + except ValueError: + raise ValueError(f"{value} is not an accepted value for {self.key} setting") + if self.options: + if value not in [k for k, s, v in self.options]: + raise ValueError(f"{value} is not an accepted value for {self.key} setting") + self.value = value + + def getShortValue(self): + if self.options: + for option_key, option_short, option_label in self.options: + if self.value == option_key: + return option_short + return self.value + ">" + + def toJson(self): + result = { + "key": self.key, + "category": self.category, + "short_key": self.short_key, + "label": self.label, + "description": self.description, + "multiworld": self.multiworld, + "aesthetic": self.aesthetic, + "default": self.default, + } + if self.options: + result["options"] = [{"key": option_key, "short": option_short, "label": option_label} for option_key, option_short, option_label in self.options] + if self.placeholder: + result["placeholder"] = self.placeholder + return result + + +class Settings: + def __init__(self, ap_options): + gfx_options = [('', '', 'Default')] + gfx_path = os.path.join("data", "sprites", "ladx") + for filename in sorted(os.listdir(gfx_path)): + if filename.endswith(".bin") or filename.endswith(".png") or filename.endswith(".bmp"): + gfx_options.append((filename, filename + ">", filename[:-4])) + if filename.endswith(".bdiff"): + gfx_options.append((filename, filename + ">", filename[:-6])) + + + self.__all = [ + Setting('seed', 'Main', '<', 'Seed', placeholder='Leave empty for random seed', default="", multiworld=False, + description="""For multiple people to generate the same randomization result, enter the generated seed number here. +Note, not all strings are valid seeds."""), + Setting('logic', 'Main', 'L', 'Logic', options=[('casual', 'c', 'Casual'), ('normal', 'n', 'Normal'), ('hard', 'h', 'Hard'), ('glitched', 'g', 'Glitched'), ('hell', 'H', 'Hell')], default='normal', + description="""Affects where items are allowed to be placed. +[Casual] Same as normal, except that a few more complex options are removed, like removing bushes with powder and killing enemies with powder or bombs. +[Normal] playable without using any tricks or glitches. Requires nothing to be done outside of normal item usage. +[Hard] More advanced techniques may be required, but glitches are not. Examples include tricky jumps, killing enemies with only pots and skipping keys with smart routing. +[Glitched] Advanced glitches and techniques may be required, but extremely difficult or tedious tricks are not required. Examples include Bomb Triggers, Super Jumps and Jesus Jumps. +[Hell] Obscure and hard techniques may be required. Examples include featherless jumping with boots and/or hookshot, sequential pit buffers and unclipped superjumps. Things in here can be extremely hard to do or very time consuming. Only insane people go for this."""), + Setting('forwardfactor', 'Main', 'F', 'Forward Factor', default=0.0, + description="Forward item weight adjustment factor, lower values generate more rear heavy seeds while higher values generate front heavy seeds. Default is 0.5."), + Setting('accessibility', 'Main', 'A', 'Accessibility', options=[('all', 'a', '100% Locations'), ('goal', 'g', 'Beatable')], default='all', + description=""" +[100% Locations] guaranteed that every single item can be reached and gained. +[Beatable] only guarantees that the game is beatable. Certain items/chests might never be reachable."""), + Setting('race', 'Main', 'V', 'Race mode', default=False, multiworld=False, + description=""" +Spoiler logs can not be generated for ROMs generated with race mode enabled, and seed generation is slightly different."""), +# Setting('spoilerformat', 'Main', 'Spoiler Format', options=[('none', 'None'), ('text', 'Text'), ('json', 'JSON')], default='none', multiworld=False, +# description="""Affects how the spoiler log is generated. +# [None] No spoiler log is generated. One can still be manually dumped later. +# [Text] Creates a .txt file meant for a human to read. +# [JSON] Creates a .json file with a little more information and meant for a computer to read.""") + Setting('heartpiece', 'Items', 'h', 'Randomize heart pieces', default=True, + description='Includes heart pieces in the item pool'), + Setting('seashells', 'Items', 's', 'Randomize hidden seashells', default=True, + description='Randomizes the secret sea shells hiding in the ground/trees. (chest are always randomized)'), + Setting('heartcontainers', 'Items', 'H', 'Randomize heart containers', default=True, + description='Includes boss heart container drops in the item pool'), + Setting('instruments', 'Items', 'I', 'Randomize instruments', default=False, + description='Instruments are placed on random locations, dungeon goal will just contain a random item.'), + Setting('tradequest', 'Items', 'T', 'Randomize trade quest', default=True, + description='Trade quest items are randomized, each NPC takes its normal trade quest item, but gives a random item'), + Setting('witch', 'Items', 'W', 'Randomize item given by the witch', default=True, + description='Adds both the toadstool and the reward for giving the toadstool to the witch to the item pool'), + Setting('rooster', 'Items', 'R', 'Add the rooster', default=True, + description='Adds the rooster to the item pool. Without this option, the rooster spot is still a check giving an item. But you will never find the rooster. Any rooster spot is accessible without rooster by other means.'), + Setting('boomerang', 'Items', 'Z', 'Boomerang trade', options=[('default', 'd', 'Normal'), ('trade', 't', 'Trade'), ('gift', 'g', 'Gift')], default='gift', + description=""" +[Normal], requires magnifier to get the boomerang. +[Trade], allows to trade an inventory item for a random other inventory item boomerang is shuffled. +[Gift], You get a random gift of any item, and the boomerang is shuffled."""), + Setting('randomstartlocation', 'Gameplay', 'r', 'Random start location', default=False, + description='Randomize where your starting house is located'), + Setting('dungeonshuffle', 'Gameplay', 'u', 'Dungeon shuffle', default=False, + description='Randomizes the dungeon that each dungeon entrance leads to'), + Setting('entranceshuffle', 'Gameplay', 'E', 'Entrance randomizer', options=[("none", '', "Default"), ("simple", 's', "Simple"), ("advanced", 'a', "Advanced"), ("expert", 'E', "Expert"), ("insanity", 'I', "Insanity")], default='none', + description="""Randomizes where overworld entrances lead to. +[Simple] single entrance caves that contain items are randomized +[Advanced] Connector caves are also randomized +[Expert] Caves/houses without items are also randomized +[Insanity] A few very annoying entrances will be randomized as well. +If random start location and/or dungeon shuffle is enabled, then these will be shuffled with all the entrances. +Note, some entrances can lead into water, use the warp-to-home from the save&quit menu to escape this."""), + Setting('boss', 'Gameplay', 'B', 'Boss shuffle', options=[('default', '', 'Normal'), ('shuffle', 's', 'Shuffle'), ('random', 'r', 'Randomize')], default='default', + description='Randomizes the dungeon bosses that each dungeon has'), + Setting('miniboss', 'Gameplay', 'b', 'Miniboss shuffle', options=[('default', '', 'Normal'), ('shuffle', 's', 'Shuffle'), ('random', 'r', 'Randomize')], default='default', + description='Randomizes the dungeon minibosses that each dungeon has'), + Setting('goal', 'Gameplay', 'G', 'Goal', options=[('8', '8', '8 instruments'), ('7', '7', '7 instruments'), ('6', '6', '6 instruments'), + ('5', '5', '5 instruments'), ('4', '4', '4 instruments'), ('3', '3', '3 instruments'), + ('2', '2', '2 instruments'), ('1', '1', '1 instrument'), ('0', '0', 'No instruments'), + ('open', 'O', 'Egg already open'), ('random', 'R', 'Random instrument count'), + ('open-4', '<', 'Random short game (0-4)'), ('5-8', '>', 'Random long game (5-8)'), + ('seashells', 'S', 'Seashell hunt (20)'), ('bingo', 'b', 'Bingo!'), + ('bingo-full', 'B', 'Bingo-25!')], default='8', + description="""Changes the goal of the game. +[1-8 instruments], number of instruments required to open the egg. +[No instruments] open the egg without instruments, still requires the ocarina with the balled of the windfish +[Egg already open] the egg is already open, just head for it once you have the items needed to defeat the boss. +[Randomized instrument count] random number of instruments required to open the egg, between 0 and 8. +[Random short/long game] random number of instruments required to open the egg, chosen between 0-4 and 5-8 respectively. +[Seashell hunt] egg will open once you collected 20 seashells. Instruments are replaced by seashells and shuffled. +[Bingo] Generate a 5x5 bingo board with various goals. Complete one row/column or diagonal to win! +[Bingo-25] Bingo, but need to fill the whole bingo card to win!"""), + Setting('itempool', 'Gameplay', 'P', 'Item pool', options=[('', '', 'Normal'), ('casual', 'c', 'Casual'), ('pain', 'p', 'Path of Pain'), ('keyup', 'k', 'More keys')], default='', + description="""Effects which items are shuffled. +[Casual] places more inventory and key items so the seed is easier. +[More keys] adds more small keys and extra nightmare keys so dungeons are easier. +[Path of pain]... just find out yourself."""), + Setting('hpmode', 'Gameplay', 'm', 'Health mode', options=[('default', '', 'Normal'), ('inverted', 'i', 'Inverted'), ('1', '1', 'Start with 1 heart'), ('low', 'l', 'Low max')], default='default', + description=""" +[Normal} health works as you would expect. +[Inverted] you start with 9 heart containers, but killing a boss will take a heartcontainer instead of giving one. +[Start with 1] normal game, you just start with 1 heart instead of 3. +[Low max] replace heart containers with heart pieces."""), + Setting('hardmode', 'Gameplay', 'X', 'Hard mode', options=[('none', '', 'Disabled'), ('oracle', 'O', 'Oracle'), ('hero', 'H', 'Hero'), ('ohko', '1', 'One hit KO')], default='none', + description=""" +[Oracle] Less iframes and heath from drops. Bombs damage yourself. Water damages you without flippers. No piece of power or acorn. +[Hero] Switch version hero mode, double damage, no heart/fairy drops. +[One hit KO] You die on a single hit, always."""), + Setting('steal', 'Gameplay', 't', 'Stealing from the shop', + options=[('always', 'a', 'Always'), ('never', 'n', 'Never'), ('default', '', 'Normal')], default='default', + description="""Effects when you can steal from the shop. Stealing is bad and never in logic. +[Normal] requires the sword before you can steal. +[Always] you can always steal from the shop +[Never] you can never steal from the shop."""), + Setting('bowwow', 'Special', 'g', 'Good boy mode', options=[('normal', '', 'Disabled'), ('always', 'a', 'Enabled'), ('swordless', 's', 'Enabled (swordless)')], default='normal', + description='Allows BowWow to be taken into any area, damage bosses and more enemies. If enabled you always start with bowwow. Swordless option removes the swords from the game and requires you to beat the game without a sword and just bowwow.'), + Setting('overworld', 'Special', 'O', 'Overworld', options=[('normal', '', 'Normal'), ('dungeondive', 'D', 'Dungeon dive'), ('nodungeons', 'N', 'No dungeons'), ('random', 'R', 'Randomized')], default='normal', + description=""" +[Dungeon Dive] Create a different overworld where all the dungeons are directly accessible and almost no chests are located in the overworld. +[No dungeons] All dungeons only consist of a boss fight and a instrument reward. Rest of the dungeon is removed. +[Random] Creates a randomized overworld WARNING: This will error out often during generation, work in progress."""), + Setting('owlstatues', 'Special', 'o', 'Owl statues', options=[('', '', 'Never'), ('dungeon', 'D', 'In dungeons'), ('overworld', 'O', 'On the overworld'), ('both', 'B', 'Dungeons and Overworld')], default='', + description='Replaces the hints from owl statues with additional randomized items'), + Setting('superweapons', 'Special', 'q', 'Enable super weapons', default=False, + description='All items will be more powerful, faster, harder, bigger stronger. You name it.'), + Setting('quickswap', 'User options', 'Q', 'Quickswap', options=[('none', '', 'Disabled'), ('a', 'a', 'Swap A button'), ('b', 'b', 'Swap B button')], default='none', + description='Adds that the select button swaps with either A or B. The item is swapped with the top inventory slot. The map is not available when quickswap is enabled.', + aesthetic=True), + Setting('textmode', 'User options', 'f', 'Text mode', options=[('fast', '', 'Fast'), ('default', 'd', 'Normal'), ('none', 'n', 'No-text')], default='fast', + description="""[Fast] makes text appear twice as fast. +[No-Text] removes all text from the game""", aesthetic=True), + Setting('lowhpbeep', 'User options', 'p', 'Low HP beeps', options=[('none', 'D', 'Disabled'), ('slow', 'S', 'Slow'), ('default', 'N', 'Normal')], default='slow', + description='Slows or disables the low health beeping sound', aesthetic=True), + Setting('noflash', 'User options', 'l', 'Remove flashing lights', default=True, + description='Remove the flashing light effects from Mamu, shopkeeper and MadBatter. Useful for capture cards and people that are sensitive for these things.', + aesthetic=True), + Setting('nagmessages', 'User options', 'S', 'Show nag messages', default=False, + description='Enables the nag messages normally shown when touching stones and crystals', + aesthetic=True), + Setting('gfxmod', 'User options', 'c', 'Graphics', options=gfx_options, default='', + description='Generally affects at least Link\'s sprite, but can alter any graphics in the game', + aesthetic=True), + Setting('linkspalette', 'User options', 'C', "Link's color", + options=[('-1', '-', 'Normal'), ('0', '0', 'Green'), ('1', '1', 'Yellow'), ('2', '2', 'Red'), ('3', '3', 'Blue'), + ('4', '4', '?? A'), ('5', '5', '?? B'), ('6', '6', '?? C'), ('7', '7', '?? D')], default='-1', aesthetic=True, + description="""Allows you to force a certain color on link. +[Normal] color of link depends on the tunic. +[Green/Yellow/Red/Blue] forces link into one of these colors. +[?? A/B/C/D] colors of link are usually inverted and color depends on the area you are in."""), + Setting('music', 'User options', 'M', 'Music', options=[('', '', 'Default'), ('random', 'r', 'Random'), ('off', 'o', 'Disable')], default='', + description=""" +[Random] Randomizes overworld and dungeon music' +[Disable] no music in the whole game""", + aesthetic=True), + ] + self.__by_key = {s.key: s for s in self.__all} + + # Make sure all short keys are unique + short_keys = set() + for s in self.__all: + assert s.short_key not in short_keys, s.label + short_keys.add(s.short_key) + self.ap_options = ap_options + + for option in self.ap_options.values(): + if not hasattr(option, 'to_ladxr_option'): + continue + name, value = option.to_ladxr_option(self.ap_options) + if value == "true": + value = 1 + elif value == "false": + value = 0 + + if name: + self.set( f"{name}={value}") + + def __getattr__(self, item): + return self.__by_key[item].value + + def __setattr__(self, key, value): + if not key.startswith("_") and key in self.__by_key: + self.__by_key[key].set(value) + else: + super().__setattr__(key, value) + + def loadShortString(self, value): + for setting in self.__all: + if isinstance(setting.default, bool): + setting.value = False + index = 0 + while index < len(value): + key = value[index] + index += 1 + for setting in self.__all: + if setting.short_key != key: + continue + if isinstance(setting.default, bool): + setting.value = True + elif setting.options: + for option_key, option_short, option_label in setting.options: + if option_key != setting.default and value[index:].startswith(option_short): + setting.value = option_key + index += len(option_short) + break + else: + end_of_param = value.find(">", index) + setting.value = value[index:end_of_param] + index = end_of_param + 1 + + def getShortString(self): + result = "" + for setting in self.__all: + if isinstance(setting.default, bool): + if setting.value: + result += setting.short_key + elif setting.value != setting.default: + result += setting.short_key + setting.getShortValue() + return result + + def validate(self): + def req(setting: str, value: str, message: str) -> None: + if getattr(self, setting) != value: + print("Warning: %s (setting adjusted automatically)" % message) + setattr(self, setting, value) + + def dis(setting: str, value: str, new_value: str, message: str) -> None: + if getattr(self, setting) == value: + print("Warning: %s (setting adjusted automatically)" % message) + setattr(self, setting, new_value) + + if self.goal in ("bingo", "bingo-full"): + req("overworld", "normal", "Bingo goal does not work with dungeondive") + req("accessibility", "all", "Bingo goal needs 'all' accessibility") + dis("steal", "never", "default", "With bingo goal, stealing should be allowed") + dis("boss", "random", "shuffle", "With bingo goal, bosses need to be on normal or shuffle") + dis("miniboss", "random", "shuffle", "With bingo goal, minibosses need to be on normal or shuffle") + if self.overworld == "dungeondive": + dis("goal", "seashells", "8", "Dungeon dive does not work with seashell goal") + if self.overworld == "nodungeons": + dis("goal", "seashells", "8", "No dungeons does not work with seashell goal") + if self.overworld == "random": + self.goal = "4" # Force 4 dungeon goal for random overworld right now. + + def set(self, value: str) -> None: + if "=" in value: + key, value = value.split("=", 1) + else: + key, value = value, "1" + if key not in self.__by_key: + raise ValueError(f"Setting {key} not found") + self.__by_key[key].set(value) + + def toJson(self): + return [s.toJson() for s in self.__all] + + def __iter__(self): + return iter(self.__all) diff --git a/worlds/ladx/LADXR/utils.py b/worlds/ladx/LADXR/utils.py new file mode 100644 index 0000000000..fcf1d2bb56 --- /dev/null +++ b/worlds/ladx/LADXR/utils.py @@ -0,0 +1,222 @@ +from typing import Optional + +from .locations.items import * + +_NAMES = { + SWORD: "Sword", + BOMB: "Bombs", + POWER_BRACELET: "Power Bracelet", + SHIELD: "Shield", + BOW: "Bow", + HOOKSHOT: "Hookshot", + MAGIC_ROD: "Magic Rod", + PEGASUS_BOOTS: "Pegasus Boots", + OCARINA: "Ocarina", + FEATHER: "Roc's Feather", + SHOVEL: "Shovel", + MAGIC_POWDER: "Magic Powder", + BOOMERANG: "Boomerang", + ROOSTER: "Flying Rooster", + + FLIPPERS: "Flippers", + SLIME_KEY: "Slime key", + TAIL_KEY: "Tail key", + ANGLER_KEY: "Angler key", + FACE_KEY: "Face key", + BIRD_KEY: "Bird key", + GOLD_LEAF: "Golden leaf", + + "RUPEE": "Rupee", + "RUPEES": "Rupees", + RUPEES_50: "50 Rupees", + RUPEES_20: "20 Rupees", + RUPEES_100: "100 Rupees", + RUPEES_200: "200 Rupees", + RUPEES_500: "500 Rupees", + SEASHELL: "Secret Seashell", + + KEY: "Small Key", + KEY1: "Key for Tail Cave", + KEY2: "Key for Bottle Grotto", + KEY3: "Key for Key Cavern", + KEY4: "Key for Angler's Tunnel", + KEY5: "Key for Catfish's Maw", + KEY6: "Key for Face Shrine", + KEY7: "Key for Eagle's Tower", + KEY8: "Key for Turtle Rock", + KEY9: "Key for Color Dungeon", + + MAP: "Dungeon Map", + MAP1: "Map for Tail Cave", + MAP2: "Map for Bottle Grotto", + MAP3: "Map for Key Cavern", + MAP4: "Map for Angler's Tunnel", + MAP5: "Map for Catfish's Maw", + MAP6: "Map for Face Shrine", + MAP7: "Map for Eagle's Tower", + MAP8: "Map for Turtle Rock", + MAP9: "Map for Color Dungeon", + + COMPASS: "Dungeon Compass", + COMPASS1: "Compass for Tail Cave", + COMPASS2: "Compass for Bottle Grotto", + COMPASS3: "Compass for Key Cavern", + COMPASS4: "Compass for Angler's Tunnel", + COMPASS5: "Compass for Catfish's Maw", + COMPASS6: "Compass for Face Shrine", + COMPASS7: "Compass for Eagle's Tower", + COMPASS8: "Compass for Turtle Rock", + COMPASS9: "Compass for Color Dungeon", + + STONE_BEAK: "Stone Beak", + STONE_BEAK1: "Stone Beak for Tail Cave", + STONE_BEAK2: "Stone Beak for Bottle Grotto", + STONE_BEAK3: "Stone Beak for Key Cavern", + STONE_BEAK4: "Stone Beak for Angler's Tunnel", + STONE_BEAK5: "Stone Beak for Catfish's Maw", + STONE_BEAK6: "Stone Beak for Face Shrine", + STONE_BEAK7: "Stone Beak for Eagle's Tower", + STONE_BEAK8: "Stone Beak for Turtle Rock", + STONE_BEAK9: "Stone Beak for Color Dungeon", + + NIGHTMARE_KEY: "Nightmare Key", + NIGHTMARE_KEY1: "Nightmare Key for Tail Cave", + NIGHTMARE_KEY2: "Nightmare Key for Bottle Grotto", + NIGHTMARE_KEY3: "Nightmare Key for Key Cavern", + NIGHTMARE_KEY4: "Nightmare Key for Angler's Tunnel", + NIGHTMARE_KEY5: "Nightmare Key for Catfish's Maw", + NIGHTMARE_KEY6: "Nightmare Key for Face Shrine", + NIGHTMARE_KEY7: "Nightmare Key for Eagle's Tower", + NIGHTMARE_KEY8: "Nightmare Key for Turtle Rock", + NIGHTMARE_KEY9: "Nightmare Key for Color Dungeon", + + HEART_PIECE: "Piece of Heart", + BOWWOW: "Bowwow", + ARROWS_10: "10 Arrows", + SINGLE_ARROW: "Single Arrow", + MEDICINE: "Medicine", + + MAX_POWDER_UPGRADE: "Magic Powder upgrade", + MAX_BOMBS_UPGRADE: "Bombs upgrade", + MAX_ARROWS_UPGRADE: "Arrows upgrade", + + RED_TUNIC: "Red Tunic", + BLUE_TUNIC: "Blue Tunic", + + HEART_CONTAINER: "Heart Container", + BAD_HEART_CONTAINER: "Anti-Heart Container", + + TOADSTOOL: "Toadstool", + + SONG1: "Ballad of the Wind Fish", + SONG2: "Manbo's Mambo", + SONG3: "Frog's Song of Soul", + + INSTRUMENT1: "Full Moon Cello", + INSTRUMENT2: "Conch Horn", + INSTRUMENT3: "Sea Lily's Bell", + INSTRUMENT4: "Surf Harp", + INSTRUMENT5: "Wind Marimba", + INSTRUMENT6: "Coral Triangle", + INSTRUMENT7: "Organ of Evening Calm", + INSTRUMENT8: "Thunder Drum", + + TRADING_ITEM_YOSHI_DOLL: "Yoshi Doll", + TRADING_ITEM_RIBBON: "Ribbon", + TRADING_ITEM_DOG_FOOD: "Dog Food", + TRADING_ITEM_BANANAS: "Bananas", + TRADING_ITEM_STICK: "Stick", + TRADING_ITEM_HONEYCOMB: "Honeycomb", + TRADING_ITEM_PINEAPPLE: "Pineapple", + TRADING_ITEM_HIBISCUS: "Hibiscus", + TRADING_ITEM_LETTER: "Letter", + TRADING_ITEM_BROOM: "Broom", + TRADING_ITEM_FISHING_HOOK: "Fishing Hook", + TRADING_ITEM_NECKLACE: "Necklace", + TRADING_ITEM_SCALE: "Scale", + TRADING_ITEM_MAGNIFYING_GLASS: "Magnifying Lens", + GEL: "Slimy Surprise", + MESSAGE: "A Special Message From Our Sponsors" +} + + +def setReplacementName(key: str, value: str) -> None: + _NAMES[key] = value + + +def formatText(instr: str, *, center: bool = False, ask: Optional[str] = None) -> bytes: + instr = instr.format(**_NAMES) + s = instr.encode("ascii") + s = s.replace(b"'", b"^") + + def padLine(line: bytes) -> bytes: + return line + b' ' * (16 - len(line)) + if center: + def padLine(line: bytes) -> bytes: + padding = (16 - len(line)) + return b' ' * (padding // 2) + line + b' ' * (padding - padding // 2) + + result = b'' + for line in s.split(b'\n'): + result_line = b'' + for word in line.split(b' '): + if len(result_line) + 1 + len(word) > 16: + result += padLine(result_line) + result_line = b'' + elif result_line: + result_line += b' ' + result_line += word + if result_line: + result += padLine(result_line) + if ask is not None: + askbytes = ask.encode("ascii") + result = result.rstrip() + while len(result) % 32 != 16: + result += b' ' + return result + b' ' + askbytes + b'\xfe' + return result.rstrip() + b'\xff' + + +def tileDataToString(data: bytes, key: str = " 123") -> str: + result = "" + for n in range(0, len(data), 2): + a = data[n] + b = data[n+1] + for m in range(8): + bit = 0x80 >> m + if (a & bit) and (b & bit): + result += key[3] + elif (b & bit): + result += key[2] + elif (a & bit): + result += key[1] + else: + result += key[0] + result += "\n" + return result.rstrip("\n") + + +def createTileData(data: str, key: str = " 123") -> bytes: + result = [] + for line in data.split("\n"): + line = line + " " + a = 0 + b = 0 + for n in range(8): + if line[n] == key[3]: + a |= 0x80 >> n + b |= 0x80 >> n + elif line[n] == key[2]: + b |= 0x80 >> n + elif line[n] == key[1]: + a |= 0x80 >> n + result.append(a) + result.append(b) + assert (len(result) % 16) == 0, len(result) + return bytes(result) + + +if __name__ == "__main__": + data = formatText("It is dangurous to go alone.\nTake\nthis\na\nline.") + for i in range(0, len(data), 16): + print(data[i:i+16]) diff --git a/worlds/ladx/LADXR/worldSetup.py b/worlds/ladx/LADXR/worldSetup.py new file mode 100644 index 0000000000..d7ca37f203 --- /dev/null +++ b/worlds/ladx/LADXR/worldSetup.py @@ -0,0 +1,136 @@ +from .patches import enemies, bingo +from .locations.items import * +from .entranceInfo import ENTRANCE_INFO + + + +MULTI_CHEST_OPTIONS = [MAGIC_POWDER, BOMB, MEDICINE, RUPEES_50, RUPEES_20, RUPEES_100, RUPEES_200, RUPEES_500, SEASHELL, GEL, ARROWS_10, SINGLE_ARROW] +MULTI_CHEST_WEIGHTS = [20, 20, 20, 50, 50, 20, 10, 5, 5, 20, 10, 10] + +# List of all the possible locations where we can place our starting house +start_locations = [ + "phone_d8", + "rooster_house", + "writes_phone", + "castle_phone", + "photo_house", + "start_house", + "prairie_right_phone", + "banana_seller", + "prairie_low_phone", + "animal_phone", +] + + +class WorldSetup: + def __init__(self): + self.entrance_mapping = {k: k for k in ENTRANCE_INFO.keys()} + self.boss_mapping = list(range(9)) + self.miniboss_mapping = { + # Main minibosses + 0: "ROLLING_BONES", 1: "HINOX", 2: "DODONGO", 3: "CUE_BALL", 4: "GHOMA", 5: "SMASHER", 6: "GRIM_CREEPER", 7: "BLAINO", + # Color dungeon needs to be special, as always. + "c1": "AVALAUNCH", "c2": "GIANT_BUZZ_BLOB", + # Overworld + "moblin_cave": "MOBLIN_KING", + "armos_temple": "ARMOS_KNIGHT", + } + self.goal = None + self.bingo_goals = None + self.multichest = RUPEES_20 + self.map = None # Randomly generated map data + + def getEntrancePool(self, settings, connectorsOnly=False): + entrances = [] + + if connectorsOnly: + if settings.entranceshuffle in ("advanced", "expert", "insanity"): + entrances = [k for k, v in ENTRANCE_INFO.items() if v.type == "connector"] + + return entrances + + if settings.dungeonshuffle and settings.entranceshuffle == "none": + entrances = [k for k, v in ENTRANCE_INFO.items() if v.type == "dungeon"] + if settings.entranceshuffle in ("simple", "advanced", "expert", "insanity"): + types = {"single"} + if settings.tradequest: + types.add("trade") + if settings.entranceshuffle in ("expert", "insanity"): + types.update(["dummy", "trade"]) + if settings.entranceshuffle in ("insanity",): + types.add("insanity") + if settings.randomstartlocation: + types.add("start") + if settings.dungeonshuffle: + types.add("dungeon") + entrances = [k for k, v in ENTRANCE_INFO.items() if v.type in types] + + return entrances + + def randomize(self, settings, rnd): + if settings.overworld == "dungeondive": + self.entrance_mapping = {"d%d" % (n): "d%d" % (n) for n in range(9)} + if settings.randomstartlocation and settings.entranceshuffle == "none": + start_location = start_locations[rnd.randrange(len(start_locations))] + if start_location != "start_house": + self.entrance_mapping[start_location] = "start_house" + self.entrance_mapping["start_house"] = start_location + + entrances = self.getEntrancePool(settings) + for entrance in entrances.copy(): + self.entrance_mapping[entrance] = entrances.pop(rnd.randrange(len(entrances))) + + # Shuffle connectors among themselves + entrances = self.getEntrancePool(settings, connectorsOnly=True) + for entrance in entrances.copy(): + self.entrance_mapping[entrance] = entrances.pop(rnd.randrange(len(entrances))) + + if settings.boss != "default": + values = list(range(9)) + if settings.heartcontainers: + # Color dungeon boss does not drop a heart container so we cannot shuffle him when we + # have heart container shuffling + values.remove(8) + self.boss_mapping = [] + for n in range(8 if settings.heartcontainers else 9): + value = rnd.choice(values) + self.boss_mapping.append(value) + if value in (3, 6) or settings.boss == "shuffle": + values.remove(value) + if settings.heartcontainers: + self.boss_mapping += [8] + if settings.miniboss != "default": + values = [name for name in self.miniboss_mapping.values()] + for key in self.miniboss_mapping.keys(): + self.miniboss_mapping[key] = rnd.choice(values) + if settings.miniboss == 'shuffle': + values.remove(self.miniboss_mapping[key]) + + if settings.goal == 'random': + self.goal = rnd.randint(-1, 8) + elif settings.goal == 'open': + self.goal = -1 + elif settings.goal in {"seashells", "bingo", "bingo-full"}: + self.goal = settings.goal + elif "-" in settings.goal: + a, b = settings.goal.split("-") + if a == "open": + a = -1 + self.goal = rnd.randint(int(a), int(b)) + else: + self.goal = int(settings.goal) + if self.goal in {"bingo", "bingo-full"}: + self.bingo_goals = bingo.randomizeGoals(rnd, settings) + + self.multichest = rnd.choices(MULTI_CHEST_OPTIONS, MULTI_CHEST_WEIGHTS)[0] + + def loadFromRom(self, rom): + import patches.overworld + if patches.overworld.isNormalOverworld(rom): + import patches.entrances + self.entrance_mapping = patches.entrances.readEntrances(rom) + else: + self.entrance_mapping = {"d%d" % (n): "d%d" % (n) for n in range(9)} + self.boss_mapping = patches.enemies.readBossMapping(rom) + self.miniboss_mapping = patches.enemies.readMiniBossMapping(rom) + self.goal = 8 # Better then nothing diff --git a/worlds/ladx/Locations.py b/worlds/ladx/Locations.py new file mode 100644 index 0000000000..69eb78dd88 --- /dev/null +++ b/worlds/ladx/Locations.py @@ -0,0 +1,247 @@ +from BaseClasses import Region, Entrance, Location +from worlds.AutoWorld import LogicMixin + + +from .LADXR.checkMetadata import checkMetadataTable +from .Common import * +from worlds.generic.Rules import add_item_rule +from .Items import ladxr_item_to_la_item_name, ItemName, LinksAwakeningItem +from .LADXR.locations.tradeSequence import TradeRequirements, TradeSequenceItem + +prefilled_events = ["ANGLER_KEYHOLE", "RAFT", "MEDICINE2", "CASTLE_BUTTON"] + +links_awakening_dungeon_names = [ + "Tail Cave", + "Bottle Grotto", + "Key Cavern", + "Angler's Tunnel", + "Catfish's Maw", + "Face Shrine", + "Eagle's Tower", + "Turtle Rock", + "Color Dungeon" +] + + +def meta_to_name(meta): + return f"{meta.name} ({meta.area})" + + +def get_locations_to_id(): + ret = { + + } + + # Magic to generate unique ids + for s, v in checkMetadataTable.items(): + if s == "None": + continue + splits = s.split("-") + + main_id = int(splits[0], 16) + sub_id = 0 + if len(splits) > 1: + sub_id = splits[1] + if sub_id.isnumeric(): + sub_id = (int(sub_id) + 1) * 1000 + else: + sub_id = 1000 + name = f"{v.name} ({v.area})" + ret[name] = BASE_ID + main_id + sub_id + + return ret + + +locations_to_id = get_locations_to_id() + + +class LinksAwakeningLocation(Location): + game = LINKS_AWAKENING + dungeon = None + + def __init__(self, player: int, region, ladxr_item): + name = meta_to_name(ladxr_item.metadata) + + self.event = ladxr_item.event is not None + if self.event: + name = ladxr_item.event + + address = None + if not self.event: + address = locations_to_id[name] + super().__init__(player, name, address) + self.parent_region = region + self.ladxr_item = ladxr_item + + def filter_item(item): + if not ladxr_item.MULTIWORLD and item.player != player: + return False + return True + add_item_rule(self, filter_item) + + +def has_free_weapon(state: "CollectionState", player: int) -> bool: + return state.has("Progressive Sword", player) or state.has("Magic Rod", player) or state.has("Boomerang", player) or state.has("Hookshot", player) + +# If the player has access to farm enough rupees to afford a game, we assume that they can keep beating the game +def can_farm_rupees(state: "CollectionState", player: int) -> bool: + return has_free_weapon(state, player) and (state.has("Can Play Trendy Game", player=player) or state.has("RAFT", player=player)) + + +class LinksAwakeningLogic(LogicMixin): + rupees = { + ItemName.RUPEES_20: 0, + ItemName.RUPEES_50: 0, + ItemName.RUPEES_100: 100, + ItemName.RUPEES_200: 200, + ItemName.RUPEES_500: 500, + } + + def get_credits(self, player: int): + if can_farm_rupees(self, player): + return 999999999 + return sum(self.count(item_name, player) * amount for item_name, amount in self.rupees.items()) + + +class LinksAwakeningRegion(Region): + dungeon_index = None + ladxr_region = None + + def __init__(self, name, ladxr_region, hint, player, world): + super().__init__(name, player, world, hint) + if ladxr_region: + self.ladxr_region = ladxr_region + if ladxr_region.dungeon: + self.dungeon_index = ladxr_region.dungeon + + +def translate_item_name(item): + if item in ladxr_item_to_la_item_name: + return ladxr_item_to_la_item_name[item] + + return item + + +class GameStateAdapater: + def __init__(self, state, player): + self.state = state + self.player = player + + def __contains__(self, item): + if item.endswith("_USED"): + return False + if item in ladxr_item_to_la_item_name: + item = ladxr_item_to_la_item_name[item] + + return self.state.has(item, self.player) + + def get(self, item, default): + if item == "RUPEES": + return self.state.get_credits(self.player) + elif item.endswith("_USED"): + return 0 + else: + item = ladxr_item_to_la_item_name[item] + return self.state.prog_items.get((item, self.player), default) + + +class LinksAwakeningEntrance(Entrance): + def __init__(self, player: int, name, region, condition): + super().__init__(player, name, region) + if isinstance(condition, str): + if condition in ladxr_item_to_la_item_name: + # Test if in inventory + self.condition = ladxr_item_to_la_item_name[condition] + else: + # Event + self.condition = condition + elif condition: + # rewrite condition + # .copyWithModifiedItemNames(translate_item_name) + self.condition = condition + else: + self.condition = None + + def access_rule(self, state): + if isinstance(self.condition, str): + return state.has(self.condition, self.player) + if self.condition is None: + return True + + return self.condition.test(GameStateAdapater(state, self.player)) + + +# Helper to apply function to every ladxr region +def walk_ladxdr(f, n, walked=set()): + if n in walked: + return + f(n) + walked.add(n) + + for o, req in n.simple_connections: + walk_ladxdr(f, o, walked) + for o, req in n.gated_connections: + walk_ladxdr(f, o, walked) + + +def ladxr_region_to_name(n): + name = n.name + if not name: + if len(n.items) == 1: + meta = n.items[0].metadata + name = f"{meta.name} ({meta.area})" + elif n.dungeon: + name = f"D{n.dungeon} Room" + else: + name = "No Name" + + return name + + +def create_regions_from_ladxr(player, multiworld, logic): + tmp = set() + + def print_items(n): + print(f"Creating Region {ladxr_region_to_name(n)}") + print("Has simple connections:") + for region, info in n.simple_connections: + print(" " + ladxr_region_to_name(region) + " | " + str(info)) + print("Has gated connections:") + + for region, info in n.gated_connections: + print(" " + ladxr_region_to_name(region) + " | " + str(info)) + + print("Has Locations:") + for item in n.items: + print(" " + str(item.metadata)) + print() + + used_names = {} + + regions = {} + + # Create regions + for l in logic.location_list: + # Temporarily uniqueify the name, until all regions are named + name = ladxr_region_to_name(l) + index = used_names.get(name, 0) + 1 + used_names[name] = index + if index != 1: + name += f" {index}" + + r = LinksAwakeningRegion( + name=name, ladxr_region=l, hint="", player=player, world=multiworld) + r.locations = [LinksAwakeningLocation(player, r, i) for i in l.items] + regions[l] = r + + for ladxr_location in logic.location_list: + for connection_location, connection_condition in ladxr_location.simple_connections + ladxr_location.gated_connections: + region_a = regions[ladxr_location] + region_b = regions[connection_location] + # TODO: This name ain't gonna work for entrance rando, we need to cross reference with logic.world.overworld_entrance + entrance = LinksAwakeningEntrance( + player, f"{region_a.name} -> {region_b.name}", region_a, connection_condition) + region_a.exits.append(entrance) + entrance.connect(region_b) + + return list(regions.values()) diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py new file mode 100644 index 0000000000..37055b7a2f --- /dev/null +++ b/worlds/ladx/Options.py @@ -0,0 +1,366 @@ +import os.path +import typing +import logging +from Options import Choice, Option, Toggle, DefaultOnToggle, Range, FreeText +from collections import defaultdict + +DefaultOffToggle = Toggle + +logger = logging.getLogger("Link's Awakening Logger") + + +class LADXROption: + def to_ladxr_option(self, all_options): + if not self.ladxr_name: + return None, None + + return (self.ladxr_name, self.name_lookup[self.value].replace("_", "")) + + +class Logic(Choice, LADXROption): + """Affects where items are allowed to be placed. + [Normal] Playable without using any tricks or glitches. Can require knowledge from a vanilla playthrough, such as how to open Color Dungeon. + [Hard] More advanced techniques may be required, but glitches are not. Examples include tricky jumps, killing enemies with only pots. + [Glitched] Advanced glitches and techniques may be required, but extremely difficult or tedious tricks are not required. Examples include Bomb Triggers, Super Jumps and Jesus Jumps. + [Hell] Obscure knowledge and hard techniques may be required. Examples include featherless jumping with boots and/or hookshot, sequential pit buffers and unclipped superjumps. Things in here can be extremely hard to do or very time consuming.""" + display_name = "Logic" + ladxr_name = "logic" + # option_casual = 0 + option_normal = 1 + option_hard = 2 + option_glitched = 3 + option_hell = 4 + + default = option_normal + +class TradeQuest(DefaultOffToggle, LADXROption): + """ + On - adds the trade items to the pool (the trade locations will always be local items) + Off - (default) doesn't add them + """ + ladxr_name = "tradequest" + +class Boomerang(Choice): + """ + [Normal], requires Magnifying Lens to get the boomerang. + [Gift], The boomerang salesman will give you a random item, and the boomerang is shuffled. + """ + + normal = 0 + gift = 1 + default = gift + +# TODO: translate to lttp parlance +class EntranceShuffle(Choice, LADXROption): + """ + [WARNING] Experimental, may break generation + Randomizes where overworld entrances lead to. + [Simple] Single-entrance caves/houses that have items are shuffled amongst each other. + [Advanced] Simple, but two-way connector caves are shuffled in their own pool as well. + [Expert] Advanced, but caves/houses without items are also shuffled into the Simple entrance pool. + [Insanity] Expert, but the Raft Minigame hut and Mamu's cave are added to the non-connector pool. + If random start location and/or dungeon shuffle is enabled, then these will be shuffled with all the non-connector entrance pool. + Note, some entrances can lead into water, use the warp-to-home from the save&quit menu to escape this.""" + option_none = 0 + option_simple = 1 + #option_advanced = 2 + #option_expert = 3 + #option_insanity = 4 + default = option_none + ladxr_name = "entranceshuffle" + +class DungeonShuffle(DefaultOffToggle, LADXROption): + """ + [WARNING] Experimental, may break generation + Randomizes + """ + ladxr_name = "dungeonshuffle" + +class BossShuffle(Choice): + none = 0 + shuffle = 1 + random = 2 + default = none + + +class DungeonItemShuffle(Choice): + option_original_dungeon = 0 + option_own_dungeons = 1 + option_own_world = 2 + option_any_world = 3 + option_different_world = 4 + #option_delete = 5 + #option_start_with = 6 + alias_true = 3 + alias_false = 0 + +class ShuffleNightmareKeys(DungeonItemShuffle): + """ + Shuffle Nightmare Keys + """ + ladxr_item = "NIGHTMARE_KEY" +class ShuffleSmallKeys(DungeonItemShuffle): + """ + Shuffle Small Keys + """ + ladxr_item = "KEY" +class ShuffleMaps(DungeonItemShuffle): + """ + Shuffle Dungeon Maps + """ + ladxr_item = "MAP" +class ShuffleCompasses(DungeonItemShuffle): + """ + Shuffle Dungeon Compasses + """ + ladxr_item = "COMPASS" +class ShuffleStoneBeaks(DungeonItemShuffle): + """ + Shuffle Owl Beaks + """ + ladxr_item = "STONE_BEAK" +class Goal(Choice, LADXROption): + """ + [Instruments] The Wind Fish's Egg will only open if you have the required number of Instruments of the Sirens, and play the Ballad of the Wind Fish. + [Seashells] The Egg will open when you bring 20 seashells. The Ballad and Ocarina are not needed. + [Open] The Egg will start pre-opened. + """ + display_name = "Goal" + ladxr_name = "goal" + option_instruments = 1 + option_seashells = 2 + option_open = 3 + + default = option_instruments + + + def to_ladxr_option(self, all_options): + + if self.value == self.option_instruments: + return ("goal", all_options["instrument_count"]) + else: + return LADXROption.to_ladxr_option(self, all_options) + +class InstrumentCount(Range, LADXROption): + ladxr_name = None + range_start = 0 + range_end = 8 + default = 8 + +#class SeashellCount(Range): +# range_start = 0 +# range_end = 20 +# default = 20 + +# Setting('goal', 'Gameplay', 'G', 'Goal', options=[('8', '8', '8 instruments'), ('7', '7', '7 instruments'), ('6', '6', '6 instruments'), +# ('5', '5', '5 instruments'), ('4', '4', '4 instruments'), ('3', '3', '3 instruments'), +# ('2', '2', '2 instruments'), ('1', '1', '1 instrument'), ('0', '0', 'No instruments'), +# ('open', 'O', 'Egg already open'), ('random', 'R', 'Random instrument count'), +# ('open-4', '<', 'Random short game (0-4)'), ('5-8', '>', 'Random long game (5-8)'), +# ('seashells', 'S', 'Seashell hunt (20)'), ('bingo', 'b', 'Bingo!'), +# ('bingo-full', 'B', 'Bingo-25!')], default='8', +# description="""Changes the goal of the game. +# [1-8 instruments], number of instruments required to open the egg. +# [No instruments] open the egg without instruments, still requires the ocarina with the balled of the windfish +# [Egg already open] the egg is already open, just head for it once you have the items needed to defeat the boss. +# [Randomized instrument count] random number of instruments required to open the egg, between 0 and 8. +# [Random short/long game] random number of instruments required to open the egg, chosen between 0-4 and 5-8 respectively. +# [Seashell hunt] egg will open once you collected 20 seashells. Instruments are replaced by seashells and shuffled. +# [Bingo] Generate a 5x5 bingo board with various goals. Complete one row/column or diagonal to win! +# [Bingo-25] Bingo, but need to fill the whole bingo card to win!"""), +class ItemPool(Choice): + """Effects which items are shuffled. +[Casual] Places multiple copies of key items. +[More keys] Adds additional small/nightmare keys so that dungeons are faster. +[Path of Pain]. Adds negative heart containers to the item pool.""" + casual = 0 + more_keys = 1 + normal = 2 + painful = 3 + default = normal + +# Setting('hpmode', 'Gameplay', 'm', 'Health mode', options=[('default', '', 'Normal'), ('inverted', 'i', 'Inverted'), ('1', '1', 'Start with 1 heart'), ('low', 'l', 'Low max')], default='default', +# description=""" +# [Normal} health works as you would expect. +# [Inverted] you start with 9 heart containers, but killing a boss will take a heartcontainer instead of giving one. +# [Start with 1] normal game, you just start with 1 heart instead of 3. +# [Low max] replace heart containers with heart pieces."""), + +# Setting('hardmode', 'Gameplay', 'X', 'Hard mode', options=[('none', '', 'Disabled'), ('oracle', 'O', 'Oracle'), ('hero', 'H', 'Hero'), ('ohko', '1', 'One hit KO')], default='none', +# description=""" +# [Oracle] Less iframes and heath from drops. Bombs damage yourself. Water damages you without flippers. No piece of power or acorn. +# [Hero] Switch version hero mode, double damage, no heart/fairy drops. +# [One hit KO] You die on a single hit, always."""), + +# Setting('steal', 'Gameplay', 't', 'Stealing from the shop', +# options=[('always', 'a', 'Always'), ('never', 'n', 'Never'), ('default', '', 'Normal')], default='default', +# description="""Effects when you can steal from the shop. Stealing is bad and never in logic. +# [Normal] requires the sword before you can steal. +# [Always] you can always steal from the shop +# [Never] you can never steal from the shop."""), +class Bowwow(Choice): + """Allows BowWow to be taken into any area. Certain enemies and bosses are given a new weakness to BowWow. + [Normal] BowWow is in the item pool, but can be logically expected as a damage source. + [Swordless] The progressive swords are removed from the item pool. + """ + normal = 0 + swordless = 1 + default = normal + +class Overworld(Choice, LADXROption): + """ + [Dungeon Dive] Create a different overworld where all the dungeons are directly accessible and almost no chests are located in the overworld. + [Tiny dungeons] All dungeons only consist of a boss fight and a instrument reward. Rest of the dungeon is removed. + """ + display_name = "Overworld" + ladxr_name = "overworld" + option_normal = 0 + option_dungeon_dive = 1 + option_tiny_dungeons = 2 + # option_shuffled = 3 + default = option_normal + +# Ugh, this will change what 'progression' means?? +#Setting('owlstatues', 'Special', 'o', 'Owl statues', options=[('', '', 'Never'), ('dungeon', 'D', 'In dungeons'), ('overworld', 'O', 'On the overworld'), ('both', 'B', 'Dungeons and Overworld')], default='', +# description='Replaces the hints from owl statues with additional randomized items'), + +#Setting('superweapons', 'Special', 'q', 'Enable super weapons', default=False, +# description='All items will be more powerful, faster, harder, bigger stronger. You name it.'), +#Setting('quickswap', 'User options', 'Q', 'Quickswap', options=[('none', '', 'Disabled'), ('a', 'a', 'Swap A button'), ('b', 'b', 'Swap B button')], default='none', +# description='Adds that the select button swaps with either A or B. The item is swapped with the top inventory slot. The map is not available when quickswap is enabled.', +# aesthetic=True), +# Setting('textmode', 'User options', 'f', 'Text mode', options=[('fast', '', 'Fast'), ('default', 'd', 'Normal'), ('none', 'n', 'No-text')], default='fast', +# description="""[Fast] makes text appear twice as fast. +# [No-Text] removes all text from the game""", aesthetic=True), +# Setting('lowhpbeep', 'User options', 'p', 'Low HP beeps', options=[('none', 'D', 'Disabled'), ('slow', 'S', 'Slow'), ('default', 'N', 'Normal')], default='slow', +# description='Slows or disables the low health beeping sound', aesthetic=True), +# Setting('noflash', 'User options', 'l', 'Remove flashing lights', default=True, +# description='Remove the flashing light effects from Mamu, shopkeeper and MadBatter. Useful for capture cards and people that are sensitive for these things.', +# aesthetic=True), +# Setting('nagmessages', 'User options', 'S', 'Show nag messages', default=False, +# description='Enables the nag messages normally shown when touching stones and crystals', +# aesthetic=True), +# Setting('gfxmod', 'User options', 'c', 'Graphics', options=gfx_options, default='', +# description='Generally affects at least Link\'s sprite, but can alter any graphics in the game', +# aesthetic=True), +# Setting('linkspalette', 'User options', 'C', "Link's color", +# options=[('-1', '-', 'Normal'), ('0', '0', 'Green'), ('1', '1', 'Yellow'), ('2', '2', 'Red'), ('3', '3', 'Blue'), +# ('4', '4', '?? A'), ('5', '5', '?? B'), ('6', '6', '?? C'), ('7', '7', '?? D')], default='-1', aesthetic=True, +# description="""Allows you to force a certain color on link. +# [Normal] color of link depends on the tunic. +# [Green/Yellow/Red/Blue] forces link into one of these colors. +# [?? A/B/C/D] colors of link are usually inverted and color depends on the area you are in."""), +# Setting('music', 'User options', 'M', 'Music', options=[('', '', 'Default'), ('random', 'r', 'Random'), ('off', 'o', 'Disable')], default='', +# description=""" +# [Random] Randomizes overworld and dungeon music' +# [Disable] no music in the whole game""", +# aesthetic=True), + +class LinkPalette(Choice, LADXROption): + """ + A-D are color palettes usually used during the damage animation and can change based on where you are. + """ + display_name = "Links Palette" + ladxr_name = "linkspalette" + option_normal = -1 + option_green = 0 + option_yellow = 1 + option_red = 2 + option_blue = 3 + option_invert_a = 4 + option_invert_b = 5 + option_invert_c = 6 + option_invert_d = 7 + default = option_normal + + def to_ladxr_option(self, all_options): + return self.ladxr_name, str(self.value) + +class TrendyGame(Choice): + """ + [Easy] All of the items hold still for you + [Normal] The vanilla behavior + [Hard] ? + [Harder] ??? + [Hardest] ???? + [Impossible] ????? + """ + option_easy = 0 + option_normal = 1 + option_hard = 2 + option_harder = 3 + option_hardest = 4 + option_impossible = 5 + default = option_normal + +class GfxMod(FreeText, LADXROption): + """ + options here correlate with sprite and name files in data/sprites/ladx + """ + display_name = "GFX Modification" + ladxr_name = "gfxmod" + normal = '' + default = 'Link' + + __spriteFiles: typing.DefaultDict[str, typing.List[str]] = defaultdict(list) + __spriteDir = os.path.join('data', 'sprites','ladx') + + extensions = [".bin", ".bdiff", ".png", ".bmp"] + def __init__(self, value: str): + super().__init__(value) + if not GfxMod.__spriteFiles: + for file in os.listdir(GfxMod.__spriteDir): + name, extension = os.path.splitext(file) + if extension in self.extensions: + GfxMod.__spriteFiles[name].append(file) + + + def to_ladxr_option(self, all_options): + if self.value == -1 or self.value == "Link": + return None, None + elif self.value in GfxMod.__spriteFiles: + if len(GfxMod.__spriteFiles[self.value]) > 1: + logger.warning(f"{self.value} does not uniquely identify a file. Possible matches: {GfxMod.__spriteFiles[self.value]}. Using {GfxMod.__spriteFiles[self.value][0]}") + return self.ladxr_name, GfxMod.__spriteFiles[self.value][0] + return self.ladxr_name, GfxMod.__spriteFiles[self.value][0] + logger.error(f"Spritesheet {self.value} not found. Falling back to default sprite.") + return None, None + +class Palette(Choice): + option_normal = 0 + option_1bit = 1 + option_2bit = 2 + option_greyscale = 3 + option_pink = 4 + option_inverted = 5 + +links_awakening_options: typing.Dict[str, typing.Type[Option]] = { + 'logic': Logic, + # 'heartpiece': DefaultOnToggle, # description='Includes heart pieces in the item pool'), + # 'seashells': DefaultOnToggle, # description='Randomizes the secret sea shells hiding in the ground/trees. (chest are always randomized)'), + # 'heartcontainers': DefaultOnToggle, # description='Includes boss heart container drops in the item pool'), + # 'instruments': DefaultOffToggle, # description='Instruments are placed on random locations, dungeon goal will just contain a random item.'), + 'tradequest': TradeQuest, # description='Trade quest items are randomized, each NPC takes its normal trade quest item, but gives a random item'), + # 'witch': DefaultOnToggle, # description='Adds both the toadstool and the reward for giving the toadstool to the witch to the item pool'), + # 'rooster': DefaultOnToggle, # description='Adds the rooster to the item pool. Without this option, the rooster spot is still a check giving an item. But you will never find the rooster. Any rooster spot is accessible without rooster by other means.'), + # 'boomerang': Boomerang, + # 'randomstartlocation': DefaultOffToggle, # 'Randomize where your starting house is located'), + 'experimental_dungeon_shuffle': DungeonShuffle, # 'Randomizes the dungeon that each dungeon entrance leads to'), + 'experimental_entrance_shuffle': EntranceShuffle, + # 'bossshuffle': BossShuffle, + # 'minibossshuffle': BossShuffle, + 'goal': Goal, + 'instrument_count': InstrumentCount, + # 'itempool': ItemPool, + # 'bowwow': Bowwow, + # 'overworld': Overworld, + 'link_palette': LinkPalette, + 'trendy_game': TrendyGame, + 'gfxmod': GfxMod, + 'palette': Palette, + 'shuffle_nightmare_keys': ShuffleNightmareKeys, + 'shuffle_small_keys': ShuffleSmallKeys, + 'shuffle_maps': ShuffleMaps, + 'shuffle_compasses': ShuffleCompasses, + 'shuffle_stone_beaks': ShuffleStoneBeaks, +} diff --git a/worlds/ladx/Rom.py b/worlds/ladx/Rom.py new file mode 100644 index 0000000000..eb573fe5b2 --- /dev/null +++ b/worlds/ladx/Rom.py @@ -0,0 +1,40 @@ + +import worlds.Files +import hashlib +import Utils +import os +LADX_HASH = "07c211479386825042efb4ad31bb525f" + +class LADXDeltaPatch(worlds.Files.APDeltaPatch): + hash = LADX_HASH + game = "Links Awakening DX" + patch_file_ending = ".apladx" + result_file_ending: str = ".gbc" + + @classmethod + def get_source_data(cls) -> bytes: + return get_base_rom_bytes() + + +def get_base_rom_bytes(file_name: str = "") -> bytes: + base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None) + if not base_rom_bytes: + file_name = get_base_rom_path(file_name) + base_rom_bytes = bytes(open(file_name, "rb").read()) + + basemd5 = hashlib.md5() + basemd5.update(base_rom_bytes) + if LADX_HASH != basemd5.hexdigest(): + raise Exception('Supplied Base Rom does not match known MD5 for USA release. ' + 'Get the correct game and version, then dump it') + get_base_rom_bytes.base_rom_bytes = base_rom_bytes + return base_rom_bytes + + +def get_base_rom_path(file_name: str = "") -> str: + options = Utils.get_options() + if not file_name: + file_name = options["ladx_options"]["rom_file"] + if not os.path.exists(file_name): + file_name = Utils.user_path(file_name) + return file_name diff --git a/worlds/ladx/Tracker.py b/worlds/ladx/Tracker.py new file mode 100644 index 0000000000..b3995db019 --- /dev/null +++ b/worlds/ladx/Tracker.py @@ -0,0 +1,236 @@ +from worlds.ladx.LADXR.checkMetadata import checkMetadataTable +import json +import logging +import websockets +import asyncio + +logger = logging.getLogger("Tracker") + + +# kbranch you're a hero +# https://github.com/kbranch/Magpie/blob/master/autotracking/checks.py +class Check: + def __init__(self, id, address, mask, alternateAddress=None): + self.id = id + self.address = address + self.alternateAddress = alternateAddress + self.mask = mask + self.value = None + self.diff = 0 + + def set(self, bytes): + oldValue = self.value + + self.value = 0 + + for byte in bytes: + maskedByte = byte + if self.mask: + maskedByte &= self.mask + + self.value |= int(maskedByte > 0) + + if oldValue != self.value: + self.diff += self.value - (oldValue or 0) +# Todo: unify this with existing item tables? + + +class LocationTracker: + all_checks = [] + + def __init__(self, gameboy): + self.gameboy = gameboy + maskOverrides = { + '0x106': 0x20, + '0x12B': 0x20, + '0x15A': 0x20, + '0x166': 0x20, + '0x185': 0x20, + '0x1E4': 0x20, + '0x1BC': 0x20, + '0x1E0': 0x20, + '0x1E1': 0x20, + '0x1E2': 0x20, + '0x223': 0x20, + '0x234': 0x20, + '0x2A3': 0x20, + '0x2FD': 0x20, + '0x2A7': 0x20, + '0x1F5': 0x06, + '0x301-0': 0x10, + '0x301-1': 0x10, + } + + addressOverrides = { + '0x30A-Owl': 0xDDEA, + '0x30F-Owl': 0xDDEF, + '0x308-Owl': 0xDDE8, + '0x302': 0xDDE2, + '0x306': 0xDDE6, + '0x307': 0xDDE7, + '0x308': 0xDDE8, + '0x30F': 0xDDEF, + '0x311': 0xDDF1, + '0x314': 0xDDF4, + '0x1F5': 0xDB7D, + '0x301-0': 0xDDE1, + '0x301-1': 0xDDE1, + '0x223': 0xDA2E, + '0x169': 0xD97C, + '0x2A7': 0xD800 + 0x2A1 + } + + alternateAddresses = { + '0x0F2': 0xD8B2, + } + + blacklist = {'None', '0x2A1-2'} + + # in no dungeons boss shuffle, the d3 boss in d7 set 0x20 in fascade's room (0x1BC) + # after beating evil eagile in D6, 0x1BC is now 0xAC (other things may have happened in between) + # entered d3, slime eye flag had already been set (0x15A 0x20). after killing angler fish, bits 0x0C were set + lowest_check = 0xffff + highest_check = 0 + + for check_id in [x for x in checkMetadataTable if x not in blacklist]: + room = check_id.split('-')[0] + mask = 0x10 + address = addressOverrides[check_id] if check_id in addressOverrides else 0xD800 + int( + room, 16) + + if 'Trade' in check_id or 'Owl' in check_id: + mask = 0x20 + + if check_id in maskOverrides: + mask = maskOverrides[check_id] + + lowest_check = min(lowest_check, address) + highest_check = max(highest_check, address) + if check_id in alternateAddresses: + lowest_check = min(lowest_check, alternateAddresses[check_id]) + highest_check = max( + highest_check, alternateAddresses[check_id]) + + check = Check(check_id, address, mask, + alternateAddresses[check_id] if check_id in alternateAddresses else None) + if check_id == '0x2A3': + self.start_check = check + self.all_checks.append(check) + self.remaining_checks = [check for check in self.all_checks] + self.gameboy.set_cache_limits( + lowest_check, highest_check - lowest_check + 1) + + def has_start_item(self): + return self.start_check not in self.remaining_checks + + async def readChecks(self, cb): + new_checks = [] + for check in self.remaining_checks: + addresses = [check.address] + if check.alternateAddress: + addresses.append(check.alternateAddress) + bytes = await self.gameboy.read_memory_cache(addresses) + if not bytes: + return False + check.set(list(bytes.values())) + + if check.value: + self.remaining_checks.remove(check) + new_checks.append(check) + if new_checks: + cb(new_checks) + return True + + +class MagpieBridge: + port = 17026 + server = None + checks = None + item_tracker = None + ws = None + + async def handler(self, websocket): + self.ws = websocket + while True: + message = json.loads(await websocket.recv()) + if message["type"] == "handshake": + logger.info( + f"Connected, supported features: {message['features']}") + if "items" in message["features"]: + await self.send_all_inventory() + if "checks" in message["features"]: + await self.send_all_checks() + + async def send_all_checks(self): + while self.checks == None: + await asyncio.sleep(0.1) + logger.info("sending all checks to magpie") + # Translate renamed IDs back to LADXR IDs + def fixup_id(the_id): + if the_id == "0x2A1": + return "0x2A1-0" + if the_id == "0x2A7": + return "0x2A1-1" + return the_id + + message = { + "type": "check", + "refresh": True, + "version": "1.0", + "diff": False, + "checks": [{"id": fixup_id(check.id), "checked": check.value} for check in self.checks] + } + + await self.ws.send(json.dumps(message)) + + async def send_new_checks(self, checks): + if not self.ws: + return + + logger.debug("Sending new {checks} to magpie") + message = { + "type": "check", + "refresh": True, + "version": "1.0", + "diff": True, + "checks": [{"id": check, "checked": True} for check in checks] + } + + await self.ws.send(json.dumps(message)) + + async def send_all_inventory(self): + logger.info("Sending inventory to magpie") + + while self.item_tracker == None: + await asyncio.sleep(0.1) + + await self.item_tracker.sendItems(self.ws) + + async def send_inventory_diffs(self): + if not self.ws: + return + if not self.item_tracker: + return + await self.item_tracker.sendItems(self.ws, diff=True) + + async def send_gps(self, gps): + if not self.ws: + return + await gps.send_location(self.ws) + + async def serve(self): + async with websockets.serve(lambda w: self.handler(w), "", 17026, logger=logger): + await asyncio.Future() # run forever + + def set_checks(self, checks): + self.checks = checks + + async def set_item_tracker(self, item_tracker): + stale_tracker = self.item_tracker != item_tracker + self.item_tracker = item_tracker + if stale_tracker: + if self.ws: + await self.send_all_inventory() + else: + await self.send_inventory_diffs() + diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py new file mode 100644 index 0000000000..2c213fc75b --- /dev/null +++ b/worlds/ladx/__init__.py @@ -0,0 +1,418 @@ +import binascii +import os + +from BaseClasses import Entrance, Item, ItemClassification, Location, Tutorial +from Fill import fill_restrictive +from worlds.AutoWorld import WebWorld, World + +from .Common import * +from .Items import (DungeonItemData, DungeonItemType, LinksAwakeningItem, + ladxr_item_to_la_item_name, links_awakening_items, + links_awakening_items_by_name) +from .LADXR import generator +from .LADXR.itempool import ItemPool as LADXRItemPool +from .LADXR.locations.tradeSequence import TradeSequenceItem +from .LADXR.logic import Logic as LAXDRLogic +from .LADXR.main import get_parser +from .LADXR.settings import Settings as LADXRSettings +from .LADXR.worldSetup import WorldSetup as LADXRWorldSetup +from .LADXR.locations.instrument import Instrument +from .LADXR.locations.constants import CHEST_ITEMS +from .Locations import (LinksAwakeningLocation, LinksAwakeningRegion, + create_regions_from_ladxr, get_locations_to_id) +from .Options import links_awakening_options +from .Rom import LADXDeltaPatch + +DEVELOPER_MODE = False + +class LinksAwakeningWebWorld(WebWorld): + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to setting up Links Awakening DX for MultiWorld.", + "English", + "setup_en.md", + "setup/en", + ["zig"] + )] + theme = "dirt" + +class LinksAwakeningWorld(World): + """Insert description of the world/game here.""" + game: str = LINKS_AWAKENING # name of the game/world + web = LinksAwakeningWebWorld() + + option_definitions = links_awakening_options # options the player can set + 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 + # Instead of dynamic numbering, IDs could be part of data. + + # The following two dicts are required for the generation to know which + # items exist. They could be generated from json or something else. They can + # include events, but don't have to since events will be placed manually. + item_name_to_id = { + item.item_name : BASE_ID + item.item_id for item in links_awakening_items + } + + item_name_to_data = links_awakening_items_by_name + + location_name_to_id = get_locations_to_id() + + # Items can be grouped using their names to allow easy checking if any item + # from that group has been collected. Group names can also be used for !hint + #item_name_groups = { + # "weapons": {"sword", "lance"} + #} + + prefill_dungeon_items = None + + player_options = None + + def convert_ap_options_to_ladxr_logic(self): + self.player_options = { + option: getattr(self.multiworld, option)[self.player] for option in self.option_definitions + } + + self.laxdr_options = LADXRSettings(self.player_options) + + self.laxdr_options.validate() + world_setup = LADXRWorldSetup() + world_setup.randomize(self.laxdr_options, self.multiworld.random) + self.ladxr_logic = LAXDRLogic(configuration_options=self.laxdr_options, world_setup=world_setup) + self.ladxr_itempool = LADXRItemPool(self.ladxr_logic, self.laxdr_options, self.multiworld.random).toDict() + + + def create_regions(self) -> None: + # Initialize + self.convert_ap_options_to_ladxr_logic() + regions = create_regions_from_ladxr(self.player, self.multiworld, self.ladxr_logic) + self.multiworld.regions += regions + + # Connect Menu -> Start + start = None + for region in regions: + if region.name == "Start House": + start = region + break + + assert(start) + + menu_region = LinksAwakeningRegion("Menu", None, "Menu", self.player, self.multiworld) + menu_region.exits = [Entrance(self.player, "Start Game", menu_region)] + menu_region.exits[0].connect(start) + + self.multiworld.regions.append(menu_region) + + # Place RAFT, other access events + for region in regions: + for loc in region.locations: + if loc.event: + loc.place_locked_item(self.create_event(loc.ladxr_item.event)) + + # Connect Windfish -> Victory + windfish = self.multiworld.get_region("Windfish", self.player) + l = Location(self.player, "Windfish", parent=windfish) + windfish.locations = [l] + + l.place_locked_item(self.create_event("An Alarm Clock")) + + self.multiworld.completion_condition[self.player] = lambda state: state.has("An Alarm Clock", player=self.player) + + def create_item(self, item_name: str): + return LinksAwakeningItem(self.item_name_to_data[item_name], self, self.player) + + def create_event(self, event: str): + return Item(event, ItemClassification.progression, None, self.player) + + def create_items(self) -> None: + exclude = [item.name for item in self.multiworld.precollected_items[self.player]] + + self.trade_items = [] + + dungeon_item_types = { + + } + from .Options import DungeonItemShuffle + self.prefill_original_dungeon = [ [], [], [], [], [], [], [], [], [] ] + self.prefill_own_dungeons = [] + # For any and different world, set item rule instead + + for option in ["maps", "compasses", "small_keys", "nightmare_keys", "stone_beaks"]: + option = "shuffle_" + option + option = self.player_options[option] + + dungeon_item_types[option.ladxr_item] = option.value + + if option.value == DungeonItemShuffle.option_own_world: + self.multiworld.local_items[self.player].value |= { + ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, 10) + } + elif option.value == DungeonItemShuffle.option_different_world: + self.multiworld.non_local_items[self.player].value |= { + ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, 10) + } + # option_original_dungeon = 0 + # option_own_dungeons = 1 + # option_own_world = 2 + # option_any_world = 3 + # option_different_world = 4 + # option_delete = 5 + + for ladx_item_name, count in self.ladxr_itempool.items(): + # event + if ladx_item_name not in ladxr_item_to_la_item_name: + continue + item_name = ladxr_item_to_la_item_name[ladx_item_name] + for _ in range(count): + if item_name in exclude: + exclude.remove(item_name) # this is destructive. create unique list above + self.multiworld.itempool.append(self.create_item("Master Stalfos' Message")) + else: + item = self.create_item(item_name) + + if not self.multiworld.tradequest[self.player] and ladx_item_name.startswith("TRADING_"): + self.trade_items.append(item) + continue + if isinstance(item.item_data, DungeonItemData): + if item.item_data.dungeon_item_type == DungeonItemType.INSTRUMENT: + # Find instrument, lock + # TODO: we should be able to pinpoint the region we want, save a lookup table please + found = False + for r in self.multiworld.get_regions(): + if r.player != self.player: + continue + if r.dungeon_index != item.item_data.dungeon_index: + continue + for loc in r.locations: + if not isinstance(loc, LinksAwakeningLocation): + continue + if not isinstance(loc.ladxr_item, Instrument): + continue + loc.place_locked_item(item) + found = True + break + if found: + break + else: + item_type = item.item_data.ladxr_id[:-1] + shuffle_type = dungeon_item_types[item_type] + if shuffle_type == DungeonItemShuffle.option_original_dungeon: + self.prefill_original_dungeon[item.item_data.dungeon_index - 1].append(item) + elif shuffle_type == DungeonItemShuffle.option_own_dungeons: + self.prefill_own_dungeons.append(item) + else: + self.multiworld.itempool.append(item) + else: + self.multiworld.itempool.append(item) + + def pre_fill(self): + dungeon_locations = [] + dungeon_locations_by_dungeon = [[], [], [], [], [], [], [], [], []] + all_state = self.multiworld.get_all_state(use_cache=False) + + # Add special case for trendy shop access + trendy_region = self.multiworld.get_region("Trendy Shop", self.player) + event_location = Location(self.player, "Can Play Trendy Game", parent=trendy_region) + trendy_region.locations.insert(0, event_location) + event_location.place_locked_item(self.create_event("Can Play Trendy Game")) + + # For now, special case first item + FORCE_START_ITEM = True + if FORCE_START_ITEM: + start_loc = self.multiworld.get_location("Tarin's Gift (Mabe Village)", self.player) + if not start_loc.item: + possible_start_items = [index for index, item in enumerate(self.multiworld.itempool) + if item.player == self.player + and item.item_data.ladxr_id in start_loc.ladxr_item.OPTIONS] + + index = self.multiworld.random.choice(possible_start_items) + start_item = self.multiworld.itempool.pop(index) + start_loc.place_locked_item(start_item) + + for r in self.multiworld.get_regions(): + if r.player != self.player: + continue + + # Set aside dungeon locations + if r.dungeon_index: + dungeon_locations += r.locations + dungeon_locations_by_dungeon[r.dungeon_index - 1] += r.locations + for location in r.locations: + if location.name == "Pit Button Chest (Tail Cave)": + # Don't place dungeon items on pit button chest, to reduce chance of the filler blowing up + # TODO: no need for this if small key shuffle + dungeon_locations.remove(location) + dungeon_locations_by_dungeon[r.dungeon_index - 1].remove(location) + # Properly fill locations within dungeon + location.dungeon = r.dungeon_index + + # Tell the filler that if we're placing a dungeon item, restrict it to the dungeon the item associates with + # This will need changed once keysanity is implemented + #orig_rule = location.item_rule + #location.item_rule = lambda item, orig_rule=orig_rule: \ + # (not isinstance(item, DungeonItemData) or item.dungeon_index == location.dungeon) and orig_rule(item) + + for location in r.locations: + # If tradequests are disabled, place trade items directly in their proper location + if not self.multiworld.tradequest[self.player] and isinstance(location, LinksAwakeningLocation) and isinstance(location.ladxr_item, TradeSequenceItem): + item = next(i for i in self.trade_items if i.item_data.ladxr_id == location.ladxr_item.default_item) + location.place_locked_item(item) + + for dungeon_index in range(0, 9): + locs = dungeon_locations_by_dungeon[dungeon_index] + locs = [loc for loc in locs if not loc.item] + self.multiworld.random.shuffle(locs) + self.multiworld.random.shuffle(self.prefill_original_dungeon[dungeon_index]) + fill_restrictive(self.multiworld, all_state, locs, self.prefill_original_dungeon[dungeon_index], lock=True) + assert not self.prefill_original_dungeon[dungeon_index] + + # Fill dungeon items first, to not torture the fill algo + dungeon_locations = [loc for loc in dungeon_locations if not loc.item] + # dungeon_items = sorted(self.prefill_own_dungeons, key=lambda item: item.item_data.dungeon_item_type) + self.multiworld.random.shuffle(self.prefill_own_dungeons) + self.multiworld.random.shuffle(dungeon_locations) + fill_restrictive(self.multiworld, all_state, dungeon_locations, self.prefill_own_dungeons, lock=True) + + name_cache = {} + + # Tries to associate an icon from another game with an icon we have + def guess_icon_for_other_world(self, other): + if not self.name_cache: + forbidden = [ + "TRADING", + "ITEM", + "BAD", + "SINGLE", + "UPGRADE", + "BLUE", + "RED", + "NOTHING", + "MESSAGE", + ] + for item in ladxr_item_to_la_item_name.keys(): + self.name_cache[item] = item + splits = item.split("_") + self.name_cache["".join(splits)] = item + if 'RUPEES' in splits: + self.name_cache["".join(reversed(splits))] = item + + for word in item.split("_"): + if word not in forbidden and not word.isnumeric(): + self.name_cache[word] = item + others = { + 'KEY': 'KEY', + 'COMPASS': 'COMPASS', + 'BIGKEY': 'NIGHTMARE_KEY', + 'MAP': 'MAP', + 'FLUTE': 'OCARINA', + 'SONG': 'OCARINA', + 'MUSHROOM': 'TOADSTOOL', + 'GLOVE': 'POWER_BRACELET', + 'BOOT': 'PEGASUS_BOOTS', + 'SHOE': 'PEGASUS_BOOTS', + 'SHOES': 'PEGASUS_BOOTS', + 'SANCTUARYHEARTCONTAINER': 'HEART_CONTAINER', + 'BOSSHEARTCONTAINER': 'HEART_CONTAINER', + 'HEARTCONTAINER': 'HEART_CONTAINER', + 'ENERGYTANK': 'HEART_CONTAINER', + 'MISSILE': 'SINGLE_ARROW', + 'BOMBS': 'BOMB', + 'BLUEBOOMERANG': 'BOOMERANG', + 'MAGICMIRROR': 'TRADING_ITEM_MAGNIFYING_GLASS', + 'MIRROR': 'TRADING_ITEM_MAGNIFYING_GLASS', + 'MESSAGE': 'TRADING_ITEM_LETTER', + # TODO: Also use AP item name + } + for name in others.values(): + assert name in self.name_cache, name + assert name in CHEST_ITEMS, name + self.name_cache.update(others) + + + uppered = other.upper() + if "BIG KEY" in uppered: + return 'NIGHTMARE_KEY' + possibles = other.upper().split(" ") + rejoined = "".join(possibles) + if rejoined in self.name_cache: + return self.name_cache[rejoined] + for name in possibles: + if name in self.name_cache: + return self.name_cache[name] + + return "TRADING_ITEM_LETTER" + + + + + def generate_output(self, output_directory: str): + # copy items back to locations + for r in self.multiworld.get_regions(self.player): + for loc in r.locations: + if isinstance(loc, LinksAwakeningLocation): + assert(loc.item) + # If we're a links awakening item, just use the item + if isinstance(loc.item, LinksAwakeningItem): + loc.ladxr_item.item = loc.item.item_data.ladxr_id + + # TODO: if the item name contains "sword", use a sword icon, etc + # Otherwise, use a cute letter as the icon + else: + loc.ladxr_item.item = self.guess_icon_for_other_world(loc.item.name) + loc.ladxr_item.custom_item_name = loc.item.name + + if loc.item: + loc.ladxr_item.item_owner = loc.item.player + else: + loc.ladxr_item.item_owner = self.player + + # Kind of kludge, make it possible for the location to differentiate between local and remote items + loc.ladxr_item.location_owner = self.player + + rom_path = "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc" + out_name = f"AP-{self.multiworld.seed_name}-P{self.player}-{self.multiworld.player_name[self.player]}.gbc" + out_file = os.path.join(output_directory, out_name) + + rompath = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.gbc") + + + + parser = get_parser() + args = parser.parse_args([rom_path, "-o", out_name, "--dump"]) + + name_for_rom = self.multiworld.player_name[self.player] + + all_names = [self.multiworld.player_name[i + 1] for i in range(len(self.multiworld.player_name))] + rom = generator.generateRom( + args, + self.laxdr_options, + self.player_options, + bytes.fromhex(self.multiworld.seed_name), + self.ladxr_logic, + rnd=self.multiworld.per_slot_randoms[self.player], + player_name=name_for_rom, + player_names=all_names, + player_id = self.player) + + handle = open(rompath, "wb") + rom.save(handle, name="LADXR") + handle.close() + patch = LADXDeltaPatch(os.path.splitext(rompath)[0]+LADXDeltaPatch.patch_file_ending, player=self.player, + player_name=self.multiworld.player_name[self.player], patched_path=rompath) + patch.write() + if not DEVELOPER_MODE: + os.unlink(rompath) + + def generate_multi_key(self): + return bytes.fromhex(self.multiworld.seed_name) + self.player.to_bytes(2, 'big') + + def modify_multidata(self, multidata: dict): + multi_key = binascii.hexlify(self.generate_multi_key()).decode() + multidata["connect_names"][multi_key] = multidata["connect_names"][self.multiworld.player_name[self.player]] diff --git a/worlds/ladx/docs/en_Links Awakening DX.md b/worlds/ladx/docs/en_Links Awakening DX.md new file mode 100644 index 0000000000..d667490c8a --- /dev/null +++ b/worlds/ladx/docs/en_Links Awakening DX.md @@ -0,0 +1,79 @@ +# Links Awakening DX + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +config file. + +## What does randomization do to this game? + +Items which the player would normally acquire throughout the game have been moved around. Logic remains, so the game is +always able to be completed, but because of the item shuffle the player may need to access certain areas before they +would in the vanilla game. + +## What items and locations get shuffled? + +All main inventory items, collectables, and ammunition can be shuffled, and all locations in the game which could +contain any of those items may have their contents changed. + +## Which items can be in another player's world? + +Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to limit +certain items to your own world. + +## What does another world's item look like in Link's Awakening? + +The game will try to pick an appropriate sprite for the item (a LttP sword will be a sword!) - it may, however, be a little odd (a Missile Pack may be a single arrow). + +If there's no appropriate sprite, a Letter will be shown. + +## When the player receives an item, what happens? + +When the player receives an item, Link will hold the item above his head and display it to the world. It's good for +business! + +## I don't know what to do! + +That's not a question - but I'd suggest clicking the crow icon on your client, which will load an AP compatible autotracker for LADXR. + +## What is this randomizer based on? + +This randomizer is based on (forked from) the wonderful work daid did on LADXR - https://github.com/daid/LADXR + +The autotracker code for communication with magpie tracker is directly copied from kbranch's repo - https://github.com/kbranch/Magpie/tree/master/autotracking + +## Some tips from LADXR... + +

Locations

+

All chests and dungeon keys are always randomized. Also, the 3 songs (Marin, Mambo, and Manu) give a you an item if you present them the Ocarina. The seashell mansion 20 shells reward is also shuffled, but the 5 and 10 shell reward is not, as those can be missed.

+

The moblin cave with Bowwow contains a chest instead. The color dungeon gives 2 items at the end instead of a choice of tunic. Other item locations are: The toadstool, the reward for delivering the toadstool, hidden seashells, heart pieces, heart containers, golden leaves, the Mad Batters (capacity upgrades), the shovel/bow in the shop, the rooster's grave, and all of the keys' (tail,slime,angler,face,bird) locations.

+

Finally, new players often forget the following locations: the heart piece hidden in the water at the castle, the heart piece hidden in the bomb cave (screen before the honey), bonk seashells (run with pegasus boots against the tree in at the Tail Cave, and the tree right of Mabe Village, next to the phone booth), and the hookshop drop from Master Stalfos in D5.

+ +

Color Dungeon

+

The Color Dungeon is part of the item shuffle, and the red/blue tunics are shuffled in the item pool. Which means the fairy at the end of the color dungeon gives out two random items.

+

To access the color dungeon, you need the power bracelet, and you need to push the gravestones in the right order: "down, left, up, right, up", going from the lower right gravestone, to the one left of it, above it, and then to the right.

+ +

Bowwow

+

Bowwow is in a chest, somewhere. After you find him, he will always be in the swamp with you, but not anywhere else.

+ +

Added things

+

In your save and quit menu, there is a 3rd option to return to your home. This has two main uses: it speeds up the game, and prevents softlocks (common in entrance rando).

+

If you have weapons that require ammunition (bombs, powder, arrows), a ghost will show up inside Marin's house. He will refill you up to 10 ammunition, so you do not run out.

+

The flying rooster is (optionally) available as an item.

+

You can access the Bird Key cave item with the L2 Power Bracelet.

+

Boomerang cave is now a random item gift by default (available post-bombs), and boomerang is in the item pool.

+

Your inventory has been increased by four, to accommodate these items now coexisting with eachother.

+ +

Removed things

+

The ghost mini-quest after D4 never shows up, his seashell reward is always available.

+

The walrus is moved a bit, so that you can access the desert without taking Marin on a date.

+ +

Logic

+

Depending on your settings, you can only steal after you find the sword, always, or never.

+

Do not forget that there are two items in the rafting ride. You can access this with just Hookshot or Flippers.

+

Killing enemies with bombs is in normal logic. You can switch to casual logic if you do not want this.

+

D7 confuses some people, but by dropping down pits on the 2nd floor you can access almost all of this dungeon, even without feather and power bracelet.

+ +

Tech

+

The toadstool and magic powder used to be the same type of item. LADXR turns this into two items that you can have a the same time. 4 extra item slots in your inventory were added to support this extra item, and have the ability to own the boomerang.

+

The glitch where the slime key is effectively a 6th golden leaf is fixed, and golden leaves can be collected fine next to the slime key.

diff --git a/worlds/ladx/docs/setup_en.md b/worlds/ladx/docs/setup_en.md new file mode 100644 index 0000000000..abd60dc82e --- /dev/null +++ b/worlds/ladx/docs/setup_en.md @@ -0,0 +1,93 @@ +# Links Awakening DX Multiworld Setup Guide + +## Required Software + +- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `Links Awakening DX` +- Software capable of loading and playing GBC ROM files + - Currently only [RetroArch](https://retroarch.com?page=platforms) 1.10.3 or newer) is supported. + - Bizhawk support will come at a later date. +- Your American 1.0 ROM file, probably named `Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc` + +## Installation Procedures + +1. Download and install LinksAwakeningClient from the link above, making sure to install the most recent version. + **The installer file is located in the assets section at the bottom of the version information**. + - During setup, you will be asked to locate your base ROM file. This is your Links Awakening DX ROM file. + +2. You should assign your emulator as your default program for launching ROM + files. + 1. Extract your emulator's folder to your Desktop, or somewhere you will remember. + 2. Right-click on a ROM file and select **Open with...** + 3. Check the box next to **Always use this app to open .gbc files** + 4. Scroll to the bottom of the list and click the grey text **Look for another App on this PC** + 5. Browse for your emulator's `.exe` file and click **Open**. This file should be located inside the folder you + extracted in step one. + +## Create a Config (.yaml) File + +### What is a config file and why do I need one? + +Your config file contains a set of configuration options which provide the generator with information about how it +should generate your game. Each player of a multiworld will provide their own config file. This setup allows each player +to enjoy an experience customized for their taste, and different players in the same multiworld can all have different +options. + +### Where do I get a config file? + +The [Player Settings](/games/Links%20Awakening%20DX/player-settings) page on the website allows you to configure +your personal settings and export a config file from them. + +### Verifying your config file + +If you would like to validate your config file to make sure it works, you may do so on the +[YAML Validator](/mysterycheck) page. + +## Generating a Single-Player Game + +1. Navigate to the [Player Settings](/games/Links%20Awakening%20DX/player-settings) page, configure your options, + and click the "Generate Game" button. +2. You will be presented with a "Seed Info" page. +3. Click the "Create New Room" link. +4. You will be presented with a server page, from which you can download your patch file. +5. Double-click on your patch file, and Links Awakening DX will launch automatically, and create your ROM from the patch file. +6. Since this is a single-player game, you will no longer need the client, so feel free to close it. + +## Joining a MultiWorld Game + +### Obtain your patch file and create your ROM + +When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done, +the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch +files. Your patch file should have a `.apladx` extension. + +Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically launch the +client, and will also create your ROM in the same place as your patch file. + +### Connect to the client + +##### RetroArch 1.10.3 or newer + +You only have to do these steps once. Note, RetroArch 1.9.x will not work as it is older than 1.10.3. + +1. Enter the RetroArch main menu screen. +2. Go to Settings --> User Interface. Set "Show Advanced Settings" to ON. +3. Go to Settings --> Network. Set "Network Commands" to ON. (It is found below Request Device 16.) Leave the default + Network Command Port at 55355. + +![Screenshot of Network Commands setting](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-en.png) +4. Go to Main Menu --> Online Updater --> Core Downloader. Scroll down and select "Nintendo - Gameboy / Color (SameBoy)". + +### Connect to the Archipelago Server + +The patch file which launched your client should have automatically connected you to the AP Server. There are a few +reasons this may not happen, however, including if the game is hosted on the website but was generated elsewhere. If the +client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it +into the "Server" input field then press enter. + +The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected". + +### Play the game + +When the client shows both Retroarch and Server as connected, you're ready to begin playing. Congratulations on +successfully joining a multiworld game! You can execute various commands in your client. For more information regarding +these commands you can use `/help` for local client commands and `!help` for server commands. From 9ee37b0ec551bd967538297aca6d4840fc256841 Mon Sep 17 00:00:00 2001 From: KonoTyran Date: Mon, 20 Mar 2023 11:44:48 -0700 Subject: [PATCH 072/172] [Minecraft] Update MinecraftClient.py (#1524) * add support for direct url for mod to download. make a nice error message if the chosen release channel is empty. * Rip out call to github api as it is no longer used. * rework error message to be more descriptive, and provide basic troubleshooting. fix command line data version not correctly overriding apmc data version. --- MinecraftClient.py | 83 +++++++++++++++++++++------------------------- 1 file changed, 38 insertions(+), 45 deletions(-) diff --git a/MinecraftClient.py b/MinecraftClient.py index 16b283f9d9..dd7a5cfd3e 100644 --- a/MinecraftClient.py +++ b/MinecraftClient.py @@ -77,49 +77,34 @@ def read_apmc_file(apmc_file): return json.loads(b64decode(f.read())) -def update_mod(forge_dir, minecraft_version: str, get_prereleases=False): +def update_mod(forge_dir, url: str): """Check mod version, download new mod from GitHub releases page if needed. """ ap_randomizer = find_ap_randomizer_jar(forge_dir) - - client_releases_endpoint = "https://api.github.com/repos/KonoTyran/Minecraft_AP_Randomizer/releases" - resp = requests.get(client_releases_endpoint) - if resp.status_code == 200: # OK - try: - latest_release = next(filter(lambda release: (not release['prerelease'] or get_prereleases) and - (minecraft_version in release['assets'][0]['name']), - resp.json())) - if ap_randomizer != latest_release['assets'][0]['name']: - logging.info(f"A new release of the Minecraft AP randomizer mod was found: " - f"{latest_release['assets'][0]['name']}") - if ap_randomizer is not None: - logging.info(f"Your current mod is {ap_randomizer}.") - else: - logging.info(f"You do not have the AP randomizer mod installed.") - if prompt_yes_no("Would you like to update?"): - old_ap_mod = os.path.join(forge_dir, 'mods', ap_randomizer) if ap_randomizer is not None else None - new_ap_mod = os.path.join(forge_dir, 'mods', latest_release['assets'][0]['name']) - logging.info("Downloading AP randomizer mod. This may take a moment...") - apmod_resp = requests.get(latest_release['assets'][0]['browser_download_url']) - if apmod_resp.status_code == 200: - with open(new_ap_mod, 'wb') as f: - f.write(apmod_resp.content) - logging.info(f"Wrote new mod file to {new_ap_mod}") - if old_ap_mod is not None: - os.remove(old_ap_mod) - logging.info(f"Removed old mod file from {old_ap_mod}") - else: - logging.error(f"Error retrieving the randomizer mod (status code {apmod_resp.status_code}).") - logging.error(f"Please report this issue on the Archipelago Discord server.") - sys.exit(1) - except StopIteration: - logging.warning(f"No compatible mod version found for {minecraft_version}.") - if not prompt_yes_no("Run server anyway?"): - sys.exit(0) + os.path.basename(url) + if ap_randomizer is not None: + logging.info(f"Your current mod is {ap_randomizer}.") else: - logging.error(f"Error checking for randomizer mod updates (status code {resp.status_code}).") - logging.error(f"If this was not expected, please report this issue on the Archipelago Discord server.") - if not prompt_yes_no("Continue anyways?"): - sys.exit(0) + logging.info(f"You do not have the AP randomizer mod installed.") + + if ap_randomizer != os.path.basename(url): + logging.info(f"A new release of the Minecraft AP randomizer mod was found: " + f"{os.path.basename(url)}") + if prompt_yes_no("Would you like to update?"): + old_ap_mod = os.path.join(forge_dir, 'mods', ap_randomizer) if ap_randomizer is not None else None + new_ap_mod = os.path.join(forge_dir, 'mods', os.path.basename(url)) + logging.info("Downloading AP randomizer mod. This may take a moment...") + apmod_resp = requests.get(url) + if apmod_resp.status_code == 200: + with open(new_ap_mod, 'wb') as f: + f.write(apmod_resp.content) + logging.info(f"Wrote new mod file to {new_ap_mod}") + if old_ap_mod is not None: + os.remove(old_ap_mod) + logging.info(f"Removed old mod file from {old_ap_mod}") + else: + logging.error(f"Error retrieving the randomizer mod (status code {apmod_resp.status_code}).") + logging.error(f"Please report this issue on the Archipelago Discord server.") + sys.exit(1) def check_eula(forge_dir): @@ -264,8 +249,13 @@ def get_minecraft_versions(version, release_channel="release"): return next(filter(lambda entry: entry["version"] == version, data[release_channel])) else: return resp.json()[release_channel][0] - except StopIteration: - logging.error(f"No compatible mod version found for client version {version}.") + except (StopIteration, KeyError): + logging.error(f"No compatible mod version found for client version {version} on \"{release_channel}\" channel.") + if release_channel != "release": + logging.error("Consider switching \"release_channel\" to \"release\" in your Host.yaml file") + else: + logging.error("No suitable mod found on the \"release\" channel. Please Contact us on discord to report this error.") + sys.exit(0) def is_correct_forge(forge_dir) -> bool: @@ -286,6 +276,8 @@ if __name__ == '__main__': help="specify java version.") parser.add_argument('--forge', '-f', metavar='1.18.2-40.1.0', dest='forge', type=str, default=False, action='store', help="specify forge version. (Minecraft Version-Forge Version)") + parser.add_argument('--version', '-v', metavar='9', dest='data_version', type=int, action='store', + help="specify Mod data version to download.") args = parser.parse_args() apmc_file = os.path.abspath(args.apmc_file) if args.apmc_file else None @@ -296,12 +288,12 @@ if __name__ == '__main__': options = Utils.get_options() channel = args.channel or options["minecraft_options"]["release_channel"] apmc_data = None - data_version = None + data_version = args.data_version or None if apmc_file is None and not args.install: apmc_file = Utils.open_filename('Select APMC file', (('APMC File', ('.apmc',)),)) - if apmc_file is not None: + if apmc_file is not None and data_version is None: apmc_data = read_apmc_file(apmc_file) data_version = apmc_data.get('client_version', '') @@ -311,6 +303,7 @@ if __name__ == '__main__': max_heap = options["minecraft_options"]["max_heap_size"] forge_version = args.forge or versions["forge"] java_version = args.java or versions["java"] + mod_url = versions["url"] java_dir = find_jdk_dir(java_version) if args.install: @@ -344,7 +337,7 @@ if __name__ == '__main__': if not max_heap_re.match(max_heap): raise Exception(f"Max heap size {max_heap} in incorrect format. Use a number followed by M or G, e.g. 512M or 2G.") - update_mod(forge_dir, f"MC{forge_version.split('-')[0]}", channel != "release") + update_mod(forge_dir, mod_url) replace_apmc_files(forge_dir, apmc_file) check_eula(forge_dir) server_process = run_forge_server(forge_dir, java_version, max_heap) From 958829d4913647d00ff8cb1fa2a08e5c9856adeb Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 20 Mar 2023 21:24:47 +0100 Subject: [PATCH 073/172] Launcher: dynamic Launcher --- Launcher.py | 105 ++-------------------------------- inno_setup.iss | 1 + setup.py | 2 +- worlds/LauncherComponents.py | 106 +++++++++++++++++++++++++++++++++++ worlds/__init__.py | 48 ++++++++++------ worlds/factorio/__init__.py | 3 + 6 files changed, 147 insertions(+), 118 deletions(-) create mode 100644 worlds/LauncherComponents.py diff --git a/Launcher.py b/Launcher.py index be6fbd76c8..be40987e32 100644 --- a/Launcher.py +++ b/Launcher.py @@ -14,10 +14,11 @@ import itertools import shlex import subprocess import sys -from enum import Enum, auto from os.path import isfile from shutil import which -from typing import Iterable, Sequence, Callable, Union, Optional +from typing import Sequence, Union, Optional + +from worlds.LauncherComponents import Component, components, Type, SuffixIdentifier if __name__ == "__main__": import ModuleUpdate @@ -70,108 +71,12 @@ def browse_files(): webbrowser.open(file) -# noinspection PyArgumentList -class Type(Enum): - TOOL = auto() - FUNC = auto() # not a real component - CLIENT = auto() - ADJUSTER = auto() - - -class SuffixIdentifier: - suffixes: Iterable[str] - - def __init__(self, *args: str): - self.suffixes = args - - def __call__(self, path: str): - if isinstance(path, str): - for suffix in self.suffixes: - if path.endswith(suffix): - return True - return False - - -class Component: - display_name: str - type: Optional[Type] - script_name: Optional[str] - frozen_name: Optional[str] - icon: str # just the name, no suffix - cli: bool - func: Optional[Callable] - file_identifier: Optional[Callable[[str], bool]] - - def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None, - cli: bool = False, icon: str = 'icon', component_type: Type = None, func: Optional[Callable] = None, - file_identifier: Optional[Callable[[str], bool]] = None): - self.display_name = display_name - self.script_name = script_name - self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None - self.icon = icon - self.cli = cli - self.type = component_type or \ - None if not display_name else \ - Type.FUNC if func else \ - Type.CLIENT if 'Client' in display_name else \ - Type.ADJUSTER if 'Adjuster' in display_name else Type.TOOL - self.func = func - self.file_identifier = file_identifier - - def handles_file(self, path: str): - return self.file_identifier(path) if self.file_identifier else False - - -components: Iterable[Component] = ( - # Launcher - Component('', 'Launcher'), - # Core - Component('Host', 'MultiServer', 'ArchipelagoServer', cli=True, - file_identifier=SuffixIdentifier('.archipelago', '.zip')), - Component('Generate', 'Generate', cli=True), - Component('Text Client', 'CommonClient', 'ArchipelagoTextClient'), - # SNI - Component('SNI Client', 'SNIClient', - file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3', - '.apsmw', '.apl2ac')), - Component('Links Awakening DX Client', 'LinksAwakeningClient', - file_identifier=SuffixIdentifier('.apladx')), - Component('LttP Adjuster', 'LttPAdjuster'), - # Factorio - Component('Factorio Client', 'FactorioClient'), - # Minecraft - Component('Minecraft Client', 'MinecraftClient', icon='mcicon', cli=True, - file_identifier=SuffixIdentifier('.apmc')), - # Ocarina of Time - Component('OoT Client', 'OoTClient', - file_identifier=SuffixIdentifier('.apz5')), - Component('OoT Adjuster', 'OoTAdjuster'), - # FF1 - Component('FF1 Client', 'FF1Client'), - # Pokémon - Component('Pokemon Client', 'PokemonClient', file_identifier=SuffixIdentifier('.apred', '.apblue')), - # TLoZ - Component('Zelda 1 Client', 'Zelda1Client'), - # ChecksFinder - Component('ChecksFinder Client', 'ChecksFinderClient'), - # Starcraft 2 - Component('Starcraft 2 Client', 'Starcraft2Client'), - # Wargroove - Component('Wargroove Client', 'WargrooveClient'), - # Zillion - Component('Zillion Client', 'ZillionClient', - file_identifier=SuffixIdentifier('.apzl')), - #Kingdom Hearts 2 - Component('KH2 Client', "KH2Client"), +components.extend([ # Functions Component('Open host.yaml', func=open_host_yaml), Component('Open Patch', func=open_patch), Component('Browse Files', func=browse_files), -) -icon_paths = { - 'icon': local_path('data', 'icon.ico' if is_windows else 'icon.png'), - 'mcicon': local_path('data', 'mcicon.ico') -} +]) def identify(path: Union[None, str]): diff --git a/inno_setup.iss b/inno_setup.iss index 815f2c01a6..4587396af0 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -105,6 +105,7 @@ Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, E Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/sni Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp +Source: "{#source_path}\ArchipelagoLauncher.exe"; DestDir: "{app}"; Flags: ignoreversion; Source: "{#source_path}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator Source: "{#source_path}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server Source: "{#source_path}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio diff --git a/setup.py b/setup.py index 5f109d7a05..4e54fab518 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ if __name__ == "__main__": ModuleUpdate.update(yes="--yes" in sys.argv or "-y" in sys.argv) ModuleUpdate.update_ran = False # restore for later -from Launcher import components, icon_paths +from worlds.LauncherComponents import components, icon_paths from Utils import version_tuple, is_windows, is_linux diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py new file mode 100644 index 0000000000..7bf3ea291e --- /dev/null +++ b/worlds/LauncherComponents.py @@ -0,0 +1,106 @@ +from enum import Enum, auto +from typing import Optional, Callable, List, Iterable + +from Utils import local_path, is_windows + + +class Type(Enum): + TOOL = auto() + FUNC = auto() # not a real component + CLIENT = auto() + ADJUSTER = auto() + + +class Component: + display_name: str + type: Optional[Type] + script_name: Optional[str] + frozen_name: Optional[str] + icon: str # just the name, no suffix + cli: bool + func: Optional[Callable] + file_identifier: Optional[Callable[[str], bool]] + + def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None, + cli: bool = False, icon: str = 'icon', component_type: Type = None, func: Optional[Callable] = None, + file_identifier: Optional[Callable[[str], bool]] = None): + self.display_name = display_name + self.script_name = script_name + self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None + self.icon = icon + self.cli = cli + self.type = component_type or \ + None if not display_name else \ + Type.FUNC if func else \ + Type.CLIENT if 'Client' in display_name else \ + Type.ADJUSTER if 'Adjuster' in display_name else Type.TOOL + self.func = func + self.file_identifier = file_identifier + + def handles_file(self, path: str): + return self.file_identifier(path) if self.file_identifier else False + + def __repr__(self): + return f"{self.__class__.__name__}({self.display_name})" + + +class SuffixIdentifier: + suffixes: Iterable[str] + + def __init__(self, *args: str): + self.suffixes = args + + def __call__(self, path: str): + if isinstance(path, str): + for suffix in self.suffixes: + if path.endswith(suffix): + return True + return False + + +components: List[Component] = [ + # Launcher + Component('', 'Launcher'), + # Core + Component('Host', 'MultiServer', 'ArchipelagoServer', cli=True, + file_identifier=SuffixIdentifier('.archipelago', '.zip')), + Component('Generate', 'Generate', cli=True), + Component('Text Client', 'CommonClient', 'ArchipelagoTextClient'), + # SNI + Component('SNI Client', 'SNIClient', + file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3', + '.apsmw', '.apl2ac')), + Component('Links Awakening DX Client', 'LinksAwakeningClient', + file_identifier=SuffixIdentifier('.apladx')), + Component('LttP Adjuster', 'LttPAdjuster'), + # Minecraft + Component('Minecraft Client', 'MinecraftClient', icon='mcicon', cli=True, + file_identifier=SuffixIdentifier('.apmc')), + # Ocarina of Time + Component('OoT Client', 'OoTClient', + file_identifier=SuffixIdentifier('.apz5')), + Component('OoT Adjuster', 'OoTAdjuster'), + # FF1 + Component('FF1 Client', 'FF1Client'), + # Pokémon + Component('Pokemon Client', 'PokemonClient', file_identifier=SuffixIdentifier('.apred', '.apblue')), + # TLoZ + Component('Zelda 1 Client', 'Zelda1Client'), + # ChecksFinder + Component('ChecksFinder Client', 'ChecksFinderClient'), + # Starcraft 2 + Component('Starcraft 2 Client', 'Starcraft2Client'), + # Wargroove + Component('Wargroove Client', 'WargrooveClient'), + # Zillion + Component('Zillion Client', 'ZillionClient', + file_identifier=SuffixIdentifier('.apzl')), + #Kingdom Hearts 2 + Component('KH2 Client', "KH2Client"), +] + + +icon_paths = { + 'icon': local_path('data', 'icon.ico' if is_windows else 'icon.png'), + 'mcicon': local_path('data', 'mcicon.ico') +} diff --git a/worlds/__init__.py b/worlds/__init__.py index 3470c1a353..e2ebb78610 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -40,6 +40,9 @@ class WorldSource(typing.NamedTuple): path: str # typically relative path from this module is_zip: bool = False + def __repr__(self): + return f"{self.__class__.__name__}({self.path}, is_zip={self.is_zip})" + # find potential world containers, currently folders and zip-importable .apworld's world_sources: typing.List[WorldSource] = [] @@ -55,24 +58,35 @@ for file in os.scandir(folder): # import all submodules to trigger AutoWorldRegister world_sources.sort() for world_source in world_sources: - if world_source.is_zip: - importer = zipimport.zipimporter(os.path.join(folder, world_source.path)) - if hasattr(importer, "find_spec"): # new in Python 3.10 - spec = importer.find_spec(world_source.path.split(".", 1)[0]) - mod = importlib.util.module_from_spec(spec) - else: # TODO: remove with 3.8 support - mod = importer.load_module(world_source.path.split(".", 1)[0]) + try: + if world_source.is_zip: + importer = zipimport.zipimporter(os.path.join(folder, world_source.path)) + if hasattr(importer, "find_spec"): # new in Python 3.10 + spec = importer.find_spec(world_source.path.split(".", 1)[0]) + mod = importlib.util.module_from_spec(spec) + else: # TODO: remove with 3.8 support + mod = importer.load_module(world_source.path.split(".", 1)[0]) - mod.__package__ = f"worlds.{mod.__package__}" - mod.__name__ = f"worlds.{mod.__name__}" - sys.modules[mod.__name__] = mod - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", message="__package__ != __spec__.parent") - # Found no equivalent for < 3.10 - if hasattr(importer, "exec_module"): - importer.exec_module(mod) - else: - importlib.import_module(f".{world_source.path}", "worlds") + mod.__package__ = f"worlds.{mod.__package__}" + mod.__name__ = f"worlds.{mod.__name__}" + sys.modules[mod.__name__] = mod + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", message="__package__ != __spec__.parent") + # Found no equivalent for < 3.10 + if hasattr(importer, "exec_module"): + importer.exec_module(mod) + else: + importlib.import_module(f".{world_source.path}", "worlds") + except Exception as e: + # A single world failing can still mean enough is working for the user, log and carry on + import traceback + import io + file_like = io.StringIO() + print(f"Could not load world {world_source}:", file=file_like) + traceback.print_exc(file=file_like) + file_like.seek(0) + import logging + logging.exception(file_like.read()) lookup_any_item_id_to_name = {} lookup_any_location_id_to_name = {} diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 6391701d7b..567ab0bbda 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -15,6 +15,9 @@ from .Technologies import base_tech_table, recipe_sources, base_technology_table get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies, \ fluids, stacking_items, valid_ingredients, progressive_rows from .Locations import location_pools, location_table +from worlds.LauncherComponents import Component, components + +components.append(Component("Factorio Client", "FactorioClient")) class FactorioWeb(WebWorld): From e6d16c905c976e3a0dca913759421128027237a6 Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Mon, 20 Mar 2023 18:43:29 -0400 Subject: [PATCH 074/172] KH2: Fixed inno_setup and fixed readme.md (#1557) --- README.md | 1 + inno_setup.iss | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/README.md b/README.md index b99182f496..febe79c09f 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Currently, the following games are supported: * Stardew Valley * The Legend of Zelda * The Messenger +* Kingdom Hearts 2 For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/inno_setup.iss b/inno_setup.iss index 4587396af0..f5f8e487ca 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -74,6 +74,7 @@ Name: "client/sni/dkc3"; Description: "SNI Client - Donkey Kong Country 3 Patch Name: "client/sni/smw"; Description: "SNI Client - Super Mario World Patch Setup"; Types: full playing; Flags: disablenouninstallwarning Name: "client/sni/l2ac"; Description: "SNI Client - Lufia II Ancient Cave Patch Setup"; Types: full playing; Flags: disablenouninstallwarning Name: "client/factorio"; Description: "Factorio"; Types: full playing +Name: "client/kh2"; Description: "Kingdom Hearts 2"; Types: full playing Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278 Name: "client/oot"; Description: "Ocarina of Time"; Types: full playing Name: "client/ff1"; Description: "Final Fantasy 1"; Types: full playing @@ -121,6 +122,7 @@ Source: "{#source_path}\ArchipelagoFF1Client.exe"; DestDir: "{app}"; Flags: igno Source: "{#source_path}\ArchipelagoPokemonClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/pkmn Source: "{#source_path}\ArchipelagoChecksFinderClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/cf Source: "{#source_path}\ArchipelagoStarcraft2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sc2 +Source: "{#source_path}\ArchipelagoKH2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/kh2 Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall [Icons] @@ -136,6 +138,7 @@ Name: "{group}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\Archipelag Name: "{group}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Components: client/pkmn Name: "{group}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Components: client/cf Name: "{group}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Components: client/sc2 +Name: "{group}\{#MyAppName} Kingdom Hearts 2 Client"; Filename: "{app}\ArchipelagoKH2Client.exe"; Components: client/kh2 Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Components: server @@ -148,6 +151,7 @@ Name: "{commondesktop}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\Ar Name: "{commondesktop}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Tasks: desktopicon; Components: client/pkmn Name: "{commondesktop}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Tasks: desktopicon; Components: client/cf Name: "{commondesktop}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Tasks: desktopicon; Components: client/sc2 +Name: "{commondesktop}\{#MyAppName} Kingdom Hearts 2 Client"; Filename: "{app}\ArchipelagoKH2Client.exe"; Tasks: desktopicon; Components: client/kh2 [Run] From 3fa6588637cd428ca00a0a8486b083bc1a148d0d Mon Sep 17 00:00:00 2001 From: Brooty Johnson <83629348+Br00ty@users.noreply.github.com> Date: Mon, 20 Mar 2023 18:45:36 -0400 Subject: [PATCH 075/172] TLoZ: Update instructions (#1558) Added 'Optional Software' to instructions for setting up Z1 --- worlds/tloz/docs/multiworld_en.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/worlds/tloz/docs/multiworld_en.md b/worlds/tloz/docs/multiworld_en.md index d3aa0afb1d..d170095270 100644 --- a/worlds/tloz/docs/multiworld_en.md +++ b/worlds/tloz/docs/multiworld_en.md @@ -7,6 +7,11 @@ - The BizHawk emulator. Versions 2.3.1 and higher are supported. Version 2.7 is recommended - [BizHawk Official Website](http://tasvideos.org/BizHawk.html) +## Optional Software + +- [Map Tracker](https://github.com/Br00ty/tloz_brooty/releases/latest) + - Used alongside [Poptracker](https://github.com/black-sliver/PopTracker) to keep track of what items/checks you've gotten. Uses auto-tracking by connecting to the Archipelago server. + ## Installation Procedures 1. Download and install the latest version of Archipelago. @@ -101,4 +106,4 @@ inaccessible, simply exit and re-enter the room. This can be used to obtain the stepladder; logic does not account for this. - Whether you've purchased from a shop is tracked via Archipelago between sessions: if you revisit a single player game, none of your shop pruchase statuses will be remembered. If you want them to be, connect to the client and server like -you would in a multiplayer game. \ No newline at end of file +you would in a multiplayer game. From cd9d0bebc8293d37e7665dace637f5c496d33f26 Mon Sep 17 00:00:00 2001 From: The T Date: Mon, 20 Mar 2023 18:55:53 -0400 Subject: [PATCH 076/172] Add LADX to Readme.md (#1559) Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index febe79c09f..9e3d6a450c 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ Currently, the following games are supported: * The Legend of Zelda * The Messenger * Kingdom Hearts 2 +* The Legend of Zelda: Link's Awakening DX For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled From 012e6ba24c95d3ead99a1b26788a4645f7e7c3e8 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 21 Mar 2023 01:44:45 +0100 Subject: [PATCH 077/172] WebHost: add Status to MultiTracker --- WebHostLib/templates/multiTracker.html | 7 +++++-- WebHostLib/tracker.py | 12 +++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/WebHostLib/templates/multiTracker.html b/WebHostLib/templates/multiTracker.html index c6defff00b..11f62fd8bd 100644 --- a/WebHostLib/templates/multiTracker.html +++ b/WebHostLib/templates/multiTracker.html @@ -36,6 +36,7 @@ {% endblock %} Checks % + Status Last
Activity @@ -51,8 +52,10 @@ {% endblock %} {{ checks["Total"] }}/{{ checks_in_area[player]["Total"] }} {{ percent_total_checks_done[team][player] }} - {%- if activity_timers[(team, player)] -%} - {{ activity_timers[(team, player)].total_seconds() }} + {{ {0: "Disconnected", 5: "Connected", 10: "Ready", 20: "Playing", + "30": "Goal Completed"}.get(states[team, player], "Unknown State") }} + {%- if activity_timers[team, player] -%} + {{ activity_timers[team, player].total_seconds() }} {%- else -%} None {%- endif -%} diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index d4260bc85a..8f9fb14881 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -1384,24 +1384,26 @@ def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[s activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp) player_names = {} + states: typing.Dict[typing.Tuple[int, int], int] = {} for team, names in enumerate(names): for player, name in enumerate(names, 1): - player_names[(team, player)] = name + player_names[team, player] = name + states[team, player] = multisave.get("client_game_state", {}).get((team, player), 0) long_player_names = player_names.copy() for (team, player), alias in multisave.get("name_aliases", {}).items(): - player_names[(team, player)] = alias - long_player_names[(team, player)] = f"{alias} ({long_player_names[(team, player)]})" + player_names[team, player] = alias + long_player_names[(team, player)] = f"{alias} ({long_player_names[team, player]})" video = {} for (team, player), data in multisave.get("video", []): - video[(team, player)] = data + video[team, player] = data return dict(player_names=player_names, room=room, checks_done=checks_done, percent_total_checks_done=percent_total_checks_done, checks_in_area=checks_in_area, activity_timers=activity_timers, video=video, hints=hints, long_player_names=long_player_names, multisave=multisave, precollected_items=precollected_items, groups=groups, - locations=locations, games=games) + locations=locations, games=games, states=states) def _get_inventory_data(data: typing.Dict[str, typing.Any]) -> typing.Dict[int, typing.Dict[int, int]]: From b7ff9b69ba3677f3e7a70fa462a94097f7fedb1f Mon Sep 17 00:00:00 2001 From: Magnemania <89949176+Magnemania@users.noreply.github.com> Date: Tue, 21 Mar 2023 09:18:59 -0400 Subject: [PATCH 078/172] WG: Added Client to Setup (#1556) --- inno_setup.iss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/inno_setup.iss b/inno_setup.iss index f5f8e487ca..67cf971b66 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -84,6 +84,7 @@ Name: "client/pkmn/blue"; Description: "Pokemon Client - Pokemon Blue Setup"; Ty Name: "client/ladx"; Description: "Link's Awakening Client"; Types: full playing; ExtraDiskSpaceRequired: 1048576 Name: "client/cf"; Description: "ChecksFinder"; Types: full playing Name: "client/sc2"; Description: "Starcraft 2"; Types: full playing +Name: "client/wargroove"; Description: "Wargroove"; Types: full playing Name: "client/zl"; Description: "Zillion"; Types: full playing Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing @@ -122,6 +123,7 @@ Source: "{#source_path}\ArchipelagoFF1Client.exe"; DestDir: "{app}"; Flags: igno Source: "{#source_path}\ArchipelagoPokemonClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/pkmn Source: "{#source_path}\ArchipelagoChecksFinderClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/cf Source: "{#source_path}\ArchipelagoStarcraft2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sc2 +Source: "{#source_path}\ArchipelagoWargrooveClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/wargroove Source: "{#source_path}\ArchipelagoKH2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/kh2 Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall @@ -151,6 +153,7 @@ Name: "{commondesktop}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\Ar Name: "{commondesktop}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Tasks: desktopicon; Components: client/pkmn Name: "{commondesktop}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Tasks: desktopicon; Components: client/cf Name: "{commondesktop}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Tasks: desktopicon; Components: client/sc2 +Name: "{commondesktop}\{#MyAppName} Wargroove Client"; Filename: "{app}\ArchipelagoWargrooveClient.exe"; Tasks: desktopicon; Components: client/wargroove Name: "{commondesktop}\{#MyAppName} Kingdom Hearts 2 Client"; Filename: "{app}\ArchipelagoKH2Client.exe"; Tasks: desktopicon; Components: client/kh2 [Run] From 9f65f22fac470457dd3d30fd7b9e10dd8c98db1e Mon Sep 17 00:00:00 2001 From: Rosalie-A <61372066+Rosalie-A@users.noreply.github.com> Date: Tue, 21 Mar 2023 10:45:31 -0400 Subject: [PATCH 079/172] TLoZ: Installer Info (#1554) --- inno_setup.iss | 72 +++++++++++++++++++++++++++++++++++++++++ worlds/tloz/__init__.py | 1 + 2 files changed, 73 insertions(+) diff --git a/inno_setup.iss b/inno_setup.iss index 67cf971b66..206c33446f 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -64,6 +64,7 @@ Name: "generator/zl"; Description: "Zillion ROM Setup"; Types: full hosting; Name: "generator/pkmn_r"; Description: "Pokemon Red ROM Setup"; Types: full hosting Name: "generator/pkmn_b"; Description: "Pokemon Blue ROM Setup"; Types: full hosting Name: "generator/ladx"; Description: "Link's Awakening DX ROM Setup"; Types: full hosting +Name: "generator/tloz"; Description: "The Legend of Zelda ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 135168; Flags: disablenouninstallwarning Name: "server"; Description: "Server"; Types: full hosting Name: "client"; Description: "Clients"; Types: full playing Name: "client/la"; Description: "Links Awakening DX Client"; Types: full playing @@ -86,6 +87,7 @@ Name: "client/cf"; Description: "ChecksFinder"; Types: full playing Name: "client/sc2"; Description: "Starcraft 2"; Types: full playing Name: "client/wargroove"; Description: "Wargroove"; Types: full playing Name: "client/zl"; Description: "Zillion"; Types: full playing +Name: "client/tloz"; Description: "The Legend of Zelda"; Types: full playing Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing [Dirs] @@ -103,6 +105,7 @@ Source: "{code:GetZlROMPath}"; DestDir: "{app}"; DestName: "Zillion (UE) [!].sms Source: "{code:GetRedROMPath}"; DestDir: "{app}"; DestName: "Pokemon Red (UE) [S][!].gb"; Flags: external; Components: client/pkmn/red or generator/pkmn_r Source: "{code:GetBlueROMPath}"; DestDir: "{app}"; DestName: "Pokemon Blue (UE) [S][!].gb"; Flags: external; Components: client/pkmn/blue or generator/pkmn_b Source: "{code:GetLADXROMPath}"; DestDir: "{app}"; DestName: "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc"; Flags: external; Components: client/ladx or generator/ladx +Source: "{code:GetTLoZROMPath}"; DestDir: "{app}"; DestName: "Legend of Zelda, The (U) (PRG0) [!].nes"; Flags: external; Components: client/tloz or generator/tloz Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/sni Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp @@ -123,6 +126,7 @@ Source: "{#source_path}\ArchipelagoFF1Client.exe"; DestDir: "{app}"; Flags: igno Source: "{#source_path}\ArchipelagoPokemonClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/pkmn Source: "{#source_path}\ArchipelagoChecksFinderClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/cf Source: "{#source_path}\ArchipelagoStarcraft2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sc2 +Source: "{#source_path}\ArchipelagoZelda1Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/tloz Source: "{#source_path}\ArchipelagoWargrooveClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/wargroove Source: "{#source_path}\ArchipelagoKH2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/kh2 Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall @@ -140,6 +144,7 @@ Name: "{group}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\Archipelag Name: "{group}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Components: client/pkmn Name: "{group}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Components: client/cf Name: "{group}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Components: client/sc2 +Name: "{group}\{#MyAppName} The Legend of Zelda Client"; Filename: "{app}\ArchipelagoZelda1Client.exe"; Components: client/tloz Name: "{group}\{#MyAppName} Kingdom Hearts 2 Client"; Filename: "{app}\ArchipelagoKH2Client.exe"; Components: client/kh2 Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon @@ -153,6 +158,7 @@ Name: "{commondesktop}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\Ar Name: "{commondesktop}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Tasks: desktopicon; Components: client/pkmn Name: "{commondesktop}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Tasks: desktopicon; Components: client/cf Name: "{commondesktop}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Tasks: desktopicon; Components: client/sc2 +Name: "{commondesktop}\{#MyAppName} The Legend of Zelda Client"; Filename: "{app}\ArchipelagoZelda1Client.exe"; Tasks: desktopicon; Components: client/tloz Name: "{commondesktop}\{#MyAppName} Wargroove Client"; Filename: "{app}\ArchipelagoWargrooveClient.exe"; Tasks: desktopicon; Components: client/wargroove Name: "{commondesktop}\{#MyAppName} Kingdom Hearts 2 Client"; Filename: "{app}\ArchipelagoKH2Client.exe"; Tasks: desktopicon; Components: client/kh2 @@ -237,6 +243,11 @@ Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Arc Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: ""; Components: client/ladx Root: HKCR; Subkey: "{#MyAppName}ladxpatch\shell\open\command"; ValueData: """{app}\ArchipelagoLinksAwakeningClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/ladx +Root: HKCR; Subkey: ".aptloz"; ValueData: "{#MyAppName}tlozpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/tloz +Root: HKCR; Subkey: "{#MyAppName}tlozpatch"; ValueData: "Archipelago The Legend of Zelda Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/tloz +Root: HKCR; Subkey: "{#MyAppName}tlozpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoZelda1Client.exe,0"; ValueType: string; ValueName: ""; Components: client/tloz +Root: HKCR; Subkey: "{#MyAppName}tlozpatch\shell\open\command"; ValueData: """{app}\ArchipelagoZelda1Client.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/tloz + Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: server Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: server Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: ""; Components: server @@ -307,6 +318,9 @@ var BlueROMFilePage: TInputFileWizardPage; var ladxrom: string; var LADXROMFilePage: TInputFileWizardPage; +var tlozrom: string; +var TLoZROMFilePage: TInputFileWizardPage; + function GetSNESMD5OfFile(const rom: string): string; var data: AnsiString; begin @@ -367,6 +381,25 @@ begin end; end; +function CheckNESRom(name: string; hash: string): string; +var rom: string; +begin + log('Handling ' + name) + rom := FileSearch(name, WizardDirValue()); + if Length(rom) > 0 then + begin + log('existing ROM found'); + log(IntToStr(CompareStr(GetSMSMD5OfFile(rom), hash))); + if CompareStr(GetSMSMD5OfFile(rom), hash) = 0 then + begin + log('existing ROM verified'); + Result := rom; + exit; + end; + log('existing ROM failed verification'); + end; +end; + function AddRomPage(name: string): TInputFileWizardPage; begin Result := @@ -413,6 +446,21 @@ begin '.sms'); end; +function AddNESRomPage(name: string): TInputFileWizardPage; +begin + Result := + CreateInputFilePage( + wpSelectComponents, + 'Select ROM File', + 'Where is your ' + name + ' located?', + 'Select the file, then click Next.'); + + Result.Add( + 'Location of ROM file:', + 'NES ROM files|*.nes|All files|*.*', + '.nes'); +end; + procedure AddOoTRomPage(); begin ootrom := FileSearch('The Legend of Zelda - Ocarina of Time.z64', WizardDirValue()); @@ -467,6 +515,8 @@ begin Result := not (BlueROMFilePage.Values[0] = '') else if (assigned(LADXROMFilePage)) and (CurPageID = LADXROMFilePage.ID) then Result := not (LADXROMFilePage.Values[0] = '') + else if (assigned(TLoZROMFilePage)) and (CurPageID = TLoZROMFilePage.ID) then + Result := not (TLoZROMFilePage.Values[0] = '') else Result := True; end; @@ -630,6 +680,22 @@ begin else Result := ''; end; + +function GetTLoZROMPath(Param: string): string; +begin + if Length(tlozrom) > 0 then + Result := tlozrom + else if Assigned(TLoZROMFilePage) then + begin + R := CompareStr(GetMD5OfFile(TLoZROMFilePage.Values[0]), '337bd6f1a1163df31bf2633665589ab0'); + if R <> 0 then + MsgBox('The Legend of Zelda ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); + + Result := TLoZROMFilePage.Values[0] + end + else + Result := ''; +end; function GetLADXROMPath(Param: string): string; begin @@ -690,6 +756,10 @@ begin l2acrom := CheckRom('Lufia II - Rise of the Sinistrals (USA).sfc', '6efc477d6203ed2b3b9133c1cd9e9c5d'); if Length(l2acrom) = 0 then L2ACROMFilePage:= AddRomPage('Lufia II - Rise of the Sinistrals (USA).sfc'); + + tlozrom := CheckNESROM('Legend of Zelda, The (U) (PRG0) [!].nes', '337bd6f1a1163df31bf2633665589ab0'); + if Length(tlozrom) = 0 then + TLoZROMFilePage:= AddNESRomPage('Legend of Zelda, The (U) (PRG0) [!].nes'); end; @@ -718,4 +788,6 @@ begin Result := not (WizardIsComponentSelected('generator/pkmn_b') or WizardIsComponentSelected('client/pkmn/blue')); if (assigned(LADXROMFilePage)) and (PageID = LADXROMFilePage.ID) then Result := not (WizardIsComponentSelected('generator/ladx') or WizardIsComponentSelected('client/ladx')); + if (assigned(TLoZROMFilePage)) and (PageID = TLoZROMFilePage.ID) then + Result := not (WizardIsComponentSelected('generator/tloz') or WizardIsComponentSelected('client/tloz')); end; diff --git a/worlds/tloz/__init__.py b/worlds/tloz/__init__.py index 551d6588ef..7cdf87c300 100644 --- a/worlds/tloz/__init__.py +++ b/worlds/tloz/__init__.py @@ -134,6 +134,7 @@ class TLoZWorld(World): self.multiworld.regions.append(menu) self.multiworld.regions.append(overworld) + def create_items(self): # refer to ItemPool.py generate_itempool(self) From ddb764a9b6d8ab6774aaf16f433d9acdb52aeeaf Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Tue, 21 Mar 2023 00:09:12 -0400 Subject: [PATCH 080/172] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20Skip=20unnecessar?= =?UTF-8?q?y=20sweeps=20for=20events?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- worlds/pokemon_rb/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index 344b96f2b9..cb9af54947 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -345,8 +345,9 @@ class PokemonRedBlueWorld(World): for item in reversed(self.multiworld.itempool): if item.player == self.player and loc.can_fill(self.multiworld.state, item, False): self.multiworld.itempool.remove(item) - state = sweep_from_pool(self.multiworld.state, self.multiworld.itempool + unplaced_items) - if state.can_reach(loc, "Location", self.player): + if item.advancement: + state = sweep_from_pool(self.multiworld.state, self.multiworld.itempool + unplaced_items) + if (not item.advancement) or state.can_reach(loc, "Location", self.player): loc.place_locked_item(item) break else: From 5a4203649df036197f5438c4a1fa08da2a33ddb4 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Tue, 21 Mar 2023 00:09:20 -0400 Subject: [PATCH 081/172] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20Quiz=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- worlds/pokemon_rb/rom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/pokemon_rb/rom.py b/worlds/pokemon_rb/rom.py index 1a5f3250aa..dd7d29967a 100644 --- a/worlds/pokemon_rb/rom.py +++ b/worlds/pokemon_rb/rom.py @@ -580,7 +580,7 @@ def write_quizzes(self, data, random): if location.player == self.player: player_name = "yourself" else: - player_name = self.multiworld.player_names[location.player] + player_name = self.multiworld.player_name[location.player] if not a: if len(self.multiworld.player_name) > 1: old_name = player_name From 856efebc39aaf12ec4afa7c829c0eee514e60a00 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 21 Mar 2023 09:50:50 -0500 Subject: [PATCH 082/172] Multiserver: Only update client status for a slot when the first enters and the last leaves (#1358) --- MultiServer.py | 8 +++++--- docs/network protocol.md | 8 ++++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 4eadbb7998..9a91b50c8a 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -781,7 +781,8 @@ async def on_client_disconnected(ctx: Context, client: Client): async def on_client_joined(ctx: Context, client: Client): - update_client_status(ctx, client, ClientStatus.CLIENT_CONNECTED) + if ctx.client_game_state[client.team, client.slot] == ClientStatus.CLIENT_UNKNOWN: + update_client_status(ctx, client, ClientStatus.CLIENT_CONNECTED) version_str = '.'.join(str(x) for x in client.version) verb = "tracking" if "Tracker" in client.tags else "playing" ctx.broadcast_text_all( @@ -798,11 +799,12 @@ async def on_client_joined(ctx: Context, client: Client): async def on_client_left(ctx: Context, client: Client): - update_client_status(ctx, client, ClientStatus.CLIENT_UNKNOWN) + if len(ctx.clients[client.team][client.slot]) < 1: + update_client_status(ctx, client, ClientStatus.CLIENT_UNKNOWN) + ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc) ctx.broadcast_text_all( "%s (Team #%d) has left the game" % (ctx.get_aliased_name(client.team, client.slot), client.team + 1), {"type": "Part", "team": client.team, "slot": client.slot}) - ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc) async def countdown(ctx: Context, timer: int): diff --git a/docs/network protocol.md b/docs/network protocol.md index c320934bd1..f4e261dcee 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -70,7 +70,7 @@ Sent to clients when they connect to an Archipelago server. | tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. Example: `WebHost` | | password | bool | Denoted whether a password is required to join this room. | | permissions | dict\[str, [Permission](#Permission)\[int\]\] | Mapping of permission name to [Permission](#Permission), keys are: "release", "collect" and "remaining". | -| hint_cost | int | The amount of points it costs to receive a hint from the 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.** | @@ -555,12 +555,16 @@ Color options: `flags` contains the [NetworkItem](#NetworkItem) flags that belong to the item ### Client States -An enumeration containing the possible client states that may be used to inform the server in [StatusUpdate](#StatusUpdate). +An enumeration containing the possible client states that may be used to inform +the server in [StatusUpdate](#StatusUpdate). The MultiServer automatically sets +the client state to `ClientStatus.CLIENT_CONNECTED` on the first active connection +to a slot. ```python import enum class ClientStatus(enum.IntEnum): CLIENT_UNKNOWN = 0 + CLIENT_CONNECTED = 5 CLIENT_READY = 10 CLIENT_PLAYING = 20 CLIENT_GOAL = 30 From c2a8b842de9d99cc950d86427c370dffe2ed8b26 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 21 Mar 2023 15:53:10 +0100 Subject: [PATCH 083/172] Core: typo --- NetUtils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetUtils.py b/NetUtils.py index ca44fdea22..2b9a653123 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -35,7 +35,7 @@ class SlotType(enum.IntFlag): @property def always_goal(self) -> bool: - """Mark this slot has having reached its goal instantly.""" + """Mark this slot as having reached its goal instantly.""" return self.value != 0b01 From 01c13ca243050575e1c5eec4dc83f4d8215914d5 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 21 Mar 2023 09:36:25 +0100 Subject: [PATCH 084/172] Docs: some clarification in running from source --- docs/running from source.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/running from source.md b/docs/running from source.md index 2bda62ec1a..cb1a8fa50b 100644 --- a/docs/running from source.md +++ b/docs/running from source.md @@ -7,10 +7,11 @@ use that version. These steps are for developers or platforms without compiled r ## General What you'll need: - * Python 3.8.7 or newer - * pip (Depending on platform may come included) - * A C compiler - * possibly optional, read OS-specific sections + * [Python 3.8.7 or newer](https://www.python.org/downloads/), not the Windows Store version + * **Python 3.11 does not work currently** + * pip: included in downloads from python.org, separate in many Linux distributions + * Matching C compiler + * possibly optional, read operating system specific sections Then run any of the starting point scripts, like Generate.py, and the included ModuleUpdater should prompt to install or update the required modules and after pressing enter proceed to install everything automatically. @@ -29,6 +30,8 @@ After this, you should be able to run the programs. Recommended steps * Download and install a "Windows installer (64-bit)" from the [Python download page](https://www.python.org/downloads) + * **Python 3.11 does not work currently** + * Download and install full Visual Studio from [Visual Studio Downloads](https://visualstudio.microsoft.com/downloads/) or an older "Build Tools for Visual Studio" from @@ -40,6 +43,8 @@ Recommended steps * It is recommended to use [PyCharm IDE](https://www.jetbrains.com/pycharm/) * Run Generate.py which will prompt installation of missing modules, press enter to confirm + * In PyCharm: right-click Generate.py and select `Run 'Generate'` + * Without PyCharm: open a command prompt in the source folder and type `py Generate.py` ## macOS @@ -59,7 +64,7 @@ setting in host.yaml at your Enemizer executable. ## Optional: SNI -SNI is required to use SNIClient. If not integrated into the project, it has to be started manually. +[SNI](https://github.com/alttpo/sni/blob/main/README.md) is required to use SNIClient. If not integrated into the project, it has to be started manually. You can get the latest SNI release at [SNI Github releases](https://github.com/alttpo/sni/releases). It should be dropped as "SNI" into the root folder of the project. Alternatively, you can point the sni setting in From 91502505a165740017797ca944d0173a5c2a2b31 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 21 Mar 2023 20:05:37 +0100 Subject: [PATCH 085/172] WebHost: fix type in states template --- WebHostLib/templates/multiTracker.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/templates/multiTracker.html b/WebHostLib/templates/multiTracker.html index 11f62fd8bd..d1e9d8764f 100644 --- a/WebHostLib/templates/multiTracker.html +++ b/WebHostLib/templates/multiTracker.html @@ -53,7 +53,7 @@ {{ checks["Total"] }}/{{ checks_in_area[player]["Total"] }} {{ percent_total_checks_done[team][player] }} {{ {0: "Disconnected", 5: "Connected", 10: "Ready", 20: "Playing", - "30": "Goal Completed"}.get(states[team, player], "Unknown State") }} + 30: "Goal Completed"}.get(states[team, player], "Unknown State") }} {%- if activity_timers[team, player] -%} {{ activity_timers[team, player].total_seconds() }} {%- else -%} From 1c69fb3c3ca650014179f6a30cf053986123f4f1 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 21 Mar 2023 15:21:27 -0500 Subject: [PATCH 086/172] The Messenger: Add more difficult logic options (#1550) --- test/TestBase.py | 8 +- worlds/messenger/Options.py | 18 +++- worlds/messenger/Rules.py | 126 ++++++++++++++++++++++--- worlds/messenger/SubClasses.py | 8 +- worlds/messenger/__init__.py | 21 ++++- worlds/messenger/test/TestAccess.py | 30 ++---- worlds/messenger/test/TestLogic.py | 107 +++++++++++++++++++++ worlds/messenger/test/TestNotes.py | 5 + worlds/messenger/test/TestShopChest.py | 4 +- 9 files changed, 277 insertions(+), 50 deletions(-) create mode 100644 worlds/messenger/test/TestLogic.py diff --git a/test/TestBase.py b/test/TestBase.py index a2c9bc28aa..17fe6425df 100644 --- a/test/TestBase.py +++ b/test/TestBase.py @@ -199,11 +199,15 @@ class WorldTestBase(unittest.TestCase): self.collect_all_but(all_items) for location in self.multiworld.get_locations(): - self.assertEqual(self.multiworld.state.can_reach(location), location.name not in locations) + loc_reachable = self.multiworld.state.can_reach(location) + self.assertEqual(loc_reachable, location.name not in locations, + f"{location.name} is reachable without {all_items}" if loc_reachable + else f"{location.name} is not reachable without {all_items}") for item_names in possible_items: items = self.collect_by_name(item_names) for location in locations: - self.assertTrue(self.can_reach_location(location)) + self.assertTrue(self.can_reach_location(location), + f"{location} not reachable with {item_names}") self.remove(items) def assertBeatable(self, beatable: bool): diff --git a/worlds/messenger/Options.py b/worlds/messenger/Options.py index 1baca12e3a..47ebf66f28 100644 --- a/worlds/messenger/Options.py +++ b/worlds/messenger/Options.py @@ -7,9 +7,19 @@ class MessengerAccessibility(Accessibility): __doc__ = Accessibility.__doc__.replace(f"default {Accessibility.default}", f"default {default}") -class Logic(DefaultOnToggle): - """Whether the seed should be guaranteed completable.""" - display_name = "Use Logic" +class Logic(Choice): + """ + The level of logic to use when determining what locations in your world are accessible. + Normal can require damage boosts, but otherwise approachable for someone who has beaten the game. + Hard has some easier speedrunning tricks in logic. May need to leash. + Challenging contains more medium and hard difficulty speedrunning tricks. + OoB places everything with the minimum amount of rules possible. Expect to do OoB. Not guaranteed completable. + """ + display_name = "Logic Level" + option_normal = 0 + option_hard = 1 + option_challenging = 2 + option_oob = 3 class PowerSeals(DefaultOnToggle): @@ -55,7 +65,7 @@ class RequiredSeals(Range): messenger_options = { "accessibility": MessengerAccessibility, - "enable_logic": Logic, + "logic_level": Logic, "shuffle_seals": PowerSeals, "goal": Goal, "music_box": MusicBox, diff --git a/worlds/messenger/Rules.py b/worlds/messenger/Rules.py index a7e0a1a76b..24e0354467 100644 --- a/worlds/messenger/Rules.py +++ b/worlds/messenger/Rules.py @@ -1,7 +1,7 @@ from typing import Dict, Callable, TYPE_CHECKING from BaseClasses import CollectionState, MultiWorld -from worlds.generic.Rules import set_rule, allow_self_locking_items +from worlds.generic.Rules import set_rule, allow_self_locking_items, add_rule from .Options import MessengerAccessibility, Goal from .Constants import NOTES, PHOBEKINS @@ -14,12 +14,14 @@ else: class MessengerRules: player: int world: MessengerWorld + region_rules: Dict[str, Callable[[CollectionState], bool]] + location_rules: Dict[str, Callable[[CollectionState], bool]] - def __init__(self, world: MessengerWorld): + def __init__(self, world: MessengerWorld) -> None: self.player = world.player self.world = world - self.region_rules: Dict[str, Callable[[CollectionState], bool]] = { + self.region_rules = { "Ninja Village": self.has_wingsuit, "Autumn Hills": self.has_wingsuit, "Catacombs": self.has_wingsuit, @@ -27,13 +29,13 @@ class MessengerRules: "Searing Crags Upper": self.has_vertical, "Cloud Ruins": lambda state: self.has_wingsuit(state) and state.has("Ruxxtin's Amulet", self.player), "Underworld": self.has_tabi, - "Forlorn Temple": lambda state: state.has_all(PHOBEKINS, self.player) and self.has_wingsuit(state), + "Forlorn Temple": lambda state: state.has_all({"Wingsuit", *PHOBEKINS}, self.player), "Glacial Peak": self.has_vertical, "Elemental Skylands": lambda state: state.has("Fairy Bottle", self.player), - "Music Box": lambda state: state.has_all(NOTES, self.player) + "Music Box": lambda state: state.has_all(set(NOTES), self.player) and self.has_vertical(state) } - self.location_rules: Dict[str, Callable[[CollectionState], bool]] = { + self.location_rules = { # ninja village "Ninja Village Seal - Tree House": self.has_dart, # autumn hills @@ -88,8 +90,11 @@ class MessengerRules: return self.has_wingsuit(state) or self.has_dart(state) def has_enough_seals(self, state: CollectionState) -> bool: - required_seals = state.multiworld.worlds[self.player].required_seals - return state.has("Power Seal", self.player, required_seals) + return not self.world.required_seals or state.has("Power Seal", self.player, self.world.required_seals) + + def true(self, state: CollectionState) -> bool: + """I know this is stupid, but it's easier to read in the dicts.""" + return True def set_messenger_rules(self) -> None: multiworld = self.world.multiworld @@ -105,14 +110,111 @@ class MessengerRules: set_rule(multiworld.get_entrance("Tower HQ -> Music Box", self.player), lambda state: state.has("Shop Chest", self.player)) - if multiworld.enable_logic[self.player]: - multiworld.completion_condition[self.player] = lambda state: state.has("Rescue Phantom", self.player) - else: - multiworld.accessibility[self.player].value = MessengerAccessibility.option_minimal + multiworld.completion_condition[self.player] = lambda state: state.has("Rescue Phantom", self.player) if multiworld.accessibility[self.player] > MessengerAccessibility.option_locations: set_self_locking_items(multiworld, self.player) +class MessengerHardRules(MessengerRules): + extra_rules: Dict[str, Callable[[CollectionState], bool]] + + def __init__(self, world: MessengerWorld) -> None: + super().__init__(world) + + self.region_rules.update({ + "Ninja Village": self.has_vertical, + "Autumn Hills": self.has_vertical, + "Catacombs": self.has_vertical, + "Bamboo Creek": self.has_vertical, + "Forlorn Temple": lambda state: self.has_vertical(state) and state.has_all(set(PHOBEKINS), self.player), + "Searing Crags Upper": self.true, + "Glacial Peak": self.true, + }) + + self.location_rules.update({ + "Howling Grotto Seal - Windy Saws and Balls": self.true, + "Glacial Peak Seal - Projectile Spike Pit": self.true, + }) + + self.extra_rules = { + "Climbing Claws": self.has_dart, + "Astral Seed": self.has_dart, + "Candle": self.has_dart, + "Key of Strength": lambda state: state.has("Power Thistle", self.player) or + self.has_dart(state) or + self.has_windmill(state), + "Key of Symbiosis": self.has_windmill, + "Autumn Hills Seal - Spike Ball Darts": lambda state: (self.has_dart(state) and self.has_windmill(state)) + or self.has_wingsuit(state), + "Glacial Peak Seal - Glacial Air Swag": self.has_windmill, + "Underworld Seal - Fireball Wave": lambda state: self.has_wingsuit(state) + or state.has_all({"Ninja Tabi", "Windmill Shuriken"}, + self.player), + } + + def has_windmill(self, state: CollectionState) -> bool: + return state.has("Windmill Shuriken", self.player) + + def set_messenger_rules(self) -> None: + super().set_messenger_rules() + for loc, rule in self.extra_rules.items(): + add_rule(self.world.multiworld.get_location(loc, self.player), rule, "or") + + +class MessengerChallengeRules(MessengerHardRules): + def __init__(self, world: MessengerWorld) -> None: + super().__init__(world) + + self.region_rules.update({ + "Forlorn Temple": lambda state: (self.has_vertical(state) and state.has_all(set(PHOBEKINS), self.player)) + or state.has_all({"Wingsuit", "Windmill Shuriken"}, self.player), + "Elemental Skylands": lambda state: self.has_wingsuit(state) or state.has("Fairy Bottle", self.player) + }) + + self.location_rules.update({ + "Fairy Bottle": self.true, + "Howling Grotto Seal - Crushing Pits": self.true, + "Underworld Seal - Sharp and Windy Climb": self.true, + "Riviere Turquoise Seal - Flower Power": self.true, + }) + + self.extra_rules.update({ + "Key of Hope": self.has_vertical, + "Key of Symbiosis": lambda state: self.has_vertical(state) or self.has_windmill(state), + }) + + +class MessengerOOBRules(MessengerRules): + def __init__(self, world: MessengerWorld) -> None: + self.world = world + self.player = world.player + + self.region_rules = { + "Elemental Skylands": lambda state: state.has_any({"Wingsuit", "Rope Dart", "Fairy Bottle"}, self.player), + "Music Box": lambda state: state.has_all(set(NOTES), self.player) + } + + self.location_rules = { + "Claustro": self.has_wingsuit, + "Key of Strength": self.has_wingsuit, + "Key of Love": lambda state: state.has_all({"Sun Crest", "Moon Crest"}, self.player), + "Pyro": self.has_tabi, + "Key of Chaos": self.has_tabi, + "Key of Courage": lambda state: state.has_all({"Demon King Crown", "Fairy Bottle"}, self.player), + "Autumn Hills Seal - Spike Ball Darts": self.has_dart, + "Ninja Village Seal - Tree House": self.has_dart, + "Underworld Seal - Fireball Wave": lambda state: state.has_any({"Wingsuit", "Windmill Shuriken"}, + self.player), + "Tower of Time Seal - Time Waster Seal": self.has_dart, + "Shop Chest": self.has_enough_seals + } + + def set_messenger_rules(self) -> None: + super().set_messenger_rules() + self.world.multiworld.completion_condition[self.player] = lambda state: True + self.world.multiworld.accessibility[self.player].value = MessengerAccessibility.option_minimal + + def set_self_locking_items(multiworld: MultiWorld, player: int) -> None: # do the ones for seal shuffle on and off first allow_self_locking_items(multiworld.get_location("Key of Strength", player), "Power Thistle") diff --git a/worlds/messenger/SubClasses.py b/worlds/messenger/SubClasses.py index 32803f5e0d..1f26a4265b 100644 --- a/worlds/messenger/SubClasses.py +++ b/worlds/messenger/SubClasses.py @@ -12,7 +12,7 @@ else: class MessengerRegion(Region): - def __init__(self, name: str, world: MessengerWorld): + def __init__(self, name: str, world: MessengerWorld) -> None: super().__init__(name, world.player, world.multiworld) self.add_locations(self.multiworld.worlds[self.player].location_name_to_id) world.multiworld.regions.append(self) @@ -38,7 +38,7 @@ class MessengerRegion(Region): class MessengerLocation(Location): game = "The Messenger" - def __init__(self, name: str, parent: MessengerRegion, loc_id: Optional[int]): + def __init__(self, name: str, parent: MessengerRegion, loc_id: Optional[int]) -> None: super().__init__(parent.player, name, loc_id, parent) if loc_id is None: self.place_locked_item(MessengerItem(name, parent.player, None)) @@ -47,8 +47,8 @@ class MessengerLocation(Location): class MessengerItem(Item): game = "The Messenger" - def __init__(self, name: str, player: int, item_id: Optional[int] = None): - if name in {*NOTES, *PROG_ITEMS, *PHOBEKINS} or item_id is None: + def __init__(self, name: str, player: int, item_id: Optional[int] = None, override_progression: bool = False) -> None: + if name in {*NOTES, *PROG_ITEMS, *PHOBEKINS} or item_id is None or override_progression: item_class = ItemClassification.progression elif name in USEFUL_ITEMS: item_class = ItemClassification.useful diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index 1c42b30494..495ec80b8f 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -3,10 +3,10 @@ from typing import Dict, Any, List, Optional from BaseClasses import Tutorial, ItemClassification from worlds.AutoWorld import World, WebWorld from .Constants import NOTES, PROG_ITEMS, PHOBEKINS, USEFUL_ITEMS, ALWAYS_LOCATIONS, SEALS, ALL_ITEMS -from .Options import messenger_options, NotesNeeded, Goal, PowerSeals +from .Options import messenger_options, NotesNeeded, Goal, PowerSeals, Logic from .Regions import REGIONS, REGION_CONNECTIONS -from .Rules import MessengerRules from .SubClasses import MessengerRegion, MessengerItem +from . import Rules class MessengerWeb(WebWorld): @@ -100,7 +100,15 @@ class MessengerWorld(World): self.multiworld.itempool += itempool def set_rules(self) -> None: - MessengerRules(self).set_messenger_rules() + logic = self.multiworld.logic_level[self.player] + if logic == Logic.option_normal: + Rules.MessengerRules(self).set_messenger_rules() + elif logic == Logic.option_hard: + Rules.MessengerHardRules(self).set_messenger_rules() + elif logic == Logic.option_challenging: + Rules.MessengerChallengeRules(self).set_messenger_rules() + else: + Rules.MessengerOOBRules(self).set_messenger_rules() def fill_slot_data(self) -> Dict[str, Any]: locations: Dict[int, List[str]] = {} @@ -114,7 +122,8 @@ class MessengerWorld(World): "music_box": self.multiworld.music_box[self.player].value, "required_seals": self.required_seals, "locations": locations, - "settings": {"Difficulty": "Basic" if not self.multiworld.shuffle_seals[self.player] else "Advanced"} + "settings": {"Difficulty": "Basic" if not self.multiworld.shuffle_seals[self.player] else "Advanced"}, + "logic": self.multiworld.logic_level[self.player].current_key, } def get_filler_item_name(self) -> str: @@ -122,4 +131,6 @@ class MessengerWorld(World): def create_item(self, name: str) -> MessengerItem: item_id: Optional[int] = self.item_name_to_id.get(name, None) - return MessengerItem(name, self.player, item_id) + override_prog = name in {"Windmill Shuriken"} and getattr(self, "multiworld") is not None \ + and self.multiworld.logic_level[self.player] > Logic.option_normal + return MessengerItem(name, self.player, item_id, override_prog) diff --git a/worlds/messenger/test/TestAccess.py b/worlds/messenger/test/TestAccess.py index eba4ad9bbf..83bdc6113d 100644 --- a/worlds/messenger/test/TestAccess.py +++ b/worlds/messenger/test/TestAccess.py @@ -1,6 +1,5 @@ from . import MessengerTestBase from ..Constants import NOTES, PHOBEKINS -from ..Options import MessengerAccessibility class AccessTest(MessengerTestBase): @@ -46,22 +45,22 @@ class AccessTest(MessengerTestBase): "Glacial Peak Seal - Ice Climbers", "Tower of Time Seal - Time Waster Seal", "Underworld Seal - Rising Fanta", "Key of Symbiosis", "Elemental Skylands Seal - Water", "Elemental Skylands Seal - Fire", "Candle", - "Ninja Village Seal - Tree House", "Climbing Claws", "Key of Hope", - "Autumn Hills Seal - Trip Saws", "Autumn Hills Seal - Double Swing Saws", - "Autumn Hills Seal - Spike Ball Swing", "Autumn Hills Seal - Spike Ball Darts", "Necro", - "Ruxxtin's Amulet", "Catacombs Seal - Triple Spike Crushers", "Catacombs Seal - Crusher Gauntlet", + "Climbing Claws", "Key of Hope", "Autumn Hills Seal - Trip Saws", + "Autumn Hills Seal - Double Swing Saws", "Autumn Hills Seal - Spike Ball Swing", + "Autumn Hills Seal - Spike Ball Darts", "Necro", "Ruxxtin's Amulet", + "Catacombs Seal - Triple Spike Crushers", "Catacombs Seal - Crusher Gauntlet", "Catacombs Seal - Dirty Pond", "Claustro", "Acro", "Bamboo Creek Seal - Spike Crushers and Doors", "Bamboo Creek Seal - Spike Ball Pits", "Bamboo Creek Seal - Spike Crushers and Doors v2", "Howling Grotto Seal - Crushing Pits", "Howling Grotto Seal - Windy Saws and Balls", - "Tower of Time Seal - Lantern Climb", "Demon King Crown", "Cloud Ruins Seal - Ghost Pit", - "Cloud Ruins Seal - Toothbrush Alley", "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room", + "Demon King Crown", "Cloud Ruins Seal - Ghost Pit", "Cloud Ruins Seal - Toothbrush Alley", + "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room", "Tower of Time Seal - Lantern Climb", "Tower of Time Seal - Arcane Orbs", "Underworld Seal - Sharp and Windy Climb", "Underworld Seal - Fireball Wave", "Elemental Skylands Seal - Air", "Forlorn Temple Seal - Rocket Maze", "Forlorn Temple Seal - Rocket Sunset", "Power Thistle", "Key of Strength", "Glacial Peak Seal - Projectile Spike Pit", "Glacial Peak Seal - Glacial Air Swag", "Fairy Bottle", "Riviere Turquoise Seal - Flower Power", "Searing Crags Seal - Triple Ball Spinner", "Searing Crags Seal - Raining Rocks", - "Searing Crags Seal - Rhythm Rocks", "Astral Seed", "Astral Tea Leaves"] + "Searing Crags Seal - Rhythm Rocks", "Astral Seed", "Astral Tea Leaves", "Rescue Phantom"] items = [["Wingsuit", "Rope Dart"]] self.assertAccessDependency(locations, items) @@ -116,8 +115,8 @@ class AccessTest(MessengerTestBase): class ItemsAccessTest(MessengerTestBase): options = { - "shuffle_seals": False, - "accessibility": MessengerAccessibility.option_items + "shuffle_seals": "false", + "accessibility": "items" } def testSelfLockingItems(self) -> None: @@ -136,14 +135,3 @@ class ItemsAccessTest(MessengerTestBase): with self.subTest("Fulfills Accessibility", location=loc, item=item_name): self.assertTrue(self.multiworld.get_location(loc, self.player).can_fill(self.multiworld.state, item, True)) - -class NoLogicTest(MessengerTestBase): - options = { - "enable_logic": "false" - } - - def testNoLogic(self) -> None: - """Test some funny locations to make sure they aren't reachable but we can still win""" - self.assertEqual(self.can_reach_location("Pyro"), False) - self.assertEqual(self.can_reach_location("Rescue Phantom"), False) - self.assertBeatable(True) diff --git a/worlds/messenger/test/TestLogic.py b/worlds/messenger/test/TestLogic.py new file mode 100644 index 0000000000..f12f3bda4e --- /dev/null +++ b/worlds/messenger/test/TestLogic.py @@ -0,0 +1,107 @@ +from BaseClasses import ItemClassification +from . import MessengerTestBase + + +class HardLogicTest(MessengerTestBase): + options = { + "logic_level": "hard" + } + + def testVertical(self) -> None: + """Test the locations that still require wingsuit or rope dart.""" + locations = [ + # tower of time + "Tower of Time Seal - Time Waster Seal", "Tower of Time Seal - Lantern Climb", + "Tower of Time Seal - Arcane Orbs", + # ninja village + "Candle", "Astral Seed", "Ninja Village Seal - Tree House", + # autumn hills + "Climbing Claws", "Key of Hope", + "Autumn Hills Seal - Trip Saws", "Autumn Hills Seal - Double Swing Saws", + "Autumn Hills Seal - Spike Ball Swing", "Autumn Hills Seal - Spike Ball Darts", + # forlorn temple + "Demon King Crown", + "Forlorn Temple Seal - Rocket Maze", "Forlorn Temple Seal - Rocket Sunset", + # catacombs + "Necro", "Ruxxtin's Amulet", + "Catacombs Seal - Triple Spike Crushers", "Catacombs Seal - Crusher Gauntlet", "Catacombs Seal - Dirty Pond", + # bamboo creek + "Claustro", + "Bamboo Creek Seal - Spike Crushers and Doors", "Bamboo Creek Seal - Spike Ball Pits", + "Bamboo Creek Seal - Spike Crushers and Doors v2", + # howling grotto + "Howling Grotto Seal - Crushing Pits", "Howling Grotto Seal - Crushing Pits", + # glacial peak + "Glacial Peak Seal - Ice Climbers", + # cloud ruins + "Acro", "Cloud Ruins Seal - Ghost Pit", + "Cloud Ruins Seal - Toothbrush Alley", "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room", + # underworld + "Underworld Seal - Rising Fanta", "Underworld Seal - Sharp and Windy Climb", + # riviere turquoise + "Fairy Bottle", "Riviere Turquoise Seal - Flower Power", + # elemental skylands + "Elemental Skylands Seal - Air", "Elemental Skylands Seal - Water", "Elemental Skylands Seal - Fire", + # phantom + "Rescue Phantom", + ] + items = [["Wingsuit", "Rope Dart"]] + self.assertAccessDependency(locations, items) + + def testWindmill(self) -> None: + """Windmill Shuriken isn't progression on normal difficulty, so test it's marked correctly and required.""" + self.assertEqual(ItemClassification.progression, self.get_item_by_name("Windmill Shuriken").classification) + windmill_locs = [ + "Key of Strength", + "Key of Symbiosis", + "Underworld Seal - Fireball Wave" + ] + for loc in windmill_locs: + with self.subTest("can't reach location with nothing", location=loc): + self.assertFalse(self.can_reach_location(loc)) + + items = self.get_items_by_name(["Windmill Shuriken", "Ninja Tabi", "Fairy Bottle"]) + self.collect(items) + for loc in windmill_locs: + with self.subTest("can reach with Windmill", location=loc): + self.assertTrue(self.can_reach_location(loc)) + + special_loc = "Autumn Hills Seal - Spike Ball Darts" + item = self.get_item_by_name("Wingsuit") + self.collect(item) + self.assertTrue(self.can_reach_location(special_loc)) + self.remove(item) + + item = self.get_item_by_name("Rope Dart") + self.collect(item) + self.assertTrue(self.can_reach_location(special_loc)) + + +class ChallengingLogicTest(MessengerTestBase): + options = { + "logic_level": "challenging" + } + + +class NoLogicTest(MessengerTestBase): + options = { + "logic_level": "oob" + } + + def testAccess(self) -> None: + """Test the locations with rules still require things.""" + all_locations = [ + "Claustro", "Key of Strength", "Key of Symbiosis", "Key of Love", "Pyro", "Key of Chaos", "Key of Courage", + "Autumn Hills Seal - Spike Ball Darts", "Ninja Village Seal - Tree House", "Underworld Seal - Fireball Wave", + "Tower of Time Seal - Time Waster Seal", "Rescue Phantom", "Elemental Skylands Seal - Air", + "Elemental Skylands Seal - Water", "Elemental Skylands Seal - Fire", + ] + for loc in all_locations: + with self.subTest("Default unreachables", location=loc): + self.assertFalse(self.can_reach_location(loc)) + + def testNoLogic(self) -> None: + """Test some funny locations to make sure they aren't reachable, but we can still win""" + self.assertEqual(self.can_reach_location("Pyro"), False) + self.assertEqual(self.can_reach_location("Rescue Phantom"), False) + self.assertBeatable(True) diff --git a/worlds/messenger/test/TestNotes.py b/worlds/messenger/test/TestNotes.py index 07745e3397..c4292e4900 100644 --- a/worlds/messenger/test/TestNotes.py +++ b/worlds/messenger/test/TestNotes.py @@ -27,4 +27,9 @@ class DefaultGoalTest(MessengerTestBase): def testGoal(self) -> None: self.assertBeatable(False) self.collect_by_name(NOTES) + rope_dart = self.get_item_by_name("Rope Dart") + self.collect(rope_dart) + self.assertBeatable(True) + self.remove(rope_dart) + self.collect_by_name("Wingsuit") self.assertBeatable(True) diff --git a/worlds/messenger/test/TestShopChest.py b/worlds/messenger/test/TestShopChest.py index c3f2c4dd55..9289ec9970 100644 --- a/worlds/messenger/test/TestShopChest.py +++ b/worlds/messenger/test/TestShopChest.py @@ -4,11 +4,11 @@ from . import MessengerTestBase class NoLogicTest(MessengerTestBase): options = { - "enable_logic": "false", + "logic_level": "oob", "goal": "power_seal_hunt", } - def testChestAccess(self): + def testChestAccess(self) -> None: """Test to make sure we can win even though we can't reach the chest.""" self.assertEqual(self.can_reach_location("Shop Chest"), False) self.assertBeatable(True) From 2fb9176511ad211d2946a4c49fb7777b9c8e1783 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Tue, 21 Mar 2023 15:23:45 -0500 Subject: [PATCH 087/172] Clique: The greatest game of all time. (#1566) --- README.md | 1 + worlds/clique/Options.py | 13 ++++ worlds/clique/__init__.py | 109 ++++++++++++++++++++++++++++++++ worlds/clique/docs/en_Clique.md | 11 ++++ worlds/clique/docs/guide_en.md | 6 ++ 5 files changed, 140 insertions(+) create mode 100644 worlds/clique/Options.py create mode 100644 worlds/clique/__init__.py create mode 100644 worlds/clique/docs/en_Clique.md create mode 100644 worlds/clique/docs/guide_en.md diff --git a/README.md b/README.md index 9e3d6a450c..2c49aba929 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Currently, the following games are supported: * The Messenger * Kingdom Hearts 2 * The Legend of Zelda: Link's Awakening DX +* Clique For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/worlds/clique/Options.py b/worlds/clique/Options.py new file mode 100644 index 0000000000..1d74d2c5a5 --- /dev/null +++ b/worlds/clique/Options.py @@ -0,0 +1,13 @@ +from typing import Dict + +from Options import Option, Toggle + + +class HardMode(Toggle): + """Only for masochists: requires 2 presses!""" + display_name = "Hard Mode" + + +clique_options: Dict[str, type(Option)] = { + "hard_mode": HardMode +} diff --git a/worlds/clique/__init__.py b/worlds/clique/__init__.py new file mode 100644 index 0000000000..c73d0437bf --- /dev/null +++ b/worlds/clique/__init__.py @@ -0,0 +1,109 @@ +from BaseClasses import Entrance, Item, ItemClassification, Location, MultiWorld, Region, Tutorial +from worlds.AutoWorld import WebWorld, World +from worlds.generic.Rules import set_rule +from .Options import clique_options + +item_table = { + "The feeling of satisfaction.": 69696969, + "Button Key": 69696968, +} + +location_table = { + "The Button": 69696969, + "The Desk": 69696968, +} + + +class CliqueWebWorld(WebWorld): + theme = "partyTime" + tutorials = [ + Tutorial( + tutorial_name="Start Guide", + description="A guide to playing Clique.", + language="English", + file_name="guide_en.md", + link="guide/en", + authors=["Phar"] + ) + ] + + +class CliqueWorld(World): + """The greatest game ever designed. Full of exciting gameplay!""" + + game = "Clique" + topology_present = False + data_version = 1 + web = CliqueWebWorld() + option_definitions = clique_options + + location_name_to_id = location_table + item_name_to_id = item_table + + def create_item(self, name: str) -> "Item": + return Item(name, ItemClassification.progression, self.item_name_to_id[name], self.player) + + def get_setting(self, name: str): + return getattr(self.multiworld, name)[self.player] + + def fill_slot_data(self) -> dict: + return {option_name: self.get_setting(option_name).value for option_name in self.option_definitions} + + def generate_basic(self) -> None: + self.multiworld.itempool.append(self.create_item("The feeling of satisfaction.")) + + if self.multiworld.hard_mode[self.player]: + self.multiworld.itempool.append(self.create_item("Button Key")) + + def create_regions(self) -> None: + if self.multiworld.hard_mode[self.player]: + self.multiworld.regions += [ + create_region(self.multiworld, self.player, "Menu", None, ["Entrance to THE BUTTON"]), + create_region(self.multiworld, self.player, "THE BUTTON", self.location_name_to_id) + ] + else: + self.multiworld.regions += [ + create_region(self.multiworld, self.player, "Menu", None, ["Entrance to THE BUTTON"]), + create_region(self.multiworld, self.player, "THE BUTTON", {"The Button": 69696969}) + ] + + self.multiworld.get_entrance("Entrance to THE BUTTON", self.player)\ + .connect(self.multiworld.get_region("THE BUTTON", self.player)) + + def get_filler_item_name(self) -> str: + return self.multiworld.random.choice(item_table) + + def set_rules(self) -> None: + if self.multiworld.hard_mode[self.player]: + set_rule( + self.multiworld.get_location("The Button", self.player), + lambda state: state.has("Button Key", self.player) + ) + + self.multiworld.completion_condition[self.player] = lambda state: \ + state.has("Button Key", self.player) + else: + self.multiworld.completion_condition[self.player] = lambda state: \ + state.has("The feeling of satisfaction.", self.player) + + +def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None): + region = Region(name, player, world) + if locations: + for location_name in locations.keys(): + location = CliqueLocation(player, location_name, locations[location_name], region) + region.locations.append(location) + + if exits: + for _exit in exits: + region.exits.append(Entrance(player, _exit, region)) + + return region + + +class CliqueItem(Item): + game = "Clique" + + +class CliqueLocation(Location): + game: str = "Clique" diff --git a/worlds/clique/docs/en_Clique.md b/worlds/clique/docs/en_Clique.md new file mode 100644 index 0000000000..bf0562e2ba --- /dev/null +++ b/worlds/clique/docs/en_Clique.md @@ -0,0 +1,11 @@ +# Clique + +## What is this game? + +Even I don't know. + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure +and export a config file. + diff --git a/worlds/clique/docs/guide_en.md b/worlds/clique/docs/guide_en.md new file mode 100644 index 0000000000..7b4b0f1c21 --- /dev/null +++ b/worlds/clique/docs/guide_en.md @@ -0,0 +1,6 @@ +# Clique Start Guide + +Go to the [Clique Game](http://clique.darkshare.site.nfoservers.com/) and enter the hostname:ip address, +then your slot name. + +Enjoy. \ No newline at end of file From 21a3c747836642be85462eb057357f2d3722af87 Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Tue, 21 Mar 2023 16:26:13 -0400 Subject: [PATCH 088/172] SA2B: v2.1 Content Update (#1563) Changelog: Features: - New goal - Grand Prix - Complete all of the Kart Races to win! - New optional Location Checks - Omosanity (Activating Omochao) - Kart Race Mode - Ring Loss option - `Classic` - lose all rings on hit - `Modern` - lose 20 rings on hit - `OHKO` - instantly die on hit, regardless of ring count (shields still protect you) - New Trap - Pong Trap Quality of Life: - SA2B is now distributed as an `.apworld` - Maximum possible number of Emblems in item pool is increased from 180 to 250 - An indicator now shows on the Stage Select screen when `Cannon's Core` is available - Certain traps (`Exposition` and `Pong`) are now possible to receive on `Route 101` and `Route 280` - Certain traps (`Confusion`, `Chaos Control`, `Exposition` and `Pong`) are now possible to receive on `FinalHazard` Bug Fixes: - Actually swap Intermediate and Expert Chao Races correctly - Don't always grant double score for killing Gold Beetles anymore - Ensure upgrades are applied properly, even when received while dying - Fix the Message Queue getting disordered when receiving many messages in quick succession - Fix Logic errors - `City Escape - 3` (Hard Logic) now requires no upgrades - `Mission Street - Pipe 2` (Hard Logic) now requires no upgrades - `Crazy Gadget - Pipe 3` (Hard Logic) now requires no upgrades - `Egg Quarters - 3` (Hard Logic) now requires only `Rouge - Mystic Melody` - `Mad Space - 5` (Hard Logic) now requires no upgrades Co-authored-by: RaspberrySpaceJam --- setup.py | 1 + worlds/sa2b/Items.py | 2 + worlds/sa2b/Locations.py | 440 +++++++++++++++--- worlds/sa2b/Missions.py | 214 +++++---- worlds/sa2b/Names/ItemName.py | 1 + worlds/sa2b/Names/LocationName.py | 223 +++++++++ worlds/sa2b/Options.py | 67 +++ worlds/sa2b/Regions.py | 274 +++++++++++ worlds/sa2b/Rules.py | 316 ++++++++++++- worlds/sa2b/__init__.py | 73 ++- .../sa2b/docs/en_Sonic Adventure 2 Battle.md | 4 +- 11 files changed, 1394 insertions(+), 221 deletions(-) diff --git a/setup.py b/setup.py index 4e54fab518..66cbb722f9 100644 --- a/setup.py +++ b/setup.py @@ -49,6 +49,7 @@ apworlds: set = { "Subnautica", "Factorio", "Rogue Legacy", + "Sonic Adventure 2 Battle", "Donkey Kong Country 3", "Super Mario World", "Stardew Valley", diff --git a/worlds/sa2b/Items.py b/worlds/sa2b/Items.py index 904a854cfa..28f71fca7a 100644 --- a/worlds/sa2b/Items.py +++ b/worlds/sa2b/Items.py @@ -79,6 +79,8 @@ trap_table = { ItemName.gravity_trap: ItemData(0xFF0034, False, True), ItemName.exposition_trap: ItemData(0xFF0035, False, True), #ItemName.darkness_trap: ItemData(0xFF0036, False, True), + + ItemName.pong_trap: ItemData(0xFF0050, False, True), } emeralds_table = { diff --git a/worlds/sa2b/Locations.py b/worlds/sa2b/Locations.py index f5831cd7ee..82fe3aa5be 100644 --- a/worlds/sa2b/Locations.py +++ b/worlds/sa2b/Locations.py @@ -504,6 +504,235 @@ beetle_location_table = { LocationName.cannon_core_beetle: 0xFF061E, } +omochao_location_table = { + LocationName.city_escape_omo_1: 0xFF0800, + LocationName.wild_canyon_omo_1: 0xFF0801, + LocationName.prison_lane_omo_1: 0xFF0802, + LocationName.metal_harbor_omo_1: 0xFF0803, + LocationName.pumpkin_hill_omo_1: 0xFF0805, + LocationName.mission_street_omo_1: 0xFF0806, + LocationName.aquatic_mine_omo_1: 0xFF0807, + LocationName.hidden_base_omo_1: 0xFF0809, + LocationName.pyramid_cave_omo_1: 0xFF080A, + LocationName.death_chamber_omo_1: 0xFF080B, + LocationName.eternal_engine_omo_1: 0xFF080C, + LocationName.meteor_herd_omo_1: 0xFF080D, + LocationName.crazy_gadget_omo_1: 0xFF080E, + LocationName.final_rush_omo_1: 0xFF080F, + + LocationName.iron_gate_omo_1: 0xFF0810, + LocationName.dry_lagoon_omo_1: 0xFF0811, + LocationName.sand_ocean_omo_1: 0xFF0812, + LocationName.radical_highway_omo_1: 0xFF0813, + LocationName.egg_quarters_omo_1: 0xFF0814, + LocationName.lost_colony_omo_1: 0xFF0815, + LocationName.weapons_bed_omo_1: 0xFF0816, + LocationName.security_hall_omo_1: 0xFF0817, + LocationName.white_jungle_omo_1: 0xFF0818, + LocationName.mad_space_omo_1: 0xFF081B, + LocationName.cosmic_wall_omo_1: 0xFF081C, + LocationName.final_chase_omo_1: 0xFF081D, + + LocationName.cannon_core_omo_1: 0xFF081E, + + LocationName.city_escape_omo_2: 0xFF0820, + LocationName.wild_canyon_omo_2: 0xFF0821, + LocationName.prison_lane_omo_2: 0xFF0822, + LocationName.metal_harbor_omo_2: 0xFF0823, + LocationName.pumpkin_hill_omo_2: 0xFF0825, + LocationName.mission_street_omo_2: 0xFF0826, + LocationName.aquatic_mine_omo_2: 0xFF0827, + LocationName.hidden_base_omo_2: 0xFF0829, + LocationName.pyramid_cave_omo_2: 0xFF082A, + LocationName.death_chamber_omo_2: 0xFF082B, + LocationName.eternal_engine_omo_2: 0xFF082C, + LocationName.meteor_herd_omo_2: 0xFF082D, + LocationName.crazy_gadget_omo_2: 0xFF082E, + LocationName.final_rush_omo_2: 0xFF082F, + + LocationName.iron_gate_omo_2: 0xFF0830, + LocationName.dry_lagoon_omo_2: 0xFF0831, + LocationName.sand_ocean_omo_2: 0xFF0832, + LocationName.radical_highway_omo_2: 0xFF0833, + LocationName.egg_quarters_omo_2: 0xFF0834, + LocationName.lost_colony_omo_2: 0xFF0835, + LocationName.weapons_bed_omo_2: 0xFF0836, + LocationName.security_hall_omo_2: 0xFF0837, + LocationName.white_jungle_omo_2: 0xFF0838, + LocationName.mad_space_omo_2: 0xFF083B, + + LocationName.cannon_core_omo_2: 0xFF083E, + + LocationName.city_escape_omo_3: 0xFF0840, + LocationName.wild_canyon_omo_3: 0xFF0841, + LocationName.prison_lane_omo_3: 0xFF0842, + LocationName.metal_harbor_omo_3: 0xFF0843, + LocationName.pumpkin_hill_omo_3: 0xFF0845, + LocationName.mission_street_omo_3: 0xFF0846, + LocationName.aquatic_mine_omo_3: 0xFF0847, + LocationName.hidden_base_omo_3: 0xFF0849, + LocationName.pyramid_cave_omo_3: 0xFF084A, + LocationName.death_chamber_omo_3: 0xFF084B, + LocationName.eternal_engine_omo_3: 0xFF084C, + LocationName.meteor_herd_omo_3: 0xFF084D, + LocationName.crazy_gadget_omo_3: 0xFF084E, + LocationName.final_rush_omo_3: 0xFF084F, + + LocationName.iron_gate_omo_3: 0xFF0850, + LocationName.dry_lagoon_omo_3: 0xFF0851, + LocationName.radical_highway_omo_3: 0xFF0853, + LocationName.egg_quarters_omo_3: 0xFF0854, + LocationName.lost_colony_omo_3: 0xFF0855, + LocationName.weapons_bed_omo_3: 0xFF0856, + LocationName.security_hall_omo_3: 0xFF0857, + LocationName.white_jungle_omo_3: 0xFF0858, + LocationName.mad_space_omo_3: 0xFF085B, + + LocationName.cannon_core_omo_3: 0xFF085E, + + LocationName.city_escape_omo_4: 0xFF0860, + LocationName.wild_canyon_omo_4: 0xFF0861, + LocationName.prison_lane_omo_4: 0xFF0862, + LocationName.metal_harbor_omo_4: 0xFF0863, + LocationName.pumpkin_hill_omo_4: 0xFF0865, + LocationName.mission_street_omo_4: 0xFF0866, + LocationName.aquatic_mine_omo_4: 0xFF0867, + LocationName.hidden_base_omo_4: 0xFF0869, + LocationName.pyramid_cave_omo_4: 0xFF086A, + LocationName.death_chamber_omo_4: 0xFF086B, + LocationName.eternal_engine_omo_4: 0xFF086C, + LocationName.crazy_gadget_omo_4: 0xFF086E, + + LocationName.iron_gate_omo_4: 0xFF0870, + LocationName.dry_lagoon_omo_4: 0xFF0871, + LocationName.radical_highway_omo_4: 0xFF0873, + LocationName.egg_quarters_omo_4: 0xFF0874, + LocationName.lost_colony_omo_4: 0xFF0875, + LocationName.security_hall_omo_4: 0xFF0877, + LocationName.white_jungle_omo_4: 0xFF0878, + LocationName.mad_space_omo_4: 0xFF087B, + + LocationName.cannon_core_omo_4: 0xFF087E, + + LocationName.city_escape_omo_5: 0xFF0880, + LocationName.wild_canyon_omo_5: 0xFF0881, + LocationName.prison_lane_omo_5: 0xFF0882, + LocationName.metal_harbor_omo_5: 0xFF0883, + LocationName.pumpkin_hill_omo_5: 0xFF0885, + LocationName.mission_street_omo_5: 0xFF0886, + LocationName.aquatic_mine_omo_5: 0xFF0887, + LocationName.death_chamber_omo_5: 0xFF088B, + LocationName.eternal_engine_omo_5: 0xFF088C, + LocationName.crazy_gadget_omo_5: 0xFF088E, + + LocationName.iron_gate_omo_5: 0xFF0890, + LocationName.dry_lagoon_omo_5: 0xFF0891, + LocationName.radical_highway_omo_5: 0xFF0893, + LocationName.egg_quarters_omo_5: 0xFF0894, + LocationName.lost_colony_omo_5: 0xFF0895, + LocationName.security_hall_omo_5: 0xFF0897, + LocationName.white_jungle_omo_5: 0xFF0898, + LocationName.mad_space_omo_5: 0xFF089B, + + LocationName.cannon_core_omo_5: 0xFF089E, + + LocationName.city_escape_omo_6: 0xFF08A0, + LocationName.wild_canyon_omo_6: 0xFF08A1, + LocationName.prison_lane_omo_6: 0xFF08A2, + LocationName.pumpkin_hill_omo_6: 0xFF08A5, + LocationName.mission_street_omo_6: 0xFF08A6, + LocationName.aquatic_mine_omo_6: 0xFF08A7, + LocationName.death_chamber_omo_6: 0xFF08AB, + LocationName.eternal_engine_omo_6: 0xFF08AC, + LocationName.crazy_gadget_omo_6: 0xFF08AE, + + LocationName.iron_gate_omo_6: 0xFF08B0, + LocationName.dry_lagoon_omo_6: 0xFF08B1, + LocationName.radical_highway_omo_6: 0xFF08B3, + LocationName.egg_quarters_omo_6: 0xFF08B4, + LocationName.lost_colony_omo_6: 0xFF08B5, + LocationName.security_hall_omo_6: 0xFF08B7, + + LocationName.cannon_core_omo_6: 0xFF08BE, + + LocationName.city_escape_omo_7: 0xFF08C0, + LocationName.wild_canyon_omo_7: 0xFF08C1, + LocationName.prison_lane_omo_7: 0xFF08C2, + LocationName.pumpkin_hill_omo_7: 0xFF08C5, + LocationName.mission_street_omo_7: 0xFF08C6, + LocationName.aquatic_mine_omo_7: 0xFF08C7, + LocationName.death_chamber_omo_7: 0xFF08CB, + LocationName.eternal_engine_omo_7: 0xFF08CC, + LocationName.crazy_gadget_omo_7: 0xFF08CE, + + LocationName.dry_lagoon_omo_7: 0xFF08D1, + LocationName.radical_highway_omo_7: 0xFF08D3, + LocationName.egg_quarters_omo_7: 0xFF08D4, + LocationName.lost_colony_omo_7: 0xFF08D5, + LocationName.security_hall_omo_7: 0xFF08D7, + + LocationName.cannon_core_omo_7: 0xFF08DE, + + LocationName.city_escape_omo_8: 0xFF08E0, + LocationName.wild_canyon_omo_8: 0xFF08E1, + LocationName.prison_lane_omo_8: 0xFF08E2, + LocationName.pumpkin_hill_omo_8: 0xFF08E5, + LocationName.mission_street_omo_8: 0xFF08E6, + LocationName.death_chamber_omo_8: 0xFF08EB, + LocationName.eternal_engine_omo_8: 0xFF08EC, + LocationName.crazy_gadget_omo_8: 0xFF08EE, + + LocationName.dry_lagoon_omo_8: 0xFF08F1, + LocationName.radical_highway_omo_8: 0xFF08F3, + LocationName.lost_colony_omo_8: 0xFF08F5, + LocationName.security_hall_omo_8: 0xFF08F7, + + LocationName.cannon_core_omo_8: 0xFF08FE, + + LocationName.city_escape_omo_9: 0xFF0900, + LocationName.wild_canyon_omo_9: 0xFF0901, + LocationName.prison_lane_omo_9: 0xFF0902, + LocationName.pumpkin_hill_omo_9: 0xFF0905, + LocationName.death_chamber_omo_9: 0xFF090B, + LocationName.eternal_engine_omo_9: 0xFF090C, + LocationName.crazy_gadget_omo_9: 0xFF090E, + + LocationName.dry_lagoon_omo_9: 0xFF0911, + LocationName.security_hall_omo_9: 0xFF0917, + + LocationName.cannon_core_omo_9: 0xFF091E, + + LocationName.city_escape_omo_10: 0xFF0920, + LocationName.wild_canyon_omo_10: 0xFF0921, + LocationName.prison_lane_omo_10: 0xFF0922, + LocationName.pumpkin_hill_omo_10: 0xFF0925, + LocationName.eternal_engine_omo_10: 0xFF092C, + LocationName.crazy_gadget_omo_10: 0xFF092E, + + LocationName.dry_lagoon_omo_10: 0xFF0931, + LocationName.security_hall_omo_10: 0xFF0937, + + LocationName.city_escape_omo_11: 0xFF0940, + LocationName.pumpkin_hill_omo_11: 0xFF0945, + LocationName.eternal_engine_omo_11: 0xFF094C, + LocationName.crazy_gadget_omo_11: 0xFF094E, + + LocationName.dry_lagoon_omo_11: 0xFF0951, + LocationName.security_hall_omo_11: 0xFF0957, + + LocationName.city_escape_omo_12: 0xFF0960, + LocationName.eternal_engine_omo_12: 0xFF096C, + LocationName.crazy_gadget_omo_12: 0xFF096E, + + LocationName.dry_lagoon_omo_12: 0xFF0971, + LocationName.security_hall_omo_12: 0xFF0977, + + LocationName.city_escape_omo_13: 0xFF0980, + LocationName.crazy_gadget_omo_13: 0xFF098E, + + LocationName.city_escape_omo_14: 0xFF09A0, +} + boss_gate_location_table = { LocationName.gate_1_boss: 0xFF0100, LocationName.gate_2_boss: 0xFF0101, @@ -530,6 +759,33 @@ chao_garden_beginner_location_table = { } chao_garden_intermediate_location_table = { + LocationName.chao_race_challenge_1: 0xFF022A, + LocationName.chao_race_challenge_2: 0xFF022B, + LocationName.chao_race_challenge_3: 0xFF022C, + LocationName.chao_race_challenge_4: 0xFF022D, + LocationName.chao_race_challenge_5: 0xFF022E, + LocationName.chao_race_challenge_6: 0xFF022F, + LocationName.chao_race_challenge_7: 0xFF0230, + LocationName.chao_race_challenge_8: 0xFF0231, + LocationName.chao_race_challenge_9: 0xFF0232, + LocationName.chao_race_challenge_10: 0xFF0233, + LocationName.chao_race_challenge_11: 0xFF0234, + LocationName.chao_race_challenge_12: 0xFF0235, + + LocationName.chao_race_hero_1: 0xFF0236, + LocationName.chao_race_hero_2: 0xFF0237, + LocationName.chao_race_hero_3: 0xFF0238, + LocationName.chao_race_hero_4: 0xFF0239, + + LocationName.chao_race_dark_1: 0xFF023A, + LocationName.chao_race_dark_2: 0xFF023B, + LocationName.chao_race_dark_3: 0xFF023C, + LocationName.chao_race_dark_4: 0xFF023D, + + LocationName.chao_standard_karate: 0xFF0301, +} + +chao_garden_expert_location_table = { LocationName.chao_race_aquamarine_1: 0xFF020C, LocationName.chao_race_aquamarine_2: 0xFF020D, LocationName.chao_race_aquamarine_3: 0xFF020E, @@ -561,37 +817,43 @@ chao_garden_intermediate_location_table = { LocationName.chao_race_diamond_4: 0xFF0228, LocationName.chao_race_diamond_5: 0xFF0229, - LocationName.chao_standard_karate: 0xFF0301, -} - -chao_garden_expert_location_table = { - LocationName.chao_race_challenge_1: 0xFF022A, - LocationName.chao_race_challenge_2: 0xFF022B, - LocationName.chao_race_challenge_3: 0xFF022C, - LocationName.chao_race_challenge_4: 0xFF022D, - LocationName.chao_race_challenge_5: 0xFF022E, - LocationName.chao_race_challenge_6: 0xFF022F, - LocationName.chao_race_challenge_7: 0xFF0230, - LocationName.chao_race_challenge_8: 0xFF0231, - LocationName.chao_race_challenge_9: 0xFF0232, - LocationName.chao_race_challenge_10: 0xFF0233, - LocationName.chao_race_challenge_11: 0xFF0234, - LocationName.chao_race_challenge_12: 0xFF0235, - - LocationName.chao_race_hero_1: 0xFF0236, - LocationName.chao_race_hero_2: 0xFF0237, - LocationName.chao_race_hero_3: 0xFF0238, - LocationName.chao_race_hero_4: 0xFF0239, - - LocationName.chao_race_dark_1: 0xFF023A, - LocationName.chao_race_dark_2: 0xFF023B, - LocationName.chao_race_dark_3: 0xFF023C, - LocationName.chao_race_dark_4: 0xFF023D, - LocationName.chao_expert_karate: 0xFF0302, LocationName.chao_super_karate: 0xFF0303, } +kart_race_beginner_location_table = { + LocationName.kart_race_beginner_sonic: 0xFF0A00, + LocationName.kart_race_beginner_tails: 0xFF0A01, + LocationName.kart_race_beginner_knuckles: 0xFF0A02, + LocationName.kart_race_beginner_shadow: 0xFF0A03, + LocationName.kart_race_beginner_eggman: 0xFF0A04, + LocationName.kart_race_beginner_rouge: 0xFF0A05, +} + +kart_race_standard_location_table = { + LocationName.kart_race_standard_sonic: 0xFF0A06, + LocationName.kart_race_standard_tails: 0xFF0A07, + LocationName.kart_race_standard_knuckles: 0xFF0A08, + LocationName.kart_race_standard_shadow: 0xFF0A09, + LocationName.kart_race_standard_eggman: 0xFF0A0A, + LocationName.kart_race_standard_rouge: 0xFF0A0B, +} + +kart_race_expert_location_table = { + LocationName.kart_race_expert_sonic: 0xFF0A0C, + LocationName.kart_race_expert_tails: 0xFF0A0D, + LocationName.kart_race_expert_knuckles: 0xFF0A0E, + LocationName.kart_race_expert_shadow: 0xFF0A0F, + LocationName.kart_race_expert_eggman: 0xFF0A10, + LocationName.kart_race_expert_rouge: 0xFF0A11, +} + +kart_race_mini_location_table = { + LocationName.kart_race_beginner: 0xFF0A12, + LocationName.kart_race_standard: 0xFF0A13, + LocationName.kart_race_expert: 0xFF0A14, +} + green_hill_location_table = { LocationName.green_hill: 0xFF001F, } @@ -605,6 +867,10 @@ final_boss_location_table = { LocationName.finalhazard: 0xFF005F, } +grand_prix_location_table = { + LocationName.grand_prix: 0xFF007F, +} + all_locations = { **mission_location_table, **upgrade_location_table, @@ -613,12 +879,18 @@ all_locations = { **pipe_location_table, **hidden_whistle_location_table, **beetle_location_table, + **omochao_location_table, **chao_garden_beginner_location_table, **chao_garden_intermediate_location_table, **chao_garden_expert_location_table, + **kart_race_beginner_location_table, + **kart_race_standard_location_table, + **kart_race_expert_location_table, + **kart_race_mini_location_table, **green_hill_location_table, **green_hill_chao_location_table, **final_boss_location_table, + **grand_prix_location_table, } boss_gate_set = [ @@ -665,62 +937,80 @@ def setup_locations(world: MultiWorld, player: int, mission_map: typing.Dict[int location_table = {} chao_location_table = {} + if world.goal[player] == 3: + if world.kart_race_checks[player] == 2: + location_table.update({**kart_race_beginner_location_table}) + location_table.update({**kart_race_standard_location_table}) + location_table.update({**kart_race_expert_location_table}) + elif world.kart_race_checks[player] == 1: + location_table.update({**kart_race_mini_location_table}) + location_table.update({**grand_prix_location_table}) + else: + for i in range(31): + mission_count = mission_count_map[i] + mission_order: typing.List[int] = mission_orders[mission_map[i]] + stage_prefix: str = stage_name_prefixes[i] - for i in range(31): - mission_count = mission_count_map[i] - mission_order: typing.List[int] = mission_orders[mission_map[i]] - stage_prefix: str = stage_name_prefixes[i] + for j in range(mission_count): + mission_number = mission_order[j] + location_name: str = stage_prefix + str(mission_number) + location_table[location_name] = mission_location_table[location_name] - for j in range(mission_count): - mission_number = mission_order[j] - location_name: str = stage_prefix + str(mission_number) - location_table[location_name] = mission_location_table[location_name] - - location_table.update({**upgrade_location_table}) - - if world.keysanity[player]: - location_table.update({**chao_key_location_table}) - - if world.whistlesanity[player].value == 1: - location_table.update({**pipe_location_table}) - elif world.whistlesanity[player].value == 2: - location_table.update({**hidden_whistle_location_table}) - elif world.whistlesanity[player].value == 3: - location_table.update({**pipe_location_table}) - location_table.update({**hidden_whistle_location_table}) - - if world.beetlesanity[player]: - location_table.update({**beetle_location_table}) - - if world.goal[player].value == 0 or world.goal[player].value == 2: - location_table.update({**final_boss_location_table}) - - if world.goal[player].value == 1 or world.goal[player].value == 2: - location_table.update({**green_hill_location_table}) + location_table.update({**upgrade_location_table}) if world.keysanity[player]: - location_table.update({**green_hill_chao_location_table}) + location_table.update({**chao_key_location_table}) - if world.chao_garden_difficulty[player].value >= 1: - chao_location_table.update({**chao_garden_beginner_location_table}) - if world.chao_garden_difficulty[player].value >= 2: - chao_location_table.update({**chao_garden_intermediate_location_table}) - if world.chao_garden_difficulty[player].value >= 3: - chao_location_table.update({**chao_garden_expert_location_table}) + if world.whistlesanity[player].value == 1: + location_table.update({**pipe_location_table}) + elif world.whistlesanity[player].value == 2: + location_table.update({**hidden_whistle_location_table}) + elif world.whistlesanity[player].value == 3: + location_table.update({**pipe_location_table}) + location_table.update({**hidden_whistle_location_table}) - for key, value in chao_location_table.items(): - if key in chao_karate_set: - if world.include_chao_karate[player]: + if world.beetlesanity[player]: + location_table.update({**beetle_location_table}) + + if world.omosanity[player]: + location_table.update({**omochao_location_table}) + + if world.kart_race_checks[player] == 2: + location_table.update({**kart_race_beginner_location_table}) + location_table.update({**kart_race_standard_location_table}) + location_table.update({**kart_race_expert_location_table}) + elif world.kart_race_checks[player] == 1: + location_table.update({**kart_race_mini_location_table}) + + if world.goal[player].value == 0 or world.goal[player].value == 2: + location_table.update({**final_boss_location_table}) + + if world.goal[player].value == 1 or world.goal[player].value == 2: + location_table.update({**green_hill_location_table}) + + if world.keysanity[player]: + location_table.update({**green_hill_chao_location_table}) + + if world.chao_garden_difficulty[player].value >= 1: + chao_location_table.update({**chao_garden_beginner_location_table}) + if world.chao_garden_difficulty[player].value >= 2: + chao_location_table.update({**chao_garden_intermediate_location_table}) + if world.chao_garden_difficulty[player].value >= 3: + chao_location_table.update({**chao_garden_expert_location_table}) + + for key, value in chao_location_table.items(): + if key in chao_karate_set: + if world.include_chao_karate[player]: + location_table[key] = value + elif key not in chao_race_prize_set: + if world.chao_race_checks[player] == "all": + location_table[key] = value + else: location_table[key] = value - elif key not in chao_race_prize_set: - if world.chao_race_checks[player] == "all": - location_table[key] = value - else: - location_table[key] = value - for x in range(len(boss_gate_set)): - if x < world.number_of_level_gates[player].value: - location_table[boss_gate_set[x]] = boss_gate_location_table[boss_gate_set[x]] + for x in range(len(boss_gate_set)): + if x < world.number_of_level_gates[player].value: + location_table[boss_gate_set[x]] = boss_gate_location_table[boss_gate_set[x]] return location_table diff --git a/worlds/sa2b/Missions.py b/worlds/sa2b/Missions.py index d9767586a6..fbff62c136 100644 --- a/worlds/sa2b/Missions.py +++ b/worlds/sa2b/Missions.py @@ -194,48 +194,52 @@ stage_name_prefixes: typing.List[str] = [ ] def get_mission_count_table(multiworld: MultiWorld, player: int): - speed_active_missions = 1 - mech_active_missions = 1 - hunt_active_missions = 1 - kart_active_missions = 1 - cannons_core_active_missions = 1 - - for i in range(2,6): - if getattr(multiworld, "speed_mission_" + str(i), None)[player]: - speed_active_missions += 1 - - if getattr(multiworld, "mech_mission_" + str(i), None)[player]: - mech_active_missions += 1 - - if getattr(multiworld, "hunt_mission_" + str(i), None)[player]: - hunt_active_missions += 1 - - if getattr(multiworld, "kart_mission_" + str(i), None)[player]: - kart_active_missions += 1 - - if getattr(multiworld, "cannons_core_mission_" + str(i), None)[player]: - cannons_core_active_missions += 1 - - speed_active_missions = min(speed_active_missions, multiworld.speed_mission_count[player].value) - mech_active_missions = min(mech_active_missions, multiworld.mech_mission_count[player].value) - hunt_active_missions = min(hunt_active_missions, multiworld.hunt_mission_count[player].value) - kart_active_missions = min(kart_active_missions, multiworld.kart_mission_count[player].value) - cannons_core_active_missions = min(cannons_core_active_missions, multiworld.cannons_core_mission_count[player].value) - - active_missions: typing.List[typing.List[int]] = [ - speed_active_missions, - mech_active_missions, - hunt_active_missions, - kart_active_missions, - cannons_core_active_missions - ] - mission_count_table: typing.Dict[int, int] = {} - for level in range(31): - level_style = level_styles[level] - level_mission_count = active_missions[level_style] - mission_count_table[level] = level_mission_count + if multiworld.goal[player] == 3: + for level in range(31): + mission_count_table[level] = 0 + else: + speed_active_missions = 1 + mech_active_missions = 1 + hunt_active_missions = 1 + kart_active_missions = 1 + cannons_core_active_missions = 1 + + for i in range(2,6): + if getattr(multiworld, "speed_mission_" + str(i), None)[player]: + speed_active_missions += 1 + + if getattr(multiworld, "mech_mission_" + str(i), None)[player]: + mech_active_missions += 1 + + if getattr(multiworld, "hunt_mission_" + str(i), None)[player]: + hunt_active_missions += 1 + + if getattr(multiworld, "kart_mission_" + str(i), None)[player]: + kart_active_missions += 1 + + if getattr(multiworld, "cannons_core_mission_" + str(i), None)[player]: + cannons_core_active_missions += 1 + + speed_active_missions = min(speed_active_missions, multiworld.speed_mission_count[player].value) + mech_active_missions = min(mech_active_missions, multiworld.mech_mission_count[player].value) + hunt_active_missions = min(hunt_active_missions, multiworld.hunt_mission_count[player].value) + kart_active_missions = min(kart_active_missions, multiworld.kart_mission_count[player].value) + cannons_core_active_missions = min(cannons_core_active_missions, multiworld.cannons_core_mission_count[player].value) + + active_missions: typing.List[typing.List[int]] = [ + speed_active_missions, + mech_active_missions, + hunt_active_missions, + kart_active_missions, + cannons_core_active_missions + ] + + for level in range(31): + level_style = level_styles[level] + level_mission_count = active_missions[level_style] + mission_count_table[level] = level_mission_count return mission_count_table @@ -243,73 +247,77 @@ def get_mission_count_table(multiworld: MultiWorld, player: int): def get_mission_table(multiworld: MultiWorld, player: int): mission_table: typing.Dict[int, int] = {} - speed_active_missions: typing.List[int] = [1] - mech_active_missions: typing.List[int] = [1] - hunt_active_missions: typing.List[int] = [1] - kart_active_missions: typing.List[int] = [1] - cannons_core_active_missions: typing.List[int] = [1] + if multiworld.goal[player] == 3: + for level in range(31): + mission_table[level] = 0 + else: + speed_active_missions: typing.List[int] = [1] + mech_active_missions: typing.List[int] = [1] + hunt_active_missions: typing.List[int] = [1] + kart_active_missions: typing.List[int] = [1] + cannons_core_active_missions: typing.List[int] = [1] - # Add included missions - for i in range(2,6): - if getattr(multiworld, "speed_mission_" + str(i), None)[player]: - speed_active_missions.append(i) - - if getattr(multiworld, "mech_mission_" + str(i), None)[player]: - mech_active_missions.append(i) - - if getattr(multiworld, "hunt_mission_" + str(i), None)[player]: - hunt_active_missions.append(i) - - if getattr(multiworld, "kart_mission_" + str(i), None)[player]: - kart_active_missions.append(i) - - if getattr(multiworld, "cannons_core_mission_" + str(i), None)[player]: - cannons_core_active_missions.append(i) - - active_missions: typing.List[typing.List[int]] = [ - speed_active_missions, - mech_active_missions, - hunt_active_missions, - kart_active_missions, - cannons_core_active_missions - ] - - for level in range(31): - level_style = level_styles[level] - - level_active_missions: typing.List[int] = copy.deepcopy(active_missions[level_style]) - level_chosen_missions: typing.List[int] = [] - - # The first mission must be M1, M2, or M4 - first_mission = 1 - - if multiworld.mission_shuffle[player]: - first_mission = multiworld.random.choice([mission for mission in level_active_missions if mission in [1, 2, 3, 4]]) - - level_active_missions.remove(first_mission) - - # Place Active Missions in the chosen mission list - for mission in level_active_missions: - if mission not in level_chosen_missions: - level_chosen_missions.append(mission) - - if multiworld.mission_shuffle[player]: - multiworld.random.shuffle(level_chosen_missions) - - level_chosen_missions.insert(0, first_mission) - - # Fill in the non-included missions + # Add included missions for i in range(2,6): - if i not in level_chosen_missions: - level_chosen_missions.append(i) + if getattr(multiworld, "speed_mission_" + str(i), None)[player]: + speed_active_missions.append(i) - # Determine which mission order index we have, for conveying to the mod - for i in range(len(mission_orders)): - if mission_orders[i] == level_chosen_missions: - level_mission_index = i - break + if getattr(multiworld, "mech_mission_" + str(i), None)[player]: + mech_active_missions.append(i) - mission_table[level] = level_mission_index + if getattr(multiworld, "hunt_mission_" + str(i), None)[player]: + hunt_active_missions.append(i) + + if getattr(multiworld, "kart_mission_" + str(i), None)[player]: + kart_active_missions.append(i) + + if getattr(multiworld, "cannons_core_mission_" + str(i), None)[player]: + cannons_core_active_missions.append(i) + + active_missions: typing.List[typing.List[int]] = [ + speed_active_missions, + mech_active_missions, + hunt_active_missions, + kart_active_missions, + cannons_core_active_missions + ] + + for level in range(31): + level_style = level_styles[level] + + level_active_missions: typing.List[int] = copy.deepcopy(active_missions[level_style]) + level_chosen_missions: typing.List[int] = [] + + # The first mission must be M1, M2, M3, or M4 + first_mission = 1 + + if multiworld.mission_shuffle[player]: + first_mission = multiworld.random.choice([mission for mission in level_active_missions if mission in [1, 2, 3, 4]]) + + level_active_missions.remove(first_mission) + + # Place Active Missions in the chosen mission list + for mission in level_active_missions: + if mission not in level_chosen_missions: + level_chosen_missions.append(mission) + + if multiworld.mission_shuffle[player]: + multiworld.random.shuffle(level_chosen_missions) + + level_chosen_missions.insert(0, first_mission) + + # Fill in the non-included missions + for i in range(2,6): + if i not in level_chosen_missions: + level_chosen_missions.append(i) + + # Determine which mission order index we have, for conveying to the mod + for i in range(len(mission_orders)): + if mission_orders[i] == level_chosen_missions: + level_mission_index = i + break + + mission_table[level] = level_mission_index return mission_table diff --git a/worlds/sa2b/Names/ItemName.py b/worlds/sa2b/Names/ItemName.py index 270b113383..7fa34ea722 100644 --- a/worlds/sa2b/Names/ItemName.py +++ b/worlds/sa2b/Names/ItemName.py @@ -51,6 +51,7 @@ tiny_trap = "Tiny Trap" gravity_trap = "Gravity Trap" exposition_trap = "Exposition Trap" darkness_trap = "Darkness Trap" +pong_trap = "Pong Trap" white_emerald = "White Chaos Emerald" red_emerald = "Red Chaos Emerald" diff --git a/worlds/sa2b/Names/LocationName.py b/worlds/sa2b/Names/LocationName.py index 34826a1ccd..9a970bda75 100644 --- a/worlds/sa2b/Names/LocationName.py +++ b/worlds/sa2b/Names/LocationName.py @@ -16,6 +16,20 @@ city_escape_hidden_2 = "City Escape - Hidden 2" city_escape_hidden_3 = "City Escape - Hidden 3" city_escape_hidden_4 = "City Escape - Hidden 4" city_escape_hidden_5 = "City Escape - Hidden 5" +city_escape_omo_1 = "City Escape - Omochao 1" +city_escape_omo_2 = "City Escape - Omochao 2" +city_escape_omo_3 = "City Escape - Omochao 3" +city_escape_omo_4 = "City Escape - Omochao 4" +city_escape_omo_5 = "City Escape - Omochao 5" +city_escape_omo_6 = "City Escape - Omochao 6" +city_escape_omo_7 = "City Escape - Omochao 7" +city_escape_omo_8 = "City Escape - Omochao 8" +city_escape_omo_9 = "City Escape - Omochao 9" +city_escape_omo_10 = "City Escape - Omochao 10" +city_escape_omo_11 = "City Escape - Omochao 11" +city_escape_omo_12 = "City Escape - Omochao 12" +city_escape_omo_13 = "City Escape - Omochao 13" +city_escape_omo_14 = "City Escape - Omochao 14" city_escape_beetle = "City Escape - Gold Beetle" city_escape_upgrade = "City Escape - Upgrade" metal_harbor_1 = "Metal Harbor - 1" @@ -27,6 +41,11 @@ metal_harbor_chao_1 = "Metal Harbor - Chao Key 1" metal_harbor_chao_2 = "Metal Harbor - Chao Key 2" metal_harbor_chao_3 = "Metal Harbor - Chao Key 3" metal_harbor_pipe_1 = "Metal Harbor - Pipe 1" +metal_harbor_omo_1 = "Metal Harbor - Omochao 1" +metal_harbor_omo_2 = "Metal Harbor - Omochao 2" +metal_harbor_omo_3 = "Metal Harbor - Omochao 3" +metal_harbor_omo_4 = "Metal Harbor - Omochao 4" +metal_harbor_omo_5 = "Metal Harbor - Omochao 5" metal_harbor_beetle = "Metal Harbor - Gold Beetle" metal_harbor_upgrade = "Metal Harbor - Upgrade" green_forest_1 = "Green Forest - 1" @@ -57,6 +76,10 @@ pyramid_cave_pipe_1 = "Pyramid Cave - Pipe 1" pyramid_cave_pipe_2 = "Pyramid Cave - Pipe 2" pyramid_cave_pipe_3 = "Pyramid Cave - Pipe 3" pyramid_cave_pipe_4 = "Pyramid Cave - Pipe 4" +pyramid_cave_omo_1 = "Pyramid Cave - Omochao 1" +pyramid_cave_omo_2 = "Pyramid Cave - Omochao 2" +pyramid_cave_omo_3 = "Pyramid Cave - Omochao 3" +pyramid_cave_omo_4 = "Pyramid Cave - Omochao 4" pyramid_cave_beetle = "Pyramid Cave - Gold Beetle" pyramid_cave_upgrade = "Pyramid Cave - Upgrade" crazy_gadget_1 = "Crazy Gadget - 1" @@ -72,6 +95,19 @@ crazy_gadget_pipe_2 = "Crazy Gadget - Pipe 2" crazy_gadget_pipe_3 = "Crazy Gadget - Pipe 3" crazy_gadget_pipe_4 = "Crazy Gadget - Pipe 4" crazy_gadget_hidden_1 = "Crazy Gadget - Hidden 1" +crazy_gadget_omo_1 = "Crazy Gadget - Omochao 1" +crazy_gadget_omo_2 = "Crazy Gadget - Omochao 2" +crazy_gadget_omo_3 = "Crazy Gadget - Omochao 3" +crazy_gadget_omo_4 = "Crazy Gadget - Omochao 4" +crazy_gadget_omo_5 = "Crazy Gadget - Omochao 5" +crazy_gadget_omo_6 = "Crazy Gadget - Omochao 6" +crazy_gadget_omo_7 = "Crazy Gadget - Omochao 7" +crazy_gadget_omo_8 = "Crazy Gadget - Omochao 8" +crazy_gadget_omo_9 = "Crazy Gadget - Omochao 9" +crazy_gadget_omo_10 = "Crazy Gadget - Omochao 10" +crazy_gadget_omo_11 = "Crazy Gadget - Omochao 11" +crazy_gadget_omo_12 = "Crazy Gadget - Omochao 12" +crazy_gadget_omo_13 = "Crazy Gadget - Omochao 13" crazy_gadget_beetle = "Crazy Gadget - Gold Beetle" crazy_gadget_upgrade = "Crazy Gadget - Upgrade" final_rush_1 = "Final Rush - 1" @@ -84,6 +120,9 @@ final_rush_chao_2 = "Final Rush - Chao Key 2" final_rush_chao_3 = "Final Rush - Chao Key 3" final_rush_pipe_1 = "Final Rush - Pipe 1" final_rush_pipe_2 = "Final Rush - Pipe 2" +final_rush_omo_1 = "Final Rush - Omochao 1" +final_rush_omo_2 = "Final Rush - Omochao 2" +final_rush_omo_3 = "Final Rush - Omochao 3" final_rush_beetle = "Final Rush - Gold Beetle" final_rush_upgrade = "Final Rush - Upgrade" @@ -102,6 +141,16 @@ prison_lane_pipe_3 = "Prison Lane - Pipe 3" prison_lane_hidden_1 = "Prison Lane - Hidden 1" prison_lane_hidden_2 = "Prison Lane - Hidden 2" prison_lane_hidden_3 = "Prison Lane - Hidden 3" +prison_lane_omo_1 = "Prison Lane - Omochao 1" +prison_lane_omo_2 = "Prison Lane - Omochao 2" +prison_lane_omo_3 = "Prison Lane - Omochao 3" +prison_lane_omo_4 = "Prison Lane - Omochao 4" +prison_lane_omo_5 = "Prison Lane - Omochao 5" +prison_lane_omo_6 = "Prison Lane - Omochao 6" +prison_lane_omo_7 = "Prison Lane - Omochao 7" +prison_lane_omo_8 = "Prison Lane - Omochao 8" +prison_lane_omo_9 = "Prison Lane - Omochao 9" +prison_lane_omo_10 = "Prison Lane - Omochao 10" prison_lane_beetle = "Prison Lane - Gold Beetle" prison_lane_upgrade = "Prison Lane - Upgrade" mission_street_1 = "Mission Street - 1" @@ -119,6 +168,14 @@ mission_street_hidden_1 = "Mission Street - Hidden 1" mission_street_hidden_2 = "Mission Street - Hidden 2" mission_street_hidden_3 = "Mission Street - Hidden 3" mission_street_hidden_4 = "Mission Street - Hidden 4" +mission_street_omo_1 = "Mission Street - Omochao 1" +mission_street_omo_2 = "Mission Street - Omochao 2" +mission_street_omo_3 = "Mission Street - Omochao 3" +mission_street_omo_4 = "Mission Street - Omochao 4" +mission_street_omo_5 = "Mission Street - Omochao 5" +mission_street_omo_6 = "Mission Street - Omochao 6" +mission_street_omo_7 = "Mission Street - Omochao 7" +mission_street_omo_8 = "Mission Street - Omochao 8" mission_street_beetle = "Mission Street - Gold Beetle" mission_street_upgrade = "Mission Street - Upgrade" route_101_1 = "Route 101 - 1" @@ -138,6 +195,10 @@ hidden_base_pipe_2 = "Hidden Base - Pipe 2" hidden_base_pipe_3 = "Hidden Base - Pipe 3" hidden_base_pipe_4 = "Hidden Base - Pipe 4" hidden_base_pipe_5 = "Hidden Base - Pipe 5" +hidden_base_omo_1 = "Hidden Base - Omochao 1" +hidden_base_omo_2 = "Hidden Base - Omochao 2" +hidden_base_omo_3 = "Hidden Base - Omochao 3" +hidden_base_omo_4 = "Hidden Base - Omochao 4" hidden_base_beetle = "Hidden Base - Gold Beetle" hidden_base_upgrade = "Hidden Base - Upgrade" eternal_engine_1 = "Eternal Engine - 1" @@ -153,6 +214,18 @@ eternal_engine_pipe_2 = "Eternal Engine - Pipe 2" eternal_engine_pipe_3 = "Eternal Engine - Pipe 3" eternal_engine_pipe_4 = "Eternal Engine - Pipe 4" eternal_engine_pipe_5 = "Eternal Engine - Pipe 5" +eternal_engine_omo_1 = "Eternal Engine - Omochao 1" +eternal_engine_omo_2 = "Eternal Engine - Omochao 2" +eternal_engine_omo_3 = "Eternal Engine - Omochao 3" +eternal_engine_omo_4 = "Eternal Engine - Omochao 4" +eternal_engine_omo_5 = "Eternal Engine - Omochao 5" +eternal_engine_omo_6 = "Eternal Engine - Omochao 6" +eternal_engine_omo_7 = "Eternal Engine - Omochao 7" +eternal_engine_omo_8 = "Eternal Engine - Omochao 8" +eternal_engine_omo_9 = "Eternal Engine - Omochao 9" +eternal_engine_omo_10 = "Eternal Engine - Omochao 10" +eternal_engine_omo_11 = "Eternal Engine - Omochao 11" +eternal_engine_omo_12 = "Eternal Engine - Omochao 12" eternal_engine_beetle = "Eternal Engine - Gold Beetle" eternal_engine_upgrade = "Eternal Engine - Upgrade" @@ -168,6 +241,16 @@ wild_canyon_chao_3 = "Wild Canyon - Chao Key 3" wild_canyon_pipe_1 = "Wild Canyon - Pipe 1" wild_canyon_pipe_2 = "Wild Canyon - Pipe 2" wild_canyon_pipe_3 = "Wild Canyon - Pipe 3" +wild_canyon_omo_1 = "Wild Canyon - Omochao 1" +wild_canyon_omo_2 = "Wild Canyon - Omochao 2" +wild_canyon_omo_3 = "Wild Canyon - Omochao 3" +wild_canyon_omo_4 = "Wild Canyon - Omochao 4" +wild_canyon_omo_5 = "Wild Canyon - Omochao 5" +wild_canyon_omo_6 = "Wild Canyon - Omochao 6" +wild_canyon_omo_7 = "Wild Canyon - Omochao 7" +wild_canyon_omo_8 = "Wild Canyon - Omochao 8" +wild_canyon_omo_9 = "Wild Canyon - Omochao 9" +wild_canyon_omo_10 = "Wild Canyon - Omochao 10" wild_canyon_beetle = "Wild Canyon - Gold Beetle" wild_canyon_upgrade = "Wild Canyon - Upgrade" pumpkin_hill_1 = "Pumpkin Hill - 1" @@ -180,6 +263,17 @@ pumpkin_hill_chao_2 = "Pumpkin Hill - Chao Key 2" pumpkin_hill_chao_3 = "Pumpkin Hill - Chao Key 3" pumpkin_hill_pipe_1 = "Pumpkin Hill - Pipe 1" pumpkin_hill_hidden_1 = "Pumpkin Hill - Hidden 1" +pumpkin_hill_omo_1 = "Pumpkin Hill - Omochao 1" +pumpkin_hill_omo_2 = "Pumpkin Hill - Omochao 2" +pumpkin_hill_omo_3 = "Pumpkin Hill - Omochao 3" +pumpkin_hill_omo_4 = "Pumpkin Hill - Omochao 4" +pumpkin_hill_omo_5 = "Pumpkin Hill - Omochao 5" +pumpkin_hill_omo_6 = "Pumpkin Hill - Omochao 6" +pumpkin_hill_omo_7 = "Pumpkin Hill - Omochao 7" +pumpkin_hill_omo_8 = "Pumpkin Hill - Omochao 8" +pumpkin_hill_omo_9 = "Pumpkin Hill - Omochao 9" +pumpkin_hill_omo_10 = "Pumpkin Hill - Omochao 10" +pumpkin_hill_omo_11 = "Pumpkin Hill - Omochao 11" pumpkin_hill_upgrade = "Pumpkin Hill - Upgrade" aquatic_mine_1 = "Aquatic Mine - 1" aquatic_mine_2 = "Aquatic Mine - 2" @@ -192,6 +286,13 @@ aquatic_mine_chao_3 = "Aquatic Mine - Chao Key 3" aquatic_mine_pipe_1 = "Aquatic Mine - Pipe 1" aquatic_mine_pipe_2 = "Aquatic Mine - Pipe 2" aquatic_mine_pipe_3 = "Aquatic Mine - Pipe 3" +aquatic_mine_omo_1 = "Aquatic Mine - Omochao 1" +aquatic_mine_omo_2 = "Aquatic Mine - Omochao 2" +aquatic_mine_omo_3 = "Aquatic Mine - Omochao 3" +aquatic_mine_omo_4 = "Aquatic Mine - Omochao 4" +aquatic_mine_omo_5 = "Aquatic Mine - Omochao 5" +aquatic_mine_omo_6 = "Aquatic Mine - Omochao 6" +aquatic_mine_omo_7 = "Aquatic Mine - Omochao 7" aquatic_mine_beetle = "Aquatic Mine - Gold Beetle" aquatic_mine_upgrade = "Aquatic Mine - Upgrade" death_chamber_1 = "Death Chamber - 1" @@ -207,6 +308,15 @@ death_chamber_pipe_2 = "Death Chamber - Pipe 2" death_chamber_pipe_3 = "Death Chamber - Pipe 3" death_chamber_hidden_1 = "Death Chamber - Hidden 1" death_chamber_hidden_2 = "Death Chamber - Hidden 2" +death_chamber_omo_1 = "Death Chamber - Omochao 1" +death_chamber_omo_2 = "Death Chamber - Omochao 2" +death_chamber_omo_3 = "Death Chamber - Omochao 3" +death_chamber_omo_4 = "Death Chamber - Omochao 4" +death_chamber_omo_5 = "Death Chamber - Omochao 5" +death_chamber_omo_6 = "Death Chamber - Omochao 6" +death_chamber_omo_7 = "Death Chamber - Omochao 7" +death_chamber_omo_8 = "Death Chamber - Omochao 8" +death_chamber_omo_9 = "Death Chamber - Omochao 9" death_chamber_beetle = "Death Chamber - Gold Beetle" death_chamber_upgrade = "Death Chamber - Upgrade" meteor_herd_1 = "Meteor Herd - 1" @@ -220,6 +330,9 @@ meteor_herd_chao_3 = "Meteor Herd - Chao Key 3" meteor_herd_pipe_1 = "Meteor Herd - Pipe 1" meteor_herd_pipe_2 = "Meteor Herd - Pipe 2" meteor_herd_pipe_3 = "Meteor Herd - Pipe 3" +meteor_herd_omo_1 = "Meteor Herd - Omochao 1" +meteor_herd_omo_2 = "Meteor Herd - Omochao 2" +meteor_herd_omo_3 = "Meteor Herd - Omochao 3" meteor_herd_beetle = "Meteor Herd - Gold Beetle" meteor_herd_upgrade = "Meteor Herd - Upgrade" @@ -239,6 +352,14 @@ radical_highway_pipe_3 = "Radical Highway - Pipe 3" radical_highway_hidden_1 = "Radical Highway - Hidden 1" radical_highway_hidden_2 = "Radical Highway - Hidden 2" radical_highway_hidden_3 = "Radical Highway - Hidden 3" +radical_highway_omo_1 = "Radical Highway - Omochao 1" +radical_highway_omo_2 = "Radical Highway - Omochao 2" +radical_highway_omo_3 = "Radical Highway - Omochao 3" +radical_highway_omo_4 = "Radical Highway - Omochao 4" +radical_highway_omo_5 = "Radical Highway - Omochao 5" +radical_highway_omo_6 = "Radical Highway - Omochao 6" +radical_highway_omo_7 = "Radical Highway - Omochao 7" +radical_highway_omo_8 = "Radical Highway - Omochao 8" radical_highway_beetle = "Radical Highway - Gold Beetle" radical_highway_upgrade = "Radical Highway - Upgrade" white_jungle_1 = "White Jungle - 1" @@ -256,6 +377,11 @@ white_jungle_pipe_4 = "White Jungle - Pipe 4" white_jungle_hidden_1 = "White Jungle - Hidden 1" white_jungle_hidden_2 = "White Jungle - Hidden 2" white_jungle_hidden_3 = "White Jungle - Hidden 3" +white_jungle_omo_1 = "White Jungle - Omochao 1" +white_jungle_omo_2 = "White Jungle - Omochao 2" +white_jungle_omo_3 = "White Jungle - Omochao 3" +white_jungle_omo_4 = "White Jungle - Omochao 4" +white_jungle_omo_5 = "White Jungle - Omochao 5" white_jungle_beetle = "White Jungle - Gold Beetle" white_jungle_upgrade = "White Jungle - Upgrade" sky_rail_1 = "Sky Rail - 1" @@ -285,6 +411,7 @@ final_chase_chao_3 = "Final Chase - Chao Key 3" final_chase_pipe_1 = "Final Chase - Pipe 1" final_chase_pipe_2 = "Final Chase - Pipe 2" final_chase_pipe_3 = "Final Chase - Pipe 3" +final_chase_omo_1 = "Final Chase - Omochao 1" final_chase_beetle = "Final Chase - Gold Beetle" final_chase_upgrade = "Final Chase - Upgrade" @@ -302,6 +429,12 @@ iron_gate_pipe_2 = "Iron Gate - Pipe 2" iron_gate_pipe_3 = "Iron Gate - Pipe 3" iron_gate_pipe_4 = "Iron Gate - Pipe 4" iron_gate_pipe_5 = "Iron Gate - Pipe 5" +iron_gate_omo_1 = "Iron Gate - Omochao 1" +iron_gate_omo_2 = "Iron Gate - Omochao 2" +iron_gate_omo_3 = "Iron Gate - Omochao 3" +iron_gate_omo_4 = "Iron Gate - Omochao 4" +iron_gate_omo_5 = "Iron Gate - Omochao 5" +iron_gate_omo_6 = "Iron Gate - Omochao 6" iron_gate_beetle = "Iron Gate - Gold Beetle" iron_gate_upgrade = "Iron Gate - Upgrade" sand_ocean_1 = "Sand Ocean - 1" @@ -317,6 +450,8 @@ sand_ocean_pipe_2 = "Sand Ocean - Pipe 2" sand_ocean_pipe_3 = "Sand Ocean - Pipe 3" sand_ocean_pipe_4 = "Sand Ocean - Pipe 4" sand_ocean_pipe_5 = "Sand Ocean - Pipe 5" +sand_ocean_omo_1 = "Sand Ocean - Omochao 1" +sand_ocean_omo_2 = "Sand Ocean - Omochao 2" sand_ocean_beetle = "Sand Ocean - Gold Beetle" sand_ocean_upgrade = "Sand Ocean - Upgrade" lost_colony_1 = "Lost Colony - 1" @@ -330,6 +465,14 @@ lost_colony_chao_3 = "Lost Colony - Chao Key 3" lost_colony_pipe_1 = "Lost Colony - Pipe 1" lost_colony_pipe_2 = "Lost Colony - Pipe 2" lost_colony_hidden_1 = "Lost Colony - Hidden 1" +lost_colony_omo_1 = "Lost Colony - Omochao 1" +lost_colony_omo_2 = "Lost Colony - Omochao 2" +lost_colony_omo_3 = "Lost Colony - Omochao 3" +lost_colony_omo_4 = "Lost Colony - Omochao 4" +lost_colony_omo_5 = "Lost Colony - Omochao 5" +lost_colony_omo_6 = "Lost Colony - Omochao 6" +lost_colony_omo_7 = "Lost Colony - Omochao 7" +lost_colony_omo_8 = "Lost Colony - Omochao 8" lost_colony_beetle = "Lost Colony - Gold Beetle" lost_colony_upgrade = "Lost Colony - Upgrade" weapons_bed_1 = "Weapons Bed - 1" @@ -345,6 +488,9 @@ weapons_bed_pipe_2 = "Weapons Bed - Pipe 2" weapons_bed_pipe_3 = "Weapons Bed - Pipe 3" weapons_bed_pipe_4 = "Weapons Bed - Pipe 4" weapons_bed_pipe_5 = "Weapons Bed - Pipe 5" +weapons_bed_omo_1 = "Weapons Bed - Omochao 1" +weapons_bed_omo_2 = "Weapons Bed - Omochao 2" +weapons_bed_omo_3 = "Weapons Bed - Omochao 3" weapons_bed_upgrade = "Weapons Bed - Upgrade" cosmic_wall_1 = "Cosmic Wall - 1" cosmic_wall_2 = "Cosmic Wall - 2" @@ -359,6 +505,7 @@ cosmic_wall_pipe_2 = "Cosmic Wall - Pipe 2" cosmic_wall_pipe_3 = "Cosmic Wall - Pipe 3" cosmic_wall_pipe_4 = "Cosmic Wall - Pipe 4" cosmic_wall_pipe_5 = "Cosmic Wall - Pipe 5" +cosmic_wall_omo_1 = "Cosmic Wall - Omochao 1" cosmic_wall_beetle = "Cosmic Wall - Gold Beetle" cosmic_wall_upgrade = "Cosmic Wall - Upgrade" @@ -373,6 +520,18 @@ dry_lagoon_chao_2 = "Dry Lagoon - Chao Key 2" dry_lagoon_chao_3 = "Dry Lagoon - Chao Key 3" dry_lagoon_pipe_1 = "Dry Lagoon - Pipe 1" dry_lagoon_hidden_1 = "Dry Lagoon - Hidden 1" +dry_lagoon_omo_1 = "Dry Lagoon - Omochao 1" +dry_lagoon_omo_2 = "Dry Lagoon - Omochao 2" +dry_lagoon_omo_3 = "Dry Lagoon - Omochao 3" +dry_lagoon_omo_4 = "Dry Lagoon - Omochao 4" +dry_lagoon_omo_5 = "Dry Lagoon - Omochao 5" +dry_lagoon_omo_6 = "Dry Lagoon - Omochao 6" +dry_lagoon_omo_7 = "Dry Lagoon - Omochao 7" +dry_lagoon_omo_8 = "Dry Lagoon - Omochao 8" +dry_lagoon_omo_9 = "Dry Lagoon - Omochao 9" +dry_lagoon_omo_10 = "Dry Lagoon - Omochao 10" +dry_lagoon_omo_11 = "Dry Lagoon - Omochao 11" +dry_lagoon_omo_12 = "Dry Lagoon - Omochao 12" dry_lagoon_beetle = "Dry Lagoon - Gold Beetle" dry_lagoon_upgrade = "Dry Lagoon - Upgrade" egg_quarters_1 = "Egg Quarters - 1" @@ -387,6 +546,13 @@ egg_quarters_pipe_1 = "Egg Quarters - Pipe 1" egg_quarters_pipe_2 = "Egg Quarters - Pipe 2" egg_quarters_hidden_1 = "Egg Quarters - Hidden 1" egg_quarters_hidden_2 = "Egg Quarters - Hidden 2" +egg_quarters_omo_1 = "Egg Quarters - Omochao 1" +egg_quarters_omo_2 = "Egg Quarters - Omochao 2" +egg_quarters_omo_3 = "Egg Quarters - Omochao 3" +egg_quarters_omo_4 = "Egg Quarters - Omochao 4" +egg_quarters_omo_5 = "Egg Quarters - Omochao 5" +egg_quarters_omo_6 = "Egg Quarters - Omochao 6" +egg_quarters_omo_7 = "Egg Quarters - Omochao 7" egg_quarters_beetle = "Egg Quarters - Gold Beetle" egg_quarters_upgrade = "Egg Quarters - Upgrade" security_hall_1 = "Security Hall - 1" @@ -399,6 +565,18 @@ security_hall_chao_2 = "Security Hall - Chao Key 2" security_hall_chao_3 = "Security Hall - Chao Key 3" security_hall_pipe_1 = "Security Hall - Pipe 1" security_hall_hidden_1 = "Security Hall - Hidden 1" +security_hall_omo_1 = "Security Hall - Omochao 1" +security_hall_omo_2 = "Security Hall - Omochao 2" +security_hall_omo_3 = "Security Hall - Omochao 3" +security_hall_omo_4 = "Security Hall - Omochao 4" +security_hall_omo_5 = "Security Hall - Omochao 5" +security_hall_omo_6 = "Security Hall - Omochao 6" +security_hall_omo_7 = "Security Hall - Omochao 7" +security_hall_omo_8 = "Security Hall - Omochao 8" +security_hall_omo_9 = "Security Hall - Omochao 9" +security_hall_omo_10 = "Security Hall - Omochao 10" +security_hall_omo_11 = "Security Hall - Omochao 11" +security_hall_omo_12 = "Security Hall - Omochao 12" security_hall_beetle = "Security Hall - Gold Beetle" security_hall_upgrade = "Security Hall - Upgrade" route_280_1 = "Route 280 - 1" @@ -418,6 +596,11 @@ mad_space_pipe_1 = "Mad Space - Pipe 1" mad_space_pipe_2 = "Mad Space - Pipe 2" mad_space_pipe_3 = "Mad Space - Pipe 3" mad_space_pipe_4 = "Mad Space - Pipe 4" +mad_space_omo_1 = "Mad Space - Omochao 1" +mad_space_omo_2 = "Mad Space - Omochao 2" +mad_space_omo_3 = "Mad Space - Omochao 3" +mad_space_omo_4 = "Mad Space - Omochao 4" +mad_space_omo_5 = "Mad Space - Omochao 5" mad_space_beetle = "Mad Space - Gold Beetle" mad_space_upgrade = "Mad Space - Upgrade" @@ -435,6 +618,15 @@ cannon_core_pipe_2 = "Cannon Core - Pipe 2" cannon_core_pipe_3 = "Cannon Core - Pipe 3" cannon_core_pipe_4 = "Cannon Core - Pipe 4" cannon_core_pipe_5 = "Cannon Core - Pipe 5" +cannon_core_omo_1 = "Cannon Core - Omochao 1" +cannon_core_omo_2 = "Cannon Core - Omochao 2" +cannon_core_omo_3 = "Cannon Core - Omochao 3" +cannon_core_omo_4 = "Cannon Core - Omochao 4" +cannon_core_omo_5 = "Cannon Core - Omochao 5" +cannon_core_omo_6 = "Cannon Core - Omochao 6" +cannon_core_omo_7 = "Cannon Core - Omochao 7" +cannon_core_omo_8 = "Cannon Core - Omochao 8" +cannon_core_omo_9 = "Cannon Core - Omochao 9" cannon_core_hidden_1 = "Cannon Core - Hidden 1" cannon_core_beetle = "Cannon Core - Gold Beetle" @@ -518,6 +710,30 @@ chao_standard_karate = "Chao Karate - Standard" chao_expert_karate = "Chao Karate - Expert" chao_super_karate = "Chao Karate - Super" +# Kart Race Definitions +kart_race_beginner_sonic = "Kart Race - Beginner - Sonic" +kart_race_beginner_tails = "Kart Race - Beginner - Tails" +kart_race_beginner_knuckles = "Kart Race - Beginner - Knuckles" +kart_race_beginner_shadow = "Kart Race - Beginner - Shadow" +kart_race_beginner_eggman = "Kart Race - Beginner - Eggman" +kart_race_beginner_rouge = "Kart Race - Beginner - Rouge" +kart_race_standard_sonic = "Kart Race - Standard - Sonic" +kart_race_standard_tails = "Kart Race - Standard - Tails" +kart_race_standard_knuckles = "Kart Race - Standard - Knuckles" +kart_race_standard_shadow = "Kart Race - Standard - Shadow" +kart_race_standard_eggman = "Kart Race - Standard - Eggman" +kart_race_standard_rouge = "Kart Race - Standard - Rouge" +kart_race_expert_sonic = "Kart Race - Expert - Sonic" +kart_race_expert_tails = "Kart Race - Expert - Tails" +kart_race_expert_knuckles = "Kart Race - Expert - Knuckles" +kart_race_expert_shadow = "Kart Race - Expert - Shadow" +kart_race_expert_eggman = "Kart Race - Expert - Eggman" +kart_race_expert_rouge = "Kart Race - Expert - Rouge" + +kart_race_beginner = "Kart Race - Beginner" +kart_race_standard = "Kart Race - Standard" +kart_race_expert = "Kart Race - Expert" + # Other Definitions green_hill = "Green Hill" green_hill_chao_1 = "Green Hill - Chao Key 1" @@ -582,6 +798,13 @@ biolizard_region = "Biolizard" green_hill_region = "Green Hill" +grand_prix = "Grand Prix" +grand_prix_region = "Grand Prix" + chao_garden_beginner_region = "Chao Garden - Beginner" chao_garden_intermediate_region = "Chao Garden - Intermediate" chao_garden_expert_region = "Chao Garden - Expert" + +kart_race_beginner_region = "Kart Race - Beginner" +kart_race_standard_region = "Kart Race - Intermediate" +kart_race_expert_region = "Kart Race - Expert" diff --git a/worlds/sa2b/Options.py b/worlds/sa2b/Options.py index 4724d30a5f..0a9f2d1622 100644 --- a/worlds/sa2b/Options.py +++ b/worlds/sa2b/Options.py @@ -9,11 +9,13 @@ class Goal(Choice): Biolizard: Finish Cannon's Core and defeat the Biolizard and Finalhazard Chaos Emerald Hunt: Find the Seven Chaos Emeralds and reach Green Hill Zone Finalhazard Chaos Emerald Hunt: Find the Seven Chaos Emeralds and reach Green Hill Zone, then defeat Finalhazard + Grand Prix: Win every race in Kart Race Mode (all standard levels are disabled) """ display_name = "Goal" option_biolizard = 0 option_chaos_emerald_hunt = 1 option_finalhazard_chaos_emerald_hunt = 2 + option_grand_prix = 3 default = 0 @@ -84,6 +86,24 @@ class DarknessTrapWeight(BaseTrapWeight): display_name = "Darkness Trap Weight" +class PongTrapWeight(BaseTrapWeight): + """ + Likelihood of receiving a trap which forces you to play a Pong minigame + """ + display_name = "Pong Trap Weight" + + +class MinigameTrapDifficulty(Choice): + """ + How difficult any Minigame-style traps are + """ + display_name = "Minigame Trap Difficulty" + option_easy = 0 + option_medium = 1 + option_hard = 2 + default = 1 + + class JunkFillPercentage(Range): """ Replace a percentage of non-required emblems in the item pool with random junk items @@ -150,6 +170,27 @@ class Beetlesanity(Toggle): display_name = "Beetlesanity" +class Omosanity(Toggle): + """ + Determines whether activating Omochao grants checks + """ + display_name = "Omosanity" + + +class KartRaceChecks(Choice): + """ + Determines whether Kart Race Mode grants checks + None: No Kart Races grant checks + Mini: Each Kart Race difficulty must be beaten only once + Full: Every Character must separately beat each Kart Race difficulty + """ + display_name = "Kart Race Checks" + option_none = 0 + option_mini = 1 + option_full = 2 + default = 0 + + class EmblemPercentageForCannonsCore(Range): """ Allows logic to gate the final mission behind a number of Emblems @@ -440,6 +481,27 @@ class CannonsCoreMission5(DefaultOnToggle): display_name = "Cannon's Core Mission 5" +class RingLoss(Choice): + """ + How taking damage is handled + Classic: You lose all of your rings when hit + Modern: You lose 20 rings when hit + OHKO: You die immediately when hit (NOTE: Some Hard Logic tricks require damage boosts!) + """ + display_name = "SADX Music" + option_classic = 0 + option_modern = 1 + option_ohko = 2 + default = 0 + + @classmethod + def get_option_name(cls, value) -> str: + if cls.auto_display_name and value == 2: + return cls.name_lookup[value].upper() + else: + return cls.name_lookup[value] + + class SADXMusic(Choice): """ Whether the randomizer will include Sonic Adventure DX Music in the music pool @@ -515,6 +577,8 @@ sa2b_options: typing.Dict[str, type(Option)] = { "keysanity": Keysanity, "whistlesanity": Whistlesanity, "beetlesanity": Beetlesanity, + "omosanity": Omosanity, + "kart_race_checks": KartRaceChecks, "required_rank": RequiredRank, "emblem_percentage_for_cannons_core": EmblemPercentageForCannonsCore, "required_cannons_core_missions": RequiredCannonsCoreMissions, @@ -533,6 +597,9 @@ sa2b_options: typing.Dict[str, type(Option)] = { "gravity_trap_weight": GravityTrapWeight, "exposition_trap_weight": ExpositionTrapWeight, #"darkness_trap_weight": DarknessTrapWeight, + "pong_trap_weight": PongTrapWeight, + "minigame_trap_difficulty": MinigameTrapDifficulty, + "ring_loss": RingLoss, "sadx_music": SADXMusic, "music_shuffle": MusicShuffle, "narrator": Narrator, diff --git a/worlds/sa2b/Regions.py b/worlds/sa2b/Regions.py index a5e75b4073..b18fa9ec78 100644 --- a/worlds/sa2b/Regions.py +++ b/worlds/sa2b/Regions.py @@ -134,6 +134,20 @@ def create_regions(world, player: int, active_locations): LocationName.city_escape_hidden_3, LocationName.city_escape_hidden_4, LocationName.city_escape_hidden_5, + LocationName.city_escape_omo_1, + LocationName.city_escape_omo_2, + LocationName.city_escape_omo_3, + LocationName.city_escape_omo_4, + LocationName.city_escape_omo_5, + LocationName.city_escape_omo_6, + LocationName.city_escape_omo_7, + LocationName.city_escape_omo_8, + LocationName.city_escape_omo_9, + LocationName.city_escape_omo_10, + LocationName.city_escape_omo_11, + LocationName.city_escape_omo_12, + LocationName.city_escape_omo_13, + LocationName.city_escape_omo_14, LocationName.city_escape_beetle, LocationName.city_escape_upgrade, ] @@ -150,6 +164,11 @@ def create_regions(world, player: int, active_locations): LocationName.metal_harbor_chao_2, LocationName.metal_harbor_chao_3, LocationName.metal_harbor_pipe_1, + LocationName.metal_harbor_omo_1, + LocationName.metal_harbor_omo_2, + LocationName.metal_harbor_omo_3, + LocationName.metal_harbor_omo_4, + LocationName.metal_harbor_omo_5, LocationName.metal_harbor_beetle, LocationName.metal_harbor_upgrade, ] @@ -190,6 +209,10 @@ def create_regions(world, player: int, active_locations): LocationName.pyramid_cave_pipe_2, LocationName.pyramid_cave_pipe_3, LocationName.pyramid_cave_pipe_4, + LocationName.pyramid_cave_omo_1, + LocationName.pyramid_cave_omo_2, + LocationName.pyramid_cave_omo_3, + LocationName.pyramid_cave_omo_4, LocationName.pyramid_cave_beetle, LocationName.pyramid_cave_upgrade, ] @@ -210,6 +233,19 @@ def create_regions(world, player: int, active_locations): LocationName.crazy_gadget_pipe_3, LocationName.crazy_gadget_pipe_4, LocationName.crazy_gadget_hidden_1, + LocationName.crazy_gadget_omo_1, + LocationName.crazy_gadget_omo_2, + LocationName.crazy_gadget_omo_3, + LocationName.crazy_gadget_omo_4, + LocationName.crazy_gadget_omo_5, + LocationName.crazy_gadget_omo_6, + LocationName.crazy_gadget_omo_7, + LocationName.crazy_gadget_omo_8, + LocationName.crazy_gadget_omo_9, + LocationName.crazy_gadget_omo_10, + LocationName.crazy_gadget_omo_11, + LocationName.crazy_gadget_omo_12, + LocationName.crazy_gadget_omo_13, LocationName.crazy_gadget_beetle, LocationName.crazy_gadget_upgrade, ] @@ -227,6 +263,9 @@ def create_regions(world, player: int, active_locations): LocationName.final_rush_chao_3, LocationName.final_rush_pipe_1, LocationName.final_rush_pipe_2, + LocationName.final_rush_omo_1, + LocationName.final_rush_omo_2, + LocationName.final_rush_omo_3, LocationName.final_rush_beetle, LocationName.final_rush_upgrade, ] @@ -248,6 +287,16 @@ def create_regions(world, player: int, active_locations): LocationName.prison_lane_hidden_1, LocationName.prison_lane_hidden_2, LocationName.prison_lane_hidden_3, + LocationName.prison_lane_omo_1, + LocationName.prison_lane_omo_2, + LocationName.prison_lane_omo_3, + LocationName.prison_lane_omo_4, + LocationName.prison_lane_omo_5, + LocationName.prison_lane_omo_6, + LocationName.prison_lane_omo_7, + LocationName.prison_lane_omo_8, + LocationName.prison_lane_omo_9, + LocationName.prison_lane_omo_10, LocationName.prison_lane_beetle, LocationName.prison_lane_upgrade, ] @@ -270,6 +319,14 @@ def create_regions(world, player: int, active_locations): LocationName.mission_street_hidden_2, LocationName.mission_street_hidden_3, LocationName.mission_street_hidden_4, + LocationName.mission_street_omo_1, + LocationName.mission_street_omo_2, + LocationName.mission_street_omo_3, + LocationName.mission_street_omo_4, + LocationName.mission_street_omo_5, + LocationName.mission_street_omo_6, + LocationName.mission_street_omo_7, + LocationName.mission_street_omo_8, LocationName.mission_street_beetle, LocationName.mission_street_upgrade, ] @@ -299,6 +356,10 @@ def create_regions(world, player: int, active_locations): LocationName.hidden_base_pipe_3, LocationName.hidden_base_pipe_4, LocationName.hidden_base_pipe_5, + LocationName.hidden_base_omo_1, + LocationName.hidden_base_omo_2, + LocationName.hidden_base_omo_3, + LocationName.hidden_base_omo_4, LocationName.hidden_base_beetle, LocationName.hidden_base_upgrade, ] @@ -319,6 +380,18 @@ def create_regions(world, player: int, active_locations): LocationName.eternal_engine_pipe_3, LocationName.eternal_engine_pipe_4, LocationName.eternal_engine_pipe_5, + LocationName.eternal_engine_omo_1, + LocationName.eternal_engine_omo_2, + LocationName.eternal_engine_omo_3, + LocationName.eternal_engine_omo_4, + LocationName.eternal_engine_omo_5, + LocationName.eternal_engine_omo_6, + LocationName.eternal_engine_omo_7, + LocationName.eternal_engine_omo_8, + LocationName.eternal_engine_omo_9, + LocationName.eternal_engine_omo_10, + LocationName.eternal_engine_omo_11, + LocationName.eternal_engine_omo_12, LocationName.eternal_engine_beetle, LocationName.eternal_engine_upgrade, ] @@ -337,6 +410,16 @@ def create_regions(world, player: int, active_locations): LocationName.wild_canyon_pipe_1, LocationName.wild_canyon_pipe_2, LocationName.wild_canyon_pipe_3, + LocationName.wild_canyon_omo_1, + LocationName.wild_canyon_omo_2, + LocationName.wild_canyon_omo_3, + LocationName.wild_canyon_omo_4, + LocationName.wild_canyon_omo_5, + LocationName.wild_canyon_omo_6, + LocationName.wild_canyon_omo_7, + LocationName.wild_canyon_omo_8, + LocationName.wild_canyon_omo_9, + LocationName.wild_canyon_omo_10, LocationName.wild_canyon_beetle, LocationName.wild_canyon_upgrade, ] @@ -354,6 +437,17 @@ def create_regions(world, player: int, active_locations): LocationName.pumpkin_hill_chao_3, LocationName.pumpkin_hill_pipe_1, LocationName.pumpkin_hill_hidden_1, + LocationName.pumpkin_hill_omo_1, + LocationName.pumpkin_hill_omo_2, + LocationName.pumpkin_hill_omo_3, + LocationName.pumpkin_hill_omo_4, + LocationName.pumpkin_hill_omo_5, + LocationName.pumpkin_hill_omo_6, + LocationName.pumpkin_hill_omo_7, + LocationName.pumpkin_hill_omo_8, + LocationName.pumpkin_hill_omo_9, + LocationName.pumpkin_hill_omo_10, + LocationName.pumpkin_hill_omo_11, LocationName.pumpkin_hill_upgrade, ] pumpkin_hill_region = create_region(world, player, active_locations, LocationName.pumpkin_hill_region, @@ -371,6 +465,13 @@ def create_regions(world, player: int, active_locations): LocationName.aquatic_mine_pipe_1, LocationName.aquatic_mine_pipe_2, LocationName.aquatic_mine_pipe_3, + LocationName.aquatic_mine_omo_1, + LocationName.aquatic_mine_omo_2, + LocationName.aquatic_mine_omo_3, + LocationName.aquatic_mine_omo_4, + LocationName.aquatic_mine_omo_5, + LocationName.aquatic_mine_omo_6, + LocationName.aquatic_mine_omo_7, LocationName.aquatic_mine_beetle, LocationName.aquatic_mine_upgrade, ] @@ -391,6 +492,15 @@ def create_regions(world, player: int, active_locations): LocationName.death_chamber_pipe_3, LocationName.death_chamber_hidden_1, LocationName.death_chamber_hidden_2, + LocationName.death_chamber_omo_1, + LocationName.death_chamber_omo_2, + LocationName.death_chamber_omo_3, + LocationName.death_chamber_omo_4, + LocationName.death_chamber_omo_5, + LocationName.death_chamber_omo_6, + LocationName.death_chamber_omo_7, + LocationName.death_chamber_omo_8, + LocationName.death_chamber_omo_9, LocationName.death_chamber_beetle, LocationName.death_chamber_upgrade, ] @@ -409,6 +519,9 @@ def create_regions(world, player: int, active_locations): LocationName.meteor_herd_pipe_1, LocationName.meteor_herd_pipe_2, LocationName.meteor_herd_pipe_3, + LocationName.meteor_herd_omo_1, + LocationName.meteor_herd_omo_2, + LocationName.meteor_herd_omo_3, LocationName.meteor_herd_beetle, LocationName.meteor_herd_upgrade, ] @@ -430,6 +543,14 @@ def create_regions(world, player: int, active_locations): LocationName.radical_highway_hidden_1, LocationName.radical_highway_hidden_2, LocationName.radical_highway_hidden_3, + LocationName.radical_highway_omo_1, + LocationName.radical_highway_omo_2, + LocationName.radical_highway_omo_3, + LocationName.radical_highway_omo_4, + LocationName.radical_highway_omo_5, + LocationName.radical_highway_omo_6, + LocationName.radical_highway_omo_7, + LocationName.radical_highway_omo_8, LocationName.radical_highway_beetle, LocationName.radical_highway_upgrade, ] @@ -452,6 +573,11 @@ def create_regions(world, player: int, active_locations): LocationName.white_jungle_hidden_1, LocationName.white_jungle_hidden_2, LocationName.white_jungle_hidden_3, + LocationName.white_jungle_omo_1, + LocationName.white_jungle_omo_2, + LocationName.white_jungle_omo_3, + LocationName.white_jungle_omo_4, + LocationName.white_jungle_omo_5, LocationName.white_jungle_beetle, LocationName.white_jungle_upgrade, ] @@ -491,6 +617,7 @@ def create_regions(world, player: int, active_locations): LocationName.final_chase_pipe_1, LocationName.final_chase_pipe_2, LocationName.final_chase_pipe_3, + LocationName.final_chase_omo_1, LocationName.final_chase_beetle, LocationName.final_chase_upgrade, ] @@ -511,6 +638,12 @@ def create_regions(world, player: int, active_locations): LocationName.iron_gate_pipe_3, LocationName.iron_gate_pipe_4, LocationName.iron_gate_pipe_5, + LocationName.iron_gate_omo_1, + LocationName.iron_gate_omo_2, + LocationName.iron_gate_omo_3, + LocationName.iron_gate_omo_4, + LocationName.iron_gate_omo_5, + LocationName.iron_gate_omo_6, LocationName.iron_gate_beetle, LocationName.iron_gate_upgrade, ] @@ -531,6 +664,8 @@ def create_regions(world, player: int, active_locations): LocationName.sand_ocean_pipe_3, LocationName.sand_ocean_pipe_4, LocationName.sand_ocean_pipe_5, + LocationName.sand_ocean_omo_1, + LocationName.sand_ocean_omo_2, LocationName.sand_ocean_beetle, LocationName.sand_ocean_upgrade, ] @@ -549,6 +684,14 @@ def create_regions(world, player: int, active_locations): LocationName.lost_colony_pipe_1, LocationName.lost_colony_pipe_2, LocationName.lost_colony_hidden_1, + LocationName.lost_colony_omo_1, + LocationName.lost_colony_omo_2, + LocationName.lost_colony_omo_3, + LocationName.lost_colony_omo_4, + LocationName.lost_colony_omo_5, + LocationName.lost_colony_omo_6, + LocationName.lost_colony_omo_7, + LocationName.lost_colony_omo_8, LocationName.lost_colony_beetle, LocationName.lost_colony_upgrade, ] @@ -569,6 +712,9 @@ def create_regions(world, player: int, active_locations): LocationName.weapons_bed_pipe_3, LocationName.weapons_bed_pipe_4, LocationName.weapons_bed_pipe_5, + LocationName.weapons_bed_omo_1, + LocationName.weapons_bed_omo_2, + LocationName.weapons_bed_omo_3, LocationName.weapons_bed_upgrade, ] weapons_bed_region = create_region(world, player, active_locations, LocationName.weapons_bed_region, @@ -588,6 +734,7 @@ def create_regions(world, player: int, active_locations): LocationName.cosmic_wall_pipe_3, LocationName.cosmic_wall_pipe_4, LocationName.cosmic_wall_pipe_5, + LocationName.cosmic_wall_omo_1, LocationName.cosmic_wall_beetle, LocationName.cosmic_wall_upgrade, ] @@ -605,6 +752,18 @@ def create_regions(world, player: int, active_locations): LocationName.dry_lagoon_chao_3, LocationName.dry_lagoon_pipe_1, LocationName.dry_lagoon_hidden_1, + LocationName.dry_lagoon_omo_1, + LocationName.dry_lagoon_omo_2, + LocationName.dry_lagoon_omo_3, + LocationName.dry_lagoon_omo_4, + LocationName.dry_lagoon_omo_5, + LocationName.dry_lagoon_omo_6, + LocationName.dry_lagoon_omo_7, + LocationName.dry_lagoon_omo_8, + LocationName.dry_lagoon_omo_9, + LocationName.dry_lagoon_omo_10, + LocationName.dry_lagoon_omo_11, + LocationName.dry_lagoon_omo_12, LocationName.dry_lagoon_beetle, LocationName.dry_lagoon_upgrade, ] @@ -624,6 +783,13 @@ def create_regions(world, player: int, active_locations): LocationName.egg_quarters_pipe_2, LocationName.egg_quarters_hidden_1, LocationName.egg_quarters_hidden_2, + LocationName.egg_quarters_omo_1, + LocationName.egg_quarters_omo_2, + LocationName.egg_quarters_omo_3, + LocationName.egg_quarters_omo_4, + LocationName.egg_quarters_omo_5, + LocationName.egg_quarters_omo_6, + LocationName.egg_quarters_omo_7, LocationName.egg_quarters_beetle, LocationName.egg_quarters_upgrade, ] @@ -641,6 +807,18 @@ def create_regions(world, player: int, active_locations): LocationName.security_hall_chao_3, LocationName.security_hall_pipe_1, LocationName.security_hall_hidden_1, + LocationName.security_hall_omo_1, + LocationName.security_hall_omo_2, + LocationName.security_hall_omo_3, + LocationName.security_hall_omo_4, + LocationName.security_hall_omo_5, + LocationName.security_hall_omo_6, + LocationName.security_hall_omo_7, + LocationName.security_hall_omo_8, + LocationName.security_hall_omo_9, + LocationName.security_hall_omo_10, + LocationName.security_hall_omo_11, + LocationName.security_hall_omo_12, LocationName.security_hall_beetle, LocationName.security_hall_upgrade, ] @@ -670,6 +848,11 @@ def create_regions(world, player: int, active_locations): LocationName.mad_space_pipe_2, LocationName.mad_space_pipe_3, LocationName.mad_space_pipe_4, + LocationName.mad_space_omo_1, + LocationName.mad_space_omo_2, + LocationName.mad_space_omo_3, + LocationName.mad_space_omo_4, + LocationName.mad_space_omo_5, LocationName.mad_space_beetle, LocationName.mad_space_upgrade, ] @@ -691,6 +874,15 @@ def create_regions(world, player: int, active_locations): LocationName.cannon_core_pipe_4, LocationName.cannon_core_pipe_5, LocationName.cannon_core_hidden_1, + LocationName.cannon_core_omo_1, + LocationName.cannon_core_omo_2, + LocationName.cannon_core_omo_3, + LocationName.cannon_core_omo_4, + LocationName.cannon_core_omo_5, + LocationName.cannon_core_omo_6, + LocationName.cannon_core_omo_7, + LocationName.cannon_core_omo_8, + LocationName.cannon_core_omo_9, LocationName.cannon_core_beetle, ] cannon_core_region = create_region(world, player, active_locations, LocationName.cannon_core_region, @@ -782,6 +974,59 @@ def create_regions(world, player: int, active_locations): chao_garden_expert_region = create_region(world, player, active_locations, LocationName.chao_garden_expert_region, chao_garden_expert_region_locations) + kart_race_beginner_region_locations = [] + if world.kart_race_checks[player] == 2: + kart_race_beginner_region_locations.extend([ + LocationName.kart_race_beginner_sonic, + LocationName.kart_race_beginner_tails, + LocationName.kart_race_beginner_knuckles, + LocationName.kart_race_beginner_shadow, + LocationName.kart_race_beginner_eggman, + LocationName.kart_race_beginner_rouge, + ]) + if world.kart_race_checks[player] == 1: + kart_race_beginner_region_locations.append(LocationName.kart_race_beginner) + kart_race_beginner_region = create_region(world, player, active_locations, LocationName.kart_race_beginner_region, + kart_race_beginner_region_locations) + + kart_race_standard_region_locations = [] + if world.kart_race_checks[player] == 2: + kart_race_standard_region_locations.extend([ + LocationName.kart_race_standard_sonic, + LocationName.kart_race_standard_tails, + LocationName.kart_race_standard_knuckles, + LocationName.kart_race_standard_shadow, + LocationName.kart_race_standard_eggman, + LocationName.kart_race_standard_rouge, + ]) + if world.kart_race_checks[player] == 1: + kart_race_standard_region_locations.append(LocationName.kart_race_standard) + kart_race_standard_region = create_region(world, player, active_locations, LocationName.kart_race_standard_region, + kart_race_standard_region_locations) + + kart_race_expert_region_locations = [] + if world.kart_race_checks[player] == 2: + kart_race_expert_region_locations.extend([ + LocationName.kart_race_expert_sonic, + LocationName.kart_race_expert_tails, + LocationName.kart_race_expert_knuckles, + LocationName.kart_race_expert_shadow, + LocationName.kart_race_expert_eggman, + LocationName.kart_race_expert_rouge, + ]) + if world.kart_race_checks[player] == 1: + kart_race_expert_region_locations.append(LocationName.kart_race_expert) + kart_race_expert_region = create_region(world, player, active_locations, LocationName.kart_race_expert_region, + kart_race_expert_region_locations) + + if world.goal[player] == 3: + grand_prix_region_locations = [ + LocationName.grand_prix, + ] + grand_prix_region = create_region(world, player, active_locations, LocationName.grand_prix_region, + grand_prix_region_locations) + world.regions += [grand_prix_region] + if world.goal[player] == 0 or world.goal[player] == 2: biolizard_region_locations = [ LocationName.finalhazard, @@ -838,6 +1083,9 @@ def create_regions(world, player: int, active_locations): chao_garden_beginner_region, chao_garden_intermediate_region, chao_garden_expert_region, + kart_race_beginner_region, + kart_race_standard_region, + kart_race_expert_region, ] @@ -867,6 +1115,8 @@ def connect_regions(world, player, gates: typing.List[LevelGate], cannon_core_em state.has(ItemName.blue_emerald, player))) if world.goal[player] == 2: connect(world, player, names, LocationName.green_hill_region, LocationName.biolizard_region) + elif world.goal[player] == 3: + connect(world, player, names, LocationName.kart_race_expert_region, LocationName.grand_prix_region) for i in range(len(gates[0].gate_levels)): connect(world, player, names, LocationName.gate_0_region, shuffleable_regions[gates[0].gate_levels[i]]) @@ -941,27 +1191,51 @@ def connect_regions(world, player, gates: typing.List[LevelGate], cannon_core_em connect(world, player, names, LocationName.gate_0_region, LocationName.chao_garden_beginner_region) connect(world, player, names, LocationName.gate_0_region, LocationName.chao_garden_intermediate_region) connect(world, player, names, LocationName.gate_0_region, LocationName.chao_garden_expert_region) + + connect(world, player, names, LocationName.gate_0_region, LocationName.kart_race_beginner_region) + connect(world, player, names, LocationName.gate_0_region, LocationName.kart_race_standard_region) + connect(world, player, names, LocationName.gate_0_region, LocationName.kart_race_expert_region) elif gates_len == 2: connect(world, player, names, LocationName.gate_0_region, LocationName.chao_garden_beginner_region) connect(world, player, names, LocationName.gate_0_region, LocationName.chao_garden_intermediate_region) connect(world, player, names, LocationName.gate_1_region, LocationName.chao_garden_expert_region) + + connect(world, player, names, LocationName.gate_0_region, LocationName.kart_race_beginner_region) + connect(world, player, names, LocationName.gate_0_region, LocationName.kart_race_standard_region) + connect(world, player, names, LocationName.gate_1_region, LocationName.kart_race_expert_region) elif gates_len == 3: connect(world, player, names, LocationName.gate_0_region, LocationName.chao_garden_beginner_region) connect(world, player, names, LocationName.gate_1_region, LocationName.chao_garden_intermediate_region) connect(world, player, names, LocationName.gate_2_region, LocationName.chao_garden_expert_region) + + connect(world, player, names, LocationName.gate_0_region, LocationName.kart_race_beginner_region) + connect(world, player, names, LocationName.gate_1_region, LocationName.kart_race_standard_region) + connect(world, player, names, LocationName.gate_2_region, LocationName.kart_race_expert_region) elif gates_len == 4: connect(world, player, names, LocationName.gate_0_region, LocationName.chao_garden_beginner_region) connect(world, player, names, LocationName.gate_1_region, LocationName.chao_garden_intermediate_region) connect(world, player, names, LocationName.gate_3_region, LocationName.chao_garden_expert_region) + + connect(world, player, names, LocationName.gate_0_region, LocationName.kart_race_beginner_region) + connect(world, player, names, LocationName.gate_1_region, LocationName.kart_race_standard_region) + connect(world, player, names, LocationName.gate_3_region, LocationName.kart_race_expert_region) elif gates_len == 5: connect(world, player, names, LocationName.gate_1_region, LocationName.chao_garden_beginner_region) connect(world, player, names, LocationName.gate_2_region, LocationName.chao_garden_intermediate_region) connect(world, player, names, LocationName.gate_3_region, LocationName.chao_garden_expert_region) + + connect(world, player, names, LocationName.gate_1_region, LocationName.kart_race_beginner_region) + connect(world, player, names, LocationName.gate_2_region, LocationName.kart_race_standard_region) + connect(world, player, names, LocationName.gate_3_region, LocationName.kart_race_expert_region) elif gates_len >= 6: connect(world, player, names, LocationName.gate_1_region, LocationName.chao_garden_beginner_region) connect(world, player, names, LocationName.gate_2_region, LocationName.chao_garden_intermediate_region) connect(world, player, names, LocationName.gate_4_region, LocationName.chao_garden_expert_region) + connect(world, player, names, LocationName.gate_1_region, LocationName.kart_race_beginner_region) + connect(world, player, names, LocationName.gate_2_region, LocationName.kart_race_standard_region) + connect(world, player, names, LocationName.gate_4_region, LocationName.kart_race_expert_region) + def create_region(world: MultiWorld, player: int, active_locations, name: str, locations=None): ret = Region(name, player, world) diff --git a/worlds/sa2b/Rules.py b/worlds/sa2b/Rules.py index a0590ba29c..ba3c8862c6 100644 --- a/worlds/sa2b/Rules.py +++ b/worlds/sa2b/Rules.py @@ -3,8 +3,8 @@ import typing from BaseClasses import MultiWorld from .Names import LocationName, ItemName from .Locations import boss_gate_set -from ..AutoWorld import LogicMixin -from ..generic.Rules import add_rule, set_rule, CollectionRule +from worlds.AutoWorld import LogicMixin +from worlds.generic.Rules import add_rule, set_rule, CollectionRule from .GateBosses import boss_has_requirement from .Missions import stage_name_prefixes, mission_orders @@ -619,6 +619,174 @@ def set_mission_upgrade_rules_standard(world: MultiWorld, player: int): lambda state: state.has(ItemName.tails_booster, player) and state.has(ItemName.eggman_jet_engine, player)) + # Omochao Upgrade Requirements + if world.omosanity[player]: + add_rule(world.get_location(LocationName.eternal_engine_omo_1, player), + lambda state: state.has(ItemName.tails_booster, player)) + + add_rule(world.get_location(LocationName.hidden_base_omo_2, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.pyramid_cave_omo_2, player), + lambda state: state.has(ItemName.sonic_bounce_bracelet, player)) + add_rule(world.get_location(LocationName.death_chamber_omo_2, player), + lambda state: state.has(ItemName.knuckles_hammer_gloves, player)) + add_rule(world.get_location(LocationName.eternal_engine_omo_2, player), + lambda state: state.has(ItemName.tails_booster, player)) + + add_rule(world.get_location(LocationName.radical_highway_omo_2, player), + lambda state: state.has(ItemName.shadow_air_shoes, player)) + add_rule(world.get_location(LocationName.weapons_bed_omo_2, player), + lambda state: state.has(ItemName.eggman_jet_engine, player)) + + add_rule(world.get_location(LocationName.mission_street_omo_3, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.hidden_base_omo_3, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.pyramid_cave_omo_3, player), + lambda state: state.has(ItemName.sonic_bounce_bracelet, player)) + add_rule(world.get_location(LocationName.eternal_engine_omo_3, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.final_rush_omo_3, player), + lambda state: state.has(ItemName.sonic_bounce_bracelet, player)) + + add_rule(world.get_location(LocationName.weapons_bed_omo_3, player), + lambda state: state.has(ItemName.eggman_jet_engine, player) and + state.has(ItemName.eggman_large_cannon, player)) + + add_rule(world.get_location(LocationName.metal_harbor_omo_4, player), + lambda state: state.has(ItemName.sonic_light_shoes, player)) + add_rule(world.get_location(LocationName.mission_street_omo_4, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.hidden_base_omo_4, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.pyramid_cave_omo_4, player), + lambda state: state.has(ItemName.sonic_bounce_bracelet, player)) + add_rule(world.get_location(LocationName.death_chamber_omo_4, player), + lambda state: state.has(ItemName.knuckles_hammer_gloves, player)) + add_rule(world.get_location(LocationName.eternal_engine_omo_4, player), + lambda state: state.has(ItemName.tails_booster, player)) + + add_rule(world.get_location(LocationName.mad_space_omo_4, player), + lambda state: state.has(ItemName.rouge_iron_boots, player)) + + add_rule(world.get_location(LocationName.cannon_core_omo_4, player), + lambda state: state.has(ItemName.tails_booster, player)) + + add_rule(world.get_location(LocationName.metal_harbor_omo_5, player), + lambda state: state.has(ItemName.sonic_light_shoes, player)) + add_rule(world.get_location(LocationName.mission_street_omo_5, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.death_chamber_omo_5, player), + lambda state: state.has(ItemName.knuckles_hammer_gloves, player)) + add_rule(world.get_location(LocationName.eternal_engine_omo_5, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.crazy_gadget_omo_5, player), + lambda state: state.has(ItemName.sonic_bounce_bracelet, player)) + + add_rule(world.get_location(LocationName.white_jungle_omo_5, player), + lambda state: state.has(ItemName.shadow_air_shoes, player)) + add_rule(world.get_location(LocationName.mad_space_omo_5, player), + lambda state: state.has(ItemName.rouge_iron_boots, player)) + + add_rule(world.get_location(LocationName.cannon_core_omo_5, player), + lambda state: state.has(ItemName.tails_booster, player) and + state.has(ItemName.eggman_jet_engine, player)) + + add_rule(world.get_location(LocationName.mission_street_omo_6, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.death_chamber_omo_6, player), + lambda state: state.has(ItemName.knuckles_hammer_gloves, player)) + add_rule(world.get_location(LocationName.eternal_engine_omo_6, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.crazy_gadget_omo_6, player), + lambda state: state.has(ItemName.sonic_bounce_bracelet, player)) + + add_rule(world.get_location(LocationName.lost_colony_omo_6, player), + lambda state: state.has(ItemName.eggman_jet_engine, player)) + + add_rule(world.get_location(LocationName.cannon_core_omo_6, player), + lambda state: state.has(ItemName.tails_booster, player) and + state.has(ItemName.eggman_jet_engine, player)) + + add_rule(world.get_location(LocationName.mission_street_omo_7, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.death_chamber_omo_7, player), + lambda state: state.has(ItemName.knuckles_shovel_claws, player) and + state.has(ItemName.knuckles_hammer_gloves, player)) + add_rule(world.get_location(LocationName.eternal_engine_omo_7, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.crazy_gadget_omo_7, player), + lambda state: state.has(ItemName.sonic_bounce_bracelet, player)) + + add_rule(world.get_location(LocationName.lost_colony_omo_7, player), + lambda state: state.has(ItemName.eggman_jet_engine, player)) + + add_rule(world.get_location(LocationName.cannon_core_omo_7, player), + lambda state: state.has(ItemName.tails_booster, player) and + state.has(ItemName.eggman_jet_engine, player) and + state.has(ItemName.knuckles_hammer_gloves, player) and + state.has(ItemName.knuckles_air_necklace, player)) + + add_rule(world.get_location(LocationName.mission_street_omo_8, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.death_chamber_omo_8, player), + lambda state: state.has(ItemName.knuckles_hammer_gloves, player)) + add_rule(world.get_location(LocationName.eternal_engine_omo_8, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.crazy_gadget_omo_8, player), + lambda state: state.has(ItemName.sonic_bounce_bracelet, player)) + + add_rule(world.get_location(LocationName.lost_colony_omo_8, player), + lambda state: state.has(ItemName.eggman_jet_engine, player)) + add_rule(world.get_location(LocationName.security_hall_omo_8, player), + lambda state: state.has(ItemName.rouge_mystic_melody, player) and + state.has(ItemName.rouge_iron_boots, player)) + + add_rule(world.get_location(LocationName.cannon_core_omo_8, player), + lambda state: state.has(ItemName.tails_booster, player) and + state.has(ItemName.eggman_jet_engine, player) and + state.has(ItemName.knuckles_hammer_gloves, player) and + state.has(ItemName.knuckles_air_necklace, player)) + + add_rule(world.get_location(LocationName.death_chamber_omo_9, player), + lambda state: state.has(ItemName.knuckles_mystic_melody, player) and + state.has(ItemName.knuckles_hammer_gloves, player)) + add_rule(world.get_location(LocationName.eternal_engine_omo_9, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.crazy_gadget_omo_9, player), + lambda state: state.has(ItemName.sonic_bounce_bracelet, player)) + + add_rule(world.get_location(LocationName.cannon_core_omo_9, player), + lambda state: state.has(ItemName.tails_booster, player) and + state.has(ItemName.eggman_jet_engine, player) and + state.has(ItemName.knuckles_hammer_gloves, player) and + state.has(ItemName.knuckles_air_necklace, player)) + + add_rule(world.get_location(LocationName.eternal_engine_omo_10, player), + lambda state: state.has(ItemName.tails_booster, player) and + state.has(ItemName.tails_bazooka, player)) + add_rule(world.get_location(LocationName.crazy_gadget_omo_10, player), + lambda state: state.has(ItemName.sonic_bounce_bracelet, player)) + + add_rule(world.get_location(LocationName.eternal_engine_omo_11, player), + lambda state: state.has(ItemName.tails_booster, player) and + state.has(ItemName.tails_bazooka, player)) + add_rule(world.get_location(LocationName.crazy_gadget_omo_11, player), + lambda state: state.has(ItemName.sonic_bounce_bracelet, player)) + + add_rule(world.get_location(LocationName.eternal_engine_omo_12, player), + lambda state: state.has(ItemName.tails_booster, player) and + state.has(ItemName.tails_bazooka, player)) + add_rule(world.get_location(LocationName.crazy_gadget_omo_12, player), + lambda state: state.has(ItemName.sonic_light_shoes, player) and + state.has(ItemName.sonic_bounce_bracelet, player) and + state.has(ItemName.sonic_flame_ring, player)) + + add_rule(world.get_location(LocationName.crazy_gadget_omo_13, player), + lambda state: state.has(ItemName.sonic_light_shoes, player) and + state.has(ItemName.sonic_bounce_bracelet, player) and + state.has(ItemName.sonic_flame_ring, player)) + # Gold Beetle Upgrade Requirements if world.beetlesanity[player]: add_rule(world.get_location(LocationName.mission_street_beetle, player), @@ -715,9 +883,6 @@ def set_mission_upgrade_rules_hard(world: MultiWorld, player: int): lambda state: state.has(ItemName.tails_booster, player)) # Mission 3 Upgrade Requirements - add_rule_safe(world, LocationName.city_escape_3, player, - lambda state: state.has(ItemName.sonic_bounce_bracelet, player) or - state.has(ItemName.sonic_mystic_melody, player)) add_rule_safe(world, LocationName.wild_canyon_3, player, lambda state: state.has(ItemName.knuckles_shovel_claws, player)) add_rule_safe(world, LocationName.prison_lane_3, player, @@ -752,9 +917,7 @@ def set_mission_upgrade_rules_hard(world: MultiWorld, player: int): add_rule_safe(world, LocationName.sand_ocean_3, player, lambda state: state.has(ItemName.eggman_jet_engine, player)) add_rule_safe(world, LocationName.egg_quarters_3, player, - lambda state: state.has(ItemName.rouge_mystic_melody, player) and - state.has(ItemName.rouge_pick_nails, player) and - state.has(ItemName.rouge_iron_boots, player)) + lambda state: state.has(ItemName.rouge_mystic_melody, player)) add_rule_safe(world, LocationName.lost_colony_3, player, lambda state: state.has(ItemName.eggman_mystic_melody, player) and state.has(ItemName.eggman_jet_engine, player)) @@ -845,8 +1008,6 @@ def set_mission_upgrade_rules_hard(world: MultiWorld, player: int): lambda state: state.has(ItemName.eggman_jet_engine, player)) add_rule_safe(world, LocationName.security_hall_5, player, lambda state: state.has(ItemName.rouge_treasure_scope, player)) - add_rule_safe(world, LocationName.mad_space_5, player, - lambda state: state.has(ItemName.rouge_iron_boots, player)) add_rule_safe(world, LocationName.cosmic_wall_5, player, lambda state: state.has(ItemName.eggman_jet_engine, player)) @@ -968,8 +1129,6 @@ def set_mission_upgrade_rules_hard(world: MultiWorld, player: int): add_rule(world.get_location(LocationName.cosmic_wall_pipe_1, player), lambda state: state.has(ItemName.eggman_jet_engine, player)) - add_rule(world.get_location(LocationName.mission_street_pipe_2, player), - lambda state: state.has(ItemName.tails_booster, player)) add_rule(world.get_location(LocationName.hidden_base_pipe_2, player), lambda state: state.has(ItemName.tails_booster, player)) add_rule(world.get_location(LocationName.death_chamber_pipe_2, player), @@ -997,9 +1156,6 @@ def set_mission_upgrade_rules_hard(world: MultiWorld, player: int): state.has(ItemName.knuckles_hammer_gloves, player)) add_rule(world.get_location(LocationName.eternal_engine_pipe_3, player), lambda state: state.has(ItemName.tails_booster, player)) - add_rule(world.get_location(LocationName.crazy_gadget_pipe_3, player), - lambda state: state.has(ItemName.sonic_bounce_bracelet, player) or - state.has(ItemName.sonic_mystic_melody, player)) add_rule(world.get_location(LocationName.weapons_bed_pipe_3, player), lambda state: state.has(ItemName.eggman_jet_engine, player)) @@ -1056,6 +1212,125 @@ def set_mission_upgrade_rules_hard(world: MultiWorld, player: int): add_rule(world.get_location(LocationName.cannon_core_hidden_1, player), lambda state: state.has(ItemName.tails_booster, player)) + # Omochao Upgrade Requirements + if world.omosanity[player]: + add_rule(world.get_location(LocationName.eternal_engine_omo_1, player), + lambda state: state.has(ItemName.tails_booster, player)) + + add_rule(world.get_location(LocationName.hidden_base_omo_2, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.death_chamber_omo_2, player), + lambda state: state.has(ItemName.knuckles_hammer_gloves, player)) + add_rule(world.get_location(LocationName.eternal_engine_omo_2, player), + lambda state: state.has(ItemName.tails_booster, player)) + + add_rule(world.get_location(LocationName.weapons_bed_omo_2, player), + lambda state: state.has(ItemName.eggman_jet_engine, player) or + state.has(ItemName.eggman_large_cannon, player)) + + add_rule(world.get_location(LocationName.hidden_base_omo_3, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.eternal_engine_omo_3, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.final_rush_omo_3, player), + lambda state: state.has(ItemName.sonic_bounce_bracelet, player)) + + add_rule(world.get_location(LocationName.weapons_bed_omo_3, player), + lambda state: state.has(ItemName.eggman_jet_engine, player)) + + add_rule(world.get_location(LocationName.hidden_base_omo_4, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.death_chamber_omo_4, player), + lambda state: state.has(ItemName.knuckles_hammer_gloves, player)) + add_rule(world.get_location(LocationName.eternal_engine_omo_4, player), + lambda state: state.has(ItemName.tails_booster, player)) + + add_rule(world.get_location(LocationName.cannon_core_omo_4, player), + lambda state: state.has(ItemName.tails_booster, player)) + + add_rule(world.get_location(LocationName.mission_street_omo_5, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.death_chamber_omo_5, player), + lambda state: state.has(ItemName.knuckles_hammer_gloves, player)) + add_rule(world.get_location(LocationName.eternal_engine_omo_5, player), + lambda state: state.has(ItemName.tails_booster, player)) + + add_rule(world.get_location(LocationName.cannon_core_omo_5, player), + lambda state: state.has(ItemName.tails_booster, player)) + + add_rule(world.get_location(LocationName.mission_street_omo_6, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.death_chamber_omo_6, player), + lambda state: state.has(ItemName.knuckles_hammer_gloves, player)) + add_rule(world.get_location(LocationName.eternal_engine_omo_6, player), + lambda state: state.has(ItemName.tails_booster, player)) + + add_rule(world.get_location(LocationName.lost_colony_omo_6, player), + lambda state: state.has(ItemName.eggman_jet_engine, player)) + + add_rule(world.get_location(LocationName.cannon_core_omo_6, player), + lambda state: state.has(ItemName.tails_booster, player)) + + add_rule(world.get_location(LocationName.mission_street_omo_7, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.death_chamber_omo_7, player), + lambda state: state.has(ItemName.knuckles_shovel_claws, player) and + state.has(ItemName.knuckles_hammer_gloves, player)) + add_rule(world.get_location(LocationName.eternal_engine_omo_7, player), + lambda state: state.has(ItemName.tails_booster, player)) + + add_rule(world.get_location(LocationName.lost_colony_omo_7, player), + lambda state: state.has(ItemName.eggman_jet_engine, player)) + + add_rule(world.get_location(LocationName.cannon_core_omo_7, player), + lambda state: state.has(ItemName.tails_booster, player) and + state.has(ItemName.knuckles_hammer_gloves, player)) + + add_rule(world.get_location(LocationName.mission_street_omo_8, player), + lambda state: state.has(ItemName.tails_booster, player)) + add_rule(world.get_location(LocationName.death_chamber_omo_8, player), + lambda state: state.has(ItemName.knuckles_hammer_gloves, player)) + add_rule(world.get_location(LocationName.eternal_engine_omo_8, player), + lambda state: state.has(ItemName.tails_booster, player)) + + add_rule(world.get_location(LocationName.lost_colony_omo_8, player), + lambda state: state.has(ItemName.eggman_jet_engine, player)) + add_rule(world.get_location(LocationName.security_hall_omo_8, player), + lambda state: state.has(ItemName.rouge_iron_boots, player)) + + add_rule(world.get_location(LocationName.cannon_core_omo_8, player), + lambda state: state.has(ItemName.tails_booster, player) and + state.has(ItemName.knuckles_hammer_gloves, player)) + + add_rule(world.get_location(LocationName.death_chamber_omo_9, player), + lambda state: state.has(ItemName.knuckles_mystic_melody, player) and + state.has(ItemName.knuckles_hammer_gloves, player)) + add_rule(world.get_location(LocationName.eternal_engine_omo_9, player), + lambda state: state.has(ItemName.tails_booster, player)) + + add_rule(world.get_location(LocationName.cannon_core_omo_9, player), + lambda state: state.has(ItemName.tails_booster, player) and + state.has(ItemName.knuckles_hammer_gloves, player)) + + add_rule(world.get_location(LocationName.eternal_engine_omo_10, player), + lambda state: state.has(ItemName.tails_booster, player) and + state.has(ItemName.tails_bazooka, player)) + + add_rule(world.get_location(LocationName.eternal_engine_omo_11, player), + lambda state: state.has(ItemName.tails_booster, player) and + state.has(ItemName.tails_bazooka, player)) + + add_rule(world.get_location(LocationName.eternal_engine_omo_12, player), + lambda state: state.has(ItemName.tails_booster, player) and + state.has(ItemName.tails_bazooka, player)) + add_rule(world.get_location(LocationName.crazy_gadget_omo_12, player), + lambda state: state.has(ItemName.sonic_light_shoes, player) and + state.has(ItemName.sonic_flame_ring, player)) + + add_rule(world.get_location(LocationName.crazy_gadget_omo_13, player), + lambda state: state.has(ItemName.sonic_light_shoes, player) and + state.has(ItemName.sonic_flame_ring, player)) + # Gold Beetle Upgrade Requirements if world.beetlesanity[player]: add_rule(world.get_location(LocationName.hidden_base_beetle, player), @@ -1096,11 +1371,12 @@ def set_rules(world: MultiWorld, player: int, gate_bosses: typing.Dict[int, int] # Mission Progression Rules (Mission 1 begets Mission 2, etc.) set_mission_progress_rules(world, player, mission_map, mission_count_map) - # Upgrade Requirements for each mission location - if world.logic_difficulty[player].value == 0: - set_mission_upgrade_rules_standard(world, player) - elif world.logic_difficulty[player].value == 1: - set_mission_upgrade_rules_hard(world, player) + if world.goal[player].value != 3: + # Upgrade Requirements for each mission location + if world.logic_difficulty[player].value == 0: + set_mission_upgrade_rules_standard(world, player) + elif world.logic_difficulty[player].value == 1: + set_mission_upgrade_rules_hard(world, player) # Upgrade Requirements for each boss gate set_boss_gate_rules(world, player, gate_bosses) diff --git a/worlds/sa2b/__init__.py b/worlds/sa2b/__init__.py index dad7503e0e..d81ec58328 100644 --- a/worlds/sa2b/__init__.py +++ b/worlds/sa2b/__init__.py @@ -9,7 +9,7 @@ from .Regions import create_regions, shuffleable_regions, connect_regions, Level gate_0_blacklist_regions from .Rules import set_rules from .Names import ItemName, LocationName -from ..AutoWorld import WebWorld, World +from worlds.AutoWorld import WebWorld, World from .GateBosses import get_gate_bosses, get_boss_name from .Missions import get_mission_table, get_mission_count_table, get_first_and_last_cannons_core_missions import Patch @@ -52,7 +52,7 @@ class SA2BWorld(World): game: str = "Sonic Adventure 2 Battle" option_definitions = sa2b_options topology_present = False - data_version = 4 + data_version = 5 item_name_groups = item_groups item_name_to_id = {name: data.code for name, data in item_table.items()} @@ -71,17 +71,21 @@ class SA2BWorld(World): def _get_slot_data(self): return { - "ModVersion": 200, + "ModVersion": 201, "Goal": self.multiworld.goal[self.player].value, "MusicMap": self.music_map, "MissionMap": self.mission_map, "MissionCountMap": self.mission_count_map, "MusicShuffle": self.multiworld.music_shuffle[self.player].value, "Narrator": self.multiworld.narrator[self.player].value, + "MinigameTrapDifficulty": self.multiworld.minigame_trap_difficulty[self.player].value, + "RingLoss": self.multiworld.ring_loss[self.player].value, "RequiredRank": self.multiworld.required_rank[self.player].value, "ChaoKeys": self.multiworld.keysanity[self.player].value, "Whistlesanity": self.multiworld.whistlesanity[self.player].value, "GoldBeetles": self.multiworld.beetlesanity[self.player].value, + "OmochaoChecks": self.multiworld.omosanity[self.player].value, + "KartRaceChecks": self.multiworld.kart_race_checks[self.player].value, "ChaoRaceChecks": self.multiworld.chao_race_checks[self.player].value, "ChaoGardenDifficulty": self.multiworld.chao_garden_difficulty[self.player].value, "DeathLink": self.multiworld.death_link[self.player].value, @@ -145,13 +149,45 @@ class SA2BWorld(World): return levels_per_gate def generate_early(self): - self.gate_bosses = get_gate_bosses(self.multiworld, self.player) + if self.multiworld.goal[self.player].value == 3: + # Turn off everything else for Grand Prix goal + self.multiworld.number_of_level_gates[self.player].value = 0 + self.multiworld.emblem_percentage_for_cannons_core[self.player].value = 0 + self.multiworld.junk_fill_percentage[self.player].value = 100 + self.multiworld.trap_fill_percentage[self.player].value = 100 + self.multiworld.omochao_trap_weight[self.player].value = 0 + self.multiworld.timestop_trap_weight[self.player].value = 0 + self.multiworld.confusion_trap_weight[self.player].value = 0 + self.multiworld.tiny_trap_weight[self.player].value = 0 + self.multiworld.gravity_trap_weight[self.player].value = 0 - def generate_basic(self): + valid_trap_weights = self.multiworld.exposition_trap_weight[self.player].value + self.multiworld.pong_trap_weight[self.player].value + + if valid_trap_weights == 0: + self.multiworld.exposition_trap_weight[self.player].value = 4 + self.multiworld.pong_trap_weight[self.player].value = 4 + + if self.multiworld.kart_race_checks[self.player].value == 0: + self.multiworld.kart_race_checks[self.player].value = 2 + + self.gate_bosses = {} + else: + self.gate_bosses = get_gate_bosses(self.multiworld, self.player) + + def create_regions(self): + self.mission_map = get_mission_table(self.multiworld, self.player) + self.mission_count_map = get_mission_count_table(self.multiworld, self.player) + + self.location_table = setup_locations(self.multiworld, self.player, self.mission_map, self.mission_count_map) + create_regions(self.multiworld, self.player, self.location_table) + + # Not Generate Basic if self.multiworld.goal[self.player].value == 0 or self.multiworld.goal[self.player].value == 2: self.multiworld.get_location(LocationName.finalhazard, self.player).place_locked_item(self.create_item(ItemName.maria)) elif self.multiworld.goal[self.player].value == 1: self.multiworld.get_location(LocationName.green_hill, self.player).place_locked_item(self.create_item(ItemName.maria)) + elif self.multiworld.goal[self.player].value == 3: + self.multiworld.get_location(LocationName.grand_prix, self.player).place_locked_item(self.create_item(ItemName.maria)) itempool: typing.List[SA2BItem] = [] @@ -159,18 +195,19 @@ class SA2BWorld(World): total_required_locations = len(self.location_table) total_required_locations -= 1; # Locked Victory Location - # Fill item pool with all required items - for item in {**upgrades_table}: - itempool += self._create_items(item) - - if self.multiworld.goal[self.player].value == 1 or self.multiworld.goal[self.player].value == 2: - # Some flavor of Chaos Emerald Hunt - for item in {**emeralds_table}: + if self.multiworld.goal[self.player].value != 3: + # Fill item pool with all required items + for item in {**upgrades_table}: itempool += self._create_items(item) - # Cap at 180 Emblems + if self.multiworld.goal[self.player].value == 1 or self.multiworld.goal[self.player].value == 2: + # Some flavor of Chaos Emerald Hunt + for item in {**emeralds_table}: + itempool += self._create_items(item) + + # Cap at 250 Emblems raw_emblem_count = total_required_locations - len(itempool) - total_emblem_count = min(raw_emblem_count, 180) + total_emblem_count = min(raw_emblem_count, 250) extra_junk_count = raw_emblem_count - total_emblem_count self.emblems_for_cannons_core = math.floor( @@ -234,6 +271,7 @@ class SA2BWorld(World): trap_weights += ([ItemName.gravity_trap] * self.multiworld.gravity_trap_weight[self.player].value) trap_weights += ([ItemName.exposition_trap] * self.multiworld.exposition_trap_weight[self.player].value) #trap_weights += ([ItemName.darkness_trap] * self.multiworld.darkness_trap_weight[self.player].value) + trap_weights += ([ItemName.pong_trap] * self.multiworld.pong_trap_weight[self.player].value) junk_count += extra_junk_count trap_count = 0 if (len(trap_weights) == 0) else math.ceil(junk_count * (self.multiworld.trap_fill_percentage[self.player].value / 100.0)) @@ -309,13 +347,6 @@ class SA2BWorld(World): self.music_map = dict(zip(musiclist_o, musiclist_s)) - def create_regions(self): - self.mission_map = get_mission_table(self.multiworld, self.player) - self.mission_count_map = get_mission_count_table(self.multiworld, self.player) - - self.location_table = setup_locations(self.multiworld, self.player, self.mission_map, self.mission_count_map) - create_regions(self.multiworld, self.player, self.location_table) - def create_item(self, name: str, force_non_progression=False) -> Item: data = item_table[name] diff --git a/worlds/sa2b/docs/en_Sonic Adventure 2 Battle.md b/worlds/sa2b/docs/en_Sonic Adventure 2 Battle.md index 89ff80e9c4..3b608808ae 100644 --- a/worlds/sa2b/docs/en_Sonic Adventure 2 Battle.md +++ b/worlds/sa2b/docs/en_Sonic Adventure 2 Battle.md @@ -10,11 +10,11 @@ The randomizer shuffles emblems and upgrade items into the AP item pool. The sto ## What is the goal of Sonic Adventure 2: Battle when randomized? -If the Biolizard goal is selected, the objective is to unlock and complete the required number of Cannon's Core Missions, then complete the Biolizard boss fight. If the Emerald Hunt goal is selected, the objective is to find the seven Chaos Emeralds then complete Green Hill Zone and optionally default Final Hazard. +If the Biolizard goal is selected, the objective is to unlock and complete the required number of Cannon's Core Missions, then complete the Biolizard boss fight. If the Emerald Hunt goal is selected, the objective is to find the seven Chaos Emeralds then complete Green Hill Zone and optionally default Final Hazard. If the Grand Prix goal is selected, the objective is to complete all levels in the kart race mode. ## What items and locations get shuffled? -All 30 story stages leading up to Cannon's Core will be shuffled and can be optionally placed behind emblem requirement gates. Mission order can be shuffled for each stage. Chao keys, animal pipes, hidden whistle spots, and gold beetles can be added as additional locations to check in each stage. Chao Garden emblems can optionally be added to the randomizer. All emblems from the selected mission range and all 28 character upgrade items get shuffled. At most 180 emblems will be added to the item pool, after which remaining items added will be random collectables (rings, shields, etc). Traps can also be optionally included. +All 30 story stages leading up to Cannon's Core will be shuffled and can be optionally placed behind emblem requirement gates. Mission order can be shuffled for each stage. Chao keys, animal pipes, hidden whistle spots, omochao, and gold beetles can be added as additional locations to check in each stage. Chao Garden emblems can optionally be added to the randomizer. All emblems from the selected mission range and all 28 character upgrade items get shuffled. At most 250 emblems will be added to the item pool, after which remaining items added will be random collectables (rings, shields, etc). Traps can also be optionally included. ## Which items can be in another player's world? From 5977e401d5f9719f49c367f54215e1bef67bbac3 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 21 Mar 2023 22:02:21 +0100 Subject: [PATCH 089/172] MultiServer: include 'Archipelago' in games, versions and checksums --- MultiServer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/MultiServer.py b/MultiServer.py index 9a91b50c8a..6fd06fece8 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -746,6 +746,7 @@ async def on_client_connected(ctx: Context, client: Client): 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, [{ 'cmd': 'RoomInfo', 'password': bool(ctx.password), From 95b01def6be918f48370053bfef69d56e37a995e Mon Sep 17 00:00:00 2001 From: Jarno Date: Wed, 22 Mar 2023 11:32:37 +0100 Subject: [PATCH 090/172] Timespinner: RC bug, lake serene is dry, when Rising Tides is off (#1570) * Tinmespinner: RC bug, lake serene is dry, when Rising Tides is off * yes --- worlds/timespinner/PreCalculatedWeights.py | 39 +++++++++++++--------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/worlds/timespinner/PreCalculatedWeights.py b/worlds/timespinner/PreCalculatedWeights.py index 514a17f8ac..64243e25ed 100644 --- a/worlds/timespinner/PreCalculatedWeights.py +++ b/worlds/timespinner/PreCalculatedWeights.py @@ -20,19 +20,31 @@ class PreCalculatedWeights: dry_lake_serene: bool def __init__(self, world: MultiWorld, player: int): - weights_overrrides: Dict[str, Union[str, Dict[str, int]]] = self.get_flood_weights_overrides(world, player) + if world and is_option_enabled(world, player, "RisingTides"): + weights_overrrides: Dict[str, Union[str, Dict[str, int]]] = self.get_flood_weights_overrides(world, player) - self.flood_basement, self.flood_basement_high = \ - self.roll_flood_setting(world, player, weights_overrrides, "CastleBasement") - self.flood_xarion, _ = self.roll_flood_setting(world, player, weights_overrrides, "Xarion") - self.flood_maw, _ = self.roll_flood_setting(world, player, weights_overrrides, "Maw") - self.flood_pyramid_shaft, _ = self.roll_flood_setting(world, player, weights_overrrides, "AncientPyramidShaft") - self.flood_pyramid_back, _ = self.roll_flood_setting(world, player, weights_overrrides, "Sandman") - self.flood_moat, _ = self.roll_flood_setting(world, player, weights_overrrides, "CastleMoat") - self.flood_courtyard, _ = self.roll_flood_setting(world, player, weights_overrrides, "CastleCourtyard") - self.flood_lake_desolation, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeDesolation") - flood_lake_serene, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeSerene") - self.dry_lake_serene = not flood_lake_serene + self.flood_basement, self.flood_basement_high = \ + self.roll_flood_setting(world, player, weights_overrrides, "CastleBasement") + self.flood_xarion, _ = self.roll_flood_setting(world, player, weights_overrrides, "Xarion") + self.flood_maw, _ = self.roll_flood_setting(world, player, weights_overrrides, "Maw") + self.flood_pyramid_shaft, _ = self.roll_flood_setting(world, player, weights_overrrides, "AncientPyramidShaft") + self.flood_pyramid_back, _ = self.roll_flood_setting(world, player, weights_overrrides, "Sandman") + self.flood_moat, _ = self.roll_flood_setting(world, player, weights_overrrides, "CastleMoat") + self.flood_courtyard, _ = self.roll_flood_setting(world, player, weights_overrrides, "CastleCourtyard") + self.flood_lake_desolation, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeDesolation") + flood_lake_serene, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeSerene") + self.dry_lake_serene = not flood_lake_serene + else: + self.flood_basement = False + self.flood_basement_high = False + self.flood_xarion = False + self.flood_maw = False + self.flood_pyramid_shaft = False + self.flood_pyramid_back = False + self.flood_moat = False + self.flood_courtyard = False + self.flood_lake_desolation = False + self.dry_lake_serene = False self.pyramid_keys_unlock, self.present_key_unlock, self.past_key_unlock, self.time_key_unlock = \ self.get_pyramid_keys_unlocks(world, player, self.flood_maw) @@ -106,9 +118,6 @@ class PreCalculatedWeights: def roll_flood_setting(world: MultiWorld, player: int, all_weights: Dict[str, Union[Dict[str, int], str]], key: str) -> Tuple[bool, bool]: - if not world or not is_option_enabled(world, player, "RisingTides"): - return False, False - weights: Union[Dict[str, int], str] = all_weights[key] if isinstance(weights, dict): From 017f91c1b513ecf4cf70e5069251d7241bf0dacc Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Wed, 22 Mar 2023 10:19:29 -0400 Subject: [PATCH 091/172] LADX: Remove duplicate Link's Awakening setup (#1573) --- inno_setup.iss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/inno_setup.iss b/inno_setup.iss index 206c33446f..4a4c8927de 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -67,7 +67,6 @@ Name: "generator/ladx"; Description: "Link's Awakening DX ROM Setup"; Types: f Name: "generator/tloz"; Description: "The Legend of Zelda ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 135168; Flags: disablenouninstallwarning Name: "server"; Description: "Server"; Types: full hosting Name: "client"; Description: "Clients"; Types: full playing -Name: "client/la"; Description: "Links Awakening DX Client"; Types: full playing Name: "client/sni"; Description: "SNI Client"; Types: full playing Name: "client/sni/lttp"; Description: "SNI Client - A Link to the Past Patch Setup"; Types: full playing; Flags: disablenouninstallwarning Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing; Flags: disablenouninstallwarning @@ -116,7 +115,7 @@ Source: "{#source_path}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignorev Source: "{#source_path}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio Source: "{#source_path}\ArchipelagoTextClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/text Source: "{#source_path}\ArchipelagoSNIClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni -Source: "{#source_path}\ArchipelagoLinksAwakeningClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/la +Source: "{#source_path}\ArchipelagoLinksAwakeningClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ladx Source: "{#source_path}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni/lttp or generator/lttp Source: "{#source_path}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft Source: "{#source_path}\ArchipelagoOoTClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot From 0c6b1827fef35aa33f34dda0b6f03a6c08b7e988 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Tue, 21 Mar 2023 22:49:33 -0400 Subject: [PATCH 092/172] Pokemon R/B: Pokemon Tower Wild Pokemon logic --- worlds/pokemon_rb/rules.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/worlds/pokemon_rb/rules.py b/worlds/pokemon_rb/rules.py index 704446d66d..c69df00366 100644 --- a/worlds/pokemon_rb/rules.py +++ b/worlds/pokemon_rb/rules.py @@ -125,6 +125,8 @@ def set_rules(world, player): "Seafoam Islands B4F - Legendary Pokemon": lambda state: state.pokemon_rb_can_strength(player), "Vermilion City - Legendary Pokemon": lambda state: state.pokemon_rb_can_surf(player) and state.has("S.S. Ticket", player), + **{f"Pokemon Tower {floor}F - Wild Pokemon - {slot}": lambda state: state.has("Silph Scope", player) for floor in range(3, 8) for slot in range(1, 11)}, + "Route 2 - Marcel Trade": lambda state: state.can_reach("Route 24 - Wild Pokemon - 6", "Location", player), "Underground Tunnel West-East - Spot Trade": lambda state: state.can_reach("Route 24 - Wild Pokemon - 6", "Location", player), "Route 11 - Terry Trade": lambda state: state.can_reach("Safari Zone Center - Wild Pokemon - 5", "Location", player), From 206f8cf5ed3d1952fa90c95360738b1ffbedab3f Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Wed, 22 Mar 2023 10:21:41 -0400 Subject: [PATCH 093/172] KH2: fixed bugs of rc1 (#1565) KH2Client: - Now checks if the world id is in the list of checks. This fixed sending out stuff on the movie - Cleaned up unused inports - Not getting starting invo if the game is not open when you connect to the server __init__: -Cleaned up print statements - Fixed the spoiler log not outputting the right amount of mcguffins after fixing them in the case of the player messing up their settings Openkh: -Fixed putting the correct dummy item on levels --- KH2Client.py | 27 +++++++++------------------ worlds/kh2/OpenKH.py | 2 +- worlds/kh2/__init__.py | 28 ++++++++++++++++++---------- 3 files changed, 28 insertions(+), 29 deletions(-) diff --git a/KH2Client.py b/KH2Client.py index 7c3f69117a..2068e11e84 100644 --- a/KH2Client.py +++ b/KH2Client.py @@ -1,11 +1,10 @@ import os import asyncio import ModuleUpdate -import typing import json import Utils from pymem import pymem -from worlds.kh2.Items import DonaldAbility_Table, GoofyAbility_Table, exclusionItem_table, CheckDupingItems +from worlds.kh2.Items import exclusionItem_table, CheckDupingItems from worlds.kh2 import all_locations, item_dictionary_table, exclusion_table from worlds.kh2.WorldLocations import * @@ -89,6 +88,7 @@ class KH2Context(CommonContext): "StatIncrease": {}, "Boost": {}, }}, + # 1,3,255 are in this list in case the player gets locations in those "worlds" and I need to still have them checked "worldIdChecks": { "1": [], # world of darkness (story cutscenes) "2": [], @@ -136,9 +136,9 @@ class KH2Context(CommonContext): self.game_connected = False self.finalxemnas = False self.worldid = { - 1: TWTNW_Checks, # world of darkness (story cutscenes) + # 1: {}, # world of darkness (story cutscenes) 2: TT_Checks, - 3: TT_Checks, # destiny island doesn't have checks to ima put tt checks here + # 3: {}, # destiny island doesn't have checks to ima put tt checks here 4: HB_Checks, 5: BC_Checks, 6: Oc_Checks, @@ -154,7 +154,7 @@ class KH2Context(CommonContext): 16: PR_Checks, 17: SP_Checks, 18: TWTNW_Checks, - 255: HB_Checks, # starting screen + # 255: {}, # starting screen } # 0x2A09C00+0x40 is the sve anchor. +1 is the last saved room self.sveroom = 0x2A09C00 + 0x41 @@ -304,22 +304,22 @@ class KH2Context(CommonContext): if item.location not in self.kh2seedsave["checked_locations"][str(item.player)] \ and item.location not in {-1, -2}: self.kh2seedsave["checked_locations"][str(item.player)].append(item.location) - if not self.kh2seedsave["starting_inventory"] and self.kh2connected: + if not self.kh2seedsave["starting_inventory"]: self.kh2seedsave["starting_inventory"] = True if cmd in {"RoomUpdate"}: if "checked_locations" in args: new_locations = set(args["checked_locations"]) # TODO: make this take locations from other players on the same slot so proper coop happens - #items_to_give = [self.kh2slotdata["LocalItems"][str(location_id)] for location_id in new_locations if + # items_to_give = [self.kh2slotdata["LocalItems"][str(location_id)] for location_id in new_locations if # location_id in self.kh2LocalItems.keys()] self.checked_locations |= new_locations async def checkWorldLocations(self): try: currentworldint = int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x0714DB8, 1), "big") - curworldid = self.worldid[currentworldint] - if currentworldint != 1: + if currentworldint in self.worldid: + curworldid = self.worldid[currentworldint] for location, data in curworldid.items(): if location not in self.locations_checked \ and (int.from_bytes( @@ -442,16 +442,9 @@ class KH2Context(CommonContext): return isChecked async def give_item(self, item, ItemType="ServerItems"): - while not self.kh2connected: - await asyncio.sleep(1) try: itemname = self.lookup_id_to_item[item] itemcode = self.item_name_to_data[itemname] - # cannot give items during loading screens - # 0x8E9DA3=load 0xAB8BC7=black 0x2A148E8=controllable 0x715568=room transition - while int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Now, 1), "big") in {255, 1}: - await asyncio.sleep(1) - if itemcode.ability: abilityInvoType = 0 TwilightZone = 2 @@ -459,7 +452,6 @@ class KH2Context(CommonContext): abilityInvoType = 1 TwilightZone = -2 if itemname in {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"}: - self.kh2seedsave["AmountInvo"][ItemType]["Growth"][itemname] += 1 return @@ -609,7 +601,6 @@ class KH2Context(CommonContext): for itemName in master_keyblade: itemData = self.item_name_to_data[itemName] - # if isChecked: # if the inventory slot for that keyblade is less than the amount they should have if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), "big") <= 0: diff --git a/worlds/kh2/OpenKH.py b/worlds/kh2/OpenKH.py index a1f9cc362e..284b9f9489 100644 --- a/worlds/kh2/OpenKH.py +++ b/worlds/kh2/OpenKH.py @@ -195,7 +195,7 @@ def patch_kh2(self, output_directory): if data.item.player == self.player: itemcode = item_dictionary_table[data.item.name].kh2id else: - itemcode = 461 + itemcode = 90 # castle map else: increaseStat(self.multiworld.per_slot_randoms[self.player].randint(0, 3)) itemcode = 0 diff --git a/worlds/kh2/__init__.py b/worlds/kh2/__init__.py index a9b86fa25d..99037ec40c 100644 --- a/worlds/kh2/__init__.py +++ b/worlds/kh2/__init__.py @@ -1,6 +1,6 @@ - from BaseClasses import Tutorial, ItemClassification +import logging from .Items import * from .Locations import all_locations, setup_locations, exclusion_table @@ -97,7 +97,8 @@ class KH2World(World): if item in ActionAbility_Table.keys() or item in SupportAbility_Table.keys(): # cannot have more than the quantity for abilties if value > item_dictionary_table[item].quantity: - print(f"{self.multiworld.get_file_safe_player_name(self.player)} cannot have more than {item_dictionary_table[item].quantity} of {item}") + logging.info(f"{self.multiworld.get_file_safe_player_name(self.player)} cannot have more than {item_dictionary_table[item].quantity} of {item}" + f"Changing the amount to the max amount") value = item_dictionary_table[item].quantity self.item_quantity_dict[item] -= value @@ -128,9 +129,12 @@ class KH2World(World): luckyemblemamount = self.multiworld.LuckyEmblemsAmount[self.player].value luckyemblemrequired = self.multiworld.LuckyEmblemsRequired[self.player].value if luckyemblemamount < luckyemblemrequired: + logging.info(f"Lucky Emblem Amount {self.multiworld.LuckyEmblemsAmount[self.player].value} is less than required " + f"{self.multiworld.LuckyEmblemsRequired[self.player].value} for player {self.multiworld.get_file_safe_player_name(self.player)}." + f" Setting amount to {self.multiworld.LuckyEmblemsRequired[self.player].value}") luckyemblemamount = max(luckyemblemamount, luckyemblemrequired) - print(f"Lucky Emblem Amount {self.multiworld.LuckyEmblemsAmount[self.player].value} is less than required \ - {self.multiworld.LuckyEmblemsRequired[self.player].value} for player {self.multiworld.get_file_safe_player_name(self.player)}") + self.multiworld.LuckyEmblemsAmount[self.player].value = luckyemblemamount + self.item_quantity_dict[ItemName.LuckyEmblem] = item_dictionary_table[ItemName.LuckyEmblem].quantity + luckyemblemamount # give this proof to unlock the final door once the player has the amount of lucky emblem required self.item_quantity_dict[ItemName.ProofofNonexistence] = 0 @@ -146,19 +150,23 @@ class KH2World(World): self.RandomSuperBoss.remove(location) # Testing if the player has the right amount of Bounties for Completion. if len(self.RandomSuperBoss) < self.BountiesAmount: - print(f"{self.multiworld.get_file_safe_player_name(self.player)} has too many bounties than bosses") + logging.info(f"{self.multiworld.get_file_safe_player_name(self.player)} has too many bounties than bosses." + f" Setting total bounties to {len(self.RandomSuperBoss)}") self.BountiesAmount = len(self.RandomSuperBoss) - self.multiworld.BountyAmount[self.player].value = len(self.RandomSuperBoss) + self.multiworld.BountyAmount[self.player].value = self.BountiesAmount if len(self.RandomSuperBoss) < self.BountiesRequired: - print(f"{self.multiworld.get_file_safe_player_name(self.player)} has too many required bounties") + logging.info(f"{self.multiworld.get_file_safe_player_name(self.player)} has too many required bounties." + f" Setting required bounties to {len(self.RandomSuperBoss)}") self.BountiesRequired = len(self.RandomSuperBoss) - self.multiworld.BountyRequired[self.player].value = len(self.RandomSuperBoss) + self.multiworld.BountyRequired[self.player].value = self.BountiesRequired if self.BountiesAmount < self.BountiesRequired: + logging.info(f"Bounties Amount {self.multiworld.BountyAmount[self.player].value} is less than required " + f"{self.multiworld.BountyRequired[self.player].value} for player {self.multiworld.get_file_safe_player_name(self.player)}." + f" Setting amount to {self.multiworld.BountyRequired[self.player].value}") self.BountiesAmount = max(self.BountiesAmount, self.BountiesRequired) - print(f"Bounties Amount {self.multiworld.BountyAmount[self.player].value} is less than required \ - {self.multiworld.BountyRequired[self.player].value} for player {self.multiworld.get_file_safe_player_name(self.player)}") + self.multiworld.BountyAmount[self.player].value = self.BountiesAmount self.multiworld.start_hints[self.player].value.add(ItemName.Bounty) self.item_quantity_dict[ItemName.ProofofNonexistence] = 0 From d48e1e447f848c5f2eccc7b0291f7228abb1b0f5 Mon Sep 17 00:00:00 2001 From: JusticePS Date: Wed, 22 Mar 2023 07:25:55 -0700 Subject: [PATCH 094/172] Adventure: implement new game (#1531) Adds Adventure for the Atari 2600, NTSC version. New randomizer, not based on prior works. Somewhat atypical of current AP rom patch games; The generator does not require the adventure rom, but writes some data to an .apadvn APContainer file that the client uses along with a base bsdiff patch to generate a final rom file. --- .gitignore | 1 + AdventureClient.py | 516 +++++++++++++ README.md | 1 + Utils.py | 8 +- data/adventure_basepatch.bsdiff4 | Bin 0 -> 1059 bytes data/lua/ADVENTURE/adventure_connector.lua | 851 +++++++++++++++++++++ data/lua/ADVENTURE/json.lua | 380 +++++++++ data/lua/ADVENTURE/socket.lua | 132 ++++ host.yaml | 19 + inno_setup.iss | 54 +- worlds/adventure/Items.py | 53 ++ worlds/adventure/Locations.py | 214 ++++++ worlds/adventure/Offsets.py | 46 ++ worlds/adventure/Options.py | 244 ++++++ worlds/adventure/Regions.py | 160 ++++ worlds/adventure/Rom.py | 321 ++++++++ worlds/adventure/Rules.py | 98 +++ worlds/adventure/__init__.py | 391 ++++++++++ worlds/adventure/docs/en_Adventure.md | 62 ++ worlds/adventure/docs/setup_en.md | 70 ++ 20 files changed, 3619 insertions(+), 2 deletions(-) create mode 100644 AdventureClient.py create mode 100644 data/adventure_basepatch.bsdiff4 create mode 100644 data/lua/ADVENTURE/adventure_connector.lua create mode 100644 data/lua/ADVENTURE/json.lua create mode 100644 data/lua/ADVENTURE/socket.lua create mode 100644 worlds/adventure/Items.py create mode 100644 worlds/adventure/Locations.py create mode 100644 worlds/adventure/Offsets.py create mode 100644 worlds/adventure/Options.py create mode 100644 worlds/adventure/Regions.py create mode 100644 worlds/adventure/Rom.py create mode 100644 worlds/adventure/Rules.py create mode 100644 worlds/adventure/__init__.py create mode 100644 worlds/adventure/docs/en_Adventure.md create mode 100644 worlds/adventure/docs/setup_en.md diff --git a/.gitignore b/.gitignore index f0df6e6f56..5f8ad6b917 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ *multisave *.archipelago *.apsave +*.BIN build bundle/components.wxs diff --git a/AdventureClient.py b/AdventureClient.py new file mode 100644 index 0000000000..4c2cb92c70 --- /dev/null +++ b/AdventureClient.py @@ -0,0 +1,516 @@ +import asyncio +import hashlib +import json +import time +import os +import bsdiff4 +import subprocess +import zipfile +from asyncio import StreamReader, StreamWriter, CancelledError +from typing import List + + +import Utils +from NetUtils import ClientStatus +from Utils import async_start +from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \ + get_base_parser +from worlds.adventure import AdventureDeltaPatch + +from worlds.adventure.Locations import base_location_id +from worlds.adventure.Rom import AdventureForeignItemInfo, AdventureAutoCollectLocation, BatNoTouchLocation +from worlds.adventure.Items import base_adventure_item_id, standard_item_max, item_table +from worlds.adventure.Offsets import static_item_element_size, connector_port_offset + +SYSTEM_MESSAGE_ID = 0 + +CONNECTION_TIMING_OUT_STATUS = \ + "Connection timing out. Please restart your emulator, then restart adventure_connector.lua" +CONNECTION_REFUSED_STATUS = \ + "Connection Refused. Please start your emulator and make sure adventure_connector.lua is running" +CONNECTION_RESET_STATUS = \ + "Connection was reset. Please restart your emulator, then restart adventure_connector.lua" +CONNECTION_TENTATIVE_STATUS = "Initial Connection Made" +CONNECTION_CONNECTED_STATUS = "Connected" +CONNECTION_INITIAL_STATUS = "Connection has not been initiated" + +SCRIPT_VERSION = 1 + + +class AdventureCommandProcessor(ClientCommandProcessor): + def __init__(self, ctx: CommonContext): + super().__init__(ctx) + + def _cmd_2600(self): + """Check 2600 Connection State""" + if isinstance(self.ctx, AdventureContext): + logger.info(f"2600 Status: {self.ctx.atari_status}") + + def _cmd_aconnect(self): + """Discard current atari 2600 connection state""" + if isinstance(self.ctx, AdventureContext): + self.ctx.atari_sync_task.cancel() + + +class AdventureContext(CommonContext): + command_processor = AdventureCommandProcessor + game = 'Adventure' + lua_connector_port: int = 17242 + + def __init__(self, server_address, password): + super().__init__(server_address, password) + self.freeincarnates_used: int = -1 + self.freeincarnate_pending: int = 0 + self.foreign_items: [AdventureForeignItemInfo] = [] + self.autocollect_items: [AdventureAutoCollectLocation] = [] + self.atari_streams: (StreamReader, StreamWriter) = None + self.atari_sync_task = None + self.messages = {} + self.locations_array = None + self.atari_status = CONNECTION_INITIAL_STATUS + self.awaiting_rom = False + self.display_msgs = True + self.deathlink_pending = False + self.set_deathlink = False + self.client_compatibility_mode = 0 + self.items_handling = 0b111 + self.checked_locations_sent: bool = False + self.port_offset = 0 + self.bat_no_touch_locations: [BatNoTouchLocation] = [] + self.local_item_locations = {} + self.dragon_speed_info = {} + + options = Utils.get_options() + self.display_msgs = options["adventure_options"]["display_msgs"] + + async def server_auth(self, password_requested: bool = False): + if password_requested and not self.password: + await super(AdventureContext, self).server_auth(password_requested) + if not self.auth: + self.auth = self.player_name + if not self.auth: + self.awaiting_rom = True + logger.info('Awaiting connection to adventure_connector to get Player information') + return + + await self.send_connect() + + def _set_message(self, msg: str, msg_id: int): + if self.display_msgs: + self.messages[(time.time(), msg_id)] = msg + + def on_package(self, cmd: str, args: dict): + if cmd == 'Connected': + self.locations_array = None + if Utils.get_options()["adventure_options"].get("death_link", False): + self.set_deathlink = True + async_start(self.get_freeincarnates_used()) + elif cmd == "RoomInfo": + self.seed_name = args['seed_name'] + elif cmd == 'Print': + msg = args['text'] + 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']])}" + self._set_message(msg, SYSTEM_MESSAGE_ID) + elif cmd == "Retrieved": + self.freeincarnates_used = args["keys"][f"adventure_{self.auth}_freeincarnates_used"] + if self.freeincarnates_used is None: + self.freeincarnates_used = 0 + self.freeincarnates_used += self.freeincarnate_pending + self.send_pending_freeincarnates() + elif cmd == "SetReply": + if args["key"] == f"adventure_{self.auth}_freeincarnates_used": + self.freeincarnates_used = args["value"] + if self.freeincarnates_used is None: + self.freeincarnates_used = 0 + self.freeincarnates_used += self.freeincarnate_pending + self.send_pending_freeincarnates() + + def on_deathlink(self, data: dict): + self.deathlink_pending = True + super().on_deathlink(data) + + def run_gui(self): + from kvui import GameManager + + class AdventureManager(GameManager): + logging_pairs = [ + ("Client", "Archipelago") + ] + base_title = "Archipelago Adventure Client" + + self.ui = AdventureManager(self) + self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + + async def get_freeincarnates_used(self): + if self.server and not self.server.socket.closed: + await self.send_msgs([{"cmd": "SetNotify", "keys": [f"adventure_{self.auth}_freeincarnates_used"]}]) + await self.send_msgs([{"cmd": "Get", "keys": [f"adventure_{self.auth}_freeincarnates_used"]}]) + + def send_pending_freeincarnates(self): + if self.freeincarnate_pending > 0: + async_start(self.send_pending_freeincarnates_impl(self.freeincarnate_pending)) + self.freeincarnate_pending = 0 + + async def send_pending_freeincarnates_impl(self, send_val: int) -> None: + await self.send_msgs([{"cmd": "Set", "key": f"adventure_{self.auth}_freeincarnates_used", + "default": 0, "want_reply": False, + "operations": [{"operation": "add", "value": send_val}]}]) + + async def used_freeincarnate(self) -> None: + if self.server and not self.server.socket.closed: + await self.send_msgs([{"cmd": "Set", "key": f"adventure_{self.auth}_freeincarnates_used", + "default": 0, "want_reply": True, + "operations": [{"operation": "add", "value": 1}]}]) + else: + self.freeincarnate_pending = self.freeincarnate_pending + 1 + + +def convert_item_id(ap_item_id: int): + static_item_index = ap_item_id - base_adventure_item_id + return static_item_index * static_item_element_size + + +def get_payload(ctx: AdventureContext): + current_time = time.time() + items = [] + dragon_speed_update = {} + diff_a_locked = ctx.diff_a_mode > 0 + diff_b_locked = ctx.diff_b_mode > 0 + freeincarnate_count = 0 + for item in ctx.items_received: + item_id_str = str(item.item) + if base_adventure_item_id < item.item <= standard_item_max: + items.append(convert_item_id(item.item)) + elif item_id_str in ctx.dragon_speed_info: + if item.item in dragon_speed_update: + last_index = len(ctx.dragon_speed_info[item_id_str]) - 1 + dragon_speed_update[item.item] = ctx.dragon_speed_info[item_id_str][last_index] + else: + dragon_speed_update[item.item] = ctx.dragon_speed_info[item_id_str][0] + elif item.item == item_table["Left Difficulty Switch"].id: + diff_a_locked = False + elif item.item == item_table["Right Difficulty Switch"].id: + diff_b_locked = False + elif item.item == item_table["Freeincarnate"].id: + freeincarnate_count = freeincarnate_count + 1 + freeincarnates_available = 0 + + if ctx.freeincarnates_used >= 0: + freeincarnates_available = freeincarnate_count - (ctx.freeincarnates_used + ctx.freeincarnate_pending) + ret = json.dumps( + { + "items": items, + "messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items() + if key[0] > current_time - 10}, + "deathlink": ctx.deathlink_pending, + "dragon_speeds": dragon_speed_update, + "difficulty_a_locked": diff_a_locked, + "difficulty_b_locked": diff_b_locked, + "freeincarnates_available": freeincarnates_available, + "bat_logic": ctx.bat_logic + } + ) + ctx.deathlink_pending = False + return ret + + +async def parse_locations(data: List, ctx: AdventureContext): + locations = data + + # for loc_name, loc_data in location_table.items(): + + # if flags["EventFlag"][280] & 1 and not ctx.finished_game: + # await ctx.send_msgs([ + # {"cmd": "StatusUpdate", + # "status": 30} + # ]) + # ctx.finished_game = True + if locations == ctx.locations_array: + return + ctx.locations_array = locations + if locations is not None: + await ctx.send_msgs([{"cmd": "LocationChecks", "locations": locations}]) + + +def send_ap_foreign_items(adventure_context): + foreign_item_json_list = [] + autocollect_item_json_list = [] + bat_no_touch_locations_json_list = [] + for fi in adventure_context.foreign_items: + foreign_item_json_list.append(fi.get_dict()) + for fi in adventure_context.autocollect_items: + autocollect_item_json_list.append(fi.get_dict()) + for ntl in adventure_context.bat_no_touch_locations: + bat_no_touch_locations_json_list.append(ntl.get_dict()) + payload = json.dumps( + { + "foreign_items": foreign_item_json_list, + "autocollect_items": autocollect_item_json_list, + "local_item_locations": adventure_context.local_item_locations, + "bat_no_touch_locations": bat_no_touch_locations_json_list + } + ) + print("sending foreign items") + msg = payload.encode() + (reader, writer) = adventure_context.atari_streams + writer.write(msg) + writer.write(b'\n') + + +def send_checked_locations_if_needed(adventure_context): + if not adventure_context.checked_locations_sent and adventure_context.checked_locations is not None: + if len(adventure_context.checked_locations) == 0: + return + checked_short_ids = [] + for location in adventure_context.checked_locations: + checked_short_ids.append(location - base_location_id) + print("Sending checked locations") + payload = json.dumps( + { + "checked_locations": checked_short_ids, + } + ) + msg = payload.encode() + (reader, writer) = adventure_context.atari_streams + writer.write(msg) + writer.write(b'\n') + adventure_context.checked_locations_sent = True + + +async def atari_sync_task(ctx: AdventureContext): + logger.info("Starting Atari 2600 connector. Use /2600 for status information") + while not ctx.exit_event.is_set(): + try: + error_status = None + if ctx.atari_streams: + (reader, writer) = ctx.atari_streams + msg = get_payload(ctx).encode() + writer.write(msg) + writer.write(b'\n') + try: + await asyncio.wait_for(writer.drain(), timeout=1.5) + try: + # Data will return a dict with 1+ fields + # 1. A keepalive response of the Players Name (always) + # 2. romhash field with sha256 hash of the ROM memory region + # 3. locations, messages, and deathLink + # 4. freeincarnate, to indicate a freeincarnate was used + data = await asyncio.wait_for(reader.readline(), timeout=5) + data_decoded = json.loads(data.decode()) + if 'scriptVersion' not in data_decoded or data_decoded['scriptVersion'] != SCRIPT_VERSION: + msg = "You are connecting with an incompatible Lua script version. Ensure your connector " \ + "Lua and AdventureClient are from the same Archipelago installation." + logger.info(msg, extra={'compact_gui': True}) + ctx.gui_error('Error', msg) + error_status = CONNECTION_RESET_STATUS + if ctx.seed_name and bytes(ctx.seed_name, encoding='ASCII') != ctx.seed_name_from_data: + msg = "The server is running a different multiworld than your client is. " \ + "(invalid seed_name)" + logger.info(msg, extra={'compact_gui': True}) + ctx.gui_error('Error', msg) + error_status = CONNECTION_RESET_STATUS + if 'romhash' in data_decoded: + if ctx.rom_hash.upper() != data_decoded['romhash'].upper(): + msg = "The rom hash does not match the client rom hash data" + print("got " + data_decoded['romhash']) + print("expected " + str(ctx.rom_hash)) + logger.info(msg, extra={'compact_gui': True}) + ctx.gui_error('Error', msg) + error_status = CONNECTION_RESET_STATUS + if ctx.auth is None: + ctx.auth = ctx.player_name + if ctx.awaiting_rom: + await ctx.server_auth(False) + if 'locations' in data_decoded and ctx.game and ctx.atari_status == CONNECTION_CONNECTED_STATUS \ + and not error_status and ctx.auth: + # Not just a keep alive ping, parse + async_start(parse_locations(data_decoded['locations'], ctx)) + if 'deathLink' in data_decoded and data_decoded['deathLink'] > 0 and 'DeathLink' in ctx.tags: + dragon_name = "a dragon" + if data_decoded['deathLink'] == 1: + dragon_name = "Rhindle" + elif data_decoded['deathLink'] == 2: + dragon_name = "Yorgle" + elif data_decoded['deathLink'] == 3: + dragon_name = "Grundle" + print (ctx.auth + " has been eaten by " + dragon_name ) + await ctx.send_death(ctx.auth + " has been eaten by " + dragon_name) + # TODO - also if player reincarnates with a dragon onscreen ' dies to avoid being eaten by ' + if 'victory' in data_decoded and not ctx.finished_game: + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) + ctx.finished_game = True + if 'freeincarnate' in data_decoded: + await ctx.used_freeincarnate() + if ctx.set_deathlink: + await ctx.update_death_link(True) + send_checked_locations_if_needed(ctx) + except asyncio.TimeoutError: + logger.debug("Read Timed Out, Reconnecting") + error_status = CONNECTION_TIMING_OUT_STATUS + writer.close() + ctx.atari_streams = None + except ConnectionResetError as e: + logger.debug("Read failed due to Connection Lost, Reconnecting") + error_status = CONNECTION_RESET_STATUS + writer.close() + ctx.atari_streams = None + except TimeoutError: + logger.debug("Connection Timed Out, Reconnecting") + error_status = CONNECTION_TIMING_OUT_STATUS + writer.close() + ctx.atari_streams = None + except ConnectionResetError: + logger.debug("Connection Lost, Reconnecting") + error_status = CONNECTION_RESET_STATUS + writer.close() + ctx.atari_streams = None + except CancelledError: + logger.debug("Connection Cancelled, Reconnecting") + error_status = CONNECTION_RESET_STATUS + writer.close() + ctx.atari_streams = None + pass + except Exception as e: + print("unknown exception " + e) + raise + if ctx.atari_status == CONNECTION_TENTATIVE_STATUS: + if not error_status: + logger.info("Successfully Connected to 2600") + ctx.atari_status = CONNECTION_CONNECTED_STATUS + ctx.checked_locations_sent = False + send_ap_foreign_items(ctx) + send_checked_locations_if_needed(ctx) + else: + ctx.atari_status = f"Was tentatively connected but error occurred: {error_status}" + elif error_status: + ctx.atari_status = error_status + logger.info("Lost connection to 2600 and attempting to reconnect. Use /2600 for status updates") + else: + try: + port = ctx.lua_connector_port + ctx.port_offset + logger.debug(f"Attempting to connect to 2600 on port {port}") + print(f"Attempting to connect to 2600 on port {port}") + ctx.atari_streams = await asyncio.wait_for( + asyncio.open_connection("localhost", + port), + timeout=10) + ctx.atari_status = CONNECTION_TENTATIVE_STATUS + except TimeoutError: + logger.debug("Connection Timed Out, Trying Again") + ctx.atari_status = CONNECTION_TIMING_OUT_STATUS + continue + except ConnectionRefusedError: + logger.debug("Connection Refused, Trying Again") + ctx.atari_status = CONNECTION_REFUSED_STATUS + continue + except CancelledError: + pass + except CancelledError: + pass + print("exiting atari sync task") + + +async def run_game(romfile): + auto_start = Utils.get_options()["adventure_options"].get("rom_start", True) + rom_args = Utils.get_options()["adventure_options"].get("rom_args") + if auto_start is True: + import webbrowser + webbrowser.open(romfile) + elif os.path.isfile(auto_start): + open_args = [auto_start, romfile] + if rom_args is not None: + open_args.insert(1, rom_args) + subprocess.Popen(open_args, + stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + +async def patch_and_run_game(patch_file, ctx): + base_name = os.path.splitext(patch_file)[0] + comp_path = base_name + '.a26' + try: + base_rom = AdventureDeltaPatch.get_source_data() + except Exception as msg: + logger.info(msg, extra={'compact_gui': True}) + ctx.gui_error('Error', msg) + + with open("data/adventure_basepatch.bsdiff4", "rb") as file: + basepatch = bytes(file.read()) + + base_patched_rom_data = bsdiff4.patch(base_rom, basepatch) + + with zipfile.ZipFile(patch_file, 'r') as patch_archive: + if not AdventureDeltaPatch.check_version(patch_archive): + logger.error("apadvn version doesn't match this client. Make sure your generator and client are the same") + raise Exception("apadvn version doesn't match this client.") + + ctx.foreign_items = AdventureDeltaPatch.read_foreign_items(patch_archive) + ctx.autocollect_items = AdventureDeltaPatch.read_autocollect_items(patch_archive) + ctx.local_item_locations = AdventureDeltaPatch.read_local_item_locations(patch_archive) + ctx.dragon_speed_info = AdventureDeltaPatch.read_dragon_speed_info(patch_archive) + ctx.seed_name_from_data, ctx.player_name = AdventureDeltaPatch.read_rom_info(patch_archive) + ctx.diff_a_mode, ctx.diff_b_mode = AdventureDeltaPatch.read_difficulty_switch_info(patch_archive) + ctx.bat_logic = AdventureDeltaPatch.read_bat_logic(patch_archive) + ctx.bat_no_touch_locations = AdventureDeltaPatch.read_bat_no_touch(patch_archive) + ctx.rom_deltas = AdventureDeltaPatch.read_rom_deltas(patch_archive) + ctx.auth = ctx.player_name + + patched_rom_data = AdventureDeltaPatch.apply_rom_deltas(base_patched_rom_data, ctx.rom_deltas) + rom_hash = hashlib.sha256() + rom_hash.update(patched_rom_data) + ctx.rom_hash = rom_hash.hexdigest() + ctx.port_offset = patched_rom_data[connector_port_offset] + + with open(comp_path, "wb") as patched_rom_file: + patched_rom_file.write(patched_rom_data) + + async_start(run_game(comp_path)) + + +if __name__ == '__main__': + + Utils.init_logging("AdventureClient") + + async def main(): + parser = get_base_parser() + parser.add_argument('patch_file', default="", type=str, nargs="?", + help='Path to an ADVNTURE.BIN rom file') + parser.add_argument('port', default=17242, type=int, nargs="?", + help='port for adventure_connector connection') + args = parser.parse_args() + + ctx = AdventureContext(args.connect, args.password) + ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + ctx.atari_sync_task = asyncio.create_task(atari_sync_task(ctx), name="Adventure Sync") + + if args.patch_file: + ext = args.patch_file.split(".")[len(args.patch_file.split(".")) - 1].lower() + if ext == "apadvn": + logger.info("apadvn file supplied, beginning patching process...") + async_start(patch_and_run_game(args.patch_file, ctx)) + else: + logger.warning(f"Unknown patch file extension {ext}") + if args.port is int: + ctx.lua_connector_port = args.port + + await ctx.exit_event.wait() + ctx.server_address = None + + await ctx.shutdown() + + if ctx.atari_sync_task: + await ctx.atari_sync_task + print("finished atari_sync_task (main)") + + + import colorama + + colorama.init() + + asyncio.run(main()) + colorama.deinit() diff --git a/README.md b/README.md index 2c49aba929..9454d0f168 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ Currently, the following games are supported: * Kingdom Hearts 2 * The Legend of Zelda: Link's Awakening DX * Clique +* Adventure For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/Utils.py b/Utils.py index 23acb9f180..e2ff19310b 100644 --- a/Utils.py +++ b/Utils.py @@ -332,7 +332,13 @@ def get_default_options() -> OptionsType: }, "wargroove_options": { "root_directory": "C:/Program Files (x86)/Steam/steamapps/common/Wargroove" - } + }, + "adventure_options": { + "rom_file": "ADVNTURE.BIN", + "display_msgs": True, + "rom_start": True, + "rom_args": "" + }, } return options diff --git a/data/adventure_basepatch.bsdiff4 b/data/adventure_basepatch.bsdiff4 new file mode 100644 index 0000000000000000000000000000000000000000..038dff1e6416d339c1f60f348a8419c6f0475e9c GIT binary patch literal 1059 zcmV+;1l;>VQ$$HdMl>*d000000000v0RR91000005C8xG0000&T4*^jL0KkKS@lBc z9smG7es+BCBLUF>00a;K2mn9;00aOa2mk-X5IfN8IjFhL4TCYl_Zra>c&fTu~w0Uo&c^`(Q@dlycj(w>sHs`J7MYI=DIn z!gYQ+MMOXH;Mi3BOT%1|80M)*PFG})eZZE;fWVT3q+%J(jVDq>NdS~U5JUw5KgHaU zP81|92riHSLRx4!F+o`-Q&}cw?KA)Y=>Px!|Ns8xU?BoQsF{FozGI1dAR0<$;~!*_ zdBEey^g!OQ0fm8rDUcIT(Vzk90qQg~2Afgp00Te(8Z-fs>Ne6(QRtd_Pc)&E(rKec zp|k|i=mg28h7%EuOaLYX#AIoLVHgt-VK4!q2vO-z)XfCOr>cI2iakToph4;Y0qO(V zpfu3L(@iwU8hS^H21U_EW5yH)WZ@a^F^ENd6Z?D6V(e^6B19nwoFH&cPH*|xjIz|U z*^G*<2>7EIj~&ZrG;{07h^jb(k&}fRz!eU-b=cB|Y}+tM5}(>16Ig^4P~xCJ0O)2< zKTH6IG3W;-p=1al-3kE(Bw|vVAy5$rTfDKG2%LYTMCBzow*k;k23lNN$SIvq=hqev z)TrQw^i*f$=Q!PgHFF?jQ8LGF5Yjs&OU)zf2|Ka(phhpkjAKd-u98MuiFjB4R|3-6 zaG)x;mmy52pB=V@Gv3OxSje0(Lxve_ieDFa2$eLzoc9seon;xwp&ffN1)1M6#|BN2 ztsW|n)~JnyGJQZJo>Z*2>2_Ef9!N&M3+id^yYkGG&~lJ)Vd>g*vAN(iYX}a8gaVYI z(6Qg4ZOP5Z6!qL6tE|M*5~d{cDzBvju`*j`5SD4IZ4Vqct0#b+VuDT_e4}m8glG?K z87d!qAipwXaV{d$$Ct1O24fQ>>A-MNEZ3|2saYW93Qo~@``reop$wxNZvL3z#elCC dnvYd7K-Mep1Rqc=8>|0|xgwk>NE0*mngDaC#S#Dj literal 0 HcmV?d00001 diff --git a/data/lua/ADVENTURE/adventure_connector.lua b/data/lua/ADVENTURE/adventure_connector.lua new file mode 100644 index 0000000000..598d6d74ff --- /dev/null +++ b/data/lua/ADVENTURE/adventure_connector.lua @@ -0,0 +1,851 @@ +local socket = require("socket") +local json = require('json') +local math = require('math') + +local STATE_OK = "Ok" +local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected" +local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made" +local STATE_UNINITIALIZED = "Uninitialized" + +local SCRIPT_VERSION = 1 + +local APItemValue = 0xA2 +local APItemRam = 0xE7 +local BatAPItemValue = 0xAB +local BatAPItemRam = 0xEA +local PlayerRoomAddr = 0x8A -- if in number room, we're not in play mode +local WinAddr = 0xDE -- if not 0 (I think if 0xff specifically), we won (and should update once, immediately) + +-- If any of these are 2, that dragon ate the player (should send update immediately +-- once, and reset that when none of them are 2 again) + +local DragonState = {0xA8, 0xAD, 0xB2} +local last_dragon_state = {0, 0, 0} +local carryAddress = 0x9D -- uses rom object table +local batRoomAddr = 0xCB +local batCarryAddress = 0xD0 -- uses ram object location +local batInvalidCarryItem = 0x78 +local batItemCheckAddr = 0xf69f +local batMatrixLen = 11 -- number of pairs +local last_carry_item = 0xB4 +local frames_with_no_item = 0 +local ItemTableStart = 0xfe9d +local PlayerSlotAddress = 0xfff9 + +local itemMessages = {} + +local nullObjectId = 0xB4 +local ItemsReceived = nil +local sha256hash = nil +local foreign_items = nil +local foreign_items_by_room = {} +local bat_no_touch_locations_by_room = {} +local bat_no_touch_items = {} +local autocollect_items = {} +local localItemLocations = {} + +local prev_bat_room = 0xff +local prev_player_room = 0 +local prev_ap_room_index = nil + +local pending_foreign_items_collected = {} +local pending_local_items_collected = {} +local rendering_foreign_item = nil +local skip_inventory_items = {} + +local inventory = {} +local next_inventory_item = nil + +local input_button_address = 0xD7 + +local deathlink_rec = nil +local deathlink_send = 0 + +local deathlink_sent = false + +local prevstate = "" +local curstate = STATE_UNINITIALIZED +local atariSocket = nil +local frame = 0 + +local ItemIndex = 0 + +local yorgle_speed_address = 0xf725 +local grundle_speed_address = 0xf740 +local rhindle_speed_address = 0xf70A + +local read_switch_a = 0xf780 +local read_switch_b = 0xf764 + +local yorgle_speed = nil +local grundle_speed = nil +local rhindle_speed = nil + +local slow_yorgle_id = tostring(118000000 + 0x103) +local slow_grundle_id = tostring(118000000 + 0x104) +local slow_rhindle_id = tostring(118000000 + 0x105) + +local yorgle_dead = false +local grundle_dead = false +local rhindle_dead = false + +local diff_a_locked = false +local diff_b_locked = false + +local bat_logic = 0 + +local is_dead = 0 +local freeincarnates_available = 0 +local send_freeincarnate_used = false +local current_bat_ap_item = nil + +local was_in_number_room = false + +local u8 = nil +local wU8 = nil +local u16 + +local bizhawk_version = client.getversion() +local is23Or24Or25 = (bizhawk_version=="2.3.1") or (bizhawk_version:sub(1,3)=="2.4") or (bizhawk_version:sub(1,3)=="2.5") +local is26To28 = (bizhawk_version:sub(1,3)=="2.6") or (bizhawk_version:sub(1,3)=="2.7") or (bizhawk_version:sub(1,3)=="2.8") + +u8 = memory.read_u8 +wU8 = memory.write_u8 +u16 = memory.read_u16_le +function uRangeRam(address, bytes) + data = memory.read_bytes_as_array(address, bytes, "Main RAM") + return data +end +function uRangeRom(address, bytes) + data = memory.read_bytes_as_array(address+0xf000, bytes, "System Bus") + return data +end +function uRangeAddress(address, bytes) + data = memory.read_bytes_as_array(address, bytes, "System Bus") + return data +end + + +function table.empty (self) + for _, _ in pairs(self) do + return false + end + return true +end + +function slice (tbl, s, e) + local pos, new = 1, {} + for i = s + 1, e do + new[pos] = tbl[i] + pos = pos + 1 + end + return new +end + +local function createForeignItemsByRoom() + foreign_items_by_room = {} + if foreign_items == nil then + return + end + for _, foreign_item in pairs(foreign_items) do + if foreign_items_by_room[foreign_item.room_id] == nil then + foreign_items_by_room[foreign_item.room_id] = {} + end + new_foreign_item = {} + new_foreign_item.room_id = foreign_item.room_id + new_foreign_item.room_x = foreign_item.room_x + new_foreign_item.room_y = foreign_item.room_y + new_foreign_item.short_location_id = foreign_item.short_location_id + + table.insert(foreign_items_by_room[foreign_item.room_id], new_foreign_item) + end +end + +function debugPrintNoTouchLocations() + for room_id, list in pairs(bat_no_touch_locations_by_room) do + for index, notouch_location in ipairs(list) do + print("ROOM "..tostring(room_id).. "["..tostring(index).."]: "..tostring(notouch_location.short_location_id)) + end + end +end + +function processBlock(block) + if block == nil then + return + end + local block_identified = 0 + local msgBlock = block['messages'] + if msgBlock ~= nil then + block_identified = 1 + for i, v in pairs(msgBlock) do + if itemMessages[i] == nil then + local msg = {TTL=450, message=v, color=0xFFFF0000} + itemMessages[i] = msg + end + end + end + local itemsBlock = block["items"] + if itemsBlock ~= nil then + block_identified = 1 + ItemsReceived = itemsBlock + end + local apItemsBlock = block["foreign_items"] + if apItemsBlock ~= nil then + block_identified = 1 + print("got foreign items block") + foreign_items = apItemsBlock + createForeignItemsByRoom() + end + local autocollectItems = block["autocollect_items"] + if autocollectItems ~= nil then + block_identified = 1 + autocollect_items = {} + for _, acitem in pairs(autocollectItems) do + if autocollect_items[acitem.room_id] == nil then + autocollect_items[acitem.room_id] = {} + end + table.insert(autocollect_items[acitem.room_id], acitem) + end + end + local localLocalItemLocations = block["local_item_locations"] + if localLocalItemLocations ~= nil then + block_identified = 1 + localItemLocations = localLocalItemLocations + print("got local item locations") + end + local checkedLocationsBlock = block["checked_locations"] + if checkedLocationsBlock ~= nil then + block_identified = 1 + for room_id, foreign_item_list in pairs(foreign_items_by_room) do + for i, foreign_item in pairs(foreign_item_list) do + short_id = foreign_item.short_location_id + for j, checked_id in pairs(checkedLocationsBlock) do + if checked_id == short_id then + table.remove(foreign_item_list, i) + break + end + end + end + end + if foreign_items ~= nil then + for i, foreign_item in pairs(foreign_items) do + short_id = foreign_item.short_location_id + for j, checked_id in pairs(checkedLocationsBlock) do + if checked_id == short_id then + foreign_items[i] = nil + break + end + end + end + end + end + local dragon_speeds_block = block["dragon_speeds"] + if dragon_speeds_block ~= nil then + block_identified = 1 + yorgle_speed = dragon_speeds_block[slow_yorgle_id] + grundle_speed = dragon_speeds_block[slow_grundle_id] + rhindle_speed = dragon_speeds_block[slow_rhindle_id] + end + local diff_a_block = block["difficulty_a_locked"] + if diff_a_block ~= nil then + block_identified = 1 + diff_a_locked = diff_a_block + end + local diff_b_block = block["difficulty_b_locked"] + if diff_b_block ~= nil then + block_identified = 1 + diff_b_locked = diff_b_block + end + local freeincarnates_available_block = block["freeincarnates_available"] + if freeincarnates_available_block ~= nil then + block_identified = 1 + if freeincarnates_available ~= freeincarnates_available_block then + freeincarnates_available = freeincarnates_available_block + local msg = {TTL=450, message="freeincarnates: "..tostring(freeincarnates_available), color=0xFFFF0000} + itemMessages[-2] = msg + end + end + local bat_logic_block = block["bat_logic"] + if bat_logic_block ~= nil then + block_identified = 1 + bat_logic = bat_logic_block + end + local bat_no_touch_locations_block = block["bat_no_touch_locations"] + if bat_no_touch_locations_block ~= nil then + block_identified = 1 + for _, notouch_location in pairs(bat_no_touch_locations_block) do + local room_id = tonumber(notouch_location.room_id) + if bat_no_touch_locations_by_room[room_id] == nil then + bat_no_touch_locations_by_room[room_id] = {} + end + table.insert(bat_no_touch_locations_by_room[room_id], notouch_location) + + if notouch_location.local_item ~= nil and notouch_location.local_item ~= 255 then + bat_no_touch_items[tonumber(notouch_location.local_item)] = true + -- print("no touch: "..tostring(notouch_location.local_item)) + end + end + -- debugPrintNoTouchLocations() + end + deathlink_rec = deathlink_rec or block["deathlink"] + if( block_identified == 0 ) then + print("unidentified block") + print(block) + end +end + +local function clearScreen() + if is23Or24Or25 then + return + elseif is26To28 then + drawText(0, 0, "", "black") + end +end + +local function getMaxMessageLength() + if is23Or24Or25 then + return client.screenwidth()/11 + elseif is26To28 then + return client.screenwidth()/12 + end +end + +function drawText(x, y, message, color) + if is23Or24Or25 then + gui.addmessage(message) + elseif is26To28 then + gui.drawText(x, y, message, color, 0xB0000000, 18, "Courier New", nil, nil, nil, "client") + end +end + +local function drawMessages() + if table.empty(itemMessages) then + clearScreen() + return + end + local y = 10 + found = false + maxMessageLength = getMaxMessageLength() + for k, v in pairs(itemMessages) do + if v["TTL"] > 0 then + message = v["message"] + while true do + drawText(5, y, message:sub(1, maxMessageLength), v["color"]) + y = y + 16 + + message = message:sub(maxMessageLength + 1, message:len()) + if message:len() == 0 then + break + end + end + newTTL = 0 + if is26To28 then + newTTL = itemMessages[k]["TTL"] - 1 + end + itemMessages[k]["TTL"] = newTTL + found = true + end + end + if found == false then + clearScreen() + end +end + +function difference(a, b) + local aa = {} + for k,v in pairs(a) do aa[v]=true end + for k,v in pairs(b) do aa[v]=nil end + local ret = {} + local n = 0 + for k,v in pairs(a) do + if aa[v] then n=n+1 ret[n]=v end + end + return ret +end + +function getAllRam() + uRangeRAM(0,128); + return data +end + +local function arrayEqual(a1, a2) + if #a1 ~= #a2 then + return false + end + + for i, v in ipairs(a1) do + if v ~= a2[i] then + return false + end + end + + return true +end + +local function alive_mode() + return (u8(PlayerRoomAddr) ~= 0x00 and u8(WinAddr) == 0x00) +end + +local function generateLocationsChecked() + list_of_locations = {} + for s, f in pairs(pending_foreign_items_collected) do + table.insert(list_of_locations, f.short_location_id + 118000000) + end + for s, f in pairs(pending_local_items_collected) do + table.insert(list_of_locations, f + 118000000) + end + return list_of_locations +end + +function receive() + l, e = atariSocket:receive() + if e == 'closed' then + if curstate == STATE_OK then + print("Connection closed") + end + curstate = STATE_UNINITIALIZED + return + elseif e == 'timeout' then + return + elseif e ~= nil then + print(e) + curstate = STATE_UNINITIALIZED + return + end + if l ~= nil then + processBlock(json.decode(l)) + end + -- Determine Message to send back + + newSha256 = memory.hash_region(0xF000, 0x1000, "System Bus") + if (sha256hash ~= nil and sha256hash ~= newSha256) then + print("ROM changed, quitting") + curstate = STATE_UNINITIALIZED + return + end + sha256hash = newSha256 + local retTable = {} + retTable["scriptVersion"] = SCRIPT_VERSION + retTable["romhash"] = sha256hash + if (alive_mode()) then + retTable["locations"] = generateLocationsChecked() + end + if (u8(WinAddr) ~= 0x00) then + retTable["victory"] = 1 + end + if( deathlink_sent or deathlink_send == 0 ) then + retTable["deathLink"] = 0 + else + print("Sending deathlink "..tostring(deathlink_send)) + retTable["deathLink"] = deathlink_send + deathlink_sent = true + end + deathlink_send = 0 + + if send_freeincarnate_used == true then + print("Sending freeincarnate used") + retTable["freeincarnate"] = true + send_freeincarnate_used = false + end + + msg = json.encode(retTable).."\n" + local ret, error = atariSocket:send(msg) + if ret == nil then + print(error) + elseif curstate == STATE_INITIAL_CONNECTION_MADE then + curstate = STATE_TENTATIVELY_CONNECTED + elseif curstate == STATE_TENTATIVELY_CONNECTED then + print("Connected!") + curstate = STATE_OK + end +end + +function AutocollectFromRoom() + if autocollect_items ~= nil and autocollect_items[prev_player_room] ~= nil then + for _, item in pairs(autocollect_items[prev_player_room]) do + pending_foreign_items_collected[item.short_location_id] = item + end + end +end + +function SetYorgleSpeed() + if yorgle_speed ~= nil then + emu.setregister("A", yorgle_speed); + end +end + +function SetGrundleSpeed() + if grundle_speed ~= nil then + emu.setregister("A", grundle_speed); + end +end + +function SetRhindleSpeed() + if rhindle_speed ~= nil then + emu.setregister("A", rhindle_speed); + end +end + +function SetDifficultySwitchB() + if diff_b_locked then + local a = emu.getregister("A") + if a < 128 then + emu.setregister("A", a + 128) + end + end +end + +function SetDifficultySwitchA() + if diff_a_locked then + local a = emu.getregister("A") + if (a > 128 and a < 128 + 64) or (a < 64) then + emu.setregister("A", a + 64) + end + end +end + +function TryFreeincarnate() + if freeincarnates_available > 0 then + freeincarnates_available = freeincarnates_available - 1 + for index, state_addr in pairs(DragonState) do + if last_dragon_state[index] == 1 then + send_freeincarnate_used = true + memory.write_u8(state_addr, 1, "System Bus") + local msg = {TTL=450, message="used freeincarnate", color=0xFF00FF00} + itemMessages[-1] = msg + end + end + + end +end + +function GetLinkedObject() + if emu.getregister("X") == batRoomAddr then + bat_interest_item = emu.getregister("A") + -- if the bat can't touch that item, we'll switch it to the number item, which should never be + -- in the same room as the bat. + if bat_no_touch_items[bat_interest_item] ~= nil then + emu.setregister("A", 0xDD ) + emu.setregister("Y", 0xDD ) + end + end +end + +function CheckCollectAPItem(carry_item, target_item_value, target_item_ram, rendering_foreign_item) + if( carry_item == target_item_value and rendering_foreign_item ~= nil ) then + memory.write_u8(carryAddress, nullObjectId, "System Bus") + memory.write_u8(target_item_ram, 0xFF, "System Bus") + pending_foreign_items_collected[rendering_foreign_item.short_location_id] = rendering_foreign_item + for index, fi in pairs(foreign_items_by_room[rendering_foreign_item.room_id]) do + if( fi.short_location_id == rendering_foreign_item.short_location_id ) then + table.remove(foreign_items_by_room[rendering_foreign_item.room_id], index) + break + end + end + for index, fi in pairs(foreign_items) do + if( fi.short_location_id == rendering_foreign_item.short_location_id ) then + foreign_items[index] = nil + break + end + end + prev_ap_room_index = 0 + return true + end + return false +end + +function BatCanTouchForeign(foreign_item, bat_room) + if bat_no_touch_locations_by_room[bat_room] == nil or bat_no_touch_locations_by_room[bat_room][1] == nil then + return true + end + + for index, location in ipairs(bat_no_touch_locations_by_room[bat_room]) do + if location.short_location_id == foreign_item.short_location_id then + return false + end + end + return true; +end + +function main() + memory.usememorydomain("System Bus") + if (is23Or24Or25 or is26To28) == false then + print("Must use a version of bizhawk 2.3.1 or higher") + return + end + local playerSlot = memory.read_u8(PlayerSlotAddress) + local port = 17242 + playerSlot + print("Using port"..tostring(port)) + server, error = socket.bind('localhost', port) + if( error ~= nil ) then + print(error) + end + event.onmemoryexecute(SetYorgleSpeed, yorgle_speed_address); + event.onmemoryexecute(SetGrundleSpeed, grundle_speed_address); + event.onmemoryexecute(SetRhindleSpeed, rhindle_speed_address); + event.onmemoryexecute(SetDifficultySwitchA, read_switch_a) + event.onmemoryexecute(SetDifficultySwitchB, read_switch_b) + event.onmemoryexecute(GetLinkedObject, batItemCheckAddr) + -- TODO: Add an onmemoryexecute event to intercept the bat reading item rooms, and don't 'see' an item in the + -- room if it is in bat_no_touch_locations_by_room. Although realistically, I may have to handle this in the rom + -- for it to be totally reliable, because it won't work before the script connects (I might have to reset them?) + -- TODO: Also remove those items from the bat_no_touch_locations_by_room if they have been collected + while true do + frame = frame + 1 + drawMessages() + if not (curstate == prevstate) then + print("Current state: "..curstate) + prevstate = curstate + end + + local current_player_room = u8(PlayerRoomAddr) + local bat_room = u8(batRoomAddr) + local bat_carrying_item = u8(batCarryAddress) + local bat_carrying_ap_item = (BatAPItemRam == bat_carrying_item) + + if current_player_room == 0x1E then + if u8(PlayerRoomAddr + 1) > 0x4B then + memory.write_u8(PlayerRoomAddr + 1, 0x4B) + end + end + + if current_player_room == 0x00 then + if not was_in_number_room then + print("reset "..tostring(bat_carrying_ap_item).." "..tostring(bat_carrying_item)) + memory.write_u8(batCarryAddress, batInvalidCarryItem) + memory.write_u8(batCarryAddress+ 1, 0) + createForeignItemsByRoom() + memory.write_u8(BatAPItemRam, 0xff) + memory.write_u8(APItemRam, 0xff) + prev_ap_room_index = 0 + prev_player_room = 0 + rendering_foreign_item = nil + was_in_number_room = true + end + else + was_in_number_room = false + end + + if bat_room ~= prev_bat_room then + if bat_carrying_ap_item then + if foreign_items_by_room[prev_bat_room] ~= nil then + for r,f in pairs(foreign_items_by_room[prev_bat_room]) do + if f.short_location_id == current_bat_ap_item.short_location_id then + -- print("removing item from "..tostring(r).." in "..tostring(prev_bat_room)) + table.remove(foreign_items_by_room[prev_bat_room], r) + break + end + end + end + if foreign_items_by_room[bat_room] == nil then + foreign_items_by_room[bat_room] = {} + end + -- print("adding item to "..tostring(bat_room)) + table.insert(foreign_items_by_room[bat_room], current_bat_ap_item) + else + -- set AP item room and position for new room, or to invalid room + if foreign_items_by_room[bat_room] ~= nil and foreign_items_by_room[bat_room][1] ~= nil + and BatCanTouchForeign(foreign_items_by_room[bat_room][1], bat_room) then + if current_bat_ap_item ~= foreign_items_by_room[bat_room][1] then + current_bat_ap_item = foreign_items_by_room[bat_room][1] + -- print("Changing bat item to "..tostring(current_bat_ap_item.short_location_id)) + end + memory.write_u8(BatAPItemRam, bat_room) + memory.write_u8(BatAPItemRam + 1, current_bat_ap_item.room_x) + memory.write_u8(BatAPItemRam + 2, current_bat_ap_item.room_y) + else + memory.write_u8(BatAPItemRam, 0xff) + if current_bat_ap_item ~= nil then + -- print("clearing bat item") + end + current_bat_ap_item = nil + end + end + end + prev_bat_room = bat_room + + -- update foreign_items_by_room position and room id for bat item if bat carrying an item + if bat_carrying_ap_item then + -- this is setting the item using the bat's position, which is somewhat wrong, but I think + -- there will be more problems with the room not matching sometimes if I use the actual item position + current_bat_ap_item.room_id = bat_room + current_bat_ap_item.room_x = u8(batRoomAddr + 1) + current_bat_ap_item.room_y = u8(batRoomAddr + 2) + end + + if (alive_mode()) then + if (current_player_room ~= prev_player_room) then + memory.write_u8(APItemRam, 0xFF, "System Bus") + prev_ap_room_index = 0 + prev_player_room = current_player_room + AutocollectFromRoom() + end + local carry_item = memory.read_u8(carryAddress, "System Bus") + bat_no_touch_items[carry_item] = nil + if (next_inventory_item ~= nil) then + if ( carry_item == nullObjectId and last_carry_item == nullObjectId ) then + frames_with_no_item = frames_with_no_item + 1 + if (frames_with_no_item > 10) then + frames_with_no_item = 10 + local input_value = memory.read_u8(input_button_address, "System Bus") + if( input_value >= 64 and input_value < 128 ) then -- high bit clear, second highest bit set + memory.write_u8(carryAddress, next_inventory_item) + local item_ram_location = memory.read_u8(ItemTableStart + next_inventory_item) + if( memory.read_u8(batCarryAddress) ~= 0x78 and + memory.read_u8(batCarryAddress) == item_ram_location) then + memory.write_u8(batCarryAddress, batInvalidCarryItem) + memory.write_u8(batCarryAddress+ 1, 0) + memory.write_u8(item_ram_location, current_player_room) + memory.write_u8(item_ram_location + 1, memory.read_u8(PlayerRoomAddr + 1)) + memory.write_u8(item_ram_location + 2, memory.read_u8(PlayerRoomAddr + 2)) + end + ItemIndex = ItemIndex + 1 + next_inventory_item = nil + end + end + else + frames_with_no_item = 0 + end + end + if( carry_item ~= last_carry_item ) then + if ( localItemLocations ~= nil and localItemLocations[tostring(carry_item)] ~= nil ) then + pending_local_items_collected[localItemLocations[tostring(carry_item)]] = + localItemLocations[tostring(carry_item)] + table.remove(localItemLocations, tostring(carry_item)) + skip_inventory_items[carry_item] = carry_item + end + end + last_carry_item = carry_item + + CheckCollectAPItem(carry_item, APItemValue, APItemRam, rendering_foreign_item) + if CheckCollectAPItem(carry_item, BatAPItemValue, BatAPItemRam, current_bat_ap_item) and bat_carrying_ap_item then + memory.write_u8(batCarryAddress, batInvalidCarryItem) + memory.write_u8(batCarryAddress+ 1, 0) + end + + + rendering_foreign_item = nil + if( foreign_items_by_room[current_player_room] ~= nil ) then + if( foreign_items_by_room[current_player_room][prev_ap_room_index] ~= nil ) and memory.read_u8(APItemRam) ~= 0xff then + foreign_items_by_room[current_player_room][prev_ap_room_index].room_x = memory.read_u8(APItemRam + 1) + foreign_items_by_room[current_player_room][prev_ap_room_index].room_y = memory.read_u8(APItemRam + 2) + end + prev_ap_room_index = prev_ap_room_index + 1 + local invalid_index = -1 + if( foreign_items_by_room[current_player_room][prev_ap_room_index] == nil ) then + prev_ap_room_index = 1 + end + if( foreign_items_by_room[current_player_room][prev_ap_room_index] ~= nil and current_bat_ap_item ~= nil and + foreign_items_by_room[current_player_room][prev_ap_room_index].short_location_id == current_bat_ap_item.short_location_id) then + invalid_index = prev_ap_room_index + prev_ap_room_index = prev_ap_room_index + 1 + if( foreign_items_by_room[current_player_room][prev_ap_room_index] == nil ) then + prev_ap_room_index = 1 + end + end + + if( foreign_items_by_room[current_player_room][prev_ap_room_index] ~= nil and prev_ap_room_index ~= invalid_index ) then + memory.write_u8(APItemRam, current_player_room) + rendering_foreign_item = foreign_items_by_room[current_player_room][prev_ap_room_index] + memory.write_u8(APItemRam + 1, rendering_foreign_item.room_x) + memory.write_u8(APItemRam + 2, rendering_foreign_item.room_y) + else + memory.write_u8(APItemRam, 0xFF, "System Bus") + end + end + if is_dead == 0 then + dragons_revived = false + player_dead = false + new_dragon_state = {0,0,0} + for index, dragon_state_addr in pairs(DragonState) do + new_dragon_state[index] = memory.read_u8(dragon_state_addr, "System Bus" ) + if last_dragon_state[index] == 1 and new_dragon_state[index] ~= 1 then + dragons_revived = true + elseif last_dragon_state[index] ~= 1 and new_dragon_state[index] == 1 then + dragon_real_index = index - 1 + print("Killed dragon: "..tostring(dragon_real_index)) + local dragon_item = {} + dragon_item["short_location_id"] = 0xD0 + dragon_real_index + pending_foreign_items_collected[dragon_item.short_location_id] = dragon_item + end + if new_dragon_state[index] == 2 then + player_dead = true + end + end + if dragons_revived and player_dead == false then + TryFreeincarnate() + end + last_dragon_state = new_dragon_state + end + elseif (u8(PlayerRoomAddr) == 0x00) then -- not alive mode, in number room + ItemIndex = 0 -- reset our inventory + next_inventory_item = nil + skip_inventory_items = {} + end + if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then + if (frame % 5 == 0) then + receive() + if alive_mode() then + local was_dead = is_dead + is_dead = 0 + for index, dragonStateAddr in pairs(DragonState) do + local dragonstateval = memory.read_u8(dragonStateAddr, "System Bus") + if ( dragonstateval == 2) then + is_dead = index + end + end + if was_dead ~= 0 and is_dead == 0 then + TryFreeincarnate() + end + if deathlink_rec == true and is_dead == 0 then + print("setting dead from deathlink") + deathlink_rec = false + deathlink_sent = true + is_dead = 1 + memory.write_u8(carryAddress, nullObjectId, "System Bus") + memory.write_u8(DragonState[1], 2, "System Bus") + end + if (is_dead > 0 and deathlink_send == 0 and not deathlink_sent) then + deathlink_send = is_dead + print("setting deathlink_send to "..tostring(is_dead)) + elseif (is_dead == 0) then + deathlink_send = 0 + deathlink_sent = false + end + if ItemsReceived ~= nil and ItemsReceived[ItemIndex + 1] ~= nil then + while ItemsReceived[ItemIndex + 1] ~= nil and skip_inventory_items[ItemsReceived[ItemIndex + 1]] ~= nil do + print("skip") + ItemIndex = ItemIndex + 1 + end + local static_id = ItemsReceived[ItemIndex + 1] + if static_id ~= nil then + inventory[static_id] = 1 + if next_inventory_item == nil then + next_inventory_item = static_id + end + end + end + end + end + elseif (curstate == STATE_UNINITIALIZED) then + if (frame % 60 == 0) then + + print("Waiting for client.") + + emu.frameadvance() + server:settimeout(2) + print("Attempting to connect") + local client, timeout = server:accept() + if timeout == nil then + print("Initial connection made") + curstate = STATE_INITIAL_CONNECTION_MADE + atariSocket = client + atariSocket:settimeout(0) + end + end + end + emu.frameadvance() + end +end + +main() diff --git a/data/lua/ADVENTURE/json.lua b/data/lua/ADVENTURE/json.lua new file mode 100644 index 0000000000..0833bf6fb4 --- /dev/null +++ b/data/lua/ADVENTURE/json.lua @@ -0,0 +1,380 @@ +-- +-- json.lua +-- +-- Copyright (c) 2015 rxi +-- +-- This library is free software; you can redistribute it and/or modify it +-- under the terms of the MIT license. See LICENSE for details. +-- + +local json = { _version = "0.1.0" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\\\", + [ "\"" ] = "\\\"", + [ "\b" ] = "\\b", + [ "\f" ] = "\\f", + [ "\n" ] = "\\n", + [ "\r" ] = "\\r", + [ "\t" ] = "\\t", +} + +local escape_char_map_inv = { [ "\\/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return escape_char_map[c] or string.format("\\u%04x", c:byte()) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if val[1] ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + --local line_count = 1 + --local col_count = 1 + --for i = 1, idx - 1 do + -- col_count = col_count + 1 + -- if str:sub(i, i) == "\n" then + -- line_count = line_count + 1 + -- col_count = 1 + -- end + -- end + -- emu.message( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(3, 6), 16 ) + local n2 = tonumber( s:sub(9, 12), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local has_unicode_escape = false + local has_surrogate_escape = false + local has_escape = false + local last + for j = i + 1, #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + end + + if last == 92 then -- "\\" (escape char) + if x == 117 then -- "u" (unicode escape sequence) + local hex = str:sub(j + 1, j + 5) + if not hex:find("%x%x%x%x") then + decode_error(str, j, "invalid unicode escape in string") + end + if hex:find("^[dD][89aAbB]") then + has_surrogate_escape = true + else + has_unicode_escape = true + end + else + local c = string.char(x) + if not escape_chars[c] then + decode_error(str, j, "invalid escape char '" .. c .. "' in string") + end + has_escape = true + end + last = nil + + elseif x == 34 then -- '"' (end of string) + local s = str:sub(i + 1, j - 1) + if has_surrogate_escape then + s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape) + end + if has_unicode_escape then + s = s:gsub("\\u....", parse_unicode_escape) + end + if has_escape then + s = s:gsub("\\.", escape_char_map_inv) + end + return s, j + 1 + + else + last = x + end + end + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + return ( parse(str, next_char(str, 1, space_chars, true)) ) +end + + +return json \ No newline at end of file diff --git a/data/lua/ADVENTURE/socket.lua b/data/lua/ADVENTURE/socket.lua new file mode 100644 index 0000000000..a98e952115 --- /dev/null +++ b/data/lua/ADVENTURE/socket.lua @@ -0,0 +1,132 @@ +----------------------------------------------------------------------------- +-- LuaSocket helper module +-- Author: Diego Nehab +-- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $ +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module and import dependencies +----------------------------------------------------------------------------- +local base = _G +local string = require("string") +local math = require("math") +local socket = require("socket.core") +module("socket") + +----------------------------------------------------------------------------- +-- Exported auxiliar functions +----------------------------------------------------------------------------- +function connect(address, port, laddress, lport) + local sock, err = socket.tcp() + if not sock then return nil, err end + if laddress then + local res, err = sock:bind(laddress, lport, -1) + if not res then return nil, err end + end + local res, err = sock:connect(address, port) + if not res then return nil, err end + return sock +end + +function bind(host, port, backlog) + local sock, err = socket.tcp() + if not sock then return nil, err end + sock:setoption("reuseaddr", true) + local res, err = sock:bind(host, port) + if not res then return nil, err end + res, err = sock:listen(backlog) + if not res then return nil, err end + return sock +end + +try = newtry() + +function choose(table) + return function(name, opt1, opt2) + if base.type(name) ~= "string" then + name, opt1, opt2 = "default", name, opt1 + end + local f = table[name or "nil"] + if not f then base.error("unknown key (".. base.tostring(name) ..")", 3) + else return f(opt1, opt2) end + end +end + +----------------------------------------------------------------------------- +-- Socket sources and sinks, conforming to LTN12 +----------------------------------------------------------------------------- +-- create namespaces inside LuaSocket namespace +sourcet = {} +sinkt = {} + +BLOCKSIZE = 2048 + +sinkt["close-when-done"] = function(sock) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function(self, chunk, err) + if not chunk then + sock:close() + return 1 + else return sock:send(chunk) end + end + }) +end + +sinkt["keep-open"] = function(sock) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function(self, chunk, err) + if chunk then return sock:send(chunk) + else return 1 end + end + }) +end + +sinkt["default"] = sinkt["keep-open"] + +sink = choose(sinkt) + +sourcet["by-length"] = function(sock, length) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function() + if length <= 0 then return nil end + local size = math.min(socket.BLOCKSIZE, length) + local chunk, err = sock:receive(size) + if err then return nil, err end + length = length - string.len(chunk) + return chunk + end + }) +end + +sourcet["until-closed"] = function(sock) + local done + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function() + if done then return nil end + local chunk, err, partial = sock:receive(socket.BLOCKSIZE) + if not err then return chunk + elseif err == "closed" then + sock:close() + done = 1 + return partial + else return nil, err end + end + }) +end + + +sourcet["default"] = sourcet["until-closed"] + +source = choose(sourcet) diff --git a/host.yaml b/host.yaml index 832662711f..4a86d054df 100644 --- a/host.yaml +++ b/host.yaml @@ -168,3 +168,22 @@ zillion_options: # You have to know the path to the emulator core library on the user's computer. rom_start: "retroarch" +adventure_options: + # File name of the standard NTSC Adventure rom. + # The licensed "The 80 Classic Games" CD-ROM contains this. + # It may also have a .a26 extension + rom_file: "ADVNTURE.BIN" + # Set this to false to never autostart a rom (such as after patching) + # True for operating system default program for '.a26' + # Alternatively, a path to a program to open the .a26 file with (generally EmuHawk for multiworld) + rom_start: true + # Optional, additional args passed into rom_start before the .bin file + # For example, this can be used to autoload the connector script in BizHawk + # (see BizHawk --lua= option) + rom_args: " " + # Set this to true to display item received messages in Emuhawk + display_msgs: true + + + + diff --git a/inno_setup.iss b/inno_setup.iss index 4a4c8927de..57e48b3805 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -87,6 +87,7 @@ Name: "client/sc2"; Description: "Starcraft 2"; Types: full playing Name: "client/wargroove"; Description: "Wargroove"; Types: full playing Name: "client/zl"; Description: "Zillion"; Types: full playing Name: "client/tloz"; Description: "The Legend of Zelda"; Types: full playing +Name: "client/advn"; Description: "Adventure"; Types: full playing Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing [Dirs] @@ -105,6 +106,7 @@ Source: "{code:GetRedROMPath}"; DestDir: "{app}"; DestName: "Pokemon Red (UE) [S Source: "{code:GetBlueROMPath}"; DestDir: "{app}"; DestName: "Pokemon Blue (UE) [S][!].gb"; Flags: external; Components: client/pkmn/blue or generator/pkmn_b Source: "{code:GetLADXROMPath}"; DestDir: "{app}"; DestName: "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc"; Flags: external; Components: client/ladx or generator/ladx Source: "{code:GetTLoZROMPath}"; DestDir: "{app}"; DestName: "Legend of Zelda, The (U) (PRG0) [!].nes"; Flags: external; Components: client/tloz or generator/tloz +Source: "{code:GetAdvnROMPath}"; DestDir: "{app}"; DestName: "ADVNTURE.BIN"; Flags: external; Components: client/advn Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/sni Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp @@ -128,6 +130,7 @@ Source: "{#source_path}\ArchipelagoStarcraft2Client.exe"; DestDir: "{app}"; Flag Source: "{#source_path}\ArchipelagoZelda1Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/tloz Source: "{#source_path}\ArchipelagoWargrooveClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/wargroove Source: "{#source_path}\ArchipelagoKH2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/kh2 +Source: "{#source_path}\ArchipelagoAdventureClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/advn Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall [Icons] @@ -145,6 +148,7 @@ Name: "{group}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoCh Name: "{group}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Components: client/sc2 Name: "{group}\{#MyAppName} The Legend of Zelda Client"; Filename: "{app}\ArchipelagoZelda1Client.exe"; Components: client/tloz Name: "{group}\{#MyAppName} Kingdom Hearts 2 Client"; Filename: "{app}\ArchipelagoKH2Client.exe"; Components: client/kh2 +Name: "{group}\{#MyAppName} Adventure Client"; Filename: "{app}\ArchipelagoAdventureClient.exe"; Components: client/advn Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Components: server @@ -160,6 +164,7 @@ Name: "{commondesktop}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\Archip Name: "{commondesktop}\{#MyAppName} The Legend of Zelda Client"; Filename: "{app}\ArchipelagoZelda1Client.exe"; Tasks: desktopicon; Components: client/tloz Name: "{commondesktop}\{#MyAppName} Wargroove Client"; Filename: "{app}\ArchipelagoWargrooveClient.exe"; Tasks: desktopicon; Components: client/wargroove Name: "{commondesktop}\{#MyAppName} Kingdom Hearts 2 Client"; Filename: "{app}\ArchipelagoKH2Client.exe"; Tasks: desktopicon; Components: client/kh2 +Name: "{commondesktop}\{#MyAppName} Adventure Client"; Filename: "{app}\ArchipelagoAdventureClient.exe"; Tasks: desktopicon; Components: client/advn [Run] @@ -247,6 +252,11 @@ Root: HKCR; Subkey: "{#MyAppName}tlozpatch"; ValueData: "Arc Root: HKCR; Subkey: "{#MyAppName}tlozpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoZelda1Client.exe,0"; ValueType: string; ValueName: ""; Components: client/tloz Root: HKCR; Subkey: "{#MyAppName}tlozpatch\shell\open\command"; ValueData: """{app}\ArchipelagoZelda1Client.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/tloz +Root: HKCR; Subkey: ".apadvn"; ValueData: "{#MyAppName}advnpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/advn +Root: HKCR; Subkey: "{#MyAppName}advnpatch"; ValueData: "Archipelago Adventure Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/advn +Root: HKCR; Subkey: "{#MyAppName}advnpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoAdventureClient.exe,0"; ValueType: string; ValueName: ""; Components: client/advn +Root: HKCR; Subkey: "{#MyAppName}advnpatch\shell\open\command"; ValueData: """{app}\ArchipelagoAdventureClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/advn + Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: server Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: server Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: ""; Components: server @@ -320,6 +330,9 @@ var LADXROMFilePage: TInputFileWizardPage; var tlozrom: string; var TLoZROMFilePage: TInputFileWizardPage; +var advnrom: string; +var AdvnROMFilePage: TInputFileWizardPage; + function GetSNESMD5OfFile(const rom: string): string; var data: AnsiString; begin @@ -490,6 +503,21 @@ begin '.z64'); end; +function AddA26Page(name: string): TInputFileWizardPage; +begin + Result := + CreateInputFilePage( + wpSelectComponents, + 'Select ROM File', + 'Where is your ' + name + ' located?', + 'Select the file, then click Next.'); + + Result.Add( + 'Location of ROM file:', + 'A2600 ROM files|*.BIN;*.a26|All files|*.*', + '.BIN'); +end; + function NextButtonClick(CurPageID: Integer): Boolean; begin if (assigned(LttPROMFilePage)) and (CurPageID = LttPROMFilePage.ID) then @@ -516,6 +544,8 @@ begin Result := not (LADXROMFilePage.Values[0] = '') else if (assigned(TLoZROMFilePage)) and (CurPageID = TLoZROMFilePage.ID) then Result := not (TLoZROMFilePage.Values[0] = '') + else if (assigned(AdvnROMFilePage)) and (CurPageID = AdvnROMFilePage.ID) then + Result := not (AdvnROMFilePage.Values[0] = '') else Result := True; end; @@ -712,6 +742,22 @@ begin Result := ''; end; +function GetAdvnROMPath(Param: string): string; +begin + if Length(advnrom) > 0 then + Result := advnrom + else if Assigned(AdvnROMFilePage) then + begin + R := CompareStr(GetMD5OfFile(AdvnROMFilePage.Values[0]), '157bddb7192754a45372be196797f284'); + if R <> 0 then + MsgBox('Adventure ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); + + Result := AdvnROMFilePage.Values[0] + end + else + Result := ''; +end; + procedure InitializeWizard(); begin AddOoTRomPage(); @@ -755,10 +801,14 @@ begin l2acrom := CheckRom('Lufia II - Rise of the Sinistrals (USA).sfc', '6efc477d6203ed2b3b9133c1cd9e9c5d'); if Length(l2acrom) = 0 then L2ACROMFilePage:= AddRomPage('Lufia II - Rise of the Sinistrals (USA).sfc'); - + tlozrom := CheckNESROM('Legend of Zelda, The (U) (PRG0) [!].nes', '337bd6f1a1163df31bf2633665589ab0'); if Length(tlozrom) = 0 then TLoZROMFilePage:= AddNESRomPage('Legend of Zelda, The (U) (PRG0) [!].nes'); + + advnrom := CheckSMSRom('ADVNTURE.BIN', '157bddb7192754a45372be196797f284'); + if Length(advnrom) = 0 then + AdvnROMFilePage:= AddA26Page('ADVNTURE.BIN'); end; @@ -789,4 +839,6 @@ begin Result := not (WizardIsComponentSelected('generator/ladx') or WizardIsComponentSelected('client/ladx')); if (assigned(TLoZROMFilePage)) and (PageID = TLoZROMFilePage.ID) then Result := not (WizardIsComponentSelected('generator/tloz') or WizardIsComponentSelected('client/tloz')); + if (assigned(AdvnROMFilePage)) and (PageID = AdvnROMFilePage.ID) then + Result := not (WizardIsComponentSelected('client/advn')); end; diff --git a/worlds/adventure/Items.py b/worlds/adventure/Items.py new file mode 100644 index 0000000000..76d7d6fd8b --- /dev/null +++ b/worlds/adventure/Items.py @@ -0,0 +1,53 @@ +from typing import Optional +from BaseClasses import ItemClassification, Item + +base_adventure_item_id = 118000000 + + +class AdventureItem(Item): + def __init__(self, name: str, classification: ItemClassification, code: Optional[int], player: int): + super().__init__(name, classification, code, player) + + +class ItemData: + def __init__(self, id: int, classification: ItemClassification): + self.classification = classification + self.id = None if id is None else id + base_adventure_item_id + self.table_index = id + + +nothing_item_id = base_adventure_item_id + +# base IDs are the index in the static item data table, which is +# not the same order as the items in RAM (but offset 0 is a 16-bit address of +# location of room and position data) +item_table = { + "Yellow Key": ItemData(0xB, ItemClassification.progression_skip_balancing), + "White Key": ItemData(0xC, ItemClassification.progression), + "Black Key": ItemData(0xD, ItemClassification.progression), + "Bridge": ItemData(0xA, ItemClassification.progression), + "Magnet": ItemData(0x11, ItemClassification.progression), + "Sword": ItemData(0x9, ItemClassification.progression), + "Chalice": ItemData(0x10, ItemClassification.progression_skip_balancing), + # Non-ROM Adventure items, managed by lua + "Left Difficulty Switch": ItemData(0x100, ItemClassification.filler), + "Right Difficulty Switch": ItemData(0x101, ItemClassification.filler), + # Can use these instead of 'nothing' + "Freeincarnate": ItemData(0x102, ItemClassification.filler), + # These should only be enabled if fast dragons is on? + "Slow Yorgle": ItemData(0x103, ItemClassification.filler), + "Slow Grundle": ItemData(0x104, ItemClassification.filler), + "Slow Rhindle": ItemData(0x105, ItemClassification.filler), + # this should only be enabled if opted into? For now, I'll just exclude them + "Revive Dragons": ItemData(0x106, ItemClassification.trap), + "nothing": ItemData(0x0, ItemClassification.filler) + # Bat Trap + # Bat Time Out + # "Revive Dragons": ItemData(0x110, ItemClassification.trap) +} + +standard_item_max = item_table["Magnet"].id + + +event_table = { +} \ No newline at end of file diff --git a/worlds/adventure/Locations.py b/worlds/adventure/Locations.py new file mode 100644 index 0000000000..2ef561b1e3 --- /dev/null +++ b/worlds/adventure/Locations.py @@ -0,0 +1,214 @@ +from BaseClasses import Location + +base_location_id = 118000000 + + +class AdventureLocation(Location): + game: str = "Adventure" + + +class WorldPosition: + room_id: int + room_x: int + room_y: int + + def __init__(self, room_id: int, room_x: int = None, room_y: int = None): + self.room_id = room_id + self.room_x = room_x + self.room_y = room_y + + def get_position(self, random): + if self.room_x is None or self.room_y is None: + return random.choice(standard_positions) + else: + return self.room_x, self.room_y + + +class LocationData: + def __init__(self, region, name, location_id, world_positions: [WorldPosition] = None, event=False, + needs_bat_logic: bool = False): + self.region: str = region + self.name: str = name + self.world_positions: [WorldPosition] = world_positions + self.room_id: int = None + self.room_x: int = None + self.room_y: int = None + self.location_id: int = location_id + if location_id is None: + self.short_location_id: int = None + self.location_id: int = None + else: + self.short_location_id: int = location_id + self.location_id: int = location_id + base_location_id + self.event: bool = event + if world_positions is None and not event: + self.room_id: int = self.short_location_id + self.needs_bat_logic: int = needs_bat_logic + self.local_item: int = None + + def get_position(self, random): + if self.world_positions is None or len(self.world_positions) == 0: + if self.room_id is None: + return None + self.room_x, self.room_y = random.choice(standard_positions) + if self.room_id is None: + selected_pos = random.choice(self.world_positions) + self.room_id = selected_pos.room_id + self.room_x, self.room_y = selected_pos.get_position(random) + return self.room_x, self.room_y + + def get_room_id(self, random): + if self.world_positions is None or len(self.world_positions) == 0: + return None + if self.room_id is None: + selected_pos = random.choice(self.world_positions) + self.room_id = selected_pos.room_id + self.room_x, self.room_y = selected_pos.get_position(random) + return self.room_id + + +standard_positions = [ + (0x80, 0x20), + (0x20, 0x20), + (0x20, 0x40), + (0x20, 0x40), + (0x30, 0x20) +] + + +# Gives the most difficult region the dragon can reach and get stuck in from the provided room without the +# player unlocking something for it +def dragon_room_to_region(room: int) -> str: + if room <= 0x11: + return "Overworld" + elif room <= 0x12: + return "YellowCastle" + elif room <= 0x16 or room == 0x1B: + return "BlackCastle" + elif room <= 0x1A: + return "WhiteCastleVault" + elif room <= 0x1D: + return "Overworld" + elif room <= 0x1E: + return "CreditsRoom" + + +def get_random_room_in_regions(regions: [str], random) -> int: + possible_rooms = {} + for locname in location_table: + if location_table[locname].region in regions: + room = location_table[locname].get_room_id(random) + if room is not None: + possible_rooms[room] = location_table[locname].room_id + return random.choice(list(possible_rooms.keys())) + + +location_table = { + "Blue Labyrinth 0": LocationData("Overworld", "Blue Labyrinth 0", 0x4, + [WorldPosition(0x4, 0x83, 0x47), # exit upper right + WorldPosition(0x4, 0x12, 0x47), # exit upper left + WorldPosition(0x4, 0x65, 0x20), # exit bottom right + WorldPosition(0x4, 0x2A, 0x20), # exit bottom left + WorldPosition(0x5, 0x4B, 0x60), # T room, top + WorldPosition(0x5, 0x28, 0x1F), # T room, bottom left + WorldPosition(0x5, 0x70, 0x1F), # T room, bottom right + ]), + "Blue Labyrinth 1": LocationData("Overworld", "Blue Labyrinth 1", 0x6, + [WorldPosition(0x6, 0x8C, 0x20), # final turn bottom right + WorldPosition(0x6, 0x03, 0x20), # final turn bottom left + WorldPosition(0x6, 0x4B, 0x30), # final turn center + WorldPosition(0x7, 0x4B, 0x40), # straightaway center + WorldPosition(0x8, 0x40, 0x40), # entrance middle loop + WorldPosition(0x8, 0x4B, 0x60), # entrance upper loop + WorldPosition(0x8, 0x8C, 0x5E), # entrance right loop + ]), + "Catacombs": LocationData("Overworld", "Catacombs", 0x9, + [WorldPosition(0x9, 0x49, 0x40), + WorldPosition(0x9, 0x4b, 0x20), + WorldPosition(0xA), + WorldPosition(0xA), + WorldPosition(0xB, 0x40, 0x40), + WorldPosition(0xB, 0x22, 0x1f), + WorldPosition(0xB, 0x70, 0x1f)]), + "Adjacent to Catacombs": LocationData("Overworld", "Adjacent to Catacombs", 0xC, + [WorldPosition(0xC), + WorldPosition(0xD)]), + "Southwest of Catacombs": LocationData("Overworld", "Southwest of Catacombs", 0xE), + "White Castle Gate": LocationData("Overworld", "White Castle Gate", 0xF), + "Black Castle Gate": LocationData("Overworld", "Black Castle Gate", 0x10), + "Yellow Castle Gate": LocationData("Overworld", "Yellow Castle Gate", 0x11), + "Inside Yellow Castle": LocationData("YellowCastle", "Inside Yellow Castle", 0x12), + "Dungeon0": LocationData("BlackCastle", "Dungeon0", 0x13, + [WorldPosition(0x13), + WorldPosition(0x14)]), + "Dungeon Vault": LocationData("BlackCastleVault", "Dungeon Vault", 0xB5, + [WorldPosition(0x15, 0x46, 0x1B)], + needs_bat_logic=True), + "Dungeon1": LocationData("BlackCastle", "Dungeon1", 0x15, + [WorldPosition(0x15), + WorldPosition(0x16)]), + "RedMaze0": LocationData("WhiteCastle", "RedMaze0", 0x17, + [WorldPosition(0x17, 0x70, 0x40), # right side third room + WorldPosition(0x17, 0x18, 0x40), # left side third room + WorldPosition(0x18, 0x20, 0x40), + WorldPosition(0x18, 0x1A, 0x3F), # left side second room + WorldPosition(0x18, 0x70, 0x3F), # right side second room + ]), + "Red Maze Vault Entrance": LocationData("WhiteCastlePreVaultPeek", "Red Maze Vault Entrance", 0xB7, + [WorldPosition(0x17, 0x50, 0x60)], + needs_bat_logic=True), + "Red Maze Vault": LocationData("WhiteCastleVault", "Red Maze Vault", 0x19, + [WorldPosition(0x19, 0x4E, 0x35)], + needs_bat_logic=True), + "RedMaze1": LocationData("WhiteCastle", "RedMaze1", 0x1A), # entrance + "Black Castle Foyer": LocationData("BlackCastle", "Black Castle Foyer", 0x1B), + "Northeast of Catacombs": LocationData("Overworld", "Northeast of Catacombs", 0x1C), + "Southeast of Catacombs": LocationData("Overworld", "Southeast of Catacombs", 0x1D), + "Credits Left Side": LocationData("CreditsRoom", "Credits Left Side", 0x1E, + [WorldPosition(0x1E, 0x25, 0x50)]), + "Credits Right Side": LocationData("CreditsRoomFarSide", "Credits Right Side", 0xBE, + [WorldPosition(0x1E, 0x70, 0x40)], + needs_bat_logic=True), + "Chalice Home": LocationData("YellowCastle", "Chalice Home", None, event=True), + "Slay Yorgle": LocationData("Varies", "Slay Yorgle", 0xD1, event=False), + "Slay Grundle": LocationData("Varies", "Slay Grundle", 0xD2, event=False), + "Slay Rhindle": LocationData("Varies", "Slay Rhindle", 0xD0, event=False), +} + +# the old location table, for reference +location_table_old = { + "Blue Labyrinth 0": LocationData("Overworld", "Blue Labyrinth 0", 0x4), + "Blue Labyrinth 1": LocationData("Overworld", "Blue Labyrinth 1", 0x5), + "Blue Labyrinth 2": LocationData("Overworld", "Blue Labyrinth 2", 0x6), + "Blue Labyrinth 3": LocationData("Overworld", "Blue Labyrinth 3", 0x7), + "Blue Labyrinth 4": LocationData("Overworld", "Blue Labyrinth 4", 0x8), + "Catacombs0": LocationData("Overworld", "Catacombs0", 0x9), + "Catacombs1": LocationData("Overworld", "Catacombs1", 0xA), + "Catacombs2": LocationData("Overworld", "Catacombs2", 0xB), + "East of Catacombs": LocationData("Overworld", "East of Catacombs", 0xC), + "West of Catacombs": LocationData("Overworld", "West of Catacombs", 0xD), + "Southwest of Catacombs": LocationData("Overworld", "Southwest of Catacombs", 0xE), + "White Castle Gate": LocationData("Overworld", "White Castle Gate", 0xF), + "Black Castle Gate": LocationData("Overworld", "Black Castle Gate", 0x10), + "Yellow Castle Gate": LocationData("Overworld", "Yellow Castle Gate", 0x11), + "Inside Yellow Castle": LocationData("YellowCastle", "Inside Yellow Castle", 0x12), + "Dungeon0": LocationData("BlackCastle", "Dungeon0", 0x13), + "Dungeon1": LocationData("BlackCastle", "Dungeon1", 0x14), + "Dungeon Vault": LocationData("BlackCastleVault", "Dungeon Vault", 0x15, + [WorldPosition(0xB5, 0x46, 0x1B)]), + "Dungeon2": LocationData("BlackCastle", "Dungeon2", 0x15), + "Dungeon3": LocationData("BlackCastle", "Dungeon3", 0x16), + "RedMaze0": LocationData("WhiteCastle", "RedMaze0", 0x17, [WorldPosition(0x17, 0x70, 0x40)]), + "RedMaze1": LocationData("WhiteCastle", "RedMaze1", 0x18, [WorldPosition(0x18, 0x20, 0x40)]), + "Red Maze Vault Entrance": LocationData("WhiteCastlePreVaultPeek", "Red Maze Vault Entrance", + 0x17, [WorldPosition(0xB7, 0x50, 0x60)]), + "Red Maze Vault": LocationData("WhiteCastleVault", "Red Maze Vault", 0x19, [WorldPosition(0x19, 0x4E, 0x35)]), + "RedMaze3": LocationData("WhiteCastle", "RedMaze3", 0x1A), + "Black Castle Foyer": LocationData("BlackCastle", "Black Castle Foyer", 0x1B), + "Northeast of Catacombs": LocationData("Overworld", "Northeast of Catacombs", 0x1C), + "Southeast of Catacombs": LocationData("Overworld", "Southeast of Catacombs", 0x1D), + "Credits Left Side": LocationData("CreditsRoom", "Credits Left Side", 0x1E, [WorldPosition(0x1E, 0x25, 0x50)]), + "Credits Right Side": LocationData("CreditsRoomFarSide", "Credits Right Side", 0x1E, + [WorldPosition(0xBE, 0x70, 0x40)]), + "Chalice Home": LocationData("YellowCastle", "Chalice Home", None, event=True) +} diff --git a/worlds/adventure/Offsets.py b/worlds/adventure/Offsets.py new file mode 100644 index 0000000000..c1e74aee9b --- /dev/null +++ b/worlds/adventure/Offsets.py @@ -0,0 +1,46 @@ +# probably I should generate this from the list file + +static_item_data_location = 0xe9d +static_item_element_size = 9 +static_first_dragon_index = 6 +item_position_table = 0x402 +items_ram_start = 0xa1 +connector_port_offset = 0xff9 +# dragon speeds are hardcoded directly in their respective movement subroutines, not in their item table or state data +# so this is the second byte of an LDA immediate instruction +yorgle_speed_data_location = 0x724 +grundle_speed_data_location = 0x73f +rhindle_speed_data_location = 0x709 + + +# in case I need to place a rom address in the rom +rom_address_space_start = 0xf000 + +start_castle_offset = 0x39c +start_castle_values = [0x11, 0x10, 0x0F] +"""yellow, black, white castle gate rooms""" + +# indexed by static item table index. 0x00 indicates the position data is in ROM and is irrelevant to the randomizer +item_ram_addresses = [ + 0xD9, # lamp + 0x00, # portcullis 1 + 0x00, # portcullis 2 + 0x00, # portcullis 3 + 0x00, # author name + 0x00, # GO object + 0xA4, # Rhindle + 0xA9, # Yorgle + 0xAE, # Grundle + 0xB6, # Sword + 0xBC, # Bridge + 0xBF, # Yellow Key + 0xC2, # White key + 0xC5, # Black key + 0xCB, # Bat + 0xA1, # Dot + 0xB9, # Chalice + 0xB3, # Magnet + 0xE7, # AP object 1 + 0xEA, # AP bat object + 0xBC, # NULL object (end of table) +] diff --git a/worlds/adventure/Options.py b/worlds/adventure/Options.py new file mode 100644 index 0000000000..a8016fc287 --- /dev/null +++ b/worlds/adventure/Options.py @@ -0,0 +1,244 @@ +from __future__ import annotations + +from typing import Dict + +from Options import Choice, Option, DefaultOnToggle, DeathLink, Range, Toggle + + +class FreeincarnateMax(Range): + """How many maximum freeincarnate items to allow + + When done generating items, any remaining item slots will be filled + with freeincarnates, up to this maximum amount. Any remaining item + slots after that will be 'nothing' items placed locally, so in multigame + multiworlds, keeping this value high will allow more items from other games + into Adventure. + """ + display_name = "Freeincarnate Maximum" + range_start = 0 + range_end = 17 + default = 17 + + +class ItemRandoType(Choice): + """Choose how items are placed in the game + + Not yet implemented. Currently only traditional supported + Traditional: Adventure items are not in the map until + they are collected (except local items) and are dropped + on the player when collected. Adventure items are not checks. + Inactive: Every item is placed, but is inactive until collected. + Each item touched is a check. The bat ignores inactive items. + + Supported values: traditional, inactive + Default value: traditional + """ + + display_name = "Item type" + option_traditional = 0x00 + option_inactive = 0x01 + default = option_traditional + + +class DragonSlayCheck(DefaultOnToggle): + """If true, slaying each dragon for the first time is a check + """ + display_name = "Slay Dragon Checks" + + +class TrapBatCheck(Choice): + """ + Locking the bat inside a castle may be a check + + Not yet implemented + If set to yes, the bat will not start inside a castle. + Setting with_key requires the matching castle key to also be + in the castle with the bat, achieved by dropping the key in the + path of the portcullis as it falls. This setting is not recommended with the bat use_logic setting + + Supported values: no, yes, with_key + Default value: yes + """ + display_name = "Trap bat check" + option_no_check = 0x0 + option_yes_key_optional = 0x1 + option_with_key = 0x2 + default = option_yes_key_optional + + +class DragonRandoType(Choice): + """ + How to randomize the dragon starting locations + + normal: Grundle is in the overworld, Yorgle in the white castle, and Rhindle in the black castle + shuffle: A random dragon is placed in the overworld, one in the white castle, and one in the black castle + overworldplus: Dragons can be placed anywhere, but at least one will be in the overworld + randomized: Dragons can be anywhere except the credits room + + + Supported values: normal, shuffle, overworldplus, randomized + Default value: shuffle + """ + display_name = "Dragon Randomization" + option_normal = 0x0 + option_shuffle = 0x1 + option_overworldplus = 0x2 + option_randomized = 0x3 + default = option_shuffle + + +class BatLogic(Choice): + """How the bat is considered for logic + + With cannot_break, the bat cannot pick up an item that starts out-of-logic until the player touches it + With can_break, the bat is free to pick up any items, even if they are out-of-logic + With use_logic, the bat can pick up anything just like can_break, and locations are no longer considered to require + the magnet or bridge to collect, since the bat can retrieve these. + A future option may allow the bat itself to be placed as an item. + + Supported values: cannot_break, can_break, use_logic + Default value: can_break + """ + display_name = "Bat Logic" + option_cannot_break = 0x0 + option_can_break = 0x1 + option_use_logic = 0x2 + default = option_can_break + + +class YorgleStartingSpeed(Range): + """ + Sets Yorgle's initial speed. Yorgle has a speed of 2 in the original game + Default value: 2 + """ + display_name = "Yorgle MaxSpeed" + range_start = 1 + range_end = 9 + default = 2 + + +class YorgleMinimumSpeed(Range): + """ + Sets Yorgle's speed when all speed reducers are found. Yorgle has a speed of 2 in the original game + Default value: 2 + """ + display_name = "Yorgle Min Speed" + range_start = 1 + range_end = 9 + default = 1 + + +class GrundleStartingSpeed(Range): + """ + Sets Grundle's initial speed. Grundle has a speed of 2 in the original game + Default value: 2 + """ + display_name = "Grundle MaxSpeed" + range_start = 1 + range_end = 9 + default = 2 + + +class GrundleMinimumSpeed(Range): + """ + Sets Grundle's speed when all speed reducers are found. Grundle has a speed of 2 in the original game + Default value: 2 + """ + display_name = "Grundle Min Speed" + range_start = 1 + range_end = 9 + default = 1 + + +class RhindleStartingSpeed(Range): + """ + Sets Rhindle's initial speed. Rhindle has a speed of 3 in the original game + Default value: 3 + """ + display_name = "Rhindle MaxSpeed" + range_start = 1 + range_end = 9 + default = 3 + + +class RhindleMinimumSpeed(Range): + """ + Sets Rhindle's speed when all speed reducers are found. Rhindle has a speed of 3 in the original game + Default value: 2 + """ + display_name = "Rhindle Min Speed" + range_start = 1 + range_end = 9 + default = 2 + + +class ConnectorMultiSlot(Toggle): + """If true, the client and lua connector will add lowest 8 bits of the player slot + to the port number used to connect to each other, to simplify connecting multiple local + clients to local BizHawks. + Set in the yaml, since the connector has to read this out of the rom file before connecting. + """ + display_name = "Connector Multi-Slot" + + +class DifficultySwitchA(Choice): + """Set availability of left difficulty switch + This controls the speed of the dragons' bite animation + + """ + display_name = "Left Difficulty Switch" + option_normal = 0x0 + option_locked_hard = 0x1 + option_hard_with_unlock_item = 0x2 + default = option_hard_with_unlock_item + + +class DifficultySwitchB(Choice): + """Set availability of right difficulty switch + On hard, dragons will run away from the sword + + """ + display_name = "Right Difficulty Switch" + option_normal = 0x0 + option_locked_hard = 0x1 + option_hard_with_unlock_item = 0x2 + default = option_hard_with_unlock_item + + +class StartCastle(Choice): + """Choose or randomize which castle to start in front of. + + This affects both normal start and reincarnation. Starting + at the black castle may give easy dot runs, while starting + at the white castle may make them more dangerous! Also, not + starting at the yellow castle can make delivering the chalice + with a full inventory slightly less trivial. + + This doesn't affect logic since all the castles are reachable + from each other. + """ + display_name = "Start Castle" + option_yellow = 0 + option_black = 1 + option_white = 2 + default = option_yellow + + +adventure_option_definitions: Dict[str, type(Option)] = { + "dragon_slay_check": DragonSlayCheck, + "death_link": DeathLink, + "bat_logic": BatLogic, + "freeincarnate_max": FreeincarnateMax, + "dragon_rando_type": DragonRandoType, + "connector_multi_slot": ConnectorMultiSlot, + "yorgle_speed": YorgleStartingSpeed, + "yorgle_min_speed": YorgleMinimumSpeed, + "grundle_speed": GrundleStartingSpeed, + "grundle_min_speed": GrundleMinimumSpeed, + "rhindle_speed": RhindleStartingSpeed, + "rhindle_min_speed": RhindleMinimumSpeed, + "difficulty_switch_a": DifficultySwitchA, + "difficulty_switch_b": DifficultySwitchB, + "start_castle": StartCastle, + +} \ No newline at end of file diff --git a/worlds/adventure/Regions.py b/worlds/adventure/Regions.py new file mode 100644 index 0000000000..4a62518fbd --- /dev/null +++ b/worlds/adventure/Regions.py @@ -0,0 +1,160 @@ +from BaseClasses import MultiWorld, Region, Entrance, LocationProgressType +from .Locations import location_table, LocationData, AdventureLocation, dragon_room_to_region + + +def connect(world: MultiWorld, player: int, source: str, target: str, rule: callable = lambda state: True, + one_way=False, name=None): + source_region = world.get_region(source, player) + target_region = world.get_region(target, player) + + if name is None: + name = source + " to " + target + + connection = Entrance( + player, + name, + source_region + ) + + connection.access_rule = rule + + source_region.exits.append(connection) + connection.connect(target_region) + if not one_way: + connect(world, player, target, source, rule, True) + + +def create_regions(multiworld: MultiWorld, player: int, dragon_rooms: []) -> None: + for name, locdata in location_table.items(): + locdata.get_position(multiworld.random) + + menu = Region("Menu", player, multiworld) + + menu.exits.append(Entrance(player, "GameStart", menu)) + multiworld.regions.append(menu) + + overworld = Region("Overworld", player, multiworld) + overworld.exits.append(Entrance(player, "YellowCastlePort", overworld)) + overworld.exits.append(Entrance(player, "WhiteCastlePort", overworld)) + overworld.exits.append(Entrance(player, "BlackCastlePort", overworld)) + overworld.exits.append(Entrance(player, "CreditsWall", overworld)) + multiworld.regions.append(overworld) + + yellow_castle = Region("YellowCastle", player, multiworld, "Yellow Castle") + yellow_castle.exits.append(Entrance(player, "YellowCastleExit", yellow_castle)) + multiworld.regions.append(yellow_castle) + + white_castle = Region("WhiteCastle", player, multiworld, "White Castle") + white_castle.exits.append(Entrance(player, "WhiteCastleExit", white_castle)) + white_castle.exits.append(Entrance(player, "WhiteCastleSecretPassage", white_castle)) + white_castle.exits.append(Entrance(player, "WhiteCastlePeekPassage", white_castle)) + multiworld.regions.append(white_castle) + + white_castle_pre_vault_peek = Region("WhiteCastlePreVaultPeek", player, multiworld, "White Castle Secret Peek") + white_castle_pre_vault_peek.exits.append(Entrance(player, "WhiteCastleFromPeek", white_castle_pre_vault_peek)) + multiworld.regions.append(white_castle_pre_vault_peek) + + white_castle_secret_room = Region("WhiteCastleVault", player, multiworld, "White Castle Vault",) + white_castle_secret_room.exits.append(Entrance(player, "WhiteCastleReturnPassage", white_castle_secret_room)) + multiworld.regions.append(white_castle_secret_room) + + black_castle = Region("BlackCastle", player, multiworld, "Black Castle") + black_castle.exits.append(Entrance(player, "BlackCastleExit", black_castle)) + black_castle.exits.append(Entrance(player, "BlackCastleVaultEntrance", black_castle)) + multiworld.regions.append(black_castle) + + black_castle_secret_room = Region("BlackCastleVault", player, multiworld, "Black Castle Vault") + black_castle_secret_room.exits.append(Entrance(player, "BlackCastleReturnPassage", black_castle_secret_room)) + multiworld.regions.append(black_castle_secret_room) + + credits_room = Region("CreditsRoom", player, multiworld, "Credits Room") + credits_room.exits.append(Entrance(player, "CreditsExit", credits_room)) + credits_room.exits.append(Entrance(player, "CreditsToFarSide", credits_room)) + multiworld.regions.append(credits_room) + + credits_room_far_side = Region("CreditsRoomFarSide", player, multiworld, "Credits Far Side") + credits_room_far_side.exits.append(Entrance(player, "CreditsFromFarSide", credits_room_far_side)) + multiworld.regions.append(credits_room_far_side) + + dragon_slay_check = multiworld.dragon_slay_check[player].value + priority_locations = determine_priority_locations(multiworld, dragon_slay_check) + + for name, location_data in location_table.items(): + require_sword = False + if location_data.region == "Varies": + if location_data.name == "Slay Yorgle": + if not dragon_slay_check: + continue + region_name = dragon_room_to_region(dragon_rooms[0]) + elif location_data.name == "Slay Grundle": + if not dragon_slay_check: + continue + region_name = dragon_room_to_region(dragon_rooms[1]) + elif location_data.name == "Slay Rhindle": + if not dragon_slay_check: + continue + region_name = dragon_room_to_region(dragon_rooms[2]) + else: + raise Exception(f"Unknown location region for {location_data.name}") + r = multiworld.get_region(region_name, player) + else: + r = multiworld.get_region(location_data.region, player) + + adventure_loc = AdventureLocation(player, location_data.name, location_data.location_id, r) + if adventure_loc.name in priority_locations: + adventure_loc.progress_type = LocationProgressType.PRIORITY + r.locations.append(adventure_loc) + + # In a tracker and plando-free world, I'd determine unused locations here and not add them. + # But that would cause problems with both plandos and trackers. So I guess I'll stick + # with filling in with 'nothing' in pre_fill. + + # in the future, I may randomize the map some, and that will require moving + # connections to later, probably + + multiworld.get_entrance("GameStart", player) \ + .connect(multiworld.get_region("Overworld", player)) + + multiworld.get_entrance("YellowCastlePort", player) \ + .connect(multiworld.get_region("YellowCastle", player)) + multiworld.get_entrance("YellowCastleExit", player) \ + .connect(multiworld.get_region("Overworld", player)) + + multiworld.get_entrance("WhiteCastlePort", player) \ + .connect(multiworld.get_region("WhiteCastle", player)) + multiworld.get_entrance("WhiteCastleExit", player) \ + .connect(multiworld.get_region("Overworld", player)) + + multiworld.get_entrance("WhiteCastleSecretPassage", player) \ + .connect(multiworld.get_region("WhiteCastleVault", player)) + multiworld.get_entrance("WhiteCastleReturnPassage", player) \ + .connect(multiworld.get_region("WhiteCastle", player)) + multiworld.get_entrance("WhiteCastlePeekPassage", player) \ + .connect(multiworld.get_region("WhiteCastlePreVaultPeek", player)) + multiworld.get_entrance("WhiteCastleFromPeek", player) \ + .connect(multiworld.get_region("WhiteCastle", player)) + + multiworld.get_entrance("BlackCastlePort", player) \ + .connect(multiworld.get_region("BlackCastle", player)) + multiworld.get_entrance("BlackCastleExit", player) \ + .connect(multiworld.get_region("Overworld", player)) + multiworld.get_entrance("BlackCastleVaultEntrance", player) \ + .connect(multiworld.get_region("BlackCastleVault", player)) + multiworld.get_entrance("BlackCastleReturnPassage", player) \ + .connect(multiworld.get_region("BlackCastle", player)) + + multiworld.get_entrance("CreditsWall", player) \ + .connect(multiworld.get_region("CreditsRoom", player)) + multiworld.get_entrance("CreditsExit", player) \ + .connect(multiworld.get_region("Overworld", player)) + + multiworld.get_entrance("CreditsToFarSide", player) \ + .connect(multiworld.get_region("CreditsRoomFarSide", player)) + multiworld.get_entrance("CreditsFromFarSide", player) \ + .connect(multiworld.get_region("CreditsRoom", player)) + + +# Placeholder for adding sets of priority locations at generation, possibly as an option in the future +def determine_priority_locations(world: MultiWorld, dragon_slay_check: bool) -> {}: + priority_locations = {} + return priority_locations diff --git a/worlds/adventure/Rom.py b/worlds/adventure/Rom.py new file mode 100644 index 0000000000..62c4019718 --- /dev/null +++ b/worlds/adventure/Rom.py @@ -0,0 +1,321 @@ +import hashlib +import json +import os +import zipfile +from typing import Optional, Any + +import Utils +from .Locations import AdventureLocation, LocationData +from Utils import OptionsType +from worlds.Files import APDeltaPatch, AutoPatchRegister, APContainer +from itertools import chain + +import bsdiff4 + +ADVENTUREHASH: str = "157bddb7192754a45372be196797f284" + + +class AdventureAutoCollectLocation: + short_location_id: int = 0 + room_id: int = 0 + + def __init__(self, short_location_id: int, room_id: int): + self.short_location_id = short_location_id + self.room_id = room_id + + def get_dict(self): + return { + "short_location_id": self.short_location_id, + "room_id": self.room_id, + } + + +class AdventureForeignItemInfo: + short_location_id: int = 0 + room_id: int = 0 + room_x: int = 0 + room_y: int = 0 + + def __init__(self, short_location_id: int, room_id: int, room_x: int, room_y: int): + self.short_location_id = short_location_id + self.room_id = room_id + self.room_x = room_x + self.room_y = room_y + + def get_dict(self): + return { + "short_location_id": self.short_location_id, + "room_id": self.room_id, + "room_x": self.room_x, + "room_y": self.room_y, + } + + +class BatNoTouchLocation: + short_location_id: int + room_id: int + room_x: int + room_y: int + local_item: int + + def __init__(self, short_location_id: int, room_id: int, room_x: int, room_y: int, local_item: int = None): + self.short_location_id = short_location_id + self.room_id = room_id + self.room_x = room_x + self.room_y = room_y + self.local_item = local_item + + def get_dict(self): + ret_dict = { + "short_location_id": self.short_location_id, + "room_id": self.room_id, + "room_x": self.room_x, + "room_y": self.room_y, + } + if self.local_item is not None: + ret_dict["local_item"] = self.local_item + else: + ret_dict["local_item"] = 255 + return ret_dict + + +class AdventureDeltaPatch(APContainer, metaclass=AutoPatchRegister): + hash = ADVENTUREHASH + game = "Adventure" + patch_file_ending = ".apadvn" + zip_version: int = 2 + + # locations: [], autocollect: [], seed_name: bytes, + def __init__(self, *args: Any, **kwargs: Any) -> None: + patch_only = True + if "autocollect" in kwargs: + patch_only = False + self.foreign_items: [AdventureForeignItemInfo] = [AdventureForeignItemInfo(loc.short_location_id, loc.room_id, loc.room_x, loc.room_y) + for loc in kwargs["locations"]] + + self.autocollect_items: [AdventureAutoCollectLocation] = kwargs["autocollect"] + self.seedName: bytes = kwargs["seed_name"] + self.local_item_locations: {} = kwargs["local_item_locations"] + self.dragon_speed_reducer_info: {} = kwargs["dragon_speed_reducer_info"] + self.diff_a_mode: int = kwargs["diff_a_mode"] + self.diff_b_mode: int = kwargs["diff_b_mode"] + self.bat_logic: int = kwargs["bat_logic"] + self.bat_no_touch_locations: [LocationData] = kwargs["bat_no_touch_locations"] + self.rom_deltas: {int, int} = kwargs["rom_deltas"] + del kwargs["locations"] + del kwargs["autocollect"] + del kwargs["seed_name"] + del kwargs["local_item_locations"] + del kwargs["dragon_speed_reducer_info"] + del kwargs["diff_a_mode"] + del kwargs["diff_b_mode"] + del kwargs["bat_logic"] + del kwargs["bat_no_touch_locations"] + del kwargs["rom_deltas"] + super(AdventureDeltaPatch, self).__init__(*args, **kwargs) + + def write_contents(self, opened_zipfile: zipfile.ZipFile): + super(AdventureDeltaPatch, self).write_contents(opened_zipfile) + # write Delta + opened_zipfile.writestr("zip_version", + self.zip_version.to_bytes(1, "little"), + compress_type=zipfile.ZIP_STORED) + if self.foreign_items is not None: + loc_bytes = [] + for foreign_item in self.foreign_items: + loc_bytes.append(foreign_item.short_location_id) + loc_bytes.append(foreign_item.room_id) + loc_bytes.append(foreign_item.room_x) + loc_bytes.append(foreign_item.room_y) + opened_zipfile.writestr("adventure_locations", + bytes(loc_bytes), + compress_type=zipfile.ZIP_LZMA) + if self.autocollect_items is not None: + loc_bytes = [] + for item in self.autocollect_items: + loc_bytes.append(item.short_location_id) + loc_bytes.append(item.room_id) + opened_zipfile.writestr("adventure_autocollect", + bytes(loc_bytes), + compress_type=zipfile.ZIP_LZMA) + if self.player_name is not None: + opened_zipfile.writestr("player", + self.player_name, # UTF-8 + compress_type=zipfile.ZIP_STORED) + if self.seedName is not None: + opened_zipfile.writestr("seedName", + self.seedName, + compress_type=zipfile.ZIP_STORED) + if self.local_item_locations is not None: + opened_zipfile.writestr("local_item_locations", + json.dumps(self.local_item_locations), + compress_type=zipfile.ZIP_LZMA) + if self.dragon_speed_reducer_info is not None: + opened_zipfile.writestr("dragon_speed_reducer_info", + json.dumps(self.dragon_speed_reducer_info), + compress_type=zipfile.ZIP_LZMA) + if self.diff_a_mode is not None: + opened_zipfile.writestr("diff_a_mode", + self.diff_a_mode.to_bytes(1, "little"), + compress_type=zipfile.ZIP_STORED) + if self.diff_b_mode is not None: + opened_zipfile.writestr("diff_b_mode", + self.diff_b_mode.to_bytes(1, "little"), + compress_type=zipfile.ZIP_STORED) + if self.bat_logic is not None: + opened_zipfile.writestr("bat_logic", + self.bat_logic.to_bytes(1, "little"), + compress_type=zipfile.ZIP_STORED) + if self.bat_no_touch_locations is not None: + loc_bytes = [] + for loc in self.bat_no_touch_locations: + loc_bytes.append(loc.short_location_id) # used for AP items managed by script + loc_bytes.append(loc.room_id) # used for local items placed in rom + loc_bytes.append(loc.room_x) + loc_bytes.append(loc.room_y) + loc_bytes.append(0xff if loc.local_item is None else loc.local_item) + opened_zipfile.writestr("bat_no_touch_locations", + bytes(loc_bytes), + compress_type=zipfile.ZIP_LZMA) + if self.rom_deltas is not None: + # this is not an efficient way to do this AT ALL, but Adventure's data is so tiny it shouldn't matter + # if you're looking at doing something like this for another game, consider encoding your rom changes + # in a more efficient way + opened_zipfile.writestr("rom_deltas", + json.dumps(self.rom_deltas), + compress_type=zipfile.ZIP_LZMA) + + def read_contents(self, opened_zipfile: zipfile.ZipFile): + super(AdventureDeltaPatch, self).read_contents(opened_zipfile) + self.foreign_items = AdventureDeltaPatch.read_foreign_items(opened_zipfile) + self.autocollect_items = AdventureDeltaPatch.read_autocollect_items(opened_zipfile) + + @classmethod + def get_source_data(cls) -> bytes: + return get_base_rom_bytes() + + @classmethod + def check_version(cls, opened_zipfile: zipfile.ZipFile) -> bool: + version_bytes = opened_zipfile.read("zip_version") + version = 0 + if version_bytes is not None: + version = int.from_bytes(version_bytes, "little") + if version != cls.zip_version: + return False + return True + + @classmethod + def read_rom_info(cls, opened_zipfile: zipfile.ZipFile) -> (bytes, bytes, str): + seedbytes: bytes = opened_zipfile.read("seedName") + namebytes: bytes = opened_zipfile.read("player") + namestr: str = namebytes.decode("utf-8") + return seedbytes, namestr + + @classmethod + def read_difficulty_switch_info(cls, opened_zipfile: zipfile.ZipFile) -> (int, int): + diff_a_bytes = opened_zipfile.read("diff_a_mode") + diff_b_bytes = opened_zipfile.read("diff_b_mode") + diff_a = 0 + diff_b = 0 + if diff_a_bytes is not None: + diff_a = int.from_bytes(diff_a_bytes, "little") + if diff_b_bytes is not None: + diff_b = int.from_bytes(diff_b_bytes, "little") + return diff_a, diff_b + + @classmethod + def read_bat_logic(cls, opened_zipfile: zipfile.ZipFile) -> int: + bat_logic = opened_zipfile.read("bat_logic") + if bat_logic is None: + return 0 + return int.from_bytes(bat_logic, "little") + + @classmethod + def read_foreign_items(cls, opened_zipfile: zipfile.ZipFile) -> [AdventureForeignItemInfo]: + foreign_items = [] + readbytes: bytes = opened_zipfile.read("adventure_locations") + bytelist = list(readbytes) + for i in range(round(len(bytelist) / 4)): + offset = i * 4 + foreign_items.append(AdventureForeignItemInfo(bytelist[offset], + bytelist[offset + 1], + bytelist[offset + 2], + bytelist[offset + 3])) + return foreign_items + + @classmethod + def read_bat_no_touch(cls, opened_zipfile: zipfile.ZipFile) -> [BatNoTouchLocation]: + locations = [] + readbytes: bytes = opened_zipfile.read("bat_no_touch_locations") + bytelist = list(readbytes) + for i in range(round(len(bytelist) / 5)): + offset = i * 5 + locations.append(BatNoTouchLocation(bytelist[offset], + bytelist[offset + 1], + bytelist[offset + 2], + bytelist[offset + 3], + bytelist[offset + 4])) + return locations + + @classmethod + def read_autocollect_items(cls, opened_zipfile: zipfile.ZipFile) -> [AdventureForeignItemInfo]: + autocollect_items = [] + readbytes: bytes = opened_zipfile.read("adventure_autocollect") + bytelist = list(readbytes) + for i in range(round(len(bytelist) / 2)): + offset = i * 2 + autocollect_items.append(AdventureAutoCollectLocation(bytelist[offset], bytelist[offset + 1])) + return autocollect_items + + @classmethod + def read_local_item_locations(cls, opened_zipfile: zipfile.ZipFile) -> [AdventureForeignItemInfo]: + readbytes: bytes = opened_zipfile.read("local_item_locations") + readstr: str = readbytes.decode() + return json.loads(readstr) + + @classmethod + def read_dragon_speed_info(cls, opened_zipfile: zipfile.ZipFile) -> {}: + readbytes: bytes = opened_zipfile.read("dragon_speed_reducer_info") + readstr: str = readbytes.decode() + return json.loads(readstr) + + @classmethod + def read_rom_deltas(cls, opened_zipfile: zipfile.ZipFile) -> {int, int}: + readbytes: bytes = opened_zipfile.read("rom_deltas") + readstr: str = readbytes.decode() + return json.loads(readstr) + + @classmethod + def apply_rom_deltas(cls, base_bytes: bytes, rom_deltas: {int, int}) -> bytearray: + rom_bytes = bytearray(base_bytes) + for offset, value in rom_deltas.items(): + int_offset = int(offset) + rom_bytes[int_offset:int_offset+1] = int.to_bytes(value, 1, "little") + return rom_bytes + + +def apply_basepatch(base_rom_bytes: bytes) -> bytes: + with open(os.path.join(os.path.dirname(__file__), "../../data/adventure_basepatch.bsdiff4"), "rb") as basepatch: + delta: bytes = basepatch.read() + return bsdiff4.patch(base_rom_bytes, delta) + + +def get_base_rom_bytes(file_name: str = "") -> bytes: + file_name = get_base_rom_path(file_name) + with open(file_name, "rb") as file: + base_rom_bytes = bytes(file.read()) + basemd5 = hashlib.md5() + basemd5.update(base_rom_bytes) + if ADVENTUREHASH != basemd5.hexdigest(): + raise Exception(f"Supplied Base Rom does not match known MD5 for Adventure. " + "Get the correct game and version, then dump it") + return base_rom_bytes + + +def get_base_rom_path(file_name: str = "") -> str: + options: OptionsType = Utils.get_options() + if not file_name: + file_name = options["adventure_options"]["rom_file"] + if not os.path.exists(file_name): + file_name = Utils.user_path(file_name) + return file_name diff --git a/worlds/adventure/Rules.py b/worlds/adventure/Rules.py new file mode 100644 index 0000000000..6f4b53faa1 --- /dev/null +++ b/worlds/adventure/Rules.py @@ -0,0 +1,98 @@ +from worlds.adventure import location_table +from worlds.adventure.Options import BatLogic, DifficultySwitchB, DifficultySwitchA +from worlds.generic.Rules import add_rule, set_rule, forbid_item +from BaseClasses import LocationProgressType + + +def set_rules(self) -> None: + world = self.multiworld + use_bat_logic = world.bat_logic[self.player].value == BatLogic.option_use_logic + + set_rule(world.get_entrance("YellowCastlePort", self.player), + lambda state: state.has("Yellow Key", self.player)) + set_rule(world.get_entrance("BlackCastlePort", self.player), + lambda state: state.has("Black Key", self.player)) + set_rule(world.get_entrance("WhiteCastlePort", self.player), + lambda state: state.has("White Key", self.player)) + + # a future thing would be to make the bat an actual item, or at least allow it to + # be placed in a castle, which would require some additions to the rules when + # use_bat_logic is true + if not use_bat_logic: + set_rule(world.get_entrance("WhiteCastleSecretPassage", self.player), + lambda state: state.has("Bridge", self.player)) + set_rule(world.get_entrance("WhiteCastlePeekPassage", self.player), + lambda state: state.has("Bridge", self.player) or + state.has("Magnet", self.player)) + set_rule(world.get_entrance("BlackCastleVaultEntrance", self.player), + lambda state: state.has("Bridge", self.player) or + state.has("Magnet", self.player)) + + dragon_slay_check = world.dragon_slay_check[self.player].value + if dragon_slay_check: + if self.difficulty_switch_b == DifficultySwitchB.option_hard_with_unlock_item: + set_rule(world.get_location("Slay Yorgle", self.player), + lambda state: state.has("Sword", self.player) and + state.has("Right Difficulty Switch", self.player)) + set_rule(world.get_location("Slay Grundle", self.player), + lambda state: state.has("Sword", self.player) and + state.has("Right Difficulty Switch", self.player)) + set_rule(world.get_location("Slay Rhindle", self.player), + lambda state: state.has("Sword", self.player) and + state.has("Right Difficulty Switch", self.player)) + else: + set_rule(world.get_location("Slay Yorgle", self.player), + lambda state: state.has("Sword", self.player)) + set_rule(world.get_location("Slay Grundle", self.player), + lambda state: state.has("Sword", self.player)) + set_rule(world.get_location("Slay Rhindle", self.player), + lambda state: state.has("Sword", self.player)) + + # really this requires getting the dot item, and having another item or enemy + # in the room, but the dot would be *super evil* + # to actually make randomized, since it is invisible. May add some options + # for how that works in the distant future, but for now, just say you need + # the bridge and black key to get to it, as that simplifies things a lot + set_rule(world.get_entrance("CreditsWall", self.player), + lambda state: state.has("Bridge", self.player) and + state.has("Black Key", self.player)) + + if not use_bat_logic: + set_rule(world.get_entrance("CreditsToFarSide", self.player), + lambda state: state.has("Magnet", self.player)) + + # bridge literally does not fit in this space, I think. I'll just exclude it + forbid_item(world.get_location("Dungeon Vault", self.player), "Bridge", self.player) + # don't put magnet in locations that can pull in-logic items out of reach unless the bat is in play + if not use_bat_logic: + forbid_item(world.get_location("Dungeon Vault", self.player), "Magnet", self.player) + forbid_item(world.get_location("Red Maze Vault Entrance", self.player), "Magnet", self.player) + forbid_item(world.get_location("Credits Right Side", self.player), "Magnet", self.player) + + # and obviously we don't want to start with the game already won + forbid_item(world.get_location("Inside Yellow Castle", self.player), "Chalice", self.player) + overworld = world.get_region("Overworld", self.player) + + for loc in overworld.locations: + forbid_item(loc, "Chalice", self.player) + + add_rule(world.get_location("Chalice Home", self.player), + lambda state: state.has("Chalice", self.player) and state.has("Yellow Key", self.player)) + + # world.random.choice(overworld.locations).progress_type = LocationProgressType.PRIORITY + + # all_locations = world.get_locations(self.player).copy() + # while priority_count < get_num_items(): + # loc = world.random.choice(all_locations) + # if loc.progress_type == LocationProgressType.DEFAULT: + # loc.progress_type = LocationProgressType.PRIORITY + # priority_count += 1 + # all_locations.remove(loc) + + # TODO: Add events for dragon_slay_check and trap_bat_check. Here? Elsewhere? + # if self.dragon_slay_check == 1: + # TODO - Randomize bat and dragon start rooms and use those to determine rules + # TODO - for the requirements for the slay event (since we have to get to the + # TODO - dragons and sword to kill them). Unless the dragons are set to be items, + # TODO - which might be a funny option, then they can just be randoed like normal + # TODO - just forbidden from the vaults and all credits room locations diff --git a/worlds/adventure/__init__.py b/worlds/adventure/__init__.py new file mode 100644 index 0000000000..b9d9d5f13c --- /dev/null +++ b/worlds/adventure/__init__.py @@ -0,0 +1,391 @@ +import base64 +import copy +import itertools +import math +import os +from enum import IntFlag +from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple + +from BaseClasses import Entrance, Item, ItemClassification, MultiWorld, Region, Tutorial, \ + LocationProgressType +from Main import __version__ +from Options import AssembleOptions +from worlds.AutoWorld import WebWorld, World +from Fill import fill_restrictive +from worlds.generic.Rules import add_rule, set_rule +from .Options import adventure_option_definitions, DragonRandoType, DifficultySwitchA, DifficultySwitchB +from .Rom import get_base_rom_bytes, get_base_rom_path, AdventureDeltaPatch, apply_basepatch, \ + AdventureAutoCollectLocation +from .Items import item_table, ItemData, nothing_item_id, event_table, AdventureItem, standard_item_max +from .Locations import location_table, base_location_id, LocationData, get_random_room_in_regions +from .Offsets import static_item_data_location, items_ram_start, static_item_element_size, item_position_table, \ + static_first_dragon_index, connector_port_offset, yorgle_speed_data_location, grundle_speed_data_location, \ + rhindle_speed_data_location, item_ram_addresses, start_castle_values, start_castle_offset +from .Regions import create_regions +from .Rules import set_rules + + +from worlds.LauncherComponents import Component, components, SuffixIdentifier + +# Adventure +components.append(Component('Adventure Client', 'AdventureClient', file_identifier=SuffixIdentifier('.apadvn'))) + + +class AdventureWeb(WebWorld): + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to setting up Adventure for MultiWorld.", + "English", + "setup_en.md", + "setup/en", + ["JusticePS"] + )] + theme = "dirt" + + +def get_item_position_data_start(table_index: int): + item_ram_address = item_ram_addresses[table_index]; + return item_position_table + item_ram_address - items_ram_start + + +class AdventureWorld(World): + """ + Adventure for the Atari 2600 is an early graphical adventure game. + Find the enchanted chalice and return it to the yellow castle, + using magic items to enter hidden rooms, retrieve out of + reach items, or defeat the three dragons. Beware the bat + who likes to steal your equipment! + """ + game: ClassVar[str] = "Adventure" + web: ClassVar[WebWorld] = AdventureWeb() + + option_definitions: ClassVar[Dict[str, AssembleOptions]] = adventure_option_definitions + 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): + super().__init__(world, player) + self.rom_name: Optional[bytearray] = bytearray("", "utf8" ) + self.dragon_rooms: [int] = [0x14, 0x19, 0x4] + self.dragon_slay_check: Optional[int] = 0 + self.connector_multi_slot: Optional[int] = 0 + self.dragon_rando_type: Optional[int] = 0 + self.yorgle_speed: Optional[int] = 2 + self.yorgle_min_speed: Optional[int] = 2 + self.grundle_speed: Optional[int] = 2 + self.grundle_min_speed: Optional[int] = 2 + self.rhindle_speed: Optional[int] = 3 + self.rhindle_min_speed: Optional[int] = 3 + self.difficulty_switch_a: Optional[int] = 0 + self.difficulty_switch_b: Optional[int] = 0 + self.start_castle: Optional[int] = 0 + # dict of item names -> list of speed deltas + self.dragon_speed_reducer_info: {} = {} + self.created_items: int = 0 + + @classmethod + def stage_assert_generate(cls, _multiworld: MultiWorld) -> None: + # don't need rom anymore + pass + + def place_random_dragon(self, dragon_index: int): + region_list = ["Overworld", "YellowCastle", "BlackCastle", "WhiteCastle"] + self.dragon_rooms[dragon_index] = get_random_room_in_regions(region_list, self.multiworld.random) + + def generate_early(self) -> None: + self.rom_name = \ + bytearray(f"ADVENTURE{__version__.replace('.', '')[:3]}_{self.player}_{self.multiworld.seed}", "utf8")[:21] + self.rom_name.extend([0] * (21 - len(self.rom_name))) + + self.dragon_rando_type = self.multiworld.dragon_rando_type[self.player].value + self.dragon_slay_check = self.multiworld.dragon_slay_check[self.player].value + self.connector_multi_slot = self.multiworld.connector_multi_slot[self.player].value + self.yorgle_speed = self.multiworld.yorgle_speed[self.player].value + self.yorgle_min_speed = self.multiworld.yorgle_min_speed[self.player].value + self.grundle_speed = self.multiworld.grundle_speed[self.player].value + self.grundle_min_speed = self.multiworld.grundle_min_speed[self.player].value + self.rhindle_speed = self.multiworld.rhindle_speed[self.player].value + self.rhindle_min_speed = self.multiworld.rhindle_min_speed[self.player].value + self.difficulty_switch_a = self.multiworld.difficulty_switch_a[self.player].value + self.difficulty_switch_b = self.multiworld.difficulty_switch_b[self.player].value + self.start_castle = self.multiworld.start_castle[self.player].value + self.created_items = 0 + + if self.dragon_slay_check == 0: + item_table["Sword"].classification = ItemClassification.useful + else: + item_table["Sword"].classification = ItemClassification.progression + if self.difficulty_switch_b == DifficultySwitchB.option_hard_with_unlock_item: + item_table["Right Difficulty Switch"].classification = ItemClassification.progression + + if self.dragon_rando_type == DragonRandoType.option_shuffle: + self.multiworld.random.shuffle(self.dragon_rooms) + elif self.dragon_rando_type == DragonRandoType.option_overworldplus: + dragon_indices = [0, 1, 2] + overworld_forced_index = self.multiworld.random.choice(dragon_indices) + dragon_indices.remove(overworld_forced_index) + region_list = ["Overworld"] + self.dragon_rooms[overworld_forced_index] = get_random_room_in_regions(region_list, self.multiworld.random) + self.place_random_dragon(dragon_indices[0]) + self.place_random_dragon(dragon_indices[1]) + elif self.dragon_rando_type == DragonRandoType.option_randomized: + self.place_random_dragon(0) + self.place_random_dragon(1) + self.place_random_dragon(2) + + def create_items(self) -> None: + for event in map(self.create_item, event_table): + self.multiworld.itempool.append(event) + exclude = [item for item in self.multiworld.precollected_items[self.player]] + self.created_items = 0 + for item in map(self.create_item, item_table): + if item.code == nothing_item_id: + continue + if item in exclude and item.code <= standard_item_max: + exclude.remove(item) # this is destructive. create unique list above + else: + if item.code <= standard_item_max: + self.multiworld.itempool.append(item) + self.created_items += 1 + num_locations = len(location_table) - 1 # subtract out the chalice location + if self.dragon_slay_check == 0: + num_locations -= 3 + + if self.difficulty_switch_a == DifficultySwitchA.option_hard_with_unlock_item: + self.multiworld.itempool.append(self.create_item("Left Difficulty Switch")) + self.created_items += 1 + if self.difficulty_switch_b == DifficultySwitchB.option_hard_with_unlock_item: + self.multiworld.itempool.append(self.create_item("Right Difficulty Switch")) + self.created_items += 1 + + extra_filler_count = num_locations - self.created_items + self.dragon_speed_reducer_info = {} + # make sure yorgle doesn't take 2 if there's not enough for the others to get at least one + if extra_filler_count <= 4: + extra_filler_count = 1 + self.create_dragon_slow_items(self.yorgle_min_speed, self.yorgle_speed, "Slow Yorgle", extra_filler_count) + extra_filler_count = num_locations - self.created_items + + if extra_filler_count <= 3: + extra_filler_count = 1 + self.create_dragon_slow_items(self.grundle_min_speed, self.grundle_speed, "Slow Grundle", extra_filler_count) + extra_filler_count = num_locations - self.created_items + + self.create_dragon_slow_items(self.rhindle_min_speed, self.rhindle_speed, "Slow Rhindle", extra_filler_count) + extra_filler_count = num_locations - self.created_items + + # traps would probably go here, if enabled + freeincarnate_max = self.multiworld.freeincarnate_max[self.player].value + actual_freeincarnates = min(extra_filler_count, freeincarnate_max) + self.multiworld.itempool += [self.create_item("Freeincarnate") for _ in range(actual_freeincarnates)] + self.created_items += actual_freeincarnates + + def create_dragon_slow_items(self, min_speed: int, speed: int, item_name: str, maximum_items: int): + if min_speed < speed: + delta = speed - min_speed + if delta > 2 and maximum_items >= 2: + self.multiworld.itempool.append(self.create_item(item_name)) + self.multiworld.itempool.append(self.create_item(item_name)) + speed_with_one = speed - math.floor(delta / 2) + self.dragon_speed_reducer_info[item_table[item_name].id] = [speed_with_one, min_speed] + self.created_items += 2 + elif maximum_items >= 1: + self.multiworld.itempool.append(self.create_item(item_name)) + self.dragon_speed_reducer_info[item_table[item_name].id] = [min_speed] + self.created_items += 1 + + def create_regions(self) -> None: + create_regions(self.multiworld, self.player, self.dragon_rooms) + + set_rules = set_rules + + def generate_basic(self) -> None: + self.multiworld.get_location("Chalice Home", self.player).place_locked_item( + self.create_event("Victory", ItemClassification.progression)) + self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) + + def pre_fill(self): + # Place empty items in filler locations here, to limit + # the number of exported empty items and the density of stuff in overworld. + max_location_count = len(location_table) - 1 + if self.dragon_slay_check == 0: + max_location_count -= 3 + + force_empty_item_count = (max_location_count - self.created_items) + if force_empty_item_count <= 0: + return + overworld = self.multiworld.get_region("Overworld", self.player) + overworld_locations_copy = overworld.locations.copy() + all_locations = self.multiworld.get_locations(self.player) + + locations_copy = all_locations.copy() + for loc in all_locations: + if loc.item is not None or loc.progress_type != LocationProgressType.DEFAULT: + locations_copy.remove(loc) + if loc in overworld_locations_copy: + overworld_locations_copy.remove(loc) + + # guarantee at least one overworld location, so we can for sure get a key somewhere + # if too much stuff is plando'd though, just let it go + if len(overworld_locations_copy) >= 3: + saved_overworld_loc = self.multiworld.random.choice(overworld_locations_copy) + locations_copy.remove(saved_overworld_loc) + overworld_locations_copy.remove(saved_overworld_loc) + + # if we have few items, enforce another overworld slot, fill a hard slot, and ensure we have + # at least one hard slot available + if self.created_items < 15: + hard_locations = [] + for loc in locations_copy: + if "Vault" in loc.name or "Credits" in loc.name: + hard_locations.append(loc) + force_empty_item_count -= 1 + loc = self.multiworld.random.choice(hard_locations) + loc.place_locked_item(self.create_item('nothing')) + hard_locations.remove(loc) + locations_copy.remove(loc) + + loc = self.multiworld.random.choice(hard_locations) + locations_copy.remove(loc) + hard_locations.remove(loc) + + saved_overworld_loc = self.multiworld.random.choice(overworld_locations_copy) + locations_copy.remove(saved_overworld_loc) + overworld_locations_copy.remove(saved_overworld_loc) + + # if we have very few items, fill another two difficult slots + if self.created_items < 10: + for i in range(2): + force_empty_item_count -= 1 + loc = self.multiworld.random.choice(hard_locations) + loc.place_locked_item(self.create_item('nothing')) + hard_locations.remove(loc) + locations_copy.remove(loc) + + # for the absolute minimum number of items, enforce a third overworld slot + if self.created_items <= 7: + saved_overworld_loc = self.multiworld.random.choice(overworld_locations_copy) + locations_copy.remove(saved_overworld_loc) + overworld_locations_copy.remove(saved_overworld_loc) + + # finally, place nothing items + while force_empty_item_count > 0 and locations_copy: + force_empty_item_count -= 1 + # prefer somewhat to thin out the overworld. + if len(overworld_locations_copy) > 0 and self.multiworld.random.randint(0, 10) < 4: + loc = self.multiworld.random.choice(overworld_locations_copy) + else: + loc = self.multiworld.random.choice(locations_copy) + loc.place_locked_item(self.create_item('nothing')) + locations_copy.remove(loc) + if loc in overworld_locations_copy: + overworld_locations_copy.remove(loc) + + def place_dragons(self, rom_deltas: {int, int}): + for i in range(3): + table_index = static_first_dragon_index + i + item_position_data_start = get_item_position_data_start(table_index) + rom_deltas[item_position_data_start] = self.dragon_rooms[i] + + def set_dragon_speeds(self, rom_deltas: {int, int}): + rom_deltas[yorgle_speed_data_location] = self.yorgle_speed + rom_deltas[grundle_speed_data_location] = self.grundle_speed + rom_deltas[rhindle_speed_data_location] = self.rhindle_speed + + def set_start_castle(self, rom_deltas): + start_castle_value = start_castle_values[self.start_castle] + rom_deltas[start_castle_offset] = start_castle_value + + def generate_output(self, output_directory: str) -> None: + rom_path: str = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.bin") + foreign_item_locations: [LocationData] = [] + auto_collect_locations: [AdventureAutoCollectLocation] = [] + local_item_to_location: {int, int} = {} + bat_no_touch_locs: [LocationData] = [] + bat_logic: int = self.multiworld.bat_logic[self.player].value + try: + rom_deltas: { int, int } = {} + self.place_dragons(rom_deltas) + self.set_dragon_speeds(rom_deltas) + self.set_start_castle(rom_deltas) + # start and stop indices are offsets in the ROM file, not Adventure ROM addresses (which start at f000) + + # This places the local items (I still need to make it easy to inject the offset data) + unplaced_local_items = dict(filter(lambda x: nothing_item_id < x[1].id <= standard_item_max, + item_table.items())) + for location in self.multiworld.get_locations(self.player): + # 'nothing' items, which are autocollected when the room is entered + if location.item.player == self.player and \ + location.item.name == "nothing": + location_data = location_table[location.name] + auto_collect_locations.append(AdventureAutoCollectLocation(location_data.short_location_id, + location_data.room_id)) + # standard Adventure items, which are placed in the rom + elif location.item.player == self.player and \ + location.item.name != "nothing" and \ + location.item.code is not None and \ + location.item.code <= standard_item_max: + # I need many of the intermediate values here. + item_table_offset = item_table[location.item.name].table_index * static_item_element_size + item_ram_address = item_ram_addresses[item_table[location.item.name].table_index] + item_position_data_start = item_position_table + item_ram_address - items_ram_start + location_data = location_table[location.name] + room_x, room_y = location_data.get_position(self.multiworld.per_slot_randoms[self.player]) + if location_data.needs_bat_logic and bat_logic == 0x0: + copied_location = copy.copy(location_data) + copied_location.local_item = item_ram_address + bat_no_touch_locs.append(copied_location) + del unplaced_local_items[location.item.name] + + rom_deltas[item_position_data_start] = location_data.room_id + rom_deltas[item_position_data_start + 1] = room_x + rom_deltas[item_position_data_start + 2] = room_y + local_item_to_location[item_table_offset] = self.location_name_to_id[location.name] \ + - base_location_id + # items from other worlds, and non-standard Adventure items handled by script, like difficulty switches + elif location.item.code is not None: + if location.item.code != nothing_item_id: + location_data = location_table[location.name] + foreign_item_locations.append(location_data) + if location_data.needs_bat_logic and bat_logic == 0x0: + bat_no_touch_locs.append(location_data) + else: + location_data = location_table[location.name] + auto_collect_locations.append(AdventureAutoCollectLocation(location_data.short_location_id, + location_data.room_id)) + # Adventure items that are in another world get put in an invalid room until needed + for unplaced_item_name, unplaced_item in unplaced_local_items.items(): + item_position_data_start = get_item_position_data_start(unplaced_item.table_index) + rom_deltas[item_position_data_start] = 0xff + + if self.multiworld.connector_multi_slot[self.player].value: + rom_deltas[connector_port_offset] = (self.player & 0xff) + else: + rom_deltas[connector_port_offset] = 0 + except Exception as e: + raise e + else: + patch = AdventureDeltaPatch(os.path.splitext(rom_path)[0] + AdventureDeltaPatch.patch_file_ending, + player=self.player, player_name=self.multiworld.player_name[self.player], + locations=foreign_item_locations, + autocollect=auto_collect_locations, local_item_locations=local_item_to_location, + dragon_speed_reducer_info=self.dragon_speed_reducer_info, + diff_a_mode=self.difficulty_switch_a, diff_b_mode=self.difficulty_switch_b, + bat_logic=bat_logic, bat_no_touch_locations=bat_no_touch_locs, + rom_deltas=rom_deltas, + seed_name=bytes(self.multiworld.seed_name, encoding="ascii")) + patch.write() + finally: + if os.path.exists(rom_path): + os.unlink(rom_path) + + # end of ordered Main.py calls + + def create_item(self, name: str) -> Item: + item_data: ItemData = item_table.get(name) + return AdventureItem(name, item_data.classification, item_data.id, self.player) + + def create_event(self, name: str, classification: ItemClassification) -> Item: + return AdventureItem(name, classification, None, self.player) diff --git a/worlds/adventure/docs/en_Adventure.md b/worlds/adventure/docs/en_Adventure.md new file mode 100644 index 0000000000..c39e0f7d91 --- /dev/null +++ b/worlds/adventure/docs/en_Adventure.md @@ -0,0 +1,62 @@ +# Adventure + +## Where is the settings page? +The [player settings page for Adventure](../player-settings) contains all the options you need to configure and export a config file. + +## What does randomization do to this game? +Adventure items may be distributed into additional locations not possible in the vanilla Adventure randomizer. All +Adventure items are added to the multiworld item pool. Depending on the settings, dragon locations may be randomized, +slaying dragons may award items, difficulty switches may require items to unlock, and limited use 'freeincarnates' +can allow reincarnation without resurrecting dragons. Dragon speeds may also be randomized, and items may exist +to reduce their speeds. + +## What is the goal of Adventure when randomized? +Same as vanilla; Find the Enchanted Chalice and return it to the Yellow Castle + +## Which items can be in another player's world? +All three keys, the chalice, the sword, the magnet, and the bridge can be found in another player's world. Depending on +settings, dragon slowdowns, difficulty switch unlocks, and freeincarnates may also be found. + +## What is considered a location check in Adventure? +Most areas in Adventure have one or more locations which can contain an Adventure item or an Archipelago item. +A few rooms have two potential locaions. If the location contains a 'nothing' Adventure item, it will send a check when +that is seen. If it contains an item from another Adventure or other game, it will show a rough approximation of the +Archipelago logo that can be touched for a check. Touching a local Adventure item also 'checks' it, allowing it to be +retrieved after a select-reset or hard reset. + +## Why isn't my item where the spoiler says it should be? +If something isn't where the spoiler says, most likely the bat carried it somewhere else. The bat's ability to shuffle +items around makes it somewhat unique in Archipelago. Touching the item, wherever it is, will award the location check +for wherever the item was originally placed. + +## Which notable items are not randomized? +The bat, dot, and map are not yet randomized. If the chalice is local, it is randomized, but is always in either a +castle or the credits screen. Forcing the chalice local in the yaml is recommended. + +## What does another world's item look like in Adventure? +It looks vaguely like a flashing Archipelago logo. + +## When the player receives an item, what happens? +A message is shown in the client log. While empty handed, the player can press the fire button to retrieve items in the +order they were received. Once an item is retrieved this way, it cannot be retrieved again until pressing select to +return to the 'GO' screen or doing a hard reset, either one of which will reset all items to their original positions. + +## What are recommended settings to tweak for beginners to the rando? +Setting difficulty_switch_a and lowering the dragons' speeds makes the dragons easier to avoid. Adding Chalice to +local_items guarantees you'll visit at least one of the interesting castles, as it can only be placed in a castle or +the credits room. + +## My yellow key is stuck in a wall! Am I softlocked? +Maybe! That's all part of Adventure. If you have access to the magnet, bridge, or bat, you might be able to retrieve +it. In general, since the bat always starts outside of castles, you should always be able to find it unless you lock +it in a castle yourself. This mod's inventory system allows you to quickly recover all the items +you've collected after a hard reset or select-reset (except for the dot), so usually it's not as bad as in vanilla. + +## How do I get into the credits room? There's a item I need in there. +Searching for 'Adventure dot map' should bring up an AtariAge map with a good walkthrough, but here's the basics. +Bring the bridge into the black castle. Find the small room in the dungeon that cannot be reached without the bridge, +enter it, and push yourself into the bottom right corner to pick up the dot. The dot color matches the background, +so you won't be able to see it if it isn't in a wall, so be careful not to drop it. Bring it to the room one south and +one east of the yellow castle and drop it there. Bring 2-3 more objects (the bat and dragons also count for this) until +it lets you walk through the right wall. +If the item is on the right side, you'll need the magnet to get it. \ No newline at end of file diff --git a/worlds/adventure/docs/setup_en.md b/worlds/adventure/docs/setup_en.md new file mode 100644 index 0000000000..038e87e767 --- /dev/null +++ b/worlds/adventure/docs/setup_en.md @@ -0,0 +1,70 @@ +# Setup Guide for Adventure: Archipelago + +## Important + +As we are using Bizhawk, this guide is only applicable to Windows and Linux systems. + +## Required Software + +- Bizhawk: [Bizhawk Releases from TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory) + - Version 2.3.1 and later are supported. Version 2.7 is recommended for stability. + - Detailed installation instructions for Bizhawk can be found at the above link. + - Windows users must run the prereq installer first, which can also be found at the above link. +- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases) + (select `Adventure Client` during installation). +- An Adventure NTSC ROM file. The Archipelago community cannot provide these. + +## Configuring Bizhawk + +Once Bizhawk has been installed, open Bizhawk and change the following settings: + +- Go to Config > Customize. Switch to the Advanced tab, then switch the Lua Core from "NLua+KopiLua" to + "Lua+LuaInterface". Then restart Bizhawk. This is required for the Lua script to function correctly. + **NOTE: Even if "Lua+LuaInterface" is already selected, toggle between the two options and reselect it. Fresh installs** + **of newer versions of Bizhawk have a tendency to show "Lua+LuaInterface" as the default selected option but still load** + **"NLua+KopiLua" until this step is done.** +- Under Config > Customize, check the "Run in background" box. This will prevent disconnecting from the client while +BizHawk is running in the background. + +- It is recommended that you provide a path to BizHawk in your host.yaml for Adventure so the client can start it automatically + +## Configuring your YAML file + +### What is a YAML file and why do I need one? + +Your YAML file contains a set of configuration options which provide the generator with information about how it should +generate your game. Each player of a multiworld will provide their own YAML file. This setup allows each player to enjoy +an experience customized for their taste, and different players in the same multiworld can all have different options. + +### Where do I get a YAML file? + +You can generate a yaml or download a template by visiting the [Adventure Settings Page](/games/Adventure/player-settings) + +### What are recommended settings to tweak for beginners to the rando? +Setting difficulty_switch_a and lowering the dragons' speeds makes the dragons easier to avoid. Adding Chalice to +local_items guarantees you'll visit at least one of the interesting castles, as it can only be placed in a castle or +the credits room. + +## Joining a MultiWorld Game + +### Obtain your Adventure patch file + +When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that is done, +the host will provide you with either a link to download your data file, or with a zip file containing everyone's data +files. Your data file should have a `.apadvn` extension. + +Drag your patch file to the AdventureClient.exe to start your client and start the ROM patch process. Once the process +is finished (this can take a while), the client and the emulator will be started automatically (if you set the emulator +path as recommended). + +### Connect to the Multiserver + +Once both the client and the emulator are started, you must connect them. Within the emulator click on the "Tools" +menu and select "Lua Console". Click the folder button or press Ctrl+O to open a Lua script. + +Navigate to your Archipelago install folder and open `data/lua/ADVENTURE/adventure_connector.lua`. + +To connect the client to the multiserver simply put `
:` on the textfield on top and press enter (if the +server uses password, type in the bottom textfield `/connect
: [password]`) + +Press Reset and begin playing From 7591404151a15015e77682c37a28fa5628dbb7dd Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sat, 18 Mar 2023 20:37:06 -0500 Subject: [PATCH 095/172] OOT: set default for adult trade start if the set is empty --- worlds/oot/Options.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/worlds/oot/Options.py b/worlds/oot/Options.py index 205938263c..35b477ae58 100644 --- a/worlds/oot/Options.py +++ b/worlds/oot/Options.py @@ -1045,6 +1045,11 @@ class AdultTradeStart(OptionSet): "Claim Check", } + def __init__(self, value: typing.Iterable[str]): + if not value: + value = self.default + super().__init__(value) + itempool_options: typing.Dict[str, type(Option)] = { "item_pool_value": ItemPoolValue, From b3895750ab443adb611bce3817433b13c0550ba1 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 21 Mar 2023 22:14:43 +0100 Subject: [PATCH 096/172] Docs: from source on macOS: update supported python versions --- worlds/generic/docs/mac_en.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/generic/docs/mac_en.md b/worlds/generic/docs/mac_en.md index 1e2d235c8c..dc7e32fa98 100644 --- a/worlds/generic/docs/mac_en.md +++ b/worlds/generic/docs/mac_en.md @@ -2,7 +2,8 @@ Archipelago does not have a compiled release on macOS. However, it is possible to run from source code on macOS. This guide expects you to have some experience with running software from the terminal. ## Prerequisite Software Here is a list of software to install and source code to download. -1. Python 3.8 or newer from the [macOS Python downloads page](https://www.python.org/downloads/macos/). +1. Python 3.9 "universal2" or newer from the [macOS Python downloads page](https://www.python.org/downloads/macos/). + **Python 3.11 is not supported yet.** 2. Xcode from the [macOS App Store](https://apps.apple.com/us/app/xcode/id497799835). 3. The source code from the [Archipelago releases page](https://github.com/ArchipelagoMW/Archipelago/releases). 4. The asset with darwin in the name from the [SNI Github releases page](https://github.com/alttpo/sni/releases). From 3ec2d45f4f5f55f9fcaade754d6643e9f325fbc5 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Thu, 23 Mar 2023 00:20:34 -0700 Subject: [PATCH 097/172] [WebHost] Improve mobile styles for WebHost (#1571) * Improve mobile styles for WebHost - Mobile view works properly on mobile Chrome and Firefox - Added thin-window view for desktop browsers that have been reduced in size - Tested in Chrome and Firefox on mobile and desktop * Improve style for small-width desktop popover --- WebHostLib/static/styles/landing.css | 2 +- WebHostLib/static/styles/themes/base.css | 49 +++++++++++++++++++----- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/WebHostLib/static/styles/landing.css b/WebHostLib/static/styles/landing.css index ea142942e1..a70ac69dd0 100644 --- a/WebHostLib/static/styles/landing.css +++ b/WebHostLib/static/styles/landing.css @@ -223,7 +223,7 @@ html{ } #landing{ - width: 700px; + max-width: 700px; min-height: 280px; margin-left: auto; margin-right: auto; diff --git a/WebHostLib/static/styles/themes/base.css b/WebHostLib/static/styles/themes/base.css index d38a8e610c..1c41bd271a 100644 --- a/WebHostLib/static/styles/themes/base.css +++ b/WebHostLib/static/styles/themes/base.css @@ -87,7 +87,44 @@ html{ height: 3rem; } -@media all and (max-width: 1580px){ +@media all and (max-width: 960px), only screen and (max-device-width: 768px) { + #base-header-right{ + display: none; + } + + #base-header-right-mobile{ + display: unset; + } +} + +@media all and (max-width: 960px){ + #base-header-right-mobile{ + margin-top: 0.5rem; + margin-right: 0; + } + + #base-header-right-mobile img{ + height: 1.5rem; + } + + #base-header-mobile-menu{ + top: 3.3rem; + width: unset; + border-left: 2px solid #d0ebe6; + border-bottom: 2px solid #d0ebe6; + filter: drop-shadow(-6px 6px 2px #2e3e83); + border-top-left-radius: 10px; + } + + #base-header-mobile-menu a{ + font-size: 2rem; + line-height: 3rem; + margin: 0; + padding: 0 1rem; + } +} + +@media only screen and (max-device-width: 768px){ html{ padding-top: 260px; scroll-padding-top: 230px; @@ -103,12 +140,4 @@ html{ margin-top: 30px; margin-left: 20px; } - - #base-header-right{ - display: none; - } - - #base-header-right-mobile{ - display: unset; - } -} +} \ No newline at end of file From 256f493ada89fe18514e5badc18b0cfd8aef6e7d Mon Sep 17 00:00:00 2001 From: zig-for Date: Thu, 23 Mar 2023 06:53:48 -0700 Subject: [PATCH 098/172] LADX: fix web gen (#1574) --- worlds/ladx/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index 2c213fc75b..adf45baf69 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -394,7 +394,7 @@ class LinksAwakeningWorld(World): args, self.laxdr_options, self.player_options, - bytes.fromhex(self.multiworld.seed_name), + bytes.fromhex(self.multiworld.seed_name[-20:]), self.ladxr_logic, rnd=self.multiworld.per_slot_randoms[self.player], player_name=name_for_rom, @@ -411,7 +411,7 @@ class LinksAwakeningWorld(World): os.unlink(rompath) def generate_multi_key(self): - return bytes.fromhex(self.multiworld.seed_name) + self.player.to_bytes(2, 'big') + return bytes.fromhex(self.multiworld.seed_name[-20:]) + self.player.to_bytes(2, 'big') def modify_multidata(self, multidata: dict): multi_key = binascii.hexlify(self.generate_multi_key()).decode() From 5bb6ff0ce0d6cc7b608a303fe23ed1ef7df8dfaf Mon Sep 17 00:00:00 2001 From: zig-for Date: Thu, 23 Mar 2023 13:22:42 -0700 Subject: [PATCH 099/172] LADX: Fixup missing descriptions (#1576) --- worlds/ladx/Options.py | 108 ++++++++++++++++++++++++---------------- worlds/ladx/__init__.py | 5 +- 2 files changed, 68 insertions(+), 45 deletions(-) diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py index 37055b7a2f..57de7430a5 100644 --- a/worlds/ladx/Options.py +++ b/worlds/ladx/Options.py @@ -18,7 +18,8 @@ class LADXROption: class Logic(Choice, LADXROption): - """Affects where items are allowed to be placed. + """ + Affects where items are allowed to be placed. [Normal] Playable without using any tricks or glitches. Can require knowledge from a vanilla playthrough, such as how to open Color Dungeon. [Hard] More advanced techniques may be required, but glitches are not. Examples include tricky jumps, killing enemies with only pots. [Glitched] Advanced glitches and techniques may be required, but extremely difficult or tedious tricks are not required. Examples include Bomb Triggers, Super Jumps and Jesus Jumps. @@ -35,32 +36,33 @@ class Logic(Choice, LADXROption): class TradeQuest(DefaultOffToggle, LADXROption): """ - On - adds the trade items to the pool (the trade locations will always be local items) - Off - (default) doesn't add them + [On] adds the trade items to the pool (the trade locations will always be local items) + [Off] (default) doesn't add them """ ladxr_name = "tradequest" class Boomerang(Choice): """ - [Normal], requires Magnifying Lens to get the boomerang. - [Gift], The boomerang salesman will give you a random item, and the boomerang is shuffled. + [Normal] requires Magnifying Lens to get the boomerang. + [Gift] The boomerang salesman will give you a random item, and the boomerang is shuffled. """ normal = 0 gift = 1 default = gift -# TODO: translate to lttp parlance class EntranceShuffle(Choice, LADXROption): """ - [WARNING] Experimental, may break generation + [WARNING] Experimental, may fail to fill Randomizes where overworld entrances lead to. [Simple] Single-entrance caves/houses that have items are shuffled amongst each other. - [Advanced] Simple, but two-way connector caves are shuffled in their own pool as well. - [Expert] Advanced, but caves/houses without items are also shuffled into the Simple entrance pool. - [Insanity] Expert, but the Raft Minigame hut and Mamu's cave are added to the non-connector pool. If random start location and/or dungeon shuffle is enabled, then these will be shuffled with all the non-connector entrance pool. Note, some entrances can lead into water, use the warp-to-home from the save&quit menu to escape this.""" + + #[Advanced] Simple, but two-way connector caves are shuffled in their own pool as well. + #[Expert] Advanced, but caves/houses without items are also shuffled into the Simple entrance pool. + #[Insanity] Expert, but the Raft Minigame hut and Mamu's cave are added to the non-connector pool. + option_none = 0 option_simple = 1 #option_advanced = 2 @@ -71,8 +73,8 @@ class EntranceShuffle(Choice, LADXROption): class DungeonShuffle(DefaultOffToggle, LADXROption): """ - [WARNING] Experimental, may break generation - Randomizes + [WARNING] Experimental, may fail to fill + Randomizes dungeon entrances within eachother """ ladxr_name = "dungeonshuffle" @@ -97,30 +99,60 @@ class DungeonItemShuffle(Choice): class ShuffleNightmareKeys(DungeonItemShuffle): """ Shuffle Nightmare Keys + [Original Dungeon] The item will be within its original dungeon + [Own Dungeons] The item will be within a dungeon in your world + [Own World] The item will be somewhere in your world + [Any World] The item could be anywhere + [Different World] The item will be somewhere in another world """ ladxr_item = "NIGHTMARE_KEY" + class ShuffleSmallKeys(DungeonItemShuffle): """ Shuffle Small Keys + [Original Dungeon] The item will be within its original dungeon + [Own Dungeons] The item will be within a dungeon in your world + [Own World] The item will be somewhere in your world + [Any World] The item could be anywhere + [Different World] The item will be somewhere in another world """ ladxr_item = "KEY" class ShuffleMaps(DungeonItemShuffle): """ Shuffle Dungeon Maps + [Original Dungeon] The item will be within its original dungeon + [Own Dungeons] The item will be within a dungeon in your world + [Own World] The item will be somewhere in your world + [Any World] The item could be anywhere + [Different World] The item will be somewhere in another world """ ladxr_item = "MAP" + class ShuffleCompasses(DungeonItemShuffle): """ Shuffle Dungeon Compasses + [Original Dungeon] The item will be within its original dungeon + [Own Dungeons] The item will be within a dungeon in your world + [Own World] The item will be somewhere in your world + [Any World] The item could be anywhere + [Different World] The item will be somewhere in another world """ ladxr_item = "COMPASS" + class ShuffleStoneBeaks(DungeonItemShuffle): """ Shuffle Owl Beaks + [Original Dungeon] The item will be within its original dungeon + [Own Dungeons] The item will be within a dungeon in your world + [Own World] The item will be somewhere in your world + [Any World] The item could be anywhere + [Different World] The item will be somewhere in another world """ ladxr_item = "STONE_BEAK" + class Goal(Choice, LADXROption): """ + The Goal of the game [Instruments] The Wind Fish's Egg will only open if you have the required number of Instruments of the Sirens, and play the Ballad of the Wind Fish. [Seashells] The Egg will open when you bring 20 seashells. The Ballad and Ocarina are not needed. [Open] The Egg will start pre-opened. @@ -133,41 +165,21 @@ class Goal(Choice, LADXROption): default = option_instruments - def to_ladxr_option(self, all_options): - if self.value == self.option_instruments: return ("goal", all_options["instrument_count"]) else: return LADXROption.to_ladxr_option(self, all_options) class InstrumentCount(Range, LADXROption): + """ + Sets the number of instruments required to open the Egg + """ ladxr_name = None range_start = 0 range_end = 8 default = 8 -#class SeashellCount(Range): -# range_start = 0 -# range_end = 20 -# default = 20 - -# Setting('goal', 'Gameplay', 'G', 'Goal', options=[('8', '8', '8 instruments'), ('7', '7', '7 instruments'), ('6', '6', '6 instruments'), -# ('5', '5', '5 instruments'), ('4', '4', '4 instruments'), ('3', '3', '3 instruments'), -# ('2', '2', '2 instruments'), ('1', '1', '1 instrument'), ('0', '0', 'No instruments'), -# ('open', 'O', 'Egg already open'), ('random', 'R', 'Random instrument count'), -# ('open-4', '<', 'Random short game (0-4)'), ('5-8', '>', 'Random long game (5-8)'), -# ('seashells', 'S', 'Seashell hunt (20)'), ('bingo', 'b', 'Bingo!'), -# ('bingo-full', 'B', 'Bingo-25!')], default='8', -# description="""Changes the goal of the game. -# [1-8 instruments], number of instruments required to open the egg. -# [No instruments] open the egg without instruments, still requires the ocarina with the balled of the windfish -# [Egg already open] the egg is already open, just head for it once you have the items needed to defeat the boss. -# [Randomized instrument count] random number of instruments required to open the egg, between 0 and 8. -# [Random short/long game] random number of instruments required to open the egg, chosen between 0-4 and 5-8 respectively. -# [Seashell hunt] egg will open once you collected 20 seashells. Instruments are replaced by seashells and shuffled. -# [Bingo] Generate a 5x5 bingo board with various goals. Complete one row/column or diagonal to win! -# [Bingo-25] Bingo, but need to fill the whole bingo card to win!"""), class ItemPool(Choice): """Effects which items are shuffled. [Casual] Places multiple copies of key items. @@ -220,10 +232,6 @@ class Overworld(Choice, LADXROption): # option_shuffled = 3 default = option_normal -# Ugh, this will change what 'progression' means?? -#Setting('owlstatues', 'Special', 'o', 'Owl statues', options=[('', '', 'Never'), ('dungeon', 'D', 'In dungeons'), ('overworld', 'O', 'On the overworld'), ('both', 'B', 'Dungeons and Overworld')], default='', -# description='Replaces the hints from owl statues with additional randomized items'), - #Setting('superweapons', 'Special', 'q', 'Enable super weapons', default=False, # description='All items will be more powerful, faster, harder, bigger stronger. You name it.'), #Setting('quickswap', 'User options', 'Q', 'Quickswap', options=[('none', '', 'Disabled'), ('a', 'a', 'Swap A button'), ('b', 'b', 'Swap B button')], default='none', @@ -258,6 +266,7 @@ class Overworld(Choice, LADXROption): class LinkPalette(Choice, LADXROption): """ + Sets link's palette A-D are color palettes usually used during the damage animation and can change based on where you are. """ display_name = "Links Palette" @@ -280,10 +289,10 @@ class TrendyGame(Choice): """ [Easy] All of the items hold still for you [Normal] The vanilla behavior - [Hard] ? - [Harder] ??? - [Hardest] ???? - [Impossible] ????? + [Hard] The trade item also moves + [Harder] The items move faster + [Hardest] The items move diagonally + [Impossible] The items move impossibly fast, may scroll on and off the screen """ option_easy = 0 option_normal = 1 @@ -295,7 +304,8 @@ class TrendyGame(Choice): class GfxMod(FreeText, LADXROption): """ - options here correlate with sprite and name files in data/sprites/ladx + Sets the sprite for link, among other things + The option should be the same name as a with sprite (and optional name) file in data/sprites/ladx """ display_name = "GFX Modification" ladxr_name = "gfxmod" @@ -327,6 +337,16 @@ class GfxMod(FreeText, LADXROption): return None, None class Palette(Choice): + """ + Sets the palette for the game. + Note: A few places aren't patched, such as the menu and a few color dungeon tiles. + [Normal] The vanilla palette + [1-Bit] One bit of color per channel + [2-Bit] Two bits of color per channel + [Greyscale] Shades of grey + [Pink] Aesthetic + [Inverted] Inverted + """ option_normal = 0 option_1bit = 1 option_2bit = 2 diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index adf45baf69..db8825db13 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -37,7 +37,10 @@ class LinksAwakeningWebWorld(WebWorld): theme = "dirt" class LinksAwakeningWorld(World): - """Insert description of the world/game here.""" + """ + After a previous adventure, Link is stranded on Koholint Island, full of mystery and familiar faces. + Gather the 8 Instruments of the Sirens to wake the Wind Fish, so that Link can go home! + """ game: str = LINKS_AWAKENING # name of the game/world web = LinksAwakeningWebWorld() From e1f46d623cd5d5c19bacc421064c53e31101a3ee Mon Sep 17 00:00:00 2001 From: zig-for Date: Thu, 23 Mar 2023 13:23:58 -0700 Subject: [PATCH 100/172] LADX: Pass in seed_name and auth separately (#1575) --- worlds/ladx/LADXR/generator.py | 14 +++++--------- worlds/ladx/LADXR/patches/titleScreen.py | 15 +++++---------- worlds/ladx/__init__.py | 11 +++++++---- 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/worlds/ladx/LADXR/generator.py b/worlds/ladx/LADXR/generator.py index 0564e276ea..cf8e67eee6 100644 --- a/worlds/ladx/LADXR/generator.py +++ b/worlds/ladx/LADXR/generator.py @@ -60,7 +60,7 @@ from ..Options import TrendyGame, Palette # Function to generate a final rom, this patches the rom with all required patches -def generateRom(args, settings, ap_settings, seed, logic, rnd=None, multiworld=None, player_name=None, player_names=[], player_id = 0): +def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, multiworld=None, player_name=None, player_names=[], player_id = 0): rom = ROMWithTables(args.input_filename) rom.player_names = player_names pymods = [] @@ -119,7 +119,7 @@ def generateRom(args, settings, ap_settings, seed, logic, rnd=None, multiworld=N patches.core.easyColorDungeonAccess(rom) patches.owl.removeOwlEvents(rom) patches.enemies.fixArmosKnightAsMiniboss(rom) - patches.bank3e.addBank3E(rom, seed, player_id, player_names) + patches.bank3e.addBank3E(rom, auth, player_id, player_names) patches.bank3f.addBank3F(rom) patches.bank34.addBank34(rom, item_list) patches.core.removeGhost(rom) @@ -269,7 +269,7 @@ def generateRom(args, settings, ap_settings, seed, logic, rnd=None, multiworld=N patches.core.addFrameCounter(rom, len(item_list)) patches.core.warpHome(rom) # Needs to be done after setting the start location. - patches.titleScreen.setRomInfo(rom, binascii.hexlify(seed).decode("ascii").upper(), settings, player_name, player_id) + patches.titleScreen.setRomInfo(rom, auth, seed_name, settings, player_name, player_id) patches.endscreen.updateEndScreen(rom) patches.aesthetics.updateSpriteData(rom) if args.doubletrouble: @@ -411,13 +411,9 @@ def generateRom(args, settings, ap_settings, seed, logic, rnd=None, multiworld=N rom.banks[bank][address + 1] = packed >> 8 SEED_LOCATION = 0x0134 - SEED_SIZE = 10 - - # TODO: pass this in # Patch over the title - assert(len(seed) == SEED_SIZE) - gameid = seed + player_id.to_bytes(2, 'big') - rom.patch(0x00, SEED_LOCATION, None, binascii.hexlify(gameid)) + assert(len(auth) == 12) + rom.patch(0x00, SEED_LOCATION, None, binascii.hexlify(auth)) for pymod in pymods: diff --git a/worlds/ladx/LADXR/patches/titleScreen.py b/worlds/ladx/LADXR/patches/titleScreen.py index 52adba6813..b81f1460eb 100644 --- a/worlds/ladx/LADXR/patches/titleScreen.py +++ b/worlds/ladx/LADXR/patches/titleScreen.py @@ -20,27 +20,22 @@ def _encode(s): return result -def setRomInfo(rom, seed, settings, player_name, player_id): - #try: - # version = subprocess.run(['git', 'describe', '--tags', '--dirty=-D'], stdout=subprocess.PIPE).stdout.strip().decode("ascii", "replace") - #except: - # version = "" - +def setRomInfo(rom, seed, seed_name, settings, player_name, player_id): try: seednr = int(seed, 16) except: import hashlib - seednr = int(hashlib.md5(seed.encode('ascii', 'replace')).hexdigest(), 16) + seednr = int(hashlib.md5(seed).hexdigest(), 16) if settings.race: - seed = "Race" + seed_name = "Race" if isinstance(settings.race, str): - seed += " " + settings.race + seed_name += " " + settings.race rom.patch(0x00, 0x07, "00", "01") else: rom.patch(0x00, 0x07, "00", "52") - line_1_hex = _encode(seed) + line_1_hex = _encode(seed_name[:20]) #line_2_hex = _encode(seed[16:]) BASE_DRAWING_AREA = 0x98a0 LINE_WIDTH = 0x20 diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index db8825db13..47c601a1f7 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -216,6 +216,8 @@ class LinksAwakeningWorld(World): self.multiworld.itempool.append(item) def pre_fill(self): + self.multi_key = self.generate_multi_key() + dungeon_locations = [] dungeon_locations_by_dungeon = [[], [], [], [], [], [], [], [], []] all_state = self.multiworld.get_all_state(use_cache=False) @@ -393,11 +395,13 @@ class LinksAwakeningWorld(World): name_for_rom = self.multiworld.player_name[self.player] all_names = [self.multiworld.player_name[i + 1] for i in range(len(self.multiworld.player_name))] + rom = generator.generateRom( args, self.laxdr_options, self.player_options, - bytes.fromhex(self.multiworld.seed_name[-20:]), + self.multi_key, + self.multiworld.seed_name, self.ladxr_logic, rnd=self.multiworld.per_slot_randoms[self.player], player_name=name_for_rom, @@ -414,8 +418,7 @@ class LinksAwakeningWorld(World): os.unlink(rompath) def generate_multi_key(self): - return bytes.fromhex(self.multiworld.seed_name[-20:]) + self.player.to_bytes(2, 'big') + return bytearray(self.multiworld.random.getrandbits(8) for _ in range(10)) + self.player.to_bytes(2, 'big') def modify_multidata(self, multidata: dict): - multi_key = binascii.hexlify(self.generate_multi_key()).decode() - multidata["connect_names"][multi_key] = multidata["connect_names"][self.multiworld.player_name[self.player]] + multidata["connect_names"][binascii.hexlify(self.multi_key).decode()] = multidata["connect_names"][self.multiworld.player_name[self.player]] \ No newline at end of file From 03cf525b2c73ae322a101c5f3a3e97aec67d21aa Mon Sep 17 00:00:00 2001 From: kindasneaki Date: Thu, 23 Mar 2023 23:11:39 -0600 Subject: [PATCH 101/172] Webhost: Add dropdown menus (#1553) * Change archipelago mod download page * Docs: change connecting to archipelago in RoR2 setup guide * dropdowns for links * change some relative sizing * change links and reorder links * dropdowns for links * change some relative sizing * change links and reorder links * mobile view was showing on desktop early * add in missing relative font sizes * clean up and add a temp downdown img * move links around * added cloud border * move arrow to the left side --- .../static/static/button-images/dropdown.png | Bin 0 -> 9836 bytes WebHostLib/static/styles/islandFooter.css | 30 ++++++++++++++++++ WebHostLib/static/styles/landing.css | 1 - WebHostLib/static/styles/themes/base.css | 27 +++++++++++++++- WebHostLib/templates/header/baseHeader.html | 22 ++++++++++--- 5 files changed, 73 insertions(+), 7 deletions(-) create mode 100644 WebHostLib/static/static/button-images/dropdown.png diff --git a/WebHostLib/static/static/button-images/dropdown.png b/WebHostLib/static/static/button-images/dropdown.png new file mode 100644 index 0000000000000000000000000000000000000000..cbc8634104895b552ed6c1d6d932dcf98b805973 GIT binary patch literal 9836 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 Dzl<*% literal 0 HcmV?d00001 diff --git a/WebHostLib/static/styles/islandFooter.css b/WebHostLib/static/styles/islandFooter.css index 96611f4eac..7d5344a9bb 100644 --- a/WebHostLib/static/styles/islandFooter.css +++ b/WebHostLib/static/styles/islandFooter.css @@ -15,3 +15,33 @@ padding-left: 0.5rem; color: #dfedc6; } +@media all and (max-width: 900px) { + #island-footer{ + font-size: 17px; + font-size: 2vw; + } +} +@media all and (max-width: 768px) { + #island-footer{ + font-size: 15px; + font-size: 2vw; + } +} +@media all and (max-width: 650px) { + #island-footer{ + font-size: 13px; + font-size: 2vw; + } +} +@media all and (max-width: 580px) { + #island-footer{ + font-size: 11px; + font-size: 2vw; + } +} +@media all and (max-width: 512px) { + #island-footer{ + font-size: 9px; + font-size: 2vw; + } +} diff --git a/WebHostLib/static/styles/landing.css b/WebHostLib/static/styles/landing.css index a70ac69dd0..202c43badd 100644 --- a/WebHostLib/static/styles/landing.css +++ b/WebHostLib/static/styles/landing.css @@ -21,7 +21,6 @@ html{ margin-right: auto; margin-top: 10px; height: 140px; - z-index: 10; } #landing-header h4{ diff --git a/WebHostLib/static/styles/themes/base.css b/WebHostLib/static/styles/themes/base.css index 1c41bd271a..0c491f7b57 100644 --- a/WebHostLib/static/styles/themes/base.css +++ b/WebHostLib/static/styles/themes/base.css @@ -42,7 +42,7 @@ html{ margin-top: 4px; } -#base-header a, #base-header-mobile-menu a{ +#base-header a, #base-header-mobile-menu a, #dropdown #dropdown-text{ color: #2f6b83; text-decoration: none; cursor: pointer; @@ -86,6 +86,31 @@ html{ #base-header-right-mobile img{ height: 3rem; } +#dropdown { + display: inline-block; +} + +#dropdown:hover #dropdown-menu { + display: block; +} +#dropdown-menu{ + display: none; + position: absolute; + background-color: #fff; + min-width: 160px; + padding: 12px 16px; + margin-left: -90px; + border-radius: 15px; + border-left: 2px solid #d0ebe6; + border-bottom: 2px solid #d0ebe6; + filter: drop-shadow(-6px 6px 2px #2e3e83); +} +#dropdown-icon { + height: 7px; + width: 14px; + margin-bottom: 3px; + margin-left: 2px; +} @media all and (max-width: 960px), only screen and (max-device-width: 768px) { #base-header-right{ diff --git a/WebHostLib/templates/header/baseHeader.html b/WebHostLib/templates/header/baseHeader.html index 80e8d6220d..5865d50988 100644 --- a/WebHostLib/templates/header/baseHeader.html +++ b/WebHostLib/templates/header/baseHeader.html @@ -11,10 +11,20 @@

a#ATm- zj6iP#Sdr=7z$^G&CjXxwXCB5JCYC-U6gxfes*!52bf-&V`A`A3y)av64xuN%E)-h& zVaQge2-Wc)Va8ieU%8$xDE0pC3V@BHsgD7GlX>tmvje?lNnm(FD$I>_I$TY6>kNt| zv#74(2KqzK^9jE3spZKBwxhHoL2YCFjF{RTE*fm)z{EkX(G&XZYN3H2vu+P4j9Mcs z4#ZSAYP>)4>iKOV$cQb3axWy3yS}RHUy5rI*PwV0_Mxs$=!Gtq7Mx~Mgg;-!?B8b? z@!+{+e@S2IA<3F&%5)+~Jo*3`8#C5_e#5^#V4}Znv;A9!Q5G@g#1vxQmU;dI;iLOU zR`#ED&EK;C{-cjsmteB1fM&-m)HFt1t#~xLG-#jR#Gqgw5R`XK3w#*c1T8K{Y>d_F zQs8v!4oD6cl0UpwhUTc{8=bosx{qVj$XNeYZR7Cp(T$xDED-d#{OvxBzx*62hpw{> zg3sL!>e%hn1m7{N=P}8B&$5>g^=&F#FZRn2OC-&G&A!Ct_Q@xwiTqYql77fjobor*b>1ej;&>s>1ek`aq%vx^^W@`qDvvI0kU7G^iOVzZ4->j zH0RDZPb6RJ80sx$`JzWd(0O{>?i8sm;VJ!LB(?VFSWAnz;5pph4vBlzCNKRpkEyw} zuQr5^r6ofW06v@m>M+JzZrkYWa&wWmICGiZ2>&CK1hUk%O{iX)75xIG4@)zIINkaWuh$|6caI#B^spJW^GUm{53f94C`EFo^Bt?NB-F#X<~8JO zG!6PoU0pwfFDM2Ie1~0@;mrCltRL{EPuNyO+iT>Fe(O$%}{0lDA@!?tOFRw;w(9jq_|xvS`5z=MfyU~UVrn<4(@*^5mWuQkMHHB^BM5;SR99X;#beJ-FgW2`KWbAHk- zKlyIxg3Afnyw8etYYK%bJe-I@iOG-IlzvilMWJh_%N+$5+x>lt!L&ow9@}J>_XmsN zt58im0w-8b?Ag==|F6@}euw?$`BUnNWr+<)!8vzV_lfNVf(xE=0&D(h@qRA{=9E8i zb{n{Od+^<|(T-e6xHocReFdMKJ&qa1k~$g#M$P)QDkQ#&=br~FOHC;7PZgi=D6`;AiF!&B^?R*d`3X0ut371o`L1mX~Tt`o>KYAzYk zNaVF}9W6kMDR#dMmSH^7ku2l zfS}EoPzTV{`RKJzxNtQCnyGKf1h)ADsLaYx*zPZS?IN{nT9o&c61Z%Mek+zp4xNZ2 z_&Zg0qg889Z{7KPo6FUw4vWW!z(Mqb#;Dn3Lvamvm~7znwi}5jX`MAf2?Fjy&Q^qmDujO54E0AQH`LTpsP^qlWiK4 zTOz+1@XWoo9CpTU6r>h@KCrbWp_koCXYYm6$+{1uB}F$oGaE1~OgLF{7=DFO24A0t zGrmdx7?nOSD&`;Xgfi@1wW9^6Gv+OOo62%U&Co*^H8XA7U%Dua9(o!(x4O^O^#U!C zLoMgld;cQ`40Nc&kW;2_@`_#sg|$azci^`To1ST%TG|f5$0739`)!RLAKrGxjF&dW zdQN7mHTLgwa=EMlSOq&^6@uYzp-lf4uprsj1se%vX%7lrYWd^pRZA<43ZP2(j!~^b z-vD!b?AWQ(X??o%-~g+aXLg*K(T;jH747QSLLGtvy(g=XoFmr0c%5F{-ipdFR}IcS zuM|(t%B3_r_4fQ*`6l-{bgiXFs_6|M5FSOi1{^-TIhUdcB(KJP&Rz0>o8hznakSIEaw6>!hD^7 zcj6s&F2GV0{fVXcTmIRvMBo0dH3XE2{tC$d5~PH>SOf`^-J5Ead-qgTcB8~HY+Vy? z4ZSK^(R5>Jn-{wF%K8=0rYj2Cx}yD$f|>|qQ|x^-9<_5Al5aZbGI8KS2Wiy7?1Slf z`_!T(8D&`Ok?Ol#nPxFrDuRk7r#*GvB6fUp7o4qpnVxGkVq^26aPD1~NNeI}1MSaz zSB}G2O<|ipnN8EBmv;c(kcK;*3v{EIm?g5Ir%VYT1Iq+y%xwKv>wW#ycTiO37|}P; z7?{12#^gtE-r=`9o$sjb@3AbFu!(eD(wekYyZQd|No~0&3VE;(eFW#+uxDa%caFch zggse27q}CkJWe(szHx(mDN7*&szi`XTpw}cSfj6!PVC{I5v(6tms z^wrx7NY zfv}D3E`3wJ!PNra5Tapa=oxVD@qNzw*=5x_w>?pA&q~(5bh&Ot_$e$i#!O>ZP^UN_ z)Q`@+JmX`T>HjiXB4AX-yc6f;c4kzvBXMt8b;QAH{7{|!U{S>DE%AHI`>7rmR!18o63b9=QOPEFpAwa3UnCX zM3dUGo`vikxmP0Htg!t{fz;vyGSw)|1ahKXC)<9=NfkfYkTF&mBS-$|CTMbm5mm?{ zBmd-pe9bwx5%#W)p6{?E25C`hG?ndNU5Q5m9PiU0W;J1&{cvG5>N@RmkNn{U*`eVE z)yzJX?x8%Twp(NSE6ScUl|H!fc=2ipeZ&lVpWwP2$~;lsFPG7~{7oOnIlyUk0Z%y) z8AzI4yb*qtnjuC%QgoN^ioLOkZrz^F)!gr}leC5*qbgNy$Ye|ybap}!Au*4?N`Cxo z8cnIm({7`VQEp~r4-L=nE`7F93g%5%5B)^0OP=pdR7Y{Eh_zL-xbP0cJcT$Btu^bsvP zQT+DPSSi{`BktRGY3*Km;`Oz7$8DKuu&KDAT0O9N$xHYqQnmUt_ULk_P+VIy_EI8u z^(F+KxtxK}6sxvJ7XxwBp+8XR;Eo}<`nV7JP}d<%N;Ld1U>JYx{cfE+3Co#=T=Y!PKHqi!VeDgP(Rg_g1_tFeqi3F`@KB#mXN+8aU1hFb zm-4&1w`vibe-zi+lrrh%(K|u!3Tbi|A4K!LE>B((5KPJetAP`2Nvbw0A$w z5(ZdOzaPK*znWk|Wq{|z5A{!Q3(pU+Ei96oAuH5bD4nA=FQ>W1=OJAq+ijG#bIso4 z=?gLaqULS2iJsQn`SfP=Yqz@qJKl9w-R!u;o+C@N;%6*2d>109 zZ)Wje8@$InTia4X-vP;xh=Uy@?$ByZ(n2eiiI5S74l%s4Lv9Zm_ ze{SZ*>)URlKQuh|{8&O777RS%S^=C|2(6JghypIETWiJ_;ir1N=7(jb90J|_YWd`g>CdVS4@i=09V`sY9$-l5PB66kXh}%p2O8Df)CPTH<;7sFT2T(U9|Hx z6FG6|o!!&?vN?W^v%(IW-plub>U{QM40)QW-z9fb&22(jR$ggWv0+z1MnKKQOF_5F zs6|^7&geFuS!B!QpCqHMFftLYB2l$l$=-=?(OcU_d?TA*%dLgol;{|Ha8COx>G&)) ziJMmXP00Gaapnusi@8yP(fcn7*>_M@lzGzOMB08=6>(m7K?rT!Em)VV6kS0Ir8rT! ziY6OwtguGQ_$dzKN)*d?O5V9xcYv!^XxR+s3Z>G->H3tlhd?K}mLF4hXNav??o2}? z+_`U6Bri}$%PZoOo-?(`O7{`E^R?tkb61P>K!n0cAnK?d9>QmSu03ROB|095L40WR%E~p@j?-O??>fGVdytu zn=x2bHCUJYD}`9szgpKb-s0 zSF^q{Dm6OGo5J0+yUj--MHz2FC_bQJnk;nw-sHXPtJSjhZo(wAV>y5ZIKugyHFi-F z0GnKF1kyAp3vC|KiRw1*&X;{&v8g0Us;q0#$|%|Xm;xcuMR1$J1;PBfh~oOW}UOMpdIouNs6Y<#2YPRaS4lpg^&OL4dc2b&0{2 zR~M#P<6AcFzIy%*UFNnNl?53Aa2@2U0(>{2tCZcCt+UD6=^B~+dWSfy!s5raIi&Ek zOLp-G@8svfZ?k5sX~BmHBXsmQThWRJ2W6Vh3kvxJzEUFRtgU3%H+9##ATcP2$&06* zw^|jkYEU%9KVCYMGoZb{VY5Zpo?Imi#fYj7^->xd%+katDq-LFJBJ%$eS;i3)r?*_ z7g@H~bkj5!5?;R;Q!HZ|yIOqasjs81M>N7ey_aT=GN&hhJabGE**22K=i3=1fV$A- zj;mI##70-MWI>jl8P#c*UsRR8P9D0=YJURw;Bf+ zks5p9Z1huPfjE!xJ~FyVPFsdyZc$!S9(k#Ye*zydCtYAR`l4e06%FeswUelhB*V6{ zY5Gx2_%-rk`y-zraeJr~9z7|iNT7_rX&$p+^l#AjuAdrwhFV?Y=R@BFkuIAWBH!Q)PVUBjwFZw9;hkT2QmRl^yCmNhZomi1lcK9#UX0V$D;hJz;AyfBSX_P(Lpd-Xf-)83moqKZoH#b#J_8sr9X{|qtHTQ$X zUlJ0+<>nNk3~$ag#2&?`E^qMfzT=|-VxLeW5cqf22LXEsc`ku}k`bitKE$_M*itl8 zlty4406^jD<^0|y9_{MRgArPc-1roo*b|FvG!PvwcCNdjTjhN5e^70FvCtfkG zfjXBX;#uhXd=!A`aitXjSl~kj07#RkM}Z`QBDx_vA3Z}tTnCEYC)Ut(gFvK~7sCe1mIlk@G`V%I#J4o3Gb-ruGAen68)_*PRu|1@Gj z;yY~XEXp2Q4akN- zlj1dcUfpVNsww$0kf1!*((obo`cT)5%0@z(66b}9w;yP7_Gh zbh(6yb@}||!p}Ww+Ig&v##-vQ!A`d)`3nxt)A3Jn?*;FezXfiXI}EqDla#6nNMidf z;nAz9uB6nC&=SJVnIKu4I}LSr-d~e(^S)l`_N?r_pE9eUg0=O;K)QQOI0Nyb$jd~n zr3o%TxPpv;m$XErm{V*z?$KoqU`Zkdf(8%H_N{W1sgxx@QYmWP{}x{wv+r?J$Bm{RUVj?{fN3MZdL$gke$S1H!su^z4)!|Lw{kg!`mQu?DZ`wvi%Vb6Y&OSqi#h$2cSL5>7A+24OZ|xjY_wK9{Nskt~0kn z9N8mKGF+-@>`fd24pykSfT?NR_?8BYGwI7dUYMa)eHM^qU0UQNI`v(r|PYl){h?xFX z`uR_K<(6So)Jmi46Bv(b4?(&1UtfNzKA3A`Rpyb@RFY=Ak;Km3oW}$PgB@61y`VSogy z?q$I>cU*WdqYz-;GZVS{(5qlmAs83zIqQo*ATy1_Xl&6%0~7*u2+`;ss-O}_LPnb* z$giu9hc4;7YAEA0V_mFx8!?VKzk{DmNMYki@9-Gv%4aw2{z;me9R6{PulXA zn&9t7cKt6Rx{!d>RHut}#_~XdbsOCEKzjJ=F_aBdcWXoWZs%hwEj)`#a^{1QI}~2< zAW=B$o#uvIw?Ch{r92PrSej9-@#*a-otKfOv@eDww1i?5(Q|vUxanFpOU{Z_xmsYZ zHTMF6b^=0E4FOw|2!kBODxE?XA8I=ZC2U;tG4R;CEEy|sN26diV)@nlr3-hWKWH)h zBK86df&{xXPLP2E7`hHlX6tUpcn77l^a-38^h(o>gx%)C@ntR<#GDI-+=*aUZb#V4leR zI<$*);2PY&+p$0UtM|&GLVw?y>a#%MZ>j2J>sua*kmkVVL(>h*1A8L zMGIXuj%kkIH#zkd%wCp9>_g;id^L#gndrn^)#64pARs_e(|3X2BX@ne@o{0+BzKtq z^N-n+^1Rk!nikd9G_JhC^ECT0MowSiIx%niS&RQs_Q8{_@jnuDd;SJ&{a3HQfl~gp zWcusxFh4A0xBjm19w@PzfQRu@nVl`!61AzvEBtW35#=S$&ec1>(|?bZjGn-OG@qom za>CbC-VL;>T_TdRN|!g_rC7S41)B2eo23kJ*({Fne}~BpyMeg%e3M?yQ$KibketciR7`ie4`d}TG?VVYEsOS(TV&s4IF z1eoQ+U?h0w^`Xf&;JG&E{OV@SUv%pH#+|j(k3xRntdn{* zc(*f?my(&Q%P-556{C_yFNyQ8GR5C|gC_MdEsfU>ndi*)`o#T|RJpe4#}oQ_m$SZK zW@~RxB0>mFaK=G?v5#+l6mtxHeo68#|5XpPjeHE_qXrn%u1wEf=su_&U2gklK*&Eo z|NTnozcp6t0n`o*a~A-H1yHTXRzY9;I0G(IN{6`apdDv%d^^yKHFVz&LRO$A@-TE0 z30)?VFT3v=)u9a|K_JPoP=fE+$5CT$oIt95*l<;_b~`R7=fCT^`K#|)2eur{Y}gWk1oQpNDGF#2fWgZ-on})*^QXNj?3k-`pfq%hjr4` z+G!t%+_p>QujA6JuDI-zk`m!_Dh)&HWX{D@*7Vnm5Nt0kxleaJ!OmveCydnGM8#-Z}X`8v!>ii;NT3Y%=ts5g3aebM0 zj>tzK>7jU|J1#&4em4?TzZ84IQ?pPfUOz$M6CY$I-j-LUrXs^7hj}*ZXB)H+Zc3+_ z1PxT;gVh2A>j@7l<*?BjN9XPkT$GF7nV%zP72m=SW#+xtd3TUKMskF8XxnuH?Etf} z4*@F`Sr+omyIc+hQ_zhun8jtG>qif@@0q8%>b87vbq);Fl zX(Ya=q2}hh3bGmZ(@*ENPS2(7t8piIdLI1zH1-z3n~fcYGt7%Wo48-SvKGf=OB^$( zd<~k9@B&lx#~@SxH+-GnTCKnQ?9cvYe-twMpLM(`AQeaS*NcnvD7nt6<%u00(yt0- zPI=_sLEPPXR|A9&_kHw%nPr=tUeuy&s!(ld^dyCe6rawxJUn<`y}BDQS-ysQ<@%Wb zNhk+<1BYPC0{L)ht`6Re(wH0?DW6gudtI=5R3?DAq}v>D4|(Bt+q$22_a8PdEvOG! z+?PT#4{A?SguL^PM0YnOTbG@=aE$5_HoBu>?1ZUkz;m1ixYldTPyX4!VH!NChuoES zdXl(##9;h8OklWzF{PUNz)#_r6Lw5m@3sfCto9)eqYN1FqUbs^JgSVz#>iHri${v# z!oPufK808^1b6$YHS&pVzzam}|H{Gq@%?RtIAIDiPh4I64l8CK%l|nj+x(pqVwpgl zLZ3+GWAcE-{890LR99U=4lPsTy9oB^!TH8CwO+db!a`(Vnet9g<=lY1JBNpwYb>VX z<8LXPlB6olno`=j&<#Q8y0f((5nZ)>%=W~qpP4d&?oEZ*Lup9@N0kHa$)?UG;4OfrYo4Ni*Cx?8s zaXW?M1JCILUgKZZjrq~8h@s-fMEK6VkPO-M#wt9rvZ-?9x@9IM{2MWJD1gF9_f=I} z;9io$C@2euCcxr4(GKvziw)>E@(plE=bJ!vk9;XdVozzir^(}vTqx4`$x`yyxr-}$ zGG#_m>_@i3USUBdbs;{3`HsNp*tO`stfn(zb9=VbXcv@X44-96_Sk>X4>vUI?bt3P zseN}x(MbC|zxRRIsSqM|wR8JV*l-Y+{r-CC-;R}rFb#KX<+>za_v{fAh<$T%@3rcT z(2I{xZ&Ans;Y-#KJ(LbAAvjMR(iD0r> zqbXkKUmcG&X+U?BGd|Em+ddjG`9ed`)6M9=AcKD3QudOt>lEx?F+B6q8FRyq(7|4}*Ti8_^=nz?LQClxW)&$Y%WPYY<6@1E>cU{3WYS zf;u*6Sger&;Idc`TH-p}G0?F17YBeS;vX+>KK}(KheVhj3HrgR|MTMijU%7uFIZp0 z(coX=(|6~#j`y}J(ftaocnP+UYpMa}np(%l$rM!QL-&~nNx`6hfaIV31Ni=9dKH*| z1nbKT8~z3*2{t%4x?OL&qg4uq%OKP032n@qVkHeK&ZV^^4?rQe(?y*<^5?CF zGm*6Y-aWrfclmF97hgD?FPj?Bi*_~|v@*riFZ40h$!=}pa^$Fbr?ybjB6n4OYfsu+ zaf#@z8h5Zq|f&qR%k91Z~o@IA&h@@@jqp|eqPoY z_g2gR_G4(wsn{NZW5?YzqlY9L%?WdiydAmafh=sKd(Iqw=)E~Ek$owC`1!U?&93>3 zD72%x3iUK26?JmlO~bLaynEudrX=i^Bz$mhWM1GZ`1QXq0#JmLv86pA zKwPF~GqM3FZx5Zfm+n0=oDgZ)eMwS)!r8AH|AE3^vc30nOgStd?(TM>=EH^`Zb!RQ z2N}2mg8g!6A7XIBI_)6khwTrEA#rnB;g?sds54wAF5A8Hv!4l5*KJ1*G*IeuI;K#{~I^r zZ_eW%d1X6B3Ak9@Mdz<<{_4sR8Fi-qll9r0F6}s5&0Xc;<2fa&;dU`byVktHbgD-IQKw+bx6tN+$1`>QI z(Bw)E{)_iu*T-JWo(4XEKE2`TkB!#qI{KTMZ!r@BImszY#4q0e*v7r+z$_d@KlKEP z95QZ3%utK~nkWE-Gj)Fc*2aM^&=8AdvdXV;{iIOf23FfA%0vfkq5&ipoquE{e)fqg z*p+j;SAY+Y#k|_~JFeYiRW%3=@e$fiV1&tp=yZyE)3TWFVPX@JXV)btfHl53L-w)D z{V|8C;Fp-zeVg0Pq;po7vk7%rR*@yFK;}IdI!PjmIM*Jbj26aExJuTRMo`6+4ohgy zh+WufOw^Vbt0tKBIRYN}4fjXld-~N7lTrG5P)vpk=Zg^tU0)epB^qaWT5;FugT*1p zy>-q5!eugS@9av8rik0nSHnvKaI6q=!lz-tn}Ik3Nha*FniJUk9cEb88$1eXF^D6T( zBYAAe!Wf;o*Z9&opTnagSCYFzY4TJH#?x9plqnQIUNIy06^&aRCAss?Ajsh(zCmt= zuC&Y7_LCBX9!*`>a~|^#(H=dxW29H@RaAk%G80UjnhXt)y8p$a|BcB2&I73iZ32x% z1jzYs{Q8I7ut_;>Rvr~KNy)Ccddszb``saaf7aXjta>L8{4^z#P(S&K|7r?n{nxYs zIvf0D*&w^z5{X!j#X&zW{pAegU4S*$|e&EiMa~5THAp0Ol9(e|Wm72n3xS`9<_6m;2`{yjLjS z+h{vV=aZW~6(e(^!7QNMH z%aZpK6D|KgJ*)qjB-Ovg8#8Z(ixMV4kW_aK)c*Z~oSDLq43bh%kKXRu9D4pC)+4Em zahWZ~{9n=ZU2Er4sA*8$Byz~^a>!}diE7B5MBi1o zyQQJ53;XSE_Q~B}Z*{BCWZ-sIoo{M%x>shI=)D>(UD$24{LW~WAi)_8ZgOw3%LaGI z!nN*&E7qu(=JeB2RigH3nXyI(Di2BLi@1$G`ar=&DPlv#clMMr0*p($ylljW^gj%L zNZig@BAt~wkaiblfVxOi@RbM1wfoqhPYLi6si+&P9Ee~Iu5vHPl$1Bnc5S`x(8gk$ zQ{GW{#l_U;Tg6z^ko>q+Zb-&yQ6_*))GGf< z;q`IJO^gPlmrJrkS>(&%!NK7=7bh3qhAlkJ6463Eu$))NO+jXTe3K2bE~>U0Gz%xx z`NvhL^O;y@{kN|Fzi21$|BfYZer%HXpjHgR7C4&^tv(rwgKo|sFAul+sSXyrt<$r| zoAb0$UUQ0^i?+KWtc+LcHZf2xJq6a=%2^_3xYdR0vK%(!o(W} z7Ue^cq&G|~H)3}BzGPFw7N-=uY<|G#jsC<+ovt-14H_bX5o)V=}pTOz$w0St`ss%`gy?dh$ z3()!(v;3c~5L65>?MFM8YFQvG={s!k;EOuRMCA5m&d`X7hYrj1WB&7E0}@bF)w&P+ z(dHI4Poz~JI#Nw88=9^kN(j%hYzfSA6^I=ev(nYj4|6yZyO-*4jl1uztjq4w!kR2g zqJMgWSEqbvRcELWtSHJW!P|*@$KFX6-In zpGH*osv(Xm%qB`m$%Pej*aaWC_bhp%@V|5!ezsWtA@%w1rGl9GMAEHZLTvT^X%+dl zn%T3eO8Iy^{=zdI_a8;an)~aE!gLyRkbe0;v<&f$jL56>bUjA0w* zZD=5m&2}9rpo9<#>a0CfoZN2<)k(TOw;0)>_X4`US?i(DLf8G9aF#C$h5qKf@tszq zrTe?lwuGtk0l}zjS`f;MVia9%lf~>nY+LGshoz~O9h={1L@PiNXQb@=3w1`5&p#zy zI4$eh%-5eAG>9x!$1P-$qe?H|u&1y^dRIN{Qj;OgaBmoeDV^{Q8D%rBSWm6oC&TOS zBuE8S!2PLmrJYALb-@(2J z2rEziMK{2$t@|2-lj2SNV{fr)G74M)0>lhxl#tDj(oW6eZ3Q9zNjbx20Wxl zQ>Bs0ZfV)>?Nanb{!k``v*@0*U1^Q=R;1`z(Uj7pXVZ>BQD|37fR5=UFz9@L|1bYn zF|fKowZKlVvX&zCKrN7D<@l+Pu9I1oviss*{|i+ zh)6VEM7~a5gn`kb=rj;_pB}GT-UWFm4ahpUX>UuHT0FA8%la&;%=v@XQ45&&0X6%6 zG*uLl1p1$w1s46o7?}LS|IM3-WMZHAcJd?a5Wq0Egx{=|A&sh7Q4F)PhAaY84DWv# z(5EKwTA%%>VZkA}ZAn@5z|pjm4FnOj(-co?9q2~xNl}@|4*iyT_&e4d4Qd^UxL0c03Pps{2w|%>9DK9c{N@k3BE;w=v<2 z0xvjZ)u+MkCf|r8160vXotLS5U*KuxgJel&_whuhfoeiZ8)p@wYVZqlsrD&O{zzTE zTCCyPVczl|?Sc}1>8t+F(NrFU5~n621WfASN{g+UDw_J5Y#zG2+HE(z4!=Q@tBzZ@ zb?h6gavUAD68^}2j*J+1z~nee)}c=7tDn@RY=)4{f^v4cy9*%&dQD{p_`&y_Xf>Y;eriIPrz=V$n zvx1#md&?4zsGP$|evt92I)e%ov}3$pt`?^0LMvzb`*1X!^;?RyC6>$n)ZxWI3@_b*_`moT3yzyph8(10nD5hq2m)++d^T(-$*4 z+0Izt`?66ygZwcqwxB=drGrb^_oK&u2Xh-pw3&mH+uO4s?KY0SVFU;2sPYR(CvZG2 zY5RH#P&&_NDDRR#rnAP4>KuT2v*HEh-z;xRIcfKG<yXya@yxJ_nj8jka>$>veEt3 z0JmL@s*IY_Y-&)9(%ZYuOS?Zi#U7(RYz;Zc?pZEfC)NMz2hn=Kv!h~$_{-5jkg)Wf zpvh<8P9Fx)wu zIs;4Sg&LU6APoF=o`1%49l5h?hr? ztn3CzIHL-xniONHGmby*{w)Q{e9WDHOv0y3j~6YX*ZEDg`A?0*)Mciy z!*)N~Uh`=Jj*$Guyt>N8zH0BBJ=3i;R!qF}R6vKMb)seiise5b5AEz=iq=NlUvTsbhr;45XC=%(H zQI0P^EqS524kS_bK8fD8XKH6(G0JB=N`3-)zZA_ipKxn#w{n$}T<~gs#zBuAIVPVN zmtv60=(=NS97YU?)FUn*c&SYoFHJ};O9mqxt9U1LX19TI7Baz4P-s>CH>Tc3a#YaB&RX>x} zMAW~zqx7x6Nj}_Pr4Adiu^g$UX9;Z9e|u%;KX-DHfQIB-iT7GJTf} zDXsVw(BLD8RJ%D+YtE;ick>2dRl z;yzm%OWmM;rfRwFxNZTkim<*?lsm=M*d013wP@l&#v8@FgyhKyOc@M(ZJ|U}ul2Mlx#mF}P25-HG9zE(2!2s)vR!rFsMJ}q2(o)_^Rl^9 zFT|?kd~=5`ees$7#IqM=pDJ|=YlJu7YGlsS%T5j8@})E8cYOs27mj%Nej>W=$kIYd)o>(kNQC-`vIXa=sNG>s zIeT$7E=j_^v9EKu<%`aW{Y%m_Z~hht=y&zMnqzJyB5D>E{BeK0ODup%=z3A`ZSQ0-Ic`T z3i?Rgi8T5YBr?CdBxrk&SE?w4Bt6-!UEw2{uyF5Xd4z?nOR9F)UR~43ih@8N0XvJ#`8S zQ;1z`p#!4$=D=`D5BBw=E?@l#LqJv7E{dhxs+PD)LaNX}apgI1R;}C-;_k~Teko-R z9(a-H+xy7o3y~BN5?dpV-!+Y!GX1b1pKk57R$0=A67)vP8n`%#h-cHN|86gaRl3}r-#&TOux9++oQ*6^X zVnZ4WN0TS7Bj`5p-~OSvc~f*!{Cf=09r`5B0yKQXDQke6tDNzd?lxR^$#4dr5rzPH zl){;Zf9MVsm@*wjbAIm$q&IqzU#|Z9e?O2XLf7p_acB5i3THs~C_~zAfUnF;Khq;= ztrxJxew;A2DV(hcw)&Gg(Q`GhjJ0-6E;B{lbU@DGxAloUylad0xBb-FH2(7My*Tp?Br0n4brp-liL$opTNFqt8q=ROV3Alf(f}Q;N!cH9r!~Cg82lkWZsL zuZ{10CigDru)%c^w6c(0)K#t8yRtEgT74JeV#`fs;xJ%_)^)aoEz@R$KlqbIfm>S@_6WDG<*`t`ST-19+-2Em=F z=;4?drX+bO+o^CiInzwTyF@j(=za09Vv?`b;oS&D<-UniZ;z}zplzQ6T@njXS`%my z%ekN{ePq$vd?aJ${h=&FxzXT{g49Gv z4hVSdBFi`0axlA?EleW*vc@14UE(JcgVeD7Ji6u))llHq(rh#kw9?|~I|kq3+g=UN zq@U^TRH{)`KhnYZF>FS5z89AtH+b-TharXG3P)s#r#jC*Pa}Cwjj}PfyE~DH zEOhP8Zw$}+jm`A)_-+m2)_Ww$ZpC?#ypwTzV#j(9Z_^!953_T09LZ{ns&ZaZ^QM8{ zRqr}ZjNCAy1N;?WH3b(G47rNCQ||yoDPgai;D_Na7!{vE&#Hr%W9h>kH6|AqUX;AH zNwcHb(m5$?i+jl`7s>9SbQ|vb!& zvx7KCzX~*jM8Cr}4zPbav_c%DF_4b{4H=+5&(_%nBa&blv_-#jr40~5gP5Zr0 zB6iLAB;QPTu17$^`iJ2QzhO8O%EKXLH zl!q-v`(_%?%msv8*rpLuXx;)6F^29uwn1%ZKGap(VJLdSMda-(?+%#Fh)Vk_$kC7M zU4yvI;(j?6xIt=g;JyF0`T4(1g-9j^YN6O^LA%I#jkXFDv97_HAmP_3CN)o1cQ$8* znT$psVY_{5+2FwFW4ID?_ziy1lXl)0PW38sOl&Cf3sQm}y(7v~ZK^XC`~v{1;Y08H z`|tg}lHQH?_jR6q-JBDV69z(#x5g*g*Nd6kQ^^^YuOWv*lo_S8>NJIW(i@^E=kEGC zWRql58I-pD1P5zbU37YC9nPAONpN=O40=ywRRSIHeBV%Rd~@46^8PJ4zw$A?=rk{A zlofm8lBt+J*Pv33jeV}>%QPHr-Cy$DQU@@K_d|lZgZ7a5Lqlu%=tlv+9crRn(|O%# z5#&H#eGZ>hEt!1P!tQi4=-X44le#z)Mm*RJYgnjykYG9_*idWVS9xBm(A`P6qWAE# zrL;N6xvti|j*@XbTzAbe=sGoo6P8y@rx7kzd&nY8Lk@N6tlc&1O5ck*7c0BSXuI*26BtYHM|G zsesy{23H{$>$tmIpDH9I&4W8ASn6E{0ny16oK=But`!`;HFk_5_ZYqNdZ+mI*WJTm z{7*hRKATQA-qz&b^1~I_G6UJs4%oKm&+fKq2~+bxuguxXQ?|62^eYK(YdC3*IG8ps zWI9_{z<9Qou7h*fs7ah?tqKrFyHL2P(Trjb&_t&;1Y3;1dsEd_d|w&HnBqovQfsh_ zV@=aO@vQ2>eZkT@a1QkNQzAz-_ry~*qX~TjtBLHW^eNxS7Quo7TZt<>8e@YdHnnV) z?uXySrRU^&e=bq9MNXIBUK80ZtNRN4IRX=cUCen=1W0v%jN$n|?yxHToLl7q^-iFw ze)`o(w2%w6U&V8b!`a->dBQKt&-!3&Cst&%s3M%DMh~QeMT~zqvuXAZjaECtWl=mPRh>@~a* zh`#eJ%pmks(RbM1+bwTsG1F*rdMWpYbei&a7{L;>)7c9Gh+m%$5T}y>aT;ht*)dOl z5dW~G1MorhAw7th7rw(xh6|b=47!xMX)+_Eq6o`g#E0A9Htv(6?P zAYUQw^Xp#@|K$akzcA6+X#K$m@1Q3jrbLStYNz~&zxeH!&_6%BZ8+d)AFu-zx}ON> zkAJ$kk8~x-le@v@(AP0_oeRD8gzA97CHHEa(v*X#nwF^ZR-Rh|0I-6S+8LH>nUQBY zW_0HWNL%pynVkLqI5}&*-1Hq520YenOkOCXDKRCel@zsa*$nIPc5sWlj1yVYh989o z&O|CDkO&w&ila(||HIyUhc(%!+rlUyDncmIOAt_0L{zF$f+8RyMidA|h>A!v zMnxb%NED=(2q-8Bh=@p+E)WP1ih_vL&`an&p@cw+-}CPC?U{GpnLT^YoV~yAoa>su z;7Xp7Jo%OTUiZ4!B0rimDy5`6_nmRm;QfNI#jXy=f#0d`!m{^nQ)rVD1g0>2LZxP9 zrbi=RjvldM0@f{FP%}l_)&Q;Z9JD%_+<4KA5g#^7Fo!b4;icUvTlHlv9L8 zvcr*+eZLHC9Hc?FCVA%ZuZT>y_ z{6A)009U{aK!NP(8N-GSrUjHCbsBb5<0lKlhM?aLzXdk#cWxbD0jZ+MVE#5zTc{pi z0GQe`DI3nvM}w(K(M*#^GzU<_4!SCO=og3JrJj@=of7HeF-vtmh2p@@?t%vQF*PVS zYkKUYhbO2J8EprMU->QFe>{jj`K9QyG;7Eo5gD`h9-8zUWJ@@_2y~jQ7pXdcJ{Z&$ zhNA%*-hLe8j#k)5h#Q@UqGjc(?(Zv*jFq6ros?bgGV{1k>-T%i6$WDWFd+;* z^brd1Y0=7es(eD3YWET}uW`l+`jvpmfZ^5aweO8vS>O?3a_-PKpY5eu%=yY5AT;>? zZ=r#uPvYnKKF^ys&17n7sw56481Ik$BypmV!XX{!2aCt_jFnde0*N258L#;rj}eb% zN%jo`N^0~zd|%nN=hM#y_~i~;u=`1V`!duBZs`?Ofwo$MvuC9j!ioBvE@G0%hMGOw z3G$U(*!zyg=d01XUOOoZ#gE0>8^^ru?!;gscP><>PlD-3HES3KsE+{3n^s~HiopBk z*tR0PRHfXFCMY3KS^L&WGqth4tlnA^=?c3!3^%s^tUBZXYGI5l)}(T8aZ;?wd7I=w zIs`A8o0@vPv3Tfpz7rS4K(*NiMnLJ*SD6qyc>Cv)_!DoxpWC^Af!9pxs3@=hT^SKU zgZ=t}rP-}zi7I+xWH$C){xGoP*S&lH4+stY?pV&h7FJ1XPTLj&nqrsKJ^+lm=E17g z=}^XZpaBW|!{`aHu~dcs85I97^jQ9jx%UtEQ2!snD1^%cgWD9!Jb@fEq1~Xn60~Tu zjmFfw^MhsqLO;wtS_9Mmdz@^|)y^@V#hh)SB_Nz40n>VT(1cwLrJGQSW;&RRg>#wo zoBsFu#`L#o??44}|6p+Xw}M$^prrr-AGmRaRg85Rv9C`g-Ei}JhT4=7wtD_5*{Ek+ zXSv`-%;T6n1`Bz_8o=Ol7ZATOg-Nt*pxF%>V)yacq(ABjAu{d2Dr5poEl&z^R~an0 zzM^V#(y8$iF~$Z|4L5*&!&cykBTh6QdR7Lz8^hVX;>Bmy;GH&r(d$$Zo}ns*nmvd^ z1xTZQS5lC^Nec2k}XEg#=EIb{v4d*+T_@;>jjf74CzMKA~Fv-l`ZMyYCJh_F&C9Ei)02VwCrdm?qiE1G7g>6R{64CUzqXfePxcNXQG<&Ex@gq zFn9y-RMA?BA3se7NqbnhC2R==q&9S^tHIq#JS#&(29>^FDu*gl&z7>2R&J-Z;_Z&!21U9BAQmeias~C-B+(acX8n7kK`9brVH>4cw$uar6 zlFxTLVEYVj1L7iI9QT|T$Yy-+O>?5Ck^$#oJF?!5$-}jo6w-u*mp>$=oTQ@x7BJ=? z-|3lq{OKWRKwb_T5H1d}McrudgFn;Na?Y5m@`{Q7Fkf^+G{O5}-YI+IbI0YM9t0UE zBfV312=&;s>4mGaFWGI_&tpMAUeBA$dLW^pDu!Y>5VROVRk*_dO!AQZF;d7N=OM%~D-rVDp8^@J9 zuoGAyWxZe$ja_XAKs!Ys+WEsZHhoS5o%7IDpaZF`iqUc5J${QVwvo1>h$69>gV;4A zFogi@p6LqBgsquwz_b^rQo=mJ+Z*gnpiS?;?>0{${1^p$=B4h!tdN##-D%Kjk7MIC zHho4fwu9lrUc zW1^4>l$9kk|qM!sr2z zL(+s4n^|h8M>POj{cbw{X#o8*uMMP|y|z0mcsIuAcq{RE-6oeGtclHwk7w_5V_dSM zpP?p!2gb+=AI2zAqn8Il>#C-{+m@1$<0X}dc7BY_*kcO6$MW$>wmy14rDORt1?xyr zjZ)w2Vd{|0tjnsOr{uj`D~RsBYrN}K=oqq<(QUmR!Z8E#8y$Jc*gQ>@g1-6rKzTXK0`Ws3thHiB^0D3U)Q| z+LxBmc-L(wI4vdOeauortraUp_xjjpm9w(=~8mcu#kv zEz0{1Xjvk%^A} zpj>t+{NQn%XS1npoT+XS6`-DiX&!yG{3w7I?}NJ+dW~K|)tvV#6m>i{T>b+l;6|1G zY1e$worf#*fv4lPZEu{}K#McI7NB_mP_fl!^qp?vpjA69-3#gDLTfq4Mo_8~(fD>mKKk01GI_)$&a&}N`G;B+ z*B7i+ zeF7D{=dnD`cl51YFRqN0fSAH805{pJJ%YBR9!L|>Kkc9@*}HW9K3X?49`VsWUT4STpylbQDc-Gqr@7O7z6rgRO6( z?{*_J>~;2iD34%VGgU0B@Xxe;V6B%qAePHLTq!60Kw{z8le8$m2LxK-=bD<;{sP&) z$*+~Ry5WgZ83C1pi?OeCqe#=(2BeIKY&>I(SSYx_Xqsq*v#Dn8uV!Jv1>es_Uj8h zlK#kFGg+i~;YDfJ$c5yY3dCvOc0YodR;<9wcvq1lmcu zDthW+%OjNWOMM@k>)JuG3_;k>{gC3%gf=>19pt94(Z&{)#CGIj$FMAS$i(d|=1v7` z>la5~;Y+p}YO%@jBr6Mv27RP2tWOjGwdp-TMrMAP6`dcoW%su5ks@4c=vLBc-TbMt*0ErM zb7k~CUT5W();H=IEi`MfZLApk_Oc@81uAjUI)1#Jf6}t=NJ-a|A&oJ;k?FznT%2;w zDW7xB7))(y_+sRn7xtFj0m+5)Kr%6&m_C949?CF8@24+%DMgim_~tR3NiA1#4K&S! z6IIVoCRBjk@V@DBX!DuRxUX??xneM#Tv!vlG^-gdf*K=pjS3V|vB~35!IpHl!JB*& zuhBZ*xl;!vgT`)}Wb=Gk$5;ooU6WM_ydYl0dKJuu1DNV-e^c zzJ~*6on3SGt*FYT?-imyb)u-1ceWqzF?g{aWb)@n=}#gehYTk=nh}nRPzp8*4<^@K zXn<(W44IJfksm9}hkVXm{BHR`?(S2yCU5xfz6or)WvQ185@gEI1sQtuZp#>AQ!Mer zqP4zwxB6?v&()=`mR6MH=WTudQbd8?n@uA<`%S+PU zZpgT+UA^Qt<;R43PIVl+O?5ertME*UCX-Wyy03SQj$Q9YPS{7<=i0wY;(guk>>%?v z?}Na3u0TFCf0{VnC!MKHQ7NyS^jMWuZCVW-{Ft3%yC4}5jZac&`UsHLT+21RDY3hJ zbwAoNi3vE5pcnlFE~d}q2yEg=G_ah4=#D$; z4Gjl}bIXcz%erR{V4ZJc6RFr4*)EfzF@awkNKi0#FaFXXBvKbU4&hzgC)wI}t|I)z zQh}A!N>Pl7b^X$#Acr4m)T<=x&mjwa>Xl`c(as^RA3@Teh(Hh%xK}PvTaHvWR_qg@ z>qQNv;i)bW>M2eQRq~6Dd=yM({^01`jj^(hWQ1;1!rI{}dH#Z3Pm*(}^?XAB)oBqR zU(+)5cb8DrW9jx*y?K13aQ&tST_`(Sr&yBljIgj?N1|AwVotl{kNR+zif_Lk)XpeF zvK}tF;GUU;%8KZysoblH6lcLUDFkW=X>9#fZf&m|$q>#@N79lgvrCZ-Tc-OLDz66* z(Pv+}yD^{A<+SSwQj4FIA`GX7GQNx^JSsUY{zHwF+QA0vP+)9drWvr5@Yn!*uOC(I z0B^9pMPwO%;KO0B?9Bj?wNR$M&v+qoot_Aw1^h)ooW%fo*By{>EZtxc%*5S< zTjetgSCroXGUV{1fAzM>Utc1?x?+v+3wCl^P$dm~g%IOkZI;GRLhu zh0Wdj0ue*oCl$}*kn`|d{L{1YRS-IQwI04bO9evNy)r;?Ep{pEw4$bB0aTvytPip+TZPHB--ZuE&*%ej&b7aB0Pe3Zvy+dU z#y<2^_}h0``~kVIN8B{i5iayETJDrFP`z*Xr|J(S22SM;4OZ7_hg#N_bUzu<7yVLI zUG?U6;NW2i7oR&KmK>&vPdN(1NiY)Rj^v{g)aDHH%cHol03;qy?v9 z;R*M^WySu7jyIj3Ehk=^)^B(Z_C$KSrhHM&2WPKb6Hja z=h#Z?%LU-8V|O2&aJHJ*yO{D}h3f!e$b<|Jt5tVoUioR?4C|}XdWKO(og-dl{|IcV ztvjNbf7B(8r%iF?uGD(ywfvW@!b73tccqqdWyPnPo9`9(72rW>?^8Nqhw26$1d}a3vda2IaHe+^)NfdCH*As1098Wdb52- z@SyW@T;2IIjZ68Dgq8)(!e77I&w5{dzBS;GCDsKf>rX-%;?qEp(!(7n9MP3rS|CE` z$8;#!SJz52(<>VVZ%PUYmTAu9eyO!S)HIDZ9=BDqD@}LiC>4(`59B$kKf!R6iEi=~-o4MEj_1l# zAX&OqfQK*fF`O3RqAOm7!32@~`%E3`dtHkvZ4qOkTf3TH*XO@)|LP_$a0dm6yDKYKg+Hzi}r2P%` zdI|F^d2aBpc#8i6&LQxVOn(6KZ$R_0lJTBY~3V8tv-KK74ZX3t9^wx1Zg?=Dqa&UO6oYb@!3FACkx(m$%6vqYa7r+Mz?(;K1%^aZXxvu9ej<14B zL%>C$#)pmwW|v(0A*r=qzu%y+tB|YWDb}S8w1;`RYZJsz_lAB>hVswx`5x4Qy|6cV zqZ?)lI@!oQ@>2S7`!LBFIbsrvtk@=`wtW*Q(}`Gz45k5i0C)*OS!D}BWfpm}hK9{( z9=u1dwPSOK!PVTQ3Ns0YEu7EZ#_?#(0ve86%!5KL$Y7w12oDEzf<`WrfJY&lw~FSb zd>ox}a82XQU)ewG-pYSHz}-r&93|iTFoRyn<1=_pGumaM{Rj!#y{wXV|NeU$z}${( zlOCucZh7(A?oU9MS)Iteh##3SJxvFJuHT$TUN=N23%3x2m|B$F3y2#{vT~ieS|3fe z3O3E=^2Xu0Ut0-+3I&@33eC*&MM# z6s~CNYjn^!PbibqMfKFem>~1&GazJeZ>Tdp9y@mz+*iJXXZNL6 zVeYOgEFRA)q~oBJkFPY?*#Rye`-h_19(kQ19(3w`6UIr^Si><-r!C7+{b%UGjH`}x z@8$>2UHUTg2VHNe5*LFu^UZ5* zsht~WRMK0Qft7r$Jr>Z$ToQ9Jq$;uZPct-`Cjh&|(5BUW3u4LTya;|+-$xhrxV^^9 zl@9SGKXRoPaNX=-`qmU3$$&7?RMH#v`yC{IL1%K`KT;T`(J_ij16tWLG~A%0=34ObPB|oBiAPT3J7go%t}GTwP>fq!4>F-kSY6!)1{OTIM`EAC{) zU{2lrQ~C<*HnGv;bi#O{gj-c+e#%xzdvEl{^$c~tgp|iY?N4LAACB1v=-YlKW=PVi zjvlF?l8s*B8_dkdlmp5l)Ye3yEjwTI7nH*d_P& zE~h0DdmFdM))rL&2hm;XQ^iSm2PaRLfIPQBMS+nYF4hOus*d9cY7Y)2$B*ffzF3&h z%9RM?vOkmp**~zKO`c=#jRkwxN_9Q~^9+4E@@7w3-l(nX!vW1&r^vQ5H;;49h_=Pa zq^(vcDW+CmVZFiYy}#HwFsCn3V$vON;v9N4=2YLYYUk`-*q#DfBlgJyZRw;9z=XB0 zaf``0&`2SOVC(hR2k8s6(b=1TbbO1=m$LGh9}-Bsn#)5nPsv4e4S0SVe3pXqc}1<`V(=Y=i7E)5>zYlS zZa(H37;!Z3-`xBDY3Y-j&}9{hCtFg1=0hbt#g-C)ku{A58x^9otv!eyrJC=bgSG>o-DXIDrdt2VAs}kOXEPNmVZgVou5nkGVvldbwFI zy!P6=`X7DQ?sANO?Oh!Eiv+$9)`8{2sG}f6ec7s_9=z>DivG?Rtf#RYF!~Z`6V&RC zt+ah6E#ND|7qGh5-<>_MsI;>Ei{o^(DbOFU1{cby|i)njS==}+` zyjIa$Lrzl9BRC#n02)I?C!j(SOaTGHoFE|dX$SU6F%zb`>Qn3-ih^bHBM9^=2KZAS zP!~t8V5c5GhL#+n5SF?rll79aWglG$K!qe@zV@(`m%o|H{=QK@%g3i5hCOp;v~K}$ za$hOaoZ{1iS?g$veS0q(OT0km z0aSE(fQLOZ1oYNquqDOnAb<)l1i(Ku$CwU~PkloJgvwCG>NA22b>>-F@h?At-w;k$ zVu-H%P&wxhTGD|Rw|fYeG;x}gcwCJ6-ufJ2eaC<+$vd8*#nkzUmdQqWx70cEmQdJb zl7^q$UbrT74vf7&YH54Na-V$8Q;u5(9J{Xn=buFNPtQa;eyPn4R1e=nw{Dd}s5UrC zbR(8Gx-*PK3KF@F$%Nl&?!EA7_wL4`D0HRbq(;zf#v$~PE~aq6MDlO$N&L%9t(>cg zT1l3UQYCJ2HyI6>JK=Q`pk#anmEAQS0m&fj!d0b&Qm6Np%wfM1M{cN`388H0au z1SKQcCvkuNEOATi9lUe}BZ{uD5x_BY#L2WNTuHEw9m>m;zjW=h!5QnLlTj}3n{%|U zagCMy;uxc5v7#OWj6jVtPXgsK3NZ><7dl%YuI(7Ig{?A&9YEc40C*0&xrzE zBW0sUh~sSsGnQxoK6*(vP+KpTNWQOk+4uf1xeM=Oy?_jjG#`Xyj?F83%0$Y)Q?jOXE)!0F8_|@_|GvLZ$g%p0rC}RGnlqN?H7lE#y0@`_`L|2 z@_g|FaLBo!{Bs58%ABQ^;@Qr_5w8wv>BU7*3ICZq)h3~55V8({g5$iuIO5fI8qpnh z-{X;gUpxx8sfhzxh_LkCKq-W{D7gM#x)tR2cLL>^e5YMjZU*+EzfgcWi2aN2cJvp= z4OgImvIT7Ds_ZNJMPRjoqlHp`UkEINWu5`_4~?-o2-~OrKmJB+cgQ$lt{s^B59y^3 zAEijU$o%3c%JbOj_{&FX^?~&UBWw2X9+Dg7{PCo8W%uL>n}__5dc5^=OE`(@r#7Wo zC4Tnfj_@>Bk`4Xlo%|Zk8!L1~i@{sCF%6%X4Kd4UkaDXV1#MYWQD_!ydG0I{LCM zAug&}`jEwwQG5BT%fi|x^4AKPA~4!UFX29fepEkmvT&cfU2u8w1n;NPLLNuAwNlS| zk-WT4hl}$eu(-$T$_LJ@TBN;%Z3v8&tUepLq<`>dn63aqIISyp0MiZumDWchxq(!i zUDqyD+a-|&>|8 z(3-0I*fblJvuq_(*hRjk7&i!2o95ajl~=@^K7gZTqQ?H9Q-=G)0QT+uVjMLyyF5x5 zhBM#1Z!}Vt{bp5`a<79}W&R`mlY_lY|3G8+&6~rQiwwEk&-_?wES? zcELB~z>T&pc7~g?Y$~SLb@NvF7Bs*O{m|2bkY!O4Q(VYR} zCFnU8ur~h1Aq-Q2xIlOH+vlA^84@Xt1TTeRBdgWA-PJA)bS-Z29zUPZZFAw}SDTg7 z6hJbz)TcSoR{sd5GY$oV#+ICnq_kbcnGNa$_nbtx;clKcb^Lmn*p4{H}Nze9efD`*W_@B(kMfxwL$;G~B3 z$@?D`nZ*84G%o)g^W0A~AGM_clrb-fN!r?nE-u6KN<2Iy&V|d)dnA4Ma*9N6wD<6x zkxHSOcyEGE`AB56>$d^fAt1(TO!_mM_8&8h`R`MN{*S1F{|fs4uMGM+s6G)Vn4k&0 z8+JX4Yfo6RUG_tX>?w;wK9Iod{QcV*Y+gR+20aSvNBrhD4=BY-U6707(ndR4QHW;G_we(~52TQA;kS;jyaP-Sli z8rEyojuxjQkrd?k)^p||mx?8fNN3&#N;66|5xM)q7p4Hr>~Hcm|H}#z=rW-8;@hjg zi`^|N@TI4KKptCkl;{oaal9VCC*76c5l!SCORR{Q_D&e|K5$?SlUc~k`U0e=0m`&2 z3N7*7!m<}%O)x9hLNwx9=-Rjw_l zf*RomltNVV$g*}rS>KC0q#kp{$yMA~n`B9-eR-(6O2kskbj;Nz0L^}NoiMg&KQtfZ zhXf6}k4n*<5$_jH?K6OVNp(*7-X$Wg432vq=e@zPyQQ&k$fOCD#}8+Jyr6(vr1#yZGrB{aS1hOdm#uf@>(r12jJy z)1;J0%nMaVqCZBfL5oRbd_U*>nnBUPhwF!8+-!zF92Q=c=CF4M4~UTuu$2I+NzNi! zG)C7s!*%ikWu!Ol`G?4>UCT+R%#^960;A#x;xDz+HAlMB9t|aPKJHRC9yR3@_L>n3sPEx;%IV4;VM~dII|11B zY!oy`u@8I!&K>E0BR?Sjj&1*gb@o>U(UlV4=GoT%I^?i7=@=O_bZ4?5O?JuNv4pA= zg4Se%s_^kdyO0zf+J~L@3yzw=i!iE(ABF+kXwdYXx%{zxN3!>h{E z$oWoC3TqUH)cg31l{$!MB$+)DS&molD#O3v-W%#kkT?Sr@U-ww3=bQWF~R~?SF6{X z*g$(YO3&V)=N*Rc-zP8Z>mxUkon#rd9hGorJh(!^vfg>A;KP` za%H8VjbfupbhkYv>fQC?q06GiX0bHmxWf)#*=T!`-}}JUfFmt58>$#_HAYsB>hr8@ zne%)RRsWT9*66$#TOw}lsA5UTB}D2-dUb`Man78id||AtNc>q2*=mLq^LFB7B5_;S z5cs7E!|*E3n9tBVRro%knhhb7nj_Gn;o+m#rNq6hzd}x)f_?@-w$HGoCQpVoankoe zVX4Fss&yv+JDNWDr~QS_r8l<)OAK8}!pm_Ns;97Ov-hPBcy1i#5LqWJpZ>QFCMSjo zJsoW;Q;<2X8b+I{DU8A&e)6Q|&aty{U#ZNJsPWk2wuQ%`pE%#z&!+a0mK#H%Gtq|!jJ&mPye(X zQ7ag87$w}u3mLZO^B#0lo3nsF)NkZ;jL4HQJa!=O%-&T;8LAk2-y4P=)A~yS!gIWX zn4Fyb!}&|7VPGHGaG~pB&J-iw(Qk}mNEk@%jdZKs-LoO#7utn~<=HOp9CzSm9pzlS ztUT30_k=VgBk5~Yv1W6vZh%3wY1J}&IuoEr>7sbq5vt2Tnjflg9e{}DIOm`^_?pS) z>JAN1lnzG1TEIZWJRZyD{#h&OAr{L#Pp6(ppggclCLhUAM0{Pyq}QTGj9fRIE= z_+IWv(9@lsd2)krY%>52WJ%njK(HD-2d7*Gg^Y?(eZrr}+;HgBSQRiyAHlj@j`X}+ zh{KJ9j#_eP8?C?nBztd|*{@6Yvpj5Z%nWLV(K>G4&p?{9D)dF!gOK8i zC=Fz&?5QrML6PgaUT<7w1iR(9$!h+XklgUz127K(?-G8GELJ*O9?}4LTB}aIcqJPW zx?xsLH|1cjY1_sJ)o(u?C(RI+0H9*e;uz+K?A~giDy!B^;w2Y?25Zxu-lHCym;b!< z^hJdhf90!7I(FKGLb>5E5LZ=Zy%EK1&FA5o@NGAjmqD*)VB@ry%1gHOjgK-H4kocm#c`khvb z_Onb7Hqo{;n7iC2ucIm9`!e;>4@d}hJk2M8CJUkz5GKtKo)d}0#qa`k6(K!=L3e(; zvv-1#szyY`#RHy~IATVHTHNmRAF8oh7G@I))zU7M!plDvQFUlxbSTf{UuWGBSJoE3WhtxBa2wy|ONpF`6ZxgIX}aP3;4IK2LFy4mu9na$RH2?<5gJ4|O>y?jtt`u??!ZA3x~6 z01c%Ne$)5>)Ke4AWbbNu_&!IM7CO*)xIkzKT+;4vpWMsC!@WR-y|?MioE z!AHvSm%Go+b;V$U3TDzVW0k``Qof0wIQl4s%R!SW_O#d3P+}h?HPbf(<=UE0|(w^t7QAk&siF`Dn8g1vDlG}d!5j9Y$Lin4< zY(Wrjb0bj*>Cnk~?-5Q5IbZcyyG|opYbhmn4q(ik#NU30Dl{*ypz?&O=xP=A!(DMG zlEvWmsAYLq=6ARYc1&}8{*<;jXTE64$}f(5qTwr6I2Mt-8Zo+uemjHSeV*zZ8jeOx zOn$QUsOT%Vm}ko;?49(8-#_%d>tdb$crkIWGgpDJS~|m@-7_lBN?S&2d5jNfm#Ns^ z!wP!*6v+EY_iN06R|TbwYCnnAo+_GhQgE8cS_1ID{VSlrW5B>w8iGui&-?g2z?Lux z%UJzh6<|Th)4w`WU9>){{dLitN?1}!M?28R$VdrPiyc)nlU4Y!{{4L4xvSOLbH}o5 zj^0b!C;4LqQW~ClXBp4IVNnri>b5rY$<6cf7OmqaeDXS7%oM0%M4*jw_+;A}g&b@S zzwfWl8pcPQbaX3F*ma|An7As?R&-bA={Q>%GeVM8WWB{B$Vg5)yv17b2}Y{d3&cn7 z3OLufkvxES7u#iJsQe?WX`gXrMa)^vDAD2cGJ-R<|1`k`Bm#FFMU~;rowS1U(F%zB zJO1{j+^@=-dS37({_s>dp|wv?`{uU&myu^sC@T!_{1MI5TYIpDs%4H0eLR=ABbvyS zDyoueSFa0|=6&QttBDWejJh8v8W34o6^Ehb8yZ3y(<<`%u$;_miK7zq;0d^x>u=)k z&r|nJf9@(cf-hB`uFXJ*4e2Hbo)uCTEF9g!)=X$(*Wy`$Cm%@^p-e>oWOOqT7C{s1B+J?GOMJe;~E6g|MGlB@^u= zA|M9D<6Et}7P^dGH1=Nl#bGrDLBrDK0V{8pMm~QUj-LPCt!hJGobsS-BA@4CO;3TY znnqJ!wxnIhf#!y(;SU0>IgLWpn_Mo}VNnw3`H;{|R3UAH(j1eNPm6pA85HMyyTgdg~sx6rlkfkNsXIpoL1nXlI1 z$9hEwebRpH#Jey4wxx@@xfIk;t=`ycXd_VJS?SSG9Fg$QOCP(Rf$s3#j;y*_S$>7a#!e zGeBq>YY8(Fn_hb#D8AubOrUdU|Q^N)~pjGtqQos z%~FuGn^bT;FOqj)9wLy1(CnJL(Zjrf)Qmn5+H@{>QR2!(n`r5!85if6Gj-YfO`9iU zfto*OC;E^Vh0M6oJKa|X-8JZbY8Xnh;PUGs6|AhA%F+fcW>Q`{7tN%)PyguR7Bv+Mnxf5)=7lzMwg-JADS_27VMlq); zEkwegS+k#c2`Z3O_i!FID$)I5`Q62$h}kQF6L?nf#jc}nyr)lK0zc{4m)OtEjLiCg63hTs#J`dLl3$vM5YlG?+Zr@dU*zqPPB5hQI z2j5he{pW_(Xvr_P z>TR-zGpguH6JucaRPj!hWtXBKmnRt3x@|Jx`XjDE;;iR59bHDs=R6o5&o{_)%$CPm!*yen(evO zNXd>UqxjI!^k7QCm?33pOjv!yzD{2zI$z~JoG9y9-+W@^T#MZ2VVJKye23k34vGFSVI z$>D5++@q-5&PY`^g-3;#x(UcakhyG{?)oc(__x8k%-1@r- zqw*Dj>fU$vc{}Q*-oM(}F?IHWcOa_pDl5MBj0Ye#!9jOvvfBzN#H(g|NCwn=PuSvn z;Pgdx|JfwL1MADdkuvX%g*fz`c?u=5x7+N8z@apsQTYUaRc@3#uzUe2sA14NiV__VTXmwUvts+{7ZZ)2^pGbw6Mu7K>4m59xIttmO!MYISHFndf zq=j3>wFi^K%AKT*&$wrF1Yiw*0(-9jdrbcnW#tXylH0kLo%0o`!c~2@C`Zc6sb_Ti z5_li?J;=N)k#XR#`S^AiI6FGGzYH|w*V14oO`w|5JDTB?$a&F3wo1>2j=yI4Pu(G} zw{H^c!#=!vezDc5Easl2!Zi0QbBHj2U_AkaK@MP4QLsk8yun~x6I=jF-a8XGw6rBD z-N9vXD7>P4)=+eL?C7ng%j0j?fR6?lPfSvc(3f|h2liql#@Y*P2kaKIn#Mr+Ow)%r z#D0UrMG2;pjMJ#t$8jg9A=m29-s*P$q<)U@j8HCH2{aoOQO5!&DA7x!{67&-+S*kc zMOR(rPdnI^l5N+$o$n^{PWwK5z^Q-Bk7wf+m|yk$F1Mt_dBKUxvoUMyY%^paVT1t4 zb}L(i6M8*7v5J6GNP?TmB%EZ}3+T2m!&d%TO!F)x8nSj6pmp@Sm;&}b(Pe-n!Cy&7 z*kJJ;%+t&%n89z71TTJ9Zv4Hd%HRCI0NRwcW`$N@Pj9g!FE9R4T(|O38WB#$v0p~S z%-RCLxrLGrg~&Dm$zuE6!7MFXnDb2}5wtC>>-&r2_9yJdh_l110BaNWEZ7!1fBozN z6A;RP<=y-Dc7lKXHTo|(y#KxDB(es0vQJ?f+UX+oDkVGr&#vEXt=498D>DjIC4B7`=U43AnU;U2^mj8`o z|CzdXI+5K)SToSQ{fpyPHZY?&uUl`$Fu_PFPkq(LP+t|mASl0N$V`#@;-|b*mlL?| z`UmRe#scy2Un*tRc7&GPKO(h%9+s#)gS@xlb~2o%mTTUwEP2ox`=2RMuI|()Vmrnf z;8V9ZZGGUl?f=3_CVx5yY?bIm2RiN2ja&d0>fcq^w8Z8~_%K<_!|e0_x)JcW|IVX> z12Vi6Yb^b!(y|5sLJqM$4iU?1QG9dQpnxM8T^Xr}#p%rK~VZzWIykVh>00ebP3G55m`6;)K znKyW8I07i2oQ?Np6%+gQ=X(IH`on=4?1P%=zxfn2k8LfQ0St=c9jsezgxS>z)irRh z^9Tkqo4QLgJ5z6b9Mh{#0~k=T`t{4$4koVRQ{gMLDsc1tba3uM(R|Wu3lfZm z^nG9qVxu;65W8(~45(+8B0T;rAo>StNP;!#DKHF)UvmIZbFWem?v75YeyP zOvpE~47Y-qyz@|}TrU?Vl6LEGX!hQo>PK;xyGEKatJc-C&Nts|Pw72q0Z&f(XT|7v zl&D$ki`7`hfqBTzqw)g4viGlc^)}EJfNF(6-bTy;tG}1f|Jnk~Qz8tO&gH}%-PHpN zWop&{M$EM`u97|MbG0D|d-IB#IPHx#l}}w43k<&7)~Sq^6N>P_a%o5dQ0KsERkjBF zwmxr@@zs-gC-DGs`BkFo=T(xPUBp`25>g!dla-AfFK7cq4=h;Glb{`NnJQCc087nD z+td*T*fYSD;sJ%?UCZgio9@c3*gss`gr*e*NCxx}C@>&nZT zG9A|M@mUP1X7C2Y9Zpv+{>3qAiFHB|*`dq8kmm+|J$KyyuQ%$z99XRC?TAD}GZ%Im z2V3XwbiPZl2U3%Dk%2GKmfkX+T{Zh_eNNSN@HpD|MQi5g)RdX#clql#Cocp>94+`? zmPr3nb^LE_`uk@m**|n1{I}fdj0y8ZVbm{<(%B}hZNkI}I!-n8{`9h5Oe9j~<;g5Y z=65<1>bGMyiEUg&2BvW{9VV6A33n-}k-m9X#K#X-JeyXa?sEi|HLeJ{vnM5&y|0P# z@du#w<(||G#3B1cm@yDv#hoY|dioc~AlS9O!~Dy#0r=8?ME&@0etp;1zcjM{jM2k> z1Ct1gKss?xqy~QI?Jo{ML})0M2^5d$7b1=pYXAIODwBVFO?jq+ehlEDc>vfW<@#!% zsJ>WF8Io@7RU@>GEKPZI!f3KTJ~&BDmcI2N?3D2)Q4#TD82n)_H|zSfxVKv8YeSoX zT!yQq7weR-WlAF40}DVxHuJ@x6{Wp+wfwdjSB$PG^~4>sGNawRv%K-1`wMJqo$ECg zvBiXC(mQsiWbf?rvV7HRbYCjW(0q^(xLGW;!wo1D(3bS=bMGw_H^s)J`@t${U@mQ)xwJ}L*dj}L=`lj|<-kfmE#e^;1byYo!;;3`k`2oeLuP|(aKEQ#U zGS4qs=z)Db2U+%=EbMo(D+83Z#ETYE`QoffJK4(feYFnJ^-B9>3wO^hd8Vei&xq=g zl6J<-dR4q?G)LE3$-HekU;12?0?#XYDjdjF=9jqiTQ7Z|*Rd~tK6C0TP@{v^PG{lK z5Kg87!-%QfgIAcuf#z$EP*(DVjUGAsi%w4o$cW$0h<$wdYp?^qgzcq0RXInqzAxW8 zN{=L-Lq=|7pU^<)w^$Y>vz%*&d3B+i)?DCLymIpx<#OS`YKtU+7LPtjp#|kUwq_79 z51u{A!-kpOrIdUAFg?4Pw8J0qspl<5^e6ZXK*y@~TII&9*Fj3PS|GgNzyd?=el>*f zJSUmZSDy@($NhYRfJB0em0v4^u5xJv1p<6D6;c}g6Z$OR?Nd%$|9{x~@_4A*y?-UC zkjO4gWh+Z6AxkDnk|f#Jxl36?iICyjO4ccg5L1+$$sWc`_DV?hb<9Yz&se|A((me= z^PF?n{ha4_&V9~vp5OC3{oz$}nd|cXUfXATf8L)9>x=in{nVcE=y8RTUM%| zV^_FL2fyHA8yS4!OCAL)#W)UkZA5I*xWnEq#cNDK&r;00c$UP*;XF0g5#m;3;ta;QJy1p4#$Jjw3oEGn2YH6QTcTI<3syU^L@?)?u!CY5K?gQ;Gho%T+-tn{CGV`r zid&VhrLR8OX5OB*`Qj|Dk#TFb;0~5*!ZpT>oMP>TYLIHH4jJ2%nX$~AWwnX(wNUct z)yS2{y&%;pJeiXrAbGZ{>WLHZt6n6V$eOB1K0W76!KMrL7SoQlV;wZrfEmg+8zLpHAsoUlu*m7#zh zG5OVqI$v*aSJx_L`Vfg{N20Z7e62ji6kUd>+|YjSj_tJ(%@gREFZ$bV+bvJ)Qi`ahzZM@B6yw%*`IVhtoxxwzG~iSY12)8%Qr4H^UN`ozO6m%O6`y zkZxbVZI-ycZ1b@MR@zx~>kgd`>W~T;yvp&c4FcZWYtiyZd`VYiCFn{xkvUC2NwgRgaL}HMxD8b2P3nGt~ zHUcDf)v!B1)pO3BuyHNwl(}sdK0c*W`0CX;52@WWPaoq)FU@??{;(pP%7@)8z-deM zx0poHI?F=P2E)Xy<18NPAfae`4sVpgv-Vjp**O2Smw_QN;(ktH`)07Wh7F~!I_?Uuy$kvi4X3B%A6q&HDqArEK@{+rY{-{IcZ`W~@Wdo$xG zJR3kuGb@8pdH#p-Ce*I}S~2sujLdNv!~UMpO7CzF&r!W2MLT$Q>P+poU|gZg0(uVE zgs#(5OMc_jiNB(ak-5jy`O>}3{o;i?GfH>nM#JsA% zm&cN>$mdZ!X$d$PI`XA1KhP1WmQL>3yyno`>IOhWhk&6o@D;#=*zF)y+9e6wkhU)5 z1&^G48EV*(A8)qbrjgs*jz?$jFs9YN`i7O^?ua!PdKf{1^ivEOIE;oJ!Q7xw2U|j) zf;7sSpzA@p*6q<(V7W|814NyB;sK)77!zE8|U$^hiyl-M35-e830-;;3!(? zYX|;U)Wy!|XA~ntySBwMeUB3l@0~qqejxY4aqTvNy97&ZQ;+K1o6Y<=ZxYWd-8<`# zC8O>ztUWRZ{VmBURr$F#OSf|gH!ik&dYDgZJ@W`ydh;x z4RuV;7aXXj_`~lJR2|{ZjYy5{K(5GO&Jz=W0l%%WbHVc6Z+{D0VoT$h0=OF} z*kA&|h}Hk&@SFd{zx3m88S<_DhOBVQ*GdejMP8OcS`Y3s@I!I#|0`@zH;|WH@QND= za1>Pr5CR<>7ENKN%O zOpXs-kkLf@%C;-u@12|vfJZX;AdFfHgna`kB+*HsRnCxwUJr5wG1)$~%yle!NU>jM z=cz(aVX9`PY0J|GS>#!vaR*ImGNItv#P#5!=WhD~y5hUkHeT`Y72T$B;!e(<#dYC~ zci>iO@K;+(`A8gog5aCc-swL=-dr#YHzYBixu(L`=ekxeOD;Nteo7kAn5C zfsJ-0d%VJV3AKb}1Oyc>*v!u3aabom$MZAe54x&!hw=P&tQLo@VaLa%sDp_&4!u>) zlpks31r?bsYe@x|nxsJEyW4pS;mW6Q=y76~?*@g1FN=pLp(fUz_BgZ8#sjxbSc|+I zUh9wREND52UkN0>A9+-sOvgI)0%y+NXEY73b^UOA%~iN%(9rOyeolXBmIxO)RG^JK=M5G&WzM(KsR0yd|SEFlABdWf#oz-ep29N{4 zPyNIkN31%vajKiPiBwAvfW6dieJ){D!_h+rh_sOjRqtMhBV~m{0S_{hrit#4>=k>o z0ak$NZqjz#UqKr0s3-B9g3KUILT)IA3ek- zPaA%uJTJ_qgtpW>H)4b^b2l&PEbb2>6tUV$`RpZ`sokT@)n6EG=<)eTTnm0qegwQX zvIr3!eVgh|{+u_g`tcG=XtGSKw1o0@HaB0PWOQ!Eq&{1!VXCV4)u1ev`v}i){2?B! z+7us6SC=X=`K6*ml_1kL=C$7cn%=ARhtQJpZ@2PqT;qIVjQ?hfO#|O8x2osUfgBGO zaMNq(My^Co(^V&_Q$vIQ5e`2@2*`;I)y*@wZ%iM>uQ^c9s3TOiw zqD~dM*uDTjaIpD~!VbJ~P_}dxajEzPL_zT%pd8opuYF^GMJhtX!lNJf|2=qnXU_oO z4iAMCLf~jm418NGvJkbaMf2Wt|0O3CbN&>#VbtrT$Tlc1{3r9XkH>qPY_&CLAtDn*9%p2Ah~zJ z4ZcyDChgM;c_&>}k{)fT-?r?U$;>|#`SGO-vMBOilKG20G146|=Pm?k($#|pwK?Er za-Fh&Xw7xraN+5X_Wr&M9WNE;6QTAd@zb>Am<{jrKMU<%8!~a^Z9g*(=}ub9>KS75 z+Tt&3RYHgs6_c-&^z)MboN_EWVyuxnCLoYeI37E{XFVQC4xLIH;oStvS_wO=h3+ir zz>Cwk&UX9Wk5}!_Q92kDTR zo4RZIc%l;=H}&_7m(u$B>#8K91mrST**MS{bXRcRfO3E99;=DO%Q>4{Y|FHz$Ydc< zvQt6?z1Rn{SA1#@zjUAc`|9oe=vexDKL0l`XZ@kJ^_IBJn8UcK1C@NMdV zx{T07@cWhHNWU01`hoC|$(B4C*S-j8B`p~%0e! z122Ji$nfJAH8H2V*w+^OJY0OS>dR#vtKdIovuscsypxG5FYb>h>RHxqvH>~D4~ zuTQoc{j0e{s#aYty&@N_w&-VOm>dxWzQG8vZRRJiAg&>XBn!2FWgF{6Dzj9c>3Jk7UHoQ_#HY~UE2^KsKm?W#nUvKB%Sz3}C zyA9^lfwAPkDBilhy|ENUAzGU20`qi>W~KFeZ-obO(MQa~-`pFK3^A9y&*LKQKeaF` zBl2=G7izAq_(G`6oTI|D^ zdqeA~G7+f&#o@0yN#9<>h0tX2%io#DXLhQ2(D@p!jfoy=HMIf1he7`4QPORQn*Iuy zOJdC1dMdJ}hfJf*WIpvV+RF9LF+(Y8Os-%au*8W#pStft2W8k+g%}`n z`>qO5EO&IF`b3OZ=m^FpAYBENDk#9e>M0+ z3q((_B=n(dn8etVk6c-gdC34r?wMCL5jM4h=6I0P+_DTNhQ)!H=gsMuhErRABaJz! zq18h>J}?X@ilz>wA1{6C(tiK4)-g!BFEdQkz{x-L)65qEraKK|UBQGRF4AU#jQ zBIiUsvTi!O&;aDPI_#+VCK<|+LeN`tDuy0UT2++K{T}o)+5quD;mFmqa+=8(zg3!f zGW#Q>=L(S<`ROukj2ZF_fRKD2T{B;_M$n%Gfw13;6Jt3vu-zRB9{-cW7ylsJg~0#f z{;CERBE5r;%#T}#y=L$D!1)%rM*kZK8I~VwL%5=SG^vK zvik6Mh0XMSe<@K;N!ldq1Pj=V4kY@%yoqWU_~oS_2LMd+Vm`K5M2B4c#zXI^KcCQM zOa^c{{0Km^{e49?b0Gvn`vXhfU+oNaHq0jq3CKKMP6@}UeNgydw{t?&Y76x2(^Xpm zlR&o{@QiWq2&J2`b?A5%EUVa9H+%QW{s5M^yJb(u!9&6GkmC@+fc=T>=@V0Vu?~au zy{z6Lc4oehVL4ttb3iB8RdLjfh3MR&YSR+7`_TS@K4MF#)_PIh-l*#({?N1@odeci zitU=#GV$`w_7|pDM8EoocP5W>+fr10IW$c&OYU8e&aR@YlV>`oNl#}S08DT{ zxE+3XZt%}6_`gzx=-;_g6BXP~do1h6gE@k$kwRJqZY>peiJ_jC7Ur=24)(l-{lEBW z`?OIt7I2qwanSIKU(hFLUaytzny{$wJVg;e#ci>b+DU*<@`bl}4_*j_y4V-%*#a%6 z%Bk0oy|YV~sKPhTo0*U$);68=u9Auo;geuC9deQ_m#KUQgxp^-f&SU;|7{gi!7}Kswb(EnB+cWVeGHGajwy zOjbz&G*1l8u83x`z1^Z$QKsgU%z14FO6K&Yn~AHdzy`6_^{DjWvRlC|1ptdK&fl4W zj6MfLG%o!P7B9n^EMDs%x$DrP@Yv^pulK|6k6mu9(nkjC^dKU3$_8*M^WohPeT1cet@bY*Uxl%TGLy?1<11EfUv(Q*IOQBffbir2Yh7+GwoJ8NO7rK1KVZ~0i5$cT~ ze`T9iCwG2i1d%0#>xBv=J|w+alhRp~{Neqp7#ZMIQ8k@)5LTO#q!|wHuqJRauFyD= zBiWaOh2R^Zb%{EnZ%)jJFk86$Z_Ns#7-~R!-p%>5E#0B!F{ZL9H zWv_{R;}vN3Wr7@40Q6N=#vC|drr22l5u*CLs@#X283LTcxM`WT#$jw2fv*cEwL|e9 z!8ULzD5N9UA4j&36*T6Ry|_mjviwC2N%n@3tPmnF`iEN( zEIABpN8klkFRm7Xl|w<&Z$8B0Gg4yRtbkbT8L$ah%Jg_ZXg3HTqifn3^3%vp1=;x~ zsyeH%Qi7AQ7sdIN?L#-2$Plds9!T(YuJ~$@37`Udf!GI#Y|zYGfqP-k3-B#9urFTw z0DBxs767{Q7x4`63>wf~#6;ZIl~I_p%hECPqqdB{SDjeowd>HmgPN1&MWfYTK>{(_ zYh*X&8oXY|T$A^1PJB?N`xIO~j4ZP0@1L!z92`Bq^_qfn!xe`rlnVBu4^`#a=_g5N zqZSuq#=m%oUX_0ewCUR2zn;qA6Jc4`L-7HCjmxC54uR;oJ4D(^v&oo_our4l~k4|_J5aec37n?re-<)mCfZ5SsaL`mlbs|Ups`xavc^U+SWke)A>_JxM{ z%0HXgML)F$^U?%$#>6J*yfLL{5taXyEljHl^SE(3mR3}aQj3Hxl7y?WFE4BJ(vHXD zJeRGW2f9K$4?&6TdTMKJxPH_@LpBG%zrd&7`tkUHQQB82Y%hrv(tV3w2Yo6gl ziZ72NZ=dA34u!E0U$oUCoJvw%#VQ=dy)KQsLd4#-yOu>4NAKvGUn1Q_^2{oCw^ExQ zCNM@at{=t@5#Ms1HBwt5!iKqPmXP6y%`;^_b7%u+8EB8Z-)Xpaco2oSK=TXP3z~gT zAds<^DQ-xr5wUmDyfvAsMwYS7GTvy_U`#1sx6;5c_$`-QJC#49x5B~Y05+>}9^9@^ z&tnFbA@8qj`#J!G2eukQfVQQmuWUC9z&&dQ)ieMDkI7l=#o)n!iyQ|KGoc#6?~x07 z-#og(7rxQj{3lJLX@krrMy)9cB0Ks+tlNERto4n5y357EpIzAAMx?4DS#o$T*0ymv zxCPsUsb0(#AhQ6I^>13@Fn-HQ^E}2uz=`~=#@#Bf3kennk17bj++Msu4g>-(>uaF4 z!P!(L_-e)!_#4Mi)u8sYG2nduu_4;#Flm7JUghvIOGph|yk8EdLBp3IhB<;-fCn;3 zXBxa4yCZlvVD*q&utmH~J_z`fNeM`}0{LTuy33y?aGCUT{{E5qTU3f9ZYdT;i+c*& zwSl#~G_JpPPWg3*N7$@A>No4W5=g-@_~n<}K&4&oE+Qv-=eoS?@t1K{C}}CeEj3it zhn#)d4Hd>eE`E8M7{V$ppHc9ovgtkUdLsd-65ns~v>$ebzc53+ed!#?7FHT2(dT9V z!2AAVi@jEHAh8@u&`1IPOs?#^iP8%=+3Z|gMQGq$lSXLN8Um`TLpPy{z|7+c{LineDy%kgiesHB&tHzYQc>x@fpHy?WJ+Ij=Q*TF5sZMcI*2+Hz3hHQ>% z8WmStCKR`-0IDcy>1B;Wj)bU_6JduxrwJ^-tQQw)kcrHU_0IobfH;o!g!jJyfGt^u zWM6&5Gmnc)^uxMANvVdPZ3Y^ziZsddyr7(^6_q=^;gIAlxPZC<-3IE%d>Y$(R`368?7I%=gxDwtK;qH`{>N}4U`xKLZ=g` z-@IO17JcF+vr@~F+L>3Jgbig0Q(G`swp7vt>qnVkkL^Mp8!Dz5`AK0!EVd5tWZ$e+ zR^BnOfbEg3sj!Pbt6D~>EHB{|aAobs#|1`sFWV%iX3js(Wnb%&eL=4#a*aq>u+5|r zIgK|}m zi;oS75y#60bx)76?zcMRIhf`FM%iDeoXp@MXqOT@uW(hOn6}EP+-BO<#^sOWh1eC>=i`l2Lg;169^(ux%kGYUWfbwhD`D%`Tl$~* z|KAB}_~xkk!_O;3pEB!Ml@hB#LjEh8;ko?P0>+hL42t+o|0vhWo zlKe#LFvwy}0Xk((jw#4KU0enfS+-12XnsACkg|-=bx=dQdPzSfR-R=iBKnZOrFsK%J4U z?Z=giEbd_MC7#FLyux}w*p-41oaE0HGX@EYQD^6l9_i)YUmWcX^_n_J0y88NhO+z(mz1ci zN}Qfl@NPKX*KefC>%B+wQu1l1icNu|JN->Lv9V=G)+}879eem6(ZjZ3zH{$!j&w5~ zFaud2N#LAq=bUIOeU@2{XmQp<-T~5QKI+dR`PWkDU-bXYx+76_*HYM{U7w#wjj;Tf zJlN8yCl*y8mGb&}7Y?N8Zw6i+NDp{*z7XN;uF9ei*5I?0-Jin>%B9Y{yVAO&QwctH z5WC7TPJ+3yWV_f5b93K4CE13y3to(O6(tF_EFsHK=nE2{8;5;R-+ViTcunBjVG0A^ z4rrugw=#B#88g=|%re#+Q`1;{JhH5t*Zo-RvM^8^Tv@XttaH!)kx@)o?MzZ5qxNXo zkFSFyU-b85-Ck8jhgoNC4j)D*_n5ZO}K@+&k(l zlXC&Z*bJJ&bCA?~uE+(hb|O_2S%dj*K)UfVgqY{qMEY?GzM21z$fQ+XFvk*LUIZ;5 z@|KU#7vT(O_y7o^d^hA0c7}!gEG9_&{^5ZM&Lv+~Ll^C){fEuZr5xH}X7<7%)g;!UBIx=-usjueK@6hrXH@KeE<%R@&dqAuMOVBB z03k!`i&qj48;@i}D4_SN*622-OEPmz9y-1(yZi7fTeriYey7bf?D_{8)$Z=xmu+nt z3iSB76%B$cP|-p!RKdB1=>MHw{bL$YLaQ4+NN*c8)R8 z&8zb4ECI`qZ}`!(VAV=UfNmTxjQ!X`MwaZh3-ACtwtw6Xb-yR2zC@Ds{JmH~i%JY}dtlV} z{pRzprjXZ_7diRatIY~QO~tE$#JSaY77vL2!PCEO=z&=ufKUQ+{D!kQ6uUV8H?0_E z3JtkH!(fs>vHx7JpX>GgiTioJ{&L`cp0DpN$e;J&uLasq`Sllh_EUa+r%|>2uhTb_ z$)Sws?Lf(S*@0a3EX;pbh?X7>Qhwdly=6RZ?r7Q7fO+8*gGBwlteBSf!4tbDKWys4 zr8Qk{e-;`g7e~Mum3S=K(?!f4jrxEzd%r&sfBq#G`Hw`P{~vv}RHjAP-~pJZHu>nJ zaYdkJ-F~U7sglLEFZ851xu26UXY`za=YT68@RI^y2S+<{g&!Yz?FzSw*Lm_DP1y{^ z5)WTcvR%^Xt=-J-d<1KY{Z&@ztROVJHvy<~525e7mLt1%L#^O9pRkMshx%pxD|(kf zZt|^3>;lq12Y3N|T3N@^eltk!8KAo>gLpA=W#5ugg=V-sfCt?XwgX^RI^c2PN{8m8 z5kHQ?JRizf!Lop};%#U_P}_is9g?x5VvNZq&z1VQQr}MI zzss3A$1ny_2lOu3gTgE5ZmLLlK(5jlF{^rbUsadeLox+K9ju@qE*xAuayT=1dpACS z%)17?wU~wr!{M|K`Nv2ItqPSDpyZF2A>2CB-(UMOa%q1y^~n3D+j(3KweNgRn@Y!% zWKtjz4Mkr~EG*?*n{2GhjJ0fZkh=Md7TOSHGuznLvd~9M8Y6iL=XaEDVcWdFbuA$^ zpWP6?nC+5yu_`$u8k7rlcjd3;HtBN>FW7ZM@CaR&jg7{u^V9Z^G_-I@mSWuMt4)vqut+A^Ruxkw?r z(dA&7tk_!Z#f6J0}H>7&i z5c`9r;3P_(njzi3BtDsmJ*PICFLPq2eL=4>`s0a+C%-sL-PpK=VM#j(h;0?OmryWa zEV+*hZ*h4(9Z7VJhD!r>@9!T?ax8ROdG2dB!mcjGohZCTZ~FM3?)=8)?NxdK60Ko7 zi{LWXVQXz!|17k5&(_oBI+pM;nyHEw;rL>23gV)sK8LvAh#Y_d9x_y!aZuRT*ZIBfd#`JmJ zC2)2!3~-E7G?}DS7gcW($==W-ed?A-M`b~M`e`;T?${3(+I|#p3#;WRAysPh zqdXgUvf+Ne8z?JVFVP&Gu&wR+Jqh#fDXcu(YrdXO7V-btQ~zq-|DbWaW$kfiA1=G8 zgg2sig{8=~`y5tlq8qg?1>G4!CG2XzN?kiOws+zXr)cY`L{zv&8DkH`HsP?FrdHP~ z=vuKIBN8_=M%S+Kk~ZYvya2u%$A05_CzQKC(Bv=EGqGUFg}CKg?r4ERz?nJRx0mdd z9{^?G;p~qh-3(L${Eb%hh9v^WAR-g5W%-ebVMB5qc?%vElX`e)YElH9ROvkTIfCDM zk4NtQa_@|ST>ympgSugg)>}9A4^P1RVT1?x+G6i)^*SpI=-)rdp6a9QQu|`3>H8dE@$wt;Cd^5$G%ID$HQH)lprEE3x$V7tH3K#5 zbq~eq2q(CcdDPC7g)RlQLof02Qh`LjjETNm_5;SAXVpb{@*fLrY1)PnIchG>hG^d; zpPjO&es>8p$tae6$S8*IQh0P5NNdpnD)AV`o*eYS=;fM8-}O;6XEWNTMEjMl=?J^M zF9JQb_VX6qa*$5I7v7^glH$tfr~F@NSr6m3yFk3?K%9RgncHC})pk61XHWp+WQ9|~ zUL5~GwR7g+;p!(+=Z8$UMak()j7hJb;|Gl}?zZyeulSym0UB4(=G$Pa>^@viI=5&)tjQ zc3s$rt~+Q0@aZ)cYS+smRY(ZCiR%}miZ0F%$Y{gXJ8;0EG)ce=HWsF$#P6?Eecj<11>zl;EJF$`23GC0ecZk8 zFrh^Hz1J0v+O!pKbzQlv$SgB16mSn#8w(lfFDZ9Di+a*Sa!XVYZr;LE(PDJA&1uSi zf82NrPY|cwY(432g>eHZL~Qwv?-v^n**ev;Rs(l5?adq1p0R{YLoBA=@wyprc`_nf zI8U?<-5Xj1QO^?l&d=9i1<|Ii!wJ4o!j0S6O50Uj>@+^gA6jK?liph() z*CuzhYEANE9`o0)zn+tfYIIP8U(ikcF*QTGXd&hK{_Ev6G3+kaDw>;I3+xS3_4n#! zpDeHoIWKzd{y{yK$K52ArTE(iH8I>U(xBZ-Wn+nZo_344D=z&^p@YT_i$^zxmG4{K z9O!z4usW%g1=&FHlx-Pfhz8_VvY4MsGeTCX9!q_kIu+&fJWAB)-=0L&KoynrX#sqCq zH1Ee?LI1{vz9HeP+iVehWgI2$gSd^|fWJz%M^~j6K(sWw5YsCobkf>B7eb(+pO*?N%?S;kOW|4G=OX*xK4Z9J>~a>m(LcR325iw|zN|0wKHi1uSxnto~`p12k-ISjHv-MP{j z{#}IaWnnh$ChgKFh13A2&sVlNNz3m!N?R+gdE<0#H-Q7q{{hsa@w!{#dgMa#Z7gUGqeHzZAM7bNcdHN{wns+Eo zog+{`sjXS}(YDFml}l?%GBTh{`zj;ZySbvY3IL~B|g0Hdh2D@4~dxd>yK= zyVgVZjH?upKqVv`t|jrJ+loh`vP`M0< z(uJ+xQON?pHrvmie|!!;X#))KTZIlHsSJ<~(7Kqvp#4BN%{mJrdVOU}8_8RR86(UU zpjiEd6(jM#M@6;7vO2KErz7aQ8Ea{3n8mqfM{>XJ0o!MF@$Cg0dGNtFg~)qpo!M!^ zvKL<=4l^#6AWK6T05Y3t!3eZ!;%Iqfd3D)BGY(p4@8jhc1>#)Y#TZ-cveaQqWaZWJS{wldfr$ zJN3Tl)g^Y3x7!JwZM#oC@07FYU-g=0Z0xm9G_w$9C~zTHMz4NlL*w8!0F++a$_fCX zQIb5UIN1whKAmgbdM!Nf`NV@sp%fptiE>G9bxLlxLMmnDV!4i{c1hX4Q7e8n3_jcPqc=LRC4FDhB1c6Jfd}#L3ADE_pu?Fx-c`SVtYN#L<(GYLm zj)hg~or7v#7Xh*hOXM=B5E6x;z63egV~<*ybHow-rC`Ywk*jrssV|A-Uv&rh`a;nB zyD`=Zik3!4@-ZUSDV}XssPFoIHy2FoJ=XDUG&Pr8rC;}D32|Thsdm?uun~iNvy(d(IZHK-{j~FxLOHiy6xKi}C*dkzC$9!d5YB-BqH#N&d zw>qTaLsB#=U+BnFY~vw`uihoELGkXASPw*v(Av?b^PfF7WX07xBv3(?O7o&Sbs zUYwAB1Cfo#i3|LoAh2K`4xl4|4|Ee`1hQM=qmb16PH1HpX7Mo8I?IUv%2vvo`wbMH z0%Bskj6F;>)-4BME7w#4W9(Bn^2dQ#8MqbZA0M@P@m|0;taf)MZYQ`ekm(=^$FKnl zTe=Y$5A=UKRL3I7lP1ogSUuVW-!RnI7X?R&>n?rcE)l@nzi14~9ki-Fm)@!^3IZTzYsi99^W^1 zUlVS?8adVK!**hMdhW$$lUpGx_$I};ty>i|&;wbTeSHV9$z=t&10}8JQ^P+oJZNLD zNjy>)X^2RMEO`kt%VTI+RwBPRzs>SVY#{gKZrmE0B}33~$&2bq+iV@0(zNQ*6fGsevIk~X=AEsIeD_3trRIU~klWe6$>Nj)(57fJL^<@!tA|szVSOh$Ekp`0pcgmZ*tjd1&Ko>HD#T1>P$l)L_LxSc z4HFuusz% z^9~+kZL+5do6*MVpiOWM#Nh(sxZ>ofU2`OJszcW0`$Y#o>H`94E}a99r7FUu6OO)X zOU}J^BKLIAGQjGWTGk-Kl8|Hx_yu*=&Q(Jib8l)4O7FaXKj>aArjKjMkcyk$k8@ou z#cju3_9$NQSqxIEtB8)5XfDnbJ8{w0V8mb~DCg1r15X@9nqA}Gp2b~Hls{Z=YW{4_ z>-C@-d3O34t@~_o7hWv`4t*UgN_DvDJDkVqP-vTEYiRiD!U4OW#9!Vyua~~_#_zzr z@Q~X5p7O)#<%dbrd#IS>(Ugh#Bh*j~+WjdU92gPu$}ywb>DHk;k%kBA>M3UpYU+fM zia|*<&LsgVuFmXZpNHz0Zsmum)A1QY@Kx)|51!MCoA+OmE{u_NS;Kbk@#AW?>E_E0 zTwt{>G`c>8Azj+>Jda!#*1CDi8cQ+Ze!P6HYw8&br&kp(@lkuF9$XfU5z&=+Zx*Mr zfOP;nUW5K#-Fg9}Wq?z*mN;8-$x?}WmHdKOqqL^~i)yUfvd|Jv@_AxPfnSbY>5_t< zoxOon>)S)zV=k{n*fvebf4UNLHgAuCdEpYLvh>R(ekAeJf?rS>D5Wu z^b(n%`DR7>WSu%YNz0e62vR}#@jMsFRqm%kmi5{*ip$GN>2?Jx-ZpPyj`TmcErAi+ z#;?<`S!Y)PODVX{K?EMn21ywA4$@o86qCja?Zy+Qmts{0)1H(R&$=oYX-UnGp0qw- z&?t+vUz|eDlCLDL3uc9;I%y+I#K7gT)NSfuUO}vs@GIWaf6wS&sfD*JlP|msm|9$E*f-v9B>p zl^fZ%ra@l39`zDn`_S%)dm%5LR%@l~=}=sBU+H2|&Q!1z?G-2#dVGxqC zL73F&K%#b4D9d*_R9+iFJ)Ciap-)%zzde#s&&y^++ge|mILd6vx#}f>Pb>==DI`3S z4EbmnMo}&3&)t0uu+#H5m-P0eq){@anUv48*~n=vzBGHg>S3>h)hr%=MzD;H!v*eN;s?c484i7;OYs5jnMMLz?4gU3-K%zDk(a-+ zWgBKHIz`LhzcFocfFL|d$D90d|EXKgW5bCaP?*Ja%Q{}cK35iBJ42aDvTeWQbMxpg7bUNVCg1ydk7AtDj^8;2q zJ~|ZIw>UUDz-J#6=U$L^Y%^9|o-_X4{+FAQ#Lm<`S`D>hMxd1_Mu_^ppqVi4t`v=` z2$OC}dQc6s!vD@m%>CiUo9pb3n@Qkr$0lYhiBhu}Cu8gHk1uy6KJT8)Zsy1*LSE?u z4s2}YF*WLUpT}-nd%-_~e~r|GKd+gV64`m$as(sJkft?8Yw6+8T2$juyn60L1<%8n zFKUT*VwybAA9R(o9geqHtwHpTaE#u+6B#w$x5@V_-*A0IDddQvK;?}BA{jD<6M0=xFUHSFP^(OmiHZM5jS zd9DOu7)d_@PoAWiKhf|Q%JNN}TsAzMYfRAe$N7p{oOouTiGP)BDJx|`=u5^0aX1B9 zRwE*YRLhx9wT!4=*&2%cZB4O8AEwYcj+rirKC9O^+_Fl4^`uBr_rb1;5T0d$ za2kq_aj{FA9esuQ9Ix}VZ@dg_p5@yzvnZ=G9$@zrm2d zl8e@PMY{uS!aJu;+QcPqexx$Z91IsO3Huat=>4w6(rH(_v0of3*WF*QQiAQtsUhsE zpz+MSATPM%ga^l0L-L4cl5IW*3R#pm78>P@J~X}a`VzXjJU8XQts(bWguHeD4Vpeo zi?8m>h-4^j2Ss7|+*5cv7DDme2Ql0vW1jR!fdWwgRouJLe_4&cjwZN=r&^(qlWR^; zy$ce=`_kl>+F+z?2tf)QF=|8AusieglzS*tHzxwBW}K2&EoZ9ahStzO7%~eRXwNeGq*;Ju3Il9wC;wU zDDfBL^(vuvJf;`iqm)=%tcE0nf|TqC?fH3W-g#19TKvz8q$0Ch3X5_h)%H2Y>i_ww zZ+2WF!Rf)gZbMozq}ON0Ew*a|p>bfSmaaNs7?BCIwV0-9MWw!54acNInxo1a7}>0u z3gbv*DJcTKlYYvb#N%VHR_pCB=GL#&Yy=g$1I{ArmVk`cwxWi@JX`JS(M^U(lB(Zs%UehSkXMtqK?X1@2id3w&Q`Hu^c zOe$s-(28nPRN?^o=hi;3w?}sP+Ed77&e|z*qu;rR8MgN5 z1GoF!XE$2_6DpE;i8l?ckCGEJrA56-SyI-| zVn|m|#e$DSxEoGe5US{Q47)ZT%obO$OYwJQD2}UTT*sMd#EZaUTehh1ipWD>r7l~rhRSFg;u}|;@0}xH&)ihi zRp{#WtXyuFBn(j_-jX$?hwQ~^A?SeHn&aNf2qKnc^TR0 zj^4hn#_E3MqvI{3XQz(r@I37PA{X_4*n97&rnY`<6jX||0Me^c1gU})rDZDuA}U1z z0U;pLg{ZWEAyE{NE=5pKA_4+ZA`qm5bP(xHItjfel&~Nnerum|?iuIo{f;}{@4M&T zJI)yA4+bztGS{4Qtu^PbJkRrsa9}9`;IoS2&4>2ywNUAwY+9W?y!j6+C1S5gC?gr@ ze7n7^TSgN7rA7n%6+@$B3)}vMz+(O0OPp67E(q)P!1I(|b2md5>o zO@HzFfh#lfcW%e$mutE59Yja#RY=}LQ)5b5<>tG0oekQDPHhH{4mn-4=Cxw+e~^ia z+ovQB_T#8A3l=H?ZUyEJAncfB8-6HA1^4?G9-#Lf7NmK^MmrB$JGeKawA@}ATDdA| z@0D1IK@gNG-`TM2Mv9=ZXJ^rP=Vx{=0HEd1IAH#7pNkkd9o))04<@v*XFmko!!9|M#OzM(6K<5YVnor~et{|DS6KvX3&e{^gEm$Bnj5^Xdg85scMe zI(!qFckl1eh0CGtQ_E{%Z^9F{CDgjC;tD(rL0A97Omm%Lx4M$I|~h*GBvQWbE`Ef6WDb z2f$k*RR&W3QJ}nnQM=t9>7bngzON_Hmp<^&IKez!ui8ISw9;`V(||cNSWLL9dOt>O zLip)gozgWP^^Sn@QXA>JX{(Rk`m)I_TC-f17k<)!2wG}}#4BdIXNbdv5fk50XVGGw zi_J~8n3ub2Z`b|(D3_9t=}>a4k0yW9X}&l6qI@Q9d>=9{hvuOz5aFo_9-(**2(}_$dof zFN+~O=|P3Yp?!VoM^CNT-5oo0dyAsi35bqGYyVsVZ*nN}2@yoY%ina)z0rD{Kt{dJ zcD?ATEddy{B$<+}$x{nOnNd5p!uy>lroXa7`jem7bv85H4x6+*Uk-bTx(EnT43X#R z%gczES33T4%3O$omcT<~TH#?plKals6*iT7MTG;Eacbsu9~lihC3I$kKfCoE=5Evx z+l@s>r{WK|M^)a_s#Ovg3$z)f`1mi}u1^m}wJM5RHI0vDUn2hGw z((ysU^uAS;731z}Hk)Z+oGF(g0yB<*rV1E&vF?3S6GgW-p*#V>PiM_V;+Xwc90;uM z3NwC#6 zlC)dWx>E|II==pJm_9t`R1!&GPrkN!P2uDDlPPp$dn0leQ7InZEn`uQ7O(?L1Wt9j zB}2gPPyHd-=38>c=*=5!6%7Z3$j!b1+Z7dhO_Tjc+jG>&Yp^4vbh` zG+^8A<=57J3*07Z<1Aw?atqw$leI}bK^JnFI6;0xjEJGBLto;acH3DE`%T14Y(CZH zJ`7PEG3QKfl|F3p=DQh_>y)hdlTq~#Y%n&$2kJ5t2?omc7^{dG?cdQ5r}DUK>@D?@ zL)eYl>wJBU-sB?%yLRRTfvatJw;=NO{MrIe?K4CsaxDp}307{=Y{sx|&KE}$=N-bx zjEidrqdiMJLthN4q;u_Pk?FU89X3TB=+gw6$#Fx7A@oBCCy%bRDG>N+9CfvzV$F!E zy+au8rG}tJF#N*0$XMm;5eIf@+q%in4=CuoM?lDN($1V-Dn$#)Lo$t5?@->vp_x~b zRx1+jelqrzGCvc*RZyCLpNa0P7@=}&Oq;w7m{Ys~FN!|CH%4vB(>;sZ-lK5ZFz3D& ziabSRT)>qaq!v9|LOGkwcUJF4 zv}oo1qQ0=xJB`?0uud|X%IQmf4s;Q8W!m`1!*ht*#^sM6dlgK+a~f%%rr+2|NW7z~ z=OJ|3vNWgK+C@bAq=j!XnAq`Xm^4YMZ{NT+!VYBmKv|=WE!vxI2Y3PM*V<>)#38z= zcm3ybs+O052JZ5NSZOEuZQ+IYJ{lfUxi5wpFXL-K9rr&lP3U90_DXYRH6u!XGdSDM zkbtp_y#a!yY6rT0-bZFA?O=Q#tRpgaq>ta5E}c15E>JIZ4}qnf${Ea}FcX)Dt;`oP zF%&38zmxCBSCMNMdpqNkX$6ssw~=*Y?a`r~fcb)&6=}FNCN~V}XL;q$n7xjya+3NxK6vBtEP>x=7CX zY1XQu=xA!dNZ%{bqT9WdZ{DE$ow#qr;8`}?kUNG)uj=JIf}!C$Qdr^%>K{F8@pN4{*k&u8D9AepEznAc|g7=^0eYFYM&ilS#TGen6-&!|uW`|&%2 ziVqv`8iPo6f;k~txJb;KWqS64PSRz+(pUK19%Q?`g6_41Uvv zF9UAk;r{H`L)@%OyfwDRn?f{pn$tcMm%4$-fOx-~S|J zfuuk+gf;mtP%?|*MoN*8;}sD)ZUijD%6M^Xf!&(W)R62*YMGG~%6@CiC52$(AAWJa zn?>zAzpJ5)avFMIhaF;TI>Cfr$8cs5d&$GIb*biBI)M>Jb@mc=`1j*VpPVSd_ZY2U zvNvGC$rF{l1r3Z1po7eS>a(_f3_vaFN1RJkb6S6_@V_26kg|CD;AO9r3IF@T+ zjWgtR$M&U$2yx;qZ_~yM)w2F_6FWFJLfn)jlD1d3*WLo>YL>{a^r|#leuXZhb}YFZ z&AygAF?pY@#k!o<3ZpMK9jlmCYzW(n#MQ)J(Y7q5SFq?_83gLdcRT1?1|Qk zP{WgtlP8AM^uQA}^dnW^D5qX)5)rL`gxUL`d8@2)K*72tB@(4}WOrL%dNhf*cJr8& z8~4GpPC-Y@CqM35FDX+`T_DQ?{lphEmP>dzJ@5${HN)Uc8vd4(HSLvl2kKN=6)J43 z922l_VMsdfp(xRHcJmFTl~_YFiHK=f=gkJIkaVr?egQTme@dH(S|mo5#pt4#kCq-f z`C&4*Jwbm~h{lVEpc%nAsgA(g?tM@#K!!xlc_ejMsCu;_TxG*PFcdwTEMDf;ygcj& z&I1i`-KMR13f%}!f&jejZ_&1g+))QcufFUqqJxlmb5wbX4iZX~W9Xx@&K`;0aby+c z8=r(w@C;S}o)pmtE6r^Zg|~|-R1Z?2_+H3Fdh|Q8T*`V$TNcB7z!@$kg`afuBbpmG zDb9#fWIf_bt=t&(bM<3+*A#b)_)T98l@Fu3M!8G#4MoR0 z`Js-O(6s#xRuT)Cn12gkv&-XO)!KWIO&NguE1K=IQ|whY!o^AZPtI@G(>Jml3pGdO z4WUGUr*|t5#k-<>tLGR+&z8mxM^wspdz;$~K&4{{DNLBszxfVQdQij(LVV=c zSmd%^efgF|X?`Ov)vz8&F+xh)&p^-~(O52w z@e~78uc|857C8Q!?qqoUNjS~v#AAF59o5omYCO@B{zLU;=HEhL7fSA z%&!@I#0D%YB%Z(&x@*cM@SAREstb}D;%=nR`@=g$Ro5~M5d$riX*V4+uH6a+)j;E8 zYcKdz!5F9}ZgtEbAEk_EJDT!8Dv+p-PnDsYU&vJs8zZBaXkLp#dwSKM>g@nD_+DQp z_9V?|!x*XV#LaS|OgsP0h09E@ete7lq8G5(d)st3=uSp@lxBIzWv@Wqy+YqcDPMin zh`6hcKUNJdpLldxWZud*R~C7#AQ2RUeEWZlyyN{ry$j6E&O8?r4Y6y>kOR-8Fux&& z`u;C7t)8)QnSlwF6t*PTMF;TBx`$Ja)O_-b$_aRp1$CNq7K2xm@Yc6%C>vBzY+Ld1S-R`=D!8nK4t2KP zJh}!Lch)k@^jECrYAb*#UR~F7S1%A8~2!9fM;lUkHuzq(X>&ZJF+0eiMtO0^O>fLRu6B3> zLSwEV3aqMliYfNj-XuZqzS^Yqk#Zy9Z+{_GLoOYRzviRutc=_^dd=Wyt6;uS2j{gpe84vZ;#Bli8vpR&;cBo_6 z!~4nwz`R06sF%L*1qgW`;(5ptl{b{^57s4B(OM=#5u`QEWKL7ad;F(_ z3{*Z(JIe?1V^&Pd+6ksb1p~#Q4oGgANf*dbN3VLRR@M5bz6OpB+WspY%?Bwc`GGk)mtr*{vY^}y(oJuDNeb(! z``(4Bxs6^bqv{^p%HLaf)F7tr(HpUQ#MtAZxT#}_P}@(_n2_RY6tfGrxMZbb*im@N zIsxLDOSwoJK|`QkP!>JxtoW&w!*#2ruG()F_v;3;w?tcOo+~IpPH4rqeIPx)+0fE^ zV$zuQSwF-zx8gmqumD&OhjonRHv|3(ZSUoZiq|Qv8Vx+pe{|w(N=qyH{eF3G%*9QQ z44e^p{AuG_f+5}UuXN751oUw|1u7UfSDTV-uNGZ;zS|p7-8vVNYE(N=>^fXr$ZZw9 z0rfG-@R+@61?V)SDoMprHbnmH*$K&qkZ`REiE~){H zb=)h}G|wc@BwPQ4?rk zo@-eS(_ToogYZu-ht`H9J|q-p_e1^4_`%kdkgmx*1jxJ+7%pE?KaX9Kv^9SCg*RF@2BL9U;#&Hf zBqom&)+wMp{0b#Q>mH>s*Y~>+17nnQxM`B1YQt3hko%*=)j+d*Qn8w2O7;(KCNf&> zs0LtEqE$}0L&1t*T<(E1{<uT?#8J7W3|$|_hqIX6RyZz=e(9( z@&KxM{f%iCh`$OI5+f03$TdW!Rt*2daSH)qaD_cvb80c48U%C|0H$Ep9$fYl0CG$zbMB@^(AL5SWLw3c!l#|$09o3llToMJ zpD&&oIRx<+M23&f91&s>!yL9r=I9Pe#)b2$GBNDJ@ipmQ#Gzo zt}3T1D2RRrEEJGPG>TKC%Ab9Qo#+KEys{~yPg%_hst$GEws5jWW*z3NfPZwiNsIBvZmEP1u22@wL67r6f$R@A)CqRN2(#-knjNc^!ezK{J-4}d zKqerkz!S|~?#4fVtMTys%$1F~-ta@Wm!A->Df8nqcC1N~-oqPN*OdLR?Z_~G_gLLc-SHmfoNSTUdO`ZQyZUAKH9{c@g~#NY;9!MTW@ zo#O>(>}T_`a%od&wms;v)ePBqtC7*>U=zcLJE_e=*;Z*Uj5QO4#;%Vv!r#pwSbE>c zi4^EKjS&Ezg3smRu#PZc!d}>-3`f!o1${Iwlj4c#DWoAugoCGN^iiANRp=Pu+LDWy zW0LcUNhh`zD{KTvdx8qt*a6c$bB!xvp36IQDYrjz_M5;@kM=FPne z65+d?(9vYa8z}ts26@mmKdJF_hz;n94I)-RBZ! zDQu@Z}5McRtRWZ-{ihFva-IVn5C-3 zba=C-?Yp&$}W@z_>x(;FZe)(v?Hz~{RMpoB-2m5Qmxyt{{fnp*06(Ua22k-Ysa z#LqKIMwZ%NjXMO#AE!PB&18DuHg8x_ix90KKctj3JBtw-;}+)dE%bQ{AHi* zgU4OM@|WzhLT?EgAAr8)J;0g(<@FHfKuJ+G`~3uqb;M|A`mj!;)SKF4K(fQV>{2hj zv}6sYjgt!fsV8h(;zUE&E${7%molwu&Ox6|%k-B+I6YAUwO-=vYg(Xok6#&nQ`tYD zJ~qKYt?o03G-fMTKDj0|;gyzw$#qY84eL3?MC^%b znM4OoW^+#%JwB#8aDDE+u>WgqUdv8LNZ2&G@q&&qVe6&dabjE%);+)+c#5CS#0ygj z^rai^Iu~&%g7=MLufBfa6UZpr!b!Iz*@rqq6#^Pb6Zm?JGr>eWTuZD7FKTjq)Guss z&ui++s8wBQ`cIciWe-+6{nPY+7<|Z*33xDptM?$YbNrYiuK@PBh%0M$LplcjSvJ2` zQ^wWjwTffWxemXk?)e_;_R=}0V{&@o0b-c>MELi!1wcIc5ahG-CQ(c*Nb1=m-%ft9 zpJ;fo7mr&Rc{m_jRrS*LAceRn^D{|KmX7-m7nvVae+oD94`6VPbxRNt2V-0brsWeF z*gYZHHlF&)J$^9@o!Fl)4W6F>DpfI0m><# z#y**CZY!;bIlh*iI(^V5ihf3>T_P$+?Lv=(Tb3^XZK`J-+)(moUf+~RqS)|Cs3ObRBe?!LL3u3kHhW6$N6CQB2iI7vxhjB##eXXaXY@|cSPfAzZ zNeESWciU^>7V~MuK2-(V3TFbYFEv+llw@_|XMkJkrd-C}a6-39bE@s|o0SxoIIkM* zfQ+$?O{ty^`xJlks-(@)KRFAHdMF75WVSvPZ_l73|CkWVWInXiUyGbp9ah_LHj3L^ zpJZ!ENleE{M<$LxeZf;Cw54=wp*QW*Kyx1%%Ygcol0Y>i(c%eJ-U?2AR3~q|_c3{4 z;q>S!UcN$RJBGbiHGFt*^|RrQ1HD5gSwCphMbffv|d}kVg*NpO-(VI6HYh zYO#kPU5QI&GiEc7y4a)H(&EU;^lFUOC?*Sn=a~rDXxm#Ax!rRG@tloMvx|DN{KY;5 zeJ=k{UcOQElOX+ZX^3)um1)ycutz38N24tjv8q3aTRNGJMsHVG-jHw);a1K5c`qg{*T0g)#q#s z1uY{o1wS7pDjzI}A8pHBm#a$q#Z2qur*6m74tPTTy?wr3gNh)+*Jp^dtyTvul2|Gp z4HyV#i=Pzl=y&;x-y@qjaGb=e^<&-dAD)g z6PM~FiGjH^Pc!#dN5GZ#DM6w>}q?pi#;8NlodLm#l`~>xaiX4am4gIua zli<6LAIOw4{I#qkKKeYWY8DGf^+xQmU-pMb&2Z7qAst5rsbkP`ZxQLpShMCGI#krSem0#-d0(PGjF%df2S| z){WI0$k!T{lF|De`Lczg#=3U0F}7{F5*HgqIr=Tk4dg2-|F1|DHcs>Nnb0FajtkCqId!bEsL6;6e31sm4 z9ZbXXp5``a;$KGt{aY_SE|{I8f1TIy)oq;@gw%CyB8A)tY!%oee)QVX28lgy9My{S zua9i*))bskOGUYkgc`8d{D|LTZ0nElwS-W)cWj6WJ36N**HQg?q6z5X#ylWSF}Mf% zheS=}E3pZZ!x6C;AD21X%e^M_Ex*Wab>9}p_w6@b)mMsK%U8iyb3c(uOX8;0@;Gc% zZ<)YI`}NOa$UdA!|HQRcT_OA1%l?KM#^N%$>+KY4#O=jFs3(L2NU+{)BtXKyCCsDC z6$?p8d*keezv)U-Hw;L8XQNwTxRG>x4Qq^IYwimqI~=ajL(r~yCzW_%x$z{nNER!+ z#=Ty@K)d_{fnHw%ALVVpnWBa$h4FXN!wjVq(B{ zbp%ziWR zTIvy-3rW2->4B-@9l1Umxr+~F+oi0RX28IQd#`{EQjx@4oIsLAP`;xWNSfif5d^Ws z6|eMj%8j*UHB~q#?~9=`T{|u@66;Car!T2TlJ@6P!blZ!qmiy~vF;$^%nj1aiwDsb zxY31*LEN%lJ7VZCns6vL#)X@!Uxa`m<*$pC91-?hNWrcpMMhA zS>Olz(L3q~9|-6e4YF4E2MkqFnOpgL66tJDyD90;v}$#QU4e~SK^0svykx0xaKgy1 zH+Uvx zlcAEkFp#5XM%LwZvK7<6jOP4O!XcQ%{2-Ye7*=nd)k^PQ2y^oi&xFN`(wq*EmueNcz0iAW(aw zffFT#h;D8W0G)2WZjX?CC4<;$pH?H*+(TX=&V*C_>&Y--TL-G~ zkiR?yVRf#)uM`M=DqS`hqg>LTF!zV#@t+9~q``P6!Ln$pNzu8vfmpR`Swk$&&6!+v zP)Q=tJ|UrVRbtrpn?wdH}i-H@LZ)Ful-gC4(hA9Fy>%zo(0vxRDp zULx7ynZoL|5OQnHC{-dYiQvrw-1ky69CHhcV`e)Jc@ut1z%j1H`llwWG@`Zl*AvqX z$TPjuNo~^%71B$93t{T={I+E8*4aJH;>;;BOk6#|L7TGOET37y+|-0DpR3@_`+0>SHMtU z6;*kVdJ`I6FKP!Q%>~AsxgT+zcs67F{iCmQs#7Gm(4!~QPT!fL26&dBVGIL$ImJ&2 zZmkWhNC9HY_-J&kK_=jA{ET})EoVL8bqrfVRmTYmhu6a1V1w7pcrHzEOGlWZ4%p5? zt8uIZIFFiJ*|iDpGY^7c`|w98_W=v}WS{`9~gD zi(r(PTDW2-pOuiFU1;@K$BW0oq_J}qi#KQV`OUzumaSlTl@|x~jv+NkqwR{9VLxd7 zgBu)g@V{WWTk66e^Uk9y3pqF-yvzVEGQdyrTh%MdWB~?wB~G< ze-rd}HBY-~@Zs96LGRtC%--IL`10}!vjlyX6LIRI$MRM~4mD)MHb< zX|z+ID0zpSB<4n>SMIbE9GoBY$l2O^^+$F`JIQ>1AJCyE8h|( zCYnX-YUQ{u&i#=u6h}w*h;Elxnm2j%dT{4t5s2ysS@!=}-R-aK$H6eEdoLgZe};l0 zijekhNBOgmNUJZX&N*?GNwnK)UeM_qcOF*8ymvWkb%O1IbtKgcAd#Mut%##!qjuf1 z$~*wPWU6gvdA1YU`} z31j|?=Ni=yHN^G4TSwrQ=0k4ZoEYew3R+p3+BV_X%G}-KeFst*DDCK76@&?R_-l$& zQ;n$dTcV)Z*NRuBa!#VR?LCgl!Bj!--o7bVQvx)@20xWS@i4>|EnN8|dNv%i!V;Ik zeLG~X;ZUGf8hiwW<7p;Y6fZ@PV6e7{0qZ=7TPNfixT2ftNi#=m4&2})!VX$|(%@nE z*K3nUK-494bP7tls)v=l6$N4l{@O8(cRvoqt=uQ3(wKdq2TJi9g%zM>S&gQ?3UM&} zp#L;?V%1zBkW>FrLPz44j_0A(=N~viNb+=-SD8;(Dg)Nj+MulpX3Bvxm7rx>{kOT$ z#=Fo5;MLg z4>%8NW3}Wj{B7m`zLwugXAWrfE1(Z>W-my{&&utu50m)c*V5aiVdp4h5WE-qtJ`YJ zg}s08_OFlIf5|P%d+0|AQG&>gGkX)1ws|6tDJ|V!|0O#1wnVp6CjSeJi2W0o{~zu; zP=C_X_F#elPkH$hRIe!RSdOyXMQvt)W7dNDsA<5F^OdJHNp2wOnp?BiG= zN}nCx@lzGCV&fCzw4LU9;cB~@wKtdIw#jFIT6F}UkNo}yG=N0BI;Z(ZaxCM?lyVqX#uPqKn)Y@FyMfA$!bN>@p`R9(4zaB6D%>T##i{dIgceLVi;8`pC z{Hdv&!^5_yx=~#eK(6v(r{p8u~wDOrS)bZ6o6f0ok^~l>F z#NR5@pYlXpOq{rC*tRnyT(+%DoPTyJd8KNtbhTw{3N;8NnzO>_|Fn0)oQVa^JIK30 z>*H;W$lM=%?ai~hl|!4Tg<8kg0d7Bi-U>37=^Y36>`R}AYte2b?KxT9?dry5mv1o&%Q2){4pv1>ljeupc$m6eU~y1U&fv9bj|ppZUAMODK^1 z?SphZC}#4ry*p;^g!qb;`@8Wp! zJ4EJA?_r3=?NpjL1&mRQ7Ne>Iymib8NMOt-P0VgoH6hxsm$MUMkEtRkA)g-mb90on z?l`9ie#e%}7G>JxbSYZHx7X$UFF`{wqDXOyAr&-qte+zda)7Mn{V^_wyFaCFHh(!! z>6oiN&!+v{-UW#z70^z8t%k*K=}A%Edpu$A~BULIWOI?gmb(& zPi;$`g;RM#*7YQ8s26}ed^w;`(wZ;*g)0-((Iz-@9RKBjNiIY83NriaAlts&xs$4{ zh^)r$?7^O4Cl52Cc52Y$z4-2#4S8rIP-~adNjf`fBGfW2#P5+=aj|V_Vj(i1v~xaX z2ldjF;~{kWM}qkAl~uHnVaXI+ogG)P$Mgr+>|%^>77$8b5R<{rtl zE~wJ!STz{d(hAdxjxL>(1>Qatoq}SF+J!}olNaZ`y0@W?3(mFY+e><$nG3g z(2fh!6x+8|5w9e%Lo^+Pj8SS=6)7;>DK$66Ul!R*Sqea3|AKbMGFBYcdH6urrpG3wKZKlR7qkJWfXCN=AXcB1d)uioPiHPcREr)ZsZX|2{Y`crb^ znHIWVhO0=aFKOOThlmzVSrEZ~)05X&c+BUFne{5E!>gy|QOH$n#Fw1;QTYQe>3!TC zuge)Z2EGjtJxM#@aC$|r>+$DRTd!BxLLZeyyAnYQW+|=n3--4-AXk`$#FR-H@kB~< zg>7410a(i<=|P4|>t?23$_P=ppkrPx{)yLuia;>VQ%l`4yBZCP@O8c(b>M6Tnz|oB zQ|A=iN3G!*?HI|e?Z`rA`&L%2!BINCz-{TsC->2bhfGH*Hc#l=C3($%)49OoF}S&) z27Wz7P~AcIQG@17njj-CAvw+>#LCePZpfwJ8PvG!L18RUU)Y=3oXqfP&qwuRDk_>_ zV^(*{NvScoiJP7%MVyK5=y>@=z}<~A?B`@oAG^a59JS4VlneEMjK8^xK1n+@PE{ao zG-@MnlT>~N#tY8tXx)+eArN=@UDNxR=b`dr4Q{Ysfq2vP>{yxSwY*s?TFK2vl2!@q zBl!W>4$|zZOm582C#AF*6YB0`bT3lnXmDyn_t(+ZGJ%ioEP24Ye2H79md7jW3qD|bvr$*>;0rzJNh{Z3m0Ju>({r%EG zg}L&I_~RlHblU!{KIS$(xTS2&K}d5sxby!Tk=y(~$g2I5fAcoc`p{+YP_!p3Vi0g^ z*JA%%_4!(0%RQdTlH`!FkMAB4f8?If;xyE~SiBnbIO*0hB;F`mPZ`~)`I|0~V7jpt z3(n5_iNweTfnpS0D^0g_q^jONvC-$I`jIzpa}sFxtp z@&!^x4%?ihEFiZoS&6c|SIuB%idrsMfJS4!?nl-$13PBG8;ZsZsJ1~mo25KX)t{}I z%nheLFp`c^=1YC>;N;wQso2?=w3#$@JwG`)q*UTdj|f%`V_jHG#EDkBme(oNaQbJlY8w^ z==ofPGOcFnH{HsGQPaOP4Ezr(_`ek^cnKwsv>}du?~NGGjiSPeJZNIX&FGUE+cdJd zh#B?|U7NSBzkFf6!1C<%dm#Rn$AY4q(9-8l%k zDMkX^x**_Q6++h6vuqjo{^DE}Tj2Fmb|CWSz? ztlC22m5SAS*P|m-gs0TH>$U>;%(iG!*A3*d}_5d{f0Nj`;7*yjt0@+?~NMgtZ4)< zZZzp`A{+7lPfmKpeFM5@bT1fETk~82-Xu5WVj84V!huk^hsB=UoApaHSo)=q!R${}X92~m z&w9Q71QXFZz8QzQtKvk6CVb^aHQ-ropIufFtz+&6XAzW2ym?vB*-X_~Rnn2Z^K87E z!G`7zZ2s4V*pib$kW4dL908r<_>eaRK<6Ilohuul(;Rd^?0JJ0eSP}1i#x&b9ZUP7 zKBW8AKd(x<+?4b_99H<2v&?Aoq}UZ#8R#+W+>?s%AJ z(r>ymPl}hY<_>`zVfZ?uYlHrbq>-Ya+_h6ueEjDK7eq7%^a8R?Pss*66k(8hFLDP| z@P!fw@z8MLyQH?F&nsGLPZX3x2xv}Yve`yce&VY4JKg6x!%zC`x~ZuLIKFt_5)!1v z{&*9-?Dhz*=A^cf?toNG$g!lvkZ0WwpsknoS|{+`Om~LIBDx;?3VV>9*QCCPM$|s8 zdw{SbV49#DNX4YKH04M=kzarupGtk5OP)=8P9}$>oR8(pKeo<1vdeF`UDXq&S`i!C zpd9Fj2>q5crYKyz`bSDYe4asw_o9-f%)3*nn}ULZ9UWQYW-yk;z2J~8SPN=(8L?P0 zyA?0g&=!KeNo0!BVaJmSR>&8;#>x4)H$ya%kKa>J@gP4R(Rj$Duu~#=*7-Rt_Q0&26!jq&C-|@0X(J6=j(WIkg{l z7*qQ7ET8M1><&UV#!p1IV#=0U*r9m%hC0WGOd!GnvtBPqno0}O6Chip z18LQvt~uc5$@af1|1STQWJ*omVgOy@rwvDEgICu6(g6E+h>zIMjpa{KM-- z!Gr>;^iK01#p%#{eep7%@e(3eJpvO%90>T0_}*cp3G&xTxHo5)N|(}N+c4XOPP1R2 zs95jD@JCd;p6I{^ziBNgqQ#W2iwtE!Xq#x4vj@);wZ-KabZUPxo4;p#cy0mhjQ)jn z07Ee?^4M6Y&>kz4m->jrP!+~@TniSekbZH{Z%At0OdujvdR}_F-52+P^K&BkjmB~f zi164sd&I@RUPU>L$ZEy#DAF{M$i#Ll;4?`Jfo>id2s(r|KQ&fK_k!V5NZ-XmW~5(@ z32u-2Va)msTr9HE!7jq(9Y00M{lgLm;B(y2#ty-67I{168-Gz>m{fc$qG5Y;iXt!+ zrf`+3zCV$vJFa|i^c^rty9EJ3-6g^3qjTYPF%id21f%bG&7^cM4+Njx_n=)#E-Z$D z7fT|xQ%R&i8n;%(Z#qfP9b}im#~;iQwy0A6aGGI(n)Mj^OBMV`2b>!Sed-XIvDubK zHyZ=aGz0*S_N1sCn1RXrTThm0phs-^Wacmk_=@VyFJ26`5p#{BU$ zJp(x40cAt?iu0rKk_G5(b@9LVQ)VE(S`1FX6iNgKyiLD{k*J`mVZxzH@zD*$gyDt* zq4>Y_V)H3}&sy;}T`mUTK_Ac^_9ocTgiM>ociLNLfNQHW)L5gx^ibGubXWl9s|I_i zEKusR5VwvmGdOHmzloZI63Zniz{CnnSV@M*oN4YbwQqJj;heCxVS77AD{&#ThH;}R z$G{0($DD{#hx&Ui?oX{RcZOlb;GaY_o$~b3X`G7hh}UKoqEV$yV0e z_<$!J?`a-$%ROC((W^MG=&pjN+WwS*o2O%$hB9Q2wel|NDIb;Bvs0n$M^z+e{I|Hy zVYrTo3u}l5?pe~hd>7IHw7oG$Z>ic($uPk7@K%vO_5AXPW?ETCNElAl?ZB!e%L2v* zn^bxba@hd+9z6?cSC2N#TLAhL1H`HZCCMFRS!`M{?*2?=-V>-PPR8gwXe2=_|NkU{ts?q#>%1#?j0l=cAU6 z6*S6&DOG5s6n?XqW__o#`0_maLTZmHeY?{@{aQE?4w1|6)RkNt&5ZSM5WldXe2l6@ z%5B$%$JO(f4p5aLFRckv(*^zoU0-^}aTc~FUcL(xW30CYT~#C$KF{+`+c|r)Ow2$U zJm9ZhBlKstVzD~@ny=h|R>kcp07lX1xVx;PnA6%uF^XcS>Mk%40eN|@R?}n5zuJG% z#EIcac+GlmWs^bN$gonz;qJJd)Y(lusoq25dA;HHE)C=ForU5MT5tU^A|_Vdl-t9B znHZis(EYZ zvKkOa-h|+5Y`m$u@*d};w9W$L`?7s)R;tIsuTAUz(3tlZK$mT;ALJrW0&e~v4^j@x z+se^_1zwua`z8o~)~+fc<6l>*P6{fPo28uLz3g_n1mmVy%&&8sTyj%mj$8=%f{5j{ z%SYu*FQ`}j;U$i91{yCqPkD=6^?8YMc%;cRFQv+>M; zRl8ODMA2@D*mRrxz-!aBJ-ocdwd#XMNq(t?kHB~#3BA~|% zWJAnqO8Y%|LKmzhqFWN;>+kPr*|&!lwY;KjJ#+80mRU1m7fXqDzD|I=yw~3m=tO{Vu*$rZHeX7rd7UwQ zDN!TV?!4$w$wu+3U3`a@)C3=gjfIHd^NgALjBSu-LKC4>oshxWdk;ul-FeUAn=x-= zJ&V3FfdJVkb7VbdT#zHoFrLT{RZN%|7x4G1v%S+R^IR1yGMQbn*SOTKDOjqUdlGZ< zvB8lj?eq>d$b}uB{`QRlj$eg@CJ0>!cynudU)S7OAa)s?h6Y(@g7HG04@C$wk?_>J zyuPb#B_P}SFS$jR`8;ns}7l+@F4BzkH zGegO2Qbz6=cGt~Y3VDB_^qQTa!n+cvx3(VT$(i^i)!}XLWVsLWGsX_L%~%|`_Q}1~ zcyQwRu+)&kPt5t()@|UJ;qhfye_#Qhf}?`ci%nC1j^fU-3xzK47d^PO)LiY8vJ;FJ z$LR!)!2yd)K=I?}rDg%4>0+T5?Lj*pzP`(J)LOv{TY7xuOkTbdSS%h(hRgm$>lZPN z!t&g7`)f{4e;;kW|7PpM;o=;T(Em&f{(tbg$k@>NY?}_Gm!O}-#xBSBqkA+=KK_=o zy>`d`J&y((YfiJ@vA{Ff@qKVv--F6)T1K<3W41l3q8ZmU#!eTFJXDyDfNzzbh#467 zp{=YE)Jc6-ujfv=^?urGeF`x&NXJq!iSxm80A zS*d;7bXRsMRp7yq?~3`J(~!c)DE6TxO$eH4YagVA-z#{P zpd1pr`a{&C2X^Nd*RWhYqS!Yq%h-iyBMjw4F6YU7jyn1YANR>0JyUTJ*C;g?lYg}@ zc-QG|ndWvWGHB&NRwMew`qv|=1xNl7-{_xy(f{0+`G4XE{rR*1s9z*r&w?{zbN#Iw zNs5uFvybk5c$i~rKc+V}ZjkGxB6P2e=kTin$NPGMp81rjK?;(UfaU$g^?mOR(CCZg z=|f$Th?j%#TTXV!m=0SKB` zjtoZIb$c1Z#9dBFiyNSPE$ZA+xt&Kf+P7t$Gu@uZBrc$eI4I*~z|N9?z`pGQyoNu2 z_um=Lb&Lr55I}rkS11V4jv>f;r7sIa5fO{CAqG)H&TYG#|^X{P-d-pk$ZrV!xRrmDAr&<)e(O9XUxP=8)?93j9mWfW6_yKge$nb%WBz2`3_$6IMrh8hX{ zrw-jH@{wP|d(r%wkoy@Y>k=~xe8@PyR^&&LS->&F{1M4JQ39W@cyhIJP09-nh=#Wi=91#caQ62;J7i(P}FM z`-p{`TX*Qjcn!8=bF=WV7&72E>AQh7313K}C0!Za5PqKTy)2l$wb@&~eeUFuC@ zBJN`GR(@i*eCc%$Gm!CrgKq@3;|1F!iY{9l#iAX{N=}V`vX>Rf(AmdLLvkUk=`QjpnlW z0B&0j-Es0_k$VbP6-~V5^hpn>Vn6aNwk+lc{>PE2^a1z};>7#DEx08EN6r&~-J|Ue zb1B8+Fk=3!@7AZY(fwdgc;96gQLG=U`mV25op+OkomTGD6lTZ|F7%W0h%O8zD&4#q z+Mlj4&WRdLoKwY2;1Jw*&#V9f?4ig5+WvZ%0W%_DsghWZdE+h(kelR$+UxJ-j8W># z(Q0|xvy{x0)g0hr@;fngN$lm92)-_6%F#m1)S>*BUG!rNoy=FB98s3Wvv^@bkSiU_OW ze&~tl(H49r6oT$V^RVA=6y2Z)7@$9vWZffu^no`72?>gJ@>6P70AjgKk&EYtv*Sxs z{Acp)EeGvmF&pJhv()v~HH`!vSs>dt_#10TaN{U>qP-KD9QJwry%(=Pf5 zlt$AJmafW)ieBDv=@-{RY;#`)5ZVw1eNQwoQloAhAu+bYer+cT04~*#)l%mjPGNe*UqM>3NLO9+UjrdwB=1TR*Zh{XsgvQnk&pgz7+^| zM84%O)mnd0&COk9_o`f)Aj4Nt+_WU}I5u%>u$lBxR)nVTBuK7a_gOU3?Lk5b zFe09WxFxl_w@bMQizmJwJ3$gShRg9@{*EJaUY5dLkd#Wj`FX9+6@3%5mj$xfKKx%5 z3}a~bLK{MFaOL($T>rXlPh6hxHcl&0xZgkbD2Sf$UT4^QK5+hed1ap(J|kKEj;Y#< zW&M|f!pcUWDYutHo6b-&hx$r>arM{z%%tJS&}q4t480 z>1@Q?@nrDHMrl1VOd3|FzI}E~hRUuQc%&8!iFQFM^p!$fu<}o>sF!$)g4Y?S5jZdCIOAW9*#86u+BGs~nW~BAD}w?^3e%Lba)o#Y2)xz9qF{kDu!XM!ihTReN=dvKK~jF_c;tiJ~|W<0tZ$pl6%K zp`Ag{v)%!ag=Xp?IHOApHb8(EbsosCQIMi;Xw8})!lss;cjA0~yf4Rw@UpwB!}X#@ zM}>ry%c~m7{R=n|%w24_w8UFbJof0WtWX#kgus?Xz&|jbj}AjOq4>N_IRDL#1C$XJ zs~K;`OYPDGzl1grjEo(G%A=bh!ZphNKpF35KcU*z;QWnXJx>ef$p-0{aXX9l2kc6C zMe5pbeMVUvR3x1q_(&&5b3%e`Q5v1K1+L3M^fVG z@#3<#+KTe=N|@11*fC1Eg6FoMv{5+yDqLBCCfet17;dy^!~JmA=1MyV{T8M?1O zYVP#%7WDj&+a_tAkDqROmF@^J+Mu6h1ZKOZsFjy}TXK$UJ`1WgRAelxbe-0)MIe22 zdllWDz}t3y=A(z&)F@HEhyHMk{9$!EhZ(@SOwEtYwQGGpJTGb-lsY_HbBIXYSxr8m z>llQNj~O<|f8S&jf3WTht^MoQ@5j>7cZErKdN89}@)_$ac`pSgGz(RtB0yt%SJL$J zbcZ3kIp4$=(HczyKrY#BkMfd@b(c>(Ea8W0^cp9bDeKSQT%RC)b3m7#=i!rOGxwkd zPw|9bF9RLqMTa5KX^QE&rny~bhIOY*lyp!?ri#BXc(`=L*@3bOxEaewks_JQ|ym62x7 z#ifZ?aC*jjP34R}{s`4Ly*;X1AmiGl? z+q+bv=9bzUR!p^4jX_xYFbLlR$1-d}DO5|u>~$9CP4ZE388@CVLq<$*TL zksLh^AAt8eNH`APJXNlthL0u=Fc-^VFy&}jKVk8*j7E&9n?l~liwdUPD?fw(#jbA#g)^$;P{Knmj}{Q!z-eqmo>7tG}aVtd?6qg)u_Xg*q~d5ETe9J z>tdGZ zaL+PZfvyY_GBk!^tkZNN@N8(;M37g}CW(IqLGl2aU5mq?TrqoD>J!C`a_VZWb}7lI zXrfE(hIyK~d{2NV#?{6+a8HB{CG_Lf=>SK;S_P{X*ayoWFoN|$7#UAG*-)YzUyjT( z)k=O^aqq6aY1XW=K~=8w4QDeOicQ+>oVeG=Pc@xLTyD#`8tPH4IZ9;gdYkF^gZQKZ zSwB8}elkDZEzchbrwKo1ot&uNHeYMIyBbK!m&7OBZ@E*()U18oDQL#`YRO4NTD z5$js>2Z2PA1z9%gKx6;(6SA-@WAH4MwIL~VzZo9fvBQgRU%b=v$cB%xq2X$1TP6@; zT2Lh;!n4x}B>RCdzYpJ&)#26D=_0@Pqa<6e$XlwfqxH`0+Hs+wX&3K3k6lsulR_FALX&3x%OCdQpOXFHoabtyXcq!yGswEQd0-*dr7RhS8%`O#9D>Z<}vxggO0V%2It$ZIm)Ke<|k! z${=wS*`1H?nm+rfms2+`sc*N!NTTO6K4^OO>%&rAw`6&X6e>S&#h3>f8a`wOc$bb4 zTTq!_MFGZ@(^tlqvMeDIL|432GwZ7R_@4X*bD(JP_(B?I8xI_K8nt3DXETAuigJb{ zGSB{H(Y$VFTDBez1NDPsD9=S4WxL}|PdrF#MnS-aSz`!Kh(>)xNilA@s9=0=k(0FF zV1|uel+HV3l9%~s(ISOR?bwX(Zoolb{n|5n2;O=|93Ao1QbGOsYmiPg+0VN$#Z*N> z+0h&bK9sYcVQo&$kH~lO&kNBnRqsXhn%LUl_Hlo7KFG!Wh3oe3>AFAWIBf88)Z`zs zk~FJgGP4!h@d=(i=B`a`-zL+d_%=h-u&XnN=;0VA_e$d={hgdeV)~uYA3f+tI)02d zgAUVSAO&Sj+7HO!f2@lfT9PY%G8H#)YVum=g72p%t@8A3k56(WK~a+vEsP!FO=Rz0 zDaV9@4GD*Z5_b^Y7ab=hL)KsnqpGWv1F}If*GqbbZucUsY!+wS+GWtDrFI9~oh(aF zJ2`vy_o4R2!$cTG4Jn6<)U{)?&bzp{d6kNcZbBdGN8>2ilRSbu`7DBswSM+*cI~yq zkSxUzCYdWwE#TewsjJCa@r@AT$kaxTwEHJ^G;WKBqoH%e{JG%n5Vv*VbK% z16gS^h<%t^c_3mpE>IZMK1^9si@-_7X{_d98#_>X8 zbChbu$)6=#=yE~y6s6q3fQGOhK)#sqK<=80G4ieQjVXNReTYxodxp={u-1e0i)(uK zOcX<%!_Q47it{jCg3%}ZeUeJjCJanJ2X$0k!w!nij_qSKfAA-$57}9U`N2zOCF5aR zy7JAFAoRZ-nAz?1up(p~J|abtHc&8m1m*MT`>bteY}WVLl7RuUfCmG!T4}bV`omQZ z_O@tZa8*p9PtxQ4sYasYd`HpE z2LVV;k+j8q$hSIyJ*}?QNi0}PGpT5f$S_gC%*+Wh7eI80j|ah`X^;W5VByk8uTDv6il)&9lB z;kTXu8gC3ue{6$;ByA>?1KdxtM3Ww>OKT9J%=8defcT5jR&?af8 zPlK|VQ!f$6whT@3B_BM$tznYHcSPf37jr+lPgaSY>I0Frn;#SC(}(efvdl4QB*YSd zso7n7_*flTZTj^HSL54Yl?#p@E{CxTx|sHQQqjEn-=7%9us!1~9_7Ix(qt|dGC~8W zMYT9SR+9PoYlS7fiIlA+Gw<>k({$o9|J%jSl+K1m*}Q%8c3YP1&qbnp6zl>ZRy!fN&gG!I4`{L9Yg;zXNMn6U{w|Y6wvi&#^Lj8}g$Npu8tlQ9h>=Jzg8l3WROq}{M zgn}0{^>E8{x0v(2KWv;??-InDEYup~HtAV;pPxtLM{>buYh~;te)!&ey)@zlVF-PI z(OW;J@r|V%KnYU6Jg_ra#kT6WlT?qH-H=?}=@%8I^7ky8zL}=|`;4sq{TVsSaADpZ z^ADNmdhBCh_XT<$Vq|M-m%0|s4tDI>yXt%EE>anUruIn+AkYaP=Brjtx)#|E;HWl_a4R`-A zJaO;EvF-Iok9ID^porw=#bHX6?Z!l2O(s%6fn8+7I)iKDzl%??QBiH-Hu^R=u?O=w^1mMnRyF;b=@mi|9O%BkivOwbAH<~iaI}idtHQvYv4^v!*C$KA@)PIuxdbfexFYg#$) z-KE)G($BAR@chxyl_oO6?ce9Y=0BSUQBDW01fBK?-2oF&(@NH9Kx5$;Hf7-HU1uqik zV*qK?N_j>;Pcf=4;z;&?T0n^BVdoNW-#}~oN5W|v!9LHZyiEwuO2CL_KnRSVhGu-_ zs3mA)zop&D_fwbS9n1Rkbi|&*+XX9xFMdO-`ww3MmEe-+LCIMlEvLR9AYp4qp+s>% zd4Eu)`5T(o-S3^;ApN!f4qFINx@-YAOOl~)cAL7O|4Xy7 zrs!_3S2{*$>-{3{M5ZuBN{exn@i2_|BASo$?Ul_eWRLS#f0st>)+yKVJ%=2k_-2JB z_Kn}WZ+iMtFpuXkcaKC|2Rf&Xc44s`C~W^DJ*od(v--DHtY#bs5X{^UTPq70O0EW6 z&-d?c{WZS%Bi8xn|Nn<`!+YqjwZOJd5?L`FW^5*g4LN`uY@UHEcdV@)8apZ{kg&5qKs5QJOZX)3YhaYv8rXGwE_&8>7%aWunrBAB! zWIr_*>fof^Jy`lu^YjSKT=rwF-RYA`LNUr~vJvcU*Dd@w#Jrlxu4$P1I!?+17@8Vx z)`cM9W?30<7&5G-BN;feQbyV*Mi0j>K5EBagX*c*W*MD@QrqjW`}DqUA}VE^*%|uw zjyk}h4^LF8haK>cJx$$@+pWJWreC9w)DWL2kz49Jf45+yOS5UrZO_QP5-XB~0#em0 zcFV^Q{<-)5G;}vRy$Uip9zujbCEOF$U+O!74%K{Qn0VKRp?L}Et1D`~W=1bA#9R3` zo$DZYzN)#;wf`Mu)&ZQY({OM=8@_wc~tRc^=mW2twh=4ztYv& z1~rv<^6u0P=;aBc-3#w#PHT~@m1r{@li%f}7kGj|;rX)t4QW*I!8L^GcMPfLIv5c#-ydWc~Q-@<5DgDJoQJ#klv_n?R0Jb_QoTE1F@>dst zmEOh7tPFBEc*X#Zbp*Q|C}|{-;Vd!0d!zw46;6e*`D(;~D{4ptY!2hCp%9KK8Zr&B zvTyHN9ENPNihpqtb2yrqd*4BZathC8BWQWBKMw)~Wk8!myZ zZh{q$v1Ymc@osG~)Kq=2G0!1GK?yh#Bv!#O&Mya+_nIOD*j65iUtBCh$h@=grUC== zCdkI`%|M8a6n7+*%j4Eas@Nl9)rtxnk=5OaN?+xrn0-1Ab-D#x#kN5NsKS3~g zcE>TT>h-Mpxwi7Cta)E#W@nC{dwch1u8UQ)^*rPdlVhSDj9K8m1y(f;EQ&wfX?}cN zozSr+84$iLDJhA%{cPVWd9TASj(r09HC$X1{IsI>m6sY%U3(&}{02r7XXo55Ck7WP zg2s(gQQQ8jYW6~w^+dH&v#rbb^(fu0Yb7HGuKCw5-0FV4Zgn8MRy^C#UN`3H>j{#F zGvT)Y7O2gg?%nOJE;diP8$cn=CR_!JwB^fJt~JpoTb4HZ3X_W`eOpm-d5#&&1h;e| zaI=p^Y`D_2Z^2E?O)&wAoL^j#G=(~K?|^^`S{#mr^4EmV(+@cCVbnDZua-t?wbIyuM z;p@oq38EyC{kasu=D+2TXNcIEwC13imlHN)7Y+v8K_8|m3~u4L7|}t}fq76Zwwf6_ zPQ{m_SM%E;YG;^pW{$)BT`A1Oi8B*y9pSu3KH{&->c!}pJSz$FhpS)c$$ClFW2D`Q zv`vbEq!}s;nx0w;iw%{e zk@L=tK9CM{liMM(~c1o1?zlDY#%972YN4uT<->lZuP1Lby<{yHKag>_!jQ zU#9j|Traggg7_NP>M%EPv0(GX*8+LiDJzA?!!{URz(1=%e8G#69B%vs%<$#4WS%88 z%~s+1NFUm1XW=QnVua>fX^j!xv-_h8bWU%Uvil7KF~hMO%Z)e8y{qH&8|*NBYnn0L znQ<$Vb*-u_%$VBrdhr&y4K6(5x-k%I?x~f{^e*f=`Zg`XP5WsCmsDZxV}3pCZghQ( zelJpAT|09!4`K`1?frI*xXVx{Wa{M+!2(qxF6)y6w&YYPLR}>2EuWuf)nn+lRO}*% z*&buof5(Zlpukf4jQO9et8hpmO+^vu7_4g5Jrx3CH#8~DcJnBKWHXfX9NPZ!!9txe z&(JWkBVrl7lhfgFkRda7w2T| z*WcglJiX-*0V`EvP-hcnuxnm@LObHUd3w#ZP_o2$gNsQ0-Lw4 zyc$V0e&{K0p@NZvoQUpIvv{xsb;`xm9?_hkECeTC~-(QVf5V5 zOpgQ~!s-WCd7I>~8s>GUjaEU^LSR z7V0h9&Q%Yo2)(2e3Ux9z{QEVE%w@#SSsW@wKfP*9|M80p@MRa^2^%^zfKvlGe5aAr;f9i};;|1O zT7%S|Ewhg1n5Qz}OhtS5foUxS0j@++2v9H0(d&{s74%)?LmQ;#NE-72=4%a=u2h!@ zPJq2IOB%kO$UU1P->)xBNwy<@RLyjanRnpQ*}6#=67r%E{c*Z+j9<)Xy7_U0?sp zLuAY};69gz6U-K`*2n^dV_p&?wmv>Zs-Ij7xkqw0Y^B(d2o1sGZfS3b~gYui%{IwJJ4m2^W3ey2gJi z@jq_Mg1XG%V*}%`W&HFG6&t0hs`Yz5X6$FYq%HQDd}UE(kDB6M)#5%rpS`X6*N(-d zoKEHCJ+a3mHxI$#E2nsROh`WC$d34@)g?&i8nkb$%=P*ocInT%H&q!kF)~}kt`d;C zZgTZzZu^Ot8=}P<_J6tAO$2b0gZ=#5uJkVzt3v2WjP(!f3YT&_rRsvJ;-ay}s;nNf zyWxX+*F{aZ1QYXNx3MHSlSxxHo^>T$S1ofwRT>=l76mdZhig8kR%PGL7+qr~Y)U^! z`?1P|at6_|AZD#jQYLSNm#j&b*831f*R?kgUqc+3UarO>bCAO2@HE6Gqj+F#7DqFR z1L3~Mg9BpMw(92+Mu=yoprCFhbe4I`AI>=k;R3?tpVbiyuyCQ1<-~HF?9po-gdS1!}BExwwlopBgv~qyn~t5 zL8ood$EZe;lR3hL^NG-eT!0L_Wadql>8l zp5w77Nec4h62{5HcQC0Y#je;lZUV6fGZ7Cf!Zm;q&W-bMWH)1iHt%|euWcems>_U& z#!u%fbM_BYOY3?4p6Hv?BFPWcN1zh4=?~j7yIlejWE(_=_oc;04i&F}M)O-cGY76) ze~m^r0#5?+&R>U~O*=4}i%Q46U@2tIqg~#6gtML#L9n_=g2;448EoQFdQ@SGW$sPS zULSMg`h;F#uyOYUW5%|V5(?nt7#2}8?BU!oVCVwT7q7t?Mdp zd2I4(Koe5Sv0(@qloTzvcTrXp=$7bTMMjM|M}Intdb8essjlKtYX-rQAIJww`j4AZ zcfUzU9Qc6DAhjL|bxJPDe!wTNUKh)-w8+$rxf1MI9n=3L`=rS&qp^^;cCU@eRINQk{ZmS{$0P4>+*QowDlLnnifDJ8(W~R2*Hi{)}ID& z{@n6Q3+&}N02ioIm{*%Ns|j^Pl5x}cXJUiB9vRy_deQu0oi1VCX`(AaBfkZ2O3^!T zgZ=#;t*-rYt5?i<7icC^1pxR9HIkemoH+E5Kb*`dP%C~&ou=$lSC13BF?ZX1qRr*t zZRh9mMo}M0Vy|*TySs;HAPiLt#tglP{gzCH)e@x`+?v(bj4@)i2nROqO5bEuYG7cX zo75#!sr!pZosIMkUm2FRM0Sib6fTF*EI54C-~n(^2TCbO-uYJyM=R@ZC_``l=;&IjIz|o`hDNI}iyCJZZL;`d#@%mp}^%ofe zoM(9ufc*ge7gC;3-;yR^m~00G-AIpv>iWgSov@xYS=gtc4eTtW6we zyZXv*D?c%7nM}?%4sXAfOrRNN@2tki*rj-CQxU4CWkFmlF#aI-oJV2)BZh6CwB`Fr z^en#pEXaJF0+s(~{_5wS;idm!IEedy`E&nJc&UE!{OBx1TwgiI_~X2ETyo9ZY*PDx zMAZFm{h05CI|X_VE6uPI*S3y)UF8jKXx_VT)po9{;}j<&%HAf$7%(_b0Eg6?mdW9B z0*)?>>$gMZpAu)jGQS5o@J0ilwH>o1 z&H~PBunndT%di0(yMl}N>llfPT|Lw{U>nA@j)FKEc6eA6e0|Ye}5;3M)MRTcJ z7IpK5iqm2p+-)uU^D1hv$FgHg-dZc|7mV%*>=(bz=(!)0n3`Wa`cm_^_4%M|h!sJ^2RQ?f=4X511w;?Mz!Y$SC4_Wva&l(82A5%CF=580DO`=XP z&->eM!PTu~7~uX)_^fkmtX+ zwhw+OY0Z1?NZXx6$uUQmS41C75()29ZEy+^707^IJK0lHhBsgUX@FWB{RD7>K5l3& z-e=#tTNg(uUHi$jL2sMtPRPQ;*jY6vbiUx8JlY7)o3x9v+85tzS^`_y!07Gry!{is z)*15QODmp_A;+|3L{5~}9T2XAo&w8{*bH*Vi<$M+?lioh@TrRSlC60D^53W$2g0Nd zJtIf3B`igKU6LK=-IXWDNm%*>jh;Z;4QlRq4Z9cBSSM*RXQ>^FO&@Xs+;YzcdC;-S z5XYoeDbHwkc(90|vH$7Ezss^v^`{-gJX!1_NNkszdabT=&3R;+2WBnOyyq9x0(Y0rN zP+9+-T@4bx9R5;TWBnXQj?<>>DL2wYpPd@M$(wBZees9UMC`PhBwD=z#!&7GW$CGo zkS;Q07Amx{PPy(9HHx%qV%U0MTzQFE!ET%@wUu6OC5a%)AGz~e#LV=uKv(4bDHovV z{RAb;E-c2Acqz@#HQ{^=wI;YQtL9@sdX<5h&F|8{yYBNX+|DvW+IM*&8=NoQk@a zyqRPdpS@-W2pJ|D_fE7tPx!j+dHGvm50M)im`LJ^`YepD;fmSvA>i%thkvixKg@~! z#pS37z)q&!#dXrA>~RwI_a+=A$z_<38P4xer6K9BaLVtv<)2vPe;IT9Pch6tf>l7b zBK>cF@9kh)>_p|eq4i9oESIo@SejaqA`5G z_221-0{&T@Jr!~DRgugx*L?mW`a4OR_G*Ppc7@hRF|sp)A{%A*e?ytN1nWmxz#~>vWQi&fbvOg1 zD$k$4xIp>p8+Y>wVz{nwLzFy%;dirpeO{tT&3nFO>BH&SRU;F&$L*Tiv;>w&J^caA zA=gJ47N(6B=}%BpUgjUSceOYZbltX98qH%u&&4x*d9O~xg#Fz6QCp0stQdIZ4(H+4L1 zZWXMLFA1Ar8c=*{%@x4kjbNF8Oc5481e**lWgidH@Y;-wHfHIEG~?FXF%S$D0zxZ4 zK0)y?^=izUhjMdU%2NyIoR5htH`mw9m|o8%{FWpF9ZRN>)_9q zSaNO$S&fSjtL$Og*>-HsPyA~bdA4+X4P=yxF&vS294pC~WuVREL}BQ7uv9fbiX_%` ztN8MG1mrIx%oxXX<2n5x4`gJES@DEW*M^t3K;(-A5?{O8;2iU`xqdzLk<-!ZLEYIr-kfQIkm?5MoeXmdGIVRBPMx%ud^vhjNNu`vrZGj4I+gw`E;Z}`}p=Ran>hJ^2l#HbE;Fwa~>zXfI2~kEhYC&77PY#Sww(ue zh^@-=D+tMC9pr}~7)wQ&KKA@#2A&64A`tX$h^WrXsce5AvGp@*XzJ=|VSriW+KMn}p1+dPp_+CXQ- zrm+lqQErtB#Jx$iMgGZzN>ep0yOZgrzLX0qhGiAO=RXsUgxe~Nycx^IIFpG9tGfdr zm_K|yg!*|J*Y6(7>kLFZ#fj;A(2{P_{36w$2TjT0C-82x{HE zX!RDL$vCsaC5*quA8cx89 z?vJ-^hUsIgFyvbuAoK&qGH!%JP7d8ulfy31q823euu}N0ogjSdL&M(0nN!u$Y1bxA zzo8U7a_!uGt!l(eYx**xGfkcQdat?4?P*kzwG-Ia?WdC2A$7@jkhmYMiqfWa1+(mF zhfwg34ET+rp7&T|>UCSA58MOtJIq}Cm2A{*+`Q!YlD8=7x0c8> z_y2qp|Lv3JpO4}XC`5z79v{Nc^Sg*4p1ho~?msL)V~%Vf-kg(3w_|78aVq@peTB^g zn1Vz680|H24&#I%9U{(WBxg2*^116^d#{0tb#RdX%k;MB2LLOQN!tLi@U>;m#WarW z*~zXgEf|)_5YEJ`SORo3wSiy#4|uVU@myyMH)K4Sh8aXG0QOFD2ZTz6M8;yW*fBM# zI+;nIW=~*F|JrZRRiw?BkBVPmJ}ua4(3>V|AOY`l&9JdBpf}lYLy?b;8As~NFBn~fb%M|DV|JYVsyubN<{)QiD~&QpCVn}fY#_88sU`E6 zh*0aZ&_<8=vG1^7uN5PgGO8c7Z8aQa?p{IoKm6CZsr$?>^1(4S8F^G4!_i+{XMS<< zI*fR31(d=%u?#0H32(WX4=P7w%)74lq9txCiMG3SDvfghljmLqctHT8EuQus9q4M5AUy(I7Tout1&`40&ueYK zR?hkcfnH5zFMA|nY(!7>IlUcr$M+3>#ZYIu!Yc5r7qWVcMM^t+c@YEfLK3G^{$dc3 zB_c<6+?zXy6M~f}S>R4u(spK}+k@cA59-&_;;}ZcO3p6#z`^G**RHS{At83bBys1g z9eQI}E64T3`Us_p*+8UPV26h5>}rN+m1Oi3vkghnv&(=`_a$RzEvs}uTK6;Xmy}Bv zh&#~IrlpyrwIk}VwFkwIc%KoSu+N}^!)z>d^2?*kdrX=9!&M25yBqUwUT}{UK2vKQT`f#xh6^BIq$;^#du#37qfUC|EW)@snjKM_hW>PhZfu5a}ZpUUvbmS$6YD_4Rj? z;xn=bK~T&YbpmHyRYmQ{8>}z#-U=G=<%pFOf(AV5xh?^o%af0X3C* z2SwHt#5XR#bI6ko8d?QTZLp93y&zaaUcjU<#9j6vO}jeNKhYjdnQj>$a@RhRy@Ps; z6zQ{Ry;D0ht$xoQLGFi5xX<@uC*S&D3I6jGo;+gYqUR#Y-qTGRC+MKIa>cM_M&fPg z*g?66&Yl|ww@YyiR$et5YVdP;$-vW3_AIfSg7N`fW=w@}O;g)NjpS|Bi zF7#vY$^bEFn=M3=$^2VOT~;47%K; zVS8$cXR5m4ZcF<@;bHm6=~IGYi?U=xn2X5GRU3@c;_jKoM@z-p>G%= zxv{E;$eZU+j+S*c`Kh#Dd?)<+VG80r`{^2Sd(c~y+$266Di}%_K5)`9(zLu_I6wWP z@{Vhr&c)HGNv&69%-nx-zEU-H9bbOc_^?2VoA5$5`9l!xoZEDNegcIe|7hge<%sA{ zB!Nrkfs)mU7s-t;myCGDzxu1wxv9iwNV&4UDnrV$I2>T!j^>a|Pb)J`N!v@2(s@Oy z#e`$2horuDKfT*B)fRJNsqg9nMN9?%)$K8cLK>5PbZcI}Z|qE07G~>}1Seph-k}AWWH;{%8?}+2?7@83Sd)mq%c=tDw0C;0996ga6D8EXRrg0DGB=VEch!VvkFH z%Aa3J2mNl2{^EKG*}8f5pRJw#KhWCgzuCzG6p(WL`9~Z}L*dJnAd)10xmyiN&zAA!TPr4U9&Z!R5Xrk zaH+__wS>e&u=N?bdr#xcd!tQK`ADXoar)I#b_iO6w(Q(`B^ zcg}+a9^>x|6B@30n}t2>->%4IrO;+REpcQd^U^!)gEi z6{=Ob7fv*Ogbvsb%*d+Apq*soZ^6k@mpRHa}4 zB-z}o(bl2XDR

@@ -26,8 +36,10 @@ {% endblock %} From 7e52b6d8bb70beaf33a0db0da3d1e431a23c7466 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 24 Mar 2023 23:14:34 +0100 Subject: [PATCH 102/172] MultiServer: if there is a hint cost, don't make it 0 (#1581) --- MultiServer.py | 2 +- kvui.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 6fd06fece8..acc68e1682 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -597,7 +597,7 @@ class Context: def get_hint_cost(self, slot): if self.hint_cost: - return max(0, int(self.hint_cost * 0.01 * len(self.locations[slot]))) + return max(1, int(self.hint_cost * 0.01 * len(self.locations[slot]))) return 0 def recheck_hints(self, team: typing.Optional[int] = None, slot: typing.Optional[int] = None): diff --git a/kvui.py b/kvui.py index a1c6959d6b..effade0dd2 100644 --- a/kvui.py +++ b/kvui.py @@ -148,9 +148,11 @@ class ServerLabel(HovererableLabel): for permission_name, permission_data in ctx.permissions.items(): text += f"\n {permission_name}: {permission_data}" if ctx.hint_cost is not None and ctx.total_locations: + min_cost = int(ctx.server_version >= (0, 3, 9)) text += f"\nA new !hint costs {ctx.hint_cost}% of checks made. " \ - f"For you this means every {max(0, int(ctx.hint_cost * 0.01 * ctx.total_locations))} " \ - "location checks." + f"For you this means every " \ + f"{max(min_cost, int(ctx.hint_cost * 0.01 * ctx.total_locations))}" \ + f" location checks." elif ctx.hint_cost == 0: text += "\n!hint is free to use." From 0386d9f6d2987cd6e277a5570a81cb00b99b113b Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Fri, 24 Mar 2023 19:22:47 -0400 Subject: [PATCH 103/172] Fix SA2B Option Display Name (#1591) --- worlds/sa2b/Options.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/sa2b/Options.py b/worlds/sa2b/Options.py index 0a9f2d1622..f4dfd833c4 100644 --- a/worlds/sa2b/Options.py +++ b/worlds/sa2b/Options.py @@ -486,9 +486,9 @@ class RingLoss(Choice): How taking damage is handled Classic: You lose all of your rings when hit Modern: You lose 20 rings when hit - OHKO: You die immediately when hit (NOTE: Some Hard Logic tricks require damage boosts!) + OHKO: You die immediately when hit (NOTE: Some Hard Logic tricks may require damage boosts!) """ - display_name = "SADX Music" + display_name = "Ring Loss" option_classic = 0 option_modern = 1 option_ohko = 2 From 77b2ed54a6ccacb9c64dc7cf12da5a120e3d2a98 Mon Sep 17 00:00:00 2001 From: zig-for Date: Fri, 24 Mar 2023 16:23:42 -0700 Subject: [PATCH 104/172] LADX: Fix D6 keylogic (#1585) * fix keylogic for d6 * markup required keys for keylogic * add test * Update __init__.py --- worlds/ladx/LADXR/logic/dungeon6.py | 4 ++-- worlds/ladx/test/TestDungeonLogic.py | 35 ++++++++++++++++++++++++++++ worlds/ladx/test/__init__.py | 4 ++++ 3 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 worlds/ladx/test/TestDungeonLogic.py create mode 100644 worlds/ladx/test/__init__.py diff --git a/worlds/ladx/LADXR/logic/dungeon6.py b/worlds/ladx/LADXR/logic/dungeon6.py index 7e51349e3a..d67138b334 100644 --- a/worlds/ladx/LADXR/logic/dungeon6.py +++ b/worlds/ladx/LADXR/logic/dungeon6.py @@ -31,8 +31,8 @@ class Dungeon6: lower_right_owl = Location(dungeon=6).add(OwlStatue(0x1D7)).connect(lower_right_side, AND(POWER_BRACELET, STONE_BEAK6)) center_1 = Location(dungeon=6).add(DroppedKey(0x1C3)).connect(miniboss, AND(COUNT(POWER_BRACELET, 2), FEATHER)) # tile room key drop - center_2_and_upper_right_side = Location(dungeon=6).add(DungeonChest(0x1B1)).connect(center_1, KEY6) # top right chest horseheads - boss_key = Location(dungeon=6).add(DungeonChest(0x1B6)).connect(center_2_and_upper_right_side, AND(KEY6, HOOKSHOT)) + center_2_and_upper_right_side = Location(dungeon=6).add(DungeonChest(0x1B1)).connect(center_1, AND(KEY6, FOUND(KEY6, 2))) # top right chest horseheads + boss_key = Location(dungeon=6).add(DungeonChest(0x1B6)).connect(center_2_and_upper_right_side, AND(AND(KEY6, FOUND(KEY6, 3), HOOKSHOT))) if options.owlstatues == "both" or options.owlstatues == "dungeon": Location(dungeon=6).add(OwlStatue(0x1B6)).connect(boss_key, STONE_BEAK6) diff --git a/worlds/ladx/test/TestDungeonLogic.py b/worlds/ladx/test/TestDungeonLogic.py new file mode 100644 index 0000000000..b9b9672b9b --- /dev/null +++ b/worlds/ladx/test/TestDungeonLogic.py @@ -0,0 +1,35 @@ +from . import LADXTestBase + +from ..Items import ItemName + +class TestD6(LADXTestBase): + # Force keys into pool for testing + options = { + "shuffle_small_keys": "any_world" + } + + def test_keylogic(self): + keys = self.get_items_by_name(ItemName.KEY6) + self.collect_by_name([ItemName.FACE_KEY, ItemName.HOOKSHOT, ItemName.POWER_BRACELET, ItemName.BOMB, ItemName.FEATHER, ItemName.FLIPPERS]) + # Can reach an un-keylocked item in the dungeon + self.assertTrue(self.can_reach_location("L2 Bracelet Chest (Face Shrine)")) + + # For each location, add a key and check that the right thing unlocks + location_1 = "Tile Room Key (Face Shrine)" + location_2 = "Top Right Horse Heads Chest (Face Shrine)" + location_3 = "Pot Locked Chest (Face Shrine)" + self.assertFalse(self.can_reach_location(location_1)) + self.assertFalse(self.can_reach_location(location_2)) + self.assertFalse(self.can_reach_location(location_3)) + self.collect(keys[0]) + self.assertTrue(self.can_reach_location(location_1)) + self.assertFalse(self.can_reach_location(location_2)) + self.assertFalse(self.can_reach_location(location_3)) + self.collect(keys[1]) + self.assertTrue(self.can_reach_location(location_1)) + self.assertTrue(self.can_reach_location(location_2)) + self.assertFalse(self.can_reach_location(location_3)) + self.collect(keys[2]) + self.assertTrue(self.can_reach_location(location_1)) + self.assertTrue(self.can_reach_location(location_2)) + self.assertTrue(self.can_reach_location(location_3)) diff --git a/worlds/ladx/test/__init__.py b/worlds/ladx/test/__init__.py new file mode 100644 index 0000000000..0e616ac557 --- /dev/null +++ b/worlds/ladx/test/__init__.py @@ -0,0 +1,4 @@ +from test.TestBase import WorldTestBase +from ..Common import LINKS_AWAKENING +class LADXTestBase(WorldTestBase): + game = LINKS_AWAKENING From 0ed3865c3045d7046d73510c1d4383bf432bcb1e Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Fri, 24 Mar 2023 18:55:24 -0500 Subject: [PATCH 105/172] The Messenger: Fix a missing location rule and missing known issue (#1586) * fix missing rule * document a missing known issue * fix a break when shuffle seals is off * test the thing i just fixed * invert the if so it's a bit faster --- worlds/messenger/Rules.py | 17 +++++++---------- worlds/messenger/docs/en_The Messenger.md | 1 + worlds/messenger/test/TestLogic.py | 1 + 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/worlds/messenger/Rules.py b/worlds/messenger/Rules.py index 24e0354467..53ea90fe29 100644 --- a/worlds/messenger/Rules.py +++ b/worlds/messenger/Rules.py @@ -134,22 +134,17 @@ class MessengerHardRules(MessengerRules): self.location_rules.update({ "Howling Grotto Seal - Windy Saws and Balls": self.true, "Glacial Peak Seal - Projectile Spike Pit": self.true, + "Claustro": self.has_wingsuit, }) self.extra_rules = { - "Climbing Claws": self.has_dart, - "Astral Seed": self.has_dart, - "Candle": self.has_dart, - "Key of Strength": lambda state: state.has("Power Thistle", self.player) or - self.has_dart(state) or - self.has_windmill(state), + "Key of Strength": lambda state: self.has_dart(state) or self.has_windmill(state), "Key of Symbiosis": self.has_windmill, "Autumn Hills Seal - Spike Ball Darts": lambda state: (self.has_dart(state) and self.has_windmill(state)) or self.has_wingsuit(state), "Glacial Peak Seal - Glacial Air Swag": self.has_windmill, - "Underworld Seal - Fireball Wave": lambda state: self.has_wingsuit(state) - or state.has_all({"Ninja Tabi", "Windmill Shuriken"}, - self.player), + "Underworld Seal - Fireball Wave": lambda state: state.has_all({"Ninja Tabi", "Windmill Shuriken"}, + self.player), } def has_windmill(self, state: CollectionState) -> bool: @@ -158,6 +153,8 @@ class MessengerHardRules(MessengerRules): def set_messenger_rules(self) -> None: super().set_messenger_rules() for loc, rule in self.extra_rules.items(): + if not self.world.multiworld.shuffle_seals[self.player] and "Seal" in loc: + continue add_rule(self.world.multiworld.get_location(loc, self.player), rule, "or") @@ -196,7 +193,7 @@ class MessengerOOBRules(MessengerRules): self.location_rules = { "Claustro": self.has_wingsuit, - "Key of Strength": self.has_wingsuit, + "Key of Strength": lambda state: self.has_vertical(state) or state.has("Power Thistle", self.player), "Key of Love": lambda state: state.has_all({"Sun Crest", "Moon Crest"}, self.player), "Pyro": self.has_tabi, "Key of Chaos": self.has_tabi, diff --git a/worlds/messenger/docs/en_The Messenger.md b/worlds/messenger/docs/en_The Messenger.md index 16faa97cd9..c40668457e 100644 --- a/worlds/messenger/docs/en_The Messenger.md +++ b/worlds/messenger/docs/en_The Messenger.md @@ -66,6 +66,7 @@ for it. The groups you can use for The Messenger are: * Sometimes upon teleporting back to HQ, Ninja will run left and enter a different portal than the one entered by the player. * Text entry menus don't accept controller input +* Opening the shop chest in power seal hunt mode from the tower of time HQ will soft lock the game. ## What do I do if I have a problem? diff --git a/worlds/messenger/test/TestLogic.py b/worlds/messenger/test/TestLogic.py index f12f3bda4e..4f9e1ecc36 100644 --- a/worlds/messenger/test/TestLogic.py +++ b/worlds/messenger/test/TestLogic.py @@ -79,6 +79,7 @@ class HardLogicTest(MessengerTestBase): class ChallengingLogicTest(MessengerTestBase): options = { + "shuffle_seals": "false", "logic_level": "challenging" } From 384577e421c2e61cce26aceb199099bf13731b60 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Sat, 25 Mar 2023 14:30:38 -0400 Subject: [PATCH 106/172] SM: cx_freeze fix (#1584) --- worlds/sm/Regions.py | 4 +- worlds/sm/Rules.py | 7 +- worlds/sm/__init__.py | 23 +- worlds/sm/variaRandomizer/LICENSE | 695 +----------------- worlds/sm/variaRandomizer/__init__.py | 3 - worlds/sm/variaRandomizer/graph/graph.py | 10 +- .../sm/variaRandomizer/graph/graph_utils.py | 12 +- worlds/sm/variaRandomizer/graph/location.py | 2 +- .../graph/vanilla/graph_access.py | 12 +- .../graph/vanilla/graph_helpers.py | 12 +- .../graph/vanilla/graph_locations.py | 10 +- worlds/sm/variaRandomizer/logic/helpers.py | 10 +- worlds/sm/variaRandomizer/logic/logic.py | 16 +- .../sm/variaRandomizer/logic/smboolmanager.py | 12 +- .../sm/variaRandomizer/patches/patchaccess.py | 10 +- worlds/sm/variaRandomizer/rando/Choice.py | 9 +- worlds/sm/variaRandomizer/rando/Filler.py | 22 +- .../sm/variaRandomizer/rando/GraphBuilder.py | 12 +- .../variaRandomizer/rando/ItemLocContainer.py | 10 +- worlds/sm/variaRandomizer/rando/Items.py | 7 +- worlds/sm/variaRandomizer/rando/RandoExec.py | 25 +- .../sm/variaRandomizer/rando/RandoServices.py | 11 +- .../sm/variaRandomizer/rando/RandoSettings.py | 6 +- worlds/sm/variaRandomizer/rando/RandoSetup.py | 23 +- .../sm/variaRandomizer/rando/Restrictions.py | 10 +- worlds/sm/variaRandomizer/randomizer.py | 27 +- worlds/sm/variaRandomizer/rom/ips.py | 2 +- worlds/sm/variaRandomizer/rom/rom.py | 2 +- worlds/sm/variaRandomizer/rom/rom_patches.py | 2 +- worlds/sm/variaRandomizer/rom/rompatcher.py | 21 +- .../sm/variaRandomizer/utils/doorsmanager.py | 9 +- worlds/sm/variaRandomizer/utils/parameters.py | 2 +- worlds/sm/variaRandomizer/utils/utils.py | 8 +- 33 files changed, 195 insertions(+), 851 deletions(-) diff --git a/worlds/sm/Regions.py b/worlds/sm/Regions.py index 966366e62f..f8ef908889 100644 --- a/worlds/sm/Regions.py +++ b/worlds/sm/Regions.py @@ -1,8 +1,8 @@ def create_regions(self, world, player: int): from . import create_region from BaseClasses import Entrance - from logic.logic import Logic - from graph.vanilla.graph_locations import locationsDict + from worlds.sm.variaRandomizer.logic.logic import Logic + from worlds.sm.variaRandomizer.graph.vanilla.graph_locations import locationsDict regions = [] for accessPoint in Logic.accessPoints: diff --git a/worlds/sm/Rules.py b/worlds/sm/Rules.py index 54468a40f2..bce9247342 100644 --- a/worlds/sm/Rules.py +++ b/worlds/sm/Rules.py @@ -1,10 +1,7 @@ from ..generic.Rules import set_rule, add_rule -from graph.vanilla.graph_locations import locationsDict -from graph.graph_utils import vanillaTransitions, getAccessPoint -from logic.logic import Logic -from rom.rom_patches import RomPatches -from utils.doorsmanager import DoorsManager +from worlds.sm.variaRandomizer.graph.vanilla.graph_locations import locationsDict +from worlds.sm.variaRandomizer.logic.logic import Logic def evalSMBool(smbool, maxDiff): return smbool.bool == True and smbool.difficulty <= maxDiff diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 43edf35c56..255551c267 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -7,8 +7,6 @@ import threading import base64 from typing import Any, Dict, Iterable, List, Set, TextIO, TypedDict -from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils - logger = logging.getLogger("Super Metroid") from .Regions import create_regions @@ -21,16 +19,17 @@ import Utils from BaseClasses import Region, Entrance, Location, MultiWorld, Item, ItemClassification, CollectionState, Tutorial from ..AutoWorld import World, AutoLogicRegister, WebWorld -from logic.smboolmanager import SMBoolManager -from graph.vanilla.graph_locations import locationsDict -from graph.graph_utils import getAccessPoint -from rando.ItemLocContainer import ItemLocation -from rando.Items import ItemManager -from utils.parameters import * -from logic.logic import Logic -from randomizer import VariaRandomizer -from utils.doorsmanager import DoorsManager -from rom.rom_patches import RomPatches +from worlds.sm.variaRandomizer.logic.smboolmanager import SMBoolManager +from worlds.sm.variaRandomizer.graph.vanilla.graph_locations import locationsDict +from worlds.sm.variaRandomizer.graph.graph_utils import getAccessPoint +from worlds.sm.variaRandomizer.rando.ItemLocContainer import ItemLocation +from worlds.sm.variaRandomizer.rando.Items import ItemManager +from worlds.sm.variaRandomizer.utils.parameters import * +from worlds.sm.variaRandomizer.logic.logic import Logic +from worlds.sm.variaRandomizer.randomizer import VariaRandomizer +from worlds.sm.variaRandomizer.utils.doorsmanager import DoorsManager +from worlds.sm.variaRandomizer.rom.rom_patches import RomPatches +from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils class SMCollectionState(metaclass=AutoLogicRegister): diff --git a/worlds/sm/variaRandomizer/LICENSE b/worlds/sm/variaRandomizer/LICENSE index 9cecc1d466..421c53a33b 100644 --- a/worlds/sm/variaRandomizer/LICENSE +++ b/worlds/sm/variaRandomizer/LICENSE @@ -1,674 +1,21 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - {one line to give the program's name and a brief idea of what it does.} - Copyright (C) {year} {name of author} - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - {project} Copyright (C) {year} {fullname} - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. +MIT License + +Copyright (c) 2022 dude & flo and others + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/worlds/sm/variaRandomizer/__init__.py b/worlds/sm/variaRandomizer/__init__.py index 6aed1dfd5a..e69de29bb2 100644 --- a/worlds/sm/variaRandomizer/__init__.py +++ b/worlds/sm/variaRandomizer/__init__.py @@ -1,3 +0,0 @@ -import sys -import os -sys.path.append(os.path.dirname(os.path.abspath(__file__))) \ No newline at end of file diff --git a/worlds/sm/variaRandomizer/graph/graph.py b/worlds/sm/variaRandomizer/graph/graph.py index bcbf138123..bf9af48dc9 100644 --- a/worlds/sm/variaRandomizer/graph/graph.py +++ b/worlds/sm/variaRandomizer/graph/graph.py @@ -1,9 +1,9 @@ import copy, logging from operator import attrgetter -import utils.log -from logic.smbool import SMBool, smboolFalse -from utils.parameters import infinity -from logic.helpers import Bosses +from worlds.sm.variaRandomizer.utils import log +from worlds.sm.variaRandomizer.logic.smbool import SMBool, smboolFalse +from worlds.sm.variaRandomizer.utils.parameters import infinity +from worlds.sm.variaRandomizer.logic.helpers import Bosses class Path(object): __slots__ = ( 'path', 'pdiff', 'distance' ) @@ -106,7 +106,7 @@ class AccessGraph(object): 'availAccessPoints' ) def __init__(self, accessPointList, transitions, dotFile=None): - self.log = utils.log.get('Graph') + self.log = log.get('Graph') self.accessPoints = {} self.InterAreaTransitions = [] self.EscapeAttributes = { diff --git a/worlds/sm/variaRandomizer/graph/graph_utils.py b/worlds/sm/variaRandomizer/graph/graph_utils.py index 75d494a65b..e147da5e57 100644 --- a/worlds/sm/variaRandomizer/graph/graph_utils.py +++ b/worlds/sm/variaRandomizer/graph/graph_utils.py @@ -1,10 +1,10 @@ import copy import random -from logic.logic import Logic -from utils.parameters import Knows -from graph.location import locationsDict -from rom.rom import snes_to_pc -import utils.log +from worlds.sm.variaRandomizer.logic.logic import Logic +from worlds.sm.variaRandomizer.utils.parameters import Knows +from worlds.sm.variaRandomizer.graph.location import locationsDict +from worlds.sm.variaRandomizer.rom.rom import snes_to_pc +from worlds.sm.variaRandomizer.utils import log # order expected by ROM patches graphAreas = [ @@ -89,7 +89,7 @@ def getAccessPoint(apName, apList=None): return next(ap for ap in apList if ap.Name == apName) class GraphUtils: - log = utils.log.get('GraphUtils') + log = log.get('GraphUtils') def getStartAccessPointNames(): return [ap.Name for ap in Logic.accessPoints if ap.Start is not None] diff --git a/worlds/sm/variaRandomizer/graph/location.py b/worlds/sm/variaRandomizer/graph/location.py index f60158c1db..92eb1ccbca 100644 --- a/worlds/sm/variaRandomizer/graph/location.py +++ b/worlds/sm/variaRandomizer/graph/location.py @@ -1,4 +1,4 @@ -from utils.parameters import infinity +from worlds.sm.variaRandomizer.utils.parameters import infinity import copy class Location: diff --git a/worlds/sm/variaRandomizer/graph/vanilla/graph_access.py b/worlds/sm/variaRandomizer/graph/vanilla/graph_access.py index b74b69026e..279f249e86 100644 --- a/worlds/sm/variaRandomizer/graph/vanilla/graph_access.py +++ b/worlds/sm/variaRandomizer/graph/vanilla/graph_access.py @@ -1,9 +1,9 @@ -from graph.graph import AccessPoint -from utils.parameters import Settings -from rom.rom_patches import RomPatches -from logic.smbool import SMBool -from logic.helpers import Bosses -from logic.cache import Cache +from worlds.sm.variaRandomizer.graph.graph import AccessPoint +from worlds.sm.variaRandomizer.utils.parameters import Settings +from worlds.sm.variaRandomizer.rom.rom_patches import RomPatches +from worlds.sm.variaRandomizer.logic.smbool import SMBool +from worlds.sm.variaRandomizer.logic.helpers import Bosses +from worlds.sm.variaRandomizer.logic.cache import Cache # all access points and traverse functions accessPoints = [ diff --git a/worlds/sm/variaRandomizer/graph/vanilla/graph_helpers.py b/worlds/sm/variaRandomizer/graph/vanilla/graph_helpers.py index f189a47606..41ffe51192 100644 --- a/worlds/sm/variaRandomizer/graph/vanilla/graph_helpers.py +++ b/worlds/sm/variaRandomizer/graph/vanilla/graph_helpers.py @@ -1,11 +1,11 @@ from math import ceil -from logic.smbool import SMBool -from logic.helpers import Helpers, Bosses -from logic.cache import Cache -from rom.rom_patches import RomPatches -from graph.graph_utils import getAccessPoint -from utils.parameters import Settings +from worlds.sm.variaRandomizer.logic.smbool import SMBool +from worlds.sm.variaRandomizer.logic.helpers import Helpers, Bosses +from worlds.sm.variaRandomizer.logic.cache import Cache +from worlds.sm.variaRandomizer.rom.rom_patches import RomPatches +from worlds.sm.variaRandomizer.graph.graph_utils import getAccessPoint +from worlds.sm.variaRandomizer.utils.parameters import Settings class HelpersGraph(Helpers): def __init__(self, smbm): diff --git a/worlds/sm/variaRandomizer/graph/vanilla/graph_locations.py b/worlds/sm/variaRandomizer/graph/vanilla/graph_locations.py index 671368e831..62eaf3c0fe 100644 --- a/worlds/sm/variaRandomizer/graph/vanilla/graph_locations.py +++ b/worlds/sm/variaRandomizer/graph/vanilla/graph_locations.py @@ -1,8 +1,8 @@ -from logic.helpers import Bosses -from utils.parameters import Settings -from rom.rom_patches import RomPatches -from logic.smbool import SMBool -from graph.location import locationsDict +from worlds.sm.variaRandomizer.logic.helpers import Bosses +from worlds.sm.variaRandomizer.utils.parameters import Settings +from worlds.sm.variaRandomizer.rom.rom_patches import RomPatches +from worlds.sm.variaRandomizer.logic.smbool import SMBool +from worlds.sm.variaRandomizer.graph.location import locationsDict locationsDict["Energy Tank, Gauntlet"].AccessFrom = { 'Landing Site': lambda sm: SMBool(True) diff --git a/worlds/sm/variaRandomizer/logic/helpers.py b/worlds/sm/variaRandomizer/logic/helpers.py index 4df4665770..3f8720d84f 100644 --- a/worlds/sm/variaRandomizer/logic/helpers.py +++ b/worlds/sm/variaRandomizer/logic/helpers.py @@ -1,11 +1,11 @@ import math -from logic.cache import Cache -from logic.smbool import SMBool, smboolFalse -from utils.parameters import Settings, easy, medium, diff2text -from rom.rom_patches import RomPatches -from utils.utils import normalizeRounding +from worlds.sm.variaRandomizer.logic.cache import Cache +from worlds.sm.variaRandomizer.logic.smbool import SMBool, smboolFalse +from worlds.sm.variaRandomizer.utils.parameters import Settings, easy, medium, diff2text +from worlds.sm.variaRandomizer.rom.rom_patches import RomPatches +from worlds.sm.variaRandomizer.utils.utils import normalizeRounding class Helpers(object): diff --git a/worlds/sm/variaRandomizer/logic/logic.py b/worlds/sm/variaRandomizer/logic/logic.py index 5d47932b78..6ce20406b9 100644 --- a/worlds/sm/variaRandomizer/logic/logic.py +++ b/worlds/sm/variaRandomizer/logic/logic.py @@ -4,20 +4,20 @@ class Logic(object): @staticmethod def factory(implementation): if implementation == 'vanilla': - from graph.vanilla.graph_helpers import HelpersGraph - from graph.vanilla.graph_access import accessPoints - from graph.vanilla.graph_locations import locations - from graph.vanilla.graph_locations import LocationsHelper + from worlds.sm.variaRandomizer.graph.vanilla.graph_helpers import HelpersGraph + from worlds.sm.variaRandomizer.graph.vanilla.graph_access import accessPoints + from worlds.sm.variaRandomizer.graph.vanilla.graph_locations import locations + from worlds.sm.variaRandomizer.graph.vanilla.graph_locations import LocationsHelper Logic.locations = locations Logic.accessPoints = accessPoints Logic.HelpersGraph = HelpersGraph Logic.patches = implementation Logic.LocationsHelper = LocationsHelper elif implementation == 'rotation': - from graph.rotation.graph_helpers import HelpersGraph - from graph.rotation.graph_access import accessPoints - from graph.rotation.graph_locations import locations - from graph.rotation.graph_locations import LocationsHelper + from worlds.sm.variaRandomizer.graph.rotation.graph_helpers import HelpersGraph + from worlds.sm.variaRandomizer.graph.rotation.graph_access import accessPoints + from worlds.sm.variaRandomizer.graph.rotation.graph_locations import locations + from worlds.sm.variaRandomizer.graph.rotation.graph_locations import LocationsHelper Logic.locations = locations Logic.accessPoints = accessPoints Logic.HelpersGraph = HelpersGraph diff --git a/worlds/sm/variaRandomizer/logic/smboolmanager.py b/worlds/sm/variaRandomizer/logic/smboolmanager.py index c09455fdaa..a351e163fa 100644 --- a/worlds/sm/variaRandomizer/logic/smboolmanager.py +++ b/worlds/sm/variaRandomizer/logic/smboolmanager.py @@ -1,11 +1,11 @@ # object to handle the smbools and optimize them -from logic.cache import Cache -from logic.smbool import SMBool, smboolFalse -from logic.helpers import Bosses -from logic.logic import Logic -from utils.doorsmanager import DoorsManager -from utils.parameters import Knows, isKnows +from worlds.sm.variaRandomizer.logic.cache import Cache +from worlds.sm.variaRandomizer.logic.smbool import SMBool, smboolFalse +from worlds.sm.variaRandomizer.logic.helpers import Bosses +from worlds.sm.variaRandomizer.logic.logic import Logic +from worlds.sm.variaRandomizer.utils.doorsmanager import DoorsManager +from worlds.sm.variaRandomizer.utils.parameters import Knows, isKnows import logging import sys diff --git a/worlds/sm/variaRandomizer/patches/patchaccess.py b/worlds/sm/variaRandomizer/patches/patchaccess.py index bce2d48658..9ed8317294 100644 --- a/worlds/sm/variaRandomizer/patches/patchaccess.py +++ b/worlds/sm/variaRandomizer/patches/patchaccess.py @@ -1,7 +1,7 @@ import os, importlib -from logic.logic import Logic -from patches.common.patches import patches, additional_PLMs -from utils.parameters import appDir +from worlds.sm.variaRandomizer.logic.logic import Logic +from worlds.sm.variaRandomizer.patches.common.patches import patches, additional_PLMs +from worlds.sm.variaRandomizer.utils.parameters import appDir class PatchAccess(object): def __init__(self): @@ -16,12 +16,12 @@ class PatchAccess(object): # load dict patches self.dictPatches = patches - logicPatches = importlib.import_module("patches.{}.patches".format(Logic.patches)).patches + logicPatches = importlib.import_module("worlds.sm.variaRandomizer.patches.{}.patches".format(Logic.patches)).patches self.dictPatches.update(logicPatches) # load additional PLMs self.additionalPLMs = additional_PLMs - logicPLMs = importlib.import_module("patches.{}.patches".format(Logic.patches)).additional_PLMs + logicPLMs = importlib.import_module("worlds.sm.variaRandomizer.patches.{}.patches".format(Logic.patches)).additional_PLMs self.additionalPLMs.update(logicPLMs) def getPatchPath(self, patch): diff --git a/worlds/sm/variaRandomizer/rando/Choice.py b/worlds/sm/variaRandomizer/rando/Choice.py index 3cb5667755..e200448b43 100644 --- a/worlds/sm/variaRandomizer/rando/Choice.py +++ b/worlds/sm/variaRandomizer/rando/Choice.py @@ -1,13 +1,14 @@ -import utils.log, random -from utils.utils import getRangeDict, chooseFromRange -from rando.ItemLocContainer import ItemLocation +import random +from worlds.sm.variaRandomizer.utils import log +from worlds.sm.variaRandomizer.utils.utils import getRangeDict, chooseFromRange +from worlds.sm.variaRandomizer.rando.ItemLocContainer import ItemLocation # helper object to choose item/loc class Choice(object): def __init__(self, restrictions): self.restrictions = restrictions self.settings = restrictions.settings - self.log = utils.log.get("Choice") + self.log = log.get("Choice") # args are return from RandoServices.getPossiblePlacements # return itemLoc dict, or None if no possible choice diff --git a/worlds/sm/variaRandomizer/rando/Filler.py b/worlds/sm/variaRandomizer/rando/Filler.py index 3408aee057..733e7cdbbb 100644 --- a/worlds/sm/variaRandomizer/rando/Filler.py +++ b/worlds/sm/variaRandomizer/rando/Filler.py @@ -1,14 +1,14 @@ -import utils.log, copy, time, random - -from logic.cache import RequestCache -from rando.RandoServices import RandoServices -from rando.Choice import ItemThenLocChoice -from rando.RandoServices import ComebackCheckType -from rando.ItemLocContainer import ItemLocation, getItemLocationsStr -from utils.parameters import infinity -from logic.helpers import diffValue2txt -from graph.graph_utils import GraphUtils +import copy, time, random +from worlds.sm.variaRandomizer.utils import log +from worlds.sm.variaRandomizer.logic.cache import RequestCache +from worlds.sm.variaRandomizer.rando.RandoServices import RandoServices +from worlds.sm.variaRandomizer.rando.Choice import ItemThenLocChoice +from worlds.sm.variaRandomizer.rando.RandoServices import ComebackCheckType +from worlds.sm.variaRandomizer.rando.ItemLocContainer import ItemLocation, getItemLocationsStr +from worlds.sm.variaRandomizer.utils.parameters import infinity +from worlds.sm.variaRandomizer.logic.helpers import diffValue2txt +from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils # base class for fillers. a filler responsibility is to fill a given # ItemLocContainer while a certain condition is fulfilled (usually @@ -25,7 +25,7 @@ class Filler(object): self.endDate = endDate self.baseContainer = emptyContainer self.maxDiff = self.settings.maxDiff - self.log = utils.log.get('Filler') + self.log = log.get('Filler') # reinit algo state def initFiller(self): diff --git a/worlds/sm/variaRandomizer/rando/GraphBuilder.py b/worlds/sm/variaRandomizer/rando/GraphBuilder.py index 3670550e16..6eeb1d865c 100644 --- a/worlds/sm/variaRandomizer/rando/GraphBuilder.py +++ b/worlds/sm/variaRandomizer/rando/GraphBuilder.py @@ -1,9 +1,9 @@ -import utils.log, random, copy - -from graph.graph_utils import GraphUtils, vanillaTransitions, vanillaBossesTransitions, escapeSource, escapeTargets -from logic.logic import Logic -from graph.graph import AccessGraphRando as AccessGraph +import random, copy +from worlds.sm.variaRandomizer.utils import log +from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils, vanillaTransitions, vanillaBossesTransitions, escapeSource, escapeTargets +from worlds.sm.variaRandomizer.logic.logic import Logic +from worlds.sm.variaRandomizer.graph.graph import AccessGraphRando as AccessGraph # creates graph and handles randomized escape class GraphBuilder(object): @@ -13,7 +13,7 @@ class GraphBuilder(object): self.bossRando = graphSettings.bossRando self.escapeRando = graphSettings.escapeRando self.minimizerN = graphSettings.minimizerN - self.log = utils.log.get('GraphBuilder') + self.log = log.get('GraphBuilder') # builds everything but escape transitions def createGraph(self): diff --git a/worlds/sm/variaRandomizer/rando/ItemLocContainer.py b/worlds/sm/variaRandomizer/rando/ItemLocContainer.py index 1ab43355c1..859fe5503f 100644 --- a/worlds/sm/variaRandomizer/rando/ItemLocContainer.py +++ b/worlds/sm/variaRandomizer/rando/ItemLocContainer.py @@ -1,8 +1,8 @@ -import copy, utils.log - -from logic.smbool import SMBool, smboolFalse -from logic.smboolmanager import SMBoolManager +import copy +from worlds.sm.variaRandomizer.utils import log +from worlds.sm.variaRandomizer.logic.smbool import SMBool, smboolFalse +from worlds.sm.variaRandomizer.logic.smboolmanager import SMBoolManager from collections import Counter class ItemLocation(object): @@ -58,7 +58,7 @@ class ItemLocContainer(object): self.itemPool = itemPool self.itemPoolBackup = None self.unrestrictedItems = set() - self.log = utils.log.get('ItemLocContainer') + self.log = log.get('ItemLocContainer') self.checkConsistency() def checkConsistency(self): diff --git a/worlds/sm/variaRandomizer/rando/Items.py b/worlds/sm/variaRandomizer/rando/Items.py index b7dacca40e..6c4d35119e 100644 --- a/worlds/sm/variaRandomizer/rando/Items.py +++ b/worlds/sm/variaRandomizer/rando/Items.py @@ -1,5 +1,6 @@ -from utils.utils import randGaussBounds, getRangeDict, chooseFromRange -import utils.log, logging, copy, random +from worlds.sm.variaRandomizer.utils.utils import randGaussBounds, getRangeDict, chooseFromRange +from worlds.sm.variaRandomizer.utils import log +import logging, copy, random class Item: __slots__ = ( 'Category', 'Class', 'Name', 'Code', 'Type', 'BeamBits', 'ItemBits', 'Id' ) @@ -392,7 +393,7 @@ class ItemPoolGenerator(object): self.maxItems = 105 # 100 item locs and 5 bosses self.maxEnergy = 18 # 14E, 4R self.maxDiff = maxDiff - self.log = utils.log.get('ItemPool') + self.log = log.get('ItemPool') def isUltraSparseNoTanks(self): # if low stuff botwoon is not known there is a hard energy req of one tank, even diff --git a/worlds/sm/variaRandomizer/rando/RandoExec.py b/worlds/sm/variaRandomizer/rando/RandoExec.py index bf440f044c..9ff6fc2d99 100644 --- a/worlds/sm/variaRandomizer/rando/RandoExec.py +++ b/worlds/sm/variaRandomizer/rando/RandoExec.py @@ -1,15 +1,16 @@ -import sys, random, time, utils.log +import sys, random, time -from logic.logic import Logic -from graph.graph_utils import GraphUtils, getAccessPoint -from rando.Restrictions import Restrictions -from rando.RandoServices import RandoServices -from rando.GraphBuilder import GraphBuilder -from rando.RandoSetup import RandoSetup -from rando.Items import ItemManager -from rando.ItemLocContainer import ItemLocation -from utils.vcr import VCR -from utils.doorsmanager import DoorsManager +from worlds.sm.variaRandomizer.utils import log +from worlds.sm.variaRandomizer.logic.logic import Logic +from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils, getAccessPoint +from worlds.sm.variaRandomizer.rando.Restrictions import Restrictions +from worlds.sm.variaRandomizer.rando.RandoServices import RandoServices +from worlds.sm.variaRandomizer.rando.GraphBuilder import GraphBuilder +from worlds.sm.variaRandomizer.rando.RandoSetup import RandoSetup +from worlds.sm.variaRandomizer.rando.Items import ItemManager +from worlds.sm.variaRandomizer.rando.ItemLocContainer import ItemLocation +from worlds.sm.variaRandomizer.utils.vcr import VCR +from worlds.sm.variaRandomizer.utils.doorsmanager import DoorsManager # entry point for rando execution ("randomize" method) class RandoExec(object): @@ -19,7 +20,7 @@ class RandoExec(object): self.vcr = vcr self.randoSettings = randoSettings self.graphSettings = graphSettings - self.log = utils.log.get('RandoExec') + self.log = log.get('RandoExec') self.player = player # processes settings to : diff --git a/worlds/sm/variaRandomizer/rando/RandoServices.py b/worlds/sm/variaRandomizer/rando/RandoServices.py index bcb076d120..6ea86c9e4a 100644 --- a/worlds/sm/variaRandomizer/rando/RandoServices.py +++ b/worlds/sm/variaRandomizer/rando/RandoServices.py @@ -1,9 +1,10 @@ -import utils.log, copy, random, sys, logging +import copy, random, sys, logging, os from enum import Enum, unique -from utils.parameters import infinity -from rando.ItemLocContainer import getLocListStr, getItemListStr, getItemLocStr, ItemLocation -from logic.helpers import Bosses +from worlds.sm.variaRandomizer.utils import log +from worlds.sm.variaRandomizer.utils.parameters import infinity +from worlds.sm.variaRandomizer.rando.ItemLocContainer import getLocListStr, getItemListStr, getItemLocStr, ItemLocation +from worlds.sm.variaRandomizer.logic.helpers import Bosses # used to specify whether we want to come back from locations @unique @@ -23,7 +24,7 @@ class RandoServices(object): self.settings = restrictions.settings self.areaGraph = graph self.cache = cache - self.log = utils.log.get('RandoServices') + self.log = log.get('RandoServices') # collect an item/loc with logic in a container from a given AP # return new AP diff --git a/worlds/sm/variaRandomizer/rando/RandoSettings.py b/worlds/sm/variaRandomizer/rando/RandoSettings.py index a2ce5908ed..030b14fff2 100644 --- a/worlds/sm/variaRandomizer/rando/RandoSettings.py +++ b/worlds/sm/variaRandomizer/rando/RandoSettings.py @@ -1,9 +1,9 @@ import sys, random from collections import defaultdict -from rando.Items import ItemManager -from utils.utils import getRangeDict, chooseFromRange -from rando.ItemLocContainer import ItemLocation +from worlds.sm.variaRandomizer.rando.Items import ItemManager +from worlds.sm.variaRandomizer.utils.utils import getRangeDict, chooseFromRange +from worlds.sm.variaRandomizer.rando.ItemLocContainer import ItemLocation # Holder for settings and a few utility functions related to them # (especially for plando/rando). diff --git a/worlds/sm/variaRandomizer/rando/RandoSetup.py b/worlds/sm/variaRandomizer/rando/RandoSetup.py index 0a73ad1dd0..c82802f8c1 100644 --- a/worlds/sm/variaRandomizer/rando/RandoSetup.py +++ b/worlds/sm/variaRandomizer/rando/RandoSetup.py @@ -1,14 +1,15 @@ -import copy, utils.log, random +import copy, random -from utils.utils import randGaussBounds -from logic.smbool import SMBool, smboolFalse -from logic.smboolmanager import SMBoolManager -from logic.helpers import Bosses -from graph.graph_utils import getAccessPoint, GraphUtils -from rando.Filler import FrontFiller -from rando.ItemLocContainer import ItemLocContainer, getLocListStr, ItemLocation, getItemListStr -from rando.Restrictions import Restrictions -from utils.parameters import infinity +from worlds.sm.variaRandomizer.utils import log +from worlds.sm.variaRandomizer.utils.utils import randGaussBounds +from worlds.sm.variaRandomizer.logic.smbool import SMBool, smboolFalse +from worlds.sm.variaRandomizer.logic.smboolmanager import SMBoolManager +from worlds.sm.variaRandomizer.logic.helpers import Bosses +from worlds.sm.variaRandomizer.graph.graph_utils import getAccessPoint, GraphUtils +from worlds.sm.variaRandomizer.rando.Filler import FrontFiller +from worlds.sm.variaRandomizer.rando.ItemLocContainer import ItemLocContainer, getLocListStr, ItemLocation, getItemListStr +from worlds.sm.variaRandomizer.rando.Restrictions import Restrictions +from worlds.sm.variaRandomizer.utils.parameters import infinity # checks init conditions for the randomizer: processes super fun settings, graph, start location, special restrictions # the entry point is createItemLocContainer @@ -49,7 +50,7 @@ class RandoSetup(object): # we have to use item manager only once, otherwise pool will change self.itemManager.createItemPool(exclude) self.basePool = self.itemManager.getItemPool()[:] - self.log = utils.log.get('RandoSetup') + self.log = log.get('RandoSetup') if len(locations) != len(self.locations): self.log.debug("inaccessible locations :"+getLocListStr([loc for loc in locations if loc not in self.locations])) diff --git a/worlds/sm/variaRandomizer/rando/Restrictions.py b/worlds/sm/variaRandomizer/rando/Restrictions.py index 2f932ecce2..953eb2ef06 100644 --- a/worlds/sm/variaRandomizer/rando/Restrictions.py +++ b/worlds/sm/variaRandomizer/rando/Restrictions.py @@ -1,13 +1,13 @@ -import copy, random, utils.log - -from graph.graph_utils import getAccessPoint -from rando.ItemLocContainer import getLocListStr +import copy, random +from worlds.sm.variaRandomizer.utils import log +from worlds.sm.variaRandomizer.graph.graph_utils import getAccessPoint +from worlds.sm.variaRandomizer.rando.ItemLocContainer import getLocListStr # Holds settings related to item placement restrictions. # canPlaceAtLocation is the main entry point here class Restrictions(object): def __init__(self, settings): - self.log = utils.log.get('Restrictions') + self.log = log.get('Restrictions') self.settings = settings # Item split : Major, Chozo, Full, Scavenger self.split = settings.restrictions['MajorMinor'] diff --git a/worlds/sm/variaRandomizer/randomizer.py b/worlds/sm/variaRandomizer/randomizer.py index 854e76b3db..6a2c33ca4e 100644 --- a/worlds/sm/variaRandomizer/randomizer.py +++ b/worlds/sm/variaRandomizer/randomizer.py @@ -3,19 +3,18 @@ from Utils import output_path import argparse, os.path, json, sys, shutil, random, copy, requests -from rando.RandoSettings import RandoSettings, GraphSettings -from rando.RandoExec import RandoExec -from graph.graph_utils import vanillaTransitions, vanillaBossesTransitions, GraphUtils, getAccessPoint -from utils.parameters import Knows, Controller, easy, medium, hard, harder, hardcore, mania, infinity, text2diff, diff2text, appDir -from rom.rom_patches import RomPatches -from rom.rompatcher import RomPatcher -from utils.utils import PresetLoader, loadRandoPreset, getDefaultMultiValues, getPresetDir -from utils.version import displayedVersion -from logic.smbool import SMBool -from utils.doorsmanager import DoorsManager -from logic.logic import Logic +from worlds.sm.variaRandomizer.rando.RandoSettings import RandoSettings, GraphSettings +from worlds.sm.variaRandomizer.rando.RandoExec import RandoExec +from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils, getAccessPoint +from worlds.sm.variaRandomizer.utils.parameters import Controller, easy, medium, hard, harder, hardcore, mania, infinity, text2diff, appDir +from worlds.sm.variaRandomizer.rom.rom_patches import RomPatches +from worlds.sm.variaRandomizer.rom.rompatcher import RomPatcher +from worlds.sm.variaRandomizer.utils.utils import PresetLoader, loadRandoPreset, getDefaultMultiValues, getPresetDir +from worlds.sm.variaRandomizer.utils.version import displayedVersion +from worlds.sm.variaRandomizer.utils.doorsmanager import DoorsManager +from worlds.sm.variaRandomizer.logic.logic import Logic -import utils.log +from worlds.sm.variaRandomizer.utils import log from worlds.sm.Options import StartLocation # we need to know the logic before doing anything else @@ -304,8 +303,8 @@ class VariaRandomizer: print("plandoRando param requires output param") sys.exit(-1) - utils.log.init(args.debug) - logger = utils.log.get('Rando') + log.init(args.debug) + logger = log.get('Rando') Logic.factory(args.logic) diff --git a/worlds/sm/variaRandomizer/rom/ips.py b/worlds/sm/variaRandomizer/rom/ips.py index dcce44a2cb..34a41e2ecf 100644 --- a/worlds/sm/variaRandomizer/rom/ips.py +++ b/worlds/sm/variaRandomizer/rom/ips.py @@ -1,6 +1,6 @@ import itertools -from utils.utils import range_union +from worlds.sm.variaRandomizer.utils.utils import range_union # adapted from ips-util for python 3.2 (https://pypi.org/project/ips-util/) class IPS_Patch(object): diff --git a/worlds/sm/variaRandomizer/rom/rom.py b/worlds/sm/variaRandomizer/rom/rom.py index 52b1fd0460..7b1cf06ffc 100644 --- a/worlds/sm/variaRandomizer/rom/rom.py +++ b/worlds/sm/variaRandomizer/rom/rom.py @@ -1,6 +1,6 @@ import base64 -from rom.ips import IPS_Patch +from worlds.sm.variaRandomizer.rom.ips import IPS_Patch def pc_to_snes(pcaddress): snesaddress=(((pcaddress<<1)&0x7F0000)|(pcaddress&0x7FFF)|0x8000)|0x800000 diff --git a/worlds/sm/variaRandomizer/rom/rom_patches.py b/worlds/sm/variaRandomizer/rom/rom_patches.py index 9fe68c9f9a..26e8a84e94 100644 --- a/worlds/sm/variaRandomizer/rom/rom_patches.py +++ b/worlds/sm/variaRandomizer/rom/rom_patches.py @@ -1,4 +1,4 @@ -from logic.smbool import SMBool +from worlds.sm.variaRandomizer.logic.smbool import SMBool # layout patches added by randomizers class RomPatches: diff --git a/worlds/sm/variaRandomizer/rom/rompatcher.py b/worlds/sm/variaRandomizer/rom/rompatcher.py index 9dae1cea87..85cd3bf312 100644 --- a/worlds/sm/variaRandomizer/rom/rompatcher.py +++ b/worlds/sm/variaRandomizer/rom/rompatcher.py @@ -1,14 +1,13 @@ import os, random, re - -from rando.Items import ItemManager -from rom.ips import IPS_Patch -from utils.doorsmanager import DoorsManager -from graph.graph_utils import GraphUtils, getAccessPoint, locIdsByAreaAddresses -from logic.logic import Logic -from rom.rom import RealROM, snes_to_pc -from patches.patchaccess import PatchAccess -from utils.parameters import appDir -import utils.log +from worlds.sm.variaRandomizer.rando.Items import ItemManager +from worlds.sm.variaRandomizer.rom.ips import IPS_Patch +from worlds.sm.variaRandomizer.utils.doorsmanager import DoorsManager +from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils, getAccessPoint, locIdsByAreaAddresses +from worlds.sm.variaRandomizer.logic.logic import Logic +from worlds.sm.variaRandomizer.rom.rom import RealROM, snes_to_pc, pc_to_snes +from worlds.sm.variaRandomizer.patches.patchaccess import PatchAccess +from worlds.sm.variaRandomizer.utils.parameters import appDir +from worlds.sm.variaRandomizer.utils import log def getWord(w): return (w & 0x00FF, (w & 0xFF00) >> 8) @@ -57,7 +56,7 @@ class RomPatcher: } def __init__(self, romFileName=None, magic=None, plando=False, player=0): - self.log = utils.log.get('RomPatcher') + self.log = log.get('RomPatcher') self.romFileName = romFileName self.race = None self.romFile = RealROM(romFileName) diff --git a/worlds/sm/variaRandomizer/utils/doorsmanager.py b/worlds/sm/variaRandomizer/utils/doorsmanager.py index bb2f5f6121..c8feacdbab 100644 --- a/worlds/sm/variaRandomizer/utils/doorsmanager.py +++ b/worlds/sm/variaRandomizer/utils/doorsmanager.py @@ -1,10 +1,11 @@ import random import copy -from logic.smbool import SMBool -from rom.rom_patches import RomPatches -import utils.log, logging +from worlds.sm.variaRandomizer.logic.smbool import SMBool +from worlds.sm.variaRandomizer.rom.rom_patches import RomPatches +import logging -LOG = utils.log.get('DoorsManager') +from worlds.sm.variaRandomizer.utils import log +LOG = log.get('DoorsManager') colorsList = ['red', 'green', 'yellow', 'wave', 'spazer', 'plasma', 'ice'] # 1/15 chance to have the door set to grey diff --git a/worlds/sm/variaRandomizer/utils/parameters.py b/worlds/sm/variaRandomizer/utils/parameters.py index 0f7b62c6b9..6bae03b465 100644 --- a/worlds/sm/variaRandomizer/utils/parameters.py +++ b/worlds/sm/variaRandomizer/utils/parameters.py @@ -1,4 +1,4 @@ -from logic.smbool import SMBool +from worlds.sm.variaRandomizer.logic.smbool import SMBool import os import sys from pathlib import Path diff --git a/worlds/sm/variaRandomizer/utils/utils.py b/worlds/sm/variaRandomizer/utils/utils.py index 402c629919..ba43d710a3 100644 --- a/worlds/sm/variaRandomizer/utils/utils.py +++ b/worlds/sm/variaRandomizer/utils/utils.py @@ -1,8 +1,8 @@ import os, json, re, random -from utils.parameters import Knows, Settings, Controller, isKnows, isSettings, isButton -from utils.parameters import easy, medium, hard, harder, hardcore, mania, text2diff -from logic.smbool import SMBool +from worlds.sm.variaRandomizer.utils.parameters import Knows, Settings, Controller, isKnows, isSettings, isButton +from worlds.sm.variaRandomizer.utils.parameters import easy, medium, hard, harder, hardcore, mania, text2diff +from worlds.sm.variaRandomizer.logic.smbool import SMBool def isStdPreset(preset): return preset in ['newbie', 'casual', 'regular', 'veteran', 'expert', 'master', 'samus', 'solution', 'Season_Races', 'SMRAT2021'] @@ -264,7 +264,7 @@ class PresetLoaderDict(PresetLoader): super(PresetLoaderDict, self).__init__() def getDefaultMultiValues(): - from graph.graph_utils import GraphUtils + from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils defaultMultiValues = { 'startLocation': GraphUtils.getStartAccessPointNames(), 'majorsSplit': ['Full', 'FullWithHUD', 'Major', 'Chozo', 'Scavenger'], From 754a57cf690d3d8aa5689419114fece393ca9dfc Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 24 Mar 2023 00:41:06 +0100 Subject: [PATCH 107/172] WebHost: give active rooms a chance to reclaim their port --- WebHostLib/customserver.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index 6e7da40737..fa392492ce 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -203,6 +203,11 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict, with Locker(room_id): try: asyncio.run(main()) + except KeyboardInterrupt: + with db_session: + room = Room.get(id=room_id) + # ensure the Room does not spin up again on its own, minute of safety buffer + room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(minutes=1, seconds=room.timeout) except: with db_session: room = Room.get(id=room_id) From f4a68f1c3d90629ebb0c30dcd4ba70b6ebea5587 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sat, 25 Mar 2023 13:35:19 -0500 Subject: [PATCH 108/172] Core: add an everywhere location group (#1582) --- worlds/AutoWorld.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 55a6b9219a..4f4e2750cd 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -36,6 +36,9 @@ class AutoWorldRegister(type): in dct.get("item_name_groups", {}).items()} dct["item_name_groups"]["Everything"] = dct["item_names"] dct["location_names"] = frozenset(dct["location_name_to_id"]) + dct["location_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set + in dct.get("location_name_groups", {}).items()} + dct["location_name_groups"]["Everywhere"] = dct["location_names"] dct["all_item_and_group_names"] = frozenset(dct["item_names"] | set(dct.get("item_name_groups", {}))) # move away from get_required_client_version function From 7fd9e71b3c08439777c4474d253e212a0e8af3bb Mon Sep 17 00:00:00 2001 From: ScootyPuffJr1 <77215594+ScootyPuffJr1@users.noreply.github.com> Date: Fri, 24 Mar 2023 13:50:10 -0400 Subject: [PATCH 109/172] TLOZ: Fix Broken Links to Player Settings Page --- worlds/tloz/docs/multiworld_en.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/tloz/docs/multiworld_en.md b/worlds/tloz/docs/multiworld_en.md index d170095270..031d4eee87 100644 --- a/worlds/tloz/docs/multiworld_en.md +++ b/worlds/tloz/docs/multiworld_en.md @@ -34,7 +34,7 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) ### Where do I get a config file? The Player Settings page on the website allows you to configure your personal settings and export a config file from -them. Player settings page: [The Legend of Zelda Player Settings Page](/games/The%20Legen%20of%20Zelda/player-settings) +them. Player settings page: [The Legend of Zelda Player Settings Page](/games/The%20Legend%20of%20Zelda/player-settings) ### Verifying your config file @@ -44,7 +44,7 @@ validator page: [YAML Validation page](/mysterycheck) ## Generating a Single-Player Game 1. Navigate to the Player Settings page, configure your options, and click the "Generate Game" button. - - Player Settings page: [The Legend of Zelda Player Settings Page](/games/The%20Legen%20of%20Zelda/player-settings) + - Player Settings page: [The Legend of Zelda Player Settings Page](/games/The%20Legend%20of%20Zelda/player-settings) 2. You will be presented with a "Seed Info" page. 3. Click the "Create New Room" link. 4. You will be presented with a server page, from which you can download your patch file. From 4456e36fbb31b94b54044fe8ade49dcb952da0d6 Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Sat, 25 Mar 2023 14:36:48 -0400 Subject: [PATCH 110/172] KH2: fixes medal being shadow archive memory address (#1589) --- worlds/kh2/Items.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/kh2/Items.py b/worlds/kh2/Items.py index 442411dbdf..8e5e004fc2 100644 --- a/worlds/kh2/Items.py +++ b/worlds/kh2/Items.py @@ -159,7 +159,7 @@ Accessory_Table = { ItemName.FencerEarring: ItemData(0x130071, 1, 46, 0x35A7), ItemName.MageEarring: ItemData(0x130072, 1, 47, 0x35A8), ItemName.SlayerEarring: ItemData(0x130073, 1, 48, 0x35AC), - ItemName.Medal: ItemData(0x130074, 1, 53, 0x35B2), + ItemName.Medal: ItemData(0x130074, 1, 53, 0x35B0), ItemName.MoonAmulet: ItemData(0x130075, 1, 35, 0x359C), ItemName.StarCharm: ItemData(0x130076, 1, 36, 0x359E), ItemName.CosmicArts: ItemData(0x130077, 1, 56, 0x35B1), From 67eb370200f73d665ce0d2278217f593f085c4e9 Mon Sep 17 00:00:00 2001 From: Trevor L <80716066+TRPG0@users.noreply.github.com> Date: Sat, 25 Mar 2023 12:37:25 -0600 Subject: [PATCH 111/172] Blasphemous: Change ending option (#1592) --- worlds/blasphemous/Options.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/worlds/blasphemous/Options.py b/worlds/blasphemous/Options.py index be43d8b7c8..b3bb3a6bf5 100644 --- a/worlds/blasphemous/Options.py +++ b/worlds/blasphemous/Options.py @@ -38,10 +38,12 @@ class ExpertLogic(Toggle): class Ending(Choice): - """Choose which ending is required to complete the game.""" + """Choose which ending is required to complete the game. + Ending A: Collect all thorn upgrades. + Ending C: Collect all thorn upgrades and the Holy Wound of Abnegation.""" display_name = "Ending" option_any_ending = 0 - option_ending_b = 1 + option_ending_a = 1 option_ending_c = 2 default = 0 From ffd7d5da747deff4dd969adc6b2b3bce190b2969 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sat, 25 Mar 2023 19:54:42 +0100 Subject: [PATCH 112/172] ModuleUpdate/Setup: install pkg_resources, check pip, typing and cleanup (#1593) * ModuleUpdater/setup: install pkg_resources and check for pip plus minor cleanup in the github actions * ModuleUpdate/setup: make flake8 happy * ModuleUpdate/setup: make mypy happier --- .github/workflows/build.yml | 6 ++--- .github/workflows/lint.yml | 4 +-- .github/workflows/release.yml | 5 ++-- .github/workflows/unittests.yml | 4 +-- ModuleUpdate.py | 43 ++++++++++++++++++++++++------ setup.py | 47 ++++++++++++++++++++++++--------- 6 files changed, 77 insertions(+), 32 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d4e1efd466..5ab2d18eac 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,8 +36,7 @@ jobs: Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force - name: Build run: | - python -m pip install --upgrade pip setuptools - pip install -r requirements.txt + python -m pip install --upgrade pip python setup.py build_exe --yes $NAME="$(ls build)".Split('.',2)[1] $ZIP_NAME="Archipelago_$NAME.7z" @@ -85,8 +84,7 @@ jobs: # charset-normalizer was somehow incomplete in the github runner "${{ env.PYTHON }}" -m venv venv source venv/bin/activate - "${{ env.PYTHON }}" -m pip install --upgrade pip PyGObject setuptools charset-normalizer - pip install -r requirements.txt + "${{ env.PYTHON }}" -m pip install --upgrade pip PyGObject charset-normalizer python setup.py build_exe --yes bdist_appimage --yes echo -e "setup.py build output:\n `ls build`" echo -e "setup.py dist output:\n `ls dist`" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7ecda45eda..c20d244ad9 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,7 +12,7 @@ on: - '**.py' jobs: - build: + flake8: runs-on: ubuntu-latest @@ -25,7 +25,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip wheel - pip install flake8 pytest pytest-subtests + pip install flake8 if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fa3dd32100..4f1a8eec4a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,9 +63,8 @@ jobs: # charset-normalizer was somehow incomplete in the github runner "${{ env.PYTHON }}" -m venv venv source venv/bin/activate - "${{ env.PYTHON }}" -m pip install --upgrade pip PyGObject setuptools charset-normalizer - pip install -r requirements.txt - python setup.py build --yes bdist_appimage --yes + "${{ env.PYTHON }}" -m pip install --upgrade pip PyGObject charset-normalizer + python setup.py build_exe --yes bdist_appimage --yes echo -e "setup.py build output:\n `ls build`" echo -e "setup.py dist output:\n `ls dist`" cd dist && export APPIMAGE_NAME="`ls *.AppImage`" && cd .. diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index 93be745a8c..254d92dd6f 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -52,8 +52,8 @@ jobs: python-version: ${{ matrix.python.version }} - name: Install dependencies run: | - python -m pip install --upgrade pip wheel - pip install flake8 pytest pytest-subtests + python -m pip install --upgrade pip + pip install pytest pytest-subtests python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt" - name: Unittests run: | diff --git a/ModuleUpdate.py b/ModuleUpdate.py index a8258ce17e..ac40dbd66b 100644 --- a/ModuleUpdate.py +++ b/ModuleUpdate.py @@ -1,7 +1,6 @@ import os import sys import subprocess -import pkg_resources import warnings local_dir = os.path.dirname(__file__) @@ -22,18 +21,50 @@ if not update_ran: requirements_files.add(req_file) +def check_pip(): + # detect if pip is available + try: + import pip # noqa: F401 + except ImportError: + raise RuntimeError("pip not available. Please install pip.") + + +def confirm(msg: str): + try: + input(f"\n{msg}") + except KeyboardInterrupt: + print("\nAborting") + sys.exit(1) + + def update_command(): + check_pip() for file in requirements_files: - subprocess.call([sys.executable, '-m', 'pip', 'install', '-r', file, '--upgrade']) + subprocess.call([sys.executable, "-m", "pip", "install", "-r", file, "--upgrade"]) + + +def install_pkg_resources(yes=False): + try: + import pkg_resources # noqa: F401 + except ImportError: + check_pip() + if not yes: + confirm("pkg_resources not found, press enter to install it") + subprocess.call([sys.executable, "-m", "pip", "install", "--upgrade", "setuptools"]) def update(yes=False, force=False): global update_ran if not update_ran: update_ran = True + if force: update_command() return + + install_pkg_resources(yes=yes) + import pkg_resources + for req_file in requirements_files: path = os.path.join(os.path.dirname(sys.argv[0]), req_file) if not os.path.exists(path): @@ -52,7 +83,7 @@ def update(yes=False, force=False): egg = egg.split(";", 1)[0].rstrip() if any(compare in egg for compare in ("==", ">=", ">", "<", "<=", "!=")): warnings.warn(f"Specifying version as #egg={egg} will become unavailable in pip 25.0. " - "Use name @ url#version instead.", DeprecationWarning) + "Use name @ url#version instead.", DeprecationWarning) line = egg else: egg = "" @@ -79,11 +110,7 @@ def update(yes=False, force=False): if not yes: import traceback traceback.print_exc() - try: - input(f"\nRequirement {requirement} is not satisfied, press enter to install it") - except KeyboardInterrupt: - print("\nAborting") - sys.exit(1) + confirm(f"Requirement {requirement} is not satisfied, press enter to install it") update_command() return diff --git a/setup.py b/setup.py index 66cbb722f9..e693f849ee 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,6 @@ import io import json import threading import subprocess -import pkg_resources from collections.abc import Iterable from hashlib import sha3_512 @@ -22,12 +21,27 @@ from pathlib import Path # This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it try: requirement = 'cx-Freeze>=6.14.7' - pkg_resources.require(requirement) - import cx_Freeze -except pkg_resources.ResolutionError: + import pkg_resources + try: + pkg_resources.require(requirement) + install_cx_freeze = False + except pkg_resources.ResolutionError: + install_cx_freeze = True +except ImportError: + install_cx_freeze = True + pkg_resources = None # type: ignore [assignment] + +if install_cx_freeze: + # check if pip is available + try: + import pip # noqa: F401 + except ImportError: + raise RuntimeError("pip not available. Please install pip.") + # install and import cx_freeze if '--yes' not in sys.argv and '-y' not in sys.argv: input(f'Requirement {requirement} is not satisfied, press enter to install it') subprocess.call([sys.executable, '-m', 'pip', 'install', requirement, '--upgrade']) + import pkg_resources import cx_Freeze # .build only exists if cx-Freeze is the right version, so we have to update/install that first before this line @@ -120,6 +134,7 @@ def download_SNI(): print(f"No SNI found for system spec {platform_name} {machine_name}") +signtool: typing.Optional[str] if os.path.exists("X:/pw.txt"): print("Using signtool") with open("X:/pw.txt", encoding="utf-8-sig") as f: @@ -144,7 +159,7 @@ exes = [ target_name=c.frozen_name + (".exe" if is_windows else ""), icon=icon_paths[c.icon], base="Win32GUI" if is_windows and not c.cli else None - ) for c in components if c.script_name + ) for c in components if c.script_name and c.frozen_name ] extra_data = ["LICENSE", "data", "EnemizerCLI", "host.yaml", "SNI"] @@ -307,7 +322,6 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE): # which should be ok with zipfile.ZipFile(self.libfolder / "worlds" / (file_name + ".apworld"), "x", zipfile.ZIP_DEFLATED, compresslevel=9) as zf: - entry: os.DirEntry for path in world_directory.rglob("*.*"): relative_path = os.path.join(*path.parts[path.parts.index("worlds")+1:]) zf.write(path, relative_path) @@ -330,9 +344,9 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE): for exe in self.distribution.executables: print(f"Signing {exe.target_name}") os.system(signtool + os.path.join(self.buildfolder, exe.target_name)) - print(f"Signing SNI") + print("Signing SNI") os.system(signtool + os.path.join(self.buildfolder, "SNI", "SNI.exe")) - print(f"Signing OoT Utils") + print("Signing OoT Utils") for exe_path in (("Compress", "Compress.exe"), ("Decompress", "Decompress.exe")): os.system(signtool + os.path.join(self.buildfolder, "lib", "worlds", "oot", "data", *exe_path)) @@ -386,7 +400,8 @@ class AppImageCommand(setuptools.Command): yes: bool def write_desktop(self): - desktop_filename = self.app_dir / f'{self.app_id}.desktop' + assert self.app_dir, "Invalid app_dir" + desktop_filename = self.app_dir / f"{self.app_id}.desktop" with open(desktop_filename, 'w', encoding="utf-8") as f: f.write("\n".join(( "[Desktop Entry]", @@ -400,7 +415,8 @@ class AppImageCommand(setuptools.Command): desktop_filename.chmod(0o755) def write_launcher(self, default_exe: Path): - launcher_filename = self.app_dir / f'AppRun' + assert self.app_dir, "Invalid app_dir" + launcher_filename = self.app_dir / "AppRun" with open(launcher_filename, 'w', encoding="utf-8") as f: f.write(f"""#!/bin/sh exe="{default_exe}" @@ -422,11 +438,12 @@ $APPDIR/$exe "$@" launcher_filename.chmod(0o755) def install_icon(self, src: Path, name: typing.Optional[str] = None, symlink: typing.Optional[Path] = None): + assert self.app_dir, "Invalid app_dir" try: from PIL import Image except ModuleNotFoundError: if not self.yes: - input(f'Requirement PIL is not satisfied, press enter to install it') + input("Requirement PIL is not satisfied, press enter to install it") subprocess.call([sys.executable, '-m', 'pip', 'install', 'Pillow', '--upgrade']) from PIL import Image im = Image.open(src) @@ -503,8 +520,12 @@ def find_libs(*args: str) -> typing.Sequence[typing.Tuple[str, str]]: return (lib, lib_arch, lib_libc), path if not hasattr(find_libs, "cache"): - data = subprocess.run([shutil.which('ldconfig'), '-p'], capture_output=True, text=True).stdout.split('\n')[1:] - find_libs.cache = {k: v for k, v in (parse(line) for line in data if '=>' in line)} + ldconfig = shutil.which("ldconfig") + assert ldconfig, "Make sure ldconfig is in PATH" + data = subprocess.run([ldconfig, "-p"], capture_output=True, text=True).stdout.split("\n")[1:] + find_libs.cache = { # type: ignore [attr-defined] + k: v for k, v in (parse(line) for line in data if "=>" in line) + } def find_lib(lib, arch, libc): for k, v in find_libs.cache.items(): From 4f2b13a67407eb3a5768505af15c8a5d635a5d71 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sat, 25 Mar 2023 16:11:51 -0500 Subject: [PATCH 113/172] The Messenger, Docs: Change links for the release (#1595) * change links to point to my fork * Add documentation about reconnecting because that's necessary apparently * change the bug report link * be non ambiguous Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- worlds/messenger/__init__.py | 2 +- worlds/messenger/docs/en_The Messenger.md | 1 + worlds/messenger/docs/setup_en.md | 35 ++++++++++++++++------- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index 495ec80b8f..0aa71506c7 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -12,7 +12,7 @@ from . import Rules class MessengerWeb(WebWorld): theme = "ocean" - bug_report_page = "https://github.com/minous27/TheMessengerRandomizerMod/issues" + bug_report_page = "https://github.com/alwaysintreble/TheMessengerRandomizerModAP/issues" tut_en = Tutorial( "Multiworld Setup Tutorial", diff --git a/worlds/messenger/docs/en_The Messenger.md b/worlds/messenger/docs/en_The Messenger.md index c40668457e..bc43369008 100644 --- a/worlds/messenger/docs/en_The Messenger.md +++ b/worlds/messenger/docs/en_The Messenger.md @@ -5,6 +5,7 @@ - [Settings Page](../../../../games/The%20Messenger/player-settings) - [Courier Github](https://github.com/Brokemia/Courier) - [The Messenger Randomizer Github](https://github.com/minous27/TheMessengerRandomizerMod) +- [The Messenger Randomizer AP Github](https://github.com/alwaysintreble/TheMessengerRandomizerModAP) - [Jacksonbird8237's Item Tracker](https://github.com/Jacksonbird8237/TheMessengerItemTracker) - [PopTracker Pack](https://github.com/alwaysintreble/TheMessengerTrackPack) diff --git a/worlds/messenger/docs/setup_en.md b/worlds/messenger/docs/setup_en.md index 3b88503362..64f209e13f 100644 --- a/worlds/messenger/docs/setup_en.md +++ b/worlds/messenger/docs/setup_en.md @@ -4,7 +4,7 @@ - [Game Info](../../../../games/The%20Messenger/info/en) - [Settings Page](../../../../games/The%20Messenger/player-settings) - [Courier Github](https://github.com/Brokemia/Courier) -- [The Messenger Randomizer Github](https://github.com/minous27/TheMessengerRandomizerMod) +- [The Messenger Randomizer AP Github](https://github.com/alwaysintreble/TheMessengerRandomizerModAP) - [Jacksonbird8237's Item Tracker](https://github.com/Jacksonbird8237/TheMessengerItemTracker) - [PopTracker Pack](https://github.com/alwaysintreble/TheMessengerTrackPack) @@ -13,17 +13,20 @@ 1. Download and install Courier Mod Loader using the instructions on the release page * [Latest release is currently 0.7.1](https://github.com/Brokemia/Courier/releases) 2. Download and install the randomizer mod - 1. Download the latest TheMessengerRandomizer.zip from the [The Messenger Randomizer Mod releases page](https://github.com/minous27/TheMessengerRandomizerMod/releases) + 1. Download the latest TheMessengerRandomizerAP.zip from the + [The Messenger Randomizer Mod AP releases page](https://github.com/alwaysintreble/TheMessengerRandomizerModAP/releases) 2. Extract the zip file to `TheMessenger/Mods/` of your game's install location + * You can have both the non-AP randomizer and the AP randomizer installed at the same time, but the AP randomizer + is backwards compatible, so the non-AP one can be safely removed. 3. Optionally, Backup your save game - * On Windows - 1. Press `Windows Key + R` to open run - 2. Type `%appdata%` to access AppData - 3. Navigate to `AppData/locallow/SabotageStudios/The Messenger` - 4. Rename `SaveGame.txt` to any name of your choice - * On Linux - 1. Navigate to `steamapps/compatdata/764790/pfx/drive_c/users/steamuser/AppData/LocalLow/Sabotage Studio/The Messenger` - 2. Rename `SaveGame.txt` to any name of your choice + * On Windows + 1. Press `Windows Key + R` to open run + 2. Type `%appdata%` to access AppData + 3. Navigate to `AppData/locallow/SabotageStudios/The Messenger` + 4. Rename `SaveGame.txt` to any name of your choice + * On Linux + 1. Navigate to `steamapps/compatdata/764790/pfx/drive_c/users/steamuser/AppData/LocalLow/Sabotage Studio/The Messenger` + 2. Rename `SaveGame.txt` to any name of your choice ## Joining a MultiWorld Game @@ -41,10 +44,20 @@ 6. Navigate to save file selection 7. Select a new valid randomizer save +## Continuing a MultiWorld Game + +At any point while playing, it is completely safe to quit. Returning to the title screen or closing the game will +disconnect you from the server. To reconnect to an in progress MultiWorld, simply load the correct save file for that +MultiWorld. + +If the reconnection fails, the message on screen will state you are disconnected. If this happens, you can return to the +main menu and connect to the server as in [Joining a Multiworld Game](#joining-a-multiworld-game), then load the correct +save file. + ## Troubleshooting If you launch the game, and it hangs on the splash screen for more than 30 seconds try these steps: -1. Close the game and remove `TheMessengerRandomizer` from the `Mods` folder. +1. Close the game and remove `TheMessengerRandomizerAP` from the `Mods` folder. 2. Launch The Messenger 3. Delete any save slot 4. Reinstall the randomizer mod following step 2 of the installation. \ No newline at end of file From 7927b2ee25c6661943c82b67c7a87d7b4063acb5 Mon Sep 17 00:00:00 2001 From: Tarokarr <49489956+Tarokarr@users.noreply.github.com> Date: Sat, 25 Mar 2023 22:29:59 +0100 Subject: [PATCH 114/172] LADX: Added: Credits for sprite sheets (#1594) --- worlds/ladx/docs/en_Links Awakening DX.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/worlds/ladx/docs/en_Links Awakening DX.md b/worlds/ladx/docs/en_Links Awakening DX.md index d667490c8a..4e109284dd 100644 --- a/worlds/ladx/docs/en_Links Awakening DX.md +++ b/worlds/ladx/docs/en_Links Awakening DX.md @@ -42,6 +42,20 @@ This randomizer is based on (forked from) the wonderful work daid did on LADXR - The autotracker code for communication with magpie tracker is directly copied from kbranch's repo - https://github.com/kbranch/Magpie/tree/master/autotracking +### Sprites + +The following sprite sheets have been included with permission of their respective authors: + +* by Madam Materia (https://www.twitch.tv/isabelle_zephyr) + * Matty_LA +* by Linker (https://twitter.com/BenjaminMaksym) + * Bowwow + * Bunny + * Luigi + * Mario + * Richard + * Tarin + ## Some tips from LADXR...

Locations

From f2e1495d39e4b58ec61809a331b9aa58fefe95b3 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 26 Mar 2023 00:13:28 +0100 Subject: [PATCH 115/172] Setup: fix broken cx_freeze import --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e693f849ee..e455c04bad 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,8 @@ if install_cx_freeze: input(f'Requirement {requirement} is not satisfied, press enter to install it') subprocess.call([sys.executable, '-m', 'pip', 'install', requirement, '--upgrade']) import pkg_resources - import cx_Freeze + +import cx_Freeze # .build only exists if cx-Freeze is the right version, so we have to update/install that first before this line import setuptools.command.build From 7d603e7d8dee42d8d884910e0c7c261bf935ec8f Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 26 Mar 2023 00:54:56 +0100 Subject: [PATCH 116/172] CI: run build_exe twice (#1598) * CI: run build_exe twice ... ... to find broken conditional imports * CI: build again in venv --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5ab2d18eac..26a6e830c1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -94,6 +94,10 @@ jobs: echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV # - copy code above to release.yml - + - name: Build Again + run: | + source venv/bin/activate + python setup.py build_exe --yes - name: Store AppImage uses: actions/upload-artifact@v3 with: From 963c33c02ab8acaa7dd057f07c17da05ff26e998 Mon Sep 17 00:00:00 2001 From: "Payden K. Pringle" Date: Sun, 26 Mar 2023 12:34:00 -0500 Subject: [PATCH 117/172] Docs: Update BizHawk Open Script wording to be clearer. (#1599) --- worlds/dkc3/docs/setup_en.md | 2 +- worlds/sm/docs/multiworld_en.md | 2 +- worlds/smw/docs/setup_en.md | 2 +- worlds/smz3/docs/multiworld_en.md | 2 +- worlds/soe/docs/multiworld_en.md | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/worlds/dkc3/docs/setup_en.md b/worlds/dkc3/docs/setup_en.md index 0ebe189a1e..f3ba472599 100644 --- a/worlds/dkc3/docs/setup_en.md +++ b/worlds/dkc3/docs/setup_en.md @@ -100,7 +100,7 @@ the lua you are using in your file explorer and copy the `socket.dll` to the bas Once you have changed the loaded core, you must restart BizHawk. 2. Load your ROM file if it hasn't already been loaded. 3. Click on the Tools menu and click on **Lua Console** -4. Click the button to open a new Lua script. +4. Click the Open Folder icon that says `Open Script` via the tooltip on mouse hover, or click the Script Menu then `Open Script...`, or press `Ctrl-O`. 5. Select the `Connector.lua` file included with your client - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only. diff --git a/worlds/sm/docs/multiworld_en.md b/worlds/sm/docs/multiworld_en.md index 826be60188..77ec660dfe 100644 --- a/worlds/sm/docs/multiworld_en.md +++ b/worlds/sm/docs/multiworld_en.md @@ -100,7 +100,7 @@ the lua you are using in your file explorer and copy the `socket.dll` to the bas Once you have changed the loaded core, you must restart BizHawk. 2. Load your ROM file if it hasn't already been loaded. 3. Click on the Tools menu and click on **Lua Console** -4. Click the button to open a new Lua script. +4. Click the Open Folder icon that says `Open Script` via the tooltip on mouse hover, or click the Script Menu then `Open Script...`, or press `Ctrl-O`. 5. Select the `Connector.lua` file included with your client - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only. diff --git a/worlds/smw/docs/setup_en.md b/worlds/smw/docs/setup_en.md index 5f600c33f0..a8f6759227 100644 --- a/worlds/smw/docs/setup_en.md +++ b/worlds/smw/docs/setup_en.md @@ -90,7 +90,7 @@ the lua you are using in your file explorer and copy the `socket.dll` to the bas Once you have changed the loaded core, you must restart BizHawk. 2. Load your ROM file if it hasn't already been loaded. 3. Click on the Tools menu and click on **Lua Console** -4. Click the button to open a new Lua script. +4. Click the Open Folder icon that says `Open Script` via the tooltip on mouse hover, or click the Script Menu then `Open Script...`, or press `Ctrl-O`. 5. Select the `Connector.lua` file included with your client - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only. diff --git a/worlds/smz3/docs/multiworld_en.md b/worlds/smz3/docs/multiworld_en.md index 735be9d519..f375cd55e1 100644 --- a/worlds/smz3/docs/multiworld_en.md +++ b/worlds/smz3/docs/multiworld_en.md @@ -98,7 +98,7 @@ the lua you are using in your file explorer and copy the `socket.dll` to the bas Once you have changed the loaded core, you must restart BizHawk. 2. Load your ROM file if it hasn't already been loaded. 3. Click on the Tools menu and click on **Lua Console** -4. Click the button to open a new Lua script. +4. Click the Open Folder icon that says `Open Script` via the tooltip on mouse hover, or click the Script Menu then `Open Script...`, or press `Ctrl-O`. 5. Select the `Connector.lua` file included with your client - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only. diff --git a/worlds/soe/docs/multiworld_en.md b/worlds/soe/docs/multiworld_en.md index 115fe5ea1a..fcaf339f7b 100644 --- a/worlds/soe/docs/multiworld_en.md +++ b/worlds/soe/docs/multiworld_en.md @@ -85,7 +85,7 @@ you may be prompted to allow it to communicate through the Windows Firewall. Once you have changed the loaded core, you must restart BizHawk. 2. Load your ROM file if it hasn't already been loaded. 3. Click on the Tools menu and click on **Lua Console** -4. Click the button to open a new Lua script. +4. Click the Open Folder icon that says `Open Script` via the tooltip on mouse hover, or click the Script Menu then `Open Script...`, or press `Ctrl-O`. 5. Select any `Connector.lua` file from your SNI installation ##### bsnes-plus-nwa From e2c4293a6d7a5c0942c863e22ce90ea141a10297 Mon Sep 17 00:00:00 2001 From: JusticePS Date: Sun, 26 Mar 2023 10:34:18 -0700 Subject: [PATCH 118/172] Adventure: Fix basepatch access from other working directories (#1600) --- AdventureClient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AdventureClient.py b/AdventureClient.py index 4c2cb92c70..c4258993c3 100644 --- a/AdventureClient.py +++ b/AdventureClient.py @@ -436,7 +436,7 @@ async def patch_and_run_game(patch_file, ctx): logger.info(msg, extra={'compact_gui': True}) ctx.gui_error('Error', msg) - with open("data/adventure_basepatch.bsdiff4", "rb") as file: + with open(Utils.user_path("data", "adventure_basepatch.bsdiff4"), "rb") as file: basepatch = bytes(file.read()) base_patched_rom_data = bsdiff4.patch(base_rom, basepatch) From 90813c0f4b7ebb27f03b3c8e61786d7f38b9f603 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 27 Mar 2023 01:04:13 +0200 Subject: [PATCH 119/172] Test: explicitly make sure there is no double use of location Name or ID (#1605) --- test/general/TestLocations.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/test/general/TestLocations.py b/test/general/TestLocations.py index f1b3349eeb..8e4e3ab1be 100644 --- a/test/general/TestLocations.py +++ b/test/general/TestLocations.py @@ -6,13 +6,19 @@ from . import setup_solo_multiworld class TestBase(unittest.TestCase): def testCreateDuplicateLocations(self): - """Tests that no two Locations share a name.""" + """Tests that no two Locations share a name or ID.""" for game_name, world_type in AutoWorldRegister.world_types.items(): multiworld = setup_solo_multiworld(world_type) - locations = Counter(multiworld.get_locations()) + locations = Counter(location.name for location in multiworld.get_locations()) if locations: self.assertLessEqual(locations.most_common(1)[0][1], 1, - f"{world_type.game} has duplicate of location {locations.most_common(1)}") + f"{world_type.game} has duplicate of location name {locations.most_common(1)}") + + locations = Counter(location.address for location in multiworld.get_locations() + if type(location.address) is int) + if locations: + self.assertLessEqual(locations.most_common(1)[0][1], 1, + f"{world_type.game} has duplicate of location ID {locations.most_common(1)}") def testLocationsInDatapackage(self): """Tests that created locations not filled before fill starts exist in the datapackage.""" From b5bd93c4201653130022b5ab3be1ac0ff8ef687d Mon Sep 17 00:00:00 2001 From: Brooty Johnson <83629348+Br00ty@users.noreply.github.com> Date: Sun, 26 Mar 2023 21:04:04 -0400 Subject: [PATCH 120/172] fixed duplicate location (#1604) removed duplicate location for "PW: Vilhelm's Leggings" --- worlds/dark_souls_3/data/locations_data.py | 1 - 1 file changed, 1 deletion(-) diff --git a/worlds/dark_souls_3/data/locations_data.py b/worlds/dark_souls_3/data/locations_data.py index 5a85916973..98eb8076c2 100644 --- a/worlds/dark_souls_3/data/locations_data.py +++ b/worlds/dark_souls_3/data/locations_data.py @@ -468,7 +468,6 @@ painted_world_table = { # DLC "PW: Vilhelm's Armor": 0x113130E8, "PW: Vilhelm's Gauntlets": 0x113134D0, "PW: Vilhelm's Leggings": 0x113138B8, - "PW: Vilhelm's Leggings": 0x113138B8, "PW: Valorheart": 0x00F646E0, # GRAVETENDER FIGHT "PW: Champions Bones": 0x40000869, # GRAVETENDER FIGHT "PW: Onyx Blade": 0x00222E00, # VILHELM FIGHT From f09f3663d6ea400bd87cbf3030d66ca225754c32 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Mon, 27 Mar 2023 00:12:10 -0400 Subject: [PATCH 121/172] [WebHost] Unify style and behavior of popover and mobile menus (#1596) * [WebHost] Unify styles for popover and mobile menus. Adjust popover menu to function the same as the mobile menu, and toggle via JS. * Remove class `first-link` in favor of CSS `:first-child`. * Adjust mobile menu link font size and padding. Change wording of popover trigger text. Add border-right to popover menu. Change "Upload Host File" to "Host Game" * Change mobile menu text to "Host Game" --- WebHostLib/static/assets/baseHeader.js | 22 ++++++++ .../{dropdown.png => popover.png} | Bin WebHostLib/static/styles/themes/base.css | 50 +++++++++++------- WebHostLib/templates/header/baseHeader.html | 24 ++++----- 4 files changed, 63 insertions(+), 33 deletions(-) rename WebHostLib/static/static/button-images/{dropdown.png => popover.png} (100%) diff --git a/WebHostLib/static/assets/baseHeader.js b/WebHostLib/static/assets/baseHeader.js index b8ee82dd63..7c9be77840 100644 --- a/WebHostLib/static/assets/baseHeader.js +++ b/WebHostLib/static/assets/baseHeader.js @@ -1,9 +1,11 @@ window.addEventListener('load', () => { + // Mobile menu handling const menuButton = document.getElementById('base-header-mobile-menu-button'); const mobileMenu = document.getElementById('base-header-mobile-menu'); menuButton.addEventListener('click', (evt) => { evt.preventDefault(); + evt.stopPropagation(); if (!mobileMenu.style.display || mobileMenu.style.display === 'none') { return mobileMenu.style.display = 'flex'; @@ -15,4 +17,24 @@ window.addEventListener('load', () => { window.addEventListener('resize', () => { mobileMenu.style.display = 'none'; }); + + // Popover handling + const popoverText = document.getElementById('base-header-popover-text'); + const popoverMenu = document.getElementById('base-header-popover-menu'); + + popoverText.addEventListener('click', (evt) => { + evt.preventDefault(); + evt.stopPropagation(); + + if (!popoverMenu.style.display || popoverMenu.style.display === 'none') { + return popoverMenu.style.display = 'flex'; + } + + popoverMenu.style.display = 'none'; + }); + + document.body.addEventListener('click', () => { + mobileMenu.style.display = 'none'; + popoverMenu.style.display = 'none'; + }); }); diff --git a/WebHostLib/static/static/button-images/dropdown.png b/WebHostLib/static/static/button-images/popover.png similarity index 100% rename from WebHostLib/static/static/button-images/dropdown.png rename to WebHostLib/static/static/button-images/popover.png diff --git a/WebHostLib/static/styles/themes/base.css b/WebHostLib/static/styles/themes/base.css index 0c491f7b57..fdfe56af20 100644 --- a/WebHostLib/static/styles/themes/base.css +++ b/WebHostLib/static/styles/themes/base.css @@ -30,6 +30,8 @@ html{ } #base-header-right{ + display: flex; + flex-direction: row; margin-top: 4px; } @@ -42,7 +44,7 @@ html{ margin-top: 4px; } -#base-header a, #base-header-mobile-menu a, #dropdown #dropdown-text{ +#base-header a, #base-header-mobile-menu a, #base-header-popover-text{ color: #2f6b83; text-decoration: none; cursor: pointer; @@ -72,41 +74,49 @@ html{ position: absolute; top: 7rem; right: 0; - padding-top: 1rem; } #base-header-mobile-menu a{ - padding: 4rem 2rem; - font-size: 5rem; + padding: 3rem 1.5rem; + font-size: 4rem; line-height: 5rem; color: #699ca8; border-top: 1px solid #d3d3d3; } +#base-header-mobile-menu :first-child, #base-header-popover-menu :first-child{ + border-top: none; +} + #base-header-right-mobile img{ height: 3rem; } -#dropdown { - display: inline-block; -} -#dropdown:hover #dropdown-menu { - display: block; -} -#dropdown-menu{ +#base-header-popover-menu{ display: none; + flex-direction: column; position: absolute; background-color: #fff; - min-width: 160px; - padding: 12px 16px; - margin-left: -90px; - border-radius: 15px; + margin-left: -108px; + margin-top: 2.25rem; + border-radius: 10px; border-left: 2px solid #d0ebe6; border-bottom: 2px solid #d0ebe6; + border-right: 1px solid #d0ebe6; filter: drop-shadow(-6px 6px 2px #2e3e83); } -#dropdown-icon { - height: 7px; + +#base-header-popover-menu a{ + color: #699ca8; + border-top: 1px solid #d3d3d3; + text-align: center; + font-size: 1.5rem; + line-height: 3rem; + margin-right: 2px; + padding: 0.25rem 1rem; +} + +#base-header-popover-icon { width: 14px; margin-bottom: 3px; margin-left: 2px; @@ -142,10 +152,10 @@ html{ } #base-header-mobile-menu a{ - font-size: 2rem; + font-size: 1.5rem; line-height: 3rem; margin: 0; - padding: 0 1rem; + padding: 0.25rem 1rem; } } @@ -165,4 +175,4 @@ html{ margin-top: 30px; margin-left: 20px; } -} \ No newline at end of file +} diff --git a/WebHostLib/templates/header/baseHeader.html b/WebHostLib/templates/header/baseHeader.html index 5865d50988..0042937d62 100644 --- a/WebHostLib/templates/header/baseHeader.html +++ b/WebHostLib/templates/header/baseHeader.html @@ -11,18 +11,16 @@
- {% endblock %} From f0403b9c9d39a8d155a78d1aab8f96845809123f Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Sun, 26 Mar 2023 15:22:35 -0400 Subject: [PATCH 122/172] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20Sort=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- worlds/pokemon_rb/rom.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/pokemon_rb/rom.py b/worlds/pokemon_rb/rom.py index dd7d29967a..9eb670b635 100644 --- a/worlds/pokemon_rb/rom.py +++ b/worlds/pokemon_rb/rom.py @@ -274,14 +274,14 @@ def process_wild_pokemon(self): for mon in mons_to_add: stat_base = get_base_stat_total(mon) candidate_locations = get_encounter_slots(self) - candidate_locations = [self.multiworld.get_location(location.name, self.player) for location in candidate_locations] if self.multiworld.randomize_wild_pokemon[self.player].current_key in ["match_base_stats", "match_types_and_base_stats"]: - candidate_locations.sort(key=lambda slot: abs(get_base_stat_total(slot.item.name) - stat_base)) + candidate_locations.sort(key=lambda slot: abs(get_base_stat_total(slot.original_item) - stat_base)) if self.multiworld.randomize_wild_pokemon[self.player].current_key in ["match_types", "match_types_and_base_stats"]: candidate_locations.sort(key=lambda slot: not any([poke_data.pokemon_data[slot.original_item]["type1"] in [self.local_poke_data[mon]["type1"], self.local_poke_data[mon]["type2"]], poke_data.pokemon_data[slot.original_item]["type2"] in [self.local_poke_data[mon]["type1"], self.local_poke_data[mon]["type2"]]])) + candidate_locations = [self.multiworld.get_location(location.name, self.player) for location in candidate_locations] for location in candidate_locations: zone = " - ".join(location.name.split(" - ")[:-1]) if self.multiworld.catch_em_all[self.player] == "all_pokemon" and self.multiworld.area_1_to_1_mapping[self.player]: From 21c6c287559fa134fc77d2a2b90cca240d99be1a Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Mon, 27 Mar 2023 11:55:13 -0400 Subject: [PATCH 123/172] [WebHost] Fix weighted-settings UI incorrectly populating range values (#1602) * Fix a bug causing weighted-settings UI to incorrectly display range values with no default as having a value of 25. * Do not set min and max values of range options to zero by default. This causes clutter in saved `.yaml` files. --- WebHostLib/static/assets/weighted-settings.js | 50 +++++++++---------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js index da4d60fcad..68e950ea4d 100644 --- a/WebHostLib/static/assets/weighted-settings.js +++ b/WebHostLib/static/assets/weighted-settings.js @@ -78,8 +78,6 @@ const createDefaultSettings = (settingData) => { break; case 'range': case 'special_range': - newSettings[game][gameSetting][setting.min] = 0; - newSettings[game][gameSetting][setting.max] = 0; newSettings[game][gameSetting]['random'] = 0; newSettings[game][gameSetting]['random-low'] = 0; newSettings[game][gameSetting]['random-high'] = 0; @@ -296,33 +294,33 @@ const buildWeightedSettingsDiv = (game, settings) => { if (((setting.max - setting.min) + 1) < 11) { for (let i=setting.min; i <= setting.max; ++i) { const tr = document.createElement('tr'); - const tdLeft = document.createElement('td'); - tdLeft.classList.add('td-left'); - tdLeft.innerText = i; - tr.appendChild(tdLeft); + const tdLeft = document.createElement('td'); + tdLeft.classList.add('td-left'); + tdLeft.innerText = i; + tr.appendChild(tdLeft); - const tdMiddle = document.createElement('td'); - tdMiddle.classList.add('td-middle'); - const range = document.createElement('input'); - range.setAttribute('type', 'range'); - range.setAttribute('id', `${game}-${settingName}-${i}-range`); - range.setAttribute('data-game', game); - range.setAttribute('data-setting', settingName); - range.setAttribute('data-option', i); - range.setAttribute('min', 0); - range.setAttribute('max', 50); - range.addEventListener('change', updateGameSetting); - range.value = currentSettings[game][settingName][i]; - tdMiddle.appendChild(range); - tr.appendChild(tdMiddle); + const tdMiddle = document.createElement('td'); + tdMiddle.classList.add('td-middle'); + const range = document.createElement('input'); + range.setAttribute('type', 'range'); + range.setAttribute('id', `${game}-${settingName}-${i}-range`); + range.setAttribute('data-game', game); + range.setAttribute('data-setting', settingName); + range.setAttribute('data-option', i); + range.setAttribute('min', 0); + range.setAttribute('max', 50); + range.addEventListener('change', updateGameSetting); + range.value = currentSettings[game][settingName][i] || 0; + tdMiddle.appendChild(range); + tr.appendChild(tdMiddle); - const tdRight = document.createElement('td'); - tdRight.setAttribute('id', `${game}-${settingName}-${i}`) - tdRight.classList.add('td-right'); - tdRight.innerText = range.value; - tr.appendChild(tdRight); + const tdRight = document.createElement('td'); + tdRight.setAttribute('id', `${game}-${settingName}-${i}`) + tdRight.classList.add('td-right'); + tdRight.innerText = range.value; + tr.appendChild(tdRight); - rangeTbody.appendChild(tr); + rangeTbody.appendChild(tr); } } else { const hintText = document.createElement('p'); From e62f989ce8fba81518ab9101aec2303bf63f0c6f Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Mon, 27 Mar 2023 13:17:06 -0400 Subject: [PATCH 124/172] Kh2 rc2 fixes (#1608) --- KH2Client.py | 105 ++++++++++++++++++++++++++++------------ worlds/kh2/Items.py | 38 +++++---------- worlds/kh2/Locations.py | 1 - worlds/kh2/Options.py | 13 ++--- worlds/kh2/__init__.py | 39 ++++++++++----- 5 files changed, 121 insertions(+), 75 deletions(-) diff --git a/KH2Client.py b/KH2Client.py index 2068e11e84..5223d8a111 100644 --- a/KH2Client.py +++ b/KH2Client.py @@ -119,7 +119,12 @@ class KH2Context(CommonContext): "LimitLevel": 0, "MasterLevel": 0, "FinalLevel": 0, - } + }, + "SoldEquipment": [], + "SoldBoosts": {"Power Boost": 0, + "Magic Boost": 0, + "Defense Boost": 0, + "AP Boost": 0} } self.slotDataProgressionNames = {} self.kh2seedname = None @@ -137,23 +142,23 @@ class KH2Context(CommonContext): self.finalxemnas = False self.worldid = { # 1: {}, # world of darkness (story cutscenes) - 2: TT_Checks, + 2: TT_Checks, # 3: {}, # destiny island doesn't have checks to ima put tt checks here - 4: HB_Checks, - 5: BC_Checks, - 6: Oc_Checks, - 7: AG_Checks, - 8: LoD_Checks, - 9: HundredAcreChecks, - 10: PL_Checks, - 11: DC_Checks, # atlantica isn't a supported world. if you go in atlantica it will check dc - 12: DC_Checks, - 13: TR_Checks, - 14: HT_Checks, - 15: HB_Checks, # world map, but you only go to the world map while on the way to goa so checking hb - 16: PR_Checks, - 17: SP_Checks, - 18: TWTNW_Checks, + 4: HB_Checks, + 5: BC_Checks, + 6: Oc_Checks, + 7: AG_Checks, + 8: LoD_Checks, + 9: HundredAcreChecks, + 10: PL_Checks, + 11: DC_Checks, # atlantica isn't a supported world. if you go in atlantica it will check dc + 12: DC_Checks, + 13: TR_Checks, + 14: HT_Checks, + 15: HB_Checks, # world map, but you only go to the world map while on the way to goa so checking hb + 16: PR_Checks, + 17: SP_Checks, + 18: TWTNW_Checks, # 255: {}, # starting screen } # 0x2A09C00+0x40 is the sve anchor. +1 is the last saved room @@ -412,7 +417,7 @@ class KH2Context(CommonContext): logger.info(e) async def verifyLevel(self): - for leveltype, anchor in {"SoraLevel": 0x24FF, + for leveltype, anchor in {"SoraLevel": 0x24FF, "ValorLevel": 0x32F6, "WisdomLevel": 0x332E, "LimitLevel": 0x3366, @@ -536,6 +541,34 @@ class KH2Context(CommonContext): self.ui = KH2Manager(self) self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + async def IsInShop(self, sellable, master_boost): + # journal = 0x741230 shop = 0x741320 + # if journal=-1 and shop = 5 then in shop + # if journam !=-1 and shop = 10 then journal + journal = self.kh2.read_short(self.kh2.base_address + 0x741230) + shop = self.kh2.read_short(self.kh2.base_address + 0x741320) + if (journal == -1 and shop == 5) or (journal != -1 and shop == 10): + # print("your in the shop") + sellable_dict = {} + for itemName in sellable: + itemdata = self.item_name_to_data[itemName] + amount = int.from_bytes( + self.kh2.read_bytes(self.kh2.base_address + self.Save + itemdata.memaddr, 1), "big") + sellable_dict[itemName] = amount + while (journal == -1 and shop == 5) or (journal != -1 and shop == 10): + journal = self.kh2.read_short(self.kh2.base_address + 0x741230) + shop = self.kh2.read_short(self.kh2.base_address + 0x741320) + await asyncio.sleep(0.5) + for item, amount in sellable_dict.items(): + itemdata = self.item_name_to_data[item] + afterShop = int.from_bytes( + self.kh2.read_bytes(self.kh2.base_address + self.Save + itemdata.memaddr, 1), "big") + if afterShop < amount: + if item in master_boost: + self.kh2seedsave["SoldBoosts"][item] += (amount - afterShop) + else: + self.kh2seedsave["SoldEquipment"].append(item) + async def verifyItems(self): try: local_amount = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Amount"].keys()) @@ -578,6 +611,8 @@ class KH2Context(CommonContext): server_boost = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Boost"].keys()) master_boost = local_boost | server_boost + master_sell = master_equipment | master_staff | master_shield | master_boost + await asyncio.create_task(self.IsInShop(master_sell, master_boost)) for itemName in master_amount: itemData = self.item_name_to_data[itemName] amountOfItems = 0 @@ -603,7 +638,8 @@ class KH2Context(CommonContext): itemData = self.item_name_to_data[itemName] # if the inventory slot for that keyblade is less than the amount they should have if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), - "big") <= 0: + "big") != 1 and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x1CFF, 1), + "big") != 13: # Checking form anchors for the keyblade if self.kh2.read_short(self.kh2.base_address + self.Save + 0x24F0) == itemData.kh2id \ or self.kh2.read_short(self.kh2.base_address + self.Save + 0x32F4) == itemData.kh2id \ @@ -618,7 +654,8 @@ class KH2Context(CommonContext): itemData = self.item_name_to_data[itemName] if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), "big") != 1 \ - and self.kh2.read_short(self.kh2.base_address + self.Save + 0x2604) != itemData.kh2id: + and self.kh2.read_short(self.kh2.base_address + self.Save + 0x2604) != itemData.kh2id \ + and itemName not in self.kh2seedsave["SoldEquipment"]: self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, (1).to_bytes(1, 'big'), 1) @@ -626,7 +663,8 @@ class KH2Context(CommonContext): itemData = self.item_name_to_data[itemName] if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), "big") != 1 \ - and self.kh2.read_short(self.kh2.base_address + self.Save + 0x2718) != itemData.kh2id: + and self.kh2.read_short(self.kh2.base_address + self.Save + 0x2718) != itemData.kh2id \ + and itemName not in self.kh2seedsave["SoldEquipment"]: self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, (1).to_bytes(1, 'big'), 1) @@ -679,16 +717,18 @@ class KH2Context(CommonContext): Equipment_Anchor_List = self.Equipment_Anchor_Dict["Accessories"] else: Equipment_Anchor_List = self.Equipment_Anchor_Dict["Armor"] - if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), - "big") != 1: # Checking form anchors for the equipment - for slot in Equipment_Anchor_List: - if self.kh2.read_short(self.kh2.base_address + self.Save + slot) == itemData.kh2id: - isThere = True + for slot in Equipment_Anchor_List: + if self.kh2.read_short(self.kh2.base_address + self.Save + slot) == itemData.kh2id: + isThere = True + if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), + "big") != 0: self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, (0).to_bytes(1, 'big'), 1) - break - if not isThere: + break + if not isThere and itemName not in self.kh2seedsave["SoldEquipment"]: + if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), + "big") != 1: self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, (1).to_bytes(1, 'big'), 1) @@ -736,7 +776,8 @@ class KH2Context(CommonContext): # Ap Boots start at +50 for some reason if itemName == "AP Boost": amountOfUsedBoosts -= 50 - if (amountOfBoostsInInvo + amountOfUsedBoosts) <= amountOfItems and amountOfBoostsInInvo < 255: + totalBoosts = (amountOfBoostsInInvo + amountOfUsedBoosts) + if totalBoosts <= amountOfItems - self.kh2seedsave["SoldBoosts"][itemName] and amountOfBoostsInInvo < 255: self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, (amountOfBoostsInInvo + 1).to_bytes(1, 'big'), 1) @@ -821,9 +862,9 @@ async def kh2_watcher(ctx: KH2Context): currentWorld = int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + 0x0714DB8, 1), "big") if location not in ctx.kh2seedsave["worldIdChecks"][str(currentWorld)]: ctx.kh2seedsave["worldIdChecks"][str(currentWorld)].append(location) - if location in ctx.kh2LocalItems: - item = ctx.kh2slotdata["LocalItems"][str(location)] - await asyncio.create_task(ctx.give_item(item, "LocalItems")) + if location in ctx.kh2LocalItems: + item = ctx.kh2slotdata["LocalItems"][str(location)] + await asyncio.create_task(ctx.give_item(item, "LocalItems")) await ctx.send_msgs(message) elif not ctx.kh2connected and ctx.serverconneced: logger.info("Game is not open. Disconnecting from Server.") diff --git a/worlds/kh2/Items.py b/worlds/kh2/Items.py index 8e5e004fc2..aa0e326c3d 100644 --- a/worlds/kh2/Items.py +++ b/worlds/kh2/Items.py @@ -856,31 +856,19 @@ Progression_Dicts = { ItemName.NamineSketches }, "AllVisitLocking": { - ItemName.CastleKey, - ItemName.CastleKey, - ItemName.BattlefieldsofWar, - ItemName.BattlefieldsofWar, - ItemName.SwordoftheAncestor, - ItemName.SwordoftheAncestor, - ItemName.BeastsClaw, - ItemName.BeastsClaw, - ItemName.BoneFist, - ItemName.BoneFist, - ItemName.ProudFang, - ItemName.ProudFang, - ItemName.SkillandCrossbones, - ItemName.SkillandCrossbones, - ItemName.Scimitar, - ItemName.Scimitar, - ItemName.MembershipCard, - ItemName.MembershipCard, - ItemName.WaytotheDawn, - ItemName.IdentityDisk, - ItemName.IdentityDisk, - ItemName.IceCream, - ItemName.IceCream, - ItemName.IceCream, - ItemName.NamineSketches, + ItemName.CastleKey: 2, + ItemName.BattlefieldsofWar: 2, + ItemName.SwordoftheAncestor: 2, + ItemName.BeastsClaw: 2, + ItemName.BoneFist: 2, + ItemName.ProudFang: 2, + ItemName.SkillandCrossbones: 2, + ItemName.Scimitar: 2, + ItemName.MembershipCard: 2, + ItemName.WaytotheDawn: 1, + ItemName.IdentityDisk: 2, + ItemName.IceCream: 3, + ItemName.NamineSketches: 1, } } diff --git a/worlds/kh2/Locations.py b/worlds/kh2/Locations.py index 87ec02be9b..9b5cc55252 100644 --- a/worlds/kh2/Locations.py +++ b/worlds/kh2/Locations.py @@ -1483,7 +1483,6 @@ exclusion_table = { LocationName.OasisMap, LocationName.OasisTornPages, LocationName.OasisAPBoost, - LocationName.StationofSerenityPotion, LocationName.StationofCallingPotion, LocationName.CentralStationPotion1, LocationName.STTCentralStationHiPotion, diff --git a/worlds/kh2/Options.py b/worlds/kh2/Options.py index 03f60f28b6..671d85323a 100644 --- a/worlds/kh2/Options.py +++ b/worlds/kh2/Options.py @@ -59,7 +59,7 @@ class SummonEXP(Range): class Schmovement(Choice): - """Level of Growth You Start With""" + """Level of Progressive Movement You Start With""" display_name = "Schmovement" option_level_0 = 0 option_level_1 = 1 @@ -106,9 +106,10 @@ class Visitlocking(Choice): class RandomVisitLockingItem(Range): + """Start with random amount of visit locking items.""" display_name = "Random Visit Locking Item" range_start = 0 - range_end = 27 + range_end = 25 default = 3 @@ -191,7 +192,7 @@ class LuckyEmblemsRequired(Range): """Number of Lucky Emblems to collect to Open The Final Door bosses. If Goal is not Lucky Emblem Hunt this does nothing.""" display_name = "Lucky Emblems Required" - range_start = 0 + range_start = 1 range_end = 60 default = 25 @@ -200,7 +201,7 @@ class LuckyEmblemsAmount(Range): """Number of Lucky Emblems that are in the pool. If Goal is not Lucky Emblem Hunt this does nothing.""" display_name = "Lucky Emblems Available" - range_start = 0 + range_start = 1 range_end = 60 default = 40 @@ -209,7 +210,7 @@ class BountyRequired(Range): """Number of Bounties that are Required. If Goal is not Hitlist this does nothing.""" display_name = "Bounties Required" - range_start = 0 + range_start = 1 range_end = 24 default = 7 @@ -218,7 +219,7 @@ class BountyAmount(Range): """Number of Bounties that are in the pool. If Goal is not Hitlist this does nothing.""" display_name = "Bounties Available" - range_start = 0 + range_start = 1 range_end = 24 default = 13 diff --git a/worlds/kh2/__init__.py b/worlds/kh2/__init__.py index 99037ec40c..e36c81e806 100644 --- a/worlds/kh2/__init__.py +++ b/worlds/kh2/__init__.py @@ -52,12 +52,12 @@ class KH2World(World): self.goofy_ability_pool = list() self.sora_keyblade_ability_pool = list() self.keyblade_slot_copy = list(Locations.Keyblade_Slots.keys()) + self.keyblade_slot_copy.remove(LocationName.KingdomKeySlot) self.totalLocations = len(all_locations.items()) self.growth_list = list() for x in range(4): self.growth_list.extend(Movement_Table.keys()) - self.visitlockingitem = list() - self.visitlockingitem.extend(Progression_Dicts["AllVisitLocking"]) + self.visitlocking_dict = Progression_Dicts["AllVisitLocking"] def fill_slot_data(self) -> dict: return {"hitlist": self.hitlist, @@ -94,7 +94,7 @@ class KH2World(World): self.sora_keyblade_ability_pool.extend(SupportAbility_Table.keys()) for item, value in self.multiworld.start_inventory[self.player].value.items(): - if item in ActionAbility_Table.keys() or item in SupportAbility_Table.keys(): + if item in ActionAbility_Table.keys() or item in SupportAbility_Table.keys() or exclusionItem_table["StatUps"]: # cannot have more than the quantity for abilties if value > item_dictionary_table[item].quantity: logging.info(f"{self.multiworld.get_file_safe_player_name(self.player)} cannot have more than {item_dictionary_table[item].quantity} of {item}" @@ -216,6 +216,15 @@ class KH2World(World): self.RandomSuperBoss.remove(randomBoss) self.totalLocations -= 1 + # Kingdom Key cannot have No Experience so plandoed here instead of checking 26 times if its kingdom key + random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.sora_keyblade_ability_pool) + while random_ability == ItemName.NoExperience: + random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.sora_keyblade_ability_pool) + self.multiworld.get_location(LocationName.KingdomKeySlot, self.player).place_locked_item(self.create_item(random_ability)) + self.item_quantity_dict[random_ability] -= 1 + self.sora_keyblade_ability_pool.remove(random_ability) + self.totalLocations -= 1 + # plando keyblades because they can only have abilities for keyblade in self.keyblade_slot_copy: random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.sora_keyblade_ability_pool) @@ -266,25 +275,33 @@ class KH2World(World): # no visit locking if self.multiworld.Visitlocking[self.player] == "no_visit_locking": - for item in self.visitlockingitem: - self.multiworld.push_precollected(self.create_item(item)) - self.item_quantity_dict[item] -= 1 - self.visitlockingitem.remove(item) + for item, amount in Progression_Dicts["AllVisitLocking"].items(): + for _ in range(amount): + self.multiworld.push_precollected(self.create_item(item)) + self.item_quantity_dict[item] -= 1 + if self.visitlocking_dict[item] == 0: + self.visitlocking_dict.pop(item) + self.visitlocking_dict[item] -= 1 # first and second visit locking elif self.multiworld.Visitlocking[self.player] == "second_visit_locking": for item in Progression_Dicts["2VisitLocking"]: self.item_quantity_dict[item] -= 1 + self.visitlocking_dict[item] -= 1 + if self.visitlocking_dict[item] == 0: + self.visitlocking_dict.pop(item) self.multiworld.push_precollected(self.create_item(item)) - self.visitlockingitem.remove(item) for _ in range(self.multiworld.RandomVisitLockingItem[self.player].value): - if len(self.visitlockingitem) <= 0: + if sum(self.visitlocking_dict.values()) <= 0: break - item = self.multiworld.per_slot_randoms[self.player].choice(self.visitlockingitem) + visitlocking_set = list(self.visitlocking_dict.keys()) + item = self.multiworld.per_slot_randoms[self.player].choice(visitlocking_set) self.item_quantity_dict[item] -= 1 + self.visitlocking_dict[item] -= 1 + if self.visitlocking_dict[item] == 0: + self.visitlocking_dict.pop(item) self.multiworld.push_precollected(self.create_item(item)) - self.visitlockingitem.remove(item) # there are levels but level 1 is there to keep code clean if self.multiworld.LevelDepth[self.player] == "level_99_sanity": From a7f7f91aaf96e4f6ece4780672ceb9750818086d Mon Sep 17 00:00:00 2001 From: Jarno Date: Mon, 27 Mar 2023 19:17:50 +0200 Subject: [PATCH 125/172] Timespinner: LOGIC FIX, RC BUG (#1610) --- worlds/timespinner/Locations.py | 46 ++++++++++++++++----------------- worlds/timespinner/Regions.py | 6 ++--- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/worlds/timespinner/Locations.py b/worlds/timespinner/Locations.py index 03f1c025dc..70c76b8638 100644 --- a/worlds/timespinner/Locations.py +++ b/worlds/timespinner/Locations.py @@ -41,8 +41,8 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int], LocationData('Upper lake desolation', 'Lake Desolation (Upper): Double jump cave floor', 1337013), LocationData('Upper lake desolation', 'Lake Desolation (Upper): Sparrow chest', 1337014), LocationData('Upper lake desolation', 'Lake Desolation (Upper): Crash site pedestal', 1337015), - LocationData('Upper lake desolation', 'Lake Desolation (Upper): Crash site chest 1', 1337016, lambda state: state.has_all({'Killed Maw'}, player)), - LocationData('Upper lake desolation', 'Lake Desolation (Upper): Crash site chest 2', 1337017, lambda state: state.has_all({'Killed Maw'}, player)), + LocationData('Upper lake desolation', 'Lake Desolation (Upper): Crash site chest 1', 1337016, lambda state: state.has('Killed Maw', player)), + LocationData('Upper lake desolation', 'Lake Desolation (Upper): Crash site chest 2', 1337017, lambda state: state.has('Killed Maw', player)), LocationData('Eastern lake desolation', 'Lake Desolation: Kitty Boss', 1337018), LocationData('Library', 'Library: Basement', 1337019), LocationData('Library', 'Library: Warp gate', 1337020), @@ -77,7 +77,7 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int], LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Secret room', 1337049, logic.can_break_walls), LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Bottom left room', 1337050), LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Last chance before Xarion', 1337051, logic.has_doublejump), - LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Xarion', 1337052, lambda state: state.has('Water Mask', player) if flooded.flood_xarion else True), + LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Xarion', 1337052, lambda state: not flooded.flood_xarion or state.has('Water Mask', player)), LocationData('Sealed Caves (Sirens)', 'Sealed Caves (Sirens): Water hook', 1337053, lambda state: state.has('Water Mask', player)), LocationData('Sealed Caves (Sirens)', 'Sealed Caves (Sirens): Siren room underwater right', 1337054, lambda state: state.has('Water Mask', player)), LocationData('Sealed Caves (Sirens)', 'Sealed Caves (Sirens): Siren room underwater left', 1337055, lambda state: state.has('Water Mask', player)), @@ -125,7 +125,7 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int], LocationData('Forest', 'Forest: Waterfall chest 1', 1337094, lambda state: state.has('Water Mask', player)), LocationData('Forest', 'Forest: Waterfall chest 2', 1337095, lambda state: state.has('Water Mask', player)), LocationData('Forest', 'Forest: Batcave', 1337096), - LocationData('Forest', 'Castle Ramparts: In the moat', 1337097, lambda state: state.has('Water Mask', player) if flooded.flood_moat else True), + LocationData('Forest', 'Castle Ramparts: In the moat', 1337097, lambda state: not flooded.flood_moat or state.has('Water Mask', player)), LocationData('Left Side forest Caves', 'Forest: Before Serene single bat cave', 1337098), LocationData('Upper Lake Serene', 'Lake Serene (Upper): Rat nest', 1337099), LocationData('Upper Lake Serene', 'Lake Serene (Upper): Double jump cave platform', 1337100, logic.has_doublejump), @@ -139,22 +139,22 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int], LocationData('Lower Lake Serene', 'Lake Serene (Lower): Under the eels', 1337106), LocationData('Lower Lake Serene', 'Lake Serene (Lower): Water spikes room', 1337107), LocationData('Lower Lake Serene', 'Lake Serene (Lower): Underwater secret', 1337108, logic.can_break_walls), - LocationData('Lower Lake Serene', 'Lake Serene (Lower): T chest', 1337109, lambda state: logic.has_doublejump_of_npc(state) if flooded.dry_lake_serene else True), + LocationData('Lower Lake Serene', 'Lake Serene (Lower): T chest', 1337109, lambda state: not flooded.dry_lake_serene or logic.has_doublejump_of_npc(state)), LocationData('Lower Lake Serene', 'Lake Serene (Lower): Past the eels', 1337110), - LocationData('Lower Lake Serene', 'Lake Serene (Lower): Underwater pedestal', 1337111, lambda state: logic.has_doublejump(state) if flooded.dry_lake_serene else True), - LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Shroom jump room', 1337112, lambda state: logic.has_doublejump(state) if not flooded.flood_maw else True), - LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Secret room', 1337113, lambda state: logic.can_break_walls(state) and (state.has('Water Mask', player) if flooded.flood_maw else True)), - LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Bottom left room', 1337114, lambda state: state.has('Water Mask', player) if flooded.flood_maw else True), + LocationData('Lower Lake Serene', 'Lake Serene (Lower): Underwater pedestal', 1337111, lambda state: not flooded.dry_lake_serene or logic.has_doublejump(state)), + LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Shroom jump room', 1337112, lambda state: not flooded.flood_maw or logic.has_doublejump(state)), + LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Secret room', 1337113, lambda state: logic.can_break_walls(state) and (not flooded.flood_maw or state.has('Water Mask', player))), + LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Bottom left room', 1337114, lambda state: not flooded.flood_maw or state.has('Water Mask', player)), LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Single shroom room', 1337115), LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 1', 1337116, lambda state: logic.has_forwarddash_doublejump(state) or flooded.flood_maw), LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 2', 1337117, lambda state: logic.has_forwarddash_doublejump(state) or flooded.flood_maw), LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 3', 1337118, lambda state: logic.has_forwarddash_doublejump(state) or flooded.flood_maw), LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 4', 1337119, lambda state: logic.has_forwarddash_doublejump(state) or flooded.flood_maw), - LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Pedestal', 1337120, lambda state: state.has('Water Mask', player) if flooded.flood_maw else True), + LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Pedestal', 1337120, lambda state: not flooded.flood_maw or state.has('Water Mask', player)), LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Last chance before Maw', 1337121, lambda state: state.has('Water Mask', player) if flooded.flood_maw else logic.has_doublejump(state)), - LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Plasma Crystal', 1337173, lambda state: state.has_any({'Gas Mask', 'Talaria Attachment'}, player) and (state.has('Water Mask', player) if flooded.flood_maw else True)), - LocationData('Caves of Banishment (Maw)', 'Killed Maw', EventId, lambda state: state.has('Gas Mask', player) and (state.has('Water Mask', player) if flooded.flood_maw else True)), - LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Mineshaft', 1337122, lambda state: state.has_any({'Gas Mask', 'Talaria Attachment'}, player) and (state.has('Water Mask', player) if flooded.flood_maw else True)), + LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Plasma Crystal', 1337173, lambda state: state.has_any({'Gas Mask', 'Talaria Attachment'}, player) and (not flooded.flood_maw or state.has('Water Mask', player))), + LocationData('Caves of Banishment (Maw)', 'Killed Maw', EventId, lambda state: state.has('Gas Mask', player) and (not flooded.flood_maw or state.has('Water Mask', player))), + LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Mineshaft', 1337122, lambda state: state.has_any({'Gas Mask', 'Talaria Attachment'}, player) and (not flooded.flood_maw or state.has('Water Mask', player))), LocationData('Caves of Banishment (Sirens)', 'Caves of Banishment (Sirens): Wyvern room', 1337123), LocationData('Caves of Banishment (Sirens)', 'Caves of Banishment (Sirens): Siren room above water chest', 1337124), LocationData('Caves of Banishment (Sirens)', 'Caves of Banishment (Sirens): Siren room underwater left chest', 1337125, lambda state: state.has('Water Mask', player)), @@ -179,11 +179,11 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int], LocationData('Castle Keep', 'Castle Keep: Royal guard tiny room', 1337141, lambda state: logic.has_doublejump(state) or logic.has_fastjump_on_npc(state)), LocationData('Royal towers (lower)', 'Royal Towers: Floor secret', 1337142, lambda state: logic.has_doublejump(state) and logic.can_break_walls(state)), LocationData('Royal towers', 'Royal Towers: Pre-climb gap', 1337143), - LocationData('Royal towers', 'Royal Towers: Long balcony', 1337144, lambda state: state.has('Water Mask', player) if flooded.flood_courtyard else True), - LocationData('Royal towers', 'Royal Towers: Past bottom struggle juggle', 1337145, lambda state: logic.has_doublejump_of_npc(state) if not flooded.flood_courtyard else True), + LocationData('Royal towers', 'Royal Towers: Long balcony', 1337144, lambda state: not flooded.flood_courtyard or state.has('Water Mask', player)), + LocationData('Royal towers', 'Royal Towers: Past bottom struggle juggle', 1337145, lambda state: flooded.flood_courtyard or logic.has_doublejump_of_npc(state)), LocationData('Royal towers', 'Royal Towers: Bottom struggle juggle', 1337146, logic.has_doublejump_of_npc), LocationData('Royal towers (upper)', 'Royal Towers: Top struggle juggle', 1337147, logic.has_doublejump_of_npc), - LocationData('Royal towers (upper)', 'Royal Towers: No struggle required', 1337148, logic.has_doublejump_of_npc), + LocationData('Royal towers (upper)', 'Royal Towers: No struggle required', 1337148), LocationData('Royal towers', 'Royal Towers: Right tower freebie', 1337149), LocationData('Royal towers (upper)', 'Royal Towers: Left tower small balcony', 1337150), LocationData('Royal towers (upper)', 'Royal Towers: Left tower royal guard', 1337151), @@ -196,10 +196,10 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int], # Ancient pyramid locations LocationData('Ancient Pyramid (entrance)', 'Ancient Pyramid: Why not it\'s right there', 1337246), LocationData('Ancient Pyramid (left)', 'Ancient Pyramid: Conviction guarded room', 1337247), - LocationData('Ancient Pyramid (left)', 'Ancient Pyramid: Pit secret room', 1337248, lambda state: logic.can_break_walls(state) and (state.has('Water Mask', player) if flooded.flood_pyramid_shaft else True)), - LocationData('Ancient Pyramid (left)', 'Ancient Pyramid: Regret chest', 1337249, lambda state: logic.can_break_walls(state) and (state.has('Water Mask', player) if flooded.flood_pyramid_shaft else True)), - LocationData('Ancient Pyramid (right)', 'Ancient Pyramid: Nightmare Door chest', 1337236, lambda state: state.has('Water Mask', player) if flooded.flood_pyramid_back else True), - LocationData('Ancient Pyramid (right)', 'Killed Nightmare', EventId, lambda state: state.has_all({'Timespinner Wheel', 'Timespinner Spindle', 'Timespinner Gear 1', 'Timespinner Gear 2', 'Timespinner Gear 3'}, player) and (state.has('Water Mask', player) if flooded.flood_pyramid_back else True)) + LocationData('Ancient Pyramid (left)', 'Ancient Pyramid: Pit secret room', 1337248, lambda state: logic.can_break_walls(state) and (not flooded.flood_pyramid_shaft or state.has('Water Mask', player))), + LocationData('Ancient Pyramid (left)', 'Ancient Pyramid: Regret chest', 1337249, lambda state: logic.can_break_walls(state) and (not flooded.flood_pyramid_shaft or state.has('Water Mask', player))), + LocationData('Ancient Pyramid (right)', 'Ancient Pyramid: Nightmare Door chest', 1337236, lambda state: not flooded.flood_pyramid_back or state.has('Water Mask', player)), + LocationData('Ancient Pyramid (right)', 'Killed Nightmare', EventId, lambda state: state.has_all({'Timespinner Wheel', 'Timespinner Spindle', 'Timespinner Gear 1', 'Timespinner Gear 2', 'Timespinner Gear 3'}, player) and (not flooded.flood_pyramid_back or state.has('Water Mask', player))) ] # 1337156 - 1337170 Downloads @@ -244,15 +244,15 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int], LocationData('Emperors tower', 'Emperor\'s Tower: Memory - Way Up There (Final Circle)', 1337187, logic.has_doublejump_of_npc), LocationData('Forest', 'Forest: Journal - Rats (Lachiem Expedition)', 1337188), LocationData('Forest', 'Forest: Journal - Bat Jump Ledge (Peace Treaty)', 1337189, lambda state: logic.has_doublejump_of_npc(state) or logic.has_forwarddash_doublejump(state) or logic.has_fastjump_on_npc(state)), - LocationData('Forest', 'Forest: Journal - Floating in Moat (Prime Edicts)', 1337190, lambda state: state.has('Water Mask', player) if flooded.flood_moat else True), + LocationData('Forest', 'Forest: Journal - Floating in Moat (Prime Edicts)', 1337190, lambda state: not flooded.flood_moat or state.has('Water Mask', player)), LocationData('Castle Ramparts', 'Castle Ramparts: Journal - Archer + Knight (Declaration of Independence)', 1337191), LocationData('Castle Keep', 'Castle Keep: Journal - Under the Twins (Letter of Reference)', 1337192), LocationData('Castle Basement', 'Castle Basement: Journal - Castle Loop Giantess (Political Advice)', 1337193), LocationData('Royal towers (lower)', 'Royal Towers: Journal - Aelana\'s Room (Diplomatic Missive)', 1337194, logic.has_pink), LocationData('Royal towers (upper)', 'Royal Towers: Journal - Top Struggle Juggle Base (War of the Sisters)', 1337195), LocationData('Royal towers (upper)', 'Royal Towers: Journal - Aelana Boss (Stained Letter)', 1337196), - LocationData('Royal towers', 'Royal Towers: Journal - Near Bottom Struggle Juggle (Mission Findings)', 1337197, lambda state: logic.has_doublejump_of_npc(state) if not flooded.flood_courtyard else True), - LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Journal - Lower Left Caves (Naivety)', 1337198, lambda state: state.has('Water Mask', player) if flooded.flood_maw else True) + LocationData('Royal towers', 'Royal Towers: Journal - Near Bottom Struggle Juggle (Mission Findings)', 1337197, lambda state: flooded.flood_courtyard or logic.has_doublejump_of_npc(state)), + LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Journal - Lower Left Caves (Naivety)', 1337198, lambda state: not flooded.flood_maw or state.has('Water Mask', player)) ) # 1337199 - 1337236 Reserved for future use diff --git a/worlds/timespinner/Regions.py b/worlds/timespinner/Regions.py index 002606245d..905cae867e 100644 --- a/worlds/timespinner/Regions.py +++ b/worlds/timespinner/Regions.py @@ -138,12 +138,12 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w connect(world, player, 'Left Side forest Caves', 'Lower Lake Serene', lambda state: state.has('Water Mask', player) or flooded.dry_lake_serene) connect(world, player, 'Left Side forest Caves', 'Space time continuum', logic.has_teleport) connect(world, player, 'Upper Lake Serene', 'Left Side forest Caves') - connect(world, player, 'Upper Lake Serene', 'Lower Lake Serene', lambda state: state.has('Water Mask', player)) + connect(world, player, 'Upper Lake Serene', 'Lower Lake Serene', lambda state: state.has('Water Mask', player) or flooded.dry_lake_serene) connect(world, player, 'Lower Lake Serene', 'Upper Lake Serene') connect(world, player, 'Lower Lake Serene', 'Left Side forest Caves') - connect(world, player, 'Lower Lake Serene', 'Caves of Banishment (upper)') + connect(world, player, 'Lower Lake Serene', 'Caves of Banishment (upper)', lambda state: not flooded.dry_lake_serene or logic.has_doublejump(state)) connect(world, player, 'Caves of Banishment (upper)', 'Upper Lake Serene', lambda state: state.has('Water Mask', player) or flooded.dry_lake_serene) - connect(world, player, 'Caves of Banishment (upper)', 'Caves of Banishment (Maw)', lambda state: logic.has_doublejump(state) or state.has_any({'Gas Mask', 'Twin Pyramid Key'}, player)) + connect(world, player, 'Caves of Banishment (upper)', 'Caves of Banishment (Maw)', lambda state: logic.has_doublejump(state) or state.has_any({'Gas Mask', 'Talaria Attachment'} or logic.has_teleport(state), player)) connect(world, player, 'Caves of Banishment (upper)', 'Space time continuum', logic.has_teleport) connect(world, player, 'Caves of Banishment (Maw)', 'Caves of Banishment (upper)', lambda state: logic.has_doublejump(state) if not flooded.flood_maw else state.has('Water Mask', player)) connect(world, player, 'Caves of Banishment (Maw)', 'Caves of Banishment (Sirens)', lambda state: state.has_any({'Gas Mask', 'Talaria Attachment'}, player) ) From b7c74919b7c9f1e69d1a13d871f2c6acddd7525c Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Mon, 27 Mar 2023 12:42:15 -0500 Subject: [PATCH 126/172] Spoiler: add player name to sphere 0 items in playthrough (#1607) --- BaseClasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index bfc1c69d27..53fe407ecd 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1208,7 +1208,7 @@ class Spoiler(): raise RuntimeError(f'Not all required items reachable. Unreachable locations: {required_locations}') # we can finally output our playthrough - self.playthrough = {"0": sorted([str(item) for item in + self.playthrough = {"0": sorted([self.multiworld.get_name_string_for_object(item) for item in chain.from_iterable(multiworld.precollected_items.values()) if item.advancement])} From ae7dad8bf935e4006c03757e62e64224f43a856a Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Tue, 28 Mar 2023 12:02:06 -0400 Subject: [PATCH 127/172] Fixed Blacklist and python 3.8 support (#1616) --- worlds/kh2/OpenKH.py | 8 +++++--- worlds/kh2/Options.py | 5 +++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/worlds/kh2/OpenKH.py b/worlds/kh2/OpenKH.py index 284b9f9489..eb1a846e42 100644 --- a/worlds/kh2/OpenKH.py +++ b/worlds/kh2/OpenKH.py @@ -1,3 +1,5 @@ +import logging + import yaml import os import Utils @@ -61,13 +63,13 @@ def patch_kh2(self, output_directory): slotDataDuping = set() for values in CheckDupingItems.values(): if isinstance(values, set): - slotDataDuping |= values + slotDataDuping = slotDataDuping.union(values) else: for inner_values in values.values(): - slotDataDuping |= inner_values + slotDataDuping = slotDataDuping.union(inner_values) if self.multiworld.Keyblade_Minimum[self.player].value > self.multiworld.Keyblade_Maximum[self.player].value: - print( + logging.info( f"{self.multiworld.get_file_safe_player_name(self.player)} has Keyblade Minimum greater than Keyblade Maximum") keyblademin = self.multiworld.Keyblade_Maximum[self.player].value keyblademax = self.multiworld.Keyblade_Minimum[self.player].value diff --git a/worlds/kh2/Options.py b/worlds/kh2/Options.py index 671d85323a..90c2c7f709 100644 --- a/worlds/kh2/Options.py +++ b/worlds/kh2/Options.py @@ -1,6 +1,8 @@ from Options import Choice, Option, Range, Toggle, OptionSet import typing +from worlds.kh2 import SupportAbility_Table, ActionAbility_Table + class SoraEXP(Range): """Sora Level Exp Multiplier""" @@ -166,8 +168,7 @@ class KeybladeAbilities(Choice): class BlacklistKeyblade(OptionSet): """Black List these Abilities on Keyblades""" display_name = "Blacklist Keyblade Abilities" - verify_item_name = True - + valid_keys = set(SupportAbility_Table.keys()).union(ActionAbility_Table.keys()) class Goal(Choice): """Win Condition From b64565594a3699873a31fe3ee29d68a3655f6d4d Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 29 Mar 2023 05:25:29 +0200 Subject: [PATCH 128/172] kvui: block settings menu --- kvui.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/kvui.py b/kvui.py index effade0dd2..0107b48eda 100644 --- a/kvui.py +++ b/kvui.py @@ -488,6 +488,10 @@ class GameManager(App): if hasattr(self, "energy_link_label"): self.energy_link_label.text = f"EL: {Utils.format_SI_prefix(self.ctx.current_energy_link_value)}J" + # default F1 keybind, opens a settings menu, that seems to break the layout engine once closed + def open_settings(self, *largs): + pass + class LogtoUI(logging.Handler): def __init__(self, on_log): From f50e85b401993b0c71a5e1500a179838b6bdc713 Mon Sep 17 00:00:00 2001 From: zig-for Date: Wed, 29 Mar 2023 05:40:19 -0700 Subject: [PATCH 129/172] LADX: Fix repeated rupee adds overwriting instead of adding (#1618) --- .../ladx/LADXR/patches/bank3e.asm/chest.asm | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/worlds/ladx/LADXR/patches/bank3e.asm/chest.asm b/worlds/ladx/LADXR/patches/bank3e.asm/chest.asm index 717a2def1d..b19e879dc3 100644 --- a/worlds/ladx/LADXR/patches/bank3e.asm/chest.asm +++ b/worlds/ladx/LADXR/patches/bank3e.asm/chest.asm @@ -451,34 +451,35 @@ AddDungeonItem: ret AddRupees20: - xor a - ld h, $14 + ld hl, $0014 jr AddRupees AddRupees50: - xor a - ld h, $32 + ld hl, $0032 jr AddRupees AddRupees100: - xor a - ld h, $64 + ld hl, $0064 jr AddRupees AddRupees200: - xor a - ld h, $C8 + ld hl, $00C8 jr AddRupees AddRupees500: - ld a, $01 - ld h, $F4 + ld hl, $01F4 jr AddRupees AddRupees: - ld [$DB8F], a - ld a, h - ld [$DB90], a + ld a, [$DB8F] + ld d, a + ld a, [$DB90] + ld e, a + add hl, de + ld a, h + ld [$DB8F], a + ld a, l + ld [$DB90], a ld a, $18 ld [$C3CE], a ret From d14ab97849f1afd9b784b97694923070281a9e3b Mon Sep 17 00:00:00 2001 From: zig-for Date: Wed, 29 Mar 2023 05:41:11 -0700 Subject: [PATCH 130/172] LADX: Fix gen failures with non-ascii player names, fix missing custom (#1619) --- worlds/ladx/LADXR/locations/shop.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/worlds/ladx/LADXR/locations/shop.py b/worlds/ladx/LADXR/locations/shop.py index e3e05d941a..b68726665f 100644 --- a/worlds/ladx/LADXR/locations/shop.py +++ b/worlds/ladx/LADXR/locations/shop.py @@ -17,18 +17,24 @@ class ShopItem(ItemInfo): def patch(self, rom, option, *, multiworld=None): mw_text = "" if multiworld: - mw_text = f" for player {rom.player_names[multiworld - 1]}" + mw_text = f" for player {rom.player_names[multiworld - 1].encode('ascii', 'replace').decode()}" + + + if self.custom_item_name: + name = self.custom_item_name + else: + name = "{"+option+"}" if self.__index == 0: # Old index, maybe not needed any more rom.patch(0x04, 0x37C5, "08", "%02X" % (CHEST_ITEMS[option])) - rom.texts[0x030] = formatText(f"Deluxe {{%s}} 200 {{RUPEES}}{mw_text}!" % (option), ask="Buy No Way") + rom.texts[0x030] = formatText(f"Deluxe {name} 200 {{RUPEES}}{mw_text}!", ask="Buy No Way") rom.banks[0x3E][0x3800 + 0x2A1] = CHEST_ITEMS[option] if multiworld: rom.banks[0x3E][0x3300 + 0x2A1] = multiworld elif self.__index == 1: rom.patch(0x04, 0x37C6, "02", "%02X" % (CHEST_ITEMS[option])) - rom.texts[0x02C] = formatText(f"{{%s}} Only 980 {{RUPEES}}{mw_text}!" % (option), ask="Buy No Way") + rom.texts[0x02C] = formatText(f"{name} only 980 {{RUPEES}}{mw_text}!", ask="Buy No Way") rom.banks[0x3E][0x3800 + 0x2A7] = CHEST_ITEMS[option] if multiworld: From 99bd525c8e0ee174057765cb8a9b289fd55b6369 Mon Sep 17 00:00:00 2001 From: zig-for Date: Wed, 29 Mar 2023 05:42:08 -0700 Subject: [PATCH 131/172] LADX: use verify() to verify sprites (#1620) --- worlds/ladx/Options.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py index 57de7430a5..1300184129 100644 --- a/worlds/ladx/Options.py +++ b/worlds/ladx/Options.py @@ -324,17 +324,22 @@ class GfxMod(FreeText, LADXROption): if extension in self.extensions: GfxMod.__spriteFiles[name].append(file) + def verify(self, world, player_name: str, plando_options) -> None: + if self.value == "Link" or self.value in GfxMod.__spriteFiles: + return + raise Exception(f"LADX Sprite '{self.value}' not found. Possible sprites are: {['Link'] + list(GfxMod.__spriteFiles.keys())}") + def to_ladxr_option(self, all_options): if self.value == -1 or self.value == "Link": return None, None - elif self.value in GfxMod.__spriteFiles: - if len(GfxMod.__spriteFiles[self.value]) > 1: - logger.warning(f"{self.value} does not uniquely identify a file. Possible matches: {GfxMod.__spriteFiles[self.value]}. Using {GfxMod.__spriteFiles[self.value][0]}") - return self.ladxr_name, GfxMod.__spriteFiles[self.value][0] - return self.ladxr_name, GfxMod.__spriteFiles[self.value][0] - logger.error(f"Spritesheet {self.value} not found. Falling back to default sprite.") - return None, None + + assert self.value in GfxMod.__spriteFiles + + if len(GfxMod.__spriteFiles[self.value]) > 1: + logger.warning(f"{self.value} does not uniquely identify a file. Possible matches: {GfxMod.__spriteFiles[self.value]}. Using {GfxMod.__spriteFiles[self.value][0]}") + + return self.ladxr_name, GfxMod.__spriteFiles[self.value][0] class Palette(Choice): """ From 53b3cd029e5dcc2318eb5adb3e06bae4f48a5b73 Mon Sep 17 00:00:00 2001 From: zig-for Date: Wed, 29 Mar 2023 05:48:10 -0700 Subject: [PATCH 132/172] LADX: Add options for sword music and nag messages (#1621) --- worlds/ladx/LADXR/generator.py | 5 +++-- worlds/ladx/Options.py | 27 +++++++++++++++++---------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/worlds/ladx/LADXR/generator.py b/worlds/ladx/LADXR/generator.py index cf8e67eee6..28966ab763 100644 --- a/worlds/ladx/LADXR/generator.py +++ b/worlds/ladx/LADXR/generator.py @@ -56,7 +56,7 @@ from . import hints from .locations.keyLocation import KeyLocation from .patches import bank34 -from ..Options import TrendyGame, Palette +from ..Options import TrendyGame, Palette, MusicChangeCondition # Function to generate a final rom, this patches the rom with all required patches @@ -181,7 +181,8 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m # patches.reduceRNG.slowdownThreeOfAKind(rom) patches.reduceRNG.fixHorseHeads(rom) patches.bomb.onlyDropBombsWhenHaveBombs(rom) - # patches.aesthetics.noSwordMusic(rom) + if ap_settings['music_change_condition'] == MusicChangeCondition.option_always: + patches.aesthetics.noSwordMusic(rom) patches.aesthetics.reduceMessageLengths(rom, rnd) patches.aesthetics.allowColorDungeonSpritesEverywhere(rom) if settings.music == 'random': diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py index 1300184129..b14d7b2af0 100644 --- a/worlds/ladx/Options.py +++ b/worlds/ladx/Options.py @@ -180,17 +180,22 @@ class InstrumentCount(Range, LADXROption): range_end = 8 default = 8 -class ItemPool(Choice): - """Effects which items are shuffled. -[Casual] Places multiple copies of key items. -[More keys] Adds additional small/nightmare keys so that dungeons are faster. -[Path of Pain]. Adds negative heart containers to the item pool.""" - casual = 0 - more_keys = 1 - normal = 2 - painful = 3 - default = normal +class NagMessages(DefaultOffToggle, LADXROption): + """ + Controls if nag messages are shown when rocks and crystals are touched. Useful for glitches, annoying for everyone else. + """ + ladxr_name = "nagmessages" + +class MusicChangeCondition(Choice): + """ + Controls how the music changes. + [Sword] When you pick up a sword, the music changes + [Always] You always have the post-sword music + """ + option_sword = 0 + option_always = 1 + default = option_always # Setting('hpmode', 'Gameplay', 'm', 'Health mode', options=[('default', '', 'Normal'), ('inverted', 'i', 'Inverted'), ('1', '1', 'Start with 1 heart'), ('low', 'l', 'Low max')], default='default', # description=""" # [Normal} health works as you would expect. @@ -388,4 +393,6 @@ links_awakening_options: typing.Dict[str, typing.Type[Option]] = { 'shuffle_maps': ShuffleMaps, 'shuffle_compasses': ShuffleCompasses, 'shuffle_stone_beaks': ShuffleStoneBeaks, + 'music_change_condition': MusicChangeCondition, + 'nag_messages': NagMessages } From bb79073ce7b7fbf3e746baa443588416f731a0f5 Mon Sep 17 00:00:00 2001 From: zig-for Date: Wed, 29 Mar 2023 05:56:10 -0700 Subject: [PATCH 133/172] LADX: Fix autotracking for shop items (#1623) --- worlds/ladx/Tracker.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/worlds/ladx/Tracker.py b/worlds/ladx/Tracker.py index b3995db019..b85086298a 100644 --- a/worlds/ladx/Tracker.py +++ b/worlds/ladx/Tracker.py @@ -160,25 +160,26 @@ class MagpieBridge: await self.send_all_inventory() if "checks" in message["features"]: await self.send_all_checks() + # Translate renamed IDs back to LADXR IDs + @staticmethod + def fixup_id(the_id): + if the_id == "0x2A1": + return "0x2A1-0" + if the_id == "0x2A7": + return "0x2A1-1" + return the_id async def send_all_checks(self): while self.checks == None: await asyncio.sleep(0.1) logger.info("sending all checks to magpie") - # Translate renamed IDs back to LADXR IDs - def fixup_id(the_id): - if the_id == "0x2A1": - return "0x2A1-0" - if the_id == "0x2A7": - return "0x2A1-1" - return the_id message = { "type": "check", "refresh": True, "version": "1.0", "diff": False, - "checks": [{"id": fixup_id(check.id), "checked": check.value} for check in self.checks] + "checks": [{"id": self.fixup_id(check.id), "checked": check.value} for check in self.checks] } await self.ws.send(json.dumps(message)) @@ -193,7 +194,7 @@ class MagpieBridge: "refresh": True, "version": "1.0", "diff": True, - "checks": [{"id": check, "checked": True} for check in checks] + "checks": [{"id": self.fixup_id(check), "checked": True} for check in checks] } await self.ws.send(json.dumps(message)) From 639606e0bec1d408bb14ba37515c23b0adbb756d Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Wed, 29 Mar 2023 20:14:45 +0200 Subject: [PATCH 134/172] worlds,clients,kvui: use user_path (#1622) --- OoTClient.py | 5 ++++- Utils.py | 5 ++++- kvui.py | 2 +- worlds/pokemon_rb/rom.py | 2 +- worlds/tloz/Rom.py | 4 ++-- 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/OoTClient.py b/OoTClient.py index 696bf39016..f8a052402f 100644 --- a/OoTClient.py +++ b/OoTClient.py @@ -289,7 +289,10 @@ async def patch_and_run_game(apz5_file): decomp_path = base_name + '-decomp.z64' comp_path = base_name + '.z64' # Load vanilla ROM, patch file, compress ROM - rom = Rom(Utils.local_path(Utils.get_options()["oot_options"]["rom_file"])) + rom_file_name = Utils.get_options()["oot_options"]["rom_file"] + if not os.path.exists(rom_file_name): + rom_file_name = Utils.user_path(rom_file_name) + rom = Rom(rom_file_name) apply_patch_file(rom, apz5_file, sub_file=(os.path.basename(base_name) + '.zpf' if zipfile.is_zipfile(apz5_file) diff --git a/Utils.py b/Utils.py index e2ff19310b..fb09061971 100644 --- a/Utils.py +++ b/Utils.py @@ -88,7 +88,10 @@ def is_frozen() -> bool: def local_path(*path: str) -> str: - """Returns path to a file in the local Archipelago installation or source.""" + """ + Returns path to a file in the local Archipelago installation or source. + This might be read-only and user_path should be used instead for ROMs, configuration, etc. + """ if hasattr(local_path, 'cached_path'): pass elif is_frozen(): diff --git a/kvui.py b/kvui.py index 0107b48eda..66da8c16a3 100644 --- a/kvui.py +++ b/kvui.py @@ -612,7 +612,7 @@ class KivyJSONtoTextParser(JSONtoTextParser): ExceptionManager.add_handler(E()) Builder.load_file(Utils.local_path("data", "client.kv")) -user_file = Utils.local_path("data", "user.kv") +user_file = Utils.user_path("data", "user.kv") if os.path.exists(user_file): logging.info("Loading user.kv into builder.") Builder.load_file(user_file) diff --git a/worlds/pokemon_rb/rom.py b/worlds/pokemon_rb/rom.py index 9eb670b635..fbb0ec41d9 100644 --- a/worlds/pokemon_rb/rom.py +++ b/worlds/pokemon_rb/rom.py @@ -955,7 +955,7 @@ def get_base_rom_path(game_version: str) -> str: options = Utils.get_options() file_name = options["pokemon_rb_options"][f"{game_version}_rom_file"] if not os.path.exists(file_name): - file_name = Utils.local_path(file_name) + file_name = Utils.user_path(file_name) return file_name diff --git a/worlds/tloz/Rom.py b/worlds/tloz/Rom.py index 0eaf5855d1..0be96d664a 100644 --- a/worlds/tloz/Rom.py +++ b/worlds/tloz/Rom.py @@ -74,5 +74,5 @@ def get_base_rom_path(file_name: str = "") -> str: if not file_name: file_name = options["tloz_options"]["rom_file"] if not os.path.exists(file_name): - file_name = Utils.local_path(file_name) - return file_name \ No newline at end of file + file_name = Utils.user_path(file_name) + return file_name From a5373e367231c528ba690be840c567c055a0b47d Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Wed, 29 Mar 2023 17:37:39 -0400 Subject: [PATCH 135/172] [WebHost] Add support for items-list, locations-list, and custom-list option types to weighted-settings (#1614) * Add support for items-list, locations-list, and custom-list option types to weighted-settings * Fix subclass detection for `items-list`, `locations-list`, and `custom-list` in weighted-settings * Move specially handled options alongside each other in the UI, and split them into logical groupings * Fix header text and location for Priority an Exclusion locations * Add universally supported "random" option to `Choice` and `TextChoice` options in weighted-settings * Update description text for exclusion items to clarify they also prevent helpful items. * Be technically correct and call them `useful` items. --- WebHostLib/options.py | 20 +- WebHostLib/static/assets/weighted-settings.js | 278 ++++++++++++++---- .../static/styles/weighted-settings.css | 58 ++-- 3 files changed, 271 insertions(+), 85 deletions(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 8f366d4fbf..942f9e8260 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -11,7 +11,7 @@ from Utils import __version__, local_path from worlds.AutoWorld import AutoWorldRegister handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hints", "start_location_hints", - "exclude_locations"} + "exclude_locations", "priority_locations"} def create(): @@ -88,7 +88,7 @@ def create(): if option_name in handled_in_js: pass - elif option.options: + elif issubclass(option, Options.Choice) or issubclass(option, Options.TextChoice): game_options[option_name] = this_option = { "type": "select", "displayName": option.display_name if hasattr(option, "display_name") else option_name, @@ -97,6 +97,7 @@ def create(): "options": [] } + has_random_option = False for sub_option_id, sub_option_name in option.name_lookup.items(): this_option["options"].append({ "name": option.get_option_name(sub_option_id), @@ -106,6 +107,15 @@ def create(): if sub_option_id == option.default: this_option["defaultValue"] = sub_option_name + if sub_option_name == "random": + has_random_option = True + + if not has_random_option: + this_option["options"].append({ + "name": "random", + "value": 'random', + }) + if option.default == "random": this_option["defaultValue"] = "random" @@ -126,21 +136,21 @@ def create(): for key, val in option.special_range_names.items(): game_options[option_name]["value_names"][key] = val - elif getattr(option, "verify_item_name", False): + elif issubclass(option, Options.ItemSet): game_options[option_name] = { "type": "items-list", "displayName": option.display_name if hasattr(option, "display_name") else option_name, "description": get_html_doc(option), } - elif getattr(option, "verify_location_name", False): + elif issubclass(option, Options.LocationSet): game_options[option_name] = { "type": "locations-list", "displayName": option.display_name if hasattr(option, "display_name") else option_name, "description": get_html_doc(option), } - elif issubclass(option, Options.OptionList) or issubclass(option, Options.OptionSet): + elif issubclass(option, Options.VerifyKeys): if option.valid_keys: game_options[option_name] = { "type": "custom-list", diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js index 68e950ea4d..e914197d4c 100644 --- a/WebHostLib/static/assets/weighted-settings.js +++ b/WebHostLib/static/assets/weighted-settings.js @@ -101,6 +101,7 @@ const createDefaultSettings = (settingData) => { newSettings[game].start_inventory = {}; newSettings[game].exclude_locations = []; + newSettings[game].priority_locations = []; newSettings[game].local_items = []; newSettings[game].non_local_items = []; newSettings[game].start_hints = []; @@ -136,21 +137,28 @@ const buildUI = (settingData) => { expandButton.classList.add('invisible'); gameDiv.appendChild(expandButton); - const weightedSettingsDiv = buildWeightedSettingsDiv(game, settingData.games[game].gameSettings); + settingData.games[game].gameItems.sort((a, b) => (a > b ? 1 : (a < b ? -1 : 0))); + settingData.games[game].gameLocations.sort((a, b) => (a > b ? 1 : (a < b ? -1 : 0))); + + const weightedSettingsDiv = buildWeightedSettingsDiv(game, settingData.games[game].gameSettings, + settingData.games[game].gameItems, settingData.games[game].gameLocations); gameDiv.appendChild(weightedSettingsDiv); - const itemsDiv = buildItemsDiv(game, settingData.games[game].gameItems); - gameDiv.appendChild(itemsDiv); + const itemPoolDiv = buildItemsDiv(game, settingData.games[game].gameItems); + gameDiv.appendChild(itemPoolDiv); const hintsDiv = buildHintsDiv(game, settingData.games[game].gameItems, settingData.games[game].gameLocations); gameDiv.appendChild(hintsDiv); + const locationsDiv = buildLocationsDiv(game, settingData.games[game].gameLocations); + gameDiv.appendChild(locationsDiv); + gamesWrapper.appendChild(gameDiv); collapseButton.addEventListener('click', () => { collapseButton.classList.add('invisible'); weightedSettingsDiv.classList.add('invisible'); - itemsDiv.classList.add('invisible'); + itemPoolDiv.classList.add('invisible'); hintsDiv.classList.add('invisible'); expandButton.classList.remove('invisible'); }); @@ -158,7 +166,7 @@ const buildUI = (settingData) => { expandButton.addEventListener('click', () => { collapseButton.classList.remove('invisible'); weightedSettingsDiv.classList.remove('invisible'); - itemsDiv.classList.remove('invisible'); + itemPoolDiv.classList.remove('invisible'); hintsDiv.classList.remove('invisible'); expandButton.classList.add('invisible'); }); @@ -226,7 +234,7 @@ const buildGameChoice = (games) => { gameChoiceDiv.appendChild(table); }; -const buildWeightedSettingsDiv = (game, settings) => { +const buildWeightedSettingsDiv = (game, settings, gameItems, gameLocations) => { const currentSettings = JSON.parse(localStorage.getItem('weighted-settings')); const settingsWrapper = document.createElement('div'); settingsWrapper.classList.add('settings-wrapper'); @@ -268,7 +276,7 @@ const buildWeightedSettingsDiv = (game, settings) => { range.setAttribute('data-type', setting.type); range.setAttribute('min', 0); range.setAttribute('max', 50); - range.addEventListener('change', updateGameSetting); + range.addEventListener('change', updateRangeSetting); range.value = currentSettings[game][settingName][option.value]; tdMiddle.appendChild(range); tr.appendChild(tdMiddle); @@ -309,7 +317,7 @@ const buildWeightedSettingsDiv = (game, settings) => { range.setAttribute('data-option', i); range.setAttribute('min', 0); range.setAttribute('max', 50); - range.addEventListener('change', updateGameSetting); + range.addEventListener('change', updateRangeSetting); range.value = currentSettings[game][settingName][i] || 0; tdMiddle.appendChild(range); tr.appendChild(tdMiddle); @@ -377,7 +385,7 @@ const buildWeightedSettingsDiv = (game, settings) => { range.setAttribute('data-option', option); range.setAttribute('min', 0); range.setAttribute('max', 50); - range.addEventListener('change', updateGameSetting); + range.addEventListener('change', updateRangeSetting); range.value = currentSettings[game][settingName][parseInt(option, 10)]; tdMiddle.appendChild(range); tr.appendChild(tdMiddle); @@ -428,7 +436,7 @@ const buildWeightedSettingsDiv = (game, settings) => { range.setAttribute('data-option', option); range.setAttribute('min', 0); range.setAttribute('max', 50); - range.addEventListener('change', updateGameSetting); + range.addEventListener('change', updateRangeSetting); range.value = currentSettings[game][settingName][parseInt(option, 10)]; tdMiddle.appendChild(range); tr.appendChild(tdMiddle); @@ -475,7 +483,7 @@ const buildWeightedSettingsDiv = (game, settings) => { range.setAttribute('data-option', option); range.setAttribute('min', 0); range.setAttribute('max', 50); - range.addEventListener('change', updateGameSetting); + range.addEventListener('change', updateRangeSetting); range.value = currentSettings[game][settingName][option]; tdMiddle.appendChild(range); tr.appendChild(tdMiddle); @@ -493,15 +501,108 @@ const buildWeightedSettingsDiv = (game, settings) => { break; case 'items-list': - // TODO + const itemsList = document.createElement('div'); + itemsList.classList.add('simple-list'); + + Object.values(gameItems).forEach((item) => { + const itemRow = document.createElement('div'); + itemRow.classList.add('list-row'); + + const itemLabel = document.createElement('label'); + itemLabel.setAttribute('for', `${game}-${settingName}-${item}`) + + const itemCheckbox = document.createElement('input'); + itemCheckbox.setAttribute('id', `${game}-${settingName}-${item}`); + itemCheckbox.setAttribute('type', 'checkbox'); + itemCheckbox.setAttribute('data-game', game); + itemCheckbox.setAttribute('data-setting', settingName); + itemCheckbox.setAttribute('data-option', item.toString()); + itemCheckbox.addEventListener('change', updateListSetting); + if (currentSettings[game][settingName].includes(item)) { + itemCheckbox.setAttribute('checked', '1'); + } + + const itemName = document.createElement('span'); + itemName.innerText = item.toString(); + + itemLabel.appendChild(itemCheckbox); + itemLabel.appendChild(itemName); + + itemRow.appendChild(itemLabel); + itemsList.appendChild((itemRow)); + }); + + settingWrapper.appendChild(itemsList); break; case 'locations-list': - // TODO + const locationsList = document.createElement('div'); + locationsList.classList.add('simple-list'); + + Object.values(gameLocations).forEach((location) => { + const locationRow = document.createElement('div'); + locationRow.classList.add('list-row'); + + const locationLabel = document.createElement('label'); + locationLabel.setAttribute('for', `${game}-${settingName}-${location}`) + + const locationCheckbox = document.createElement('input'); + locationCheckbox.setAttribute('id', `${game}-${settingName}-${location}`); + locationCheckbox.setAttribute('type', 'checkbox'); + locationCheckbox.setAttribute('data-game', game); + locationCheckbox.setAttribute('data-setting', settingName); + locationCheckbox.setAttribute('data-option', location.toString()); + locationCheckbox.addEventListener('change', updateListSetting); + if (currentSettings[game][settingName].includes(location)) { + locationCheckbox.setAttribute('checked', '1'); + } + + const locationName = document.createElement('span'); + locationName.innerText = location.toString(); + + locationLabel.appendChild(locationCheckbox); + locationLabel.appendChild(locationName); + + locationRow.appendChild(locationLabel); + locationsList.appendChild((locationRow)); + }); + + settingWrapper.appendChild(locationsList); break; case 'custom-list': - // TODO + const customList = document.createElement('div'); + customList.classList.add('simple-list'); + + Object.values(settings[settingName].options).forEach((listItem) => { + const customListRow = document.createElement('div'); + customListRow.classList.add('list-row'); + + const customItemLabel = document.createElement('label'); + customItemLabel.setAttribute('for', `${game}-${settingName}-${listItem}`) + + const customItemCheckbox = document.createElement('input'); + customItemCheckbox.setAttribute('id', `${game}-${settingName}-${listItem}`); + customItemCheckbox.setAttribute('type', 'checkbox'); + customItemCheckbox.setAttribute('data-game', game); + customItemCheckbox.setAttribute('data-setting', settingName); + customItemCheckbox.setAttribute('data-option', listItem.toString()); + customItemCheckbox.addEventListener('change', updateListSetting); + if (currentSettings[game][settingName].includes(listItem)) { + customItemCheckbox.setAttribute('checked', '1'); + } + + const customItemName = document.createElement('span'); + customItemName.innerText = listItem.toString(); + + customItemLabel.appendChild(customItemCheckbox); + customItemLabel.appendChild(customItemName); + + customListRow.appendChild(customItemLabel); + customList.appendChild((customListRow)); + }); + + settingWrapper.appendChild(customList); break; default: @@ -727,21 +828,22 @@ const buildHintsDiv = (game, items, locations) => { const hintsDescription = document.createElement('p'); hintsDescription.classList.add('setting-description'); hintsDescription.innerText = 'Choose any items or locations to begin the game with the knowledge of where those ' + - ' items are, or what those locations contain. Excluded locations will not contain progression items.'; + ' items are, or what those locations contain.'; hintsDiv.appendChild(hintsDescription); const itemHintsContainer = document.createElement('div'); itemHintsContainer.classList.add('hints-container'); + // Item Hints const itemHintsWrapper = document.createElement('div'); itemHintsWrapper.classList.add('hints-wrapper'); itemHintsWrapper.innerText = 'Starting Item Hints'; const itemHintsDiv = document.createElement('div'); - itemHintsDiv.classList.add('item-container'); + itemHintsDiv.classList.add('simple-list'); items.forEach((item) => { - const itemDiv = document.createElement('div'); - itemDiv.classList.add('hint-div'); + const itemRow = document.createElement('div'); + itemRow.classList.add('list-row'); const itemLabel = document.createElement('label'); itemLabel.setAttribute('for', `${game}-start_hints-${item}`); @@ -755,29 +857,30 @@ const buildHintsDiv = (game, items, locations) => { if (currentSettings[game].start_hints.includes(item)) { itemCheckbox.setAttribute('checked', 'true'); } - itemCheckbox.addEventListener('change', hintChangeHandler); + itemCheckbox.addEventListener('change', updateListSetting); itemLabel.appendChild(itemCheckbox); const itemName = document.createElement('span'); itemName.innerText = item; itemLabel.appendChild(itemName); - itemDiv.appendChild(itemLabel); - itemHintsDiv.appendChild(itemDiv); + itemRow.appendChild(itemLabel); + itemHintsDiv.appendChild(itemRow); }); itemHintsWrapper.appendChild(itemHintsDiv); itemHintsContainer.appendChild(itemHintsWrapper); + // Starting Location Hints const locationHintsWrapper = document.createElement('div'); locationHintsWrapper.classList.add('hints-wrapper'); locationHintsWrapper.innerText = 'Starting Location Hints'; const locationHintsDiv = document.createElement('div'); - locationHintsDiv.classList.add('item-container'); + locationHintsDiv.classList.add('simple-list'); locations.forEach((location) => { - const locationDiv = document.createElement('div'); - locationDiv.classList.add('hint-div'); + const locationRow = document.createElement('div'); + locationRow.classList.add('list-row'); const locationLabel = document.createElement('label'); locationLabel.setAttribute('for', `${game}-start_location_hints-${location}`); @@ -791,29 +894,89 @@ const buildHintsDiv = (game, items, locations) => { if (currentSettings[game].start_location_hints.includes(location)) { locationCheckbox.setAttribute('checked', '1'); } - locationCheckbox.addEventListener('change', hintChangeHandler); + locationCheckbox.addEventListener('change', updateListSetting); locationLabel.appendChild(locationCheckbox); const locationName = document.createElement('span'); locationName.innerText = location; locationLabel.appendChild(locationName); - locationDiv.appendChild(locationLabel); - locationHintsDiv.appendChild(locationDiv); + locationRow.appendChild(locationLabel); + locationHintsDiv.appendChild(locationRow); }); locationHintsWrapper.appendChild(locationHintsDiv); itemHintsContainer.appendChild(locationHintsWrapper); + hintsDiv.appendChild(itemHintsContainer); + return hintsDiv; +}; + +const buildLocationsDiv = (game, locations) => { + const currentSettings = JSON.parse(localStorage.getItem('weighted-settings')); + locations.sort(); // Sort alphabetical, in-place + + const locationsDiv = document.createElement('div'); + locationsDiv.classList.add('locations-div'); + const locationsHeader = document.createElement('h3'); + locationsHeader.innerText = 'Priority & Exclusion Locations'; + locationsDiv.appendChild(locationsHeader); + const locationsDescription = document.createElement('p'); + locationsDescription.classList.add('setting-description'); + locationsDescription.innerText = 'Priority locations guarantee a progression item will be placed there while ' + + 'excluded locations will not contain progression or useful items.'; + locationsDiv.appendChild(locationsDescription); + + const locationsContainer = document.createElement('div'); + locationsContainer.classList.add('locations-container'); + + // Priority Locations + const priorityLocationsWrapper = document.createElement('div'); + priorityLocationsWrapper.classList.add('locations-wrapper'); + priorityLocationsWrapper.innerText = 'Priority Locations'; + + const priorityLocationsDiv = document.createElement('div'); + priorityLocationsDiv.classList.add('simple-list'); + locations.forEach((location) => { + const locationRow = document.createElement('div'); + locationRow.classList.add('list-row'); + + const locationLabel = document.createElement('label'); + locationLabel.setAttribute('for', `${game}-priority_locations-${location}`); + + const locationCheckbox = document.createElement('input'); + locationCheckbox.setAttribute('type', 'checkbox'); + locationCheckbox.setAttribute('id', `${game}-priority_locations-${location}`); + locationCheckbox.setAttribute('data-game', game); + locationCheckbox.setAttribute('data-setting', 'priority_locations'); + locationCheckbox.setAttribute('data-option', location); + if (currentSettings[game].priority_locations.includes(location)) { + locationCheckbox.setAttribute('checked', '1'); + } + locationCheckbox.addEventListener('change', updateListSetting); + locationLabel.appendChild(locationCheckbox); + + const locationName = document.createElement('span'); + locationName.innerText = location; + locationLabel.appendChild(locationName); + + locationRow.appendChild(locationLabel); + priorityLocationsDiv.appendChild(locationRow); + }); + + priorityLocationsWrapper.appendChild(priorityLocationsDiv); + locationsContainer.appendChild(priorityLocationsWrapper); + + // Exclude Locations const excludeLocationsWrapper = document.createElement('div'); - excludeLocationsWrapper.classList.add('hints-wrapper'); + excludeLocationsWrapper.classList.add('locations-wrapper'); excludeLocationsWrapper.innerText = 'Exclude Locations'; const excludeLocationsDiv = document.createElement('div'); - excludeLocationsDiv.classList.add('item-container'); + excludeLocationsDiv.classList.add('simple-list'); locations.forEach((location) => { - const locationDiv = document.createElement('div'); - locationDiv.classList.add('hint-div'); + const locationRow = document.createElement('div'); + locationRow.classList.add('list-row'); const locationLabel = document.createElement('label'); locationLabel.setAttribute('for', `${game}-exclude_locations-${location}`); @@ -827,40 +990,22 @@ const buildHintsDiv = (game, items, locations) => { if (currentSettings[game].exclude_locations.includes(location)) { locationCheckbox.setAttribute('checked', '1'); } - locationCheckbox.addEventListener('change', hintChangeHandler); + locationCheckbox.addEventListener('change', updateListSetting); locationLabel.appendChild(locationCheckbox); const locationName = document.createElement('span'); locationName.innerText = location; locationLabel.appendChild(locationName); - locationDiv.appendChild(locationLabel); - excludeLocationsDiv.appendChild(locationDiv); + locationRow.appendChild(locationLabel); + excludeLocationsDiv.appendChild(locationRow); }); excludeLocationsWrapper.appendChild(excludeLocationsDiv); - itemHintsContainer.appendChild(excludeLocationsWrapper); + locationsContainer.appendChild(excludeLocationsWrapper); - hintsDiv.appendChild(itemHintsContainer); - return hintsDiv; -}; - -const hintChangeHandler = (evt) => { - const currentSettings = JSON.parse(localStorage.getItem('weighted-settings')); - const game = evt.target.getAttribute('data-game'); - const setting = evt.target.getAttribute('data-setting'); - const option = evt.target.getAttribute('data-option'); - - if (evt.target.checked) { - if (!currentSettings[game][setting].includes(option)) { - currentSettings[game][setting].push(option); - } - } else { - if (currentSettings[game][setting].includes(option)) { - currentSettings[game][setting].splice(currentSettings[game][setting].indexOf(option), 1); - } - } - localStorage.setItem('weighted-settings', JSON.stringify(currentSettings)); + locationsDiv.appendChild(locationsContainer); + return locationsDiv; }; const updateVisibleGames = () => { @@ -906,13 +1051,12 @@ const updateBaseSetting = (event) => { localStorage.setItem('weighted-settings', JSON.stringify(settings)); }; -const updateGameSetting = (evt) => { +const updateRangeSetting = (evt) => { const options = JSON.parse(localStorage.getItem('weighted-settings')); const game = evt.target.getAttribute('data-game'); const setting = evt.target.getAttribute('data-setting'); const option = evt.target.getAttribute('data-option'); document.getElementById(`${game}-${setting}-${option}`).innerText = evt.target.value; - console.log(event); if (evt.action && evt.action === 'rangeDelete') { delete options[game][setting][option]; } else { @@ -921,6 +1065,26 @@ const updateGameSetting = (evt) => { localStorage.setItem('weighted-settings', JSON.stringify(options)); }; +const updateListSetting = (evt) => { + const options = JSON.parse(localStorage.getItem('weighted-settings')); + const game = evt.target.getAttribute('data-game'); + const setting = evt.target.getAttribute('data-setting'); + const option = evt.target.getAttribute('data-option'); + + if (evt.target.checked) { + // If the option is to be enabled and it is already enabled, do nothing + if (options[game][setting].includes(option)) { return; } + + options[game][setting].push(option); + } else { + // If the option is to be disabled and it is already disabled, do nothing + if (!options[game][setting].includes(option)) { return; } + + options[game][setting].splice(options[game][setting].indexOf(option), 1); + } + localStorage.setItem('weighted-settings', JSON.stringify(options)); +}; + const updateItemSetting = (evt) => { const options = JSON.parse(localStorage.getItem('weighted-settings')); const game = evt.target.getAttribute('data-game'); diff --git a/WebHostLib/static/styles/weighted-settings.css b/WebHostLib/static/styles/weighted-settings.css index 7639fa1c72..cc5231634e 100644 --- a/WebHostLib/static/styles/weighted-settings.css +++ b/WebHostLib/static/styles/weighted-settings.css @@ -157,41 +157,29 @@ html{ background-color: rgba(0, 0, 0, 0.1); } -#weighted-settings .hints-div{ +#weighted-settings .hints-div, #weighted-settings .locations-div{ margin-top: 2rem; } -#weighted-settings .hints-div h3{ +#weighted-settings .hints-div h3, #weighted-settings .locations-div h3{ margin-bottom: 0.5rem; } -#weighted-settings .hints-div .hints-container{ +#weighted-settings .hints-container, #weighted-settings .locations-container{ display: flex; flex-direction: row; justify-content: space-between; +} + +#weighted-settings .hints-wrapper, #weighted-settings .locations-wrapper{ + width: calc(50% - 0.5rem); font-weight: bold; } -#weighted-settings .hints-div .hints-wrapper{ - width: 32.5%; -} - -#weighted-settings .hints-div .hints-wrapper .hint-div{ - display: flex; - flex-direction: row; - cursor: pointer; - user-select: none; - -moz-user-select: none; -} - -#weighted-settings .hints-div .hints-wrapper .hint-div:hover{ - background-color: rgba(0, 0, 0, 0.1); -} - -#weighted-settings .hints-div .hints-wrapper .hint-div label{ - flex-grow: 1; - padding: 0.125rem 0.5rem; - cursor: pointer; +#weighted-settings .hints-wrapper .simple-list, #weighted-settings .locations-wrapper .simple-list{ + margin-top: 0.25rem; + height: 300px; + font-weight: normal; } #weighted-settings #weighted-settings-button-row{ @@ -280,6 +268,30 @@ html{ flex-direction: column; } +#weighted-settings .simple-list{ + display: flex; + flex-direction: column; + + max-height: 300px; + overflow-y: auto; + border: 1px solid #ffffff; + border-radius: 4px; +} + +#weighted-settings .simple-list .list-row label{ + display: block; + width: calc(100% - 0.5rem); + padding: 0.0625rem 0.25rem; +} + +#weighted-settings .simple-list .list-row label:hover{ + background-color: rgba(0, 0, 0, 0.1); +} + +#weighted-settings .simple-list .list-row label input[type=checkbox]{ + margin-right: 0.5rem; +} + #weighted-settings .invisible{ display: none; } From f3dad894ec3db37698c6f5b81450e8b605f3ef04 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Wed, 29 Mar 2023 21:27:35 -0400 Subject: [PATCH 136/172] Update ArchipIDLE item count to 200, and add a few more items (#1627) --- worlds/archipidle/Items.py | 9 +++++++++ worlds/archipidle/Rules.py | 14 ++++++++++---- worlds/archipidle/__init__.py | 10 +++++----- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/worlds/archipidle/Items.py b/worlds/archipidle/Items.py index 945d3aae60..2b5e6e9a81 100644 --- a/worlds/archipidle/Items.py +++ b/worlds/archipidle/Items.py @@ -300,4 +300,13 @@ item_table = ( 'Roomba with a Knife', 'Wet Cat', 'The missing moderator, Frostwares', + '1,793 Crossbows', + 'Holographic First Edition Charizard (Gen 1)', + 'VR Headset', + 'Archipelago 1.0 Release Date', + 'Strand of Galadriel\'s Hair', + 'Can of Meow-Mix', + 'Shake-Weight', + 'DVD Collection of Billy Mays Infomercials', + 'Old CD Key', ) diff --git a/worlds/archipidle/Rules.py b/worlds/archipidle/Rules.py index ddf906c21a..cdd48e7604 100644 --- a/worlds/archipidle/Rules.py +++ b/worlds/archipidle/Rules.py @@ -16,22 +16,28 @@ class ArchipIDLELogic(LogicMixin): def set_rules(world: MultiWorld, player: int): for i in range(16, 31): set_rule( - world.get_location(f"IDLE for at least {int(i / 2)} minutes {30 if (i % 2) else 0} seconds", player), + world.get_location(f"IDLE item number {i}", player), lambda state: state._archipidle_location_is_accessible(player, 4) ) for i in range(31, 51): set_rule( - world.get_location(f"IDLE for at least {int(i / 2)} minutes {30 if (i % 2) else 0} seconds", player), + world.get_location(f"IDLE item number {i}", player), lambda state: state._archipidle_location_is_accessible(player, 10) ) for i in range(51, 101): set_rule( - world.get_location(f"IDLE for at least {int(i / 2)} minutes {30 if (i % 2) else 0} seconds", player), + world.get_location(f"IDLE item number {i}", player), lambda state: state._archipidle_location_is_accessible(player, 20) ) + for i in range(101, 201): + set_rule( + world.get_location(f"IDLE item number {i}", player), + lambda state: state._archipidle_location_is_accessible(player, 40) + ) + world.completion_condition[player] =\ lambda state:\ - state.can_reach(world.get_location("IDLE for at least 50 minutes 0 seconds", player), "Location", player) + state.can_reach(world.get_location("IDLE item number 200", player), "Location", player) diff --git a/worlds/archipidle/__init__.py b/worlds/archipidle/__init__.py index 5054872dbe..dc980f13a3 100644 --- a/worlds/archipidle/__init__.py +++ b/worlds/archipidle/__init__.py @@ -25,7 +25,7 @@ class ArchipIDLEWorld(World): """ game = "ArchipIDLE" topology_present = False - data_version = 4 + data_version = 5 hidden = (datetime.now().month != 4) # ArchipIDLE is only visible during April web = ArchipIDLEWebWorld() @@ -37,8 +37,8 @@ class ArchipIDLEWorld(World): location_name_to_id = {} start_id = 9000 - for i in range(1, 101): - location_name_to_id[f"IDLE for at least {int(i / 2)} minutes {30 if (i % 2) else 0} seconds"] = start_id + for i in range(1, 201): + location_name_to_id[f"IDLE item number {i}"] = start_id start_id += 1 def generate_basic(self): @@ -46,10 +46,10 @@ class ArchipIDLEWorld(World): self.multiworld.random.shuffle(item_table_copy) item_pool = [] - for i in range(100): + for i in range(200): item = ArchipIDLEItem( item_table_copy[i], - ItemClassification.progression if i < 20 else ItemClassification.filler, + ItemClassification.progression if i < 40 else ItemClassification.filler, self.item_name_to_id[item_table_copy[i]], self.player ) From 4ff282a384be64f00bb1e6a308d01491e1cbeeeb Mon Sep 17 00:00:00 2001 From: t3hf1gm3nt <59876300+t3hf1gm3nt@users.noreply.github.com> Date: Thu, 30 Mar 2023 09:29:06 -0400 Subject: [PATCH 137/172] [TLOZ] Pols Voice Logic Fix (#1630) * TLOZ: Pols Voice Logic Fix Was informed that Pols Voice require certain weapons to kill that might not be guaranteed by the starting weapon. This is the only regular enemy in the game that cannot be killed by either any of the starting weapons or bombs which can be bought, so adding this rule should prevent any issues. --- worlds/tloz/Rules.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/worlds/tloz/Rules.py b/worlds/tloz/Rules.py index c73ea470b5..1e66e5a849 100644 --- a/worlds/tloz/Rules.py +++ b/worlds/tloz/Rules.py @@ -33,9 +33,11 @@ def set_rules(tloz_world: "TLoZWorld"): (state.has("Blue Ring", player) and state.has("Heart Container", player, int(hearts / 2))) or (state.has("Red Ring", player) and - state.has("Heart Container", player, int(hearts / 4))) + state.has("Heart Container", player, int(hearts / 4)))) + if "Pols Voice" in location.name: # This enemy needs specific weapons + add_rule(world.get_location(location.name, player), + lambda state: state.has_group("swords", player) or state.has("Bow", player)) - ) # No requiring anything in a shop until we can farm for money for location in shop_locations: add_rule(world.get_location(location, player), From 3ef0a56ec2409bd99687be4d10907bd5e006e4b8 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Wed, 29 Mar 2023 22:28:00 -0400 Subject: [PATCH 138/172] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20Another=20quiz=20?= =?UTF-8?q?fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- worlds/pokemon_rb/rom.py | 10 +++++----- worlds/pokemon_rb/text.py | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/worlds/pokemon_rb/rom.py b/worlds/pokemon_rb/rom.py index fbb0ec41d9..67e3791541 100644 --- a/worlds/pokemon_rb/rom.py +++ b/worlds/pokemon_rb/rom.py @@ -577,10 +577,7 @@ def write_quizzes(self, data, random): for location in self.multiworld.get_filled_locations(): if location.item.name == "Secret Key" and location.item.player == self.player: break - if location.player == self.player: - player_name = "yourself" - else: - player_name = self.multiworld.player_name[location.player] + player_name = self.multiworld.player_name[location.player] if not a: if len(self.multiworld.player_name) > 1: old_name = player_name @@ -588,7 +585,10 @@ def write_quizzes(self, data, random): player_name = random.choice(list(self.multiworld.player_name.values())) else: return encode_text("You're playingin a multiworldwith otherplayers?") - return encode_text(f"The Secret Key wasfound by{player_name[:17]}?") + if player_name == self.multiworld.player_name[self.player]: + player_name = "yourself" + player_name = encode_text(player_name, force=True, safety=True) + return encode_text(f"The Secret Key wasfound by") + player_name + encode_text("") elif q == 2: if a: return encode_text(f"#mon ispronouncedPo-kay-mon?") diff --git a/worlds/pokemon_rb/text.py b/worlds/pokemon_rb/text.py index feb54e656a..d20891d7d7 100644 --- a/worlds/pokemon_rb/text.py +++ b/worlds/pokemon_rb/text.py @@ -118,9 +118,9 @@ def encode_text(text: str, length: int=0, whitespace=False, force=False, safety= special = False for char in text: if char == ">": - if spec_char in unsafe_chars and safety: - raise KeyError(f"Disallowed Pokemon text special character '<{spec_char}>'") try: + if spec_char in unsafe_chars and safety: + raise KeyError(f"Disallowed Pokemon text special character '<{spec_char}>'") encoded_text.append(special_chars[spec_char]) except KeyError: if force: @@ -135,10 +135,10 @@ def encode_text(text: str, length: int=0, whitespace=False, force=False, safety= elif special is True: spec_char += char else: - if char in unsafe_chars and safety: - raise KeyError(f"Disallowed Pokemon text character '{char}'") try: encoded_text.append(char_map[char]) + if char in unsafe_chars and safety: + raise KeyError(f"Disallowed Pokemon text character '{char}'") except KeyError: if force: encoded_text.append(char_map[" "]) From af44c1ba3da6b58f993d1f2bdc8f66d6902c0133 Mon Sep 17 00:00:00 2001 From: KernRat <128561623+KernRat@users.noreply.github.com> Date: Thu, 30 Mar 2023 15:30:25 +0200 Subject: [PATCH 139/172] Factorio: Fixed outdated energy link troughput in docs (#1626) --- worlds/factorio/docs/en_Factorio.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/factorio/docs/en_Factorio.md b/worlds/factorio/docs/en_Factorio.md index 8f2c3f69fd..61bceb3820 100644 --- a/worlds/factorio/docs/en_Factorio.md +++ b/worlds/factorio/docs/en_Factorio.md @@ -39,6 +39,6 @@ EnergyLink is an energy storage supported by certain games that is shared across In Factorio, if enabled in the player settings, EnergyLink Bridge buildings can be crafted and placed, which allow depositing excess energy and supplementing energy deficits, much like Accumulators. -Each placed EnergyLink Bridge provides 1 MW of throughput. The shared storage has unlimited capacity, but 25% of energy +Each placed EnergyLink Bridge provides 10 MW of throughput. The shared storage has unlimited capacity, but 25% of energy is lost during depositing. The amount of energy currently in the shared storage is displayed in the Archipelago client. It can also be queried by typing `/energy-link` in-game. From cd4fd18706418abce27132a1d551e00c5e7a85e3 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 30 Mar 2023 15:30:43 +0200 Subject: [PATCH 140/172] Core: update modules (#1612) --- Utils.py | 4 ++-- requirements.txt | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Utils.py b/Utils.py index fb09061971..f1db35635b 100644 --- a/Utils.py +++ b/Utils.py @@ -151,8 +151,8 @@ def cache_path(*path: str) -> str: if hasattr(cache_path, "cached_path"): pass else: - import appdirs - cache_path.cached_path = appdirs.user_cache_dir("Archipelago", False) + import platformdirs + cache_path.cached_path = platformdirs.user_cache_dir("Archipelago", False) return os.path.join(cache_path.cached_path, *path) diff --git a/requirements.txt b/requirements.txt index 8c73949dd4..5b50664475 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ colorama>=0.4.5 websockets>=10.3 PyYAML>=6.0 -jellyfish>=0.9.0 +jellyfish>=0.11.0 jinja2>=3.1.2 schema>=0.7.5 kivy>=2.1.0 -bsdiff4>=1.2.2 -appdirs>=1.4.4 +bsdiff4>=1.2.3 +platformdirs>=3.2.0 From 59b78528a970632d6a1e88f9c050c3b260f2ed52 Mon Sep 17 00:00:00 2001 From: Rosalie-A <61372066+Rosalie-A@users.noreply.github.com> Date: Thu, 30 Mar 2023 09:31:16 -0400 Subject: [PATCH 141/172] TLoZ: Fix description and off-by-one error (#1625) --- data/lua/TLoZ/TheLegendOfZeldaConnector.lua | 2 +- worlds/tloz/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/lua/TLoZ/TheLegendOfZeldaConnector.lua b/data/lua/TLoZ/TheLegendOfZeldaConnector.lua index aee4412bc0..ac33ed3cc4 100644 --- a/data/lua/TLoZ/TheLegendOfZeldaConnector.lua +++ b/data/lua/TLoZ/TheLegendOfZeldaConnector.lua @@ -621,7 +621,7 @@ function receive() -- Determine Message to send back memDomain.rom() - local playerName = uRange(0x1F, 0x10) + local playerName = uRange(0x1F, 0x11) playerName[0] = nil local retTable = {} retTable["playerName"] = playerName diff --git a/worlds/tloz/__init__.py b/worlds/tloz/__init__.py index 7cdf87c300..311215c157 100644 --- a/worlds/tloz/__init__.py +++ b/worlds/tloz/__init__.py @@ -36,7 +36,7 @@ class TLoZWeb(WebWorld): class TLoZWorld(World): """ The Legend of Zelda needs almost no introduction. Gather the eight fragments of the - Triforce of Courage, enter Death Mountain, defeat Ganon, and rescue Princess Zelda. + Triforce of Wisdom, enter Death Mountain, defeat Ganon, and rescue Princess Zelda. This randomizer shuffles all the items in the game around, leading to a new adventure every time. """ From 20e80d06cf90a15d49156c2f6af66b9c3577c6a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilhelm=20Sch=C3=BCrmann?= Date: Thu, 30 Mar 2023 15:55:38 +0200 Subject: [PATCH 142/172] LADX: Add Lua connector for BizHawk (#1579) This is a Lua script for BizHawk that implements the relevant parts of the RetroArch networking API used by the Archipelago LADX Client. socket.lua and core.dll are exact copies of the same files in data/lua/OOT and various other folders. There is a PR consolidating these into the base folder, which this commit is anticipating. LADX "just works"(tm) when this is loaded in Bizhawk. --- data/lua/connector_ladx_bizhawk.lua | 137 ++++++++++++++++++++++++++++ data/lua/core.dll | Bin 0 -> 29184 bytes data/lua/socket.lua | 132 +++++++++++++++++++++++++++ worlds/ladx/docs/setup_en.md | 13 ++- 4 files changed, 279 insertions(+), 3 deletions(-) create mode 100644 data/lua/connector_ladx_bizhawk.lua create mode 100644 data/lua/core.dll create mode 100644 data/lua/socket.lua diff --git a/data/lua/connector_ladx_bizhawk.lua b/data/lua/connector_ladx_bizhawk.lua new file mode 100644 index 0000000000..e318015cb0 --- /dev/null +++ b/data/lua/connector_ladx_bizhawk.lua @@ -0,0 +1,137 @@ +-- SPDX-FileCopyrightText: 2023 Wilhelm Schürmann +-- +-- SPDX-License-Identifier: MIT + +-- This script attempts to implement the basic functionality needed in order for +-- the LADXR Archipelago client to be able to talk to BizHawk instead of RetroArch +-- by reproducing the RetroArch API with BizHawk's Lua interface. +-- +-- RetroArch UDP API: https://github.com/libretro/RetroArch/blob/master/command.c +-- +-- Only +-- VERSION +-- GET_STATUS +-- READ_CORE_MEMORY +-- WRITE_CORE_MEMORY +-- commands are supported right now. +-- +-- USAGE: +-- Load this script in BizHawk ("Tools" -> "Lua Console" -> "Script" -> "Open Script") +-- +-- All inconsistencies (like missing newlines for some commands) of the RetroArch +-- UDP API (network_cmd_enable) are reproduced as-is in order for clients written to work with +-- RetroArch's current API to "just work"(tm). +-- +-- This script has only been tested on GB(C). If you have made sure it works for N64 or other +-- cores supported by BizHawk, please let me know. Note that GET_STATUS, at the very least, will +-- have to be adjusted. +-- +-- +-- NOTE: +-- BizHawk's Lua API is very trigger-happy on throwing exceptions. +-- Emulation will continue fine, but the RetroArch API layer will stop working. This +-- is indicated only by an exception visible in the Lua console, which most players +-- will probably not have in the foreground. +-- +-- pcall(), the usual way to catch exceptions in Lua, doesn't appear to be supported at all, +-- meaning that error/exception handling is not easily possible. +-- +-- This means that a lot more error checking would need to happen before e.g. reading/writing +-- memory. Since the end goal, according to AP's Discord, seems to be SNI integration of GB(C), +-- no further fault-proofing has been done on this. +-- + + +local socket = require("socket") +local udp = socket.udp() + +udp:setsockname('127.0.0.1', 55355) +udp:settimeout(0) + + +while true do + -- Attempt to lessen the CPU load by only polling the UDP socket every x frames. + -- x = 10 is entirely arbitrary, very little thought went into it. + -- We could try to make use of client.get_approx_framerate() here, but the values returned + -- seemed more or less arbitrary as well. + -- + -- NOTE: Never mind the above, the LADXR Archipelago client appears to run into problems with + -- interwoven GET_STATUS calls, leading to stopped communication. + -- For GB(C), polling the socket on every frame is OK-ish, so we just do that. + -- + --while emu.framecount() % 10 ~= 0 do + -- emu.frameadvance() + --end + + local data, msg_or_ip, port_or_nil = udp:receivefrom() + if data then + -- "data" format is "COMMAND [PARAMETERS] [...]" + local command = string.match(data, "%S+") + if command == "VERSION" then + -- 1.14 is the latest RetroArch release at the time of writing this, no other reason + -- for choosing this here. + udp:sendto("1.14.0\n", msg_or_ip, port_or_nil) + elseif command == "GET_STATUS" then + local status = "PLAYING" + if client.ispaused() then + status = "PAUSED" + end + + if emu.getsystemid() == "GBC" then + -- Actual reply from RetroArch's API: + -- "GET_STATUS PLAYING game_boy,AP_62468482466172374046_P1_Lonk,crc32=3ecb7b6f" + -- CRC32 isn't readily available through the Lua API. We could calculate + -- it ourselves, but since LADXR doesn't make use of this field it is + -- simply replaced by the hash that BizHawk _does_ make available. + + udp:sendto( + "GET_STATUS " .. status .. " game_boy," .. + string.gsub(gameinfo.getromname(), "[%s,]", "_") .. + ",romhash=" .. + gameinfo.getromhash() .. "\n", + msg_or_ip, port_or_nil + ) + else -- No ROM loaded + -- NOTE: No newline is intentional here for 1:1 RetroArch compatibility + udp:sendto("GET_STATUS CONTENTLESS", msg_or_ip, port_or_nil) + end + elseif command == "READ_CORE_MEMORY" then + local _, address, length = string.match(data, "(%S+) (%S+) (%S+)") + address = tonumber(address, 16) + length = tonumber(length) + + -- NOTE: mainmemory.read_bytes_as_array() would seem to be the obvious choice + -- here instead, but it isn't. At least for Sameboy and Gambatte, the "main" + -- memory differs (ROM vs WRAM). + -- Using memory.read_bytes_as_array() and explicitly using the System Bus + -- as the active memory domain solves this incompatibility, allowing us + -- to hopefully use whatever GB(C) emulator we want. + local mem = memory.read_bytes_as_array(address, length, "System Bus") + local hex_string = "" + for _, v in ipairs(mem) do + hex_string = hex_string .. string.format("%02X ", v) + end + hex_string = hex_string:sub(1, -2) -- Hang head in shame, remove last " " + local reply = string.format("%s %02x %s\n", command, address, hex_string) + udp:sendto(reply, msg_or_ip, port_or_nil) + elseif command == "WRITE_CORE_MEMORY" then + local _, address = string.match(data, "(%S+) (%S+)") + address = tonumber(address, 16) + + local to_write = {} + local i = 1 + for byte_str in string.gmatch(data, "%S+") do + if i > 2 then + table.insert(to_write, tonumber(byte_str, 16)) + end + i = i + 1 + end + + memory.write_bytes_as_array(address, to_write, "System Bus") + local reply = string.format("%s %02x %d\n", command, address, i - 3) + udp:sendto(reply, msg_or_ip, port_or_nil) + end + end + + emu.frameadvance() +end diff --git a/data/lua/core.dll b/data/lua/core.dll new file mode 100644 index 0000000000000000000000000000000000000000..3e9569571ab0947dcb7bcd789dc9c06c009d072d GIT binary patch literal 29184 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 B{xARl literal 0 HcmV?d00001 diff --git a/data/lua/socket.lua b/data/lua/socket.lua new file mode 100644 index 0000000000..a98e952115 --- /dev/null +++ b/data/lua/socket.lua @@ -0,0 +1,132 @@ +----------------------------------------------------------------------------- +-- LuaSocket helper module +-- Author: Diego Nehab +-- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $ +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module and import dependencies +----------------------------------------------------------------------------- +local base = _G +local string = require("string") +local math = require("math") +local socket = require("socket.core") +module("socket") + +----------------------------------------------------------------------------- +-- Exported auxiliar functions +----------------------------------------------------------------------------- +function connect(address, port, laddress, lport) + local sock, err = socket.tcp() + if not sock then return nil, err end + if laddress then + local res, err = sock:bind(laddress, lport, -1) + if not res then return nil, err end + end + local res, err = sock:connect(address, port) + if not res then return nil, err end + return sock +end + +function bind(host, port, backlog) + local sock, err = socket.tcp() + if not sock then return nil, err end + sock:setoption("reuseaddr", true) + local res, err = sock:bind(host, port) + if not res then return nil, err end + res, err = sock:listen(backlog) + if not res then return nil, err end + return sock +end + +try = newtry() + +function choose(table) + return function(name, opt1, opt2) + if base.type(name) ~= "string" then + name, opt1, opt2 = "default", name, opt1 + end + local f = table[name or "nil"] + if not f then base.error("unknown key (".. base.tostring(name) ..")", 3) + else return f(opt1, opt2) end + end +end + +----------------------------------------------------------------------------- +-- Socket sources and sinks, conforming to LTN12 +----------------------------------------------------------------------------- +-- create namespaces inside LuaSocket namespace +sourcet = {} +sinkt = {} + +BLOCKSIZE = 2048 + +sinkt["close-when-done"] = function(sock) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function(self, chunk, err) + if not chunk then + sock:close() + return 1 + else return sock:send(chunk) end + end + }) +end + +sinkt["keep-open"] = function(sock) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function(self, chunk, err) + if chunk then return sock:send(chunk) + else return 1 end + end + }) +end + +sinkt["default"] = sinkt["keep-open"] + +sink = choose(sinkt) + +sourcet["by-length"] = function(sock, length) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function() + if length <= 0 then return nil end + local size = math.min(socket.BLOCKSIZE, length) + local chunk, err = sock:receive(size) + if err then return nil, err end + length = length - string.len(chunk) + return chunk + end + }) +end + +sourcet["until-closed"] = function(sock) + local done + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function() + if done then return nil end + local chunk, err, partial = sock:receive(socket.BLOCKSIZE) + if not err then return chunk + elseif err == "closed" then + sock:close() + done = 1 + return partial + else return nil, err end + end + }) +end + + +sourcet["default"] = sourcet["until-closed"] + +source = choose(sourcet) diff --git a/worlds/ladx/docs/setup_en.md b/worlds/ladx/docs/setup_en.md index abd60dc82e..2fbd67dafa 100644 --- a/worlds/ladx/docs/setup_en.md +++ b/worlds/ladx/docs/setup_en.md @@ -4,8 +4,8 @@ - [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `Links Awakening DX` - Software capable of loading and playing GBC ROM files - - Currently only [RetroArch](https://retroarch.com?page=platforms) 1.10.3 or newer) is supported. - - Bizhawk support will come at a later date. + - [RetroArch](https://retroarch.com?page=platforms) 1.10.3 or newer. + - [BizHawk](https://tasvideos.org/BizHawk) 2.8 or newer. - Your American 1.0 ROM file, probably named `Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc` ## Installation Procedures @@ -65,7 +65,7 @@ client, and will also create your ROM in the same place as your patch file. ### Connect to the client -##### RetroArch 1.10.3 or newer +#### RetroArch 1.10.3 or newer You only have to do these steps once. Note, RetroArch 1.9.x will not work as it is older than 1.10.3. @@ -77,6 +77,13 @@ You only have to do these steps once. Note, RetroArch 1.9.x will not work as it ![Screenshot of Network Commands setting](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-en.png) 4. Go to Main Menu --> Online Updater --> Core Downloader. Scroll down and select "Nintendo - Gameboy / Color (SameBoy)". +#### BizHawk 2.8 or newer (older versions untested) + +1. With the ROM loaded, click on "Tools" --> "Lua Console" +2. In the new window, click on "Script" --> "Open Script..." +3. Navigate to the folder Archipelago is installed in, and choose data/lua/connector_ladx_bizhawk.lua +4. Keep the Lua Console open during gameplay (minimizing it is fine!) + ### Connect to the Archipelago Server The patch file which launched your client should have automatically connected you to the AP Server. There are a few From 4eea91daabc5efb8e82cdbff1a76011cb41b6098 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Thu, 30 Mar 2023 10:25:25 -0400 Subject: [PATCH 143/172] Stardew Valley: Improve Documentation (#1631) --- worlds/stardew_valley/__init__.py | 3 ++- worlds/stardew_valley/docs/setup_en.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index 306a3ec7e0..79c057fcbe 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -42,7 +42,8 @@ class StardewWebWorld(WebWorld): class StardewValleyWorld(World): """ - Stardew Valley farming simulator game where the objective is basically to spend the least possible time on your farm. + Stardew Valley is an open-ended country-life RPG. You can farm, fish, mine, fight, complete quests, + befriend villagers, and uncover dark secrets. """ game = "Stardew Valley" option_definitions = stardew_valley_options diff --git a/worlds/stardew_valley/docs/setup_en.md b/worlds/stardew_valley/docs/setup_en.md index 7ac9c8a814..30d5f3da2d 100644 --- a/worlds/stardew_valley/docs/setup_en.md +++ b/worlds/stardew_valley/docs/setup_en.md @@ -23,7 +23,7 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) ### Where do I get a YAML file? -You can customize your settings by visiting the [Stardew Valley Player Settings Page](/games/Stardew Valley/player-settings) +You can customize your settings by visiting the [Stardew Valley Player Settings Page](../player-settings) ## Joining a MultiWorld Game From bf5282dfa8840d38a35dcfde89541103d9856281 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Thu, 30 Mar 2023 15:56:26 -0500 Subject: [PATCH 144/172] add Toggle options back to player settings and remove unnecessary check (#1633) --- WebHostLib/options.py | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 942f9e8260..545556d4bb 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -88,7 +88,7 @@ def create(): if option_name in handled_in_js: pass - elif issubclass(option, Options.Choice) or issubclass(option, Options.TextChoice): + elif issubclass(option, Options.Choice) or issubclass(option, Options.Toggle): game_options[option_name] = this_option = { "type": "select", "displayName": option.display_name if hasattr(option, "display_name") else option_name, @@ -97,28 +97,15 @@ def create(): "options": [] } - has_random_option = False for sub_option_id, sub_option_name in option.name_lookup.items(): - this_option["options"].append({ - "name": option.get_option_name(sub_option_id), - "value": sub_option_name, - }) - + if sub_option_name != "random": + this_option["options"].append({ + "name": option.get_option_name(sub_option_id), + "value": sub_option_name, + }) if sub_option_id == option.default: this_option["defaultValue"] = sub_option_name - if sub_option_name == "random": - has_random_option = True - - if not has_random_option: - this_option["options"].append({ - "name": "random", - "value": 'random', - }) - - if option.default == "random": - this_option["defaultValue"] = "random" - elif issubclass(option, Options.Range): game_options[option_name] = { "type": "range", From d5b4a91a13e3013e589228cbbaaaac30d06e4289 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Thu, 30 Mar 2023 16:18:56 -0500 Subject: [PATCH 145/172] The Messenger: Have users explicitly remove old version (#1632) --- worlds/messenger/docs/setup_en.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/worlds/messenger/docs/setup_en.md b/worlds/messenger/docs/setup_en.md index 64f209e13f..d4a3eafa0e 100644 --- a/worlds/messenger/docs/setup_en.md +++ b/worlds/messenger/docs/setup_en.md @@ -16,8 +16,9 @@ 1. Download the latest TheMessengerRandomizerAP.zip from the [The Messenger Randomizer Mod AP releases page](https://github.com/alwaysintreble/TheMessengerRandomizerModAP/releases) 2. Extract the zip file to `TheMessenger/Mods/` of your game's install location - * You can have both the non-AP randomizer and the AP randomizer installed at the same time, but the AP randomizer - is backwards compatible, so the non-AP one can be safely removed. + * You cannot have both the non-AP randomizer and the AP randomizer installed at the same time. The AP randomizer + is backwards compatible, so the non-AP mod can be safely removed, and you can still play seeds generated from the + non-AP randomizer. 3. Optionally, Backup your save game * On Windows 1. Press `Windows Key + R` to open run From 1dc4e2b44b241418fe2614c5264304a4d7973c5b Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Thu, 30 Mar 2023 19:01:31 -0400 Subject: [PATCH 146/172] Restore "random" option to weighted-settings (#1635) * Restore "random" option to weighted-settings, adjust capitalization of hardcoded settings * Set default value as "random" for Choice, TextChoice, and Toggle options with no default value --- WebHostLib/options.py | 11 +++++++++++ WebHostLib/static/assets/weighted-settings.js | 12 +++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 545556d4bb..a4d7ccc17c 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -106,6 +106,9 @@ def create(): if sub_option_id == option.default: this_option["defaultValue"] = sub_option_name + if not this_option["defaultValue"]: + this_option["defaultValue"] = "random" + elif issubclass(option, Options.Range): game_options[option_name] = { "type": "range", @@ -157,6 +160,14 @@ def create(): json.dump(player_settings, f, indent=2, separators=(',', ': ')) if not world.hidden and world.web.settings_page is True: + # Add the random option to Choice, TextChoice, and Toggle settings + for option in game_options.values(): + if option["type"] == "select": + option["options"].append({"name": "Random", "value": "random"}) + + if not option["defaultValue"]: + option["defaultValue"] = "random" + weighted_settings["baseOptions"]["game"][game_name] = 0 weighted_settings["games"][game_name] = {} weighted_settings["games"][game_name]["gameSettings"] = game_options diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js index e914197d4c..e471e0837a 100644 --- a/WebHostLib/static/assets/weighted-settings.js +++ b/WebHostLib/static/assets/weighted-settings.js @@ -470,7 +470,17 @@ const buildWeightedSettingsDiv = (game, settings, gameItems, gameLocations) => { const tr = document.createElement('tr'); const tdLeft = document.createElement('td'); tdLeft.classList.add('td-left'); - tdLeft.innerText = option; + switch(option){ + case 'random': + tdLeft.innerText = 'Random'; + break; + case 'random-low': + tdLeft.innerText = "Random (Low)"; + break; + case 'random-high': + tdLeft.innerText = "Random (High)"; + break; + } tr.appendChild(tdLeft); const tdMiddle = document.createElement('td'); From 30cfd3186c225c0c39485a351090de55d191c8e6 Mon Sep 17 00:00:00 2001 From: zig-for Date: Fri, 31 Mar 2023 05:05:51 -0700 Subject: [PATCH 147/172] LADX: Fix local paths (#1634) --- worlds/ladx/LADXR/rom.py | 3 ++- worlds/ladx/LADXR/settings.py | 11 +---------- worlds/ladx/Options.py | 8 +++++--- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/worlds/ladx/LADXR/rom.py b/worlds/ladx/LADXR/rom.py index e7ff2f244d..ea4f14089f 100644 --- a/worlds/ladx/LADXR/rom.py +++ b/worlds/ladx/LADXR/rom.py @@ -1,4 +1,5 @@ import binascii +import Utils b2h = binascii.hexlify h2b = binascii.unhexlify @@ -6,7 +7,7 @@ h2b = binascii.unhexlify class ROM: def __init__(self, filename): - data = open(filename, "rb").read() + data = open(Utils.local_path(filename), "rb").read() #assert len(data) == 1024 * 1024 self.banks = [] for n in range(0x40): diff --git a/worlds/ladx/LADXR/settings.py b/worlds/ladx/LADXR/settings.py index d52e8fe45d..848d64390d 100644 --- a/worlds/ladx/LADXR/settings.py +++ b/worlds/ladx/LADXR/settings.py @@ -69,15 +69,6 @@ class Setting: class Settings: def __init__(self, ap_options): - gfx_options = [('', '', 'Default')] - gfx_path = os.path.join("data", "sprites", "ladx") - for filename in sorted(os.listdir(gfx_path)): - if filename.endswith(".bin") or filename.endswith(".png") or filename.endswith(".bmp"): - gfx_options.append((filename, filename + ">", filename[:-4])) - if filename.endswith(".bdiff"): - gfx_options.append((filename, filename + ">", filename[:-6])) - - self.__all = [ Setting('seed', 'Main', '<', 'Seed', placeholder='Leave empty for random seed', default="", multiworld=False, description="""For multiple people to generate the same randomization result, enter the generated seed number here. @@ -201,7 +192,7 @@ Note, some entrances can lead into water, use the warp-to-home from the save&qui Setting('nagmessages', 'User options', 'S', 'Show nag messages', default=False, description='Enables the nag messages normally shown when touching stones and crystals', aesthetic=True), - Setting('gfxmod', 'User options', 'c', 'Graphics', options=gfx_options, default='', + Setting('gfxmod', 'User options', 'c', 'Graphics', default='', description='Generally affects at least Link\'s sprite, but can alter any graphics in the game', aesthetic=True), Setting('linkspalette', 'User options', 'C', "Link's color", diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py index b14d7b2af0..8d30186670 100644 --- a/worlds/ladx/Options.py +++ b/worlds/ladx/Options.py @@ -3,6 +3,7 @@ import typing import logging from Options import Choice, Option, Toggle, DefaultOnToggle, Range, FreeText from collections import defaultdict +import Utils DefaultOffToggle = Toggle @@ -318,12 +319,13 @@ class GfxMod(FreeText, LADXROption): default = 'Link' __spriteFiles: typing.DefaultDict[str, typing.List[str]] = defaultdict(list) - __spriteDir = os.path.join('data', 'sprites','ladx') + __spriteDir: str = None extensions = [".bin", ".bdiff", ".png", ".bmp"] def __init__(self, value: str): super().__init__(value) - if not GfxMod.__spriteFiles: + if not GfxMod.__spriteDir: + GfxMod.__spriteDir = Utils.local_path(os.path.join('data', 'sprites','ladx')) for file in os.listdir(GfxMod.__spriteDir): name, extension = os.path.splitext(file) if extension in self.extensions: @@ -344,7 +346,7 @@ class GfxMod(FreeText, LADXROption): if len(GfxMod.__spriteFiles[self.value]) > 1: logger.warning(f"{self.value} does not uniquely identify a file. Possible matches: {GfxMod.__spriteFiles[self.value]}. Using {GfxMod.__spriteFiles[self.value][0]}") - return self.ladxr_name, GfxMod.__spriteFiles[self.value][0] + return self.ladxr_name, self.__spriteDir + "/" + GfxMod.__spriteFiles[self.value][0] class Palette(Choice): """ From 8971340a66bb43bf33f0fce29050f2bd1b57e8cd Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 31 Mar 2023 14:43:05 +0200 Subject: [PATCH 148/172] Core: update version to 0.4.0 --- Utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Utils.py b/Utils.py index f1db35635b..60b3904ff6 100644 --- a/Utils.py +++ b/Utils.py @@ -39,7 +39,7 @@ class Version(typing.NamedTuple): build: int -__version__ = "0.3.9" +__version__ = "0.4.0" version_tuple = tuplize_version(__version__) is_linux = sys.platform.startswith("linux") From 6e271b643d03582d62c97d4156375d3b5dca2c0e Mon Sep 17 00:00:00 2001 From: Justin LaLone Date: Fri, 31 Mar 2023 12:29:32 -0700 Subject: [PATCH 149/172] Adventure: Added examples for automatically loading adventure_connector.lua in BizHawk --- host.yaml | 2 ++ worlds/adventure/docs/setup_en.md | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/host.yaml b/host.yaml index 4a86d054df..fd5c759032 100644 --- a/host.yaml +++ b/host.yaml @@ -180,6 +180,8 @@ adventure_options: # Optional, additional args passed into rom_start before the .bin file # For example, this can be used to autoload the connector script in BizHawk # (see BizHawk --lua= option) + # Windows example: + # rom_args: "--lua=C:/ProgramData/Archipelago/data/lua/ADVENTURE/adventure_connector.lua" rom_args: " " # Set this to true to display item received messages in Emuhawk display_msgs: true diff --git a/worlds/adventure/docs/setup_en.md b/worlds/adventure/docs/setup_en.md index 038e87e767..658162d8c2 100644 --- a/worlds/adventure/docs/setup_en.md +++ b/worlds/adventure/docs/setup_en.md @@ -27,6 +27,10 @@ Once Bizhawk has been installed, open Bizhawk and change the following settings: BizHawk is running in the background. - It is recommended that you provide a path to BizHawk in your host.yaml for Adventure so the client can start it automatically +- At the same time, you can set an option to automatically load the adventure_connector.lua script when launching BizHawk +from AdventureClient. +Default Windows install example: +```rom_args: "--lua=C:/ProgramData/Archipelago/data/lua/ADVENTURE/adventure_connector.lua"``` ## Configuring your YAML file @@ -62,7 +66,8 @@ path as recommended). Once both the client and the emulator are started, you must connect them. Within the emulator click on the "Tools" menu and select "Lua Console". Click the folder button or press Ctrl+O to open a Lua script. -Navigate to your Archipelago install folder and open `data/lua/ADVENTURE/adventure_connector.lua`. +Navigate to your Archipelago install folder and open `data/lua/ADVENTURE/adventure_connector.lua`, if it is not +configured to do this automatically. To connect the client to the multiserver simply put `
:` on the textfield on top and press enter (if the server uses password, type in the bottom textfield `/connect
: [password]`) From 510a460d8424206f3afbeea1dae58621f2151e41 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 1 Apr 2023 19:54:44 +0200 Subject: [PATCH 150/172] Multiserver: location name groups fix (#1643) --- MultiServer.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index acc68e1682..21aee68a36 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -260,7 +260,8 @@ class Context: def _init_game_data(self): for game_name, game_package in self.gamespackage.items(): - self.checksums[game_name] = game_package["checksum"] + 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 for location_name, location_id in game_package["location_name_to_id"].items(): @@ -268,7 +269,7 @@ class Context: 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[game_name]) + set(game_package["location_name_to_id"]) | set(self.location_name_groups.get(game_name, [])) 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 @@ -356,9 +357,7 @@ class Context: [{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments} for text in texts])) - # loading - def load(self, multidatapath: str, use_embedded_server_options: bool = False): if multidatapath.lower().endswith(".zip"): import zipfile @@ -446,9 +445,10 @@ class Context: logging.info(f"Loading embedded data package for game {game_name}") self.gamespackage[game_name] = data self.item_name_groups[game_name] = data["item_name_groups"] - self.location_name_groups[game_name] = data["location_name_groups"] + if "location_name_groups" in data: + self.location_name_groups[game_name] = data["location_name_groups"] + del data["location_name_groups"] del data["item_name_groups"] # remove from data package, but keep in self.item_name_groups - del data["location_name_groups"] self._init_game_data() for game_name, data in self.item_name_groups.items(): self.read_data[f"item_name_groups_{game_name}"] = lambda lgame=game_name: self.item_name_groups[lgame] From 34de5a57af54647da4da6a353629a73cb67938bc Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Sat, 1 Apr 2023 13:55:43 -0400 Subject: [PATCH 151/172] KH2: updated the docs for options and faq (#1641) --- worlds/kh2/Options.py | 95 +++++++++++++++----------- worlds/kh2/docs/en_Kingdom Hearts 2.md | 6 +- 2 files changed, 61 insertions(+), 40 deletions(-) diff --git a/worlds/kh2/Options.py b/worlds/kh2/Options.py index 90c2c7f709..31708218e2 100644 --- a/worlds/kh2/Options.py +++ b/worlds/kh2/Options.py @@ -61,7 +61,7 @@ class SummonEXP(Range): class Schmovement(Choice): - """Level of Progressive Movement You Start With""" + """Level of Progressive Movement Abilities You Start With""" display_name = "Schmovement" option_level_0 = 0 option_level_1 = 1 @@ -72,7 +72,7 @@ class Schmovement(Choice): class RandomGrowth(Range): - """Amount of Random Growth Abilities You Start With""" + """Amount of Random Progressive Movement Abilities You Start With""" display_name = "Random Starting Growth" range_start = 0 range_end = 20 @@ -80,7 +80,7 @@ class RandomGrowth(Range): class KeybladeMin(Range): - """Minimum Stats for the Keyblade""" + """Minimum Stats for Keyblades""" display_name = "Keyblade Minimum Stats" range_start = 0 range_end = 20 @@ -88,7 +88,7 @@ class KeybladeMin(Range): class KeybladeMax(Range): - """Maximum Stats for the Keyblade""" + """Maximum Stats for Keyblades""" display_name = "Keyblade Max Stats" range_start = 0 range_end = 20 @@ -97,11 +97,16 @@ class KeybladeMax(Range): class Visitlocking(Choice): """Determines the level of visit locking - No Visit Locking:No visit locks(everything is sphere 1.Not Recommended)# BK is making a lot of money - Second Visit Locking:Start with 13 visit locking items for every first visit. - First and Second Visit Locking:One item for First Visit Two For Second Visit""" + + No Visit Locking: Start with all 25 visit locking items. + + + Second Visit Locking: Start with 13 visit locking items for every first visit. + + + First and Second Visit Locking: One item for First Visit Two For Second Visit""" display_name = "Visit locking" - option_no_visit_locking = 0 # starts with 27 visit locking + option_no_visit_locking = 0 # starts with 25 visit locking option_second_visit_locking = 1 # starts with 13 (no icecream/picture) option_first_and_second_visit_locking = 2 # starts with nothing default = 2 @@ -123,8 +128,8 @@ class SuperBosses(Toggle): class Cups(Choice): """Olympus Cups Toggles - No Cups: No Cups. - Cups: Has every cup except Paradox. + No Cups: All Cups are placed into Excluded Locations. + Cups: Hades Paradox Cup is placed into Excluded Locations Cups and Hades Paradox: Has Every Cup On.""" display_name = "Olympus Cups" option_no_cups = 0 @@ -135,10 +140,13 @@ class Cups(Choice): class LevelDepth(Choice): """Determines How many locations you want on levels - Level 50:23 checks spread through 50 levels - Level 99:23 checks spread through 99 levels - Level 50 sanity:check per level for 50 levels - Level 99 sanity:check per level for 99 levels + + Level 50:23 checks spread through 50 levels. + Level 99:23 checks spread through 99 levels. + + Level 50 sanity: 49 checks spread through 50 levels. + Level 99 sanity: 98 checks spread through 99 levels. + Level 1: no checks on levels(checks are replaced with stats)""" display_name = "Level Depth" option_level_50 = 0 @@ -156,13 +164,17 @@ class PromiseCharm(Toggle): class KeybladeAbilities(Choice): - """Action:Has Action Abilities on Keyblades - Support:Has Support Abilities on Keyblades""" + """ + Action: Action Abilities in the Keyblade Slot Pool. + + Support: Support Abilities in the Keyblade Slot Pool. + + Both: Action and Support Abilities in the Keyblade Slot Pool.""" display_name = "Keyblade Abilities" option_support = 0 option_action = 1 option_both = 2 - default = 2 + default = 0 class BlacklistKeyblade(OptionSet): @@ -170,11 +182,14 @@ class BlacklistKeyblade(OptionSet): display_name = "Blacklist Keyblade Abilities" valid_keys = set(SupportAbility_Table.keys()).union(ActionAbility_Table.keys()) + class Goal(Choice): """Win Condition - Three Proofs: Obtain the Three Proofs and Have a Gold Crown on Sora's Head. - Lucky Emblem Hunt: Acquire The Amount of Lucky Emblems you have set. - Hitlist: Kill the Superbosses on the "Hitlist" then synth ultima weapon.""" + Three Proofs: Get a Gold Crown on Sora's Head. + + Lucky Emblem Hunt: Find Required Amount of Lucky Emblems . + + Hitlist (Bounty Hunt): Find Required Amount of Bounties""" display_name = "Goal" option_three_proofs = 0 option_lucky_emblem_hunt = 1 @@ -190,16 +205,18 @@ class FinalXemnas(Toggle): class LuckyEmblemsRequired(Range): - """Number of Lucky Emblems to collect to Open The Final Door bosses. + """Number of Lucky Emblems to collect to Win/Unlock Final Xemnas Door. + If Goal is not Lucky Emblem Hunt this does nothing.""" display_name = "Lucky Emblems Required" range_start = 1 range_end = 60 - default = 25 + default = 30 class LuckyEmblemsAmount(Range): """Number of Lucky Emblems that are in the pool. + If Goal is not Lucky Emblem Hunt this does nothing.""" display_name = "Lucky Emblems Available" range_start = 1 @@ -208,8 +225,9 @@ class LuckyEmblemsAmount(Range): class BountyRequired(Range): - """Number of Bounties that are Required. - If Goal is not Hitlist this does nothing.""" + """Number of Bounties to collect to Win/Unlock Final Xemnas Door. + + If Goal is not Hitlist this does nothing.""" display_name = "Bounties Required" range_start = 1 range_end = 24 @@ -218,7 +236,8 @@ class BountyRequired(Range): class BountyAmount(Range): """Number of Bounties that are in the pool. - If Goal is not Hitlist this does nothing.""" + + If Goal is not Hitlist this does nothing.""" display_name = "Bounties Available" range_start = 1 range_end = 24 @@ -226,30 +245,30 @@ class BountyAmount(Range): KH2_Options: typing.Dict[str, type(Option)] = { + "LevelDepth": LevelDepth, "Sora_Level_EXP": SoraEXP, - "Final_Form_EXP": FinalEXP, - "Master_Form_EXP": MasterEXP, - "Limit_Form_EXP": LimitEXP, - "Wisdom_Form_EXP": WisdomEXP, "Valor_Form_EXP": ValorEXP, + "Wisdom_Form_EXP": WisdomEXP, + "Limit_Form_EXP": LimitEXP, + "Master_Form_EXP": MasterEXP, + "Final_Form_EXP": FinalEXP, "Summon_EXP": SummonEXP, "Schmovement": Schmovement, - "Keyblade_Minimum": KeybladeMin, - "Keyblade_Maximum": KeybladeMax, - "Visitlocking": Visitlocking, - "RandomVisitLockingItem": RandomVisitLockingItem, - "SuperBosses": SuperBosses, - "LevelDepth": LevelDepth, - "Promise_Charm": PromiseCharm, - "KeybladeAbilities": KeybladeAbilities, - "BlacklistKeyblade": BlacklistKeyblade, "RandomGrowth": RandomGrowth, + "Promise_Charm": PromiseCharm, "Goal": Goal, "FinalXemnas": FinalXemnas, "LuckyEmblemsAmount": LuckyEmblemsAmount, "LuckyEmblemsRequired": LuckyEmblemsRequired, "BountyAmount": BountyAmount, "BountyRequired": BountyRequired, + "Keyblade_Minimum": KeybladeMin, + "Keyblade_Maximum": KeybladeMax, + "Visitlocking": Visitlocking, + "RandomVisitLockingItem": RandomVisitLockingItem, + "SuperBosses": SuperBosses, + "KeybladeAbilities": KeybladeAbilities, + "BlacklistKeyblade": BlacklistKeyblade, "Cups": Cups, } diff --git a/worlds/kh2/docs/en_Kingdom Hearts 2.md b/worlds/kh2/docs/en_Kingdom Hearts 2.md index da0a21efcd..bb14731699 100644 --- a/worlds/kh2/docs/en_Kingdom Hearts 2.md +++ b/worlds/kh2/docs/en_Kingdom Hearts 2.md @@ -37,7 +37,7 @@ It is added to your inventory. If you obtain magic, you will need to pause your

What Happens if I die before Room Saving?

-When you die in Kingdom Hearts 2, you are reverted to the last non-boss room you entered and your status is reverted to what it was at that time. However, in archipelago, any item that you have sent/received will not be taken away from the player, unlike vanilla Kingdom Hearts 2. +When you die in Kingdom Hearts 2, you are reverted to the last non-boss room you entered and your status is reverted to what it was at that time. However, in archipelago, any item that you have sent/received will not be taken away from the player, any chest you have opened will remain open, and you will keep your level but lose the expereince. Unlike vanilla Kingdom Hearts 2. For example, if you are fighting Roxas and you receive Reflect Element and you die fighting Roxas, you will keep that reflect. You will still need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable. @@ -93,6 +93,8 @@ With the help of Shananas, Num, and ZakTheRobot we have many QoL features such a - If you have a continuous/constant crash (in the same area/event every time) you will want to reverify your installed files. This can be done by doing the following: Open Epic Game Store --> Library --> Click Triple Dots --> Manage --> Verify - Why am I getting dummy items or letters? - You will need to get the `JaredWeakStrike/APCompanion` (you can find how to get this in the setup guide) +- Why is my HP/MP continuously increasing without stopping? + - You do not have `JaredWeakStrike/APCompanion` setup correctly. Make Sure it is above the GOA in the mod manager. - Why am I not sending or receiving items? - Make sure you are connected to the KH2 client and the correct room (for more information reference the setup guide) - Why did I not load in to the correct visit @@ -102,4 +104,4 @@ With the help of Shananas, Num, and ZakTheRobot we have many QoL features such a - How do I load an auto save? - To load an auto-save, hold down the Select or your equivalent on your prefered controller while choosing a file. Make sure to hold the button down the whole time. - How do I do a soft reset? - - Hold L1+L2+R1+R2+Start or your equivalent on your prefered controller at the same time to immediately reset the game to the start screen. \ No newline at end of file + - Hold L1+L2+R1+R2+Start or your equivalent on your prefered controller at the same time to immediately reset the game to the start screen. From e43bb99622e4059f1ddfce84dc0a3c7965c2da7f Mon Sep 17 00:00:00 2001 From: Freya Arbjerg Date: Sat, 1 Apr 2023 19:56:08 +0200 Subject: [PATCH 152/172] Fix broken styling of multitracker navigation (#1644) --- WebHostLib/templates/multiTrackerNavigation.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/templates/multiTrackerNavigation.html b/WebHostLib/templates/multiTrackerNavigation.html index f712f33679..7fc405b6fb 100644 --- a/WebHostLib/templates/multiTrackerNavigation.html +++ b/WebHostLib/templates/multiTrackerNavigation.html @@ -2,7 +2,7 @@
{% for enabled_tracker in enabled_multiworld_trackers %} {% set tracker_url = url_for(enabled_tracker.endpoint, tracker=room.tracker) %} - {{ enabled_tracker.name }} {% endfor %}
From f015cf42989fa8c3309fe6d525a2514a44a35653 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 1 Apr 2023 22:40:14 +0200 Subject: [PATCH 153/172] MultiServer: compat fix if checksum is not present (#1642) --- MultiServer.py | 2 +- WebHostLib/customserver.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 21aee68a36..258b73c6ee 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -761,7 +761,7 @@ async def on_client_connected(ctx: Context, client: Client): '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}, + in ctx.gamespackage.items() if game in games and "checksum" in game_data}, 'seed_name': ctx.seed_name, 'time': time.time(), }]) diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index fa392492ce..b34e196178 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -94,7 +94,7 @@ class WebHostContext(Context): multidata = self.decompress(room.seed.multidata) game_data_packages = {} - for game in list(multidata["datapackage"]): + for game in list(multidata.get("datapackage", {})): game_data = multidata["datapackage"][game] if "checksum" in game_data: if self.gamespackage.get(game, {}).get("checksum") == game_data["checksum"]: @@ -102,8 +102,9 @@ class WebHostContext(Context): # games package could be dropped from static data once all rooms embed data package del multidata["datapackage"][game] else: - data = Utils.restricted_loads(GameDataPackage.get(checksum=game_data["checksum"]).data) - game_data_packages[game] = data + row = GameDataPackage.get(checksum=game_data["checksum"]) + if row: # None if rolled on >= 0.3.9 but uploaded to <= 0.3.8. multidata should be complete + game_data_packages[game] = Utils.restricted_loads(row.data) return self._load(multidata, game_data_packages, True) From 5f447f4e6b3461433df759cb4b73fd4347e26475 Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Sat, 1 Apr 2023 21:54:07 -0400 Subject: [PATCH 154/172] SMW: Fix Option Tooltip (#1651) --- worlds/smw/Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/smw/Options.py b/worlds/smw/Options.py index a9416b633d..60135896c8 100644 --- a/worlds/smw/Options.py +++ b/worlds/smw/Options.py @@ -88,7 +88,7 @@ class BowserCastleRooms(Choice): class BossShuffle(Choice): """ - How the rooms of Bowser's Castle Front Door behave + How bosses are shuffled None: Bosses are not shuffled Simple: Four Reznors and the seven Koopalings are shuffled around Full: Each boss location gets a fully random boss From 5ed56db48a5a450eab6941e156cfb3fb141e99a2 Mon Sep 17 00:00:00 2001 From: zig-for Date: Mon, 3 Apr 2023 17:23:39 -0700 Subject: [PATCH 155/172] LADX: Fix crash in item pick up with > 100 players (#1658) --- worlds/ladx/LADXR/generator.py | 6 +++--- worlds/ladx/LADXR/patches/bank3e.asm/itemnames.asm | 8 +++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/worlds/ladx/LADXR/generator.py b/worlds/ladx/LADXR/generator.py index 28966ab763..cb9de281b9 100644 --- a/worlds/ladx/LADXR/generator.py +++ b/worlds/ladx/LADXR/generator.py @@ -259,9 +259,9 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m mw = None if spot.item_owner != spot.location_owner: mw = spot.item_owner - if mw > 255: - # Don't torture the game with higher slot numbers - mw = 255 + if mw > 100: + # There are only 101 player name slots (99 + "The Server" + "another world"), so don't use more than that + mw = 100 spot.patch(rom, spot.item, multiworld=mw) patches.enemies.changeBosses(rom, world_setup.boss_mapping) patches.enemies.changeMiniBosses(rom, world_setup.miniboss_mapping) diff --git a/worlds/ladx/LADXR/patches/bank3e.asm/itemnames.asm b/worlds/ladx/LADXR/patches/bank3e.asm/itemnames.asm index 8495d0898b..0c1bc9d699 100644 --- a/worlds/ladx/LADXR/patches/bank3e.asm/itemnames.asm +++ b/worlds/ladx/LADXR/patches/bank3e.asm/itemnames.asm @@ -67,7 +67,12 @@ MessageAddFromPlayerOld: ; hahaha none of this follows calling conventions MessageAddPlayerName: - ; call MessagePad + ; call MessagePad + + cp 101 + jr C, .continue + ld a, 100 +.continue: ld h, 0 ; bc = a, hl = a ld l, a ld b, 0 @@ -79,6 +84,7 @@ MessageAddPlayerName: add hl, bc ; 17 ld bc, MultiNamePointers add hl, bc ; hl = MultiNamePointers + wLinkGiveItemFrom * 17 + call MessageCopyString ret From 8d73746d5b703fad5d5f7210bb6744e6acf7d15e Mon Sep 17 00:00:00 2001 From: toasterparty Date: Mon, 3 Apr 2023 17:25:59 -0700 Subject: [PATCH 156/172] [OC2] Spelling Mistakes (#1656) --- worlds/overcooked2/Options.py | 8 ++++---- worlds/overcooked2/docs/setup_en.md | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/worlds/overcooked2/Options.py b/worlds/overcooked2/Options.py index c859634092..cb4e43d25d 100644 --- a/worlds/overcooked2/Options.py +++ b/worlds/overcooked2/Options.py @@ -28,7 +28,7 @@ class OC2Toggle(Toggle): class LocationBalancing(Choice): - """Location balancing affects the density of progression items found in your world relative to other wordlds. This setting changes nothing for solo games. + """Location balancing affects the density of progression items found in your world relative to other worlds. This setting changes nothing for solo games. - Disabled: Location density in your world can fluctuate greatly depending on the settings of other players. In extreme cases, your world may be entirely populated with filler items @@ -48,7 +48,7 @@ class RampTricks(OC2Toggle): class DeathLink(Choice): - """DeathLink is an opt-in feature for Multiworlds where individual death events are propogated to all games with DeathLink enabled. + """DeathLink is an opt-in feature for Multiworlds where individual death events are propagated to all games with DeathLink enabled. - Disabled: Death will behave as it does in the original game. @@ -88,7 +88,7 @@ class ShuffleLevelOrder(OC2OnToggle): class IncludeHordeLevels(OC2OnToggle): - """Includes "Horde Defence" levels in the pool of possible kitchens when Shuffle Level Order is enabled. Also adds + """Includes "Horde Defense" levels in the pool of possible kitchens when Shuffle Level Order is enabled. Also adds two horde-specific items into the item pool.""" display_name = "Include Horde Levels" @@ -119,7 +119,7 @@ class ShorterLevelDuration(OC2OnToggle): class ShortHordeLevels(OC2OnToggle): """Modifies horde levels to contain roughly 1/3rd fewer waves than in the original game. - The kitchen's health is sacled appropriately to preserve the same approximate difficulty.""" + The kitchen's health is scaled appropriately to preserve the same approximate difficulty.""" display_name = "Shorter Horde Levels" diff --git a/worlds/overcooked2/docs/setup_en.md b/worlds/overcooked2/docs/setup_en.md index 8738e3ed58..de7bbb5bc8 100644 --- a/worlds/overcooked2/docs/setup_en.md +++ b/worlds/overcooked2/docs/setup_en.md @@ -19,9 +19,9 @@ ## Overview -*OC2-Modding* is a general purpose modding framework which doubles as an Archipelago MultiWorld Client. It works by using Harmony to inject custom code into the game at runtime, so none of the orignal game files need to be modified in any way. +*OC2-Modding* is a general purpose modding framework which doubles as an Archipelago MultiWorld Client. It works by using Harmony to inject custom code into the game at runtime, so none of the original game files need to be modified in any way. -When connecting to an Archipelago session using the in-game login screen, a modfile containing all relevant game modifications is automatically downloaded and applied. +When connecting to an Archipelago session using the in-game login screen, a mod file containing all relevant game modifications is automatically downloaded and applied. From this point, the game will communicate with the Archipelago service directly to manage sending/receiving items. Notifications of important events will appear through an in-game console at the top of the screen. From ffd968d89d34adfe8599fde5f8b49fc364544268 Mon Sep 17 00:00:00 2001 From: toasterparty Date: Mon, 3 Apr 2023 17:26:24 -0700 Subject: [PATCH 157/172] [OC2] Fix Overworld Access Logic for World 3 (#1655) --- worlds/overcooked2/Overcooked2Levels.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/worlds/overcooked2/Overcooked2Levels.py b/worlds/overcooked2/Overcooked2Levels.py index e0d23eb6c3..816e1e514a 100644 --- a/worlds/overcooked2/Overcooked2Levels.py +++ b/worlds/overcooked2/Overcooked2Levels.py @@ -398,11 +398,11 @@ overworld_region_by_level = { "2-4": OverworldRegion.main, "2-5": OverworldRegion.main, "2-6": OverworldRegion.main, - "3-1": OverworldRegion.main, - "3-2": OverworldRegion.main, - "3-3": OverworldRegion.main, - "3-4": OverworldRegion.main, - "3-5": OverworldRegion.main, + "3-1": OverworldRegion.stonehenge_mountain, + "3-2": OverworldRegion.stonehenge_mountain, + "3-3": OverworldRegion.stonehenge_mountain, + "3-4": OverworldRegion.stonehenge_mountain, + "3-5": OverworldRegion.stonehenge_mountain, "3-6": OverworldRegion.main, "4-1": OverworldRegion.main, "4-2": OverworldRegion.main, From cdd460ae150234499d8d61159673081908411e60 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Mon, 3 Apr 2023 19:27:36 -0500 Subject: [PATCH 158/172] The Messenger: some miscellaneous cleanup. Also found a logic bug. oops. (#1654) --- worlds/messenger/Constants.py | 14 +++++++------- worlds/messenger/Regions.py | 4 ++-- worlds/messenger/Rules.py | 11 ++++++----- worlds/messenger/__init__.py | 4 ++-- worlds/messenger/docs/en_The Messenger.md | 10 ++++++---- worlds/messenger/docs/setup_en.md | 13 +++++++------ worlds/messenger/test/TestAccess.py | 4 ++-- worlds/messenger/test/TestLogic.py | 10 +++++----- 8 files changed, 37 insertions(+), 33 deletions(-) diff --git a/worlds/messenger/Constants.py b/worlds/messenger/Constants.py index d57081edac..f967fec8cb 100644 --- a/worlds/messenger/Constants.py +++ b/worlds/messenger/Constants.py @@ -6,7 +6,7 @@ NOTES = [ "Key of Courage", "Key of Love", "Key of Strength", - "Key of Symbiosis" + "Key of Symbiosis", ] PROG_ITEMS = [ @@ -20,18 +20,18 @@ PROG_ITEMS = [ "Sun Crest", "Moon Crest", # "Astral Seed", - # "Astral Tea Leaves" + # "Astral Tea Leaves", ] PHOBEKINS = [ "Necro", "Pyro", "Claustro", - "Acro" + "Acro", ] USEFUL_ITEMS = [ - "Windmill Shuriken" + "Windmill Shuriken", ] # item_name_to_id needs to be deterministic and match upstream @@ -53,7 +53,7 @@ ALL_ITEMS = [ "Moon Crest", *PHOBEKINS, "Power Seal", - "Time Shard" # there's 45 separate instances of this in the client lookup, but hopefully we don't care? + "Time Shard", # there's 45 separate instances of this in the client lookup, but hopefully we don't care? ] # locations @@ -87,7 +87,7 @@ ALWAYS_LOCATIONS = [ "Necro", "Pyro", "Claustro", - "Acro" + "Acro", ] SEALS = [ @@ -149,5 +149,5 @@ SEALS = [ "Elemental Skylands Seal - Air", "Elemental Skylands Seal - Water", - "Elemental Skylands Seal - Fire" + "Elemental Skylands Seal - Fire", ] diff --git a/worlds/messenger/Regions.py b/worlds/messenger/Regions.py index 468c69cfd8..ab84f0b3ce 100644 --- a/worlds/messenger/Regions.py +++ b/worlds/messenger/Regions.py @@ -22,7 +22,7 @@ REGIONS: Dict[str, List[str]] = { "Sunken Shrine": ["Ninja Tabi", "Sun Crest", "Moon Crest", "Key of Love"], "Elemental Skylands": ["Key of Symbiosis"], "Corrupted Future": ["Key of Courage"], - "Music Box": ["Rescue Phantom"] + "Music Box": ["Rescue Phantom"], } """seal locations have the region in their name and may not need to be created so skip them here""" @@ -47,6 +47,6 @@ REGION_CONNECTIONS: Dict[str, Set[str]] = { "Dark Cave": {"Catacombs", "Riviere Turquoise"}, "Riviere Turquoise": set(), "Sunken Shrine": {"Howling Grotto"}, - "Elemental Skylands": set() + "Elemental Skylands": set(), } """Vanilla layout mapping with all Tower HQ portals open. from -> to""" diff --git a/worlds/messenger/Rules.py b/worlds/messenger/Rules.py index 53ea90fe29..a459fdb7d0 100644 --- a/worlds/messenger/Rules.py +++ b/worlds/messenger/Rules.py @@ -32,7 +32,7 @@ class MessengerRules: "Forlorn Temple": lambda state: state.has_all({"Wingsuit", *PHOBEKINS}, self.player), "Glacial Peak": self.has_vertical, "Elemental Skylands": lambda state: state.has("Fairy Bottle", self.player), - "Music Box": lambda state: state.has_all(set(NOTES), self.player) and self.has_vertical(state) + "Music Box": lambda state: state.has_all(set(NOTES), self.player) and self.has_vertical(state), } self.location_rules = { @@ -44,6 +44,7 @@ class MessengerRules: "Howling Grotto Seal - Windy Saws and Balls": self.has_wingsuit, "Howling Grotto Seal - Crushing Pits": lambda state: self.has_wingsuit(state) and self.has_dart(state), # searing crags + "Astral Tea Leaves": lambda state: state.can_reach("Astral Seed", "Location", self.player), "Key of Strength": lambda state: state.has("Power Thistle", self.player), # glacial peak "Glacial Peak Seal - Ice Climbers": self.has_dart, @@ -74,7 +75,7 @@ class MessengerRules: # corrupted future "Key of Courage": lambda state: state.has_all({"Demon King Crown", "Fairy Bottle"}, self.player), # the shop - "Shop Chest": self.has_enough_seals + "Shop Chest": self.has_enough_seals, } def has_wingsuit(self, state: CollectionState) -> bool: @@ -165,7 +166,7 @@ class MessengerChallengeRules(MessengerHardRules): self.region_rules.update({ "Forlorn Temple": lambda state: (self.has_vertical(state) and state.has_all(set(PHOBEKINS), self.player)) or state.has_all({"Wingsuit", "Windmill Shuriken"}, self.player), - "Elemental Skylands": lambda state: self.has_wingsuit(state) or state.has("Fairy Bottle", self.player) + "Elemental Skylands": lambda state: self.has_wingsuit(state) or state.has("Fairy Bottle", self.player), }) self.location_rules.update({ @@ -188,7 +189,7 @@ class MessengerOOBRules(MessengerRules): self.region_rules = { "Elemental Skylands": lambda state: state.has_any({"Wingsuit", "Rope Dart", "Fairy Bottle"}, self.player), - "Music Box": lambda state: state.has_all(set(NOTES), self.player) + "Music Box": lambda state: state.has_all(set(NOTES), self.player), } self.location_rules = { @@ -203,7 +204,7 @@ class MessengerOOBRules(MessengerRules): "Underworld Seal - Fireball Wave": lambda state: state.has_any({"Wingsuit", "Windmill Shuriken"}, self.player), "Tower of Time Seal - Time Waster Seal": self.has_dart, - "Shop Chest": self.has_enough_seals + "Shop Chest": self.has_enough_seals, } def set_messenger_rules(self) -> None: diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index 0aa71506c7..d23f4da34f 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -20,7 +20,7 @@ class MessengerWeb(WebWorld): "English", "setup_en.md", "setup/en", - ["alwaysintreble"] + ["alwaysintreble"], ) tutorials = [tut_en] @@ -89,7 +89,7 @@ class MessengerWorld(World): if item not in { "Power Seal", "Time Shard", *NOTES, - *{collected_item.name for collected_item in self.multiworld.precollected_items[self.player]} + *{collected_item.name for collected_item in self.multiworld.precollected_items[self.player]}, # this is a set and currently won't create items for anything that appears in here at all # if we get in a position where this can have duplicates of items that aren't Power Seals # or Time shards, this will need to be redone. diff --git a/worlds/messenger/docs/en_The Messenger.md b/worlds/messenger/docs/en_The Messenger.md index bc43369008..e25be4b907 100644 --- a/worlds/messenger/docs/en_The Messenger.md +++ b/worlds/messenger/docs/en_The Messenger.md @@ -56,20 +56,22 @@ for it. The groups you can use for The Messenger are: is recommended to quit to title and reload the save * After reaching ninja village a teleport option is added to the menu to reach it quickly * Toggle Windmill Shuriken button is added to option menu once the item is received +* The mod option menu will also have a hint item button, as well as a release and collect button that are all placed when + the player fulfills the necessary conditions. ## Currently known issues * Necro cutscene will sometimes not play correctly, but will still reward the item * Ruxxtin Coffin cutscene will sometimes not play correctly, but will still reward the item -* If you receive the Fairy Bottle while in Quillshroom Marsh, The Decurse Queen cutscene will not play. You can exit +* If you receive the Fairy Bottle while in Quillshroom Marsh, The De-curse Queen cutscene will not play. You can exit to Searing Crags and re-enter to get it to play correctly. * If you defeat Barma'thazël, the cutscene afterward will not play correctly since that is what normally transitions you to 2nd quest. The game will not kill you if you fall here, so you can teleport to HQ at any point after defeating him. * Sometimes upon teleporting back to HQ, Ninja will run left and enter a different portal than the one entered by the - player. + player. This may also cause a softlock. * Text entry menus don't accept controller input -* Opening the shop chest in power seal hunt mode from the tower of time HQ will soft lock the game. +* Opening the shop chest in power seal hunt mode from the tower of time HQ will softlock the game. ## What do I do if I have a problem? If you believe something happened that isn't intended, please get the `log.txt`from the folder of your game installation -and send a bug report either on github or the [Archipelago Discord Server](http://archipelago.gg/discord) +and send a bug report either on GitHub or the [Archipelago Discord Server](http://archipelago.gg/discord) diff --git a/worlds/messenger/docs/setup_en.md b/worlds/messenger/docs/setup_en.md index d4a3eafa0e..0a57c2b83f 100644 --- a/worlds/messenger/docs/setup_en.md +++ b/worlds/messenger/docs/setup_en.md @@ -10,10 +10,11 @@ ## Installation -1. Download and install Courier Mod Loader using the instructions on the release page +1. Read the [Game Info Page](../../../../games/The%20Messenger/info/en) for how the game works, caveats and known issues +2. Download and install Courier Mod Loader using the instructions on the release page * [Latest release is currently 0.7.1](https://github.com/Brokemia/Courier/releases) -2. Download and install the randomizer mod - 1. Download the latest TheMessengerRandomizerAP.zip from the +3. Download and install the randomizer mod + 1. Download the latest TheMessengerRandomizerAP.zip from [The Messenger Randomizer Mod AP releases page](https://github.com/alwaysintreble/TheMessengerRandomizerModAP/releases) 2. Extract the zip file to `TheMessenger/Mods/` of your game's install location * You cannot have both the non-AP randomizer and the AP randomizer installed at the same time. The AP randomizer @@ -35,10 +36,10 @@ 2. Navigate to `Options > Third Party Mod Options` 3. Select `Reset Randomizer File Slots` * This will set up all of your save slots with new randomizer save files. You can have up to 3 randomizer files at a - time, but must do this step again to start new runs afterwards. + time, but must do this step again to start new runs afterward. 4. Enter connection info using the relevant option buttons - * **The game is limited to alphanumerical characters and `-` so when entering the host name replace `.` with ` ` and - ensure that your player name when generating a settings file follows these constrictions** + * **The game is limited to alphanumerical characters and `-` so when entering the host name replace `.` with ` `. + Ensure that your player name when generating a settings file follows these constrictions** * This defaults to `archipelago.gg` and does not need to be manually changed if connecting to a game hosted on the website. 5. Select the `Connect to Archipelago` button diff --git a/worlds/messenger/test/TestAccess.py b/worlds/messenger/test/TestAccess.py index 83bdc6113d..84b29406c2 100644 --- a/worlds/messenger/test/TestAccess.py +++ b/worlds/messenger/test/TestAccess.py @@ -35,7 +35,7 @@ class AccessTest(MessengerTestBase): "Tower of Time Seal - Lantern Climb", "Tower of Time Seal - Arcane Orbs", "Underworld Seal - Sharp and Windy Climb", "Underworld Seal - Fireball Wave", "Elemental Skylands Seal - Air", "Forlorn Temple Seal - Rocket Maze", - "Forlorn Temple Seal - Rocket Sunset", "Astral Seed"] + "Forlorn Temple Seal - Rocket Sunset", "Astral Seed", "Astral Tea Leaves"] items = [["Wingsuit"]] self.assertAccessDependency(locations, items) @@ -116,7 +116,7 @@ class AccessTest(MessengerTestBase): class ItemsAccessTest(MessengerTestBase): options = { "shuffle_seals": "false", - "accessibility": "items" + "accessibility": "items", } def testSelfLockingItems(self) -> None: diff --git a/worlds/messenger/test/TestLogic.py b/worlds/messenger/test/TestLogic.py index 4f9e1ecc36..2fd8111030 100644 --- a/worlds/messenger/test/TestLogic.py +++ b/worlds/messenger/test/TestLogic.py @@ -4,7 +4,7 @@ from . import MessengerTestBase class HardLogicTest(MessengerTestBase): options = { - "logic_level": "hard" + "logic_level": "hard", } def testVertical(self) -> None: @@ -14,7 +14,7 @@ class HardLogicTest(MessengerTestBase): "Tower of Time Seal - Time Waster Seal", "Tower of Time Seal - Lantern Climb", "Tower of Time Seal - Arcane Orbs", # ninja village - "Candle", "Astral Seed", "Ninja Village Seal - Tree House", + "Candle", "Astral Seed", "Ninja Village Seal - Tree House", "Astral Tea Leaves", # autumn hills "Climbing Claws", "Key of Hope", "Autumn Hills Seal - Trip Saws", "Autumn Hills Seal - Double Swing Saws", @@ -54,7 +54,7 @@ class HardLogicTest(MessengerTestBase): windmill_locs = [ "Key of Strength", "Key of Symbiosis", - "Underworld Seal - Fireball Wave" + "Underworld Seal - Fireball Wave", ] for loc in windmill_locs: with self.subTest("can't reach location with nothing", location=loc): @@ -80,13 +80,13 @@ class HardLogicTest(MessengerTestBase): class ChallengingLogicTest(MessengerTestBase): options = { "shuffle_seals": "false", - "logic_level": "challenging" + "logic_level": "challenging", } class NoLogicTest(MessengerTestBase): options = { - "logic_level": "oob" + "logic_level": "oob", } def testAccess(self) -> None: From cbf72becc1ece4bf73628664a883966d4282113e Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 23 Mar 2023 21:21:11 +0100 Subject: [PATCH 159/172] Subnautica: group items and removal of item pool option --- worlds/subnautica/Items.py | 25 +++++++++++++++-- worlds/subnautica/Options.py | 9 ------- worlds/subnautica/__init__.py | 51 ++++++++++++++++++++--------------- 3 files changed, 52 insertions(+), 33 deletions(-) diff --git a/worlds/subnautica/Items.py b/worlds/subnautica/Items.py index cf1f8ed24a..600e1d1996 100644 --- a/worlds/subnautica/Items.py +++ b/worlds/subnautica/Items.py @@ -40,8 +40,8 @@ item_table: Dict[int, ItemDict] = { 'tech_type': 'CyclopsThermalReactorModule'}, 35007: {'classification': ItemClassification.filler, 'count': 1, - 'name': 'Stillsuit', - 'tech_type': 'WaterFiltrationSuitFragment'}, + 'name': 'Water Filtration Suit', + 'tech_type': 'WaterFiltrationSuit'}, 35008: {'classification': ItemClassification.progression, 'count': 1, 'name': 'Alien Containment', @@ -359,6 +359,18 @@ item_table: Dict[int, ItemDict] = { 'count': 0, 'name': 'Partition Door', 'tech_type': 'BasePartitionDoor'}, + # new items that the mod implements + + # Awards all furniture as a bundle + 35100: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Furniture', + 'tech_type': 'Furniture'}, + # Awards all farming blueprints as a bundle + 35101: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Farming', + 'tech_type': 'Farming'}, } advancement_item_names: Set[str] = set() @@ -371,8 +383,15 @@ for item_id, item_data in item_table.items(): else: non_advancement_item_names.add(item_name) +group_items: Dict[int, Set[int]] = { + 35100: {35025, 35047, 35048, 35056, 35057, 35058, 35059, 35060, 35061, 35062, 35063, 35064, 35065, 35067, 35068, + 35069, 35070, 35073, 35074}, + 35101: {35049, 35050, 35051, 35071, 35072, 35074} +} + if False: # turn to True to export for Subnautica mod from .Locations import location_table + from NetUtils import encode itemcount = sum(item_data["count"] for item_data in item_table.values()) assert itemcount == len(location_table), f"{itemcount} != {len(location_table)}" payload = {item_id: item_data["tech_type"] for item_id, item_data in item_table.items()} @@ -380,3 +399,5 @@ if False: # turn to True to export for Subnautica mod with open("items.json", "w") as f: json.dump(payload, f) + with open("group_items.json", "w") as f: + f.write(encode(group_items)) diff --git a/worlds/subnautica/Options.py b/worlds/subnautica/Options.py index 03834cdbba..91c4866142 100644 --- a/worlds/subnautica/Options.py +++ b/worlds/subnautica/Options.py @@ -35,14 +35,6 @@ class EarlySeaglide(DefaultOnToggle): display_name = "Early Seaglide" -class ItemPool(Choice): - """Valuable item pool leaves all filler items in their vanilla locations and - creates random duplicates of important items into freed spots.""" - display_name = "Item Pool" - option_standard = 0 - option_valuable = 1 - - class Goal(Choice): """Goal to complete. Launch: Leave the planet. @@ -108,7 +100,6 @@ class SubnauticaDeathLink(DeathLink): options = { "swim_rule": SwimRule, "early_seaglide": EarlySeaglide, - "item_pool": ItemPool, "goal": Goal, "creature_scans": CreatureScans, "creature_scan_logic": AggressiveScanLogic, diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index b786bcc474..bc1e4f696c 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -1,4 +1,7 @@ +from __future__ import annotations + import logging +import itertools from typing import List, Dict, Any from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification @@ -7,7 +10,7 @@ from . import Items from . import Locations from . import Creatures from . import Options -from .Items import item_table +from .Items import item_table, group_items from .Rules import set_rules logger = logging.getLogger("Subnautica") @@ -84,37 +87,41 @@ class SubnauticaWorld(World): def create_items(self): # Generate item pool - pool = [] + pool: List[SubnauticaItem] = [] extras = self.multiworld.creature_scans[self.player].value - valuable = self.multiworld.item_pool[self.player] == Options.ItemPool.option_valuable - for item in item_table.values(): - for i in range(item["count"]): - subnautica_item = self.create_item(item["name"]) - if item["name"] == "Neptune Launch Platform": - self.multiworld.get_location("Aurora - Captain Data Terminal", self.player).place_locked_item( - subnautica_item) - elif valuable and ItemClassification.filler == item["classification"]: - extras += 1 - else: - pool.append(subnautica_item) - for item_name in self.multiworld.random.choices(sorted(Items.advancement_item_names - {"Neptune Launch Platform"}), - k=extras): + grouped = set(itertools.chain.from_iterable(group_items.values())) + + for item_id, item in item_table.items(): + if item_id in grouped: + extras += item["count"] + else: + for i in range(item["count"]): + subnautica_item = self.create_item(item["name"]) + if item["name"] == "Neptune Launch Platform": + self.multiworld.get_location("Aurora - Captain Data Terminal", self.player).place_locked_item( + subnautica_item) + else: + pool.append(subnautica_item) + + group_amount: int = 3 + assert len(group_items) * group_amount <= extras + for name in ("Furniture", "Farming"): + for _ in range(group_amount): + pool.append(self.create_item(name)) + extras -= group_amount + + for item_name in self.multiworld.random.choices( + sorted(Items.advancement_item_names - {"Neptune Launch Platform"}), k=extras): item = self.create_item(item_name) - item.classification = ItemClassification.filler # as it's an extra, just fast-fill it somewhere pool.append(item) self.multiworld.itempool += pool def fill_slot_data(self) -> Dict[str, Any]: goal: Options.Goal = self.multiworld.goal[self.player] - item_pool: Options.ItemPool = self.multiworld.item_pool[self.player] swim_rule: Options.SwimRule = self.multiworld.swim_rule[self.player] vanilla_tech: List[str] = [] - if item_pool == Options.ItemPool.option_valuable: - for item in Items.item_table.values(): - if item["classification"] == ItemClassification.filler: - vanilla_tech.append(item["tech_type"]) slot_data: Dict[str, Any] = { "goal": goal.current_key, @@ -126,7 +133,7 @@ class SubnauticaWorld(World): return slot_data - def create_item(self, name: str) -> Item: + def create_item(self, name: str) -> SubnauticaItem: item_id: int = self.item_name_to_id[name] return SubnauticaItem(name, From eb503adb13f289a491f5b94b2cc5523f742d9994 Mon Sep 17 00:00:00 2001 From: toasterparty Date: Mon, 3 Apr 2023 21:53:05 -0700 Subject: [PATCH 160/172] [OC2] Only Calculate Priority Locations Once (#1662) --- worlds/overcooked2/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/overcooked2/__init__.py b/worlds/overcooked2/__init__.py index f481b3e556..d28fc23947 100644 --- a/worlds/overcooked2/__init__.py +++ b/worlds/overcooked2/__init__.py @@ -249,8 +249,9 @@ class Overcooked2World(World): self.level_mapping = None def set_location_priority(self) -> None: + priority_locations = self.get_priority_locations() for level in Overcooked2Level(): - if level.level_id in self.get_priority_locations(): + if level.level_id in priority_locations: location: Location = self.multiworld.get_location(level.location_name_item, self.player) location.progress_type = LocationProgressType.PRIORITY From a86fd378600c452f952bf563b40eae7221391d75 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 4 Apr 2023 12:29:20 -0500 Subject: [PATCH 161/172] Core: set convert_name_groups to true for LocationSet (#1663) --- Options.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Options.py b/Options.py index 8c73962602..48d802b5b1 100644 --- a/Options.py +++ b/Options.py @@ -904,6 +904,7 @@ class StartHints(ItemSet): class LocationSet(OptionSet): verify_location_name = True + convert_name_groups = True class StartLocationHints(LocationSet): From 8f52e4654f56d74e28224daca0e2967795d20fc0 Mon Sep 17 00:00:00 2001 From: JusticePS <5125765+JusticePS@users.noreply.github.com> Date: Tue, 4 Apr 2023 10:32:03 -0700 Subject: [PATCH 162/172] Adventure: Change user_path to local_path for locating basepatch file (#1639) --- AdventureClient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AdventureClient.py b/AdventureClient.py index c4258993c3..06eea5215c 100644 --- a/AdventureClient.py +++ b/AdventureClient.py @@ -436,7 +436,7 @@ async def patch_and_run_game(patch_file, ctx): logger.info(msg, extra={'compact_gui': True}) ctx.gui_error('Error', msg) - with open(Utils.user_path("data", "adventure_basepatch.bsdiff4"), "rb") as file: + with open(Utils.local_path("data", "adventure_basepatch.bsdiff4"), "rb") as file: basepatch = bytes(file.read()) base_patched_rom_data = bsdiff4.patch(base_rom, basepatch) From 03aa9b3604f5a687ab2d19ac43b2153fc2a09b26 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 4 Apr 2023 23:38:23 -0500 Subject: [PATCH 163/172] Update AP icons (#1637) --- data/icon.ico | Bin 219326 -> 261182 bytes data/icon.png | Bin 35062 -> 109228 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/data/icon.ico b/data/icon.ico index a142b56b143dff85a9f1f04bba0b9435aa1461c0..9e13df8df1a7fd6bbf6bf96de50b51f5b9d6fff8 100644 GIT binary patch literal 261182 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

E<&D4bXd*g3 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

iwK=#={0f9n62~zed$pd+KB#s=N#O#yP z0$v;uQ$nC1+$2xPDPf3F`T;P(vqwS#GLVxXjN}|r;!-PtGh-N}PQbzi{KwD!DeZT6 zRPIkB0-ja?sFoYxE*SxL)c_zOW?K=bTTQLtn>SFigN-3GMkok0JcuxFQx

G-P6$0AP^aX-PI&`fAwwz(U(5M=O97nb|6A>Di_z_d z`RN`i{rfOzyi#rnhh%%XAjVQ_fF&G&4E9<~C_9A_NxCHFfJ_k)B;O?uN&|8lkR8Jr z&A zGqZcs_iuM~?b?V=GXO@1azq{GHp+V<_fI_hDE;Zb`Xlt~XZ4g*^AVcaH)SnqM0pybwHpnz` z2v}%U-n z-?NRN%?}=3Xc?!X0Kfg^ucx=)d)r;Reih5b3Jdl6OKD!s?I(6*C4D8nCoKP<75uyk ztW#ER5n!(tdqwseEBT#N{P#xb->%blcZOZ_NkagTol3llLMmhlACp%jfShCqPy~o{ z5m@X~bb|*;o|8O8_J$TyK4EBbQ*c4^`Uwk?niAd#pTsLmUyKFo|1mQ2ttgQZ z1$CFG(uK!RTJio(=~ER7z%+yiU}6x|Gg1)83WSpz*C_nzt7Z&%YC;2~MnHuW)I5RA z8J_+4n#Sya!Vl08K}3XSZPrA9*8)O;5_xz;j+|VAi@GncIAa|a*yIfc9w0@&oEQNq zVc1Gc=tqkG1cmxT z7;6Cdiu6~f*%$&ar~GZ)UBy2=xW&0sVhj<8QJymcjsWk0t|N3F@LtNHg6AyHJG&NX zvbpR+1!QxRx&aR47+|OIRWS^pDVSEPTvR&2gMjG49&j1dEt$!8NNE5^f)5THr6zd@ z9$)^_pV0pP!#9(QIW%jI{MQow<_dz;n?SoEoE8hb*^Q_t^G@8f*<(%-dx!}b-+`Hw~ zV)is}4~L{_d;kC-07*naR0i{c?3H*KeChzM~*-uENF%Q z@yq`MJ$CNVTVeg*Nx45Q1@NwpyKVqfWWZgUfo%X*@S~22*1f~+QRfGnH{dWO9RWAl z5FUHKYmGZB` z-e)?|o6j%+pdT`hjx(-Yi@0(PI6P(?9x*msM&C2~K}%w?{;oLKc=#cQ;DPtASrHH* zNRJXY2b@1gIKS`k$tOLwJ#c=X@#x1qE}nPjLLFE9;A?fx7?>QMw7VclSyrx6{z`Zy zOPv5&*jDtl*Nw+P%^hYf^b(W_zU?D^?Nh&vr#|`YKtwl_i#7-TwBjG9Z31&__noo8 zTl56vHqXR!b)03{@$Ev7531aM`r}XIPrv@(4&VI2-*-fkrypqS=xK5IV1!;v?aPe7 z@^?|-4#5ZVEJc0{-giXkyhkVm;tK8`QvfAfsPg{Jn>U4D$MdHOzc$LJaDQt6=qIZG z@1CKPDZS12n?Ii*3KGL?J_b+t0g{?GW(RCCm~}&OA&ENz(BBSrA|qKISR)EK(|<8v$=*p{_G#&cb@**S0tRq+#PB}Dd%~OVBy6^f z;3K>T_V)-+KIU=$9Ps$ZfX5&0uv*?3SmQ*9piHARd6!%5oZN0HFeK@ML{VqW>H%0N zzzKmUGYKIuGp7MD^?3M^Pvdi+`Q7ZCJB#wS=XMR-^IY7o&ik)7hPgDLImrLEHiJ_X z_+8&zDh|+{lzR&i)a8M1Meg12e&MUQeEkYN|Ju*tLDKj~IX+6fW3RD)=Nx>7WCS=# z6YhMV688teC1APK)MCvpFhyUC0CgY=^lu)2QTR2T#9V?g5eDJweQBoiR00{2PVD9V5aKLKmK2WG*jw>L0qID7^TVI+2XvLJw03b+#b~OfA zx}XInWG2d)xN|Ff!?%- zE$M}R3;`IC(3t;LD%^;F1Wd?$Cn5bQIk<)I@5`7&5$sK_4FZ(V0Q1N>uvSA>t~hf> zcEZ$mEd4wZdFgb>QoYergr|r=QY6W-c>O~fFlgB(uwyE`00Maeq8I?m|C6Q^af1ut zJpR{j{LlS`)%jZu`Q0LMWp#zzib0fSac=)1BbDtMEtGv41lVo@^`j*O z?TP`G4d6qp{B7I`xv!0ZngdWb392XR)JZ$Px3~OvzxA*4H^2LLZqsiOh~QbW0K6vz z=ZG?ZZRW1?=(>)~v+ooUv^oOb$$J($2+AD_weqhrg6bkZh7y*vy$Z&!tNGnGMSnY^ zoH9-JJzK{aSqQoywQd7KyE<+sSpj2=E_mzuS1JF>X>GNd$m?9Kt+5r#M(FeiPVfRuq)8ww;L zg3OG)r6|ReM7i$o5&8%OC!bjaVAC^l9<7s{?b_@j)LT*DH8aZVAu{>_I9?~b{&vFt z9^sQuW_;=?!jq3VtX3ZG0~!OhAt2!ZVf2!rPF63sUYYm!5Sh37uY8ck3gORd>XWNfCDj+V*-IV_6a@> zB1{hOU<3~MlP~_e_~lRhraPnD?=t$^ES3+Q+}~>i0GMu|hSSFCxwfk6*kEmCwYkzY z7od#^?iv$s;o%DpqaOwwua8F*AcVs3Cs1MhkDa~hv_4ZtwM4iEKIRbo?d^88AuuwcjU1>3 z01+rDQIH&B3~x#Q=Au{CyP0?Z|y? z>|1rvI#Xa*TA+i!o=EC_cqtInc?Tv@^b0RpK?d`ild!PN zAqc}?_(QIW_18K6Rqqe9F#pZY_Cr4Es72jX3>q%56heYl0VPPoE}6HK&BG^T3LH5~ z0D*il3LJ#b;k<+l9C@Opv^K;(+90D*`)apYa z_xB?NV9#@L!8Ru7F?#-M$Of>||IeidS$ombP=MK+?VQ8ok37a#+*OKkD5-was(zyd zUpLn%2fhX6zu88-{#L?^zleC}a>AyU8u?s@1XLZdKH9_Q46HnXys-XlECOMFpK$GZ zMjzYtq{czTo6Z2%+YACQM5&rb8vmCeMnLc+@qY^^FtEQ81-#ixSc284@H+dRfYp)^ zqo5Hf0tZKomwuV>%IgV_KH~7HPkKD_Nnmev<`VDIDaPyqvEeU z0fPrJ8tWf>aM#y$)^WTZ@cP>sufLV?i(mHW z2T_(r);sBgPH7AVv71q_-7*#nQL>vYW4mP>uM;lz4mmS|CwK=8gT(uj*1`5P#vN1C zkT?O3w0F3dP=K`V1;Ca?Fbz2zZy3vkpbnWCi=gMHr=AJudoc{krDX|Y3Kw?oIZs^55JeRG+I~o7_*^Xr40mmP_}0U-+%`gP*_P-aouX zoqGFTo69U0i{klr!61Gi4t8No3-ne`zzD*X;y2HKoRsRs=I-(bFbYm6d`39wL5MYG^jucHtK)O5MY)tt7G8@)D;qa5>9|iL*W8=>fy)( zS+Xm{DKKqS)Zn;0kHODilFRqrqIitTt`Pwd?9~!y8=OKVoHUm@KwI^@n zpc?+0V^^AIFvkwIn|mga^4`r^#cEGc;E>7zAC8H-LlbJke_E}PF9hGOx2yjk*kDNgNrRIRL?(O=#lWU8S)f19& z{YJ+3e%#~bHweG^8HZ0i!Pr~*3GwhOhQP_NKz$5?^8t%)$)$qlj}r6$#JEMd-{N%TKW7BgWWYKb2snf6pWoYe-+1;b{JoccN=KXZ*eJ*c zMFl)u1{SWXlzu32eya>>%HNQsLhz$z=eXGxe}*%+Bj!FM9%b}bBJvd=A2QM*BX1bo zGjh*mW!3H3!ov;}fcQR_P#tObjvV066a0CH?h_u|19YAd9`)!hIx!e@eclnn;Dj-t zSp=+zi5UXg0(ep;Ik^%Bkb-Q=-y#n`Y~kSW@ZLlC+B08H=U3-;&1h{D?1KJlnXL`} zTH)_f{tu+w-wzG|Fh9W-3p8eMZ2+|86ziB^O&+pMVV5^>CPRRTXm7R0{m|3YgB%t9 zS#MI#z`IuyzWu!o4h}Om8}Y#NF0hABzU>3y;st>ZkJk*Yh=4IMu3k@YP8E3KqLdjx z>>1>!yaq63^E2QBBLsOfMTT35CBjTdd75$HJs|`tA&FrCwi||1F0I7#Al>&M28eAg zUbFGc$Z<++B;9aw25uZMe)vLTsH`cG|)t}*oj+6}=tET`1~|0pp3=f+v&-jw)h2!MqEYP6t!HpBm{*+2vU z%Wgqm|ID-LC$Ig&Z->5+{1zdU%6(lBZjhmag%41&g+_$%WIEq%&RF$j9vJIOJ@O?& zyqXcOX5>R52;~T{DSK6sB z+&*FPq=!2XxW|Fz;~m`U4z1ZLL_j1x2_$5~AT0ux^4M6-kprdb|8N+xmzBbV{caDR zd-5~!q5TUrDs(dw%b@>=DdN}4d+YgssO0{>7yvhc1Px*_g3qo>OYM8@f;_? z{=CN{5Bcf5iMX6F1SZHyF#_cJG=wvE^Qd{Qp}0AZ;AZUqId5QY{o0zf_5YEA{@Zaz zxo-`C))1&O0Rm2@2Da$m%`yWfe_Jh<{@G`KgI{^)O(_$u%D)Q|66gvscx>cTGyF?S zUbA{n`|%d)o0+e)&Dgx#gRc_ewT$6XM!cHDt8czbPG!3_WgV5mU~Zp~H!SbP=`olY zlxdtby*ry%v}SlBu*z*dX2kb0I0J4;I64;*9`#r}?tv#VmLCi7EBApy87)`9i~)u? zHCYVj_U^m2C zdssE>(mwj;`DPgbz%7OYM4>rw=JNV1W4%rI@rym4e{sOkah8&~-~#oxx4FIR43rRo z^O90`a17~Vtsi!7RQ8-0tb!{_Rv`NXf#nxV!fHuauc5_*iY5hD-U(+&oqdHZ_f~|9 z=Osg6oy2V)A|pj9jA43fy%zcqlGauiiuA^U7z8MW6lVuNjvS@oHGJ!6WCP+tW5*D=Mdn17^lg- z837I>8F0=3SVHEuP6KSi0(D)7TOtJSFIV)LPdv@fyTIhrTqO=@jGV<|BPP$ z4g@f+6Z$t2`Zp7X%L(a7pnU2G+;j2R=Z`lYl$?6^ii2bZc%vUliWJx^=!2Qz0)Ya7 zx6(b2dmht=?D_!9umvYZx&fpc5!+WHXhAsYBD%*NR!;}Gr-8-AaQntU8y+CiPNA{~ z<%;X$$iZa?%7lgQ@X+2x>@8Qjy#3Y$yV<&&3-`62ey#i;c)7oC27q2{W}#}g-ayN` zY;&&LJj~YnpF;s|Yjz2uya{$wTfR(;+2z=xI9Bp0#1@5xW96m^Uq~<6QG;|H7ZERf)~y? z#0;$0j2Id3UXhcsJbGpbQAiI|>s15R4YI48AW3CRz)=)rp>U#jydFae3L#SG>FAj8 zlb=Vt_IAc^ezwEYpO8=ltP>d;Zbs$DpUe=r`EA_sz6t7CNPxEz2P_cahdt;2EaMcp zH@~x%*&5=nGXks}u&>^;62fg(z%ID=)J3scEa{VvJ%Q^-2Z$+EEPspfkG(%YRbIbb zH^=L_e547ChPXxe5ZHLzJ zkk;FpBVg?^5bDp{PV-hJ+W$jRk?c5(G1g2T#z#MBY#Uz_D! z*KcI}PfYj{YU!_sYeX5=Z zOpO8K0JZ5>RvgF-gBl?)4pD9V*K&WRMX?AS9=-4|A8po@IG1KVZPT7JL;lT3F=M!# zuzfjU`({GCn!#4Nt1?-*28pXdkQrjj${|>v-q{6Z#(Lh=n*hK`QxaLd`6E;^G`$** zYA6uHfr^t-{-)f6(oTzjbi_z6MbN9j=0e8e>5T3fLif0b3%4kr)uqZEmHmd{KwdHh zyf$ZYQ`B$QzQ0SxP5HO!e=Wji4^#f9G5;SpxxeoQK-Dm04vNV$uh06hiLU4{A50tQ z`A&=gX5h6q2R!#rYrJ>s&VaAs}OL+Q|oooO+ z-r})9(;Xjkn?%hCm@5iW=l<^61lslgt1w!x%Z&b-6u683Q)mCQuZS+)T4+y1@nC0P<%07(;1MLIE8Y+n6xl&b;!L0}! z#W>&uq#KOwO9}leKzJ-;`HY8q2Iy|uhC|0>5^T?qTTA-mRYk>JUVn9j6R%rc(~^Eq z0OrOnsIHFZ*LnUn=B&8-y$`(8GJs3( zM*RJEkMPQCeetwyT)f~)x?Aah1=f5j$d2We^(09}S~3+;PP1{C*Gr3W1;X@SkA#JwMqS%9OYBqp$1Z zopJ1vyE&O|(*Wm+ezmnf+BI2+vmY3_f6xp7YM$-?2IaNFJoo*%_va@2wv2(4ffs(h z!FT`Z7{?n%2xBpv@)69Y*Q2cYr*s6sd(A(poA#K zR(ZJQ|Mh3|Y9~hL93LfBfB%&oGmU~;1fVqrTJOuAcNq!xXzv?v8?p~TiDz=gH@kV9 zf#KbR>)+WR-N;I;Q-gljj)bW#K%U?ql2Bki)(jr~BS%KOngB;qyCZQ)(*vuapmJZ1 z-Jp`>85EpYU|Bs4^Egxn%g``j**SZF*Q}fXRl*FEK!F(rfP@{YdLD(?sDDG2@W9bO z_3*!R*!w)tJ>|!Yp=ydIH%|9~J1}-30WAlh&IFjN2H5thsXhM(PVOHR1E3bndQ+Xt z=dIswANBF>_!uJNx$hm}+uuI|6TvGHFTp&0t$UwDIWmg}d+~ykihFBm=X>#jbi#Z8 zYEo}p2zXtm-Z=w7$#k65gI2G+^Mu~Bmh=UNo=dDebFCbk3AXo03nLKViO>Z}!z<)M zH4277l&~pzA|>^a9mD#0%Wr#EZHy|VLY9KI>4oMZ6VC6;ym^O7k)b)qy)ODQc`iGF zwD&zYWc=fgB97L;XMf#e(cRG{FlPkR#b(^h`>iK&g8cKKcmWTEwlM$$Mt ztMyJn)n1Ym!Bn9FQc4M0cJP=bkT!4nJv;ZU?v77lJih$ z;0zH;ujgH?mrD6B2yO}F4J2*Z{;oU$D^bi_M!b}8_-;b?l*iteJ;KAjnl}4xMZUbV zW6lVeLju~=-}ZIQ`>Bn9e=z{+!Pc>5Dwu6q&-&56-x>$)1NZ>KZX+}B{pXMI+>efs zRG4zMDtoMOzIx78v>`EYyvYLPLpB~3amgZ58R!UZ zNyu9k?;WCC-HMRc#swHpfE6|**eqy*pChrsZQpZKPs2(G+l*2z z4SB3pLi%9>Nw~lop<#Ng{td~DSbjQV`MDrrifZofg7R-iyB>Fi1!@BDTsr#1`+t!5 zKKI=Kz;3U;&gZQwg0-*1%;#I9-~)}68UOIzL%i_P8r#7FT(UNXVHARXD=_%}9^vp< z$bOE*OYarRj**3x5A&2IeQ%-UKUpHImGb1+%mUA^x1_&T)ak{PQ}(L`AuJsF?dbgzxRmY7ei704z}LSV(A{d9QFR*n zYzm@Xr*mtw%|ov}O=^zC%uN4+0+cbQ{A=ah8VL3G<_v*d4_AHfj_kxl;t?OeJmB#A z8}yf>6xdn-%cndxuQAYTyW95c%}A}$0SDpViMl8mLS>}NAV`o8JBU(NYnu~50dV_H zIW;_)QPLV92HBGb!UYG8Kw2|6GKP0$T8XJTKN=^mHH{%iqGkWoE1wg-<|D@XrxE?@8T-$6@K1{~bQhKAX$P*6s?&HXbIkt-PWc1(*#J=Qe@^k+Mp{?* zvgfw(&s-0HIh5eGjDcZb{LQzog!guOH9{nYY_a45l{_9 z0qUF7$|~)6+-$VQ#2xeg>yeH!4*zM3lsJi?a{sIvHz70{$l(6+a9f9 z-pczfXRhE}*^uSJoXHOmLO~#-9^%cDM z@&>^J&wR4OjYGknQ_k?JC<%F$n@(4)W9s7y{x+MWjB~6bU;OZE>69G>Vh(m^j`It#eU8@&B znib%H{S{$IvVoW}67zT|7Q;WkuW6BLKunqfX-FX^O!o#gOhyJUA?qzRG(>Tce{PN8y#!7`-ZIEBSUv1)6vXOm#0ysAzsd-3##>3k z?gs&REfoPp9z^hJgI%n10xe1S02}?Aas<-a*!$Jz?I#(9CA72c-}-HtVK8Y4O7)j#X5VC7=ZTmAC&o$tdP2IMvH#T$-6wrXb-s;J zuakQ1)p9#0AC+q4Lbt5>JtFlo?%cSOg$OLRLjN zMph+7aIp~2d>B}e0Vn6)1u-%qX4!pSNhnip4Si~z#zdB-srD73Q&~=-sOGO8*jT2&u!MN!GC9b#rm8h{Zb|Sfhjc6pnHJihhs5*EO7N9}Iu6vK zeAewdVa^|GH^704-^r?)7yuHGBa3n2ASYv}gYFwts(Ohr;DXc@;DIIOhyXH7D+9EU z@P=>#zaSoorpSR~qyxs`e;u&;T*lrPgBU+| zZq)0qbtvb0PPFT=y*dCOO$UGAWc{dq6pWUR6P3eB&b-v7tT8*%_blSrza*x2oj_36`spk8tiz_B@_Unu@j&JnB`cR zMX1aOXAMg@0?zF@Jao}vy%o=Yp&v?7Awf_<9UKXN02y#ej07nSDoWfr1q66B_$|G! zaFEoCDKY#kH3qzs`4@CRK48SZ;7P9+g|{HJN@P=OI}A$J0^#y|8PEN&$M65ng7;Rp z6@=TR{OfLXweq)zy;rvao%0OxuecFV&+S<^-`41#+mPD~ja~b|*#`sL%Mn-q{s_k} z_fjCI8sUNiTNsWZo<3)(qz5(jb=M_F$dR=|CWD6z&XV(=dSUk$v@0_5AmIiizy+-j zprwG*nkVNtYE&^i*Z~x1%p1-OctSW&poN4F5T)HNb%gGsM>^Kb1C!$*Mgs-;f8C#; zbSJE4nh0WKY~%o(<#$VK;%e%OR5G+Ukq{a=K`JG(35)KT17q{Eh;)^)_gewsVfR7q zY-@nF>u*;_O4CVmPH1b0K9Gj~JvRV&q;Bt0{%x#(i7B+&*8M)s2)LOiaAL`%#Q5f4 zy^H5xJi>D6km7jWDGS+-SrpZ>b5giRbLf*Q*Uctlz0TqVtP^6CY=E2z!8>qPcwK|| zIqPKjQP>WSaxoT&1bJ9=;*Ap-5s#3PDBs`-VDat&^SVXrrYs6P*zv4(e}&31ap4IB zFJ*NUTNF62+AJ9?}2~1#?$5#lsvl=R*908Au)+23 z9U~oUMZFWnw;&494(U=>J`ZmEWy(qc?5-!MDLUAi627462;+QK~93z6a zQ=OP_qX-%&2HAI<%6$fS&6+i!EPI3RNsVie)Chngj6FbB5zt}e0_g8ieK#4x{#QN& z_exftsR05-91-Q1lOjoWIJ;ga6W#V9!GVbJv4;XuV%#`j3^8M~l{h|mDUrMA9QOAe4v#WY9-H%UM=&B^ z?E#xmJ16=1Ms{<0dO0zO98zK&9%hi2-^iUn?#`(pp~|#w(#xu($-5INf?`ZKcug^k zTAs9znub(30#-;!Qt*sN5H&vONPpK=$vLv*mAQ`+i!=kkK4a)W&XNt`Ix#SuW)WC+ z1VfCLvFjiz6u=As(yWOQ39w_o`A)(QJEE_DDX{ijXobICA9Gu)oq4@xYXo2qV|mcJ zD_C0&)L6k8BPx)A;wH@e^hb8o`@tO4Kj-bYjzY^DI*TE2OM?f-)$bhP%6~lqAf!9V z?H@hgvF^Qk?h8V==#YCxI%4?FAzbu;WAyK*LbwB@kWaEEJz)x|bCPg@3k2G-r2b_& zCsXEQ{_V`w&A341NIdZbg$AiawDTZg160>GVd|F)R%NepYVrwnT4a9YTFkI;=as-{ zX0)BwEbDjXyiN8=-((;iWE?#=AV14keb!3|;nv4#C_vkadWDmSpCNr#49E-oBy?SFP1irnc!=`84IE+{=9YP3Ls0W=Z%Ngp)+hVth0W~VLTsSNjgd2xR3joP6 z94Ui7i*oi^Qf?Lj@CwIemOB1f*}AKauvp4@9Id4@AY6qssAbN~7)%XS@TGJIG|xaw z7FU(bd?)8*Us3zL2x;(w07$AMu;}FdA&#U);VKv;AVz{zSA@Kr>3l61(MVNvBpBQfDjfdV)Lx@ z(e(dTP^q*jfEo`;(gkzR*#Am*JLJID+^7+$T0gZ;Xl(>oLn`C^)QF#%JP(v?-!lWC zQvS68u*(RTQ~s^uuQ!5Sv6uQcC#O?Cr_A{K=dRK}{_r}uBS1aY;3bWXF?!K^tAH*z z9IrD{5?DNR4*4izxo}vm90*d!f1R>;$`0s)D5jjaFl1BC#n6zmQ6;+(NZn%EoPnV2 zehs%zN&eq6v=;&Evq0zj3&O(>ixeEK8ID*=`g$r%epU}TLx_Lg0bS>iv%D@bOHDz{ zNsI@tx%J*jUcPgJLewU%dFeiA`kzM$0SIzn+oYQ?|A#?S8Z||LNC}oe=`v`eAPL2ZmRcjAHdyx{Ucfn=tPED?2Dj-# zh8q7Wg#z0BVAozyD}0lG+Y9DC)1raC`K`JAV0OYeG2Z|7A#S{|M(P!Y*X5A41_%MA zqn_V)z#)}-{JCf3UXL{}^09o*%5A$qKqnBtnc8ewH9%S1c`fm9YxgVl^dWJ1tpNf+ z$bK{;2R6&Bie8KWDye!!0o(Ui1X?)ob~GqRxl3Y1NJT$R1m4KGI+G43$OshT2Z>>j zk2CzfAP$_>0AgkLw@QC*b?Et7H&Qd5&Hl1_2^bjLpGEK%IQR8XG81kxYPoOa+#vjn zlaA>gM8?}Lfafqp`&vUEcz`%R_c{bn3*cOwZ!Vv@7WQ3SfH`45&EmJg0O-}%*Zg{S_!$V&+yupGaVRRspgYJ~Nf_ zy*4?+U^C@FDp{6$8?~qpc&zWuEU1Ec^_E(fSlIC~i7~LW;NWMV-tQj(5ibxBn_9-v4D)u95`_Q^nQ zn8b@9A&IE$=?uB^UhPdQ(QNTfkbM5dUqn3mh~p<8D+b7%@~=nhleC1uc7s^WJnFKc zb(OG5&T#cxAK5We17r>foP%ELJ)r&n93QA91lqW%cE|(s<-fg7SATGfcq74g4&6gu zD(&eJI5W7H_2rj@e4K?iOOW!skzqtuhU@_jsvs31^jV>DHW-~t_e*K5OQTU>#sk~$ zL-v6x_10D>Egbwl2|;h8e^U#b&HSuq#Gx1m0GIF|S^a-S2sz6hXULo78#4ngK#Y{E zMu(yo2-;4(G$WeI@#O#Z-{ub0^9~2Nr5F|YD1!%p^&GaZMsUwK_l=I-m?3ahi@KF_ zGXh$aZ@^gp-yXYKe$UGHgJ1;QO9P2Zu?ZX(y?5jL}d?0ERxpt-N;HQ=(i1vSTNAl@}da`J6s0!bCbl(k+#65}Q40dpR&&m=E+pz~w%q2RUSVO3AX z!U963RBy^Lpkb2`2rfx`w&GVmA^t+6JwmwMxpz6_%6Yr&U##?`Wg%19C z0}8tnQ=CAF)x#U1a5Ty}>1P=uDgb~Na_O9^&rZxCkugdb3l}le_`t-F0rVJq1d56i zs$ga@1y)A`?B zusf$-#EyGCGwcj4y4f1}#`W396y-}e+4{RS0P68T)Bw3B2Ea_(-!7}awd8j}|8x2O zZQ7rWxf{*^FhtIO@n0^xOYd))8I)tjb`34y!;*XMMA2F2IRa}z98q3F%%d0UAS`(g z?61T)Sg)mZuOljF76-<#73Ib->zztL940Bo!`jo^sZ=<#oFNtZJOmx+yb}^{Mj_;o z?gIdnY{RAa2OJPI?&yQ0_i-Mp7g|VyVJv^B;vY8_Q`kZ0%BmIS%*y=N(#AkhWVuq{ z77i#LjsAv}`vr~PE5simW61r`_l!p+X@3NA45x@dW?-?D>$KN~?hpajZ)808!-4+o zzh30!qFCZJ5>T%pZeG`htUZ@&D8I2Tq%eH@^8k-FQK2fP{qy)WGGWG5Cc;?z70QX6B@x;TDAMf=3&{tG;H83oj0qS(Hu3E~H(SR)b=%n;-mOD{%4$KiL#f4hw za}siZ1GpDsr$DAzjVLv^Kzhwt&TF`oaqyj<&VOUU{z5ru`wOjHbMt>&pBX2O0OR(U zfp1EGj`Ul~0oRZFjRM>&1fVql&T8@3@aY`f*AjqEi~Ua~nHlu$?_R-AUOYfbjKs{2 z94!|r=t`&q0I4viO?oz22tdK1>jc(Io2+ST+D(t8I-Xhe-ji|#0M6n$q#}f(kr*s- z{fr5LrB;;IiZo~G3P=v{&LIRznaSp{>tX9=8764rwu&(lJg~RY+Wbir_Oc_KKj$zE zjN{FS)J-gZ_4*msDp;ZaokSe}M$9XoG&sl!s{VN&IzZl)uzSQO5C-vgzjDU6>*&5r$6gED9 zJ#R3r3($57t5JZ~WB5pnGu8lU!+-6Z*84*Jnc6XE9fDm5K&=os56o};=lALOtu5Tj zA+0mwaYEiCXbVlvxDm}v3&P@}M_Om(f#EudtMN8Vu^vsABaaetOUG#6&OrF)^N$>v2@&H?~tCHeHIgak#fod?{$LwMLB9p@rr0MzT( z3ZyD?R%)9QwBCmUX_$@mj+!w)3HD-Y=-agx)n_#g2Hm7;^wR?fvJtS>IVpEee;dXR zGIT?a5ie&PJ=fFu-(N(xS9Vsr=W~j8&NytHRU`SCfo~xJLjY>NPn-EuKY;tp2e|77 zKvm;=m*?Nc{aOpZh5eT}>G_|g_<@-l{n9Uw(%(JzzVDMLbngkvrQ^MQPyI%fhQ`&R zcI1;zeqz;|TIm9zcebKLFjHovEsqdB03XD&?dxga@IEHi_bmIi&Eai@i0uI9V?~RD3DyfJKiKVR}zA~JLW*-2| zj7={U0Xr{z0AcjYUGPc}GV-1k3-WziH$LUFh5~jO0o9!=@ZcN|p!HhoIAPl=v?T@>Lg0g1J?wVJ zZDawp4nT_v))3ZP1m6y7F+bP+V=XL7vISUL&fAVqRWly4`98=3cCTkJ`@ z@O3r=V~XJzRutkf206CZ%%R*X@{k#h9NdCrX16GH2#C!C$5@|36+U$pI-sXyh(ICm z>Hb0r&k?j}k?uL1fV_qXw0*yBTO(`O6(LJe;$FUUWBxr=p1;QIyay~L$6@Nh zCygI^6~34l=~%z(G$c_GjI3x6kr*Z+j2RO#jzXFA|K5?JC(x{ektkW;6f3k;&x!m^ zkdzRleXzsWLYVc-Y@`KK+gXYul{h0wjL>)kQY}#`7z0vy@JBBW{Ln>mk3Kx%1E5() z?f(fQJh;mUm?H$%oPc_huv8&4F77)-c!x(Vn{#`?F7&UhuDFl+l;D;RWU>F7jO^qhM8vP_-RbKwL~RHVUSEYxEPzjAnFa3KW3{^Um421bR>8pgm9A{B1D{)zim zg$m4wknck_n9POCR2u>Pn+epBKlk;Js5{jND89wa`xf%E!a(Nnn*mTG0CNVw16Kp& zt{MOp+*e!uZOpHQPH*;T4S@Q%rzw8`!n*zDU%%si`PQ*4zRXzdc?knpcRY#1GkGEh zjs0td9n$R}C1`%WSZ=PebgLOCr&9kdX(10sC}9TZCldp)2mER@7NVBM^p%xOYRL=3 zemp>7*^*AQ16E5(?du0d2!zFgq?NFxB&uT))msPwrGQR3E>KeV7=+-Y6W?kjDUsPA zK~O>O9F3)hS>Lp3i3Q%nD99;^u~N{1nxF_sPXvcTJTwFVOmI5EWuEOj#Rx#@P2dRJ z4^-N-5(7uajOSm7?)U#y7nfrpk<&H^ThlcPP*)AxRW!uH0IjEB-*we-KQczZZPoy( z=dZ20);b6+^M7s+nBDmEkn@{=^`5)*U#^0dj-3~n%gX#(4G{BEhie0p=IQ&DL*6om zg9yKJ=(-?_QdgJ4F#HiR`5qu|xNCJ?f zdgs-#8RChjEVT}b>yEJ)kfrWfYLO}P!f9|C(|0I&{0r>|5Y?bCqsI=PtNTDSA`~$j zV#0;ESQ`&yw}HpGGv=`w5^r5jc=>BU~9Y13g|Je z=Nc-!=NrP^7`IsiWLGNXoYJr3{;eZm_pVVH1@_0=-@58AfA2bTN5bA`FnN%WejyM( zG5lVD3l8x(3j^J0&ir8%uYXUG#I0lsn9`*V;@N8|pX@DJJoRoo8;(*=T#SP;_Z|lS zTSd%d8Vw>_WBEM^f9(-EtpSpbq`trHC8RYqoS|hbVMTI~Ysn(-lv(AJO6;9FjJ*%M zTvI_5>X@L(chG8sYK|G=kz-?}+Y}5cleQrerLdp{MIFR?xBETlMn}ifbptXf~ns&s9i8Bl(wE?r!!!<41Si79%9%%4>9=|h*A)NACL>QYL08q%E)zFZ+9$OG+ zmfw5f7m+{yxO0y@q}Igl^)zP$n0F9yb7f}iVO#gMff&SSK@Hm&KI|xf%~NYMomK;6 zj`2U2_Sc31?RVM1N)g(w+;V8yP`AuI@ z5A8du0;zK%n`&fGb+;ssA5%s@0oDM7>>3=#*gYgA&k>I`4|ap}l zrT>A|05J!^{BUhkJbuUaJ|5*h+cALki;=@)j=LwE5sSi z`RRA|z3v=l4X+#;^?5fe@ zIbhkLG$$(8rjMLVF15En7DIzE8Xrkhh)Pz48^aHlT4bq+F|lwDFrq9re_D`8*$!HCNg4R+;M`w$aYft<(TH4FPB?f!=KH zCf@(%%D^xG**l>>h(H#34=YiGxtDmL;(TBL@gQM1NRn!*a+n8ESmc1b&86dA>3*3o zDFy60f?p|oKDn!={B^=inYyW>BnQpkA2~nL+IRe19)NTsOVvRJ;9W^ooSL*UJ!Yub zSr}?p-n4V6l`oA3MeXIo3=RFUJTUUHmNxEXKCF+zWN%Q_N**O85newVNYa~CUZoo$ zMpfLT8&XH00CxMtw5W{+`9DX-(GLb6uVpWm!bAzz<1{&+OH4d6hU$6O5 zcYEA513*VRajn(AEB<#{Y0uL<|D7A<^@ASI|MLx(GYgYn^7TLjELUD~>2sbaLRJmB zS~yGPh0p2tt-_$d;xy*hQ$?_25%8E<``+{9 z9Lp?M7w^4z(u)r07IM5HYMb3Axh!W!KM4Q9WF%{PQN0XZr6}#$1h=FOL6rLtNxPoj{J5nVG=?U#ooNKEtk#NflQB{Jw+R z6TF)%=$aq~Lp%`TAZONk0Qz?ecUc)j$9P(q?SIZ!nc2>a5WXVga$|q915dCE@bCgicGny+>aZV98*4H8>4P9SdeWlgR!Y!WZ@K*D zaw)>xYe62c#l%IZW0LBF&I{>oZiGPCUy233SPHxU;4ov^p?J9l{{ztUL)J7xJ&0{s zGzyL>YH2zo*JI^;Q-m5*uB0=DlGZ2JV~as5_Mwm&Eza+~a2*^8dLUH;7S>#6_S)?wY-U{nsTTrXuS_5DS zf&N-3dy6q6;IxXCZFKd+W(Ca6&z$l&RL2mj zsbN7P<9iwF=VQ13wJuVa1Y}x$ax){~Lt6vnt{4Cf>1WRR-<1M*Gf%08`F!&mvqk^| zcyvCi#KILcbr!e0mTH4rMGh!4) z?$rRYTsbp}1csL|hI#hciXwqZ4+JxYtP>)j`-dCToSXxK6DOf=Cge1+u9m1=V1wEH z0RZs!rG(etO5xMbc;k6Cvpo6#E+b%WeYelMYS+&xvpb^vtJ)kp$DQCC+@%^IyXI@x zK5)XLyZ-V~eDhoHcWKB#QpLA)2>Tv+n@8e>h3~R>msx6nm{m#C5vaGLijlyHkv7_x zOV5N9%C@=!^{Oek@);xJKVYo>X$@@#@zS9Nw8Xzy*0m}Ba@@)oFe4r`;onGpeJ(IM z1=*`uX=+x|cru$73drNcl7rOvXF31873EEx{LOj}*k`FDv{FN!VgEFm{ zXg6oPyE60DjkaroW?odk1yCVaat1apC4@&@xBLyi1x=Li96DaFXA3~=lJyTo4UoHq z0PIr!bI^agDc%h7tJ8dTDt;RWheQ6>KVEhzWt=mkNNQ!mFu=2tzSTneR!Ou zV_*$LDH@Tglp=$^Yl}Yw!g3+G_?xX&#}a|ftX6NeoQ!SioP-+&SG5GzO~cN^LA>&0 z`~mga%?hZfMPq1yfUJs~9N<7>k)Fz^F*rVWLRbKCK;b8p!XlOW%jOvnO2HrPT~vh& z1bxX_(jNUn=7gE_8Knu6ncU8xMjpcoKYeMSPdwq$-fH4o)!QOfuWhaGA_H1{+ct+D z(|F)%gtp?xeF%F5w93ClNbYG3kp9MyU;T?q-S%oP9H1bJj5A|cNAMsss&o1>vpc)Em?(e%WP&704}8%d*NgNZ10Q5Ek^(@2Ah8>#c$L2Xb^;WbW^3j#PZdV! z3nLIcylueECqZK1*)xwZ>4F&HXEzLltG!w0aUlFRzccD*pAJo2CVJi1YW4SzrEkNa z&&3FTVf|@;(xwaSFs|7H44eR=mlL_L(JfveJuZglE?a0g`;CGXvw!a`_@l zt{pHB#d7xxDNnJ~`A11&Gs+3bgM0ePjDO;Y0-!C3oR<9s5sH&t?{V}RXY#U00)|XF z3=!Qx*MGpc5H0lr<3(%{7`rvDfvQ@8S6>T$`RkfhFOM&rkM(|G*fMmFf!xQU0PEj_ zs?x@THy&yI+J$32o3j`8#occ`a!H!^zvTp6;z&G}0=(q?_xpX)Ay85L?0^4ty7~Ox zms!Pd#pc1n=2?tmFR_6Q%IGL*yOwIXeD4KdX6QjENxR=&{u@UAfmaJyq-w^3O#QV6 zv=dr)z@~lg^P_P9uv`U70PopU1l{{i*WB9V5<3APYT>Z-@&NzXQ}; z#r=zJtCaX%OMy^FuAgx56SHOgIbr*^=nS#&!gdvs*nN_z0`c8ORAOI>K&{jRVjx3idr>w3_}Q z+vntHX$T@H>Ul&!x=0H%up1b+_kzRGuSe0dR7bzXHPa3-lnVUh|1J7^znjW#*LchE z@qV1g^VXPuj0m_D-P+;-#BU2=uEB-he`Gte8%=D`&N z-U#8H2)FM>FPqyS{}GlveGHkZfXw3FIGyQecr?*V@D#WA=Q|6$!vvISX$i%G)`1`a zhG%38N*=6Fz?eN0$97yq)|xl55fh=-E1*>eq{#PMv!hT*WB_2rnC*VdGgQfx5385{ zmEy6RZjp{_Tb)TApr~~=Ji7_YHEUj42kyRpE%^NJD*w^%=cgj?G2RuAkM}LE%NE#; z++cr%PXUni@jUkbr#8LteDOU81pTqb`KQ(2|KJ)R^P%X^|Kv*^pJnK=qD}-1z;t7Y zc@-n)O7b0ACe}mBE(BH-njA#C21rX6oG%J?0oZ(Uo&6}IR((gUHom{Al*o{}0wL!M zg0c0lc+u;JojLWM!vjar>+>gt4zS(<1rV6N+D#^8b%L`UP=o!e87D$6EjO zpMRCgEQ`mgKrLd%YorCi+tDu&mkB8|uC5u=qyQ<3xIVXkRMeUNzT?ADOomqtdB?6^ zzD!^uJx+?_$*g)r76D_m^9qeufh%!2`ACiY=LPK`HG|oYh-p$xiq`nXjG}ckR8mNx zHUZZosm5;wfR72IF>yLk6Ixces3r&yFy>(RFKLIkO)0sSKO1{Gk+u>-lm4X!b^@4* zABO;7rI1I5z{pRL50$dAPHyz44)nl}HM`irRwUpY3)o+^0zdg_Nx%DBiJo0IZgPBl z%lNJrg)L+ESiU-Qg9Sb77$hDz(l*1p!Mkgd7jlmDJd zk32?{ymY<_1^xB^_Dg;HE7Jg>XAJVl7Vj7-G0H*O*oVUM*945$)@Nh-rhu4@ovr|w zke?;1^RLUC>p{4s2NXeSQBe=s2g=3*8G#o5Zo)THXVL}&d+H&q=+dOCi!#QXvt`R} zP8jQmviW)wC4bsQ?qv1~hB&r44u!Z8_SA-|eTE0S z_T)l7%WwHbi*s{*lm`%*yWi`-=f`n&Trrabs5gqkPilJpzw%7^VtRZFc=vX0oesD( z&*O3O&rY}uXg^vc;6q0M*0+4U5Uz8gTOq(p6aJmiJ^=jre|(jH{>xkC%nSN+*N&4( z*s204TY_j9E!=mwub57XPd^XnI1$20JI!>R0B-nHM)U8?1Z31Ew?|5xW@tv3@8YzsJyKW?t!-j&-Mf+ z%d?vS?G_(GGyW~VPv`IP3Lu0cAkd{00EpPE3mM?PEU?!EpoaXtb6N$6+6 zgIPU2su|0ors${o*KiN73_qthb0hIpU(7=?xsANA=Iu%&%6!iTr2`F}7ckL}-qYVy z?Ef&0!|!nI-r2h9J$xW6$&3%70n#NlTXgNCkbgfG2Vg7ozYd3PMF6%Ii8a|LhSJ4t zo<;ukpMS|yQLxW4O4^oX6+)S+6VMpZajNCZvysW<-tI#RYb>@fhyc4CFp|C3DhbGq z8jSF^8>~@InH9-_2;i)BN8$6V#Jof(&d_em+1_*4_7)nen0$gY<4V}=7^;esixEZc zzSd5CMJhPG8QlO7RKk}r(a4G#>M!HpUnD4 zzn^fmKO1`e9i!DQVyteJy6tf;8ybDZ5B~qC4~FlfZz8e=0BZwy2?$&|&i?E#8UNnT zdh8LOi}Mddy*~FKL~Vsfx4Qg&XAO{YtoZ5w@df5Xfy{!tzdzyAfA$=t*3x^rpIz%8 zjWpJb83U&`1>^w0LjDdwXT^M5ZBZ75W&)39v}INs-(fhHajV2Y+8Y{`du_&zcNS@o z$pHl{N*QE2ofSMVh~**~m@%@9h2m_0PW5pZ)v> zTE(JHoKUMEC2ov#qU46kyy&?oK&F|WIkiw=tzx1XiJgqg4Fs4dk9n1;3ylL-=U+>O zs+AF~!CR#7gh*2|!dw+4QJavp2q1(!_5wXt-j>`*$Al~umg}pwXsLQEU%>&44~NJ(aQw)5dwB+D>w!A{VwgnzQ!@MdH?ZV zH>ta}-!`z=2$`a;7D#pFjL$$$9! zNgf@1%aN}ahIQ0yOYG(f=K>+IugERNC1|$<=W}Aq_&@dBPP2P{%(!p8F01#q08qau z_CR2*khZ=i-&q3$3ViWrU-K7#`n8e(oEe|}^A{Md228i60V2gBc4oW;ni*1z+{V}0 z!~6dJY=gK?f_fw19}JKpC~qrvKOQYTz(9WKYYSP_z`S=K&JtmK3aJWEBI13N&uJsJ zQZtekb&yFHX8ZbFV}4Y1-HWi;us#Lq$KQRv@eum)&da`Az@eq`e19?O;Jv4)kFlNh!yi_i8L9X!2i-gAufT2(411D{VZhCWwi@24D#1K->7(va0jn zRZO4Py!(R${1ejvS#KXJ0?=QOOKE^L09e2Gu1wzt+n@ZapQm5{>J6TMdX3BsftcDC z0U$QCn#hfY*UWo_K$V~>7*hh&Sngp=%z&9Nh^mFSQvymM?Dq+^n8yt)9`9m5duP!$ zaie3h!sbYb*Zp!LBG0M!JjFZ^b|c|*LhJGeEU5yh<@BeyMbBJEAgXbe=cBehn+qQL!~TGz$Z_KPp7{=t8n3UQk^TjQ~x zxDgf%L%79!4?}psIRMaZA3Zy`|J^MB@E8!-`rEJWgXiAp%OkH{0sw@Eq`xi0f4zDw zjQ>_z@VzuZPPems{(pbPS_R|vfN~U+A}DW89>pRXCt+#WxIOhC(4xp!$#^~NV{J0g zAYwv(o-C9v!0<97?HM|I32zL1)h55Y7*aFj?IseSqFV245UZdy_Y3RPN(RHIt(FqX zgtRj*jGX+lKE~lhy*_Ht!pRKt)+dGZ3==|wpCR*mMs&<}y;^k8&#Q&-W1VpUC5;Jg z);@a`t$E$hXerY1!!&j*HM7A(Z5vh|6a`h!?DOue{sv# z&v(cJqf7#w1^cT3l*pQ0Rlz(9uJ#$F0y!s$2+9%21A{ZSh{mvIoQ{eNMe^2^Ox9wv zR6(gGM=O<3E|i${8#6ErWS@6%QLCb;Hu9j?)@x=ntRjl4mJzQNIGwB+5rk3Iyn<+4 z6gNWD27P!7F-Z-Os|lPWGkC|Nw#GjObV8kv+)!G%9VZ}uMSY(`B8(%j8_1RV;9-2@ zEaXrL1AwN@Z~J(_C-CS&0084$EL{Q5TuwogBo1Z2*@a93uo2)&hfU=H`yDl~6g>|( z0RvkE#E2SQD^Z|T82s($760;INdEL?z^(_|dQpgZ5G6iaWWc_GNQi^<;s01n; zsdEJhz(a0^irQ3R$WcHENLT4RYF1+m!`j@AHaK}C?0z^P?Guh)&S##dRDg$iWP0B9rDd#;hs(Wde#W-f{mW_h`~{GI5=^+Evm-PXK^+269?? zEMbYzX?I>?KqVxR+cYK%k9*qR~{5l~I-f{3`J4}B53R5eL2!!bjP|Mc? zR@RqN0;eXLqD;2$7WPkQ%>9-sXwLzDx{(x{6MULc)-DR522MoGYp-1hp9t2E zK)qbhodd8h%>Fy~N9)j7X0NnxV9@|bu`&hbUB+i?lJ?JA5wPC)pRjNM zCM>d`)O|;=0DyR}pKDKnfXBcF0&**)1OqbJk26Mk z-1!=-oNqyu)9(EIp3uOMJAoYi^{(mC!yAERWc#V1SXtosd#Uh0o(4!i&OfN(kL_bc z0M_CEcgg#&jsK;5-^EQ8_&5Ldi?NDWewYYj0*FQP9M!DFCny zW?%e2`|SZYH`PMNUazmKrLKkh{k34Z2TOh2u{X^zV^mx~C**453Wmoj-uaDtgOC!@&Z8Bx=P*?P7C@CSRY5HZ^_kfaI4>c3A}7^>76b0anw_)Y zRS{aM)f|(z*JfH2`$7}Fkp}5p>F+FE5SPK9&*XZ;@cSi>>l#2nI`gC279*4rVcCbH zHChvk0J8=+z#<;E_loN)YMKObPk;V-)j$4&B*U=kvhz{^03ZNKL_t&zQf|(N-j^F- zOIDQl&3Fg`)Gwu|4FKK+2wXbu$Bh5hBZ!F=4*>vh?zP8a*N6U};bAJ^ zgOLBe_;;^{H^019EdVn&@F*%q{G|pd5fj?vc5M=emz%7d#tdTWe{z}pcWMSYdOU0A(_DVqJaS+Z5N^)p2!e zn23NTTSw03PX&@WZl6U3s!;K`)cfYG0$1Zy>t-9aTQ;4qqpBNNEP3_F{?kioNf;s zObO4t=n(iJ0)_-siu}J#fbi;*8#NH{@)?5@@b-=3eo~A(GhkDJMtZ>vt9K@30}3y$ z$hw~;doPS~6FKss7952{Xf^yz#ji8gvFP;?g5U)V*^R8C8R7{2vrlv)v?fZuds}t6 zu4f>??=P*6Rr)W-SIMp$&CB;9S|G6)b=zFD@c{@+ibB-@*y}ff*Kaic){k06I4+FI zEkJyUAasRuZSWodteb_zZ*ljT#{dCV|EJaK?NY^M?jDZ;fO_HYEdbD;^QBg)>k5GH ztN~JvwSN9@ze;ryv<`A=9)1-|_h<|@z(VE`_DTc{jQotPWiM%K!V_0W)yRWP3>u9y z5ViB#rQPz2WX%4fnz4^7?st(#T7e4p{N1QV^i59M-6&cd4d3LXz!A{di-W}tptIej zUbbIawK+BLx!I2jMgKm<%Q)jHkeZ+nV@UyIw1{Mj1`zDJyMS0o{gU%9G)5^jDzE)g z(Gu|xUDha^d;ZG>u?Wanq=o=wyKw6{zj0OCy*T`&r15tX@w1Pj0kQ@K>*#kpw$Ac{ zY#%!X@K^=FW5)lzCVXsv{+D0Tw>Oh}h6-keh~jWJfrzlb8nEB{YQVtmDuKxAYzr#h zeyiB+(=xi;xSbAW#G@xR+=zq~j1YOuOHpIfm#WR+&66!MCPZY0uS%5N*#uMQ84$36 zuB|=pvui>k!tI?vR9#h%aGWdd?iHLEIl+W(TuCo3oWR!=HN*Vk1shp#T0-<;@Dyle zfhqYTqY3>~MN568WF?Q0b8lgOcy9wCQz>-*K&$tU#R2D_AdLAL@zEDe@oODS-lZvs zwl>9pkv)rpty=9GwyFSG_TNVr7I0x;=omHOye{?|AM0$aqWOCiyV z+ZX@-Ret@8Tcu6|M7>0m2#_K`f<^x{(tzg4%cRJ|h&nGaw*-%NiLum~*bH@6?=0uQ zIjoi&LOCf=#6Ur3GE~$mi-xttonJUV!)RQizSf*6^1(Pm!I{z7v_`BuAJA08;wx^$ zA-3OU=rufRFJrU??EF~b4_^=a(Gw{UB{UG#NPe*(HXq3VN&8Oo^szm1HAIT2or8Jz z7QH*GUg*}0vlv-Bhx6;vfA7I}yREzuoc^}r>VL{hH>b;69|hPN`;RH#Zt(!tbM`kg z0)U4Hx?Yf;GX9rHfA2i$Re?YGS3l)9-`-;w5{7XA$m;YtTaWJfR4|MgAY0TK#$4xY zNL&RdV(oyi13&yUEt3UU zNNI^|g>~u9LcrnRQ=Te?8c4F1_789`ykc2Ok};AWZ)(EZ()RsZKd&UGA`7TbD@ z$r^_qDE<-`pl=HrN7RS^*8m`lYY!|g0RdopF##UVsWkuygMZ2UUju+GkkX%Xzwm|U zA8CM|{oi{;TkFBat*3iG5{eu(kU0^rD+K1Y& zxK%BvqUG*291{NxD+Bbw&M+@sVCjq=oqIT*y8kcH0_s+LMJOn8j8Gp9A?j!6WM3wN zh5uuXZD3$IHv}MQ4uP*J@TdsEIS#s98(_)#s~<-(q9DXCA8$kg;+p&GssIXb_wQ%j z{b4eVg@>O8&Tmm~|JEt*Eh6A!qCgME{D-at_|O2LUuZ6Q|7$Ria1H`4h5i9NoC~qt zA7=gLUg&Ul#LcAm?6+Qk2$+rqIWsf?w>L*zKi^}w%ecQQ$V0|%pD~OXOoaWjgxys} zPK^7v1#=N7rSsKXJvRmdQj`{&QeVLATFjVQc^@qX2X0(pgWcw3cS`ZOWCIk%^aQjj z1`y_GMNX}35L)fGb|aw_fx6yr6VCAiCjzp|@$UD8U>;GiTKX2och8@*$rPuA#->%Z z;|ZQX3hzUEAh8(v>kg`T30< zf|{AAA|PfYB_IJz3CT;w7}3zmWO`FdX7B{iDnLfZ8Hf-ST$IP02JX@WfEo5$U0o<| z9Il@QF?ol-D6>;Q$RZR>es#HoHZPxwofc60FNfbIzaI`4d7y+fm%4g zucLDs9*kZ9QN^%S5g2DddK7!ab5sC z1yRRS45yQM_c<>ufE&q=Y=S2SvV(xqN(62BU^n7SZeab`qrGn|Ght98DgcU`sAoWd z8LfOTjDJf5NL(2k0xF7H)P;TQd}>9L2R^yyZ|##8Z@<(fO(5cx7h-i9*z|uWS_t}hdPC)Pd$Mf3wUjopNEhg*Zejfm^ z{<~jxwL6yFJ@=qzt&hGYL&f)=H2kss=l|;$>Dw=FaQ))S(^?8tHD@%j+QDzc(+1A zgc!Apgv93+>LY<5zTPRY7=8bV7G1#EVVmjggr(aav{Xr_A@1+59i3ESC0`9%W{&(3vbQM56ZvlZzfM6^1|1O@& z*>C^Ve}0uur-J7%_ed$>a5v%p?Gc}R_6&eE_ZfEyyIsPt8!%56JI-KJ*(!>gn+f0i zeYH{{BIKM}-Z;Yd*3^bfO4Pl>>P&lX9xpYy+v=nh6C5I2Y&?BHY@gv|t$V{u4}%X< zWCtu&fcv9h+!3B8778woE*evj;P*;(%*Rz*NC zKoA6^AXfGyzJ=v)MzrPi39Y@cD7EVt|Ejuo-w88$DpKg^`e-AF6UJ!a1pV`=m zVe-DU0~bXhFmW-vTb}C=!>Vfi2VllaCnqH;Ta7=V_5Wy_D8&6TT3VTGkuDL5(c?iO41odC#Pp7}FsQ*%O`jl~Di@Cm@pS`2I6$R)O zLf28Rj}8Jp1OTAb-&>ET41ZdE=Doas0Ql;gBM``v2>a_D@{pYfSlfU%z-{j^Pub7rM!b@a&p#JPGQgw*QeZP3}t4lAF&7O+uK+QUSZs z+6)r`#}isl07}bfRwZv~Z2O56!BWDvAoMW)>TSq+fZ$i=?4GG~>NdIJeZM9s@rwd*9ctq)5|p?iQ94{N`(`_lk`M=mUKg0o%)Ou+4}po-4S zG+r2zYd{<)ALCrG7{j%PAB)9f;s2)qz`LXXF3qcT1fYMu#QyI;x4sTr(T}w(asYs} z=O52cZm<5!o2mjHu1um=2{;@ktDYGd@G5{fGj`@WKt`PN@h3$b#4^&}SOmeL;n73? zRa=A*N2>y8-9-hM?<+VH(sh@_cLdqg2Z6IjJd_Mb=xmLMqO_P`$EHP4OK0rGuNb+A z7~@xe?q=3l|M-+klJmYR43_QGs-y9NRU5q@z^aU78 zG$Z!k{cWSdKffN4-8^s2aP|D56&CgIC}3zS5CJhWwJcl+g?~OU-NBBri3r6$e8z_S zGbY9KOW~_eIMM)@0RQ@V9r@l$18hm)Uz)>ffc?F;k3|C9GX9T&fc`V<#p$WV3dd8? zpZ(%nkh`rS0%pdz%aBrWxSMe}7JT;O7nn=I;W%R)2WR%0OmG?!=96IDCp>>KU>F#+ zR4^099fO2$JXQ-qOH>fM2cU3LiGrOMpw+tb0UiYqiyXYTHkexlxVtwtzqnkef}Gr@ zrWp`M);;d45sjFZ5&*Rx2DKET}UcPuVKt)li ztrtPkv!Av5u&4v-nb9X*sxhEIG&L$NEvjn7%{gRSec=HuhCk960m|+Uio1K=efqL3 zO^+F=el7^k0YGR2^Z;STk|J2=)%$&|=hynN_4%#&)o)u%?b^UU_UOO2{=I(AZ-qkF zrBLhK|K)3}z_0$|tAPpN9a|MlRuJt#i6Nw@A{cT8;3QxISj+%44UlesS`_==8Zh0| z#fv335E0y^^YKhTlckNTkoMf9cD0zd8X6!$5C)-<@-z;>6GO^CY6LWer6d*b+Iw?) zyec3OXh+Z!OglfKiQ=o(Bp%im)8y^uX>bxia3dL-9cI8=WWxV%<&Z(@)deUd2O^OQ zaDz{g0`UDPqqRppGyZW+<-!e*XhkpMP5q zr;@Q zm)da;H$27bD@GOb`o{q+G#v&byoXW+cZcc(J_81TdD*s^5wd+p&55CFPUh*V^A9qc zcYi!t-fkQSi515QENKQGkB{@P=XdD2G) zz6$`@GWc6jwXKEhg0Hoi4u~kg`t|iSS+5Cek7%t6a>@I-xE*g!^7hLEhdw{U7H~@= z2@Ebd$msL6MdTPuQy*R`cp7lAych&%TDiv^v z2e37F-(}5ta{Evzfb~VV1Ona#2y87_kD{$V|Bqkt@l;@S<{pZZDC$yNz1ZRE#oiV7 ztZ|P*z-hpc6F&Lu3b$`3h!`=>YR5gy2f^L#1Sw{icKd{@eZpxn&l_s_@wqDI*^~vR z*^KIbmw=A-K23@;TX|siegoR+fX3i$Z22CZ!~QHHAaRRCo}W*^ZpS#DfYT(+*cLA# zjC6p7#!Vjq>bddB7J{oCVPIhPC_tnRf|)Nc7JYcfEnKfgQsmX}j1<9A)4zly8$!qV zq8Oonsp0k8_i9m;vJe;v#*F~rBtTanWPp_#c!)?q`@61YjeA>m2V6ISwdoO%S36_} zIZ^o#z~}9a>Jl;73<;i%)77Jo0>suE$4lyZ>$&k1c)s+&#&f$ecnAb!_4g$}umu46 z^WRz!pQ;9Wa{KD1-_m@l;DN37o=x9lo~@nlZa-kS_WMWxJ)7uGo^jw!L zM_LXAq5sh!xhzV70rcC26JyhBRj-W)>g;p5hynNn>%i-PK2StU2ec(Yfo$oCbZx}I zdTU`W9sv+RjurJzkrO+SO?|Ge?xy%IGa-M%7EznD8E=hfp4xud?*#c>Y3Y)M`0x4OFx2VUFWAiLUEkEdOF_XuZL#ID_ zfS-=l2Oa~X4@{W1AS^&;42kgK2N{VOw|CV^bK1h@q=f1ybY#YhXN+l99F8`lER_(Z zh@5z4r%$ftb)3|@8VaNmYL|}(OuK(BUZxjbcT6N0NU0SVvLFrIjJAkkcCRK39r<;b zt9$&8sDl95Kj2Y{-YZZnk&qrL6FI10d#{Qa@9eF8r2@4WKcdW@$JdVSufXAC<$Uq~ zHRGTBdPY=&)PG|zPp68ay6!*Q1qP7`EgdIIGmIHc2O)8%mPkHM031&W5!95LH@~&T z7!D)O-DlIrXJam`J?iyb>ka*u@$Zf2B>;ep|HrYeAN}iFM*q@x`i;Bi_i+XuUk4QU z*?;(gPScE}0CR%X@Eb$FPJ&tmJfx=B2en^dp9CsoydW~!;1o++GsgBq3IdYrfb7`4 zNNO#wbyDcDf-+%vo|X`DM}Dn$0#so5VQMXTHO4BNkye3);lbPB~!8B1!p2Oc`(<3ksy(1yI5sbrUZ=W6HIq)*y1N27?OIceuKuof*Q323BQL-~c9Nev5s~|E;eOR~kN<46cEdGBY1bnm zx-3q=s(Ssu=7+&4mxTedrVbuzli$cjE!{K;WHE_KUA>TtgoK z6jc?+!;HKeQKo|Z^&VgW5u=iT$vpFUnsFk+>u(O24i)pP*o_G(`zUr}$YU%Jz*Gdd zn+?&E8QJ+%vEOApyJo1e*W)`Ph3ER%Lda&=)ynIfc%shCRT0;nr`BMHyGfE6;LONA zK*^1_Og82sui3Qw2{7d?`#Zxr_g!BxQYhK$}zu zMBc5n1ZxxoLh%%Yx|gqh4XW!Nl!BI0X;BQZt0`(>&S^3OFP-sUssa>w0H~O!Rmrp$ zOxs`a?M=Vlm|vItmPc2_luX7rrSAB7i2^7PYLDuG;LKEX|HsDvV~I(t_ru5POX}A9 z>^~d(dd&03BmeKW_3^Ha{v{yLzrMu*SObB_)`cwxV14cnfWC%520 zlh(CTU0U+g3RAv|Gs-aI7$?>~10&N_04L+U2;7(|8dzYTbXbKOxH$#*O zG*~Dbur-Khid>~Hu{u+&m=3g^p?lH@o$^9(Y2_Hb^rnPdmx~EC8%$7%s+lo z0|d*}-vqpmMFSoaz}lKq-+B8O9DsQ4C;3*G@)8(W1Bgd~!0}Y{%P((SpoT$Yq8%@P z@C+#@R0u=`q^1v0#k^>#=7!FtqTT~#5*+Sk;KdHtL&CT>&mno=dudH|1oKJ31DmJe zAt_P8&20rpnu48)$e12EgP8cK+7c(g}{z_4ZfM)Fu3z%q5R?#yGr-}8Xkii`nr%M#ev2!8fYrTWthD)YjzWB zDH~we6NaRi{ajm)pACE@CJb5O?IH|3VF~>Q2+GbAm}d;T@qw3q$+-4LzBl4ny_dVo zyao-l@&DdnUh)%~JuU`uw(EtCj@2-z!D{}Fe1MmO{eEaJgAp(!rBZh4A zO+>*XBNO3tCm62=WM(ikcF(i1?2AHJK}nE?49aAwzFEwR%+@_H#<7Zms^{V-flRZl zIRFx~mBsA{X)r(`#f_$eSk4ZluhN)v zSAzUw?p}Ro5D-TFk|^F&ARyL<54L^iDuDj)t;gE%UkYEolko?DSKr*@{_Y6n3?3K) z0W&os+^|c?DS?^r{F6QI?0FQe++_7ZYhjKZvnb5Ysi~g{S6nPFk%JSBV#?<#*iHKpT&72{p;)3 z)=S^JfPnaW>$CBQ^ScB{)@#J2b6Foh0O02D@0k4Ant2 zGdN|GV*!dufHQ2Yp6+IpS4WSynFdHZxyU)Y5D$S`j7Hrt5OzI{D5H&1JcY6D*(Nge zpK&oEanUa8eAmsLt`@%@1ZgCs=L+?4*(86P1^7?^VC!JE4F5XRx0M3e3ISeRVSoDb*OK=;7EyRddH@#kvEJSTuAYsUPQ~Oo zogA13GydgNkw$=-G3=7H5f;VazF1V?wqSof;L{)Ok(kW5R3K%-e$VEKM{Pc|h0_5i zr;#lvlewJ6WUm6A3HNu^O3y?sB$>{~5$7dG$4r=JY5U<6q&zzgqc0GWhaXiHMX0g% zDUi8){Y*gh+MD8oJ`X@jrYAr^li0NkfTDuHzKlm1UX&#Uz+k3D7|B7?T$XaaAo-~R zH5vG+5%PCq-<17)-)9YcJpY(O5h&{b03ZNKL_t(DP>Q8dB%&}0=5@KaK&ZV{0g5PC zJPi?`5B8gYEriK4`Q)B{&S*J*=7kdwN{N=nX=DAV3E3O}c8j~$`ejL1-+RoUZ_Sgf z#c1nzp4y)J%wr(n(tGQDUpoGGMparA!_{C0tHKmHhYSKirV6OB?wFIU4#~z*N(`9= zq!u+gzM1gshgaA=9~SjE)z&;G!zWIJ!G%!|6%0lin@?cP#SXG0au`;*a^;-~0u-#z z*xHx0?tjhOkMne}OX~Z3#*m}f=LZ8PHM-yt%+rP&_F$Dn{r=9v{2)M2Scr|0)=(H< z!dGSj&dLFM$zPx|{;G{j(DnseKh79q(mnf}Ym&cV{9C(UPo_3-Lul2#Cz_&E!TtRS^RZw)6_CfH8D2}v z0K>duTR2=yyT{9WU z?(W`r1~u|)&OZ=QoF)?&+U*o6F+fWfJir%2#5L|;_xmA0PK$S5!#nqoTo`dtWAiJt zmFdj|O{c#~q+G(|@ACVq$VA9Y5DEgJeNe>CKh*xRdmg0-l5-&jUsUH}Kwxl?)w&+~ z^~%|p)ggiu;r|v5NJJ^wem1sP6t;Yc^@7kZOZ~Fb1BNZb{@82l?Ol!OI~(0czPlAA z=$Bnu{rx`A4>qZS`B-RoJ)lfgwZeEZ1EU-Zjwi)wnsIft2UIan1(Xs(A{YeF$beMj z>x|vY0VyTaQUE68fsHtKs!$la-<)j&;O0@PiD-GJJ5hjUUxTUwl`vcr@{5EBBg?&5 zSbdJGL6*-(=Y=MFofzO9TEu}ephOVw69OpGbuthzi&q$sE8H0n2?w6K(bPO&QJ_#O zvqPc=C|ZLc!9oo5btNzd594x5tR(!FP2`xghfcGU*P%?=(DlB7Sy-8&p)DmTt7Iy?(Z$zx5bHD zd;XWI06y6EF-U;1ZGnKbm%sIRa?;0EtLnFJ5A1gqU^gnW4H!-d6%|sdvFEeVwaLA8 zRbUvnG23@n+4}}mC@HS46F4*GNpQTcII*D2Avj|l#S|7|W;KFgmD=*s9-RUrM8$;t zj`8d|;r>u@JXvW^%$-sdG+DdgcKW=C_3#8xB4iM*Mn+MqkB=R+Fg&ny*UzJ_7(}K$yR3 zKu=Wx_Wl7L3Cq`PMru}IK+8Kfk2X%c2+CdclZggIXk~!5Zu!Z!-n}+C2?hYPE^2?o zfSwHL(r7AtGTFScp9u1>8}_PJ8Yv2{1WXFK7tqMYA;>V+I|tw(@U^WQGP^O36*`Q3 zPil;t)%&+`!=<|ptARi<-_`%FFn&;lku60*9@){^!}X@+WEB?^T8A@~{MtI3MUrY>^7YFRFEiuK zHz!O}F?GQ);dBy|0_?8{5JL5FAQd)hMpDJ?w0tihZ>q`WJ-Uf^I${U zzE;Iam#$xi^cN7J3Q$)I9gU83nTIjI+GGra9vc0@+V9DHjRPR)(kiy2Artl^;Z)Si z?+nQg16lPzLF*g91Plq7Pl`mq^|ORpEES<*x!;r+&#sK=e}A-9VH_>bALpDr)SsBV zc@ikdfsyX;1tkE$)?A3!)?P1G|9`MH;^Ct5k7ek;%l2K4`@MnD@pdwqUs$lSLn`shFWs>PCS{>KY#>z`4EW-99~5u1?tF zJ)@o$Kw#d)GAAQlM^yo4pTD+#FuCd-V;RIje~_4egfpeT=4g0%Y`-^rIfM7yZuIf?gkd+}>WU#yoK6K$&zq-= zX{xAI8Y)y35GU_mr=L3(ihGi#h!qhB69)kxz~pHNn*h{Wnudp4E4<7QV5kwK!sCyJhr^6~rwweV zANTA+B4K!&%AO;DCIS=&-=9NPICK)CJD&y+5GUwC&j^N97f~RnftX2+@n4z}peZg! zZ8j&um;njl7GHEY(i;jZi3-C{rhrJEU-1Y41s5PC``zFJ`1QDC+}HC$SO4F7pC9?m zf9>1*oX6GvNE}5zsjhPNx-bo%w~)$keQKo3?2xnnqf7WULSK_ z(u)dErKKqkLg+^z#NbTG&oekN>P_|Xx$wlzm_+Bn#EkrcO)TgrW(_&4qIvvHpu?7V zBhb0H5J4R|SM&4w8m$_#l|L``@ zdCE&#u=h*D_=jzes)WcqwsJ+lK#_VFe>QJjC)joOQorbmAxAsj#?j~)|Ka?QgB6o} z@Pim^&DE-lL~yK61n{092m4)1vG}o(0i*LUs4FlQ6dE2>>|6q|TOgo+JZAiR(9rMi zHzooQ+uHbVo$xxmvHt)1dlwgiyThE0v*7usFLAm*;&hl$OU1Jn&mk%(#o|_+=Or`+ zMJXop3m8$*a{5M28*)P4nKv;VXAcY6=v7r5PgYz`Z28AB`S>F9A#9S7mj|&A_6XIZ zvFpW0Xms8qD25E|2FBZysrjYER?<|(8R~W^1m<-o4^s+RG^5Pi$bqWV(pW{Tsm`o#*goc1$Hk(xbR!_(Se1dD2gYSSue7+F@Wii&het%CY zY}Eu|)Qg?-dvl>H5Kmj7=L021NZ4XG$gHSUFx{7S@S=boO=}=MV~Zvhaf`PIdx2n#e%O9!>?#DH zpxMfcjd2=4)W&vGwqt+?UbOn%c*{g)09Bh&kH|nP>mxwDb01%G6CY};a%Tv8fuB|) zs3sH?Ey&MJ8a=q9q~{!=ATomqdQ_E*E@(P$|TkdXI7-=;R!3|xq_6CassiTbqe7O@>09x!XO%`(~VZh@Y%7N z@Q(U|LhiNuBxeuj6#+1-$^C-C$wJ??+U1B+H9*Z98O(Dh0hDR^J-7e~$9I6aq8Wah z#O8H`XI_e-a?-@9Wb;uN(swf_nLiIA=c!*c=#f}Y&F zXE@Q1+S&yyAiytjD?1Ex)}jId7{X8jXCOe)os+m{U+hhSX)Zv_@FMd3)%gZRUF-)~ z!icbls~i5-`EPL1qT({lQRkmw5rvaNGr(5_N_tL649DgW(Fp&)|K!72prFyxC-tCv zeonIf#3S1!An;UFytk8QAmIC^0X`N9@Dd2Pgl4wF|4(iTV4f@PZ{L8JFkX#dFphTz z0Ko403X}+^(*#Zl!;p|NBM)gI0nw5Z8%L|dFI6lQC<|Ie`Rsn^aA#t>Jb#n`n; z9136B#%k7%jH2#ll-8e76^+p!1MAgy=RlZkXYCgL* zFp?hp03tt6vP2panRt&o8LgpMtUIHTj>}QoJOiT%1F70N)Qol*aeKMt>;u#<$Kc0j zh$u{x19LwXH}DWMvNQk`X$wKi`;%BIKnofnus6m{9ty1Yq1*=sss7|CJi8ZOwS^_on-QIGO>@+=~;bbqCd@(GPh~ zgD=6f5z*`s+jz};9VaJru{3>mv;qXA$y;G77AhmO7qCAX+Y{zDqWEMzhfJC&92)uXkFi#&)HdL>b+6vpc z{t;RQfmE{ST?N#ePg7EBF-+#sN4Q_rO8G)i?94+Bzz~Z>Rm({R^U%ZisvQUjrNa>3 z5m-5-^ARj9fJK#0me+`2A^zEoyBmLabP|Hr=K!M6*`p~1=aB?pNg?=sI*SbWB|aO~ zp)F7iz{u?x{JFGk*|w+b;$uKy{r&GX?>?3ez}9Pzo%9yEd2(HyONFXHs^D}uBJV~F z!-(DW&L+GFh9P0xKSvr8%3N^r1>KCSjq*GU#w7L$C}K?hFkZxFq?-95%Lhs&pI~+h zCbnY>tcaDBFngH2NZTi~m$7-Py_DGNd#%N%6oL46jnu;qJgjD)JcJe?A=l&tIy3xL z)YotGGUnODed3}HL5OMA+HFH}?S$FVZmL!aR~bMvh;|&sbC`1x`@1n^$-q!7j6T0a zynL^rYd;r(@eq3uyexXP z0-U>xNB}YF%{Q@Adl_Vr0abt$db)+YO(06B-~0+XA2a0)Z~~3jNT2++V*ecJli!~B z#g7IWcK@IkBn=75p8GEX;n4K3h=3FUS5pYJYJmhMBAFDbYD)mS?u97oNl;asdSx}{ z`P!-n48U5EevJs5c>E_><019he9CTIJ?%(L7+xmqemp=YMZFQE=M3TmISDv3c+Zd{ zd}y9$-6d_|d3Z`d-SCnda0}bQnEj#XR~&@IzG|ST#i)lDq1mDs@fnJI zzeh+SDnS_@ofTV`70!Jy{R99lS{pw`X*u7Bjs)-kaF%A&3Y2fdK%5Q9-bdLo&TIR( z_1e2UKFIbl+X46QT{?lszW3yIIJ)3F6L!~oguKr%4o>PJ%b~g9Ev2-{$P8#D>k`I- zF&jay%pMX|ZJsQVdCTNv8#+5m0|ZR86oad}wm%_7R6*hU2JY|e>+@_3+8qH9vJ#?9 z5HaH4FtCx9px&Jii1DS)Vn3c-f*6Xbf>JEEm@;AKA@RBBS!zHOrg<(NW#q+Jr$ipA z7e9aYkiX^pHBMZu{-2<24MZ4f5Q^II5te9jq!0j45r|kAcd`Ym7AFL1SHi@E{k~h! z0KeW&0wij`ZKaR)`vI~VtR`T{Xd3^b=p=saPUh@{z%cIK5Xh=R9)|y;$Q`&bl4CMqmB?cROOqKiUPI@j&5If?mu*UFeec_q z6F@7<-~SC%1SAzY+3K#Rd&tczKm=vjjo3Xydhr?f<&TL!{hgVg{UA}^(Z{+#rrt;p zGT>}seMM#G=dg9m1b~_toT@k5s-_5;h-6lkY}G?5iU~&Y%Q=DBl>Vw$)Km}a8!6YW zsj9W-RuV|99UHyP)HqtCJtIF$sCO0hNNBRUfC*ns;vsW4wG3kydj?qjoUmut!tbol zV#g5O{4Bh`)VAwnV2205E2fvN!nHQ6yI>>y0gC}7_2rg8E=(jrl2a_9_s(-}OI8G* zBBx*DZ^xePdk?*Sh%Tjr5mBgSuxU@wNg;O*CS1or9AMa(M-1ad#n6GhoE#iP5 zP0RFYO^h61=&YzW6)F%)@0 zWgw;8Lg~?!FIu|Vz!flCLm#OXr-OwP0XDY18zQKAv+TfmU@PSj_nrr5{6j+oCvw|p zWIOg$!f6%=jBJ<(ySEk!XUvnmKTtvrwJO-L@?gfQiW#?P)vII+$E+SwC;LL}@|MA- zH$!EuZq;*usutEB(n5TTC|;DvRg*FV1dG?3&?*H=NcH+XNgf`Tx~K?>niphJz;=x0 zwAe`=rT|PGfv`QR-|M)Z*0+Ehzy$_NWG#G$fto-NImaR=%k2-*EXmhhBLsq4gP&|5 zVHjvp|KlM2Eg}D4*P}@@KzSY5zQZNH9Q-UYXR46EUK4rLuX7q1C|2oBJe-7SzdlhSaPi$x;F` z8_$C%A^(58y=jvzNpc-_+#@pYt$Ocu_bebVz$}0Ohy(?YG?_?#lIbtYB$-4r(TvnW z!axj2U;rBcL(BlPFFoD!wyJw?W`w&w9QVkq>Uv9W%C_5eU)5bQBO}7yj~_oiRUDoG zw;x^xv%gLns3VBhbqs>8b-*t~4h11dUYMp62c#R8rz_e`AI4~Xy22q$V~C?)!a&6c z+hXX?BM@(~Nv3*V+irwQpjZm7^(6Kgpfd@8-s1BSPQfJMK;W*??$kJd?wpWB6ok{o zc4f2JmEVrZ;U9erL~m!<2U-y0kz9a%)bDWU#3oC&I>~zJ*;g6pL8dGuq;&kkJ+53U z-*<7Q41}A7UdK3g0Bc15;kw=FBala;rMtsU4m58lGJlfk`@lo*{k-oCJoLARP=>cY zK94GZkNciaz9R@H2#vK-%C}a`hXdRkYg5lk^u*zDkSVlQCvToUC z4jp>q!zhx|Kmi*5UPVv}A@jpbL$t2pvCAe;3#Z&K;Wl>sqvJqlr#Fg^)FAJ5BOl@F zK<)q}8tD`OkxED;StCa_rT9(W1DEkSGe8>`UU%6l4u&X6qyPlNW6*G7dfSi{GoluV z?@pU0>EJjZomvK|*RQcevXPKAV4i%y_{RgUIivP4_(Roi)b}ew1#nk2P`3AEMMOFh zpB)0=?rlZ?)laSc{JYvd{IS^S1zA9@ z?Hi`={RWsS`1B(u;pn+Uglx{Po08EU=c|=$fTNLWS^+4u0$oHJx=UvVLB#v5JL*Bv zVc`@Bv=cJ7AO^$YbHS8=#(UFQg@F!41PR431hg_JaS)Eu8pbTjU=fv9}c+5&vEaS2A+WG+US(7lB1P?qSf=(C-}3 zI}Sj?`1|zUa00L+_X&oi@6JCFbB;l9jWp7gwt%URoSuupOWXhzAewtwDhXzl(>bmw z_9k0E#AGHX$d-)^IS7eqZqxU9>97@RIpE_s0QYU)Lr%a$x&Ql*&*b<#rGW1r51sUV zpYc_%d-yQJ&GY9tK6(dI6;%qn8r_$a z!X&WMC@w++dpJ~JW={Zmz&0AlxH&H8hTd6cf4m0V*dT}zgW!u_o^d!-s8WLOFfn{( zKAd7g=8jqw$AjW9Q}Z8D4Pec10?@jms6(~lFojr9XT<&JTJhwu;`s~1Wg*E;HKc{H z34>@Mymx&dF>qNO@4dtC6d{q1pKGWQP4tL762?EwG^8(rW#1Z60E7e3fvp+bx8ki!-em*;1+!*FbaG1I3_NnXaHYfyt0Zt_9}DI`X_+w*0gdIsyRimnr8C9RP)B z4|L68t8sIOfWEXqtGYKwJCk1|RiI9VKg*n16?iu`o^oO?>s=%=h$qC|Z18v12-?t&o|%-#sRetlek0`#Xwr>g9(Ndtwk zO!|DPX#61q|EkCR*TX$TpFcr4;3swf@=JFov_CQ2``fFG|2@+%e*ad7sN!&S1$QnQ z>P3*Tsw%jCas?>`4kHuPsTVKd+L{T97If>VQgJ+1ESHY=fBrlsphWIhsS0S+2(JL7 zQd0EhW(@-*IG?x(R12MdH(VBj1ztBzWYJ4ZauKkTUo38O$IF{Gv;uO0h!~r56vfSL z!>wy5RSB|-T`dZ;j@BHjC{+DUt$UK+%owqsFE;uTKpgABGQpQO)I>NOSmt=sjj;Uc zqg&sT$8?_7X3@TP+azj4(2GUcUuPMeAad-LlEfAy!o4hTYs~GB4vTd|AOxH7SA?N2 z7-J<8H5qZw-02WJ*u@vB001BWNkluvG&%O7C*;kTtf`x$%*jpjYnI6gN9>HZhFLFy5J!28Q>UDO$JxQ5+4C3L@E zGY$-tK(^1ZgW2M*Xrx)oA zh(7)j^vhpE`SLd))8XR}BzE^=r!!lk(ndn}-;1Lc8xu*3pj|rpN~enQ-vuy8U(BsX zKAv7b;7X-Y38~;BklvAvc9?4pCbC$tgrZxG)Lb0 z%qyAzg{Y*DxfqeHCl6^u#k}#eN7-B=0ge#e;rV!y?}kNp44L9Y;(A=`IG?b5>Kl~r zbd2)6J@fBrjmwXKSX=~?`8SvcgeWds6hBy45^@4u6g_F%hPl!_&iW1+IC)xzP6Dgqh~ zDTm@BiiS>5W0DZxNojC_bY)$6z}b+Pv%WJ<07lcC1am!%KEl8TeAd<=FUJ1hAKC7s z;kg;4E4&60kldh(4PF<%UuS+qzum5%jvjB3H#*KPAk3a6)c_Z7?r)Ks_sG9%We(k! zopPxA-RM7uAdEH%gPa4B_eOaRFpQuEA@%D9_-aTF*H{O`iQ3i$$CRIMi@`22b{Y=B zC4!0L=7>p+Jiaq+0;SSo$_4uih1_I%IKDH|A^_59yV{+_$D6Jn{rCOHL-7Br-v5b? zPvig$o$HStuW|?;GX4&v=;kY(gV!F1nNa82R-h;j*H<{*+!7Kq*4tV)Ts?Y(tLuvM z>B5`c9MB-_xZ&mV6Pv?4xk9NGMR|>)3K-Z%xX?8)2{DieqV@}YniR*!6*teBi#K=J zxzQ_E2?a5M7cd2prFglyhiDBzG@$a+(M@nZcdSLCQy`6?IiO-FrBKWVAhA(sU|TT# zd-X7KQRb(NxN9}soNYkxEgEa|PUEfxXe$>^Rp8R-fIMw|kbopu_#X3OSp`eLe3?Gh z8u_|zICY{BY5cRC&;d--COSkr$3HX&5IMP^PlO?gYYygqsgcL8eBCtBX?lDuFcY~% z-m(-r3G=i?_FJ3ZJ?y{n0MhX9QxPBM@XIaMUwjYC55JB6?0xvY!^jT4|LD%f-%FhW zQs=;WU&l%4d}Ip1I#dhXdhi0`V)uD`EsPjrTlKa8=Nri38Zym*HFl-T`>Pdld<5w^KlgzXh3& zAA6ubk`{>5QUO#7x^OL8ln!br25O;TP*=+_moDAgO37fY%H}^%8ZslPP%MmSM!dQU zH3cJ|Z3R%U)Ym(s?d?`mw;<|$ILFs4GY;e_;b&(mLBZU;9thn_4dP#C?YlUUk8&i^ zW9NX%L*`a1o#Fw(E1eDzA8Tuy?ARQa@D*J>y7h&FzuUDF-cO^Pejtf{9Ct*AQz;U8 zd>bW$$xZb$I~HQ1jPd4B<+x#UwvsH|qKtTnb1osXyz)e3Vyc8?KoPmb)PyDK6Q6sr z;W+74tcOY;W-AT+B)0#~@sQzvo5yEzd{PA9p%d5}{`uQ_`2EYw&B>RW}buPGhcEa&$#&np_Tcfu>MS9Y?S@!NI z4oF1jd78Pu8915hfh0YFvln$EP_o7C;eurz}&f*01HAoMv;f}0a?zR=RPMn1yzRl(sPxIGzQj>9bB zMDV%8gU!!|M5PjrZ-&v(N8mu0fXO{uu8&Fq%Jc%aXA22pcK4eno>$t0JBC&Ob6#_b z6amc1sRFe)uC6phd%~EDU@mlKZci>{yf*6j>s}f-jQ+0KPX^lg8i;HY{NpS?H}vQ4 z7iN5_SvjL(7di5@&y2~vir9=y?q2K(9&TO4&{I`${-quKY zVRJ5Y7v;g>r9hPh)o!etPL733$G+k~v_Rsu3@sPUH z*X8Ls*6;rX^n)Lu{KoI2eDSMq9Pm7q{V~u0o&@Kh5aAmZL~M*rj~ng)%7`XiYAeou z^b+&XARhT-I8egC6_WO7jKTpC;oq7;!H7LY!tBE9R%LTC&6JE8TIXEU$}(nzvDR=d zM1*Ut8(@W<3B&Mj28q{YD^m$EC@lZWz$3@cFwWiAQNj-?P6sD?pK!18(SEn5l43;2 z64CwMx6RKVQGSHd5@bK$p?L|}Q+<(@9Vq#CY<&^N^on&b0;qVJ+yZqvM7ObA3Sf&x z1q9#kNi58uB2p#!c}QvCyp}zuRuJivHy#g7;8%GL_l^90dwT!*&g1;}$LHA$=)N)E z8~^)`z(dZ!>%U0g@4a(;S*tv5V3cEE?rktdYe5yodRZU_TtB(S<+Mc9&d3=6=3|BG zHQxL36Fhq73eSIai{-X4YK@$t0$A6E)qv}AAe@HqE}gl!b?JD~Eb?U05J*9NX-|h9aA-^6lg`%+5=r2zDhJb z0xpsB&eA)BM4dcm{&K-++_N^y{9cw#0T7XI?@m=g0k96@4j1m;#291{Cu{UKs70_C zwe=4(;rCi;7mo+FFTTC?A;Vh~s1r9M2P-57|Dh@l6P-To8wyY-;5a8YhM!-fwtt#v z^wS~O%KLVl1I17Tj5&9I4!-v2FF(ZjZ~i0N2R~w@tpz?L*7mb}3!PGrJyrWW43xEn zC3(HUBcd0r;av4l``b_fWa9>Y3abTza3Q{ZpVR}S9(V>jCx-}wqFlcNlnD@J{lACa zg*A8& z==FPd#&(}af7NZ+I{{fCz^<5Csw;ydpqfT5elI=jF?+#{G#2j$fPm;Zx+u^fm__6ww*4e?@YXlOO2-CFCo~`P2FA1{qGEvel=s_~n72NDwHfk3%md5vfatpr&n>uf z2H+BoRY3l-m;O~@4n7TVzzG6u$Z0+%!RX6pKDrBfpf%az*c-ZIkxd8KJoETWPks6uX6%E)$v(50QU{Q-+TU$!M|_#A94a-d!Z^~=hH1L z+_d%iGh97>0+oXGVmMx3u_Oc-?5%9G3&H5JSBheORI#oNZE4K=)+m>Wezfj_X(~ZP zanw>!t75q{^wyXIRmEyFNb}5^Y44N>CYg^esySt7Yl+n(%yD-YMu(Z3g$&deh$a@% z>eh?k`nuvUD+PfT!})BuTuAT($l!+pRTR@q2cm`jY37g>NXbqu!0BSRT!>E8Noe?2 z8&bt%KMzqNHOH!+feY9HjW0@KF4xSa07&5?8ruB4%)O@)VCLH)SxF!`TnQdu32ttw zJgBHD^_MgRJA1mDiElFezFu(o{$Jwq z{cpi8FCk^3vFO=J?*12jU%WQ95fYuyi;gdY1ChMVis9U^j>%nlnU(Fq~bu!TW-- z(+@Ai&_Dbk?B+w%fAqU3zwvuaQ{)RAcPaoLI>z6`=>+8PAshkqJNKDRdmsM}a+=uU zPJy9d)j_CyH#_=bu+wIgQi&2mnBJRHHW*P6FtKwW>+zMUc&outpt?oWyq}w4qC+FR zXAYJeFBYG}0dc2sFJTBhNqpj=eeW1#$%<&CBX^vEjB35x<`fE$eFHX8pNQ(y$*Jjg z`e55T^}{!UU~URI2hI_thu^WK53r#bnZfeSq1amqlTfd&U?l@M{;3lqQ#dyrmOc9g6faEkr_K2-ihn4j;~L3A{_ixuwgy*aL~E8hL$0q2{J+ndJnAw+JvDC(q` zro#MpXEnkK9i?UN29GkL2#~mtWFPGV>&mKx?#z2Ft6^Dyn_H$wYJ8tm!vw)JD~geD zCrTomI!nvfmFbN~SAuyim?y#Q*>E}&@+Wf`m@6UtwFe<^7toGKCD_9r9{X^fkF?nyPWEq@oHU6HW6Y(JZ zYj^Y>8#H%(&qzAwYc^ge6?(YlXV}=4k&t{6@+ci$pjS^Ai7zL(HQ0;y4O@z4|YDnZmBqZuXH8?@)rrG=|Y2SDSC*t=W_YM7<9&ded*H_c@m9KmWFV_jr z-~TzfJ089FUgXm>br~$Ba)Hww51aX1!ST~*w?Y`8Qd$+nZUNNsr{0Set2*Z8Tq zZ8|PXc<4ehUGJcdc@JsQD0l24Sl2Dzsws3coClW741p4NEDLk<*2rzhy}5|u>Pq1! z!?KX+pRje2*su&}5%On#(c;`KX2R0Q3Ld5Iae5*#79G)sG@>fm)Con|8L-F(-P7=A z$)m_rBMlIxhspF8-7)>^!!`b&#Q59!2A6;M3oJkV4*KmgZfwH(`t`$~z%2FkcQZU3 z)4ib|hsP#3)TcnGKf*&%je36oS{dbQfiEXmy9A+W1*}1CpJN~id-z`vcQ(mdbeP?Z z8*+DeBt2b9XayKV4_go}fNWi7!U0L=CBj1uPYZ%w9_fLBmF;gM1n-1&Twpg(v3&2F zC?Eb9^=p3w93OM7ct*!>Mgs;fV6*-xlAI5xiW~q3JBDhZG4I9TXtaNaXayYqK?L<0 zuuDNZ1!+(OS6@1y%mt?pSOrm2!ZI{K>`d=ji$V{4*x3LRmncUKsoW$T6b%=2XfIUkZ`#&?^!HeRb4#mE8;lg#K5M0}>8iv^@xwJU|a* z{`VIa5i*95&LG95hmh0RaOaK;r!6=zmO1|_9QzY{7U)dJhkxIRy8gKlFI*ahRuaJv(lnJq5I&R1l7@6WO1LntuUWTW~&IFwN5t26A+` zY3P0f>t#iqDhX9WLR0IOfemxaV5yX`iwMjNW{%zrMOo9ot{u;xoGuE`q#K`3{pHG3G$oHqn&pdB+{Q3U-QGX&Zz{fED?pS~M z1DyWre?oiyOTap8xdcQX5q57ogSL4*yvHC)l@NZEfq3tb`3QlqM&B8lNjQfHI5;C; zDV5N_bznL3wXt*Ca>BSsY>{$C|K3(I`W3MA3t;@+fW;uw#PT$)Q37ZfNmbb8oJ5uVY}*uR#O$PC$0w zWAx3S!8>#D#dUB4QaH!0K!W9HzOq4&Fw*YLVXc!{?tqI2@i@LWG(gmREb?-tG<*WM zIhLCs>Iw#tO=0kv6Sv(o;8vqexv+Cz0Wc}Ncjo&=*;!EO?4n{Y=*@)A$JroP3Vjr% z1uM%0`%5FTa8%yj8yh)v`hJf3mM+v@u>GR}0x3L*ocG2jAVqB1I01{(wJPd_m# zkhZqLcMZQIrX3=(&zydw6*8}n5|Y8Uj{2pbHl|c?3X+)TVrRNWoDN43*x6Yz;eem- z?ooT09Z|rS;9GpW;~~2iyZm_m|J(6-C;>j?1l*7E`vbrGSjHa!rb)}8MwDL!T4uD? zhp`m|oaavUzoMv99n4#Evg`@5(^d6+j&K6t@J_}|??%RSr|d952p$s`NAG0vn`JNh z%{@f#p$?|FoI93Nr)Q}W&W4T-n%t+kAT;#M+sgB$ByntV9-=vF!@1BPz+GJ{Y7wk! zG|qvu%r8h#wJ5svIL|FgVDI$!9F^S9AR?$rr{Q@wOjDNjapMkGXKbnxUQNm(j|t(7 zw34yE4+yf(STh2zq3N&5F(BzM`w{F7c_H*KDdSrUJO9;aBRo&RTTqO)z8|gezia&8 z81=V?)8G9mF8}c7uqBoCI{N7)VfVVDLgxL^0&gqa7N9a;uk#UhX8Ed=8R(trZ&F}= z2}ia-k4K0}?^eSur&<~SN{#99*S1`O4v^eWRp?~nl554skh&Jo(#KqZn(08#ku zbJ+L)4E_8J_(#7FosY;sOn}#-0qF!F9nCNZv7(V~61>B!pd8{pCO58`ltc04EYP5F zp7-<0s8STC4=*vdf?SvRbZpfwdkqj@z%sq6$Yw?~s@!AH9JY33>8-7f^=ZfYyaTBt z)p(LJ3G`s_mAP-NyjEMugQyPl6Pp@&2l|asDz}6~A3%;R7%+Zc99e+U7Yrj!v?v?_ zp_4HXg3iy9F0kUmF-XG?&`Gdci_{08*BCq_moTyxpLKURjVFULBZ!w0uvOHf`M{H2dX_cv<1;)A=c>}WWgQ(!0$~b7Yr2XEe}r6yT`{d`1kFizvgdmb$lKO zKpOn}hCg|rZ*l_ee_kFR87LLS?|$P;{_p~xD7D~nB1M(Kj-#2Orm+T4Y9*mC^Gwbh>_(mwxJq>7BU(U17%W|o zl;)~*C>*hxH1@lI49&r+#4S3q;1GB;+z}y8!Mw3BHL&>35VJqP;ULf#gxd#>Wz^&+ zYCzfCqQL0_z$p+^r|3So%gMvZ`d^x0?Z#$2QC2s#O%YI{5~3D_fXS9aW8}|U?%*j6 zs6HA0noNF=CQ50@J#zg@hJQQ`vkpfA*y{Z2PT}v97=L%i`t%n#{muW2Yo;PYjjk1 z%jZsz9fP^4`30{)3*`8iC4?8|{apl7XXw>Cz;X*}o`%y|DnN=mrL(QTBLt-CU@VGs zJ|N8fTUc3(vcft6vEJ?uc#fMPh$?`0$Qqv=V*yZ*X#MHmqTW1(fAxU6BQK+rFpwsGl$P?uaH>@+~k9h!cK z3M3C^r zj(B|~Jbk#j4&t4|dPiFq+&q1WM~@%l0zfM_;=-Q5NG7v`FrJ3k!@4v&1mL8pMI#ym z%nYYnCgUDGVaeZgU^-!4S{Sx8u2FteWe9+be7krE+GM#ICouH_h+MdbBOpM+^{UKm zTSUz?Fx-fGcf7deMr$#t$NPgN&jv&hn2ARV-qhUU_g@zNZaI@_ytNb-ED3Kq0Fd>c z=m7w3Yyr=Wz|a{fnF!mhtM7Sl5$$Viazp}Z)BzU-4l`wYA=$8Ji{@()$&$WWNcsyg z$gRY0ME*PI|Kl3}-f{lXU*q<>{~3O{0m`(iyB{-?KLYBNl!k7!2BL+8k=|hI1v(uV zUH6rS9MP$AKElM=xIh#(TKAUW%)yy+e#p*klmZ#o=#j?{3_Kz>gZC5mGQTN$p`+#|3WEZP&p zqP=!H{n<=R$c=%qflWz8NMKYesVEI{O#?)u+gt1tw=vuYg&c%=ZUgMb;N8&A5`7|w zD99}Ax3kduWt8hESB0&0Z=#%f4%U2klv&2w?LEe{vad)Ycj(+l++yCq$21!|dll$` z$OFRZu#;2h=OAJDvGEq-mXF>AiP~&l9wYq&bx-WfKZ*mcKzr%%wTE+JfocS@aJHQU zMXdiKC{GmqWawv}e3{rykhP1l5fAf54Y=s74f)LV{iQZ-e4 zfItq1h~Se^Z19lm1q^Fn!;zq2DpO&U+Kf;&SYykUzu^U2uWX+TEbJmx?+3b%fgn!6pIV(d{vYZWkTUJlnOkV z7=@kAB>iQcd~Jqh&GbYFf(*FUN9q6&WI0!=1Dermw+I8$vEhXmJY@!z}h`{#XKar(}m;N^Gz6G$?R zZhTn^Ap)Ha@ZLsq9w{>sf%O8r+yZq%xq1hpiuU}2EtCL4$S3mm8auv)k@pUn4vgS+ zfd@S9gIpLa%8^UIw`}&mOYVRa=IvpD9ykp%0JN3Y;oG&LZ#n#}yo`okQmW3m9>%D# zH#E_Q$ic|rQ8aS6r&5@ocjriJyF&=v!buF-BnRyD0`mPogSA`eul+71i9sjfRq25H zPC&qaL;^e}E4Ir@PL6vSYt(w?v>p9SV?z=hsjuozR($wfy>+wku1-2dTo26wg+)@y;^(6QMFjGz)C z8#pb%oqhoKkJlOQhl1z*^Uu`SKXiN+RRFJh><#=wM*mau03Kf-@b2RSZqEy*>X3KD?XMPEJyS+WX1gD2tU@WBBXZIGj@-;Uj{ z8+W26VNf>XpMmfFi2t>bzZp({|6lOp@4ks7qZuRg!#PZUD2&dS5 zM+CkuVFc6qL>Xah+bBYIc;<#n$(zQ{*Fv!%4|t!y9MLpDgWx?JfvM8LsdMxR$nP4j z-u>9c02v-uWm+M|m>Tj5bVK{mUqhD#)7Soxgt(l4Z*l^5gC$`o6ha50D&a`;p>IJ@ z!_XHKY2va{R7R?zTnTVy3NR5qcsLiLG}@8Yf;ce8_rPFI?19_~YtX}!Y!S6O^ZF_q6MiD{nB+XBC4(3TM)T@`oeX~F`RGKW-4`1vlDzCB=K?+)|7}qC znbNn^Ig*+0fSqG{L_tV?o(Hml>?@KSs3aF31$r&8XMJleo?mH(b|b+L@SU?p$0a!@ z#D2;z~<@45H%PEWzH#i!@DL#uEkXgsb13Rld zB&z|k)<60Ih*l~FCd2PvQ#f}FdPmhIpuJKmMI(qUYw8Gd5;vv zkpe;MnT9<=L_u7S2)#2Gz#2)4hetfOG~O*7zjQnX0`Pz{z{Sq)o@Oz(eJ6|)DXG4q z|Lkue?E?MAzkrl^14UkO>`8&?#HOadV21|W$q743D=4eM&BdC#UIgV4(}2tK71kVG zdKf%L?OGTB*cxb|Nr)104Y`LC5WW`>&Oing!g1?S0_+x1`fBh6=mM0gO!ZZk1N$V< z_?@H_Mbz}$N<$|O$hAO^g$}@tg=5b(EP@~wg53pot#!HA!~h#nJ2-fMumu3_fGiG~ zw|z6EAdxs9cKCG4AobvG;HTjtY&{hT-R5ol$ylBXk1viX5qap%dkS7z|=y z*@7;QJ!|j`a{3*Zerc<->nO_r;|kvTxNqM+uED?mS)c0otg8Ut2QRbVO^Gh9t)0-9*CzR` zsUHAwK#sp|8ReHS`fh9*q*3DMf|9Ce>M&#Tp?-EaCUhTIx%vdCd%txmd)EW11?S@PH9fA;RsR&yq}*h=2*M4 zOh{xz`V!~1_o9Kj8%IWN{CsW-={(4A0Jii3kM#Ly=Ue!5s=VGQ{~pl4 z?vR0fW1m+U|NBQ@Ry_Nwe~+8L|8w-4XDD?-dGzkqX4k=JInqkge1u)ju+DtHuM1$& z_$H$#Lv%u`G=NbW#ybBn#?I;!`hjfVlW@EsZj`3y5zzmJ)8=SGrx`L|!`Ia82j_1X z8?ip-bD{@#$^?h(@DwK=yQQxGQf?r-jt&uO|j`sdPptKe8jsG6yaD~){e-kMXVf>TTFOjkWz;Zi}ZKgZQ ztSB=}a3^!|R-!_a>sKa+&N^DYP3X`d#2hRYDg1REyL!?Te1Y7zdD+^$RzSS{`dFZ|ybk`~A8%{)Ul$C&jZ^Tk zj?cmYc-=)j6#cgce)qaGz`bSg(eMA~|KuzEfBCmlSQ4Ba9MQ32B#h`L(?vT#WhtsCmZWIDS$Pfq^KQ2Nf@^8YD!R4~kQ~(WW-w_uD=~QGXAQz`78~n62Dj!OSw(RbxJ3?uU z-=2+ldLzFgjsDe9G3_+_DfYv>{vP@-Rfh2|x*-7fjsN`_b>H5&1Gb#-;h+5*oPP8j zNGUjc`Dt$>9VvVcZ+?68dLp@7WuT__&F$hYJz5e2-%mK0}?2;0a9$X2QVooXp1 z(iHJA=72C&mZ>8^nD>{oB<1e-Yw!W~dI@_(V9yB4BKefV@(y@q*VF%TtmH}s_i z@i!vma4HNLNYlX!?k~?OE|~_3=StBY4zdG~S?POKrfmR0IfS!*>#*vmR|PsDjA0nf zFud&rHmTj{_&gN)I^ZwqfVyF;Hszoa1TPqkb>M(6&W1u&HY2^yrRPwGQ?uXkJ^Wf} zfZY<^u@RB`7;~=GySC7F<$`(;lxs!137rJv`EX2~<&lDM@9DDyC&9@humw@pxam5$ zk#H#$@dXO{oFeOuS)~G@%U$d@KwD&Dwee% z<@H(07Z+Izy0ZSYwKa4AfI3e!R_1`xd+zQO-;jjc9j&b>rE*axdp}SF$;Bun?e*SZ z*3qpKb_;bp1Yb`cZ|bpb=h-lx}QMMzud*fI1OQbQqpKT`^A` z*N-ZVZg2yVSh%b{AaD!l)CEHdw|B>~5{cH%)WS5gSr6b?8`AUGZJ@z981n&fqqsRc1!_%;RURKk{|5*Y8iB!X72a^1p{7|LyZ$s)R4a*by+On_ zqZ+&th*D=?O6cfJg2-Z_M6t7&B-$@PX3p#~^m* zIJYCA;Po;-pAM2i+BqEZGBiC%S~b|ypQC;EPcZ%7zXT3X-s}YIQvtYJ9!L-&Pyllj zE2y?b(QYno?rsk4iqah3mCj;V_;oe3Qx9V%=tx2!0UdJkVhq^+l7`Eje5FY1DI)R@ zXj-9^o(8s{l;Lcp&Y55;F%&w zs3N#83>LWly~}6HT4Q`yF41thlsyJsSFPr+c%J`$$Y$<;|G7H?pBDmf|H59y z{@?Qf9=gE$KOgk8zVPlN`JJ!-8vgv-KcS-8*ib5svqu*#73!x~3U$CRq<4i17y}N6 zIm+=!XsC6V5GvJ57(Yo;68%8kI}C-*e|o1f;H+qC$8uf+O0PIxDN3nWPE=IOnr0dv zg`wlcixnzc3($-UNkVp@JJyAy!5Xk+>!C;(Ik=Q7zSfa!p8{VrMl)}V#TdDa%_d==_>px#8l?EQS&oWcFXc2I%t>M>vzsRTylZVf1u z{q?>8jgF|ms3Gr6BaF>9?uVE$t<@qO7D&PPPA$;GHT2PYyoQTGDv<-q2TPt<3>q2k zWOtKQgXa}dr#o-Od)J*FHQ9LObxa;cuqjtwg@0n9@r;^c1JywQQEutV(!)*+5~D0N?dK|ab(+Y@5ZG4 zL>19&#N?p?;s88gZtqT2zi`~m0d$MDx-2hNu!o38n#jAOUxF(U)esOsXTd6%?nCr4BR0S*uFfUJIJ6vF(aPbfF z4lovqM8(zG3|7M6XR@ngJK)?4xVbt=KC7hw6G$M8yrJ#)^mwE(SBdVNRZxqfHOJ|) z!Dr838g5P<$61GAHZzn+1vt|A35dVQNGlhG3L+^l!J z|IPm$H$V6*l<63??h!TE4zbb7h`B8r2`-3eww%!~C#WiHxh0$uI{sQ}-1D8mOnrPQ z!}8eqCDKST{|}}Tg5W2mvXyV|G-hRroO*n&Zn=C|qy!4nm6Ap|_pXfGmJ85Uaua6C zWn&E|AnHD~5)JUS@G}$EF%KEsH0(;qx=b;+Tgqw}{(DXV*V4{mC{gq2ot+HhW=aZ6 zAnNGFF*Fx;>4E$@cc4H2IqHx80xdWJe0#p~Rt`^??cmvcP*G@MvsALmerG)!1DW7@PsXhwtdYa}kly zGzi{;Z|?zQbio|t;7CnsN+vHmCm_}(;#!g)0bylXoZC3ypz^SwX3MKhsE zHdI1+u>$fv{7lqt`cipd;=J;BmBD|T$3u?8XL5WV-2m@j=tCTU{o`%80B`iL{^%cn zrTuUJ?yqWZhIPH5XoY*B7cWsp21mP`VBK&$Ua{c@JQ~d~Ct7L^VjfEXi_~cEX-GZu z?$P82Y$dEUO5zL@a1o)xUgu&v947S2(w}u{c+q!}N)BBpPzjhb6d@aUo`A!#;C$N$ z`xJ0!jT|c|m=k-|kRr}M%>_{iC>I=O&KY8z_t`r2`X!8Ut&9Y--oB8R zAn9D>MhS!?oQbjcgE%W>w7~lX-2F62j>*7Er-A3;-5?$%v8g^-G%hPR2U$Jf;JMK; z@W`9RcgLw{Cc7P|g?b2mk4a*(hc7Enq~sbiXuklJq#Y47FQW5 z%jA3r9O|UQCde{!-4jr+75!{%b_4!fioh?9zLLWuE|A$Fs%%{xyRt_VtKqoxTOx<) zEcew|b02e397HN&{Id|nJr=IU{D?ikyF)@E9I;0M7_E)VBMlWo8)Lkud1L^`M;PF; z`+oSyJ9-s5QnpQZZwB1>pcTORJ8$8n$~K?I*9y;jyT)8fPe8%zpmF;GoHUV zQJS|I3T5zOudsmU8_q9Zpw^0MKJxb_Fz={Z;ob@13TvMY9jffMH!GND6vU<#L>>2t z*tw%50TIPV^j0cYvGqO}#t4XgLr&8PAQB+T^1bH=VQVzvy*pZ` z(@>&q>EXaKCoK^XfT@l;F@jHzf>FE^H?qu^GrIsDuA>xnLK3z3{wU|{eKV#5 zTHj8vOb5txKtDZW_dO`GYK0NJ(>YMKtVWqBgph3gA=;qO`2Z==`09Lv5ppHzFVX=N zw_3OP6<(bP;dV)s!W}RfJO&9gAj?NK{$4y$iZqOp$vW|xZ3Qy5fi%$hUiKkiA$mtl zl-)|-?RVq;g%OG!-{tOW_%dG0`pdsZEfxCJKLkn*C*Y%ELF0Fxw@Eht#hvRpc0KJ5 z>x8{^R+OgbtBH2?67angMzOAia@wbWr@?4g3!MfT7smDMtwV>X29In$W_=?TGvAJ_ z8Yd*Zr3th!7_jCzf4HK|1v(4r6JVo!#FI@WsaWZB8CP&j4UGA^_nmSWi8J=vNXBn$Q>IhYOvc zd|kVBpo9t{oP3KSLEgM;fOHbn$K`b*KzrMD-#+H^w?)M78~=|u0-yBw)DFOo2>2@R z=Ao$nRgZ_#00~yT_W1G_9_!b?@*e*3`7MpNcb3;UWo}EYXuUy-;P~j8k#@hJPDi+R zv}HlLeuR`jm4pYAywnZrvZ72tt##D;i<8J!6_8R`^B=Ih7D25Q-7KU(S)Ow^R>JWM znbi)K3tFrIb4CpX0&~Op)V9TuFliWV0ftt9g%Ma+YVRKoL?-|$11_tf)Cq@c<`>t+ zaXeDHfNFxQV)6(VMJ;p!9824_ru_mm0BzyIs?)Fq0D$Fec)l=#pJ%26sDI$m^I$W8 zW{&fvqo|LPz9jxB4U?$)=9|~_Kkh)&Z^xAPuoui0PFbGxBmlv^__15 zwGe7HhZK&DIEI1M`3h1i#eJ;PK`1k<-$3h;(76c7fy*gMvjkEn=Hjg*3a((@piE0?A1a8AXxZ2p&Iaiq$4~WCXi8*^28E2x|^Z@{O_Ri0s8CznD;smlLvVLviFP9X_rWE2-E~R zDcSy$p_syEMO}jr)Ep@6QG&|)eRrnN(%2C><9N{5qc%WqEZt*U1PT5TAl=X}j6}(A zQuNdS2`4@~9%>cV-FGSiq8n^Usb7lY*oBeq9e=cdiiL$-saywc5%44N2Vi{6{eNTJ>&%!*A|ZPSUjUitxQuR-o_mT#sbk#BX*1@WJBT*H zLhb*PqdcNR&=-SDJonUU+2=gVBSpDRZR|H5_nm+@MgRAmfcwtF-Vyjz$7g~7gcI=4 z1-=UUU*mD#_&-z`aQ~toL;_R=|HB`B9e?$mpU^9ga-rTMXKx0#hEfl}qye7;?$lY= zsc_2>AB(c6L^+$H9x7~Qiz_O=1tA9x^p3R~>SQDzi3I7*(OL_m$q8;AixR+c$;s#> z%UO^TLzJQ?>}G~(s*u8xx}eHp5bshfJD^JmxN2=co%rpg2o6EsG89O36wDU8a5@c} z%3nl59#Kl>)^fmyJMYl(Sri;vn~y34Ex?l}(KyJcbAWIHAcAQka?(-*Cm_FCqS;U6 z{YtsqH7qeb|3o0)Ur4`#wJS%l`mhFEnTwGqwx!;iGGiqCR>D zu#UEzA`f3->zR?^bcnHx6j5(+5*jdg5C}!Kw1QbWa-0bP{d3a{s9e(khP-e_WFS=R z3H`@?WTe)C^+cn+vNbhIg-99s*6~QhrA|Z^qD?i^1z|u-Wh9=0LovvnIs$3@mESW4 zxHl3MBQ@cDC9<*S0Ho7^NO(A~FHa2jFV3akdEXEg%H9F6AObTb60;ZC6_mb8NBha& zqFj9u`p!Q>7UbS-X4eZR$zTEKeIak2(VRc&IYqtq=eJ_@Dmuf7|}&|MOe*!>2F#3&FP0Ds{Kr zJEr*nH$$x@)c*uE>a)8wNN*5Tl=%P(XaFJw(}8lrlI4Iw3>zCY98)dK|9dWg&bwz! z69B;JhLWw_dPE!~yB4D_S#Z288sl8+(yoF04nN z=Mww?uGQ0NOB1}jZ5-r=FxoVxrj$Q*cNB@HLm-ULz%({g-4I%LLpK@ahv|4|VL9O1 zxM>zaa1NY#dkKC*d?=xJcjn7Q0qp_LGa8}Mx8MNi z$VW>dfzaEE(fnb)eRLsGIYf(=76 zxD4(?emN4(fEXlG6PV-sFVT&X=kI-m{qQfKzx%J)MKDQlI?g7q{%%TP=PYnNG1}Ft zD3Km(i$beWBLE_`voz2FlwJVu%p<3vi`sdEVV(=gqZg(u#f2rw78)Q9a1gjBXbS(# z1$yLpTWWv^hLO*-qgJ_F76|@L;@aG4NF~G%`XYDcgQOvkQ4;m2k@ta?dWZt0&VFx! zTKb(K+>E_|V_Jz+^a~BV2DmNw@C9O^H9`1bilrz~|Kt_)Sj05|8^x zKt6vJ0`Oq;pU3Zf?aTF_{o(K7KYsJS;&S_9Y%nG3HcJgdSO#RO0bhEz*js4S=lPf* zV(fWArisaxh9%3?ic$jf>d^t0(}K%sfrZ#gb^v2^V^|j(&?F~+z{}Z+GRhD{l*pjc zTW@VwmJ(55seqf{;uZ$Zs3>cjgzyH=&;YHCghGmP_~yh%WKwGICVYd-EWWSCtREhJ z7*Yovh!*IrJNW{ag_5=!sD=cdng)AqY%{D%Y2s?BZCEG2K1G)L;yIUewT`R zxZ=27ZbFo2i+H`QL$_amktP@nMxGo`IK>aR+wL}`4uJ58W{ckphIS5V-aJQG9Ag+I z9P}t@EO3yd2$Mi5V#Xxd@19vqIA1iRd25BFlA%t_^Ny%IB0~4U9H(QWa2Iasy9Jdf z$^AUe5I0IBBGxy?HSa>%(2bQk6Z*9kqb@KGyM2oGy>FuY{{IM>4oH&Reb4dNzW@Lr z07*naRK>tV0!EY!jI4+%RsYoZ(>jf8nTdK;2W(c@sw~y(e2quM>QNVON@c{U}uIZ(NW3Nq*=BH z0E_{TuW_w4P!G_!Q^1G&rYL|(V5bmi(okUSca49}`Mm`2J^Wyd-SI1L3jRN04`1g9 zJd_516B7QJ9G@2g@U|Cw9|3rX6OdYavBCko!VxH{_}72<8}0x3<8M`j>K5GLu7teZ zk^Ru=6&B`smj&x`#?|$s(CuGQ9zBYE*N35J^%RXbbL5KRTRxS>w;{!U^1T2?WSR-` zbgF5ajrluC&s+FAfQ)O?;eaTzqJimvmoJtO1ObtPur-Hc$^fFiISHe;9!85eYCa`I1(5B~a3@a+44jcm??TAY*Gl6mYqFla;9?U@`y*HYjj}bUxHFb_kbvlh=$a9xgbcXTI8TgD*Nef$MXGq26@thwcZXL1 zEd;qhBHra@#o+VAo{^-u(OBr>1Z2V?v~DbSOLfk^oi&`1usX<1QYDvE5WN&I>f6Wn zcsTf~&_m?Sqi!Bkc$raxxWyK-x7vKVQw2=q#+s2Z70RfN*grZ()IU-doqKjWQ`QenV6}=%ld4?lh&$&loA7*sCFY-|-6*4quwi z01kNg@ZHacoPbY4ivRz(d$(pww(LCU8*|Rgwf3&6bM4cot(L4-OGs+Tx>*vI5GW)h zEE!}R1P~AqIC$ZK7x)J}@j$=}FMcuv5j+54a0G;f2o!`QhmDMNv1RMlt!v*-cc1Rl z=TcRBua%i|j={q>#>`x+)~-_>I@J!yh*f*n+Ut@zbIv)&_{KNBaq01AKAsl>uq7^B zN&##i=V-td1=x}xHYVim<43;#WBBl^FW`+g-v)H?d_5uidOVV>r}Vx{DJa9ps`~`i zyblGttG(x!>EUMqH+GKi#KcpG-O?HFmebSimyv7Zf>B>Wiv}j$U8VM%+>|5u_58G2yC@s z78}NCj2Nd;GCwcn6|x=*u-`MSusF)lRIx0Kj>-@uKVUy5Z|);5^NSOve>?(yJT+FA z4*~Gr7ybla`JMk8S{IU{-@&w~)nG2PVn15sVFy@}oM$8)B1 zX#9Mn6uvUl4N2Va(n5bvNGo<7R`}Y;h%^@J#J}g_4!5q?ozm*$`V2)}Zyw!psP{@3 zRz5-WMF_a551+-P8HL z;y_8LuTdxyY)#4a@f^Aopri~Gpw118!(^7|Y_pzSP?eT2!40DTaJGNv2W(PqB>uq( zg62k9I!XZmD7+nQQKSM__Ddutdk$S#V8YU9=Rv8ly>Q+EwMy?D(ETXC1^mSW8#m(c z#z$6-<#ax#^Lq+;)EKS%bLx$NB(s_V!QA%`U(Z!U`4co0N!2U4)rl+Q7_u7EGldr{ghAsSBQvsKpfDiQez#M?SDL(1_oI3&W z^V|`5ngeiY050zGuf6i1{LuG(9B=;eW3*)^i*DbwE=nrCP*{$%EaB;4XT#E2l0(7CJNYVtDUi1*-vkpLR|`ZH93q#6ONnc6%DpoCGC#?mrs+ zq}3itM`)ge{^N*cJl{OTo4@gkm><52TwF#FbO77&;1O$($h@z6X1-sXWM|q?=UYgW zvi0b^`CS)RjP&iNE-=&{jjnBktw;Hi6JhkW(CCB2)Ibvif|OTy++535QpPqR8TYL( zlT>K19R82IJ$L-(W9AKqTJ`TkcM8J5cBh5jD#eGATbIP#w$})He*gs}uh4zhT|K`s zZs4q~#+Q(l-?c=kWfwuR8dAkrJ>Ty9`k00%&|x8gg=%YT=uyh14a;YL8)g3j(!Ez! zbnB_5fzo}9)d@&I0rTr6Ar-^0E9^oiP1>YZn-bz41k!|}IO@GeJfFsNB*m@9F=IYb z$6kE*lEH;;Y}F=!(6FnnO`1gEoF!okVlVk^v(hOaQ=iWpw-C`OPi8?I6nPZL!0T$u z+5GLs8AKkErw}rbyalfv(H(>?t((%C#c7@*+_4lg9D?5cFvU5opVJNst%mX-_j5BP zPoNzd^1Y0_lkV`O9V;a;f#dH@esuOuo4%i^nFCBw*$`fe@5Nbg{Y^W09|>TrBlEs? zaD5mWTVp! zQYGZ3TEXovK?+j>b)Jz^LXr`y3Y707nI(lZULK8^llhXa`F{5tx6ysygFUjU5R@T9 z3ZeaFi5w~epJnDkOdhecW@r@{MlVB5lmM1c*aD-?lmulIZx!4COQ@clkFRgl^XuLg zbeZT_Wbb*+n@FAVamU55%)SX_u-hTv&W1Y?3S8}krG*D$F9QLz(E-rH4v40$E1EH< z&)>(R|VWe{yn`CljF`ALMQACzy5#X<}06vWUBg=-JWyA6dLyu zAY|?DTh-_MnzbS7^Ce*V3m{d863zO#*`~}*HZ|_FXN^4qQbF&QhnQapEY21;^4^+y z1BrJn_4ve8kj5Qg>b(T0AZTrY=>q9tqB|O96c-Y|_L#K27Clm188Yl4DLMpyA_^N)IMFLAJyCo(b$y!(sfbCkjLMHk>js(z;A!b%1j6_RQh5 z8SHoi{rv9$AO8_nbKv6b>;c5;^D$!X62T^H_dAdjs4Zb=8Bne{apNNePI}{ciFeOb zC)24BmadKaev~M7f}0qc0y*TQU2!y+{z-)O@STtY1ibRz9fl^8(U0Lx>fzt6~H`tDWVZ$rF_7r(WA>>EFn z{`j+RAtAXTD{Brv2}&A}2PgGuL#q`z7vwZxns0zSV7J?GaYLz=#&7|x&D^`Zw5csK zNC4xk;yU1cs>=dx6=k<)6xkHDvV=#MirSjP;~s4%dhyE~Fub#UT^l@Q#}p*|2zA7WH(>oSMCz<}y zejgByPIRo%3PY4a#(4T8CgC=Ciob(-a8W~h=Zi>he;WAkw|7r69Jte)iPh-A8Sic! zM9Bxpg(ZQlDpE~IRZ&J}?ahP+sj$TG$`y9L_4)mH(aCR=A~;(_xCB%HZdo1XdNb8e~_nyUM7u zA@4co>P;hhn+4^HCEe}d=v4t7u|QXh;||by*xetodwF=801!44?P-Spu9EmILU0KI z_&|^6;Q+*iC!7w#`2d`A0A>)NiQfmDIssjiYV-4=BXIwEpMLtsJ|TbdxxZ^|o{%MB zsRxwZHAW|5nHjxrA)B_&llQb9aR0?um~+g6w#=C430DtZWI2_Sl6tNd!O#w4=Uw+e zhHf4bOtOn;H7xUjWv=WipA&%Gy~~s{QqHKYt}vwo?20wtM}ePiU^uFyB5x3YTiKSz{V3exTxHXQ)(Pk?~_X;!c$o%Xw9>O6r0 z8AlrH8Vo5yO_B#U;?DSw_cYI)+nJ{oSJ)f{*&-mG8h|?n5cNwqNtIZ)WMSmN@>()J zN7tU$jf1lN@~{GM{e$c|{Yat7lRvxXw%%8kE2_(Dlc!MM_$0~;uOr=i`KggcIQijQ zNCG7K1E?^Up5Unf7x5{E48zb2br5O;BtYHZ7}V2CKvOzzqJ}&a4;UEDs}nP~8X)G{ z1Z>Hx&gA#AOjIZ|QK)6|4g^#xHK2h`L!>~-=oh3gs*c9=tjfmufq#}{fs8Esiz)VQ7^8-6RPzPYc0|1^JfVLSVru3UJrD^z#6I_Q(Ii{J;P5KN;Tm;#ZwL zpO8DdnjO@Vc-|6%{r-vu-r;y{%*B?Hz=T_csUVFk)2XeYEe)j%NZC7;`DvxZy6ZNv z1hEVwAclEbaGWN;L15Ltom?@35V&VrL1e=eIcKsWw9zzjpMnGq)|BsP5*KJ)i~9*@8L z23zXxuSrreqi%mLX!F4hv`2O>3RLz~h+H1M!@6r>dsit5d4CVE2Afx=GIuf0bvn-3 z%RaR+9TeoY4i&iZjydX{jkUR7gX98~fyOzaVt@TQ4*VXGm>Wie7gY&?zBsfV=VCsO z1aNqq2B>>j2qQ1+IZKRoT7RYtlo8Tq*Gt$K;V@VsCgjenz!8k(eJNB|@?St$PekE< zZOzFrC(*r+Uly+KKnon%f}tyUQg1{1x!Lg%>;FS z1Ws$Jhf0XP0m}_FKu|rBH=rfw4S)y)`%k#D(6pPFVQ80!1FOqWi>@>89K8I;QVNzBJ3+8 zB#~ET8L-7rj}561p^5V~0a_)>_40S#7fEt98BdOFka{Woz2(4N`uQ{j`~!Cao@W{0 zIRvtG1TGQ(=Cj|&&Ho3SmIPkd5l)ZSUU^V{>R+<3= z47-HmZ6&#E24LL z+A)SA?Dp3o{sd&%I0M}=*g63l19(0Y{g`gv!@HmPy&lDH4RChN2W^#U8el_4Q>0-B zxZzG^=#gnuQs_qUk2|D7>k%mx8j&7K`Y&#Dsh=4XO6lhVkohkFYzbY03R@ zZm5G$K1?*F+xUi{eo1AzS#lVLN3&K}QV`z{IMJ$C{i z@cc-C!08EJ%71L5|7r7cn+Di=Ljx`k#KqlEMC7mj)c4K**RTHW_@ytu%LOBXl$iSr zMY@#54GeYr8ND)c&%*%81t}n~L`Ha97KiI;R5=-4BG`fe!^va{%+rLNxxtp*K=!FG zQuX|3h!xG#+zlr}tILsNxAf#@p_BH2&=jq9*;@ennLlXki-gLEB%e|7mwA^cppRPN1KoM{IC5Y>U4nVg2P*1#tYx@O??8@<85cu z!yJlaVjq?=mdEdMyu+{xHRY{V)Z53ND^w?UjTHH*mHNd2Z8&Q^Hewm~3}WX4f<>Q( z9gSd{`_3w5WUd|ps|!*dpq?|oIPX)2j91*y$_SCf?tx|3udl=Ze(&jogkOEKh*l3P z#CPKT>XAI)>5Wi2WO=OQfog}qDdElyJqb+0Nt824*QR8$1d_Y1%lrUuXBIgv+W0}`~_XsFG#13;Kiy$`$;C*$<8u}xVDfSfb(Adr@6{4!w8 zFdY})z-joc8OHs9oY;j=;d}%mS&-fN+_^~!3z#xc-z=LOx6nI?K+!COGPuJe-$~7# zg_Kyj7fOaPFlOEaCI0U$@#G>tDT}xIUAy{4y?^$<#Ur}?1tNVS{Bs@=+v$J4@~MA> zH-G;(Ax4SV7r*J_DEmE@o5xg3tBX553EC<$3REksRt#6yt`QCzi&lXo(7Hg6k1*VO z8CEOW?Rywsd<{{Pn64i5^hPQ>Sk%V5kd)O;!V+${cY9UV-#L1Z(=w>r~3UA=PF?#20f7^VKJ7&%!kTk$j$fUc0$fA{N*3-75P9_`&PBERzn z@cPF9TSb6QoK824w*9|^k0Vq9ErwK-sYRCzPFW=n$=c!&w|K-V%ucKCBBq99f(FmW zyAuFom4riKDYiMfk;cU4N#6g&goH)|xfLeSXp2O`xbPle zWuV+=P}gRkKbhWo z?*`co2qVf2U1oZ@gO?t)2`L2>yhrHOi?QKJ4vgB%n@knf8irv+(?%w*d7DlMmU(vh zVM4e-g1RwgfM>p#>0OH$gzP7e#G{_r>(sTPwFNhi9OAPci<2>YY}j89*j*LeJe+#< zR$a&@-i-ss-QfEz;Xrl(;s}}oOIzq5lr^{9bk+0DC9$Sh2zeqIJ0uR=fmF>)D5dWX zA=jId*ZqriE&q6qBW9AbOaFSW^d9}^Qv?9ArU3eJ!6lq-AL9$Z^ABL!FkD}w9gk?s zg0@V|Z_2unniKTY3~f3BbwSx(K@y`~Q-#(Asjxv%-raY#Kt;KJ0eRfJffe6mooLxr z0nrVTEmJHQ{@)aOyy0Ysy7}0JyW{8=8Fl>Ii_e8D|5jaQ7;BVniWBi3#CZE19`+;_ z>f+~_VnE>anvj&~m_!8aWzZ7W)cwA6a^C1X0)A_d7T1Ny?u(K~uRMqAR_1u0U($&mqg`UO?(%%zn3jwoBpB@1tuv|Bhkmp3ecwbip1YncG z-d@(Oh)y(YvqDjO_fjbtobiHHgXGNJsuPryQaKNB!I5 zdFlXMivHiP4DeY{fC6kgmpmN>_~`4e$Y1`Kz8in*7k=Fh25&H1^}0*1GgA^CkuhJ-s2~@?JkKyw%+o?6RT86rRVWl{l|z;|d?E=d+M}vi zl&NWF-kJh+;dy3FUXRblMB)xiFwg~pR0w}+U4?MO4gYBbA4Vv27PujGGGGw@@_EF+ z4GiVf6+BxeZ~ehP#`NeNm>KF!#H7`Qx$1#@ik>@mr^1#ANfOfagEhnfR-++8&4@zz zv{VL+4?c`EUSWCnOFkWhDq+I-+{ORSOMlqf4Xe zBs+>S^w7fHflhT0Mdl++UBM7O{vznzt)gTwNPicP%{|fzee@RW z-8X>OzxDKQXKx`2;FC~%9wjdi^hYiU%~1e94Ur;Nh6LMX%*O;I63}3RwgBe63vAQ) zW*7>xJ6yH0N`PHSeK$zX#@7fm609~l2)--ZVzA6~gc-=f4-JqFdXQ2=9%a=A@QjVs zO}fO1hthz2MPqChMw>(3mWb57jL-kZJ;zo|H-)3%#`lJidytP~Np+ObJY@kxxng;9 ztwd0u0IK5nXmvK|Fr@I)AW5(sD%#R8-7H9BLK%49n$iU$fl%?pl07*naR3}jy-GKk$ zIl!h$SQmPbi%kcZ zjtlA%Q52DcTo@^&1gI*e;{sK~em_8xU^;kxu87OZE0#+6-WaQ#f!Yj*n+bWyxOZ>x z0*cMBdvv%ghT;N9>!{5ZI@FCR=>hZaCSi~6(6{)+U^kaf4Zcua>Iy!rdT zf#q<+`z1>sGf7_7SJGwzMO#)y9tQRWFhlPKXhx$Nxp1F#G;j&+;N^r6hvA<#Gh@sG z!VWJ+I1r(%=SG*`yEWi)iURaTLb1ym$$iba4Jp3MR)+-PBnfhch3HZ^3whx04$(^{>SEyx#f|Uk zn&4Qu9s>!F18lkhmZP8Bg1fjAaW76tnXRI}@%z9_--tZ!PmJRU3eeL7K8pk!RzOQSk=;%)JOUp- z>+;0Vh|UdJfTn)V0APZ=2X-$P=wvv!28axaT!7UBK<_ou5*zMJdTOLSUm%rEK%{uo z-zNpD214Zh7dB0U65bbKw66m69s~e%s!;V(?uC^Pk_4@>HF8^;_iNzPG-pwkD+YJ- zVIcszV=4iO$f!0&zRGAvIu0NH?w2oS-OgtaPL1|?x!)!E`&mRTY2aKk{vWs#@bw}A zhBuUPcS+!k)4{n*AZV)#@Jz=mFWxWz&d>cQ{`0@_zd=%273G9NFGJZe$1johZb*6X z#h_TGBhoHAln-t?1;}}X)+1CG*8ZxKt9&!hneS=Sj0RFKgySwgk1pbQ1Nux2}@jHWCDN-28{GAFsX^U^mokNC_R@6=lRH5$}Y5Y0DTMi)sH z!%zs*4+#m4fm;SraiuTcpkr1h7*rD$V_p9=b#`5bgw?=+^114t6(T-n=$iT`*D7RJVdI!sBMd9< zP(%(sUM~eegSG{7eB6h!*Zli4+8Vp{nR!&t=P8U7;zJRw(`cgzJLVO?h7tZ66EMfw>n1uleQr+Ma3^+m;Uu@+K z)w!aSf*~|O;F8&6!qtmA)I-B`pu;03(CQi>LNvr&|G-pH_622MP>+rI^C00J0sHC% ztH5$w>A&rg`-yUT@d7M!h0OBAdSv8poyb=Y;gtHGF!DZ|f3NtSNE9Ok#RP3q@^MOX z`UYPgT~bp4hU)?(u-c(HwIsO`X*0A&$DHJ8>m`M0AX3_nF34mGoei}$eC)?Rg6mgy z!9lxpoEzv%(Ela7cFFiZtpZ^CeILm2TpR%B0dyY`mj~sp6u=QIvly|W0Xv)yQg8vD zDGeYZ@d|C&VqgMc+MEkYx$e;h+4kw|>pW6mT^bFtx`9u{ z_a5!!s#YuJX+mhWo3N=205oGtV7i?EF^u~w#Fvh%ar8Va6x z8X(eut#`FvXE>uh;`e;MI8+<&>CJnhlrJSZxq;QB4CKMvSU zf{W{zuVI7BH~XGrSk+Fq?^7jX-++>WSQsz>oZpZb`OiLXaQ;JRC4f$YiD9{=O5yIh zU_P*7tEsxP#_?P~7v@XbH^x=1=-q)!~N5#E*ef5P~i*|Ko}E^z?1pV^&&r|x}U zU9?LYgk|7Usk(FD&p zB1!^Ni9X~s$Vn60JfSYP00iUpOa2Ws9J(y1OZ5PQwWi9@nWVX<$O@z~qFKYu(Y>gMx9OpH|b@r$h(Z35{4mRzt1RTqxGLxwZC(Xfs+|_;kmc|=%3=@o1g6+ z?kqMoGr-yYuuNlb1{o8QB$f&;6Lt?qv}K`~O^~ic)`&rz1e%DlhzFoO$aDj!%Mqy6 zH_A-}strgPDV56?;>L~SQ8#)4m4Kf`q`)0N5olvy3vv_^x**HuGvmFK4?Z)0B3-w@ zylgR+mC1@ub)uj6XL`Q9=lP9MdKdFC^9qpFUpKGHF-Z|`(YK%p%}088TD6PBV`TN^Msn#d(}iImGmWHE(fF&8P~E3|GUPGCGMI8PuQCwt4w#v4 z`Nki9P42&b6}INm@wCYQ67jDa{4?YKEG2*+`0?Bv01?WZ;K>0xCr6wc{OGeh_|JHZ z{wP>00-m`f5CC3(^+owBKmNV)KmGjQGr>K!x=cVqMM`A-!&vX#<{Ll+!~VW|`o3{^ z&P%PKRYK+@@}*w)U#oZIDj7&X8An{*zmGOA)Z6fKC-sdztLp%aV^5*7G*7*=SVnfu z^T?Y~?vv<%m_SrGnT`3sx}Xn>x_2+Wu}2gP7;bNlPS!JIWLFY247-GJ%n(^&GX{nU z^Q>$<658I0eaS6>N_nA-=1vQ9e+fmBZ;0k_z9EYR8T>TwAODZMzhLut`mL_Zj4%A& zucIx5jH7zP03anKRkXRnazjcxP9Ou+TP%tcSX0#L=wzXEx)E6e#yw`XbMp)_aL39_i%&WcYi^gENi@g!5y>IRrHHg&VmeVv#N||6JSN zMqY_ig9qTKMB99BAKl# z1puStr8eNvj8xdQv<%sO74z575fDc&WE9_rRztns66pbdoyedct_y};K0$7XB!ux^ z*KMBbk5CKLXyxrfNdPb(7v#}p&_QL^_K1@2 zL|Y8CR!q}^{k;)aukP{9f9WHWh&)aAKPJo5qW`B!-d`&JyNdw$fja_UPXxd*ruWqX zIClW{{#-Z!gTFo;fbi+w7X=Uj{>o4MK>eqm`h5PQ&%H&WpNsntj#8omsdU9X$pg%Q zwoK60kjoXCl3ZsXqa0GumWoKG(9$Ow4aoZ*|*Yx`LhkwF&oBIE%?8`d@8qSk=d{7WeN zrL+qEi1RT{jQ%r1U+;YGkMYhMpYAzA<%zmPGt)NIFL1tv0P0b?q(T<8FeRX^LL|E5 zIsDoHn70UaBu3m&#tvw0=}Ksl0}97#z~g#Mzz%p$aPtk;3#?8&MG<&kRF9fgqfay-uZv288xdrVi1z*3 zdih{lod$OjjE3JJNh~GUwv5ovkHSo?4`1^611pY<_&K=tKp;Kh&N{DOPaGmC14}32 zC4mN>lflO}zRiD|Zh-f`2z>ZkaA_1T9wK0^86)J*WpSC{Tm*T@NQL?J(7>l-WW6t`YU4DEH)T5+*O+37$QtVEkPsE|q4qH$H^A42`^Dr6P^c_g z_?rKnpQGd;IP}l*zcmdJs(>XW9ByVT%Ys@f<^}k+ANU4by%cI*7mxG6HtgJa)W1cx z?~=WLCi&kF68;CCvjcDe1-Jx}TuK96LIk#4fUPb-Ba*M3a@#{ck3 z>AlBCXSx>zL!Ao;kZ})~^$CTP8A&@_q`Hmx3QU-{Z7u-Bpv5IUlt#a4!#q#OY3Kwf z?oQIRPYe_x%DDi?V`mYMyAk{Q_n=>W2gkz!*3_f*1PD=pW*%gUcwKNrhz67lLqlui zCgdSu*bi8yO5@jujzhWB1$t}VqF8}MDa}9_$j((lg=0pM45)a65=O7B0%8d{IS(rX zaiK#=*zGcgA$gSV8UdXU2shHQM#axK@BaAz`4v3;!WVG;(hE-hGe~QYVd$Gs5bspk znNjk}kuYnJG$J?w);ArmW)Rj9=j;uAP?5(SpcS?_5zzSt8@q2$oWB`U1w;p!m)3>X znxdWVhS(|i4;gCYM{3rIoKZUtv@wmkN}8t~ z_mf&11e=Nq1!mU(NjW6WX;8I>gCF~qryyY@Ll>q_o6)IQ9naN>6PY>-U)5k$`aatQ zhyX-Sfk=mJyC9~W;OhPeGsAMIlsh+2Pr=um`FU~{7^hh#vI<+P2mp9G#GS+}kU_9} zalps^(nnODAm}`CJT3aaMEpB<0?s9X?==3;<9OZ(0PrMyu^p%wq`@Cy{EvQaBTmx* z;TS}pfUPD#>~_HW*otrcrq}UvKly#j-~Pql99r|7k;Bu{RmgzuTZlomVp)zz3WyBs zHYEZ>MxAc3%rk%lGhqv0H6&z`bd;WJGv@4TG{`X$?WpcYrnc4@w~yXKDg#=r^k7@V zbiCze;tW*?J$iLEG>Li{(OS46916fNK$c~N-b7Ga!!*skqQH<~jj4f@1O(4#_xj<6 zQRpm0vk)lKC{CJJIv|#y5NBXhvZYlLokRwtY^);4KrRVoNUra{Df5#R5m=w!*o>zf z%kdVEzVxRU?_DDeg=v6Fa%Cuhi70GYQ1*LRUC3HDflV_|3er$wpF49yB!?P z8;2WC{^mRlID~3#Et88S*%Am4z&Olyid zH!M@-Ufge&b&f(Hn|VkWb{U696XE{m0z^h0#yl^mwV`T-LUEjDeD{xjOzyvS^|amQ z+@@_)+jo_?zhCtKb&~*&S3&2VjR40od{YB1I5sfV}zqa5V3{ zc_K(Z{)69v&wuG{`3JxCr!Z^K)?EFsz_h~D3`)qz%+OkesiNxKi$Syr9fRqB>c@P0*^y zB~k|h7|Ugv6AYvEZ^SGj0%?k6ayC1Fl=vCe$Q(v&FyclzbBJ4PCONT^44&4AKr=Br zYYsn(qZ2vjX!Q#Sn;Wq#lDSex=6f1|C_o?>PrI9^$Cp3-hiEn<@Aq_Ws^akY5mL?= z_l$De;!&U(1)d}^hdK-$2G%-zFgGCY_Ul$Fi6w-IO=%P^tvVkOEtvu4Fxz~Cln3P9 zl{5d%^WzbvdWgJr99%ZnT7~q^YT|U)#8S8(Wy%^^!;t_H&-Zg4cO7;!M_>$?k5E%~ zGR%dDK|P>{N&dN3Cv?ABqMT29j=wYgWz+O0t*?W))`FkY`ZxccApG?-KuAMv92pTv z1p0mtXcz$WXpYESh%T+syFiAK&RUcWO5$bes?KQhEgg@xcy8OB3g-l&Ep%M!414f z-8G4#(v?X2%>t@H1-+C|5Rto25%79Zh#;;2#F`$BkBa3H(Yh(`d%In*92U-dC*@kq z@x*mz?(ms`t5KL z{y#VR?h`-bCipcRUk?NTxa=uiB1=SsAI3e5{dnPVP6XVd0Pj0eL&@pi_^BT}eDNy} zuRi_8TW-V%D@q!n%Td}g*)rWCje8&skRd~BWquZ!u$4#gk_T^mQF|Id!w^(%NF=5V zFf?xDVGPiie>Ja_*4RKebggC7WkG9{0_LI;&k<)f9|4WLS%`2La>+i!@6XT~Mcnn}@d$0d`j-zuT`XRQ$sGNnV#M zLLGo%%PA zYJddCNnn)7J1M`q3>&qsRhaal z%Pu9X6^yvu30Nj@zi}}h)rNUqfHHKY9fNgU|Lo;^Wz1NPlR^e=Y$V^%S-sSAN^>BJXL7!F6jR}^4~`M=bdij_e&_i z|F1Xzkp=+PK}%Tc_=5{juntTZ_kjQJLIMWR@Smdr&y1CP+n;}3{_0PB|MEZn!ap45 zr9qboNHbC%8L>-2U1pIySwi8&aRsf`w6joKoiR@br11&@lFcH@x12hurPj$K9Y!9^ zxp~`M000%%l91Kh8l(DD3Q8F;AS0EGwk)V^!LZ*E)~dB{_-hMw^bEgd{$T&BcUxP){QMsA@=C8D0@sC7(iD^>Vlbq{bY-Y2QGj zTV$uCia>weV=?3;hi1CVX(N>*1Fr2{sCi2A3o+j9I4eaw-vMWeNr0{#9gHvfW z{?d&`O8_Bg#?nBx_KmVULq)?Nh0VDPL`9$)%txO`0@2b z0D#q-iIPCFTGpRi$RrSeZ52SM1a2z@?kWq6+@3*E;{ppwRtH1S!PGB>r zu?$O*AZf%h9VN?P3>=ah%Y}wF575}?8*6wwV%faJF2sLS(BWDTk_Hk|$`qnd=EQ;Q zuQO;PGGRHRHp0hj@9TMVu+b0`!zs6lE&Dm3VGYa}8%`cP}d z&BMn?DPf*whl8V277ez}gqT$lj)w!vQZRVWa}Iz2VR&GGu;XD=Z6y~;iT1=n_=~(@ z7~F1npE|P)pZt5jjK}Z1hp`k4y8)8ob2AtHJfjRWVv$F-BuK^K2w1NQKt$ev*QERF z1M>88yfp5-368DNdY#ag15z3+UES+qKT(zl1L}0c_QVffrjhYzw(Bb7;$>}4b`yb3 zM+b|TCTx%2;n8S_vn`qs^h-HfP21gwWc8vf4AOJ~3K~w}Esr3%`(|MHc6nONm z%9M&5<#2Arn{e4elm^~wV>R~cz3K+PJKw9J^CY}i1^8d<1Q^iXegpE+??xV?hwkZd zLIw+d!A6)LW>mk5Sbz>ZNC7pXsz zcAN8f1csa2BMoyiXrUmNi6M<7%^^_=x8EkPJ@2wqM6G^Gsk5&F2}w}}qFAASpgREE ze>kquoXs3XJR3+?JoV{NE9Ig^T&+*hY`uSGKs`pe=$bBYb^#r8S5{>;KsZtidcNyl z{APUg6R(9Wx$8KO{?Dt}E*bmt$0dpT^ZNgj=>K`-|ADW63Lt)-!zY)x02!+Tu#Nb) zKSLNOZyw=TJaeyr-B9G;{+S=H?>@RIzx}Dt0+LqUelz6V{m5fUYt76^zNyQE>2QNQ zUPJO8R(8HeHJ73UBV}V7S*z+zO&B#18-_#i+BXg;8wf$z|2q{%P`QlA;||N=mc%E( z)dyz}Tu5+=1($athH(e270W!MXVQ()nJG|Y1iam?w9pVB%l~*hdeo{wBSSrx0%So| zFmIRz<8Hv=5o?ysD5>fCdyJmEl7i8_AthDF0qlIX!?4>CVR?K5(*;55 z>xh7P^+PATB_|V6c2)#b4@|R?Gdz<(vtlTu|0MCxD+0104+Ymd0V*)v&W!HG zu-}caqIg^nSiCtw4nfz*O(+#e{+L#K=LJg3r4fC-Ap#f3^%}7#4LT`wc$BgS2 z2kiGFj<>V7(QQo4^?6H!OnGDN)eLd4ABacr9b9qsLvhiwb#H`-xNZu12IA= zlJTxvxh~SD^4Ko1{x2E+yGVdfH~=DY=k)rs9$z;HVACI9>j0dm0Wvs1Vay9w<-bdh zEeGJ75EzaC@csd$y!hZc{q=wS$Cm%@Z%^g7KJ|tmG1tEw9zy|?a^?9d6IC-cgQXF9 z&uE%?SLXwm=ev;Ipc~ARH#<>E%ep~Hxd4`2_{9gt8r8$Xxt0V|!Mw~EAZS**l3$hx zike6FPCSFblqrCDI^gi=5q4MCC&yt<4VDBXX$r9h zl|=Zw&PXXEiFrPwAPpl@87=Rwh}P7(6V7@I;oTKW&r%pnpicmYuHm{9a5M+Ykz~{$ zP(-1<2;up%L)u*d5cUp;%{tHpfSa6TfAg=smz^|zXPbbE2+-lujCr!A=8M*G;WkNwiuQ`c-#RP_8GZkN{KJJ zHvCB$W{UYho>3kX@=h4NH#&E4B&4?1-$ww#Fr;0N?yYeTQ#e_1AQ4#|Eec$vYJ&`d zANjYwOJ4ZU_1D7sKM!8dm2JKQT7)NY1EFhPE6j)Z$RH2(Zk@ zUYFjgGVkhn{n{#Ias|U-KpDw44~=cOGFH+A6q1orQ#2tFNWfIU?Zd|y#sOt0NT#lN zz`ZnOK|8WG`FJ>BzPZKq{cGI6zXz-^IxdG4;({PBzBX=*PT&R`ov-;47c` z6qaei^~?8>OUC^82FLdv;>8cW>|%QvWjFd`51nTk7H45=t(YD^!r|>BqhJw~JixLe zG)f01cf$!9EqEFPKpKhISV|_&l<)Gi9;Nh%-SP_c3yFQ^C5GHEJsQr`D1bKDd;o?K zR_;SIv9;`Y#Z4XDyjd!xXXCe`5j|h7g6P@M;amSc$;B%j%u@x)dnTYq^u0GgR1tIz zK}7jnaw3T;m?@uaXS5Cyn{{X)g!5zG@W)~P)sAc&4qG?!{+X&B{pc;_jJgl#C4#m( zP%+o?2;@x1iT$lm#X#$2!}x zG>5vWpK(}@9@PRS8)UK0?i>qB%E*>Z5G4aJmcc8G7!^wvRBJdKXVk{l!zoeC@-VSV z0gE8X44)N@?oD8=B6{Me(5RZ{?IZ07fd@q9zE&WI>pW{lh0r`^@)5jsxLAY0AcJ2+ zI7VRiLx}XUY*ocHRVNhlvyanEv{jkrP{yfe2U+Q$bHU3*0`CN~$HwOSXy|^t3u&oFPBmS%iz_t?L+yPjLrO)65 zym0@z{HK5YXQ%(-Z~mR#AAj~uSlSDWM3kn6G~6SBff2jVaGPl;0D@vUmI0B1<`ASh zyi4^==J5#EfsKT8L0gU}xI)gX5@^c=k%XKF3|IGIMzRhGev3zM+#EGFWCKwJV%5F! zFL_roNy&3+-bWzdOS8Tiwc239Hx}c#miY^WAP-nzutX>twK! zGB+tzw0Xw#@EzQH5F=8O0|od6SSB3dDx80e{& z{uXzr%g)cguEsyK0o-78B%uH95OEL(haCx-ZfuckMKa<$)pVZ$)OrcPxcrZVQ?Sln=F}vibd)j@Mp!VfUZ>2S20#)qneU(w}_p%VuWWlgzLz6H)=% z@wV6X>oSX)n#cf@9i)sT2i7Bi*11L|K&!m>k_U|A6>`~QK0bm-LK&`*%8nb4H4^>W zgt*xZ(M3tjy$3HLIb4`?_Pw7B zY@J;RF|7nHmR5OwI-<11-sXTZ6(W>cF0MzwjgoD|WP<{1yZr@=7@#qa-|jl*>4>j> z;WM0D>O{VjFziRPrD1-2gJqiW;%l!$mIcf4h%yemMkUcM7nX76-5%rp`;bzI$cUhf zRJqf6^894$hG+%Xohtyb$Z4~Gy{x3QG9q8y=j*gVr&~s+y&(-DZW5lF*Mw(}oDwkZ zJ)P7Ly$Ps&Pv@lUuOVeXYcrjcGD7kQZBF7u^Lw=F_=nst4WX+Gnvnw#4n-;m^4v-8 zU-IglZH(hu7U(KN21t$!<^xH7p(UHdNW^&0Ltx`Dj4Zc5@jG<)YLN!2=i@6o%B)GoEl)`bE zP;$Z5y*=i6#^Jayn##`ok%axdf@N;Fbq$dH)rjnf&`NBz?$`oc4pzBWNlvgt`QfGw zbum}aT!o@!^#nP@VA|_AFaGg%;#fco!@j_vSZYL5fz&zlk5K&6jRU}WF)tNYFOT?{ z|L{kq>lgN4EA$`aze}wDXEFTGl>dL;#vk}+g8%@hD8OBiO2SG8+&Tg0|BontivlE^ zIsw4{%if#D%CaQqVP8aMRrTp}?tb4s^S0r9C;Mu2d@_+ne->ZM~zxxOD zd!P86iolAL;}L{_QW8WhZ!W|rrI6$^l+7LOj;_KaNR2H^>N#B7OOgRsgxeY?>R!|bx@i(TeR+3UTn3GTnI9SVBB0?z%yWPf-(pmq% zOe69%BF4@l+0Y>lE97xRNs~DUMPTdz+tP{1%ca>PWMlV-U=Y9Yjm;4NnZOZy3sNF4 zdo$0Z!Mn{Fs2k+MrrdA5{K-GCREV84N+vUseMA~3k5J6ulp-iQ?*S34VuHKaj7ooU z36e9)G`4;L+?!Us=E<)kdBvHr>UmfjFCb^-=rX?$TZ%NNYfv)E?hK8cETtg^^)$s5 z0M7wdN{(aoW)$7g12H6zfEc$xIa)6QkE|=YK&XN}Vhnlls5eaz+E_}1jUYI)`-2(( zer;u5DK+$kgARw>ZaF94TrELP&h zAmb7N0Oqy5l-jP1ag2sxKc_rlmv%_GxJEx?x3h*#f!T?)X7I8uG(gn&HGSVBm4tCj zW~_b9TM6^50g`gYm@}r7(09Q^vDh>~Qu5!GWX^da1lItm^GcZ04~_WBb>0h*JoUn` zC|uA=149=)kg4m(=NPji(B`qOmq_R+AmXj4l;K=twSdKsCjM@P(azIhR`o@6o z{5RiEH=nt3zh*!6((MPU`#o$fGXBfQJ`wPu-2XWQ;43}8CJulv5K(POxH_>lT3;IR zCA7bf2-I-&5d6Q70svox6F>l7eB%@JH-7vF^}qSae@g%2*MDD2&SYLBL3seFG2X!e z)L9-a@6HIggD_muMga6pQ>r#QF(3ug?lwTdWM{jT-MngCfMB@2ZKUeav;rxQ2FV30 zb2Bj>z&mCDfFlPPIgJRhLtL%FUIQ-EuHB$!AOMOTVql~)8EH=hS+Yl)>UnHN=f{}S zdd27|6Rcw2X+M4mNTf3SusD#*=wQtcsG@JiaGNB|x0;bVwf5OTq#EF3L6c`)3 zTgC2~Mh;Z5Ygm1%hPe3g_e!W+5QchAPVK&m%nflCQT1tW!5-b*|>LiC_0N zt<;!kiCu5I5t>9@FT$wi?Bc;)VG`%5BF0&>Yn z*_@~%gszL=fpO>VDej!Q2FNP-_gmZCni|SZq>WwG2wALWpw;#-=g;~MI9H{mWc+tx z*TkF|c-v#UUZU#l!b1^K60)M}s(@-m8#Z66inHAoXXA)<-y_Cg0ZJr%=!f5vUiyY- z`2o#-9u%bR%lsaK|L+(5UsU`5Rf+zAuN?wVM|H;uvjb2o0+!CeA;Z7#04$CEp)|mv zCu{r82ae~Td5r$XkN>&xzxq4>c=&~1{!PuLP=ywaiEj`aDS^U(Fsx0uLK3N}XVLQmUAFutTmyg#a;niCyo!t4zr`ax?k%05 zrJ%67vYoaYUp*pgaIF~msGtDJBVaEBBk~(o;T!86p%c@-`mFY>Xva6tAI2B`H>8rzL|B zQ9Jp8O#p_l`J`#@`(#8qA48o&b*K=r<3P&KW6)KsUEh z(uB|ti(lQlT4zTB?H)acaMkAwz{!7bH9$c@A#+GY1SL%rogi2$?s1_+~JOta1 ziwJVgcK>?Mh1f-`)JpwQQIt{et`ENn@A$xrf#=eqi?)|T(f=U?V88tD5bS@b_P;DX z_p2WL17BkY;6S@z#bHulEe$;60PqDrmySV;Q!xA5SiE-Mar4^c;jjNYKah@w*!|<5 z{S{4VB2__}M#NzafEznC#3K+}3*DS{#9;-qegzylRCv+)(sw{)bX{+TRsfJ7biJiA zi7dk5ny0N+*P~0(8Sc9_c9n$>ux+2)0_#MF^OxvqbX0B54+(vrc$ zH#3$stxIZanJL=mwUSkx^YE%5$)=d-Q9VjFVIvV$6{_by{!RvdhNDghya`2&gVoLi8jQDz=W(ofop*D9Ry;M+iqkla#y4Hb0$qay#cT3 zhiR&ansz}d#hhy=DOU(z08k*rz$%(~%z$yTY@ezQ{!|i-1jvw7Q#Yo0phfYPuYV5T z{-f_J{SX(6_S_NH*OmtPVQT)DQ2+k(i{Sr@YX84#<7RI1jVzI|0kmz-48? zA(G(o-%BT;A^~3%DG&fIpRD7*|Chc;&d#>{Pk!$IR_30Fqvf$^&Q#U?97NVrz*>xw zO4dLGj{Qtf0)PrTs}q6LyzokbLx`pe(DH2+?keud*m_~*Im%EoJj_wz?Jhd!A^q4a7iXo8rYc9E5Uuvzp=6Uc$cSuEqHcDOj?njvY3eWx z0SY@7>!jx!Ru>xz5NeZ@YE=BkgA}l)am209e;z8y+6udmcXHz@LKh*ExOZQByHhsg9ZxK#dfr?mQWx&*o{83@+r6zoB%IJ7_jz z3;~2I5mk|-Ae4-=af@4LcX2YT(RZDd3zMOQl14BQ0@2(RlNBUpP>Tq5J5xVg^*wq9 zikbsh)Hpvy3ND?jkwx(8ojVB3I6hh-MkAWCse`$Jx01iUisboxPG7c00B8unU@f5!Q-~N_#>FRn}8HRZ=pTER7 z9QhxD{g=7^tv~L^0k~fU!1DXQ;^R?p0Dy~~fT|C0sMdc;0^HB}@5BEMh4WF*dwJm> zi|`3n z!wbOWLj7h^0SO#Us488a!wbe;kTO9uAi|_#8Q$C^2Cq!8C`TbEC0V_)+HsSKCPOuw z5vv{enmwt=0zJ_91HkF6SFqijAqE0-XR&th?uP*4Fu1V>)>kiMynE&XNdJet&ew)eX3_{?TA`yKR1aE_%m036 z{Tm+xVq{JD`>1L$l$@XmZvcjt>Y{mW+m5kVha3%+BLYf+Z0{mmx_RF< zdhTNuyHSl^6)yYh*AtAU0$9m`({8ki7Re^(3&D2Up-Ng8HHlvKYsw{KmqwIQF!UYz zz5_^6Tu;Cn2q}vCdR7ztfk)#6!jv*@ot|P9J9K?SR`K2uK4nChvXL|IoNaLH?p+N1 zfUb|td6Z%ZL>(d~9>zKcG`4&Qr4-ybJwq-9myXxyTpnH6)l{XDHq8ODY6!m$Fs@Z) z{U5K6pm0<{6;D2Q9Y65n9~`eee%ZahgZuqr5udX|^>HZgUyc6m!1JZ?Khy#^!&`dpZLtHDruy=JwxbMwuw^keN+HlNTaHtOlPEV(2{mY+dIymvZbbp-58sHDbt*OI}BP{uPieiq4BJ$(SK+3$*f zUUsJ?SyxEHoT>pCsj=4NbFegls^!SZc^o|BMdV!wTSTtKzG1(vt1(0YBR`{;{2 zKcFzhs<|@35D>A?QH99VP_G)%lNO)^AQ|MZDj-Q#0U^#<$HKrn%%WD$W^<#?0}#`# zAZ48ExajCj>yB75w5#>+b#WU(u&zIdBU+B)&HHj46^$qWchK>4Z(ZKIm9&r9|17lBxF;PscV(7| z7P6dq03#JcR}`iJa{XG{0r>y`AOJ~3K~%~KVhD)dFC)pE4w(tPgw?9Y^>EqOAR)U0 zUPLfXrfD&B9bz=pt5n`XOT_@W2u8^;E~@wvvOQrc2~$q!Vr=M=`u!vxph!&ts$iEU zOCM;)jVG_*&;PX#Z=ZbjM)ak7DDbO4b@8Y{XthC0rN4b6zs33Jf8P-pao+GBjQ+ox z#vgbD9Dv1GPC78(B7ETIA?^Qt1Yr5|A_M=WI05Klh~NLG-&fxEr@k)#mw)T;^&kC( zUr}W;L>0yeCpVH%g>ppA*K_DW6o^6x4p9L`nKqEnfrydI2$2j(ZP6J`dRLvvO@MI@ zd^2v`LUFBaa*cq97<=osrrNZz0Yz0LY{;fAH!JeJhG#s=|DL3hzAP^&B^%k$|5ha- z7wb=6grO3wb0R83HyfJ4{k-AY7u2-#Y+ue@g~N!LsN&^Mf7~3M;0;4iqZ^AhO3B!r zZji?bd7RF*9M*Q8bniR|^pfUf88BC7G3sIOOMlckF z(3&LG^aRLP8HZ~9eO}p*-Osly$h@UMPH*AxwsBz1HrNQLDuDHU93lc2C*E4QZ7KIB>A3;^NE&P`L8+x^?TY@d+(z8%8p0E0oX$UmJm#x{H2i(nDqhn5r9LF zeFtEFlJ_eH7PslegNgX5$FBGP#eedb%iCXiF8=pF{qy?Tt=kYa%_^vMm8-mN5TI%X zQ%MoJ0U_KZg<^O5igM`Dtxha*PdjTcL2x7o6&Php4v zfYzRtgE=IQ>(0V3s78^X#iT$@_{W+3En*~42q3bCMnwv!nDC7%A<`Ob=GHyU=wChxt zsANbcCrkSI6FYe?Kogt6A_n>+XU6Ad=;!^-p^#wFe~ru)@V3N8K($uH^GbKI3lI3! zQG*%|*BgV}U^*dD&GD;5JbP}}wcXOTOV5abnxY(*?0qTr-A(mRTjbyB_dO4zB~#-G zP_VpynOw!s+cXn5N6ZYn5jjz{4B&!B?5c}uj)Xc&QIETc1}&XpCmL!=DBHWXwib<_ z?mL>#>y<<*?wni6YLN_(5%l_gKx7jftE4zl0e3w#Wu#)#wp~zvFi{Oh+T){eueEcVk}kxkcuFBkC3{?=KuTBg?Ktmk;M2c z?0+i|+g(+fG_YwkS+KC&bgM2y?R8Si9*GH&0*V&oVhxd&WI}c0&&1fJEpBb^;F&i* zhL8MvAEvjw`?*Xrxo8>jFE7~A?(FCN_iO!^5&pgdur&S`ssHVl|0(d*h5qY!BpiUn zSYCuWs&URZZ`hac{{Bzk{KW-kWMS!TDZHw9@t*r32KS8PqgD8~e)v1{+u!^g{_DT< z55mWO>*J;zC(j48YmS zO6pshdb+_K4I)E0sJDpO%Zl13?>Pd8hy6{-6E=5m8~>ti!V1&VXKnS<1fb-MZs<|- z*n@-WfQcJHCYBO2BygmLUN|huBqn>$VgaV5Xrt4Xw0%fkL0=So+KJ=jT) z6njvAWTm9Z;8+UYk4wf+S_gi?SMlSJm#+MIzjn!{8(a`tO; zz&4hqLN!JLRA+%i&Ksw_m8ofqoF;T3K6uAi{BEv;Uv70(y{>ox2YFepmDy%V$RY@2 zh?SAv#HNUyvmymWGNv@53muLI(*RKga$0DBsNF*$bT}FqQ=U+mk&<9?JpdJh>H{&l zegy&{<;fj6{~TWvsz7AMsvj_=gd&1f-y@fdoXzNqy1Jn#w&M<23XX;&L=MO$!O{)^ zRb9|Bl6C%VzNVCrvh_phy56se1v*4l)_6Ld;*Bppfgk-3ziaoxOHYS|@n2qved8~< zzwuwf{?*8D7C)DdCH#L;{=Y&0U#*(|L*r3$0G5Wi5&{)-MZgjTSU!OHg6{tti+?Xa zbNM=8)dD#1nGZFXDSY#L-inu=e>(l-|NOsqKlKlPPQUQlU1}wAQebUxXM_O`#m1Xf^odWLS*Bdm=u zly?{ZNGpM$Ns0HVk!P%$UeAb*!7_Un)q@IC-9+4Jk`h+q;^Kn)=xi^&*IWy0u= zQgCt~Ga;1;W3J7f2%4Rn5E!h~&;)>xOTsQq<|qPy6y%buiz&fH$pp{6^-28bfAC$? z)6YJ}zCY~;h7U6SONrk@gug>Zf8X$5Bm%S_`F~BvBjW%p#3JgXRD(^JNrC$)!19*c zhj-3>Jmy@>{w=DD$Fe$LA5nO~@dG!nUG9G3-}t`r;rG82|Lyfk#1p z^QdolnkP(~+vrx8O*bGLNexUO4JceMkLCU?qE}-sGeg84ss=M7Flu3c75lMXBL1Av z8UV3J%{3*^qII9M{R@$H8)zAE?fMm*oo=w*ZXv1Q@|6>)04WI=2~!9r73z6^Yjy;W znw&JLCJfYd9XNL25K+bv#f=Cv10tHoon1sG<3Xg!ft)#?&N0k6PG8rQGl(*TkE{ey z{al-JA;2+$yA{gWEmJJ4a<|1**Fr>N_PYp`iT*geUQ$ab(83T`0vsQm0ELbD-pE%@ z9PF%?9m)F!FhE7!Ihd;f>dj%WP9==CS6f7{=bVAr_*cgMoF-Tj{bb<;cr^)TxE&>1 z?!F2DserN@@T&7(Q3sAzMFTZ)A0}vw_Bq+Nf@!p$^&Tk&hIR!2R^H326MzGBbIZP) z1K_P@VKQ|ljD_>l@Sx#$u?{oN(fA`Pi8;@VXBIkdRZP+*dO9$YhTK^@?zi<2x!T*Pp|CX@-*VGAkWE=n= zy7#>9Qu`NjX`C;5?2qMV)z81^QPT@cC*+}SZDtPde(MYPn}7YUIxdwU0 zC-6S>vG;CaW;2+^tvJ@+b5NSQz(LQpJ(Wr8B0)!CMUTK8=PIBmIRffM8!VGAvS z0-UAqzhfc=(gwwndEA00l)+CW{~?TSWj>z4UC%{SX6Gy@idq{z8MtYkOmtANKF23a3}%TCF{= z*FbCgTWXt_7J=xlaR}6P92y~xoVNinGFoI<<$%;v1}iM5Tz7Bs?tYpLK6gVq8;nZ2 z%Z|)@#N`s!r0!@e#Dc7W%j~4JyOvyKuQk-E$3cGmixCcW5B^_p0~9qFSRe(ez*LWG z!3%(R6`Z#3GpE_2c&+Sz@wM!Yv*w$%ZX+HORA^h_NSaqxbSrSbhLlqm2byCPm5gq| zWWRTzE`QMe%QJ)yo#&L@Ib`c6GT*nZdgsEvQcL&T88a$@#YBvVo+FNiW1MYoV{{D= zaSad^LDX+6^KxjWQIMf%+eQiuaRp_QudeNd?VxNSG2Af2qOFogOlPlo6!ADX^d9Ya>taceoaP?% zfH@XD18O;TlK{26oD|FdQ7~kIoZ(H}p*9L?l@D(0tS1cl<_1(r(Pc|iUKstQv8TO% z?_UT7kaEVWryDQR3~^7D zlv1>)wOw_$9U+oMnL%1>VdzYfGEii`zs1`ySIE7VUz#&v;wy&2q&g`J&3~@~(RQdX zd0ciS3@fc-g0c8r8kJPdn8XeWf~F13n4?AZP%BG>?%bGHt_DyMC#a(UB0#ObCvpcR z%uzWo8~sM$^EK+dAGFLB0hI$&ok?p%RQ=?d*SCs+)>EOP0?qMIg(@{8YTm7r0!ROR z28G@se+A-dc3wCdLIAZs4b0v!$F2o!?*ii)YJ(*+lwMv40HvVg^BkIk3)LoP;SeY) zI%I}?rYKcOZe>8d2EM>5O~6(GRMZ`I*!|PT9;p~HaGW;A(k}^{>C}v~N8c@7KtSsf zS~*23zTsJ$Yq$o80zhEv`x&L8+jeAf?slbl>S$_v>4e#U=4@?Vzw z?YH(_M*e4ek9`E-A}634{{npNBmX)cDF?vEY#Glx0LwJMzOmm=3t;j4WvXELH~WiY z`F(YKNo9g{5D`6b^IH7T@BcQu|6OmUzx((9nf_1z%P(R|wLaUvN~Ctgvy!wtouMXx z&8QaSY2%Q+mkCwYH55=L5UG)}bisl)mN~~B3Id`z05WY1QufHJREB&za0W;`-wJy_ zkt?VHss)lqgb)D^b2L&E66&-m;1PR)cX2rtJ>9vn~3*!|Hg2)$x%>ck1UB8tTB&Ns0>7RA+{Qw2V7Qnt+o_h^?N! zVmG#>7Ez#NLkoOq&HH|hWh%^DiEGYKcY`X$Iy)LmT2YW@;;4TwIYjHwjm>2@K+$g`nFB{Qnm{VPy)1X`J3bpRWq zzZz#3`cadgHO&ljd^L~8aVgp7#Qsk52pu<8aS*yi=#0g8!Eg^Z$|@N&x*!l4pF^N7 z`eP0ec`In*V0|nq5>s0@I|`s>giaf~U;88b>JXs|5hPtyeLQ%0y3UadB0>lqh?5g{ z0YOzdtoOKA0H8DjD{78`MZ^ltqoGz}9D?K;AfO)Vgul9Zv3u5)J#Q!FsEebNqzjx{ zfl%M?LJY82LBS~thLMX6Jr^qFtk3G6LAX2c#WAn8{sazL7 zu6nVxp0igE0EFu|FX3PRv2UmMeemtOzHjDiU;Xv`1-KaYzaQg&Ki_|L;I%CdNB`%H zKhE2jujzQ?8~{fFmJxb&4eJB~=N*8HjPmle<>%;u*V|fOJg+~OPQU^$eE3shCcOFi zXY@b+iNC0S=?6YcKmE~P)c@fh{U5a5dA&Aw=K`8$FuX%q7*4#L$)W~v=vY-CPJ$s0 zL~P}hRvKo8JFZSJZEl$iE}4@d-pW(el;rKez_71=%P}5+>*%yauh6uGB zqT~XNmdUf;V!DdX?ATcDeAk3zK^pE@&iW6*`gp3Y)>~>t$GZ7ht=A; z{fU?o;FY5za2JtN!MGg_#%GIYkOkaDdvL9iFnFHpLaa)8Fo=|4sRS*fpNDtttEG1E zXddp!K%uDsT?C7^h+WDAhV^c5MugeQ&^(AnFxga6c4*)45l}OzQ zdD9{apoIZeR8n8P?W)gSBg47O(8~+eC%icv6}`|>fV}bd1}lX`2juMj%iLH9ARv$D zu7|p#vRHWSsikhx1J~;8%vR*f*P2RevL#X+qBm5aK)mKZyAxo)bC&>C6>oE#vz@-G z8U7foe@Lz=C>Ww-&O^lv?Y?Wr2qVYVv>mnUY4^2bTPPw(B|%_7 zU=Ei5C)Yj@puiBJ?AKaBZO&_YtnNFOn#@-6SP?}q-ud1a@E88dpWQwC!jtUM!-VtY z;SVzY`;dQy{r4gMYTWlD|1(@n{@btlKbQM|;DY%ojYrM_0Gbm3%oDZG1z1)F0Ov3C zzTrRgy#9N6F%CHc^|73%L*MyOBZfe4een(YpZ?X4$Pa(_2f{!6Xa8J(^6&o)-8!xH z;INx{+L%%421hVsE1@H2&x+@GiGWgeAdLt#_KKWK*lprRir6FN_P0)XDSN{3&Jp1bdwXRWS`>%000i1 zbs1A_(RelZb~*tFoAz^dU75Jj)?UzYcI8Jkq3h5dUy&CpUR)}^mGpyYc<6CXS}# zuQiIVXoyGDj#yODA*ebE!N#fJV5msrC3spc_%_haecnn4aM|I98?}O>RtOa@+fy|I zpJi%mOT09)@&lCEly9L{4Ui`{`W3S=F&<*;2T`D2S_v!tg*Ehn8*>$LoewmpyMO@K z00|+qza>vmkdhGnq}EB!znFFkp4m;Jh&0p}l!O>R(3w`>=mCo|m@B z&7FqoXjIsEfpjgUKI`8Q0aq^{6J`Zh{8$GP>YINhgNY zZJ~CvlcL_t2q;A`Z8nHQZ%#?Z*6o6F1o<;eUv;>%N3UjasxR{a9O4{;f&_n@> z0!hZ9P|n6faJ4$bXi<8epwzju#qAuefl@$)oU2ltZR`T}_O(R42jJcT&r=i(nVprA zppzLEdC1ac#$dQ@wEoR?g=(XE`23hy^EpZI!k;h9FhbHk~UZEaLzN!solO`|&p zie-g>p$fi!W>y%K8KnLl{-WB&P~t$E-3bCrk3s>k%N(nv23n5;GE~7dE&RK>0SK|l z7Q1JIdCS54ovJf$<3M1i_rBEru13T*=k^^Y+{Eqo;7YLdE_do}Z%f8*-1u7Y$`2w0 zs(ha6YPio*`36$^1$c>aGwhhxmhTZp$*Aec*{*V_NQ9rYni8j#_SVj*V#p52QZgZ+ zC~!8NT4^6g2$=&^_xUC8t3@ppM`{hr*iwj3zu__b&|my8z5j#n*z|o@%ltarzhdi< z;XefXA1d`*M*C*~PW`d(0NgM4|415t;1PBJfOAg3J`zyJ(x~qn`a{q6BmP4Mf9VJ; z5rO4<4tbhi^p$01e(veV@nawPv+^U~`=R{1pZHAwzx~p`)Sv(7zfQmYJAY`UE-8Ux z*ZOtaQK%bcL77HyH+X%%83wdw>k5t$#A_q^Ri-r6PFzk#%gGG76m&-?Ac{>2H|_2s z_G^UBo29ffwT<0+7IY>bOP)LSdTp6@kfXj$-;Jv$c`!i zh?`WAi?{t0jGSYTL-t8=M zWi$F!Sy9#GNtpxU(XmCy<7h_KtTL*4BljzWE~0GIN(Sw~Epjt8BH#E-{HMwiH)w$X zfFT?}ML}I}hRnrKERPZ_dab$l>h$CZpwPrz;QA9pa06E?AKtI5_8oBe-}kEupKsSU;fxh&2JyJo#Pix%r$-(;+y*<+cA>=es%y=T@yc3PcMd zc7ELZy8W9eAjTdc7%w7(24*+oN+^DM?ZH2(oHEhB}?CXhO{Y10~ zVpvo%h@mW97@$z3Y#JbefaI!<3j3Qtv1LML-dxzz~dzO!j^a#F` zbEM<-s(bfaU(i4G(sTM(|H60ge)YFL-hcGx|DW(nzx*-${%3A2PFm-Uv36%`5T~hO z!CB_!na@H&)?XllVh`~RjpG22fP8y+J~~Hezrx~+9=QnaympF9m)D40KxEV0A9oWp z3wE0gj*gG8S`C0G(p0cJ+o2R`X#fHc`VL{}l-b~R6IZfIjMxt#?NOXjZgt8QVTua! zxHAMLCv-<^GeX`W4PA%f>J5Z`z(lVA0NA@9!gIYk?NDWlxf{UU07*N8bCtba?haJ4 zM?yZ&IYT8`_rl>A;phZqdl!^OP`5I+xMUE+T17jYu5eOdzXEasudlcU09mellkFTi zTIrx>YdFQh<@GH(mfc-jOYZD7hk>l*PCc(Z!|99RhSZq$WQJVCjdXxcJIMAfDD*(r z+xGJCV11!20pf&PRtx~~8 zB#o{CA^^BJH^p=E3Tx*bIQlkXLOrESINROEdbo@jI}~r#Y{Ego(hFwFP`5g%<(eRn z&}`HzF&Cb?QcrWpoZq+QVMN%#5lr5>(W{h7nXo!q;p^Z3CVa>DzMtOt?iaT)#^9Iy zL2ca+1#|n{M}>$003ZNKL_t)Be_!Nh-}u*vf8PnX2>M^v{WtsJDT+sOJn{|zaLx$; z=KB1L#=fCG1nuubLiM#;2D0x2ELogA=2pZ@XW);82GqgKbh18LecmE-Naj7di ztkiEfvS`x@aCv*?nG!;6xinI%J}4Rky;&PO*`E_s(N!WYZos}>U!A~FJCE#Ip|Q4W^YP$vgU23+3) zAp+bvXQAfl9T^aDM}obqut0;f>)Fx;OR^;9suTdt@uCo*C8V|N8#6NvRVSiNMw*3<^bo zP?m(+D!6FYEJg^NQyAVoR4v&n91tAQm8<6_@Dkmc4ogJrUJpba8_@N*9 z4*KQ~y=z*p2O^?n`*+N%;+}CR$g6w%vc|tg{}&nmr2}vg^k13(2aW&11@H)s*M$S% zV;TKFDB7=&<-gB~Ff2ar&_b!+;-S2ez?=ZV?1e7764qn;W8*!6nXX^G)P3`N-lpI5 z?zbW5EU(_VoBr@qFRwoKTfdKg`I~=$Kl<#;_~d85fZKOwiUfDV;s_jIQvehYR+r{R zJ`T0kx^H$zLM53Y3!Nbddqc7rk83v`$FomAhG(9*j(5N91$y^epT~=Dd}6$EvW_8! zTIv7AG5fxl`3c4xpSB~qRm7b;XOJSeaqTj=oBJV@0!R@M3M3b-)&rVIk4wqqlt3GY z1e}RZP;77CLF~Jxh^EXaDO)!-f)m(mlx(eO%aqLV7zW5|uY!YDONcb0BDq6V;a$K! zA9*q*IB$DOFc;Cqb+8*I7PS)zc7ed_f>ds1c)txIO#4zhqcYEFCtH76c%(d&!`NyHLzvmx{{FnLug!5uPOW3~}`_sLDA2R+IO8q`6_5K%)*Np?P zC=Xnc0S`s`haL}t{`bp4>ht}4(em{UGZGMChHi-lJZQ0gk>?L|UF0WjTpOOaaZSJR zT`!>&(X;I?zx?WL{p=TB8$S8jSMc#qeh$C?N1w$jx9;GzI~&}(eTH!|$<1jJgsxwF zC>A>sA&(o>QmhyQV(c(<0XMH-#g)s)xOwd|-tfc?yzs^+=-H6fiNSBDi#NgxIaIJKI9V&;lk^ zb|IY2t-FXZ;ONpZx-J@l5V7T_Z6$xw4Fw za^Pt7_uxv{V((y9Q593L6DU{(3a_)5f->$vq2}jd4vYvSFaibCj(p8wlVZ98A|UM` zp5UP5l8+FvS#EsnM%P4#GLv_M9S}wUd0)~KSSYIKDy^%1Bm-`W* z%+S08CQD<~)C_rCGMDG*Pqwdw{H6a;i}093pI7(mMBM1SBl{p^m&w|v{X@#lZ|+vw|FdV_Rb zT&7kZc0avnU*9+W)o>r`=yy@A|GwPsQZwKlwZBK*_yeya2LL$d1OT(Qx4f4iV#SM8C3Vj+t@w>Lqgk6c|S{ibYn?(Q^7K3bg?RL75Ty z4x3wdF|1dh)nM&XMZij+U`9Fskd!UA8Ryoo3IL)I_PS*Nqzy|MU4|0x1O3i?VZUA)yxIePrjc3qOET`?(hl029 z9m)F`cpbY4xL+kWy}^P!$PlMT=k-y8$yo zw*pOKEv^aH6%as%Vk!xyO2I^AF}S!R!?8y{oM5}X1$GCF$Ygyh(_iuk0((iHrL~UM z*RkE*X$Y9W(^qvXRcl#1S0O2im|ztnnkI_`693D}SR%j?1T1Ftg& z062&Q>@NyuB%RB@ETR47^L+zfy~O>6Q*hqXO8|z&&x{!ns3j@ON`ZZ*>X+RaSPmw_ z(Q4qM)i6AUD?@$#B8KvzK|})%8~Cs2u|M=6!tv!RiqH|WLUOUlhXKh3*>;_Y3XS6g zh~m=aHOB2uRTTnjJqiW7qXEo}OOId2@#RZ6ef1XdW@`j1H^M?>X@XI~myX-UU1fk7 zgpxDH(>rbqVJd@QP_%S|phk@*zs#^GG`K`7wDvUjWUZRyE0b%Zi-Oqdx+w&&@CYr} zES0yw0i>?sP-NB|sL*zZKwxky(-Vt&LxN5)$3RV}N5z`(P&Jn13WuHpu92H+w&Saz zyKpn$r0P5Yn0#;ypc0^|m_rbH%f|=rZ_y2)ZUq_7Omv3;f~5wGcc3%#-3@ zZ@(j=rjn>RYa_XMl#C*V5`<{y=_rWqPAwf!3TQY2^=oLA19rm&*S|oB?zlY6k|p%L z5oWh>#5hf!BX5*H=b9u~0i?P%K?kK`DrjWomf~fUDO>eP41?oHbz4+`2t@_Ev;lWL z0@pqnl|$e%-;P2^5u9ybL#`Siz`YtE1Za%DzNCJfk&nTsl?s6SoC^o5tp}KMdvbZe zhraE7_`dJ^R(j!0Zzz2q_xH$$-7gQ3{c6sCzp2l@V9&mqUp4y6$I?ky=KS{=|Ml^@ z8LvAB;J~OC{?O~o=lhRCuOCVkEWOJ81y+9!IPX|gf?%x@*iQ}A@l`dZuNSZMVFwZE z-OZMIw>7Wax{GU<*XWoqrV%HXJin^|Gbxx+ij^`lX?q?*M1QGAni5h<2BSuk$yGJs zl^_D{dfPljfj)w}4l<6WNJxypf(%7uMi>SUavWN(S^@w9JiqQ71|c#*l)7=&Gy}** zT?`&(%tS|qf>vwedq{z#9c0`=rx7yk%n+CC-x&Ui zJL3hU(Tpn9BZ5Wu0H9`I%z3HCK;2-MIcW6W1U8vF&@RKoryL~+?1DsuCVgC^>^g75 z3e>M5o7;2MfjWd<1hhNz_QwFAuB0uXX|zffCh)LU&50;<;Qk0@dk0mTSY36HR3S2< z$}^(~%J$U0Q(+th;->yb1gjwUB1Q;|lj9?-hFEjz%U$xC_wyLSuZjID5AK~Q`G>O$_T03scV4L zO{=12wq_SwNgziwN?o-8ntSH|w=FOP!aLsiB0l(`chNU};GOyA8rGnGWy*)+ z#v0{_)pr{l-a9!iJL$0M7}y4H>F$=np8Eqd=-%dwT6vwK5;7}`U&pOEGBqmaU|a)l zYdmZ3TaCMT878U=rS0h%wOhYnO55@`*h4*-i97}^=$$HB)!2aSn zlslUCo>!wk!kmEW4D^e~K1r}T0}s*S!PFMTX5!AV&8#-NwC`HSnTt>)G2U)uo3@w7!X%S$eUA0$q>(RGZ~}2))fehT!>}vlmLQl z$R&>!!E+T~GGokQqB2^FwYDz=;^gE-SXrP;_9_ZcoQ&u2K8ZkNwEB4`&DBOwL=-69D)Ef_`kU3g&mBg8ni<5 zc7kTS8LdLW!dH~-T~i&*2|}dd=!8@Rx^<>M`{&iXeC18Jf~GkslkM_;+%%jtdF?NC zgus01(i$(FJgYCh_)Piy=U?OB_^sdLU;mBY#o1=pzRT8lRH_zh zGDO}FLb8>J#vUb408A@DNnzbQ1M)%xq^OmYYF9h;xu&FRfQ+M|B18eJ;ZpmqS|Ok! zcUViNkI1nO5q*Op1p^s)dYun!`Wo+(lv0C+5t$K^5 zAYcJvA3=eUb`xSBt!$4>vbYqBeyw$?c*7tw4hoB4MFb_gP)`?;Q^M}{?fF2RrB0#{ zR!Au*(_|`y%>d5gIkn-CQNnmJOVW%}l{B{Z%*}+XYo68pgRMM^x)q=TnYM-q3`d}@ zH@%27H3Ny9A=^_^9RvVzIbf?k10+Lsr;Zx9d~_xLDFAj^WXT@cD^MmttcSBz~JpGtH{nSnUz&E|8eEKsl$KU?l zPvE!z)gR(_fA3TH-F^SD;A3IG$p~mm3%Z;#M`% z5GKE#A#^xeUj=i(ECZ~b@3#g&vGeYTWND43pSp<`o_`8&ecSW&b#Hzno`2z~>Du*6 zF@~V9J@St`_EFp=aeKK}Uj+FtS^m`lxJciBN%*UuwO{gA+5U%8(65W}C&B>$uwNmt zV8XNr`;g&3lm^%*kSx8*{XWqv%!a={9~M6cnBnhmFCwti5V)vn;Psp)xL<_-py>ah z(18#;qR=7bf)E2bMC6>B9T2f@WvYsl3n&J}7%fs)YAR~P+@eaQ`SnoQ*-mb=diG#H z+1UCZHZFh&Bxi8n10u*dBZO+a6~zrkAVMBTl+hwP<0GI+4YN_h-I$TV-%QYCEOhoV zLf^p+if(o4FftZL(69!F2uaCuwhCy;)(?jRn4%rCYs1@N5>$4!=xYA~ml&?)c{*+& z`Sx5D(1?d@hgFZ=5evH|3kU(V#kI*35*nnUKrI_=ihIWFYoDQcY=*x5zZ!n=+uftx zh93C$P&ZgB-`yE3L2k~7L;qeBIP)0lMWN6`$E~#~b~M943^j*Fr0NaaZ<&5cgNSgv zUWGTm>5ckzFTN4q{q5hRcTP{!r$6<|@EgDV2l%z$_Tw=uD3S^9E!-;)pH|xN-e4JpJ@zc;fME zc5~|5Dd~U*Eqz4sigMf3wW> zFE#!T<^5lm<4=qO0NmpQv~`O)_YNF(0QRAzeKcUXz?LY$0P}2@4!{bwX!f0eLx@1Y zj0zB5f6l<6=>5>&>|cweQ+7YZh*Ie9@P*af@wE3xc`a4!#)N&W!x~Taj~lzYEB@IUg>sPPnJKymlKJtTqS~uHWe&yBM`tr-K z4WIe!EBK>7`W!y-N1w&3uie4jI~&}-bB1Y3C{+U_&Sb^vq}AwtT4;cDF(7swx)^Zd zhG~FYyLK5*K6wMrzVQiq=9$NE^XAp@#?32OAFVoX#S7S8_vIc-qhFt2l<%)by2ADs z)%Vxuy92N76aAJ7fA{44U+=`~KT!?U`pCfRQGqB_gU~x`gDTm-;jr*bJbl`XYAA9fqY|D0)hkawr*}c}oE3PC25_((#61K!kk^vjr5IeRB$iz69l%2%?1%E@Tl1im;Dt5V&GEN9~%9z*~ z454sL{HTPoDL}X&2Jx_v&OPVsy;k>}bBz2j#_TnBpI&S4BkogLv5P~89IYuL#f%Wfegu@|# zU13fDmto!0u#%{0Gm-Ur$*T@jGSo9+VuGmYx|@UTtatCd^Q!14Qh@Cx6atbmQ8`bJ ziX-YEx{|B+sen`5T@_aoD3LP9k{mzhzvBR62L(xPg6ybeE8j* zwfE_#tew~qI{QOh2~*%RIrubsdEcu6b;5vcG|vu}00f2%$*=>^I+tc`fN5d?{{8#& zN*!!S(MgN4IJGiafF#8xCYTWK+@BwouZe}gbm!u%yK`}dceNZKq8P?euC8|J@@kiJ z&N>XKd-(8jU7Q1C95Z&i5tUAQ)pzLoh_lVwCKT?T59?Jo#2EO_o%8N&vx@y|&jGTO z_19~3dtGkF`{tSdyyQQ3_5-dv{nqgxlJ9S|zUTq~phmS`AgpW^_QTatllkqY ziGZbf(l!8CY5~lHfHnX)76`Phfi?(e@dCj0$^d4Mf$I+UX>Iu_va z7M=e5```M)iwL1Z9ul_0h@1=hF5+stLrNLX-@A)`)#Lv2_ps^%BxQ`FIjPO22df$* zTwMVHDq3#bjm9qkBXS8{gcdbolcFd>dzD5X$s*DHTs~0M)NKOJs}k(YYbFh>6WNa? zo|es%PXRa|pyLRfPe1_*9m4tyGVGktg%J9+v9qO}Q3RJXfs%;Wl-UF@)jR=WMCgnu z4#Sm|*&T?e*(U=$LnM;@NH@rYg6g(A(2uFo(b}nG`;JgY`n(uz+6C)#6Ym+O^m^}u z>tsWnE~vpl)tRqxM-6;@5@2E;R6vlS+baMv{p}=COf+%O*YBII5?ZwEE(xj#XLqLw z0bZT?zVV%)87ho${>%-B=xb}tdk&E2?!PHHKDg09XnF+KGXAMPLau*j57$-O$`0 zbEG#p_WAQ;_s8B_`u?T$o^KZpe0EyD_2vwl^E()JTbyk+kW!FR!R2Pp_Ch!Gqj$~f3a9}HowlZvBG>xMY9J4JUK~q%z>f*bqXj%Dwn)W zV6*&+g!Jh1;QrFI9@cjtd1ohpZy5PTn%Z7;jD-D+q2p53XoEecb8CNr5H{44s1O8zJK-yBZ0aIUSHfMP<>e=C6Ny$nhUS`@b3X<*n?CJ^-k8U}Ug${`IwE0KvBJR0Du{6j~j98xZt( zvK=rF2CDOK!GiP2fi@60BtX)GZUY@)O0^T6O(i=aL}6adU|0rQr?QfnY!IT)B{&xf4oJTSOz zX20a<=liC89nJGY&c7Xl471O*tY9~uZTH{uR;W5|t22kZS@#f8>;~iH=RgR2hV^C* zh=N1F)#VnezPA8DEJKto+7B#>;z<6k@1Q9`iW=VvS(`~<&LwDPV}t%56Pp4bfH)z< zomt&S26+HNB?f{4524<3W=8OgyK=&V;S~aZy%;qt69F6$F7AP2hy3{S&~dOJpj$zK z$-!yfL9zW;(uwoH&Q z{#Cl(cAT$$ps<#NlQr!DcIvOI1vOOT1Y2K3f@VK-hz+?<*}#RW*A6bNEPq~UDC1z& z1rHX8_Ph!J6f$WNUOO?myKe!_Qw~Q^9Q~J1AuDI9Z((Ay!R~FJEcPhWp&PT=igQZmYn>dV4xirTnYx-w!k4i zfu+nExW0{Zn`i5-qdyi5&0n9t)-JofNVZO&XWKKk8VDRa>-3%Pd7u3ApZh7J9{#!j z03ZNKL_t&@hAl3y2Apqt%eGmtIbWlsVrXbBVD7E1Sj0vFeL}!Md_6+f8NZB|mU9*m z+n5!5(TKq)3NQsn_OrPLA}jCrJ<`L+(2^0l4x#T{ST|6Z8qRt{Z3W%y-Tj9 z0Y$_>e?-ROHw6K*}TYt;b-D0Y)7pY<-RKW>Es0#lhB zJ%}KAfKd@wyu*G>(Q6%6y7yUhs~Q+r0fo*m6H@HQtf$;W3Z&EaC@_uJX&#|UR>CI& z$CdFii~|gj8DJ>=1kGh1EO0;-)}acBtf0vdq2qu&Y>n1AnCx-Y7pNxiAf}Fq?g z=MQ`d2LQkm)qr{ewm`wQ6jg(QHv5lw5*N_nNswUc*xMlB5Ge3i5U>OmY#+DT^`U-x z+Xgrk0Nmv4=cT$b`@3m3`+dDqwiS#H`;)9|{=TgM+<0Mp_j}$?yjp|P2xiL^t*>qw zUhE<$5TJx1CDS+u(5*TX@$uenI}Jce&g^IEZS3XjK*8A9iWuE4SIQnE%gLpnY=!MnsMUg zCw4LQH0{NjoUBQP|ee%87i)e~Ypg7HtmZ=~yHX`uksg?691D^oi$7LBX-A z*PF@v9IJF3N-z#pRa-~BG}phB@z4LRb^3E5pE=O)3FrSN!B2ca_9YzvRP$;8usi%IkoXGtzDcCbHS;Rgcg` zl&t~pAcV@kMoJJA>@FW8I7p6Mf8NIEY!9diFb*c@5~G3tN`c7EiF5#{XY_;#V<1I= zl58TtGi%GpgE@MwJOj1^DXKZnxB}4*l1AU-jLPC(c?{}!UmXWzr-G5{NSjPwN(OW1 z{04TYREb&@1;-xfdSD!UTu&jxc7L!=ScH9+E_K49^7{)QI(TcsnHx13u~D}MC^*5B z$s*^fj*Oum6klvvze`$>GkmFw}^&cwvf6+RB;1mE%HUkMB8X2sw)oMU>Xl+HH1_0H; zwZuOyBjA$LpWph~XXldwPtz7?aREygn)bv5mYx2gKxkeKmWuFKJMg!F8d;w*4Zkav0y%eh&XesC`^4?4L&J6>Q zTJN5tk>)6Tmfm(FhX}+lImSE|NXgh-FuE=xtk;l~Evv5mc!*W7WnWQXB4aZ2tbPAo z06Ys9F-V)Jf;f7n?8HEX>1S6876jyE>nElMASfUXLxZ(tQ8?wK;G9A;;gA`VW=- zzo?x*a0&pPuqF^_0c>pm&^o^A{FitG+Dbs{pj)TDISqYH$f zR_YJIosufHLxSXti}Q_vJO~pwO#s9gJVORj5kw~Ev4<@~)B=nm=#&up0FD71U60;- z-%^tG+yU^W2WOL!O}i12%)u!LjN9=o}4o=DUlx3}zG<|hg)9)Ad zXJdnrGD)RFDJel3Haf+DAV`i9q!sDdMhGGj(n>2zml8?~QWBzc4JD)Jo(^E}Tv=RWtj@45+{s94aJ8c=hWH3l$XB3bB8M-QH#eE@4)ue5`0r`v_8G(o9w z9m0r|S+5~jqiDf@Jm}N}9jA5kRR1k)qEzB_BQ^@1g36Hd9oJ}~Xw*5(|LE$<+g92B4^e@qRGpHGWQlz)egbbD~4RKv?1 zIIB1MSL)M*174_{m{aG`g44V~zaRY#czzr4cm;$E=5_PIi?xcL*AKRawGiHZ&EE7M z^;LYc6mmEof;C&0a_2&5z~IG>k=zgTjy2!AzX8M%O=@_}vh%s_18iJeJfEJ?Gi4mT zQ4L^)#2LLVusk(?TUznr{?yZf%P+>4S796(PT(G+&vCUea&3c>RF0W-s+7Xl49n#* zO1Ks2&}yB*U#vMoD=!IF5Y6OXaOpqtqS6i!@SfX5TjlG1WUV}nke4}Zfxd^ElAdmm zm@Dz(;zYq<@Jl986rG|6Dg^!8C37+SaLE!9DOnLhI2HwfB1i2KN{fROtZ&Eal-e`|l5{LqDB#(t$h{$QQI&8*Auwo&8xhH}_tx%j*+r?MJ% zt-)-szX_8yjFVAgrdci`JV{R#@7u#mvVCP?;Q)X7@`zXPOQbky!t9hje9ZCHGuMd4 zh?|=J8jr;Lm}C&>I&1!V$d(G5A2+0PfJ)GQ!5O?jU1Sb`bv7iZ`(?_j*y02LX5K1A zF*1s82LO!zDoS#tybx$+%7Cpooq!m5zaI8^lLbPK-9(-UB*)yzEw$-$CL3RJe5mw> z_5D_W?C|Ou&C$dQAqZg)6)9qj22mB zCDwQ_<*@S#| zjL>>+>9MHO<^HcQHv0Y_1koxlvCS69P9t+nsF3`n>qlUoe4iUFbl-*waJ9rbC%7@8 zdmnQ7%-Q|%3F*L(Pd6Qhx!lgHRmZhmLoGqjkCl4523f= zIbFS!B8;!^J{0YeDE|8b{2Cgo#35L+$aDd#Dxl$mk&p3Jmpvql$ZUgX4~U?!KQ$#7 z_?=^%*WOpP)SEGY-Ot*yY{5(J<>0nya;?2CkOPeVJxe&O2lnr|9j`^YaZUnt39POCIfkLjwjbVAiqkN zzP|GCTq!xXRhHsn0FOwKpJ9aD@|bSId?7WL9KN)`)NuFMX}uG%BCr)|I!5q&7rLpf zl7{)?Onee@Fu#x5E~ZFVG_~(UhY(6DB1*6#iaF$+gIY&VjoxR$Gpg1&l*q9$KfN9l zm4xloG@kL}+B{2J)O)WPL)gP>;b;s?Od>;7;|z&)aP4=l8SZ66urn}sUC^p>Vo~hd z?NxfkouJLhP;d#jz#M7(3riO{54sYLgY0`dVO4s6ZaOH!^m2f(#djgTbGfj)#A)r7 zi;ptS93R{{4D+N0{zkhLp4_%zvjYoE!P^GE6>Y?G+4u@#Un|7mcj0u>(so>44+FT> z3-d;n8g;8-w{g1=je zbK0FDY|&48PKUC8-$}Zr6i8PakfV;o7rKJHPn0Ux$5m|ffOz=}2TE18nHjBzzP;^3 zy1{A4&~8}B6d`EG;;?Y-0(T$SiJH2=J`N7>08qdA-!$7~PvtWN_T~@WJ2(ygypvhD z_b0#Q&#xm6;oX6I?@#x?_P?2}tU}1IpT~uo=C#;nGE67eZgu;8d3jVa8@ z_A7~AN-a&{*nA6mIg5mu$D!&p^mgJ4Yz<|O(9cNpNVD_$9rvxdE;7;Ibp~ck^x;Xg z51#?CkgXv1tt^M*fM_0T&|=$dmJv9KzPKQVZ3jkgfPn~+g*f3RHta2X0>AD9Uu-xN z&`3rD7p6)Yeg~1X&$3SiOrO}0W%nHyI&$9_>Z+z_U2;!`S$Brz80Wcu?xe~%YJV0P zKn$_))+!XMi)%%8x9`G32IS2`LCK4?G;ihPxbJ`LCtA#xcfEI+fq-MY*Z{CC8B7=* zgMq%!NK@O@C4+M<$8i){;?9`_Uoq3m&#B$7>FbitMB|&owmce6_WVuzw$wvgz!@hv3`}>Vkak9zsOQi7)|@vZjTt-UJLPy-brPD2evk(-IOFd> ztwosH-V>3Ovb<&sWu|uobzq{-z$ua4D;)@a{+OR8}V2F-;s@%yF#M@6vS$SI+7$&z}aq15=YrddNbg5c3R zuYz5;o|w&Z7ApT+)trflnubLxi0QCzgjQOO6i2=ez5Z@pc@;p$Reix;m9VklO?Z%< zhsKM4pO!2+YT)-G2&X@DqT8!Nt!Y#e*s0QXy^gp*E))w9FwdN#l1}h(d_1re@ozyW&48_Dxo!g!% z%ryFQwGz);Wnb-5!jj0u{1wYoQHV7M?FsiFn9yKAR9WnVuX%1c?Zb+=`5PGDXRU$H zkSW}n@WvRVw;1O8=;+{s*JMW-VbGL;z)~)4h)Jm4lUN%{$IL3PqOYroulWdeHMM0` zR|9;HQ2rx+J#e@Vw|DZ- zvGXr%Bg&LX3qZXe$>gwqsrsfLKWe41tb&1IYcpU*Ub&-0r71>``=094z1a8#wl?6J z+q)>83@!3hvTtW#ZZ+M7k(Mf>yC7-4L<$y*Y4N&>!inP;5$O`}P}7227VsyJl)B6L z-1X=5l{bWu_YwG6Txk~FnIy<%MjGY z^^4hr948+UD>Xk=v`kw=XLj>erhVjy>H!`isK5i5rXl<(g0=%IxPJ@1_0x zy8n9VuL2@?JM7|3dJEl^uuEr&_{V*JnIc=(MHyH`%0F7szsp-!cs5H_0L8IN$~ESI z*}p+pP0UC0Ucw)<8_j^CTcSTGrA}w|i&86{M~Ay2*mjZ-mNNd3nCf4tuiL}ejmqf& z8;(5P%IM7UmAh22?M1I@H|~%z7JXCSXxD;EKD58WJaQv(w&Nb&^2eM)sO)Jo;ePV^ zzmF)Wj?$V(mY;60g0-pJm62^7vgg{lQip1`EKqk&E{Pcc{q%dO6ve)&w{7aR=cdep z*spXmmADjcdHJM7Q&>Q)7O{U{u!A{}HBK_ICtUc7MM^}kj;-~ijGu_xX+u$B3|w?J zbhq@$ZW@blo`3K1N{=z{Gp)`a@GF^Wk@0uk+*RoYH^b$SQ*o_xA14>uyvp>3w_ zHM9^RyMjFq=B$T$b)6HAC>|<77s1gi>#^#KG9@b-|p%=5e8ocY0L08w`DbF zeA-eV?}k0l^m~M)a~F*?;zr4pit)1`slE<&F@d;O!1kF9uo!F`&Dt3y^d8n{uVYNr zju7_90cxT-_<;$@a~4*DxE^uNiIU9YX}bE$d+8nwBN{`v-ejTful2%RL@cd6Ia#(hJ`bGum5)g-&d@wRO%`YIV~@L8%+UX##_;d_gw_#oUgkFVeI% zj4*JWejP2^mmy(e9ezG%a{aru0@HKqr110Y5X6%h{dbsh(NyBfpqHWBGF z!wftJW9Rl5LsSBvSNwu2hO#fJcbnNh2;_L5{r1bQxm@t00&I9nH*rx=Au1pd1u#&i2ZDR71snGRIiow3pr7ACd-7^)F!aaNZ!W-H z0W7~#%!{=!MTIk}PI=r_t;(UIRRMa^{*=nQxMc%7Wv%2;04X#H%YAvtI`Ipd_6+e@ zqWGrPTL_AmUbBc~yNY&=*>R<~=2K9_>nVeR?p4)Bl-N!NN${<>htRFlI&1Z>T6 zbwWbq-1Rdt>lYT9jk=$5Vk}0#2628i#h!f^WzHiXsT3e(13bp?lcN8D&-~n7WrBJk zA#e=g@w;~yKS+9pMBHWPA zW2fREOi7z@GY~oHKUnWo^J(B3X7L?fE%&s#_1&~IuY?1;)Pqu{uVTn77lD`IV0yJS ziMW`~)fc-n1ZVe=EJ`znt}6Y8M6Yk`eWm*}6BsP5Aboik(^ zS>)%wD^DbXm&Iiy9`21r^GyiTrSkXqvd2u8+!Fpx%f0I26cRst6vt?pkfQpTOig1Z z3xuX7#}=3H6e9IhlSH{?n7D-p6x~q>n6$wN`IAcJ zF-wJM5#FzzzOK`COo`6;;YSBvjC6@N=JBL?>(irs*LJ0hq8~P{n*CCjfmff_SWXB5 z0kj)HLYq=@b1x3Re40N_C%WJD?9n6rRO~6CX#{4~iW&$G>wXlp{=J}(!p7p#m+xD5 z%M!x^;`|gdMTo|qTC;579)q#BO3gI+)wg{3-3MhJC|rSSAp5SV3$NO1Y%zW@zd2jp zpShULogS&8bug?Ggj9Id^u33*p2W5~yJXjBYhd%Podr=nlQfrC-)uBtG=dOH$i8Dg z55QVv@5%&RIikkm)-!11R*~Q6vmPgwxr-Mf#l?*}J8^=L&I#Q4oIC+q0_BBpFvN=d zdsu}cja8Z0HW?~iQK<|^uDEw#4$M#XmGjlMZJ5@O+q#?Xm(WLVB6ltc{PHwFRZVNG zTxw;wP)1Br`RmPKBMG7Ru+MRI%9j)tB)tj7czN#dc(ZvIkowdXx3HgxeJZnQ$}INq zlUF;ncqpIIXqbxozQGp`LaB89_{oN8Ejf_hFc@Su6fyR%eLKA^?0@%yhkl+1HUIik z>%i)qK)W8RIrj+}Nn z$OE4cLFoI)iP1Jy_&8gqiba&2EbI4jVLvm=l)dwx#wb6s&y2}Dk4ouKt>Ny}^nBbj zqR@v>EB8>5an)r>x+`W!6yHvnRUk}kuYnUH;q|1cj6+{ zXnM84{G9>Q6qm4UJh3KR`AwLoekMt%Dr}C32TcV2Yi%AN!;W2hpeVg{9`poz3kvKL zs-Cq%1eHd9+dex33>`u-u>VE;T`k4dE!GT0l=RR_;n}Hf$cAIU4YfseRphzFG_kU1 zwr@>V{N^cW5#fRl$;vFRG`lip{yc#ARMbpo@=>t8ewmp_8nU|dgf_G! zHQhk%I3m4&2X_aZ?H@MOg$Z;Mqw7iiF?sdF(-K-5&!;=G*$|($A+9mz^lJKk`ZV#Y z(T^h1EkLzbQHbC6l+Wrs?oAL@a@JKYXt_G>5l8OvYUOGQkb@&$(4LRM!SD>SucK5# zPASG%v?=ZzWFWk!YxrK10DZWZ$+Ir`D1*t0fqJ%=c=OxFecU~rLSFFKJQu~(*ph_W zN#lpFHll&VbwE%`uu8Eu{PP{=W-3_!Ytc5CyE~R1OIhF^>hr`9+b{DtakD{$Dp4@c z3CuS&8}=awqT-C#_;|>|z3`hNO4*|IQcgoF?0uC-#z^Edsvg1XJ9NBbemln>J7U)O zpA(*^`m`TKZ#G(tIm>VtJOM_w!Gc=UkjR_#g+Zlc=)84$Idnr)XX0&RW|UsQN^lEj z=}YIYeBD#VkD4GMitpWQKV6;$(Sn8P(1X!N{+lyN;!XrvWs0==b#uV`7R>w`#jRRu z5;PuIYu6(7`z*VUP#KpE#>zG{gN?R^hr}VQl_Lu5A}_@TphN9g<%)XfvT?gds^b&3 zjEt701^i9on(ZYoBIM-uyzmnP8aj}=ZeD*PD|aVr*>D1wkFKQ8?;#Ve$pe{|#AX(c z)ELh86)fgvS?4K*vBo~pi5b2F)2n+b$7oCi9=}?5+@cRs<>{QKqB_{7$$qs~sG4Mq zQwnhhREK&ttGPGH%LWI3mXA*;hb%snRW(+-rrjA96*kINuR6~2(Q$D2JC-~A*7pO3 zEF$*8SBQ?AW;nEFD-)tlk#pN88^GO>1ND%>L-$-5as1l@irfhy7V0986ZUr-Q?8mY z&6HPtZK!(CTh-Ap{pP+n-P(S2Pw^4};6RjgVr8J`sRVWD_?Ut!k8Oo#&O`IxuY{Vc z%I)3@E)(Zi(J%9hE&)X8tIETfto&mSGe~9p$7T@4AcvKIlTUQWEqXa-R(7)8UiZ^Y z1*_nYrE(SJC6m3#H4b`t9Ua{=htCaQ&zODe6VO!GhH&YTX0z0Mw7 zBx5d;i{g9wx{|C< zklQ&%))chNgk`2--Cg{?05D_!G2~vvAFQ`t{@cP6m%Xq(L4z{a=;^KE)3R!0yxX#0 z`XnFGHYMC{a<*ECS0FC}>KfrY201U_lX66PbBu*jL|s1@s%{U_4vHwu+b;W>_%e0h z0Z8fT`qc&R@T4IBv+=h-!<8>W^|{{tT~`H%##x^DYutGG0%RPF9g8M}aF7>KeLbAA z0H!%is4@(=wKWrc>Q>s%k9d%uxUH>mjo8u2FA0am+bd`Kg3>prKR2;>wM>gJt2EwY zN9Ic{s}P}~u=c~N^BVd#QAL?ey=3+iHg@cFV?S6jYeI_~z~4vWQMFMC5%vBt9<6Ei zqLj5I+P&8_Z|MXbd2D|(BMje*w?%JbRL=2rU(%j!q@PGw&>*e-2rd71x}E|Ot*rxi zj_H! zr6zVHT=+{g2mTJOzm(#8`q_PQ_zzgb*DR9gDT|dAfs`--nvi$fWPXZ2u9vskYWl$n zfl`-uL5+6KnXSV7lrUE{WpMIUW;cX}3^M|nSVK~VmUklgIdsCTHR`vLy5o!?j1f*{ zLTs6d_llX!xp{#Ss~jWhy8 z%r-e)`%SX7t^hyXei%>2KBI_`_Hwbi*mq%wLm#POEYYR^C|{ z`S$!)UblCpIWNjR*b}&Oq!9_&c>OGYU_SfhcwRD@i*6~rfJo#$SNE4SZ>ADdjR&DUFf6M$6^)L zs50=o*pliz`}5fo*sUHNc@k&rTs-8)+@UYm4;kL4Kj~60S6I`>CWk&<`faspUJms> zbp^1?VgE7^&p6qssB*gEA8k-WJG{>auQ2zU7q$yi=WnBKc<2j_H73A?p7Hc{)Ok+p{VeQlH$2&t$0W%rQd=)GE$OLRP)6C^wy{U6 zgF_x9_b__nYJ#w#84X<`uV80lR+3((mP;zh|D(P(Hr(VJ7u*rfXiEZb;O`t|gtRsF zu7UH`dFO}&nAo$!$-8Z>K_Nrh({#{!eUFM8yKe)h-R2MozbyRnq4>!D8g0VL@$ZHE z&xu{N_W|!3v+r31?_zDap|oYX);==-Fk7!?d`YMG5|Wg=0ALG`iElZzM5rQm8Hh*+ zFG5)2t5jd>48Viu*@S03H~rw?5PO?*pa-scEVY0&R==0|t&f?Y4zLg7&^!fKD7qG4II*R?cQ{`2YgBii^L7 z`q8H^^HE=7C$}hRA(84}mr0rh31O$*5Y))4&^9l(gXn;Z?&?SYNs%cv9zO6=x|8#{ zYT0GAZiI^{p+o*GN;CS*LlW10Pr4d>-1(NL(|k?cP^PR9-$vFREPW^Vj#;(>L@F>*wquRx8L0WEQ8vnY)e7N(W|6U_onk)3DwT77eV84)b@OI zClh}u`gY~G<%_Ye%JZs(c*lhHn!Xh6MdPZYvaey*!caIeEj-0I3dWnzC54ciAJkjz z8WE^?Z+73Hl-b;pSHe5YkDTedU!&%*JD6&n3C!R(JzIOtpO;n62uouo!*BlF(mg%8 z4aULe^j&T%Pt#qgdOguPmW8|O>esgUhOXsFrn3R^g(apvKZ@Q-WKx9>@~#Ljy#D^g z6p4xFpmv0H1@9DJ*S!K)i!rQsYkYmZyRr>(B;ugfOF8?M5B$ZD20if!^jwWft;bFQMA@)zoqmF7{CZ(OhH^yV3OH@G zV&$*A$0TMBkz3g<={6K)_U)Ru*uvr}zUjX}mQ1QK&Ht2}Z=$U-fxA=#qT+2fA}h=f z4e`f+8e7*9uek_~h`xkM&bRy(>3Ot8h5$)$t(Bs@ooVr@Dl@1v=y(yRNAB#^)LwCK zq7}5~bNXALs7uwwlTYpbm5iq9OmF;l1i%PBGi~!@$J+DxubU<~Z`sTrBhKZO!kVC^ zye4bbtY2OQ$cRZ$kqP!cl;Xki2nv2dYP3lfC{AjflTs9!=szk|6S4z&HG)%R`rMLV`_#=WiNt%u$ z4D@=Y6_sB(JioGQ^_dBqcDRjNCdN+me#zZn1hPm_%?B#frvr7C!L^-f038(y&HD}- zVOA}TntK3>w+F|>wEpSfYTqVYZV!tSeu+Q)P!T(-fR)dPpf@t{3}sq0HlW~UOB`S? z1$ZYmuaff6-;mjSYNf*(YI&J9&|?iG7cHZ?N#118-VKa+m~Bw!B0nN$5aJa_Td7%UOVrVT@&k++c<-hR5FL7`z~1e;h#l z=sstP6+mZh>z)`5PvBAl!>bggu?|&0Z)37h1Vpl;^u(wChPZRH8>4C=v6ElI#_)BGk zT$c4r;M6Y+lqi~i>9uf@enz|p0ST;NYRrCmGMc(QqJ(|_HG{~uxVFfY z<^!&*5!j;M^SO5dWb2YrXNOcm0yK?|K$|W~M^v92c=SF+l8#d7MrTDw$u_cIuy`Wa zp(|0zTld-qE)jQRKs(IF7X0X1t+Pd$1(=r)9$w8EOQFxBMg4k;%P{+o*FC9*+&lN7 z_Q;8{M6KUf^XP^1zSZ3|2E3=lvi*8L z_x>fPaif(mF?YkUvgE(RwYKa5pfMn)jg=D-W^Q{7sC=+Fjd;ZrtdOygYewd&ymt^^ z89)*GcOh2?ahh;(_yPq%URkt}$TgET%FvCUhagEvC#&Rpa4&Rr%ADr@VA`Kx zUo2ynM=tf(uqE8fUTWt%ds#zE(aG~e5^umZhP@diTddG477U!BmvBzy0GdR}lj@g; ziz(K6nW*Ows<;WZ7r!pM>lLHU7T?LV<2JR=rE-~n5`ve{DGegCUfqi6X^UqhQ;XBo z)s1NYgWNvYk_kus-E(alB`rIBQi~f~=7~S)LL4gpE44WzZ&BTF-nv*RB%U(&N_PP$-7Dw=#p^WmIF*C; z^b^^z1=$_q#V_J|%v0Rs16FDq7Vs}Q6zqM;&0Uc~?gYv&j_c>80}D)|+!?QJc7<`E zjo5*fj;Jsl+Id;0wwq%7bXQFtb?gRO;^g>Tk$uofuY%b3;esmHSFW`xoZ)p|F<#-a z4ktS`!IJ&0*@bno_P1}5F9_@$a`?3W5J!+R1TS&l`+apSnc=T{5Txr}w;7CQej~p3 zhV1$H788TrP%T-3;q6G50IXG_kIb(^R=636@aWZri{dcv3ctC>15L^|pQ_Fdw45CJ zi&@P+Cx~{nbm)~|PmbkI;8Qu#M&OPyCl2}XI8J#(&o}lD3#wJ5*D*0u7T*7hmBRmx zmDAJ=*Y$U<)d1)m$QwABKG&mRd}B#_%d?#6r$0yN3Kx;#^v}EK-EkGdV<2+F;jwV| zD2dQinCNI^7aVFpX*Mk?v(70Ac@|p#F^B16Rh0bcMgd|u1;(+@Sy8m~rlr~ulx$ej zVU5Sdl-T6t11Mk~Y!k~?=e+=SS}An!?jk{yp``3l1~J*$>Zj~zeuLVI+Akqiw< zFnZ&ZAG6XNSgW>M%QNpH*hh<^7fR}D;_t7y3jK!QcFEup`j1K%%F8et1d@ZGL!e^- zIgZ!)jtvxm!ISc#eStkPeIH`MPp>av`H$+=>clA}rvBJ3yxl%+8(&DqEQ_aA*w(h& zH-;b9j`83hEn{?Ny^5(MR!$KxKxwkq<}L{`7YM#vHcXM>eBXnOyG<;9G*f#clbGfy zN{*(3mEizyAARhWmDNrzVGEHQw$Y7Iy6pRiNsW`Tt(x5eU}qKkEf}7=_GG@e>dCT? zlB#l?!1v|L_6fJ4#ttzH0)@c-)$(Cwh zVCc)BXG2G_AHsiZVxzBc{c-Tupc=5#I}o%v$~T{kVw>vlon!u$~G1Kpr!W zrg&8gTF*xwqJ~~8^=R-&Uo@aTV4#+^@msz?vA5#z8(YrzD0;wdXS*ewG4 zZ{5qHd1VWK1y2mcrxUyIXK#09iLLne3YQ-X&{yB5?98{XJgJ^UjcXT8j4^T@MhNRL z@7rW=ijR5rMZ|^D}2A*tYgwPm$q@CLO6`99! zXGPCle9_qZ3)|i4ylqmEM9z5Yfmfp5ARi>&@L(ZQ3 zcKw>iY^>pL=nb|?Rd1)(z9Cx^tBD@-)4E2`T5~WD_;f_A4?3N#d%;X`mRjtKKF`z1 z>77_7s@7+^x;_zV4vO<7J7$nMkYk*(O!n!gB3d z7%f$rIbb=4$jT!Auck010eo+!Xv?t{Hsyv&gPyY+Dub7v#bAjmV9(Fz$6Mu>d$t#0 zr)-#YuRSlqk=M1&oT*j3+|5og^fdipN{Wug9f|yHUi+00kUH+$Z8u|v-K4C$Llgf( z8(TV#)~L8|cTf6q->l@%*uUjOVbor-o#cz))$Zd~AHO?uf}^aydPs>}ERV`k)oAdk z#tB!k`40lH^dC%0m~FAI^r*hU_mP_I!&uzo{t4Q5WFU_u)6L4K%#8luB%c>+{%KWE zW691Q+M&4lt!Lbb$k~)NEAKCGMc_sa;LiT()xqiQUhT^dNWEKv^rkcNh>p?05<36w zRw$lO%vHeBx>x#6kytqY$>PUB{m60d{foLA#JU5#ijDoH<+IBHLa*Ub$#gN;yJmdm zkty~|vP}@d`~yDB&yY&m_NMm_wn7KobvS7O4T1N?yXIdQ6FTBNt`@8HZvpp{FTGMhkMTziFH(kJrUk~DNORZhAY0z z=bQ1-I(pJ}D_r_|s&~|7F<4&Yc~+9~ZY<88Rf==l55|MudD3zi6lF4ELeag!N?~A1 zakyRfutBZ4#4Z*1jjVFf{A1E?Krg5#QiH+>3M zO+Z35tw|XC3YYcYxqaJs#4#Brb#5P5InZAgP{TXId8HdD@u|WaA7Neyl!83a-ep9L zFd=v2PW|s;znbv@lUm=2J1@PQFURpP-rO!Vb|IMH%3tGCA;`!|GG0^waL$wzi)=H1~So`nvw8y#I4IC7^Y4 z(d9HtYOItFqDBM%!YDDx`nF_#?di)=*C%1~= znZT1xDxG%W^GfPuJ3hdtlY@jpL*jh~P&zT&)iRov-C21MHPzKrg8wUmfUYI}7 zvr^1b^mF2_7jbj6^gK-yEiHYNXbW~G?his%-V*R` zoZ;6YMHUGhz(m(#MqH5jolN^b(h53;)ou(Fn+BNg(uu-ngML23U4K~`W6ENcsD33@ zvuSs-!Xxe?ylGf#(CKkv*9^3(AexIiq8*9q2b2VNerx@v7YK&fak_jn#8c{5=YuE;{c&);OnS)qsSj;#BsynwKYQ zj1&`J1Kx&b4*SD#y;2u3SyHiPyZ{4*eO*{<33jr9`qsKkpdC2z6PjHd1W4}ipHrKc zIv8~A;+Nun*lGB^<_Xe(;gUX~b*^>wl*S}g%Ukjl#+gvX3QoL8f9*!eC=|B6o=+MB zmj%gu=&+B}{)_O*!ydOB9G>`K{vn2jl!?sJ%0zXKyf6E<_GgyDL~$s{haKsAso#$g z=xR-F-ue&v3Ys4I`~8r5M+xSlO*XyTb|Vpe-b4#_y)j>mBo$Uq!Z?Y)KC0X>GfTg? zhdEkFambrWxM?n99eh$kgYs>F0!1kW%CH&D^;8CSMRdp|v{nrr>XUr+D5w56j7;mN z1HsSWjbhgs1q;jGQleJ?jX4PMd{#w3NC|L|I7Bw-h9`GhLS$8BR zQM0}M8Rdqu!$h*Z7169wWYNm|2?^is}=7To~v)d z-`b!57`=GEkqT0Wp;j1Ao+pG-x2z*BRf4uo>s{_2Ax6$Jk1x`f2?<6QbB{K*C;ExS z2bh}i(f_r?b1*+aSkVQb|LJnHcc>kwzYsb1KBtWHnA98OId9ExlEI6u+0OPCtypFbE<*TK_4GsG@$`dI} zOcA@Ug8{`h`oWcZq&2vzd`f%%8>{9LjMRK4j^CKxYg_(Y;D{~deSoryVqi40m=V~q zAa-X^XKW~g@?k&y;rQKb{42!eM`D?k)lBc*Nn*x+uJfh-LYTe_QD2Gj!c7E-4#%b) z9jBb;ZupW|5~S=j%1l!5fYt;xrroI8+#O%ZXl{9COQN+(eSt#Gd5;b|ZE9E?Ms8q7Z5)m>sC;%xR9wv%-*~v+Q%1=l^O`@nBPamIpm+>9m4a+~PLIPb*RoeXXNa$Ti}7 z{w{Xe|0uanasLkFYi4v}+l-Sa^s zqUafhlB_C)VC!?)z?qmsLI7U=hX5FreFuPZ>f?|RFB`a=2`D2yES>#=kfMJQI-PqOH&+wU8u z&@zvZIgbdSpDo+A)(CW@qP*gD8wfi%gD-|=#(4r*m|?k{11iZ)CDcutWqhhM%7`hx z{(!WNLC_c**P@F7vDw$H)8*3l8!EjJpoo~{}@HXIHUDP^+Z#JNt zvrpF#rF=(rs0-=H0V|LCB@8hd0Vj^P5|0ntBPg)K#jO0acVOHL7cB51Mu|^koBh?w zVF%_8G!yPJKdnf*rUcih3Fwa;Yg@UhT8IH}{cC*2{X12hrxtG+mdL+f;X3>z*;;kw zB|Dk|4L*Vc`=4bdkq3)|=tpdBojKcR)?^-HCcMBXE~c+#Hj(;_^~sYJ_H-g9c>I!2 z*EhS>G9hPcGTQ&)zPTNmR_0H>&Q0Tqs)<;8J~BgoofL!w!u&L<=QfXA+J@j0@5pK= z?N}!8OQ-g|9WgDXBT1=QQ<5TF!?8)OijSx7(8jl;{+xTRUq5{K`n4o;Zd-9BWM#%} zdSY%)SMA0lT`<$dZ%sKlY3u1Ll)SzVQkc4uB`?>e@!vmT_N{`aJwAR-Fj!Ade~0s?dcqB&8=E$7?#OJ?E@VLRQc$(NI-ma5A|Rt#zo`H%koxLDy5HHCry z2LP<)%OOax9yi@?OMtWn2n~W-)+OHF5q$a7a3sDzumVvb@K$4jj;_L>jB}a11MCB$ zx1^Nx5`Uk3qw^HBN?dGo0?ip$zG!gB7&A}Ai%IxqRoO8t|spK z=E1+l-5LC4bMVCmgXO#7n5m1jOvO8Y$A4XZ2|};{7wx^d&foxwHnjt>I@5iA&BIsIf z$XTX{o@>6F(JzDGwIBmPSQp-?W{_)uc zg#DHJ*cWjXr16)o z@lfK~t`hL;7V(Q9jHga+?|gM=(LRzZO5?(MWnr;n*O38#HfH;`_+zVV^M1VI%bHut z&)o>fxqtYYXy()VQ@I~63W52>d&7wA2=Vi2MdGUqk`>>%n{*kiIWuzXEYOmR1$2<- z4mg1Wbg?zSTl0R{jsd^|QevoD@l)LnJzm}pi^hcfiP~dnJ!6{s;0qnfPtFv3KNau? z^-6fe6pHst3ZWLZ)jsF2j=A?|H)PL7=g!mjJ_RfwI;6FkPy4*fKWltCMQnkN)>oZ* zx+lVa#>SXs1RP9Af9+yaiZy>EXgyrNa(K)h!U(6siUb)8VAQPWSP9DAqeQD#8>G&- z6=O9!D#hslbkd;qH^3ndfw{QVLKyL%eV1_g&(CyRcdDq6jgB`=4B+4W!GoB}u(g

eL`};85{hgONaP@I?$|c=S`IU;8 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 L94xENak2jgo2Sda diff --git a/data/icon.png b/data/icon.png index 0cf21041e6d61457cdb5e83b44d178a4ba76efdf..4fd9334dff842e24b3de9d24ffd3fd6b6bdd2db5 100644 GIT binary patch literal 109228 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

Vx&N*+6IkU@hR{|;#_EqfvVAtt|^9Tf*mz-0l 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 M+2*I*A6dWuAJ?zx-2eap literal 35062 zcmV( zaB^>EX>4U6ba`-PAZ2)IW&i+q+O3>vmL)fGrT=3Uy#&k#EC=H`y#p=3zYC8fSrk>I z)XJ!0M80^>9S$?Q17JvZ{^S2%_kZ})bMr12Yqi(v`ICF@aqvU)zyCQu-@n&_&+os# z{)C_Z2KSf0hOUjOy}`rCId^cTMf-Zcf0p|Pd9R98tV5@-!0s)%kaj@ET#9c%9ru~!q4`;I$zBXze4ugFF*NO zp@$ffzafY1!U#8hj)Z)vM%)JYP35gK+ z8d3>0c#F|O$SF1gG#GhIIZh%FN{O36#+*_mHjO;u-h9Twdv7f9d!14tJ|)#;#HOb~ zuyQWskL5-Vjgm?(rPN}jm0pIcspeX0tyWcY!;)nyR?Vzgx6x9|t+d*#wbt9{u_sEh z^xCbr-uoEbb7VKl;M0S5j4|U(GtV;XwAp5#W05{9ud?d0)mC3)$DMZGW!G)H?Y_qe zhg5PWpK|K4(@sC*l502Je9NuZZoB=C-=g*t)&KPM--ufHiCTP-();YUsPU?qF7z%D zgfm4kBVsWJB5sNR2^|$P-$Kq&kyFfkk5om846-OTJNV&3C4~8eSZ??&cE3mNAIHrx z*MAqc_)j9|6uSR~$VGS3pW^lxQQJbo^&<9Fq2|;ls*m5FHewr0cKm+$ci)94Owi7@ zvW=C)$fu`zZ*BARYfM0J^_IN%EO~}gTJx#-d}HMe?Y2tx5R|#XwGM2R(bv3n>=Nvr zv{&7+-&z1hSLQjh7u##BG}@tPJ(rxvbD_}3ylsqC>)bn6o_fZ(?`{mmSLAlL_Iq0)A3PsCt(?njtiGqvKlO4*szN5_@_jE%4U` zD>vMsrgZit9rsy%5<(JnjqWpW9uCj=u3(&xQ%(dR@_3&-LUMS0*;)o~dgurj{Xe=HsT-V%HqkU zOAG9|p!!OYYTQ$GI?H+?H-(J%f$($QZmS*;!4G5r@49w)d3*)nsL_`vrBSH<#9TG! z#1qD6!kP9n;go$tJj=LXW!mBUxv0buPRq6{8SO5Q5x93>yF&4M*2FUq6Q0FDoJ}e7 zD613;u^Q8Um}Mo7*_D*ud_kINK{Nv_O$$kFHlgE){Oxvze132W_sdNxm(0GJX6 zRml@E!LS}^Y}DO~ZstRj^AvjZqdK~?kqi)Fr;4u!1<{CH)MiO}_r5b}U4g8|g?VxA z7H@EdR;flo`ps_+5RKkRsJ%q(gCC&fs1=wb5e=9QQ8Sp%JiClYHXC6bx4CliHDD5W zCQq$`Vv!s!fbOirx=;eVaDb5m z>i8-mB@lSn#&_e3mBkgnU=u>YaXd1k*(Hh?Xr0e#ItSH^mp^J#z*Or zb?1)UG*|*g6@Z(-;dGF0oYDtB&-y=j)CX?vkm7~E>(M}n0Y?StOYkNv%PSDw-cGBN zheq)e-?=tnS!*UJqFI>P!g98THwwFB3H<3`G!QmT%B0x$LX9DxL)F{HL{+yja|3wX zEF~j;Dt-h3P$j=|qwJwwrUIKliKSq>|6#NhY{E;4L}Rdr%)YZwBVLt2 z+>jfkt_$4o&Mdrm7netb0`*RlPO$JnRmE3{6BpHQaaYP z0X87E6f&AHyEBoeDAzIrXiL&g(l1P4639C;R={aE!@rR-H|UcF?I4W&B}dbk172

7@cB?Mc{UCXZMGWltOI{prMNnUo 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 i#(|%4;QwD7`2PT}#zTLH(M?kT0000 Date: Tue, 4 Apr 2023 22:54:51 -0600 Subject: [PATCH 164/172] Blasphemous: Small logic fixes (#1667) --- worlds/blasphemous/Rules.py | 45 ++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/worlds/blasphemous/Rules.py b/worlds/blasphemous/Rules.py index 6bf4a6858d..01d9643542 100644 --- a/worlds/blasphemous/Rules.py +++ b/worlds/blasphemous/Rules.py @@ -114,51 +114,66 @@ class BlasphemousLogic(LogicMixin): return self.has("Taranto to my Sister", player) def _blasphemous_tirana(self, player): - return self.has("Tirana of the Celestial Bastion", player) + return self.has("Tirana of the Celestial Bastion", player) and \ + self.has("Fervour Upgrade", player, 2) def _blasphemous_aubade(self, player): - return self.has("Aubade of the Nameless Guardian", player) + return self.has("Aubade of the Nameless Guardian", player) and \ + self.has("Fervour Upgrade", player, 2) def _blasphemous_cherub_6(self, player): return self.has_any({"Debla of the Lights", "Taranto to my Sister", "Verdiales of the Forsaken Hamlet", \ - "Tirana of the Celestial Bastion", "Cloistered Ruby"}, player) + "Cloistered Ruby"}, player) or \ + (self.has("Tirana of the Celestial Bastion", player) and \ + self.has("Fervour Upgrade", player, 2)) def _blasphemous_cherub_13(self, player): return self.has_any({"Ranged Skill", "Debla of the Lights", "Taranto to my Sister", \ - "Cante Jondo of the Three Sisters", "Aubade of the Nameless Guardian", "Tirana of the Celestial Bastion", \ - "Cloistered Ruby"}, player) + "Cante Jondo of the Three Sisters", "Cloistered Ruby"}, player) or \ + (self.has_any({"Aubade of the Nameless Guardian", "Tirana of the Celestial Bastion"}, player) and \ + self.has("Fervour Upgrade", player, 2)) def _blasphemous_cherub_20(self, player): return self.has_any({"Debla of the Lights", "Lorqiana", "Zarabanda of the Safe Haven", "Taranto to my Sister", \ - "Cante Jondo of the Three Sisters", "Aubade of the Nameless Guardian", "Tirana of the Celestial Bastion", \ - "Cloistered Ruby"}, player) + "Cante Jondo of the Three Sisters", "Cloistered Ruby"}, player) or \ + (self.has_any({"Aubade of the Nameless Guardian", "Tirana of the Celestial Bastion"}, player) and \ + self.has("Fervour Upgrade", player, 2)) def _blasphemous_cherub_21(self, player): return self.has_any({"Debla of the Lights", "Taranto to my Sister", "Cante Jondo of the Three Sisters", \ - "Verdiales of the Forsaken Hamlet", "Tirana of the Celestial Bastion", "Cloistered Ruby"}, player) + "Verdiales of the Forsaken Hamlet", "Cloistered Ruby"}, player) or \ + (self.has("Tirana of the Celestial Bastion", player) and \ + self.has("Fervour Upgrade"), player, 2) def _blasphemous_cherub_22_23_31_32(self, player): return self.has_any({"Debla of the Lights", "Taranto to my Sister", "Cloistered Ruby"}, player) def _blasphemous_cherub_24_33(self, player): return self.has_any({"Debla of the Lights", "Taranto to my Sister", "Cante Jondo of the Three Sisters", \ - "Tirana of the Celestial Bastion", "Cloistered Ruby"}, player) + "Cloistered Ruby"}, player) or \ + (self.has("Tirana of the Celestial Bastion", player) and \ + self.has("Fervour Upgrade", player, 2)) def _blasphemous_cherub_25(self, player): return self.has_any({"Debla of the Lights", "Lorquiana", "Taranto to my Sister", \ - "Cante Jondo of the Three Sisters", "Verdiales of the Forsaken Hamlet", "Aubade of the Nameless Guardian", \ - "Cantina of the Blue Rose", "Cloistered Ruby"}, player) + "Cante Jondo of the Three Sisters", "Verdiales of the Forsaken Hamlet", "Cantina of the Blue Rose", \ + "Cloistered Ruby"}, player) or \ + (self.has("Aubade of the Nameless Guardian", player) and \ + self.has("Fervour Upgrade", player, 2)) def _blasphemous_cherub_27(self, player): return self.has_any({"Ranged Skill", "Debla of the Lights", "Lorquiana", "Taranto to my Sister", \ - "Cante Jondo of the Three Sisters", "Aubade of the Nameless Guardian", "Cantina of the Blue Rose", \ - "Cloistered Ruby"}, player) + "Cante Jondo of the Three Sisters", "Cantina of the Blue Rose", "Cloistered Ruby"}, player) or \ + (self.has("Aubade of the Nameless Guardian", player) and \ + self.has("Fervour Upgrade", player, 2)) def _blasphemous_cherub_38(self, player): return self.has_any({"Ranged Skill", "Lorquiana", "Cante Jondo of the Three Sisters", \ - "Aubade of the Nameless Guardian", "Cantina of the Blue Rose", "Cloistered Ruby"}, player) or \ + "Cantina of the Blue Rose", "Cloistered Ruby"}, player) or \ (self.has("The Young Mason's Wheel", player) and \ - self.has("Brilliant Heart of Dawn", player)) + self.has("Brilliant Heart of Dawn", player)) or \ + (self.has("Aubade of the Nameless Guardian", player) and \ + self.has("Fervour Upgrade", player, 2)) def _blasphemous_wheel(self, player): return self.has("The Young Mason's Wheel", player) From 397ce8343eb957245c7a2d24f801693daa712ac9 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Wed, 5 Apr 2023 00:59:59 -0400 Subject: [PATCH 165/172] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20Pok=C3=A9dex=20op?= =?UTF-8?q?tion=20fixes=20(#1666)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Pokémon R/B: Pokedex option fixes * Pokémon R/B: Missing option display names --- worlds/pokemon_rb/__init__.py | 5 +- worlds/pokemon_rb/basepatch_blue.bsdiff4 | Bin 38871 -> 38879 bytes worlds/pokemon_rb/basepatch_red.bsdiff4 | Bin 38878 -> 38928 bytes worlds/pokemon_rb/options.py | 2 + worlds/pokemon_rb/rom.py | 1 + worlds/pokemon_rb/rom_addresses.py | 282 ++++++++++++----------- 6 files changed, 148 insertions(+), 142 deletions(-) diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index cb9af54947..b223568ff0 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -197,6 +197,7 @@ class PokemonRedBlueWorld(World): + self.multiworld.paralyze_trap_weight[self.player].value + self.multiworld.ice_trap_weight[self.player].value) for location in locations: + event = location.event if not location.inclusion(self.multiworld, self.player): continue if location.original_item in self.multiworld.start_inventory[self.player].value and \ @@ -208,7 +209,7 @@ class PokemonRedBlueWorld(World): elif location.original_item == "Pokedex": if self.multiworld.randomize_pokedex[self.player] == "vanilla": self.multiworld.get_location(location.name, self.player).event = True - location.event = True + event = True item = self.create_item("Pokedex") elif location.original_item.startswith("TM"): if self.multiworld.randomize_tm_moves[self.player]: @@ -220,7 +221,7 @@ class PokemonRedBlueWorld(World): if (item.classification == ItemClassification.filler and self.multiworld.random.randint(1, 100) <= self.multiworld.trap_percentage[self.player].value and combined_traps != 0): item = self.create_item(self.select_trap()) - if location.event: + if event: self.multiworld.get_location(location.name, self.player).place_locked_item(item) elif "Badge" not in item.name or self.multiworld.badgesanity[self.player].value: item_pool.append(item) diff --git a/worlds/pokemon_rb/basepatch_blue.bsdiff4 b/worlds/pokemon_rb/basepatch_blue.bsdiff4 index 6123d714151c7543e02cb834abeb42db32b7f87a..895c5ddfe1c6ba36b6380db85a74a4d7bc29da0e 100644 GIT binary patch literal 38879 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 LP81|GO2Br2lwrCg literal 38871 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 diff --git a/worlds/pokemon_rb/basepatch_red.bsdiff4 b/worlds/pokemon_rb/basepatch_red.bsdiff4 index efe4de42ea01b779a5c9dbf4930cd87e52c8c3d6..1d69c48241af40eabac514550e6c9d8f0ed98c8e 100644 GIT binary patch literal 38928 zcmZ^~Wl$VE;O@J?0t?06Wm#N`EDkNQxNC8j#oeX2yL)jf?pEC0-L1u2v=mCud;Vwc z-236&e8`i@OeQkPB){Z|ga%AbS{ll^g8}%TP^@lpd2l}@nu`zM1rN|O-oWJ8C7`> z;DNLhIzNwEUig57BPvtMg=9+x5Gj!qFNFGtAuXxi2LYfq=O~etD8WhCswB&BQE@_1 z;5av&yCOOs#4B1=o)VZkS-`E7tOBh#RLs@f`YtejC%UBBF)}zDJ`X4i+mu$~0*MGgs}$vJXa3=<$=aK#s4Cx@i zI2?`%fJ3R0=Ap8(5+x;CU^Hx^{Lo0*Fb8stNcA#1bSnuDd9iS2AsW?AJUnr{cikOd zi-cyRFoO1JNE%ZybOsSe+eBg=#<~qN&s(^z@7Wv9cb2x@KFyu^Y%vip?`CBD!IrCY zib(qRlZVz~srQ1c*OD_mxjz6xC8cf z!cY{@61N(n*yZ;MDR2xm`#jZbC<@s33I!>AdqtF{C@lEX*!VNuy)~Urgjf)oX@0id z;W3%k@7VFysLK-FjP3pSwj4U;g|IVvW$t9h2Q8+3ntxiWf91KY@TqP_wV22zq5kaK zT_>Ret2bO1pu>!#C=3b;sjf&tv;v{&Q5c^)W9d05CRZ{YCqOPrpcQ2kIbJ?ucpog;Yl-;I-3FB=6;=QgaK1lL}D zwMpRmZR}hige7ldVlhX4UA*Yqhg&?9rNvDP-pr^s%P5_!X?|WpH-cQ9?wPzRa~4{; z98w=l3h}(zG_qQjI=9na

HGv>puH(l4q%5_T*2w@WNa= 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 literal 38878 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

D8=XTmlIrNPH?6tWHiJPSn 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 Date: Wed, 5 Apr 2023 01:07:15 -0400 Subject: [PATCH 166/172] Wargroove: Client is not added to the Start Menu. (#1664) * Wargroove - Fixed client not showing up in the windows search menu. --- inno_setup.iss | 1 + 1 file changed, 1 insertion(+) diff --git a/inno_setup.iss b/inno_setup.iss index 57e48b3805..866399d322 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -149,6 +149,7 @@ Name: "{group}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoSta Name: "{group}\{#MyAppName} The Legend of Zelda Client"; Filename: "{app}\ArchipelagoZelda1Client.exe"; Components: client/tloz Name: "{group}\{#MyAppName} Kingdom Hearts 2 Client"; Filename: "{app}\ArchipelagoKH2Client.exe"; Components: client/kh2 Name: "{group}\{#MyAppName} Adventure Client"; Filename: "{app}\ArchipelagoAdventureClient.exe"; Components: client/advn +Name: "{group}\{#MyAppName} Wargroove Client"; Filename: "{app}\ArchipelagoWargrooveClient.exe"; Components: client/wargroove Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Components: server From e778e49574d286c97c9176821ddaf1acee2fa728 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Wed, 5 Apr 2023 12:11:34 -0500 Subject: [PATCH 167/172] Core: Fix ZeroDivisionError if a world is contained 100% locked locations. (#1659) * Core: Fix divide by zero error if all locations are priority * Core: 100% makes more sense than 0% in this context * Core: Revert a some "autoformatter" damage * Core: Move comparisons a bit earlier. Worlds that are 100% filled with locked locations should now be completely skipped in the progression balancing step now as we filter them out at the very beginning of the stage now. Also skips progression balancing if it turns out all locations are locked early before attempting to balance. --- Fill.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/Fill.py b/Fill.py index ef84a23b01..6fa5ecb00d 100644 --- a/Fill.py +++ b/Fill.py @@ -1,11 +1,10 @@ -import logging -import typing import collections import itertools +import logging +import typing from collections import Counter, deque -from BaseClasses import CollectionState, Location, LocationProgressType, MultiWorld, Item, ItemClassification - +from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld from worlds.AutoWorld import call_all from worlds.generic.Rules import add_item_rule @@ -526,16 +525,16 @@ def balance_multiworld_progression(world: MultiWorld) -> None: checked_locations: typing.Set[Location] = set() unchecked_locations: typing.Set[Location] = set(world.get_locations()) - reachable_locations_count: typing.Dict[int, int] = { - player: 0 - for player in world.player_ids - if len(world.get_filled_locations(player)) != 0 - } total_locations_count: typing.Counter[int] = Counter( location.player for location in world.get_locations() if not location.locked ) + reachable_locations_count: typing.Dict[int, int] = { + player: 0 + for player in world.player_ids + if total_locations_count[player] and len(world.get_filled_locations(player)) != 0 + } balanceable_players = { player: balanceable_players[player] for player in balanceable_players @@ -552,6 +551,10 @@ def balance_multiworld_progression(world: MultiWorld) -> None: def item_percentage(player: int, num: int) -> float: return num / total_locations_count[player] + # If there are no locations that aren't locked, there's no point in attempting to balance progression. + if len(total_locations_count) == 0: + return + while True: # Gather non-locked locations. # This ensures that only shuffled locations get counted for progression balancing, @@ -825,7 +828,6 @@ def distribute_planned(world: MultiWorld) -> None: for player in worlds: locations += non_early_locations[player] - block['locations'] = locations if not block['count']: From 4c248722643185faefadc25cf34f16629255ce5a Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 14 Mar 2023 00:04:34 +0100 Subject: [PATCH 168/172] CI: update ubuntu to 20.04 18.04 will not be supported starting 2023-04-01 --- .github/workflows/build.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 26a6e830c1..849e752305 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,8 +52,8 @@ jobs: path: dist/${{ env.ZIP_NAME }} retention-days: 7 # keep for 7 days, should be enough - build-ubuntu1804: - runs-on: ubuntu-18.04 + build-ubuntu2004: + runs-on: ubuntu-20.04 steps: # - copy code below to release.yml - - uses: actions/checkout@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4f1a8eec4a..42594721d0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,8 +29,8 @@ jobs: # build-release-windows: # this is done by hand because of signing # build-release-macos: # LF volunteer - build-release-ubuntu1804: - runs-on: ubuntu-18.04 + build-release-ubuntu2004: + runs-on: ubuntu-20.04 steps: - name: Set env run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV From a61a1f58c6b41cc121403fb88fcf009eea6f417a Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 8 Mar 2023 17:53:43 +0100 Subject: [PATCH 169/172] Network: allow filtering checked and missing by text fragment --- CommonClient.py | 7 +++++-- MultiServer.py | 30 ++++++++++++++++++++++-------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index afce98d8ca..4892f69f06 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -68,14 +68,17 @@ class ClientCommandProcessor(CommandProcessor): self.output(f"{self.ctx.item_names[item.item]} from {self.ctx.player_names[item.player]}") return True - def _cmd_missing(self) -> bool: - """List all missing location checks, from your local game state""" + def _cmd_missing(self, filter_text = "") -> bool: + """List all missing location checks, from your local game state. + Can be given text, which will be used as filter.""" if not self.ctx.game: self.output("No game set, cannot determine missing checks.") return False count = 0 checked_count = 0 for location, location_id in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id.items(): + if filter_text and filter_text not in location: + continue if location_id < 0: continue if location_id not in self.ctx.locations_checked: diff --git a/MultiServer.py b/MultiServer.py index 258b73c6ee..ea055b662e 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -1327,27 +1327,41 @@ class ClientMessageProcessor(CommonCommandProcessor): "Sorry, !remaining requires you to have beaten the game on this server") return False - def _cmd_missing(self) -> bool: - """List all missing location checks from the server's perspective""" + def _cmd_missing(self, filter_text="") -> bool: + """List all missing location checks from the server's perspective. + Can be given text, which will be used as filter.""" locations = get_missing_checks(self.ctx, self.client.team, self.client.slot) if locations: - texts = [f'Missing: {self.ctx.location_names[location]}' for location in locations] - texts.append(f"Found {len(locations)} missing location checks") + names = [self.ctx.location_names[location] for location in locations] + if filter_text: + names = [name for name in names if filter_text in name] + texts = [f'Missing: {name}' for name in names] + if filter_text: + texts.append(f"Found {len(locations)} missing location checks, displaying {len(names)} of them.") + else: + texts.append(f"Found {len(locations)} missing location checks") self.output_multiple(texts) else: self.output("No missing location checks found.") return True - def _cmd_checked(self) -> bool: - """List all done location checks from the server's perspective""" + def _cmd_checked(self, filter_text="") -> bool: + """List all done location checks from the server's perspective. + Can be given text, which will be used as filter.""" locations = get_checked_checks(self.ctx, self.client.team, self.client.slot) if locations: - texts = [f'Checked: {self.ctx.location_names[location]}' for location in locations] - texts.append(f"Found {len(locations)} done location checks") + names = [self.ctx.location_names[location] for location in locations] + if filter_text: + names = [name for name in names if filter_text in name] + texts = [f'Checked: {name}' for name in names] + if filter_text: + texts.append(f"Found {len(locations)} done location checks, displaying {len(names)} of them.") + else: + texts.append(f"Found {len(locations)} done location checks") self.output_multiple(texts) else: self.output("No done location checks found.") From 815e7e6b0a8d12c20e280a9abcc3553a23e5cee3 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Thu, 6 Apr 2023 01:19:58 +0200 Subject: [PATCH 170/172] Core: default data_version to 0 (#1668) * Core: default data_version to 0 This allows new (ap-)worlds to function with old clients without having to define a version. * Blasphemous: fix data_version --- worlds/AutoWorld.py | 2 +- worlds/blasphemous/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 4f4e2750cd..d8f1bfd474 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -159,7 +159,7 @@ 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] = 1 + data_version: ClassVar[int] = 0 """ Increment this every time something in your world's names/id mappings changes. diff --git a/worlds/blasphemous/__init__.py b/worlds/blasphemous/__init__.py index 70aea1ef76..a7a86826c3 100644 --- a/worlds/blasphemous/__init__.py +++ b/worlds/blasphemous/__init__.py @@ -32,7 +32,7 @@ class BlasphemousWorld(World): game: str = "Blasphemous" web = BlasphemousWeb() - data_version: 1 + data_version = 1 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)} From 47989325f8ca7d83bd713bb85699283d76e0136f Mon Sep 17 00:00:00 2001 From: kindasneaki Date: Wed, 5 Apr 2023 17:27:56 -0600 Subject: [PATCH 171/172] [Webhost] header closing tag moved after mobile menu (#1650) * Change archipelago mod download page * Docs: change connecting to archipelago in RoR2 setup guide * /header off by one --- WebHostLib/templates/header/baseHeader.html | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/WebHostLib/templates/header/baseHeader.html b/WebHostLib/templates/header/baseHeader.html index 0042937d62..4090ff477f 100644 --- a/WebHostLib/templates/header/baseHeader.html +++ b/WebHostLib/templates/header/baseHeader.html @@ -30,14 +30,14 @@ Menu

+ - {% endblock %} From c626618221f6973294c5aebd8050ae9ea57cf462 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Thu, 6 Apr 2023 00:08:41 -0400 Subject: [PATCH 172/172] Update ArchipIDLE's documentation and create items in `create_items` func. (#1669) --- worlds/archipidle/__init__.py | 16 ++++++++-------- worlds/archipidle/docs/en_ArchipIDLE.md | 5 +++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/worlds/archipidle/__init__.py b/worlds/archipidle/__init__.py index dc980f13a3..768b7604e7 100644 --- a/worlds/archipidle/__init__.py +++ b/worlds/archipidle/__init__.py @@ -21,7 +21,7 @@ class ArchipIDLEWebWorld(WebWorld): class ArchipIDLEWorld(World): """ - An idle game which sends a check every thirty seconds, up to one hundred checks. + An idle game which sends a check every thirty seconds, up to two hundred checks. """ game = "ArchipIDLE" topology_present = False @@ -41,7 +41,13 @@ class ArchipIDLEWorld(World): location_name_to_id[f"IDLE item number {i}"] = start_id start_id += 1 - def generate_basic(self): + def set_rules(self): + set_rules(self.multiworld, self.player) + + def create_item(self, name: str) -> Item: + return Item(name, ItemClassification.progression, self.item_name_to_id[name], self.player) + + def create_items(self): item_table_copy = list(item_table) self.multiworld.random.shuffle(item_table_copy) @@ -57,12 +63,6 @@ class ArchipIDLEWorld(World): self.multiworld.itempool += item_pool - def set_rules(self): - set_rules(self.multiworld, self.player) - - def create_item(self, name: str) -> Item: - return Item(name, ItemClassification.progression, self.item_name_to_id[name], self.player) - def create_regions(self): self.multiworld.regions += [ create_region(self.multiworld, self.player, 'Menu', None, ['Entrance to IDLE Zone']), diff --git a/worlds/archipidle/docs/en_ArchipIDLE.md b/worlds/archipidle/docs/en_ArchipIDLE.md index 066f3f05bf..3d57e3a055 100644 --- a/worlds/archipidle/docs/en_ArchipIDLE.md +++ b/worlds/archipidle/docs/en_ArchipIDLE.md @@ -2,8 +2,9 @@ ## What is this game? -ArchipIDLE is the 2022 Archipelago April Fools' Day joke. It is an idle game that sends a location check every -thirty seconds, up to one hundred checks. +ArchipIDLE was originally the 2022 Archipelago April Fools' Day joke. It is an idle game that sends a location check +on regular intervals. Updated annually with more items, gimmicks, and features, the game is visible +only during the month of April. ## Where is the settings page?

EY-C;1+AHPCtG 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¬}l$e#d` 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 Date: Tue, 28 Feb 2023 11:23:42 +0100 Subject: [PATCH 008/172] Core: count the world types --- Main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Main.py b/Main.py index 04a7e3bff6..2ef61f5d38 100644 --- a/Main.py +++ b/Main.py @@ -79,7 +79,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No world.state = CollectionState(world) logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed) - logger.info("Found World Types:") + logger.info(f"Found {len(AutoWorld.AutoWorldRegister.world_types)} World Types:") longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types) max_item = 0 From 0cf82066609d25acaf6685e52b4b0dd640454c07 Mon Sep 17 00:00:00 2001 From: el-u <109771707+el-u@users.noreply.github.com> Date: Wed, 1 Mar 2023 20:23:58 +0100 Subject: [PATCH 009/172] launcher: add .apl2ac support --- Launcher.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Launcher.py b/Launcher.py index d5ade1f184..27510a882d 100644 --- a/Launcher.py +++ b/Launcher.py @@ -132,7 +132,8 @@ components: Iterable[Component] = ( Component('Text Client', 'CommonClient', 'ArchipelagoTextClient'), # SNI Component('SNI Client', 'SNIClient', - file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3', '.apsmw')), + file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3', + '.apsmw', '.apl2ac')), Component('LttP Adjuster', 'LttPAdjuster'), # Factorio Component('Factorio Client', 'FactorioClient'), From 805f33c39ef565c174a3cb7aae513d92920472b3 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Fri, 3 Mar 2023 00:08:24 +0100 Subject: [PATCH 010/172] Witness: Bugfixes in response to beta tests (#1473) * Make all Keep Pressure Plates logically required for the Laser Panel * Added more Tutorial checks * Added the remaining two Shipwreck Boat EPs to the exclude list for normal * Improved itempool filling system, added warning if usefuls had to be eaten * Moved creation of said warning string to utils * Fixed logic bug causing broken seeds on Mountain Floor 2 * Hints system change * Expert Logic Fix * Fixed typo * Better wording * Added missing games to junk hints * Made sure Entrance names are unique * Fixed missing Obelisk Side * Disable Non Randomized + EP Shuffle fix * Fixed disable_non_randomized precompleted EPs being 'disabled' instead of 'precompleted' * Fixed if/elif error * Tutorial Gate Open local symbol item becomes local_early_item in expert instead * Bump required client version. There is a beta client that sends 0.3.9. * Removed print statement, oops * Fixed itempool manipulation in pre_fill * Replaced string concats with fstrings * Improved make_warning_string function signature Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Improved performance on removing multiple items from multiworld itempool * Comment * Fixed errors with the code * Made removal from itempool not fail unit test for multiple references * Moved all item creation to create_items, got rid of itempool modifying system * Colored Squares is no longer a good item, that's outdated * Removed double if * React to from_pool: false by removing a junk item * Fixed warning if only Fnc Brain was removed * Make use of string truthiness instead * Made reading of plandoed items safer --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- worlds/witness/WitnessLogic.txt | 9 +- worlds/witness/WitnessLogicExpert.txt | 11 +- worlds/witness/WitnessLogicVanilla.txt | 9 +- worlds/witness/__init__.py | 136 ++++++++---- worlds/witness/hints.py | 198 +++++++++--------- worlds/witness/items.py | 11 +- worlds/witness/locations.py | 7 +- worlds/witness/player_logic.py | 10 +- worlds/witness/regions.py | 18 +- .../witness/settings/Disable_Unrandomized.txt | 26 +-- .../witness/settings/EP_Shuffle/EP_Easy.txt | 4 +- worlds/witness/utils.py | 36 ++++ 12 files changed, 292 insertions(+), 183 deletions(-) diff --git a/worlds/witness/WitnessLogic.txt b/worlds/witness/WitnessLogic.txt index 308cba385f..2b3c25f23b 100644 --- a/worlds/witness/WitnessLogic.txt +++ b/worlds/witness/WitnessLogic.txt @@ -411,7 +411,7 @@ Keep Tower (Keep) - Keep - 0x04F8F: 158206 - 0x0361B (Tower Shortcut Panel) - True - True Door - 0x04F8F (Tower Shortcut) - 0x0361B 158704 - 0x0360E (Laser Panel Hedges) - 0x01A0F & 0x019E7 & 0x019DC & 0x00139 - True -158705 - 0x03317 (Laser Panel Pressure Plates) - 0x01D3F - Shapers & Black/White Squares & Colored Squares & Stars & Stars + Same Colored Symbol & Dots +158705 - 0x03317 (Laser Panel Pressure Plates) - 0x033EA & 0x01BE9 & 0x01CD3 & 0x01D3F - Shapers & Black/White Squares & Colored Squares & Stars & Stars + Same Colored Symbol & Dots Laser - 0x014BB (Laser) - 0x0360E | 0x03317 159240 - 0x033BE (Pressure Plates 1 EP) - 0x033EA - True 159241 - 0x033BF (Pressure Plates 2 EP) - 0x01BE9 - True @@ -909,7 +909,7 @@ Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 158430 - 0x09FD8 (Near Row 5) - 0x09FD7 - Colored Squares & Symmetry & Colored Dots Door - 0x09FFB (Staircase Near) - 0x09FD8 -Mountain Floor 2 Blue Bridge (Mountain Floor 2) - Mountain Floor 2 Beyond Bridge - TrueOneWay - Mountain Floor 2 At Door - TrueOneWay: +Mountain Floor 2 Blue Bridge (Mountain Floor 2) - Mountain Floor 2 Beyond Bridge - TrueOneWay - Mountain Floor 2 At Door - 0x09ED8: Mountain Floor 2 At Door (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EDD: Door - 0x09EDD (Elevator Room Entry) - 0x09ED8 & 0x09E86 @@ -1113,8 +1113,9 @@ Obelisks (EPs) - Entry - True: 159735 - 0xFFE35 (River Obelisk Side 6) - 0x035CB & 0x035CF - True 159740 - 0xFFE40 (Quarry Obelisk Side 1) - 0x28A7B & 0x005F6 & 0x00859 & 0x17CB9 & 0x28A4A - True 159741 - 0xFFE41 (Quarry Obelisk Side 2) - 0x334B6 & 0x00614 & 0x0069D & 0x28A4C - True -159742 - 0xFFE42 (Quarry Obelisk Side 3) - 0x289CF & 0x289D1 & 0x33692 - True -159743 - 0xFFE43 (Quarry Obelisk Side 4) - 0x03E77 & 0x03E7C - True +159742 - 0xFFE42 (Quarry Obelisk Side 3) - 0x289CF & 0x289D1 - True +159743 - 0xFFE43 (Quarry Obelisk Side 4) - 0x33692 - True +159744 - 0xFFE44 (Quarry Obelisk Side 5) - 0x03E77 & 0x03E7C - True 159750 - 0xFFE50 (Town Obelisk Side 1) - 0x035C7 - True 159751 - 0xFFE51 (Town Obelisk Side 2) - 0x01848 & 0x03D06 & 0x33530 & 0x33600 & 0x28A2F & 0x28A37 & 0x334A3 & 0x3352F - True 159752 - 0xFFE52 (Town Obelisk Side 3) - 0x33857 & 0x33879 & 0x03C19 - True diff --git a/worlds/witness/WitnessLogicExpert.txt b/worlds/witness/WitnessLogicExpert.txt index f100a3095b..b396e5351e 100644 --- a/worlds/witness/WitnessLogicExpert.txt +++ b/worlds/witness/WitnessLogicExpert.txt @@ -270,7 +270,7 @@ Door - 0x0368A (Stairs) - 0x03677 159413 - 0x00614 (Lift EP) - 0x275FF & 0x03675 - True Quarry Boathouse (Quarry Boathouse) - Quarry - True - Quarry Boathouse Upper Front - 0x03852 - Quarry Boathouse Behind Staircase - 0x2769B: -158146 - 0x034D4 (Intro Left) - True - Stars & Eraser +158146 - 0x034D4 (Intro Left) - True - Stars & Stars + Same Colored Symbol + Eraser 158147 - 0x021D5 (Intro Right) - True - Shapers & Eraser 158148 - 0x03852 (Ramp Height Control) - 0x034D4 & 0x021D5 - Rotated Shapers 158166 - 0x17CA6 (Boat Spawn) - True - Boat @@ -411,7 +411,7 @@ Keep Tower (Keep) - Keep - 0x04F8F: 158206 - 0x0361B (Tower Shortcut Panel) - True - True Door - 0x04F8F (Tower Shortcut) - 0x0361B 158704 - 0x0360E (Laser Panel Hedges) - 0x01A0F & 0x019E7 & 0x019DC & 0x00139 - True -158705 - 0x03317 (Laser Panel Pressure Plates) - 0x01BE9 - Shapers & Rotated Shapers & Triangles & Stars & Stars + Same Colored Symbol & Colored Squares & Black/White Squares +158705 - 0x03317 (Laser Panel Pressure Plates) - 0x033EA & 0x01BE9 & 0x01CD3 & 0x01D3F - Shapers & Rotated Shapers & Triangles & Stars & Stars + Same Colored Symbol & Colored Squares & Black/White Squares Laser - 0x014BB (Laser) - 0x0360E | 0x03317 159240 - 0x033BE (Pressure Plates 1 EP) - 0x033EA - True 159241 - 0x033BF (Pressure Plates 2 EP) - 0x01BE9 - True @@ -909,7 +909,7 @@ Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 158430 - 0x09FD8 (Near Row 5) - 0x09FD7 - Stars & Stars + Same Colored Symbol & Rotated Shapers & Eraser Door - 0x09FFB (Staircase Near) - 0x09FD8 -Mountain Floor 2 Blue Bridge (Mountain Floor 2) - Mountain Floor 2 Beyond Bridge - TrueOneWay - Mountain Floor 2 At Door - TrueOneWay: +Mountain Floor 2 Blue Bridge (Mountain Floor 2) - Mountain Floor 2 Beyond Bridge - TrueOneWay - Mountain Floor 2 At Door - 0x09ED8: Mountain Floor 2 At Door (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EDD: Door - 0x09EDD (Elevator Room Entry) - 0x09ED8 & 0x09E86 @@ -1113,8 +1113,9 @@ Obelisks (EPs) - Entry - True: 159735 - 0xFFE35 (River Obelisk Side 6) - 0x035CB & 0x035CF - True 159740 - 0xFFE40 (Quarry Obelisk Side 1) - 0x28A7B & 0x005F6 & 0x00859 & 0x17CB9 & 0x28A4A - True 159741 - 0xFFE41 (Quarry Obelisk Side 2) - 0x334B6 & 0x00614 & 0x0069D & 0x28A4C - True -159742 - 0xFFE42 (Quarry Obelisk Side 3) - 0x289CF & 0x289D1 & 0x33692 - True -159743 - 0xFFE43 (Quarry Obelisk Side 4) - 0x03E77 & 0x03E7C - True +159742 - 0xFFE42 (Quarry Obelisk Side 3) - 0x289CF & 0x289D1 - True +159743 - 0xFFE43 (Quarry Obelisk Side 4) - 0x33692 - True +159744 - 0xFFE44 (Quarry Obelisk Side 5) - 0x03E77 & 0x03E7C - True 159750 - 0xFFE50 (Town Obelisk Side 1) - 0x035C7 - True 159751 - 0xFFE51 (Town Obelisk Side 2) - 0x01848 & 0x03D06 & 0x33530 & 0x33600 & 0x28A2F & 0x28A37 & 0x334A3 & 0x3352F - True 159752 - 0xFFE52 (Town Obelisk Side 3) - 0x33857 & 0x33879 & 0x03C19 - True diff --git a/worlds/witness/WitnessLogicVanilla.txt b/worlds/witness/WitnessLogicVanilla.txt index 294950c305..f0e84e3ebb 100644 --- a/worlds/witness/WitnessLogicVanilla.txt +++ b/worlds/witness/WitnessLogicVanilla.txt @@ -411,7 +411,7 @@ Keep Tower (Keep) - Keep - 0x04F8F: 158206 - 0x0361B (Tower Shortcut Panel) - True - True Door - 0x04F8F (Tower Shortcut) - 0x0361B 158704 - 0x0360E (Laser Panel Hedges) - 0x01A0F & 0x019E7 & 0x019DC & 0x00139 - True -158705 - 0x03317 (Laser Panel Pressure Plates) - 0x01D3F - Shapers & Black/White Squares & Rotated Shapers +158705 - 0x03317 (Laser Panel Pressure Plates) - 0x033EA & 0x01BE9 & 0x01CD3 & 0x01D3F - Shapers & Black/White Squares & Rotated Shapers Laser - 0x014BB (Laser) - 0x0360E | 0x03317 159240 - 0x033BE (Pressure Plates 1 EP) - 0x033EA - True 159241 - 0x033BF (Pressure Plates 2 EP) - 0x01BE9 - True @@ -909,7 +909,7 @@ Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 158430 - 0x09FD8 (Near Row 5) - 0x09FD7 - Colored Squares & Symmetry Door - 0x09FFB (Staircase Near) - 0x09FD8 -Mountain Floor 2 Blue Bridge (Mountain Floor 2) - Mountain Floor 2 Beyond Bridge - TrueOneWay - Mountain Floor 2 At Door - TrueOneWay: +Mountain Floor 2 Blue Bridge (Mountain Floor 2) - Mountain Floor 2 Beyond Bridge - TrueOneWay - Mountain Floor 2 At Door - 0x09ED8: Mountain Floor 2 At Door (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EDD: Door - 0x09EDD (Elevator Room Entry) - 0x09ED8 & 0x09E86 @@ -1113,8 +1113,9 @@ Obelisks (EPs) - Entry - True: 159735 - 0xFFE35 (River Obelisk Side 6) - 0x035CB & 0x035CF - True 159740 - 0xFFE40 (Quarry Obelisk Side 1) - 0x28A7B & 0x005F6 & 0x00859 & 0x17CB9 & 0x28A4A - True 159741 - 0xFFE41 (Quarry Obelisk Side 2) - 0x334B6 & 0x00614 & 0x0069D & 0x28A4C - True -159742 - 0xFFE42 (Quarry Obelisk Side 3) - 0x289CF & 0x289D1 & 0x33692 - True -159743 - 0xFFE43 (Quarry Obelisk Side 4) - 0x03E77 & 0x03E7C - True +159742 - 0xFFE42 (Quarry Obelisk Side 3) - 0x289CF & 0x289D1 - True +159743 - 0xFFE43 (Quarry Obelisk Side 4) - 0x33692 - True +159744 - 0xFFE44 (Quarry Obelisk Side 5) - 0x03E77 & 0x03E7C - True 159750 - 0xFFE50 (Town Obelisk Side 1) - 0x035C7 - True 159751 - 0xFFE51 (Town Obelisk Side 2) - 0x01848 & 0x03D06 & 0x33530 & 0x33600 & 0x28A2F & 0x28A37 & 0x334A3 & 0x3352F - True 159752 - 0xFFE52 (Town Obelisk Side 3) - 0x33857 & 0x33879 & 0x03C19 - True diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index 71bebb6eb1..f6fae6bddc 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -14,7 +14,7 @@ from .items import WitnessItem, StaticWitnessItems, WitnessPlayerItems from .rules import set_rules from .regions import WitnessRegions from .Options import is_option_enabled, the_witness_options, get_option_value -from .utils import best_junk_to_add_based_on_weights, get_audio_logs +from .utils import best_junk_to_add_based_on_weights, get_audio_logs, make_warning_string from logging import warning @@ -38,7 +38,7 @@ class WitnessWorld(World): """ game = "The Witness" topology_present = False - data_version = 12 + data_version = 13 static_logic = StaticWitnessLogic() static_locat = StaticWitnessLocations() @@ -52,7 +52,7 @@ class WitnessWorld(World): location_name_to_id = StaticWitnessLocations.ALL_LOCATIONS_TO_ID item_name_groups = StaticWitnessItems.ITEM_NAME_GROUPS - required_client_version = (0, 3, 8) + required_client_version = (0, 3, 9) def _get_slot_data(self): return { @@ -93,13 +93,13 @@ class WitnessWorld(World): self.items = WitnessPlayerItems(self.locat, self.multiworld, self.player, self.player_logic) self.regio = WitnessRegions(self.locat) - def create_regions(self): - self.regio.create_regions(self.multiworld, self.player, self.player_logic) - - def generate_basic(self): self.log_ids_to_hints = dict() self.junk_items_created = {key: 0 for key in self.items.JUNK_WEIGHTS.keys()} + def create_regions(self): + self.regio.create_regions(self.multiworld, self.player, self.player_logic) + + def create_items(self): # Generate item pool pool = [] for item in self.items.ITEM_TABLE: @@ -109,22 +109,12 @@ class WitnessWorld(World): pool.append(witness_item) self.items_by_name[item] = witness_item - less_junk = 0 - - dog_check = self.multiworld.get_location( - "Town Pet the Dog", self.player - ) - - dog_check.place_locked_item(self.create_item("Puzzle Skip")) - - less_junk += 1 - for precol_item in self.multiworld.precollected_items[self.player]: if precol_item.name in self.items_by_name: # if item is in the pool, remove 1 instance. item_obj = self.items_by_name[precol_item.name] if item_obj in pool: - pool.remove(item_obj) # remove one instance of this pre-collected item if it exists + pool.remove(item_obj) # remove one instance of this pre-collected item if it exists for item in self.player_logic.STARTING_INVENTORY: self.multiworld.push_precollected(self.items_by_name[item]) @@ -132,15 +122,8 @@ class WitnessWorld(World): for item in self.items.EXTRA_AMOUNTS: for i in range(0, self.items.EXTRA_AMOUNTS[item]): - if len(pool) < len(self.locat.CHECK_LOCATION_TABLE) - len(self.locat.EVENT_LOCATION_TABLE) - less_junk: - witness_item = self.create_item(item) - pool.append(witness_item) - - # Put in junk items to fill the rest - junk_size = len(self.locat.CHECK_LOCATION_TABLE) - len(pool) - len(self.locat.EVENT_LOCATION_TABLE) - less_junk - - for i in range(0, junk_size): - pool.append(self.create_item(self.get_filler_item_name())) + witness_item = self.create_item(item) + pool.append(witness_item) # Tie Event Items to Event Locations (e.g. Laser Activations) for event_location in self.locat.EVENT_LOCATION_TABLE: @@ -150,27 +133,108 @@ class WitnessWorld(World): location_obj = self.multiworld.get_location(event_location, self.player) location_obj.place_locked_item(item_obj) - self.multiworld.itempool += pool + # Find out how much empty space there is for junk items. -1 for the "Town Pet the Dog" check + itempool_difference = len(self.locat.CHECK_LOCATION_TABLE) - len(self.locat.EVENT_LOCATION_TABLE) - 1 + itempool_difference -= len(pool) - def pre_fill(self): - # Put good item on first check if there are any of the designated "good items" in the pool + # Place two locked items: Good symbol on Tutorial Gate Open, and a Puzzle Skip on "Town Pet the Dog" good_items_in_the_game = [] + plandoed_items = set() + + for v in self.multiworld.plando_items[self.player]: + if v.get("from_pool", True): + plandoed_items.update({self.items_by_name[i] for i in v.get("items", dict()).keys() + if i in self.items_by_name}) + if "item" in v and v["item"] in self.items_by_name: + plandoed_items.add(self.items_by_name[v["item"]]) for symbol in self.items.GOOD_ITEMS: item = self.items_by_name[symbol] - if item in self.multiworld.itempool: # Only do this if the item is still in item pool (e.g. after plando) + if item in pool and item not in plandoed_items: + # for now, any item that is mentioned in any plando option, even if it's a list of items, is ineligible. + # Hopefully, in the future, plando gets resolved before create_items. + # I could also partially resolve lists myself, but this could introduce errors if not done carefully. good_items_in_the_game.append(symbol) if good_items_in_the_game: random_good_item = self.multiworld.random.choice(good_items_in_the_game) - first_check = self.multiworld.get_location( - "Tutorial Gate Open", self.player - ) item = self.items_by_name[random_good_item] - first_check.place_locked_item(item) - self.multiworld.itempool.remove(item) + if get_option_value(self.multiworld, self.player, "puzzle_randomization") == 1: + self.multiworld.local_early_items[self.player][random_good_item] = 1 + else: + first_check = self.multiworld.get_location( + "Tutorial Gate Open", self.player + ) + + first_check.place_locked_item(item) + pool.remove(item) + + dog_check = self.multiworld.get_location( + "Town Pet the Dog", self.player + ) + + dog_check.place_locked_item(self.create_item("Puzzle Skip")) + + # Fill rest of item pool with junk if there is room + if itempool_difference > 0: + for i in range(0, itempool_difference): + self.multiworld.itempool.append(self.create_item(self.get_filler_item_name())) + + # Remove junk, Functioning Brain, useful items (non-door), useful door items in that order until there is room + if itempool_difference < 0: + junk = [ + item for item in pool + if item.classification in {ItemClassification.filler, ItemClassification.trap} + and item.name != "Functioning Brain" + ] + + f_brain = [item for item in pool if item.name == "Functioning Brain"] + + usefuls = [ + item for item in pool + if item.classification == ItemClassification.useful + and item.name not in StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT + ] + + removable_doors = [ + item for item in pool + if item.classification == ItemClassification.useful + and item.name in StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT + ] + + self.multiworld.per_slot_randoms[self.player].shuffle(junk) + self.multiworld.per_slot_randoms[self.player].shuffle(usefuls) + self.multiworld.per_slot_randoms[self.player].shuffle(removable_doors) + + removed_junk = False + removed_usefuls = False + removed_doors = False + + for i in range(itempool_difference, 0): + if junk: + pool.remove(junk.pop()) + removed_junk = True + elif f_brain: + pool.remove(f_brain.pop()) + elif usefuls: + pool.remove(usefuls.pop()) + removed_usefuls = True + elif removable_doors: + pool.remove(removable_doors.pop()) + removed_doors = True + + warn = make_warning_string( + removed_junk, removed_usefuls, removed_doors, not junk, not usefuls, not removable_doors + ) + + if warn: + warning(f"This Witness world has too few locations to place all its items." + f" In order to make space, {warn} had to be removed.") + + # Finally, add the generated pool to the overall itempool + self.multiworld.itempool += pool def set_rules(self): set_rules(self.multiworld, self.player, self.player_logic, self.locat) diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index baa6dd45dd..4c36c4826b 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -2,96 +2,96 @@ from BaseClasses import MultiWorld from .Options import is_option_enabled, get_option_value joke_hints = [ - ("Quaternions", "break", "my brain"), - ("Eclipse", "has nothing", "but you should do it anyway"), - ("", "Beep", ""), - ("Putting in custom subtitles", "shouldn't have been", "as hard as it was..."), - ("BK mode", "is right", "around the corner"), - ("", "You can do it!", ""), - ("", "I believe in you!", ""), - ("The person playing", "is", "cute <3"), - ("dash dot, dash dash dash", "dash, dot dot dot dot, dot dot", "dash dot, dash dash dot"), - ("When you think about it,", "there are actually a lot of", "bubbles in a stream"), - ("Never gonna give you up", "Never gonna let you down", "Never gonna run around and desert you"), - ("Thanks to", "the Archipelago developers", "for making this possible."), - ("Have you tried ChecksFinder?", "If you like puzzles,", "you might enjoy it!"), - ("Have you tried Dark Souls III?", "A tough game like this", "feels better when friends are helping you!"), - ("Have you tried Donkey Kong Country 3?", "A legendary game", "from a golden age of platformers!"), - ("Have you tried Factorio?", "Alone in an unknown multiworld.", "Sound familiar?"), - ("Have you tried Final Fantasy?", "Experience a classic game", "improved to fit modern standards!"), - ("Have you tried Hollow Knight?", "Another independent hit", "revolutionising a genre!"), - ("Have you tried A Link to the Past?", "The Archipelago game", "that started it all!"), - ("Have you tried Meritous?", "You should know that obscure games", "are often groundbreaking!"), - ("Have you tried Ocarine of Time?", "One of the biggest randomizers,", "big inspiration for this one's features!"), - ("Have you tried Raft?", "Haven't you always wanted to explore", "the ocean surrounding this island?"), - ("Have you tried Risk of Rain 2?", "I haven't either.", "But I hear it's incredible!"), - ("Have you tried Rogue Legacy?", "After solving so many puzzles", "it's the perfect way to rest your brain."), - ("Have you tried Secret of Evermore?", "I haven't either", "But I hear it's great!"), - ("Have you tried Slay the Spire?", "Experience the thrill of combat", "without needing fast fingers!"), - ("Have you tried SMZ3?", "Why play one incredible game", "when you can play 2 at once?"), - ("Have you tried Starcraft 2?", "Use strategy and management", "to crush your enemies!"), - ("Have you tried Super Mario 64?", "3-dimensional games like this", "owe everything to that game."), - ("Have you tried Super Metroid?", "A classic game", "that started a whole genre."), - ("Have you tried Timespinner?", "Everyone who plays it", "ends up loving it!"), - ("Have you tried VVVVVV?", "Experience the essence of gaming", "distilled into its purest form!"), - ("Have you tried The Witness?", "Oh. I guess you already have.", " Thanks for playing!"), - ("Have you tried Super Mario World?", "I don't think I need to tell you", "that it is beloved by many."), - ("Have you tried Overcooked 2?", "When you're done relaxing with puzzles,", - "use your energy to yell at your friends."), - ("Have you tried Zillion?", "Me neither. But it looks fun.", "So, let's try something new together?"), - ("Have you tried Hylics 2?", "Stop motion might just be", "the epitome of unique art styles."), - ("Have you tried Pokemon Red&Blue?", "A cute pet collecting game", "that fascinated an entire generation."), - ("Waiting to get your items?", "Try BK Sudoku!", "Make progress even while stuck."), - ("One day I was fascinated", "by the subject of", "generation of waves by wind"), - ("I don't like sandwiches", "Why would you think I like sandwiches?", "Have you ever seen me with a sandwich?"), - ("Where are you right now?", "I'm at soup!", "What do you mean you're at soup?"), - ("Remember to ask", "in the Archipelago Discord", "what the Functioning Brain does."), - ("", "Don't use your puzzle skips", "you might need them later"), - ("", "For an extra challenge", "Try playing blindfolded"), - ("Go to the top of the mountain", "and see if you can see", "your house"), - ("Yellow = Red + Green", "Cyan = Green + Blue", "Magenta = Red + Blue"), - ("", "Maybe that panel really is unsolvable", ""), - ("", "Did you make sure it was plugged in?", ""), - ("", "Do not look into laser with remaining eye", ""), - ("", "Try pressing Space to jump", ""), - ("The Witness is a Doom clone.", "Just replace the demons", "with puzzles"), - ("", "Test Hint please ignore", ""), - ("Shapers can never be placed", "outside the panel boundaries", "even if subtracted."), - ("", "The Keep laser panels use", "the same trick on both sides!"), - ("Can't get past a door? Try going around.", "Can't go around? Try building a", "nether portal."), - ("", "We've been trying to reach you", "about your car's extended warranty"), - ("I hate this game. I hate this game.", "I hate this game.", "-chess player Bobby Fischer"), - ("Dear Mario,", "Please come to the castle.", "I've baked a cake for you!"), - ("Have you tried waking up?", "", "Yeah, me neither."), - ("Why do they call it The Witness,", "when wit game the player view", "play of with the game."), - ("", "THE WIND FISH IN NAME ONLY", "FOR IT IS NEITHER"), - ("Like this game? Try The Wit.nes,", "Understand, INSIGHT, Taiji", "What the Witness?, and Tametsi."), - ("", "In a race", "It's survival of the Witnesst"), - ("", "This hint has been removed", "We apologize for your inconvenience."), - ("", "O-----------", ""), - ("Circle is draw", "Square is separate", "Line is win"), - ("Circle is draw", "Star is pair", "Line is win"), - ("Circle is draw", "Circle is copy", "Line is win"), - ("Circle is draw", "Dot is eat", "Line is win"), - ("Circle is start", "Walk is draw", "Line is win"), - ("Circle is start", "Line is win", "Witness is you"), - ("Can't find any items?", "Consider a relaxing boat trip", "around the island"), - ("", "Don't forget to like, comment, and subscribe", ""), - ("Ah crap, gimme a second.", "[papers rustling]", "Sorry, nothing."), - ("", "Trying to get a hint?", "Too bad."), - ("", "Here's a hint:", "Get good at the game."), - ("", "I'm still not entirely sure", "what we're witnessing here."), - ("Have you found a red page yet?", "No?", "Then have you found a blue page?"), - ( - "And here we see the Witness player,", - "seeking answers where there are none-", - "Did someone turn on the loudspeaker?" - ), - ( - "Hints suggested by:", - "IHNN, Beaker, MrPokemon11, Ember, TheM8, NewSoupVi,", - "KF, Yoshi348, Berserker, BowlinJim, oddGarrett, Pink Switch." - ), + "Quaternions break my brain", + "Eclipse has nothing, but you should do it anyway.", + "Beep", + "Putting in custom subtitles shouldn't have been as hard as it was...", + "BK mode is right around the corner.", + "You can do it!", + "I believe in you!", + "The person playing is cute. <3", + "dash dot, dash dash dash, dash, dot dot dot dot, dot dot, dash dot, dash dash dot", + "When you think about it, there are actually a lot of bubbles in a stream.", + "Never gonna give you up\nNever gonna let you down\nNever gonna run around and desert you", + "Thanks to the Archipelago developers for making this possible.", + "Have you tried ChecksFinder?\nIf you like puzzles, you might enjoy it!", + "Have you tried Dark Souls III?\nA tough game like this feels better when friends are helping you!", + "Have you tried Donkey Kong Country 3?\nA legendary game from a golden age of platformers!", + "Have you tried Factorio?\nAlone in an unknown multiworld. Sound familiar?", + "Have you tried Final Fantasy?\nExperience a classic game improved to fit modern standards!", + "Have you tried Hollow Knight?\nAnother independent hit revolutionising a genre!", + "Have you tried A Link to the Past?\nThe Archipelago game that started it all!", + "Have you tried Meritous?\nYou should know that obscure games are often groundbreaking!", + "Have you tried Ocarina of Time?\nOne of the biggest randomizers, big inspiration for this one's features!", + "Have you tried Raft?\nHaven't you always wanted to explore the ocean surrounding this island?", + "Have you tried Risk of Rain 2?\nI haven't either. But I hear it's incredible!", + "Have you tried Rogue Legacy?\nAfter solving so many puzzles it's the perfect way to rest your brain.", + "Have you tried Secret of Evermore?\nI haven't either But I hear it's great!", + "Have you tried Slay the Spire?\nExperience the thrill of combat without needing fast fingers!", + "Have you tried SMZ3?\nWhy play one incredible game when you can play 2 at once?", + "Have you tried Starcraft 2?\nUse strategy and management to crush your enemies!", + "Have you tried Super Mario 64?\n3-dimensional games like this owe everything to that game.", + "Have you tried Super Metroid?\nA classic game, yet still one of the best in the genre.", + "Have you tried Timespinner?\nEveryone who plays it ends up loving it!", + "Have you tried VVVVVV?\nExperience the essence of gaming distilled into its purest form!", + "Have you tried The Witness?\nOh. I guess you already have. Thanks for playing!", + "Have you tried Super Mario World?\nI don't think I need to tell you that it is beloved by many.", + "Have you tried Overcooked 2?\nWhen you're done relaxing with puzzles, use your energy to yell at your friends.", + "Have you tried Zillion?\nMe neither. But it looks fun. So, let's try something new together?", + "Have you tried Hylics 2?\nStop motion might just be the epitome of unique art styles.", + "Have you tried Pokemon Red&Blue?\nA cute pet collecting game that fascinated an entire generation.", + "Have you tried Lufia II?\nRoguelites are not just a 2010s phenomenon, turns out.", + "Have you tried Minecraft?\nI have recently learned this is a question that needs to be asked.", + "Have you tried Subnautica?\nIf you like this game's lonely atmosphere, I would suggest you try it.", + + "Have you tried Sonic Adventure 2?\nIf the silence on this island is getting to you, " + "there aren't many games more energetic.", + + "Waiting to get your items?\nTry BK Sudoku! Make progress even while stuck.", + "One day I was fascinated by the subject of generation of waves by wind.", + "I don't like sandwiches. Why would you think I like sandwiches? Have you ever seen me with a sandwich?", + "Where are you right now?\nI'm at soup!\nWhat do you mean you're at soup?", + "Remember to ask in the Archipelago Discord what the Functioning Brain does.", + "Don't use your puzzle skips, you might need them later.", + "For an extra challenge, try playing blindfolded.", + "Go to the top of the mountain and see if you can see your house.", + "Yellow = Red + Green\nCyan = Green + Blue\nMagenta = Red + Blue", + "Maybe that panel really is unsolvable.", + "Did you make sure it was plugged in?", + "Do not look into laser with remaining eye.", + "Try pressing Space to jump.", + "The Witness is a Doom clone.\nJust replace the demons with puzzles", + "Test Hint please ignore", + "Shapers can never be placed outside the panel boundaries, even if subtracted.", + "The Keep laser panels use the same trick on both sides!", + "Can't get past a door? Try going around. Can't go around? Try building a nether portal.", + "We've been trying to reach you about your car's extended warranty.", + "I hate this game. I hate this game. I hate this game.\n- Chess player Bobby Fischer", + "Dear Mario,\nPlease come to the castle. I've baked a cake for you!", + "Have you tried waking up?\nYeah, me neither.", + "Why do they call it The Witness, when wit game the player view play of with the game.", + "THE WIND FISH IN NAME ONLY, FOR IT IS NEITHER", + "Like this game?\nTry The Wit.nes, Understand, INSIGHT, Taiji What the Witness?, and Tametsi.", + "In a race, It's survival of the Witnesst.", + "This hint has been removed. We apologize for your inconvenience.", + "O-----------", + "Circle is draw\nSquare is separate\nLine is win", + "Circle is draw\nStar is pair\nLine is win", + "Circle is draw\nCircle is copy\nLine is win", + "Circle is draw\nDot is eat\nLine is win", + "Circle is start\nWalk is draw\nLine is win", + "Circle is start\nLine is win\nWitness is you", + "Can't find any items?\nConsider a relaxing boat trip around the island!", + "Don't forget to like, comment, and subscribe.", + "Ah crap, gimme a second.\n[papers rustling]\nSorry, nothing.", + "Trying to get a hint? Too bad.", + "Here's a hint: Get good at the game.", + "I'm still not entirely sure what we're witnessing here.", + "Have you found a red page yet? No? Then have you found a blue page?", + "And here we see the Witness player, seeking answers where there are none-\nDid someone turn on the loudspeaker?", + + "Hints suggested by:\nIHNN, Beaker, MrPokemon11, Ember, TheM8, NewSoupVi," + "KF, Yoshi348, Berserker, BowlinJim, oddGarrett, Pink Switch.", ] @@ -186,7 +186,7 @@ def make_hint_from_item(multiworld: MultiWorld, player: int, item: str): if location_obj.player != player: location_name += " (" + multiworld.get_player_name(location_obj.player) + ")" - return location_name, item, location_obj.address if(location_obj.player == player) else -1 + return location_name, item, location_obj.address if (location_obj.player == player) else -1 def make_hint_from_location(multiworld: MultiWorld, player: int, location: str): @@ -196,7 +196,7 @@ def make_hint_from_location(multiworld: MultiWorld, player: int, location: str): if item_obj.player != player: item_name += " (" + multiworld.get_player_name(item_obj.player) + ")" - return location, item_name, location_obj.address if(location_obj.player == player) else -1 + return location, item_name, location_obj.address if (location_obj.player == player) else -1 def make_hints(multiworld: MultiWorld, player: int, hint_amount: int): @@ -258,9 +258,9 @@ def make_hints(multiworld: MultiWorld, player: int, hint_amount: int): for loc, item in always_hint_pairs.items(): if item[1]: - hints.append((item[0], "can be found at", loc, item[2])) + hints.append((f"{item[0]} can be found at {loc}.", item[2])) else: - hints.append((loc, "contains", item[0], item[2])) + hints.append((f"{loc} contains {item[0]}.", item[2])) multiworld.per_slot_randoms[player].shuffle(hints) # shuffle always hint order in case of low hint amount @@ -279,9 +279,9 @@ def make_hints(multiworld: MultiWorld, player: int, hint_amount: int): del priority_hint_pairs[loc] if item[1]: - hints.append((item[0], "can be found at", loc, item[2])) + hints.append((f"{item[0]} can be found at {loc}.", item[2])) else: - hints.append((loc, "contains", item[0], item[2])) + hints.append((f"{loc} contains {item[0]}.", item[2])) continue if next_random_hint_is_item: @@ -290,10 +290,10 @@ def make_hints(multiworld: MultiWorld, player: int, hint_amount: int): continue hint = make_hint_from_item(multiworld, player, prog_items_in_this_world.pop()) - hints.append((hint[1], "can be found at", hint[0], hint[2])) + hints.append((f"{hint[1]} can be found at {hint[0]}.", hint[2])) else: hint = make_hint_from_location(multiworld, player, locations_in_this_world.pop()) - hints.append((hint[0], "contains", hint[1], hint[2])) + hints.append((f"{hint[0]} contains {hint[1]}.", hint[2])) next_random_hint_is_item = not next_random_hint_is_item @@ -301,4 +301,4 @@ def make_hints(multiworld: MultiWorld, player: int, hint_amount: int): def generate_joke_hints(multiworld: MultiWorld, player: int, amount: int): - return [(x, y, z, -1) for (x, y, z) in multiworld.per_slot_randoms[player].sample(joke_hints, amount)] + return [(x, -1) for x in multiworld.per_slot_randoms[player].sample(joke_hints, amount)] diff --git a/worlds/witness/items.py b/worlds/witness/items.py index 914c1af2c0..4beb3b0290 100644 --- a/worlds/witness/items.py +++ b/worlds/witness/items.py @@ -205,13 +205,10 @@ class WitnessPlayerItems: ] if is_option_enabled(multiworld, player, "shuffle_discarded_panels"): - if is_option_enabled(multiworld, player, "shuffle_discarded_panels"): - if get_option_value(multiworld, player, "puzzle_randomization") == 1: - self.GOOD_ITEMS.append("Arrows") - else: - self.GOOD_ITEMS.append("Triangles") - if not is_option_enabled(multiworld, player, "disable_non_randomized_puzzles"): - self.GOOD_ITEMS.append("Colored Squares") + if get_option_value(multiworld, player, "puzzle_randomization") == 1: + self.GOOD_ITEMS.append("Arrows") + else: + self.GOOD_ITEMS.append("Triangles") self.GOOD_ITEMS = [ StaticWitnessLogic.ITEMS_TO_PROGRESSIVE.get(item, item) for item in self.GOOD_ITEMS diff --git a/worlds/witness/locations.py b/worlds/witness/locations.py index 37f61646d1..ccdeda1b56 100644 --- a/worlds/witness/locations.py +++ b/worlds/witness/locations.py @@ -13,13 +13,10 @@ class StaticWitnessLocations: """ ID_START = 158000 - EXTRA_LOCATIONS = { + GENERAL_LOCATIONS = { "Tutorial Front Left", "Tutorial Back Left", "Tutorial Back Right", - } - - GENERAL_LOCATIONS = { "Tutorial Gate Open", "Outside Tutorial Vault Box", @@ -302,6 +299,7 @@ class StaticWitnessLocations: "Quarry Obelisk Side 2", "Quarry Obelisk Side 3", "Quarry Obelisk Side 4", + "Quarry Obelisk Side 5", "Town Obelisk Side 1", "Town Obelisk Side 2", "Town Obelisk Side 3", @@ -338,6 +336,7 @@ class StaticWitnessLocations: "Quarry Obelisk Side 2", "Quarry Obelisk Side 3", "Quarry Obelisk Side 4", + "Quarry Obelisk Side 5", "Town Obelisk Side 1", "Town Obelisk Side 2", "Town Obelisk Side 3", diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index 34b53b62ff..eb5b7934db 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -433,7 +433,6 @@ class WitnessPlayerLogic: self.ADDED_CHECKS = set() self.VICTORY_LOCATION = "0x0356B" self.EVENT_ITEM_NAMES = { - "0x01A0F": "Keep Laser Panel (Hedge Mazes) Activates", "0x09D9B": "Monastery Shutters Open", "0x193A6": "Monastery Laser Panel Activates", "0x00037": "Monastery Branch Panels Activate", @@ -442,8 +441,11 @@ class WitnessPlayerLogic: "0x00139": "Keep Hedges 1 Knowledge", "0x019DC": "Keep Hedges 2 Knowledge", "0x019E7": "Keep Hedges 3 Knowledge", - "0x01D3F": "Keep Laser Panel (Pressure Plates) Activates", - "0x01BE9": "Keep Laser Panel (Pressure Plates) Activates - Expert", + "0x01A0F": "Keep Hedges 4 Knowledge", + "0x033EA": "Pressure Plates 1 Knowledge", + "0x01BE9": "Pressure Plates 2 Knowledge", + "0x01CD3": "Pressure Plates 3 Knowledge", + "0x01D3F": "Pressure Plates 4 Knowledge", "0x09F7F": "Mountain Access", "0x0367C": "Quarry Laser Stoneworks Requirement Met", "0x009A1": "Swamp Between Bridges Far 1 Activates", @@ -492,11 +494,9 @@ class WitnessPlayerLogic: "0x17D02": "Windmill Blades Spinning", "0x0A0C9": "Cargo Box EP completable", "0x09E39": "Pink Light Bridge Extended", - "0x01CD3": "Pressure Plates 3 EP available", "0x17CC4": "Rails EP available", "0x2896A": "Bridge Underside EP available", "0x00064": "First Tunnel EP visible", - "0x033EA": "Pressure Plates 1 EP available", "0x03553": "Tutorial Video EPs availble", "0x17C79": "Bunker Door EP available", "0x275FF": "Stoneworks Light EPs available", diff --git a/worlds/witness/regions.py b/worlds/witness/regions.py index 69b0317d85..0e15cafe10 100644 --- a/worlds/witness/regions.py +++ b/worlds/witness/regions.py @@ -27,20 +27,18 @@ class WitnessRegions: ) def connect(self, world: MultiWorld, player: int, source: str, target: str, player_logic: WitnessPlayerLogic, - panel_hex_to_solve_set=frozenset({frozenset()})): + panel_hex_to_solve_set=frozenset({frozenset()}), backwards: bool = False): """ connect two regions and set the corresponding requirement """ source_region = world.get_region(source, player) target_region = world.get_region(target, player) - #print(source_region) - #print(target_region) - #print("---") + backwards = " Backwards" if backwards else "" connection = Entrance( player, - source + " to " + target, + source + " to " + target + backwards, source_region ) @@ -92,10 +90,18 @@ class WitnessRegions: self.connect(world, player, region_name, connection[0], player_logic, frozenset({frozenset()})) continue + backwards_connections = set() + for subset in connection[1]: if all({panel in player_logic.DOOR_ITEMS_BY_ID for panel in subset}): if all({reference_logic.CHECKS_BY_HEX[panel]["id"] is None for panel in subset}): - self.connect(world, player, connection[0], region_name, player_logic, frozenset({subset})) + backwards_connections.add(subset) + + if backwards_connections: + self.connect( + world, player, connection[0], region_name, player_logic, + frozenset(backwards_connections), True + ) self.connect(world, player, region_name, connection[0], player_logic, connection[1]) diff --git a/worlds/witness/settings/Disable_Unrandomized.txt b/worlds/witness/settings/Disable_Unrandomized.txt index 8f6034ccb9..dbe9caa5be 100644 --- a/worlds/witness/settings/Disable_Unrandomized.txt +++ b/worlds/witness/settings/Disable_Unrandomized.txt @@ -114,15 +114,17 @@ Disabled Locations: 0x17CAA (River Garden Entry Panel) -0x034A7 (Left Shutter EP) -0x034AD (Middle Shutter EP) -0x034AF (Right Shutter EP) -0x339B6 (Eclipse EP) - 0x03549 - True -0x33A29 (Window EP) - 0x03553 - True -0x33A2A (Door EP) - 0x03553 - True -0x33B06 (Church EP) - 0x0354E - True -0x3352F (Gate EP) -0x33600 (Patio Flowers EP) -0x035F5 (Tinted Door EP) -0x000D3 (Green Room Flowers EP) -0x33A20 (Theater Flowers EP) \ No newline at end of file +Precompleted Locations: +0x034A7 +0x034AD +0x034AF +0x339B6 +0x33A29 +0x33A2A +0x33B06 +0x3352F +0x33600 +0x035F5 +0x000D3 +0x33A20 +0x03BE2 \ No newline at end of file diff --git a/worlds/witness/settings/EP_Shuffle/EP_Easy.txt b/worlds/witness/settings/EP_Shuffle/EP_Easy.txt index 3e168b5891..939055169a 100644 --- a/worlds/witness/settings/EP_Shuffle/EP_Easy.txt +++ b/worlds/witness/settings/EP_Shuffle/EP_Easy.txt @@ -9,4 +9,6 @@ Precompleted Locations: 0x33857 0x33879 0x016B2 -0x036CE \ No newline at end of file +0x036CE +0x03B25 +0x28B2A \ No newline at end of file diff --git a/worlds/witness/utils.py b/worlds/witness/utils.py index dcb335edd9..7182545cf5 100644 --- a/worlds/witness/utils.py +++ b/worlds/witness/utils.py @@ -3,6 +3,42 @@ from Utils import cache_argsless from itertools import accumulate from typing import * from fractions import Fraction +from collections import Counter + + +def make_warning_string(any_j: bool, any_u: bool, any_d: bool, all_j: bool, all_u: bool, all_d: bool) -> str: + warning_string = "" + + if any_j: + if all_j: + warning_string += "all " + else: + warning_string += "some " + + warning_string += "junk" + + if any_u or any_d: + if warning_string: + warning_string += " and " + + if all_u: + warning_string += "all " + else: + warning_string += "some " + + warning_string += "usefuls" + + if any_d: + warning_string += ", including " + + if all_d: + warning_string += "all " + else: + warning_string += "some " + + warning_string += "non-essential door items" + + return warning_string def best_junk_to_add_based_on_weights(weights: Dict[Any, Fraction], created_junk: Dict[Any, int]): From 21fb16291d53b179aabfeacd52657a627df2f101 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Thu, 2 Mar 2023 17:30:40 -0600 Subject: [PATCH 011/172] Tests: test that the game is beatable for WorldTestBases (#1495) * Tests: test that the game is beatable for WorldTestBases * update docstring * don't test the bases with default options for real this time * invert the property so worlds can use it easier * setup check should be or * test class needs to always be constructed * skip default tests before multiworld setup * check if the calling method is in the base's __dict__ * shorter property and functional setup skipping * shorter property and functional setup skipping --- test/TestBase.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/test/TestBase.py b/test/TestBase.py index fb8031a900..a2c9bc28aa 100644 --- a/test/TestBase.py +++ b/test/TestBase.py @@ -112,7 +112,11 @@ class WorldTestBase(unittest.TestCase): self.world_setup() def world_setup(self, seed: typing.Optional[int] = None) -> None: - if type(self) is WorldTestBase: + if type(self) is WorldTestBase or \ + (hasattr(WorldTestBase, self._testMethodName) + and not self.run_default_tests and + getattr(self, self._testMethodName).__code__ is + getattr(WorldTestBase, self._testMethodName, None).__code__): return # setUp gets called for tests defined in the base class. We skip world_setup here. if not hasattr(self, "game"): raise NotImplementedError("didn't define game name") @@ -208,16 +212,20 @@ class WorldTestBase(unittest.TestCase): # following tests are automatically run @property - def skip_default_tests(self) -> bool: + def run_default_tests(self) -> bool: """Not possible or identical to the base test that's always being run already""" - constructed = hasattr(self, "game") and hasattr(self, "multiworld") - return not constructed or (not self.options - and self.setUp is WorldTestBase.setUp - and self.world_setup is WorldTestBase.world_setup) + return (self.options + or self.setUp.__code__ is not WorldTestBase.setUp.__code__ + or self.world_setup.__code__ is not WorldTestBase.world_setup.__code__) + + @property + def constructed(self) -> bool: + """A multiworld has been constructed by this point""" + return hasattr(self, "game") and hasattr(self, "multiworld") def testAllStateCanReachEverything(self): - """Ensure all state can reach everything with the defined options""" - if self.skip_default_tests: + """Ensure all state can reach everything and complete the game with the defined options""" + if not (self.run_default_tests and self.constructed): return with self.subTest("Game", game=self.game): excluded = self.multiworld.exclude_locations[1].value @@ -226,10 +234,13 @@ class WorldTestBase(unittest.TestCase): if location.name not in excluded: with self.subTest("Location should be reached", location=location): self.assertTrue(location.can_reach(state), f"{location.name} unreachable") + with self.subTest("Beatable"): + self.multiworld.state = state + self.assertBeatable(True) def testEmptyStateCanReachSomething(self): """Ensure empty state can reach at least one location with the defined options""" - if self.skip_default_tests: + if not (self.run_default_tests and self.constructed): return with self.subTest("Game", game=self.game): state = CollectionState(self.multiworld) From 4ea582f14ef423b5ffee8446ded1c571cf72125d Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 3 Mar 2023 09:51:36 +0100 Subject: [PATCH 012/172] Windows: allow arm64 setup (#1496) --- inno_setup.iss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inno_setup.iss b/inno_setup.iss index f7748ced02..85dd49c62c 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -25,9 +25,9 @@ OutputBaseFilename=Setup {#MyAppName} {#MyAppVersionText} Compression=lzma2 SolidCompression=yes LZMANumBlockThreads=8 -ArchitecturesInstallIn64BitMode=x64 +ArchitecturesInstallIn64BitMode=x64 arm64 ChangesAssociations=yes -ArchitecturesAllowed=x64 +ArchitecturesAllowed=x64 arm64 AllowNoIcons=yes SetupIconFile={#MyAppIcon} UninstallDisplayIcon={app}\{#MyAppExeName} From 798d8233972da4ecce3aedb43d48d753e218b3dd Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Fri, 3 Mar 2023 11:22:46 -0500 Subject: [PATCH 013/172] Core: Check for show_in_spoiler (#1500) --- BaseClasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index f91b81bdff..5fa3b76988 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1464,7 +1464,7 @@ class Spoiler(): AutoWorld.call_all(self.multiworld, "write_spoiler", outfile) locations = [(str(location), str(location.item) if location.item is not None else "Nothing") - for location in self.multiworld.get_locations()] + for location in self.multiworld.get_locations() if location.show_in_spoiler] outfile.write('\n\nLocations:\n\n') outfile.write('\n'.join( ['%s: %s' % (location, item) for location, item in locations])) From 3a926849a02c12b44d19472a0e81b2216ebe2ee0 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Fri, 3 Mar 2023 09:43:25 +0100 Subject: [PATCH 014/172] CI: run unittests on macos this is to ensure dependencies can be installed and loaded on macos (on AMD64) --- .github/workflows/unittests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index c86d637243..fe61c8b0f0 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -23,6 +23,8 @@ jobs: os: windows-latest - python: {version: '3.10'} # current os: windows-latest + - python: {version: '3.10'} # current + os: macos-latest steps: - uses: actions/checkout@v2 From 9fa1f4e85fe4f01c44c14add1a885351f22e8867 Mon Sep 17 00:00:00 2001 From: FlySniper Date: Fri, 3 Mar 2023 12:24:09 -0500 Subject: [PATCH 015/172] Wargroove: Fixed Wargroove Client not removing communication files (#1492) --- WargrooveClient.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/WargrooveClient.py b/WargrooveClient.py index fec20cc861..16bfeb15ab 100644 --- a/WargrooveClient.py +++ b/WargrooveClient.py @@ -1,4 +1,6 @@ from __future__ import annotations + +import atexit import os import sys import asyncio @@ -78,17 +80,18 @@ class WargrooveContext(CommonContext): # self.game_communication_path: files go in this path to pass data between us and the actual game if "appdata" in os.environ: options = Utils.get_options() - root_directory = options["wargroove_options"]["root_directory"].replace("/", "\\") - data_directory = "lib\\worlds\\wargroove\\data\\" - dev_data_directory = "worlds\\wargroove\\data\\" - appdata_wargroove = os.path.expandvars("%APPDATA%\\Chucklefish\\Wargroove\\") - if not os.path.isfile(root_directory + "\\win64_bin\\wargroove64.exe"): + root_directory = os.path.join(options["wargroove_options"]["root_directory"]) + data_directory = os.path.join("lib", "worlds", "wargroove", "data") + dev_data_directory = os.path.join("worlds", "wargroove", "data") + appdata_wargroove = os.path.expandvars(os.path.join("%APPDATA%", "Chucklefish", "Wargroove")) + if not os.path.isfile(os.path.join(root_directory, "win64_bin", "wargroove64.exe")): print_error_and_close("WargrooveClient couldn't find wargroove64.exe. " "Unable to infer required game_communication_path") - self.game_communication_path = root_directory + "\\AP" + self.game_communication_path = os.path.join(root_directory, "AP") if not os.path.exists(self.game_communication_path): os.makedirs(self.game_communication_path) - + self.remove_communication_files() + atexit.register(self.remove_communication_files) if not os.path.isdir(appdata_wargroove): print_error_and_close("WargrooveClient couldn't find Wargoove in appdata!" "Boot Wargroove and then close it to attempt to fix this error") @@ -109,10 +112,7 @@ class WargrooveContext(CommonContext): async def connection_closed(self): await super(WargrooveContext, self).connection_closed() - for root, dirs, files in os.walk(self.game_communication_path): - for file in files: - if file.find("obtain") <= -1: - os.remove(root + "/" + file) + self.remove_communication_files() @property def endpoints(self): @@ -123,10 +123,12 @@ class WargrooveContext(CommonContext): async def shutdown(self): await super(WargrooveContext, self).shutdown() + self.remove_communication_files() + + def remove_communication_files(self): for root, dirs, files in os.walk(self.game_communication_path): for file in files: - if file.find("obtain") <= -1: - os.remove(root+"/"+file) + os.remove(root + "/" + file) def on_package(self, cmd: str, args: dict): if cmd in {"Connected"}: From a4b61118cfb6f49dc4cd0283013243503cf5c141 Mon Sep 17 00:00:00 2001 From: Jarno Date: Sat, 4 Mar 2023 08:16:05 +0100 Subject: [PATCH 016/172] Timespinner: Refactorings + fix for #1460 (#1484) --- worlds/timespinner/Locations.py | 2 +- worlds/timespinner/Regions.py | 351 ++++++++++++++++---------------- worlds/timespinner/__init__.py | 291 ++++++++++++-------------- 3 files changed, 305 insertions(+), 339 deletions(-) diff --git a/worlds/timespinner/Locations.py b/worlds/timespinner/Locations.py index 960444acb8..03f1c025dc 100644 --- a/worlds/timespinner/Locations.py +++ b/worlds/timespinner/Locations.py @@ -14,7 +14,7 @@ class LocationData(NamedTuple): rule: Callable[[CollectionState], bool] = lambda state: True -def get_locations(world: Optional[MultiWorld], player: Optional[int], +def get_location_datas(world: Optional[MultiWorld], player: Optional[int], precalculated_weights: PreCalculatedWeights) -> Tuple[LocationData, ...]: flooded: PreCalculatedWeights = precalculated_weights diff --git a/worlds/timespinner/Regions.py b/worlds/timespinner/Regions.py index ab8ee97ac6..002606245d 100644 --- a/worlds/timespinner/Regions.py +++ b/worlds/timespinner/Regions.py @@ -1,64 +1,64 @@ from typing import List, Set, Dict, Tuple, Optional, Callable from BaseClasses import CollectionState, MultiWorld, Region, Entrance, Location from .Options import is_option_enabled -from .Locations import LocationData +from .Locations import LocationData, get_location_datas from .PreCalculatedWeights import PreCalculatedWeights from .LogicExtensions import TimespinnerLogic -def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location], - precalculated_weights: PreCalculatedWeights): +def create_regions_and_locations(world: MultiWorld, player: int, precalculated_weights: PreCalculatedWeights): + locationn_datas: Tuple[LocationData] = get_location_datas(world, player, precalculated_weights) - locations_per_region = get_locations_per_region(locations) + locations_per_region: Dict[str, List[LocationData]] = split_location_datas_per_region(locationn_datas) regions = [ - create_region(world, player, locations_per_region, location_cache, 'Menu'), - create_region(world, player, locations_per_region, location_cache, 'Tutorial'), - create_region(world, player, locations_per_region, location_cache, 'Lake desolation'), - create_region(world, player, locations_per_region, location_cache, 'Upper lake desolation'), - create_region(world, player, locations_per_region, location_cache, 'Lower lake desolation'), - create_region(world, player, locations_per_region, location_cache, 'Eastern lake desolation'), - create_region(world, player, locations_per_region, location_cache, 'Library'), - create_region(world, player, locations_per_region, location_cache, 'Library top'), - create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower left'), - create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower right (upper)'), - create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower right (lower)'), - create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower right (elevator)'), - create_region(world, player, locations_per_region, location_cache, 'Sealed Caves (Sirens)'), - create_region(world, player, locations_per_region, location_cache, 'Military Fortress'), - create_region(world, player, locations_per_region, location_cache, 'Military Fortress (hangar)'), - create_region(world, player, locations_per_region, location_cache, 'The lab'), - create_region(world, player, locations_per_region, location_cache, 'The lab (power off)'), - create_region(world, player, locations_per_region, location_cache, 'The lab (upper)'), - create_region(world, player, locations_per_region, location_cache, 'Emperors tower'), - create_region(world, player, locations_per_region, location_cache, 'Skeleton Shaft'), - create_region(world, player, locations_per_region, location_cache, 'Sealed Caves (upper)'), - create_region(world, player, locations_per_region, location_cache, 'Sealed Caves (Xarion)'), - create_region(world, player, locations_per_region, location_cache, 'Refugee Camp'), - create_region(world, player, locations_per_region, location_cache, 'Forest'), - create_region(world, player, locations_per_region, location_cache, 'Left Side forest Caves'), - create_region(world, player, locations_per_region, location_cache, 'Upper Lake Serene'), - create_region(world, player, locations_per_region, location_cache, 'Lower Lake Serene'), - create_region(world, player, locations_per_region, location_cache, 'Caves of Banishment (upper)'), - create_region(world, player, locations_per_region, location_cache, 'Caves of Banishment (Maw)'), - create_region(world, player, locations_per_region, location_cache, 'Caves of Banishment (Sirens)'), - create_region(world, player, locations_per_region, location_cache, 'Castle Ramparts'), - create_region(world, player, locations_per_region, location_cache, 'Castle Keep'), - create_region(world, player, locations_per_region, location_cache, 'Castle Basement'), - create_region(world, player, locations_per_region, location_cache, 'Royal towers (lower)'), - create_region(world, player, locations_per_region, location_cache, 'Royal towers'), - create_region(world, player, locations_per_region, location_cache, 'Royal towers (upper)'), - create_region(world, player, locations_per_region, location_cache, 'Temporal Gyre'), - create_region(world, player, locations_per_region, location_cache, 'Ancient Pyramid (entrance)'), - create_region(world, player, locations_per_region, location_cache, 'Ancient Pyramid (left)'), - create_region(world, player, locations_per_region, location_cache, 'Ancient Pyramid (right)'), - create_region(world, player, locations_per_region, location_cache, 'Space time continuum') + create_region(world, player, locations_per_region, 'Menu'), + create_region(world, player, locations_per_region, 'Tutorial'), + create_region(world, player, locations_per_region, 'Lake desolation'), + create_region(world, player, locations_per_region, 'Upper lake desolation'), + create_region(world, player, locations_per_region, 'Lower lake desolation'), + create_region(world, player, locations_per_region, 'Eastern lake desolation'), + create_region(world, player, locations_per_region, 'Library'), + create_region(world, player, locations_per_region, 'Library top'), + create_region(world, player, locations_per_region, 'Varndagroth tower left'), + create_region(world, player, locations_per_region, 'Varndagroth tower right (upper)'), + create_region(world, player, locations_per_region, 'Varndagroth tower right (lower)'), + create_region(world, player, locations_per_region, 'Varndagroth tower right (elevator)'), + create_region(world, player, locations_per_region, 'Sealed Caves (Sirens)'), + create_region(world, player, locations_per_region, 'Military Fortress'), + create_region(world, player, locations_per_region, 'Military Fortress (hangar)'), + create_region(world, player, locations_per_region, 'The lab'), + create_region(world, player, locations_per_region, 'The lab (power off)'), + create_region(world, player, locations_per_region, 'The lab (upper)'), + create_region(world, player, locations_per_region, 'Emperors tower'), + create_region(world, player, locations_per_region, 'Skeleton Shaft'), + create_region(world, player, locations_per_region, 'Sealed Caves (upper)'), + create_region(world, player, locations_per_region, 'Sealed Caves (Xarion)'), + create_region(world, player, locations_per_region, 'Refugee Camp'), + create_region(world, player, locations_per_region, 'Forest'), + create_region(world, player, locations_per_region, 'Left Side forest Caves'), + create_region(world, player, locations_per_region, 'Upper Lake Serene'), + create_region(world, player, locations_per_region, 'Lower Lake Serene'), + create_region(world, player, locations_per_region, 'Caves of Banishment (upper)'), + create_region(world, player, locations_per_region, 'Caves of Banishment (Maw)'), + create_region(world, player, locations_per_region, 'Caves of Banishment (Sirens)'), + create_region(world, player, locations_per_region, 'Castle Ramparts'), + create_region(world, player, locations_per_region, 'Castle Keep'), + create_region(world, player, locations_per_region, 'Castle Basement'), + create_region(world, player, locations_per_region, 'Royal towers (lower)'), + create_region(world, player, locations_per_region, 'Royal towers'), + create_region(world, player, locations_per_region, 'Royal towers (upper)'), + create_region(world, player, locations_per_region, 'Temporal Gyre'), + create_region(world, player, locations_per_region, 'Ancient Pyramid (entrance)'), + create_region(world, player, locations_per_region, 'Ancient Pyramid (left)'), + create_region(world, player, locations_per_region, 'Ancient Pyramid (right)'), + create_region(world, player, locations_per_region, 'Space time continuum') ] if is_option_enabled(world, player, "GyreArchives"): regions.extend([ - create_region(world, player, locations_per_region, location_cache, 'Ravenlord\'s Lair'), - create_region(world, player, locations_per_region, location_cache, 'Ifrit\'s Lair'), + create_region(world, player, locations_per_region, 'Ravenlord\'s Lair'), + create_region(world, player, locations_per_region, 'Ifrit\'s Lair'), ]) if __debug__: @@ -70,127 +70,126 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData flooded: PreCalculatedWeights = precalculated_weights logic = TimespinnerLogic(world, player, precalculated_weights) - names: Dict[str, int] = {} - connect(world, player, names, 'Lake desolation', 'Lower lake desolation', lambda state: logic.has_timestop(state) or state.has('Talaria Attachment', player) or flooded.flood_lake_desolation) - connect(world, player, names, 'Lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player)) - connect(world, player, names, 'Lake desolation', 'Skeleton Shaft', lambda state: logic.has_doublejump(state) or flooded.flood_lake_desolation) - connect(world, player, names, 'Lake desolation', 'Space time continuum', logic.has_teleport) - connect(world, player, names, 'Upper lake desolation', 'Lake desolation') - connect(world, player, names, 'Upper lake desolation', 'Eastern lake desolation') - connect(world, player, names, 'Lower lake desolation', 'Lake desolation') - connect(world, player, names, 'Lower lake desolation', 'Eastern lake desolation') - connect(world, player, names, 'Eastern lake desolation', 'Space time continuum', logic.has_teleport) - connect(world, player, names, 'Eastern lake desolation', 'Library') - connect(world, player, names, 'Eastern lake desolation', 'Lower lake desolation') - connect(world, player, names, 'Eastern lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player)) - connect(world, player, names, 'Library', 'Eastern lake desolation') - connect(world, player, names, 'Library', 'Library top', lambda state: logic.has_doublejump(state) or state.has('Talaria Attachment', player)) - connect(world, player, names, 'Library', 'Varndagroth tower left', logic.has_keycard_D) - connect(world, player, names, 'Library', 'Space time continuum', logic.has_teleport) - connect(world, player, names, 'Library top', 'Library') - connect(world, player, names, 'Varndagroth tower left', 'Library') - connect(world, player, names, 'Varndagroth tower left', 'Varndagroth tower right (upper)', logic.has_keycard_C) - connect(world, player, names, 'Varndagroth tower left', 'Varndagroth tower right (lower)', logic.has_keycard_B) - connect(world, player, names, 'Varndagroth tower left', 'Sealed Caves (Sirens)', lambda state: logic.has_keycard_B(state) and state.has('Elevator Keycard', player)) - connect(world, player, names, 'Varndagroth tower left', 'Refugee Camp', lambda state: state.has('Timespinner Wheel', player) and state.has('Timespinner Spindle', player)) - connect(world, player, names, 'Varndagroth tower right (upper)', 'Varndagroth tower left') - connect(world, player, names, 'Varndagroth tower right (upper)', 'Varndagroth tower right (elevator)', lambda state: state.has('Elevator Keycard', player)) - connect(world, player, names, 'Varndagroth tower right (elevator)', 'Varndagroth tower right (upper)') - connect(world, player, names, 'Varndagroth tower right (elevator)', 'Varndagroth tower right (lower)') - connect(world, player, names, 'Varndagroth tower right (lower)', 'Varndagroth tower left', logic.has_keycard_B) - connect(world, player, names, 'Varndagroth tower right (lower)', 'Varndagroth tower right (elevator)', lambda state: state.has('Elevator Keycard', player)) - connect(world, player, names, 'Varndagroth tower right (lower)', 'Sealed Caves (Sirens)', lambda state: logic.has_keycard_B(state) and state.has('Elevator Keycard', player)) - connect(world, player, names, 'Varndagroth tower right (lower)', 'Military Fortress', logic.can_kill_all_3_bosses) - connect(world, player, names, 'Varndagroth tower right (lower)', 'Space time continuum', logic.has_teleport) - connect(world, player, names, 'Sealed Caves (Sirens)', 'Varndagroth tower left', lambda state: state.has('Elevator Keycard', player)) - connect(world, player, names, 'Sealed Caves (Sirens)', 'Varndagroth tower right (lower)', lambda state: state.has('Elevator Keycard', player)) - connect(world, player, names, 'Sealed Caves (Sirens)', 'Space time continuum', logic.has_teleport) - connect(world, player, names, 'Military Fortress', 'Varndagroth tower right (lower)', logic.can_kill_all_3_bosses) - connect(world, player, names, 'Military Fortress', 'Temporal Gyre', lambda state: state.has('Timespinner Wheel', player)) - connect(world, player, names, 'Military Fortress', 'Military Fortress (hangar)', logic.has_doublejump) - connect(world, player, names, 'Military Fortress (hangar)', 'Military Fortress') - connect(world, player, names, 'Military Fortress (hangar)', 'The lab', lambda state: logic.has_keycard_B(state) and logic.has_doublejump(state)) - connect(world, player, names, 'Temporal Gyre', 'Military Fortress') - connect(world, player, names, 'The lab', 'Military Fortress') - connect(world, player, names, 'The lab', 'The lab (power off)', logic.has_doublejump_of_npc) - connect(world, player, names, 'The lab (power off)', 'The lab') - connect(world, player, names, 'The lab (power off)', 'The lab (upper)', logic.has_forwarddash_doublejump) - connect(world, player, names, 'The lab (upper)', 'The lab (power off)') - connect(world, player, names, 'The lab (upper)', 'Emperors tower', logic.has_forwarddash_doublejump) - connect(world, player, names, 'The lab (upper)', 'Ancient Pyramid (entrance)', lambda state: state.has_all({'Timespinner Wheel', 'Timespinner Spindle', 'Timespinner Gear 1', 'Timespinner Gear 2', 'Timespinner Gear 3'}, player)) - connect(world, player, names, 'Emperors tower', 'The lab (upper)') - connect(world, player, names, 'Skeleton Shaft', 'Lake desolation') - connect(world, player, names, 'Skeleton Shaft', 'Sealed Caves (upper)', logic.has_keycard_A) - connect(world, player, names, 'Skeleton Shaft', 'Space time continuum', logic.has_teleport) - connect(world, player, names, 'Sealed Caves (upper)', 'Skeleton Shaft') - connect(world, player, names, 'Sealed Caves (upper)', 'Sealed Caves (Xarion)', lambda state: logic.has_teleport(state) or logic.has_doublejump(state)) - connect(world, player, names, 'Sealed Caves (Xarion)', 'Sealed Caves (upper)', logic.has_doublejump) - connect(world, player, names, 'Sealed Caves (Xarion)', 'Space time continuum', logic.has_teleport) - connect(world, player, names, 'Refugee Camp', 'Forest') - #connect(world, player, names, 'Refugee Camp', 'Library', lambda state: not is_option_enabled(world, player, "Inverted")) - connect(world, player, names, 'Refugee Camp', 'Space time continuum', logic.has_teleport) - connect(world, player, names, 'Forest', 'Refugee Camp') - connect(world, player, names, 'Forest', 'Left Side forest Caves', lambda state: state.has('Talaria Attachment', player) or logic.has_timestop(state)) - connect(world, player, names, 'Forest', 'Caves of Banishment (Sirens)') - connect(world, player, names, 'Forest', 'Castle Ramparts') - connect(world, player, names, 'Left Side forest Caves', 'Forest') - connect(world, player, names, 'Left Side forest Caves', 'Upper Lake Serene', logic.has_timestop) - connect(world, player, names, 'Left Side forest Caves', 'Lower Lake Serene', lambda state: state.has('Water Mask', player) or flooded.dry_lake_serene) - connect(world, player, names, 'Left Side forest Caves', 'Space time continuum', logic.has_teleport) - connect(world, player, names, 'Upper Lake Serene', 'Left Side forest Caves') - connect(world, player, names, 'Upper Lake Serene', 'Lower Lake Serene', lambda state: state.has('Water Mask', player)) - connect(world, player, names, 'Lower Lake Serene', 'Upper Lake Serene') - connect(world, player, names, 'Lower Lake Serene', 'Left Side forest Caves') - connect(world, player, names, 'Lower Lake Serene', 'Caves of Banishment (upper)') - connect(world, player, names, 'Caves of Banishment (upper)', 'Upper Lake Serene', lambda state: state.has('Water Mask', player) or flooded.dry_lake_serene) - connect(world, player, names, 'Caves of Banishment (upper)', 'Caves of Banishment (Maw)', lambda state: logic.has_doublejump(state) or state.has_any({'Gas Mask', 'Twin Pyramid Key'}, player)) - connect(world, player, names, 'Caves of Banishment (upper)', 'Space time continuum', logic.has_teleport) - connect(world, player, names, 'Caves of Banishment (Maw)', 'Caves of Banishment (upper)', lambda state: logic.has_doublejump(state) if not flooded.flood_maw else state.has('Water Mask', player)) - connect(world, player, names, 'Caves of Banishment (Maw)', 'Caves of Banishment (Sirens)', lambda state: state.has('Gas Mask', player)) - connect(world, player, names, 'Caves of Banishment (Maw)', 'Space time continuum', logic.has_teleport) - connect(world, player, names, 'Caves of Banishment (Sirens)', 'Forest') - connect(world, player, names, 'Castle Ramparts', 'Forest') - connect(world, player, names, 'Castle Ramparts', 'Castle Keep') - connect(world, player, names, 'Castle Ramparts', 'Space time continuum', logic.has_teleport) - connect(world, player, names, 'Castle Keep', 'Castle Ramparts') - connect(world, player, names, 'Castle Keep', 'Castle Basement', lambda state: state.has('Water Mask', player) or not flooded.flood_basement) - connect(world, player, names, 'Castle Keep', 'Royal towers (lower)', logic.has_doublejump) - connect(world, player, names, 'Castle Keep', 'Space time continuum', logic.has_teleport) - connect(world, player, names, 'Royal towers (lower)', 'Castle Keep') - connect(world, player, names, 'Royal towers (lower)', 'Royal towers', lambda state: state.has('Timespinner Wheel', player) or logic.has_forwarddash_doublejump(state)) - connect(world, player, names, 'Royal towers (lower)', 'Space time continuum', logic.has_teleport) - connect(world, player, names, 'Royal towers', 'Royal towers (lower)') - connect(world, player, names, 'Royal towers', 'Royal towers (upper)', logic.has_doublejump) - connect(world, player, names, 'Royal towers (upper)', 'Royal towers') - #connect(world, player, names, 'Ancient Pyramid (entrance)', 'The lab (upper)', lambda state: not is_option_enabled(world, player, "EnterSandman")) - connect(world, player, names, 'Ancient Pyramid (entrance)', 'Ancient Pyramid (left)', logic.has_doublejump) - connect(world, player, names, 'Ancient Pyramid (left)', 'Ancient Pyramid (entrance)') - connect(world, player, names, 'Ancient Pyramid (left)', 'Ancient Pyramid (right)', lambda state: logic.has_upwarddash(state) or flooded.flood_pyramid_shaft) - connect(world, player, names, 'Ancient Pyramid (right)', 'Ancient Pyramid (left)', lambda state: logic.has_upwarddash(state) or flooded.flood_pyramid_shaft) - connect(world, player, names, 'Space time continuum', 'Lake desolation', lambda state: logic.can_teleport_to(state, "Present", "GateLakeDesolation")) - connect(world, player, names, 'Space time continuum', 'Lower lake desolation', lambda state: logic.can_teleport_to(state, "Present", "GateKittyBoss")) - connect(world, player, names, 'Space time continuum', 'Library', lambda state: logic.can_teleport_to(state, "Present", "GateLeftLibrary")) - connect(world, player, names, 'Space time continuum', 'Varndagroth tower right (lower)', lambda state: logic.can_teleport_to(state, "Present", "GateMilitaryGate")) - connect(world, player, names, 'Space time continuum', 'Skeleton Shaft', lambda state: logic.can_teleport_to(state, "Present", "GateSealedCaves")) - connect(world, player, names, 'Space time continuum', 'Sealed Caves (Sirens)', lambda state: logic.can_teleport_to(state, "Present", "GateSealedSirensCave")) - connect(world, player, names, 'Space time continuum', 'Upper Lake Serene', lambda state: logic.can_teleport_to(state, "Past", "GateLakeSereneLeft")) - connect(world, player, names, 'Space time continuum', 'Left Side forest Caves', lambda state: logic.can_teleport_to(state, "Past", "GateLakeSereneRight")) - connect(world, player, names, 'Space time continuum', 'Refugee Camp', lambda state: logic.can_teleport_to(state, "Past", "GateAccessToPast")) - connect(world, player, names, 'Space time continuum', 'Castle Ramparts', lambda state: logic.can_teleport_to(state, "Past", "GateCastleRamparts")) - connect(world, player, names, 'Space time continuum', 'Castle Keep', lambda state: logic.can_teleport_to(state, "Past", "GateCastleKeep")) - connect(world, player, names, 'Space time continuum', 'Royal towers (lower)', lambda state: logic.can_teleport_to(state, "Past", "GateRoyalTowers")) - connect(world, player, names, 'Space time continuum', 'Caves of Banishment (Maw)', lambda state: logic.can_teleport_to(state, "Past", "GateMaw")) - connect(world, player, names, 'Space time continuum', 'Caves of Banishment (upper)', lambda state: logic.can_teleport_to(state, "Past", "GateCavesOfBanishment")) - connect(world, player, names, 'Space time continuum', 'Ancient Pyramid (entrance)', lambda state: logic.can_teleport_to(state, "Time", "GateGyre") or (not is_option_enabled(world, player, "UnchainedKeys") and is_option_enabled(world, player, "EnterSandman"))) - connect(world, player, names, 'Space time continuum', 'Ancient Pyramid (left)', lambda state: logic.can_teleport_to(state, "Time", "GateLeftPyramid")) - connect(world, player, names, 'Space time continuum', 'Ancient Pyramid (right)', lambda state: logic.can_teleport_to(state, "Time", "GateRightPyramid")) + connect(world, player, 'Lake desolation', 'Lower lake desolation', lambda state: logic.has_timestop(state) or state.has('Talaria Attachment', player) or flooded.flood_lake_desolation) + connect(world, player, 'Lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player)) + connect(world, player, 'Lake desolation', 'Skeleton Shaft', lambda state: logic.has_doublejump(state) or flooded.flood_lake_desolation) + connect(world, player, 'Lake desolation', 'Space time continuum', logic.has_teleport) + connect(world, player, 'Upper lake desolation', 'Lake desolation') + connect(world, player, 'Upper lake desolation', 'Eastern lake desolation') + connect(world, player, 'Lower lake desolation', 'Lake desolation') + connect(world, player, 'Lower lake desolation', 'Eastern lake desolation') + connect(world, player, 'Eastern lake desolation', 'Space time continuum', logic.has_teleport) + connect(world, player, 'Eastern lake desolation', 'Library') + connect(world, player, 'Eastern lake desolation', 'Lower lake desolation') + connect(world, player, 'Eastern lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player)) + connect(world, player, 'Library', 'Eastern lake desolation') + connect(world, player, 'Library', 'Library top', lambda state: logic.has_doublejump(state) or state.has('Talaria Attachment', player)) + connect(world, player, 'Library', 'Varndagroth tower left', logic.has_keycard_D) + connect(world, player, 'Library', 'Space time continuum', logic.has_teleport) + connect(world, player, 'Library top', 'Library') + connect(world, player, 'Varndagroth tower left', 'Library') + connect(world, player, 'Varndagroth tower left', 'Varndagroth tower right (upper)', logic.has_keycard_C) + connect(world, player, 'Varndagroth tower left', 'Varndagroth tower right (lower)', logic.has_keycard_B) + connect(world, player, 'Varndagroth tower left', 'Sealed Caves (Sirens)', lambda state: logic.has_keycard_B(state) and state.has('Elevator Keycard', player)) + connect(world, player, 'Varndagroth tower left', 'Refugee Camp', lambda state: state.has('Timespinner Wheel', player) and state.has('Timespinner Spindle', player)) + connect(world, player, 'Varndagroth tower right (upper)', 'Varndagroth tower left') + connect(world, player, 'Varndagroth tower right (upper)', 'Varndagroth tower right (elevator)', lambda state: state.has('Elevator Keycard', player)) + connect(world, player, 'Varndagroth tower right (elevator)', 'Varndagroth tower right (upper)') + connect(world, player, 'Varndagroth tower right (elevator)', 'Varndagroth tower right (lower)') + connect(world, player, 'Varndagroth tower right (lower)', 'Varndagroth tower left', logic.has_keycard_B) + connect(world, player, 'Varndagroth tower right (lower)', 'Varndagroth tower right (elevator)', lambda state: state.has('Elevator Keycard', player)) + connect(world, player, 'Varndagroth tower right (lower)', 'Sealed Caves (Sirens)', lambda state: logic.has_keycard_B(state) and state.has('Elevator Keycard', player)) + connect(world, player, 'Varndagroth tower right (lower)', 'Military Fortress', logic.can_kill_all_3_bosses) + connect(world, player, 'Varndagroth tower right (lower)', 'Space time continuum', logic.has_teleport) + connect(world, player, 'Sealed Caves (Sirens)', 'Varndagroth tower left', lambda state: state.has('Elevator Keycard', player)) + connect(world, player, 'Sealed Caves (Sirens)', 'Varndagroth tower right (lower)', lambda state: state.has('Elevator Keycard', player)) + connect(world, player, 'Sealed Caves (Sirens)', 'Space time continuum', logic.has_teleport) + connect(world, player, 'Military Fortress', 'Varndagroth tower right (lower)', logic.can_kill_all_3_bosses) + connect(world, player, 'Military Fortress', 'Temporal Gyre', lambda state: state.has('Timespinner Wheel', player)) + connect(world, player, 'Military Fortress', 'Military Fortress (hangar)', logic.has_doublejump) + connect(world, player, 'Military Fortress (hangar)', 'Military Fortress') + connect(world, player, 'Military Fortress (hangar)', 'The lab', lambda state: logic.has_keycard_B(state) and logic.has_doublejump(state)) + connect(world, player, 'Temporal Gyre', 'Military Fortress') + connect(world, player, 'The lab', 'Military Fortress') + connect(world, player, 'The lab', 'The lab (power off)', logic.has_doublejump_of_npc) + connect(world, player, 'The lab (power off)', 'The lab') + connect(world, player, 'The lab (power off)', 'The lab (upper)', logic.has_forwarddash_doublejump) + connect(world, player, 'The lab (upper)', 'The lab (power off)') + connect(world, player, 'The lab (upper)', 'Emperors tower', logic.has_forwarddash_doublejump) + connect(world, player, 'The lab (upper)', 'Ancient Pyramid (entrance)', lambda state: state.has_all({'Timespinner Wheel', 'Timespinner Spindle', 'Timespinner Gear 1', 'Timespinner Gear 2', 'Timespinner Gear 3'}, player)) + connect(world, player, 'Emperors tower', 'The lab (upper)') + connect(world, player, 'Skeleton Shaft', 'Lake desolation') + connect(world, player, 'Skeleton Shaft', 'Sealed Caves (upper)', logic.has_keycard_A) + connect(world, player, 'Skeleton Shaft', 'Space time continuum', logic.has_teleport) + connect(world, player, 'Sealed Caves (upper)', 'Skeleton Shaft') + connect(world, player, 'Sealed Caves (upper)', 'Sealed Caves (Xarion)', lambda state: logic.has_teleport(state) or logic.has_doublejump(state)) + connect(world, player, 'Sealed Caves (Xarion)', 'Sealed Caves (upper)', logic.has_doublejump) + connect(world, player, 'Sealed Caves (Xarion)', 'Space time continuum', logic.has_teleport) + connect(world, player, 'Refugee Camp', 'Forest') + #connect(world, player, 'Refugee Camp', 'Library', lambda state: not is_option_enabled(world, player, "Inverted")) + connect(world, player, 'Refugee Camp', 'Space time continuum', logic.has_teleport) + connect(world, player, 'Forest', 'Refugee Camp') + connect(world, player, 'Forest', 'Left Side forest Caves', lambda state: state.has('Talaria Attachment', player) or logic.has_timestop(state)) + connect(world, player, 'Forest', 'Caves of Banishment (Sirens)') + connect(world, player, 'Forest', 'Castle Ramparts') + connect(world, player, 'Left Side forest Caves', 'Forest') + connect(world, player, 'Left Side forest Caves', 'Upper Lake Serene', logic.has_timestop) + connect(world, player, 'Left Side forest Caves', 'Lower Lake Serene', lambda state: state.has('Water Mask', player) or flooded.dry_lake_serene) + connect(world, player, 'Left Side forest Caves', 'Space time continuum', logic.has_teleport) + connect(world, player, 'Upper Lake Serene', 'Left Side forest Caves') + connect(world, player, 'Upper Lake Serene', 'Lower Lake Serene', lambda state: state.has('Water Mask', player)) + connect(world, player, 'Lower Lake Serene', 'Upper Lake Serene') + connect(world, player, 'Lower Lake Serene', 'Left Side forest Caves') + connect(world, player, 'Lower Lake Serene', 'Caves of Banishment (upper)') + connect(world, player, 'Caves of Banishment (upper)', 'Upper Lake Serene', lambda state: state.has('Water Mask', player) or flooded.dry_lake_serene) + connect(world, player, 'Caves of Banishment (upper)', 'Caves of Banishment (Maw)', lambda state: logic.has_doublejump(state) or state.has_any({'Gas Mask', 'Twin Pyramid Key'}, player)) + connect(world, player, 'Caves of Banishment (upper)', 'Space time continuum', logic.has_teleport) + connect(world, player, 'Caves of Banishment (Maw)', 'Caves of Banishment (upper)', lambda state: logic.has_doublejump(state) if not flooded.flood_maw else state.has('Water Mask', player)) + connect(world, player, 'Caves of Banishment (Maw)', 'Caves of Banishment (Sirens)', lambda state: state.has_any({'Gas Mask', 'Talaria Attachment'}, player) ) + connect(world, player, 'Caves of Banishment (Maw)', 'Space time continuum', logic.has_teleport) + connect(world, player, 'Caves of Banishment (Sirens)', 'Forest') + connect(world, player, 'Castle Ramparts', 'Forest') + connect(world, player, 'Castle Ramparts', 'Castle Keep') + connect(world, player, 'Castle Ramparts', 'Space time continuum', logic.has_teleport) + connect(world, player, 'Castle Keep', 'Castle Ramparts') + connect(world, player, 'Castle Keep', 'Castle Basement', lambda state: state.has('Water Mask', player) or not flooded.flood_basement) + connect(world, player, 'Castle Keep', 'Royal towers (lower)', logic.has_doublejump) + connect(world, player, 'Castle Keep', 'Space time continuum', logic.has_teleport) + connect(world, player, 'Royal towers (lower)', 'Castle Keep') + connect(world, player, 'Royal towers (lower)', 'Royal towers', lambda state: state.has('Timespinner Wheel', player) or logic.has_forwarddash_doublejump(state)) + connect(world, player, 'Royal towers (lower)', 'Space time continuum', logic.has_teleport) + connect(world, player, 'Royal towers', 'Royal towers (lower)') + connect(world, player, 'Royal towers', 'Royal towers (upper)', logic.has_doublejump) + connect(world, player, 'Royal towers (upper)', 'Royal towers') + #connect(world, player, 'Ancient Pyramid (entrance)', 'The lab (upper)', lambda state: not is_option_enabled(world, player, "EnterSandman")) + connect(world, player, 'Ancient Pyramid (entrance)', 'Ancient Pyramid (left)', logic.has_doublejump) + connect(world, player, 'Ancient Pyramid (left)', 'Ancient Pyramid (entrance)') + connect(world, player, 'Ancient Pyramid (left)', 'Ancient Pyramid (right)', lambda state: logic.has_upwarddash(state) or flooded.flood_pyramid_shaft) + connect(world, player, 'Ancient Pyramid (right)', 'Ancient Pyramid (left)', lambda state: logic.has_upwarddash(state) or flooded.flood_pyramid_shaft) + connect(world, player, 'Space time continuum', 'Lake desolation', lambda state: logic.can_teleport_to(state, "Present", "GateLakeDesolation")) + connect(world, player, 'Space time continuum', 'Lower lake desolation', lambda state: logic.can_teleport_to(state, "Present", "GateKittyBoss")) + connect(world, player, 'Space time continuum', 'Library', lambda state: logic.can_teleport_to(state, "Present", "GateLeftLibrary")) + connect(world, player, 'Space time continuum', 'Varndagroth tower right (lower)', lambda state: logic.can_teleport_to(state, "Present", "GateMilitaryGate")) + connect(world, player, 'Space time continuum', 'Skeleton Shaft', lambda state: logic.can_teleport_to(state, "Present", "GateSealedCaves")) + connect(world, player, 'Space time continuum', 'Sealed Caves (Sirens)', lambda state: logic.can_teleport_to(state, "Present", "GateSealedSirensCave")) + connect(world, player, 'Space time continuum', 'Upper Lake Serene', lambda state: logic.can_teleport_to(state, "Past", "GateLakeSereneLeft")) + connect(world, player, 'Space time continuum', 'Left Side forest Caves', lambda state: logic.can_teleport_to(state, "Past", "GateLakeSereneRight")) + connect(world, player, 'Space time continuum', 'Refugee Camp', lambda state: logic.can_teleport_to(state, "Past", "GateAccessToPast")) + connect(world, player, 'Space time continuum', 'Castle Ramparts', lambda state: logic.can_teleport_to(state, "Past", "GateCastleRamparts")) + connect(world, player, 'Space time continuum', 'Castle Keep', lambda state: logic.can_teleport_to(state, "Past", "GateCastleKeep")) + connect(world, player, 'Space time continuum', 'Royal towers (lower)', lambda state: logic.can_teleport_to(state, "Past", "GateRoyalTowers")) + connect(world, player, 'Space time continuum', 'Caves of Banishment (Maw)', lambda state: logic.can_teleport_to(state, "Past", "GateMaw")) + connect(world, player, 'Space time continuum', 'Caves of Banishment (upper)', lambda state: logic.can_teleport_to(state, "Past", "GateCavesOfBanishment")) + connect(world, player, 'Space time continuum', 'Ancient Pyramid (entrance)', lambda state: logic.can_teleport_to(state, "Time", "GateGyre") or (not is_option_enabled(world, player, "UnchainedKeys") and is_option_enabled(world, player, "EnterSandman"))) + connect(world, player, 'Space time continuum', 'Ancient Pyramid (left)', lambda state: logic.can_teleport_to(state, "Time", "GateLeftPyramid")) + connect(world, player, 'Space time continuum', 'Ancient Pyramid (right)', lambda state: logic.can_teleport_to(state, "Time", "GateRightPyramid")) if is_option_enabled(world, player, "GyreArchives"): - connect(world, player, names, 'The lab (upper)', 'Ravenlord\'s Lair', lambda state: state.has('Merchant Crow', player)) - connect(world, player, names, 'Ravenlord\'s Lair', 'The lab (upper)') - connect(world, player, names, 'Library top', 'Ifrit\'s Lair', lambda state: state.has('Kobo', player) and state.can_reach('Refugee Camp', 'Region', player)) - connect(world, player, names, 'Ifrit\'s Lair', 'Library top') + connect(world, player, 'The lab (upper)', 'Ravenlord\'s Lair', lambda state: state.has('Merchant Crow', player)) + connect(world, player, 'Ravenlord\'s Lair', 'The lab (upper)') + connect(world, player, 'Library top', 'Ifrit\'s Lair', lambda state: state.has('Kobo', player) and state.can_reach('Refugee Camp', 'Region', player)) + connect(world, player, 'Ifrit\'s Lair', 'Library top') def throwIfAnyLocationIsNotAssignedToARegion(regions: List[Region], regionNames: Set[str]): @@ -203,7 +202,7 @@ def throwIfAnyLocationIsNotAssignedToARegion(regions: List[Region], regionNames: raise Exception("Timespinner: the following regions are used in locations: {}, but no such region exists".format(regionNames - existingRegions)) -def create_location(player: int, location_data: LocationData, region: Region, location_cache: List[Location]) -> Location: +def create_location(player: int, location_data: LocationData, region: Region) -> Location: location = Location(player, location_data.name, location_data.code, region) location.access_rule = location_data.rule @@ -211,17 +210,15 @@ def create_location(player: int, location_data: LocationData, region: Region, lo location.event = True location.locked = True - location_cache.append(location) - return location -def create_region(world: MultiWorld, player: int, locations_per_region: Dict[str, List[LocationData]], location_cache: List[Location], name: str) -> Region: +def create_region(world: MultiWorld, player: int, locations_per_region: Dict[str, List[LocationData]], name: str) -> Region: region = Region(name, player, world) if name in locations_per_region: for location_data in locations_per_region[name]: - location = create_location(player, location_data, region, location_cache) + location = create_location(player, location_data, region) region.locations.append(location) return region @@ -250,19 +247,13 @@ def connectStartingRegion(world: MultiWorld, player: int): space_time_continuum.exits.append(teleport_back_to_start) -def connect(world: MultiWorld, player: int, used_names: Dict[str, int], source: str, target: str, +def connect(world: MultiWorld, player: int, source: str, target: str, rule: Optional[Callable[[CollectionState], bool]] = None): + sourceRegion = world.get_region(source, player) targetRegion = world.get_region(target, player) - if target not in used_names: - used_names[target] = 1 - name = target - else: - used_names[target] += 1 - name = target + (' ' * used_names[target]) - - connection = Entrance(player, name, sourceRegion) + connection = Entrance(player, "", sourceRegion) if rule: connection.access_rule = rule @@ -271,7 +262,7 @@ def connect(world: MultiWorld, player: int, used_names: Dict[str, int], source: connection.connect(targetRegion) -def get_locations_per_region(locations: Tuple[LocationData, ...]) -> Dict[str, List[LocationData]]: +def split_location_datas_per_region(locations: Tuple[LocationData, ...]) -> Dict[str, List[LocationData]]: per_region: Dict[str, List[LocationData]] = {} for location in locations: diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index cb52459b52..d31a4f1968 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -1,10 +1,10 @@ -from typing import Dict, List, Set, Tuple, TextIO -from BaseClasses import Item, MultiWorld, Location, Tutorial, ItemClassification +from typing import Dict, List, Set, Tuple, TextIO, Union +from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification from .Items import get_item_names_per_category, item_table, starter_melee_weapons, starter_spells, filler_items -from .Locations import get_locations, EventId +from .Locations import get_location_datas, EventId from .Options import is_option_enabled, get_option_value, timespinner_options from .PreCalculatedWeights import PreCalculatedWeights -from .Regions import create_regions +from .Regions import create_regions_and_locations from worlds.AutoWorld import World, WebWorld class TimespinnerWebWorld(WebWorld): @@ -29,7 +29,6 @@ class TimespinnerWebWorld(WebWorld): tutorials = [setup, setup_de] - class TimespinnerWorld(World): """ Timespinner is a beautiful metroidvania inspired by classic 90s action-platformers. @@ -44,21 +43,16 @@ class TimespinnerWorld(World): required_client_version = (0, 3, 7) item_name_to_id = {name: data.code for name, data in item_table.items()} - location_name_to_id = {location.name: location.code for location in get_locations(None, None, None)} + location_name_to_id = {location.name: location.code for location in get_location_datas(None, None, None)} item_name_groups = get_item_names_per_category() - locked_locations: List[str] - location_cache: List[Location] precalculated_weights: PreCalculatedWeights def __init__(self, world: MultiWorld, player: int): super().__init__(world, player) - - self.locked_locations = [] - self.location_cache = [] self.precalculated_weights = PreCalculatedWeights(world, player) - def generate_early(self): + def generate_early(self) -> None: # in generate_early the start_inventory isnt copied over to precollected_items yet, so we can still modify the options directly if self.multiworld.start_inventory[self.player].value.pop('Meyef', 0) > 0: self.multiworld.StartWithMeyef[self.player].value = self.multiworld.StartWithMeyef[self.player].option_true @@ -67,44 +61,27 @@ class TimespinnerWorld(World): if self.multiworld.start_inventory[self.player].value.pop('Jewelry Box', 0) > 0: self.multiworld.StartWithJewelryBox[self.player].value = self.multiworld.StartWithJewelryBox[self.player].option_true - def create_regions(self): - locations = get_locations(self.multiworld, self.player, self.precalculated_weights) - create_regions(self.multiworld, self.player, locations, self.location_cache, self.precalculated_weights) + def create_regions(self) -> None: + create_regions_and_locations(self.multiworld, self.player, self.precalculated_weights) - def create_item(self, name: str) -> Item: - return create_item_with_correct_settings(self.multiworld, self.player, name) + def create_items(self) -> None: + self.create_and_assign_event_items() - def get_filler_item_name(self) -> str: - trap_chance: int = get_option_value(self.multiworld, self.player, "TrapChance") - enabled_traps: List[str] = get_option_value(self.multiworld, self.player, "Traps") + excluded_items: Set[str] = self.get_excluded_items() - if self.multiworld.random.random() < (trap_chance / 100) and enabled_traps: - return self.multiworld.random.choice(enabled_traps) - else: - return self.multiworld.random.choice(filler_items) + self.assign_starter_items(excluded_items) - def set_rules(self): - setup_events(self.player, self.locked_locations, self.location_cache) + self.multiworld.itempool += self.get_item_pool(excluded_items) + def set_rules(self) -> None: final_boss: str - if is_option_enabled(self.multiworld, self.player, "DadPercent"): + if self.is_option_enabled("DadPercent"): final_boss = "Killed Emperor" else: final_boss = "Killed Nightmare" self.multiworld.completion_condition[self.player] = lambda state: state.has(final_boss, self.player) - def generate_basic(self): - excluded_items: Set[str] = get_excluded_items(self, self.multiworld, self.player) - - assign_starter_items(self.multiworld, self.player, excluded_items, self.locked_locations) - - pool = get_item_pool(self.multiworld, self.player, excluded_items) - - fill_item_pool_with_dummy_items(self, self.multiworld, self.player, self.locked_locations, self.location_cache, pool) - - self.multiworld.itempool += pool - def fill_slot_data(self) -> Dict[str, object]: slot_data: Dict[str, object] = {} @@ -112,12 +89,12 @@ class TimespinnerWorld(World): for option_name in timespinner_options: if (option_name not in ap_specific_settings): - slot_data[option_name] = get_option_value(self.multiworld, self.player, option_name) + slot_data[option_name] = self.get_option_value(option_name) slot_data["StinkyMaw"] = True slot_data["ProgressiveVerticalMovement"] = False slot_data["ProgressiveKeycards"] = False - slot_data["PersonalItems"] = get_personal_items(self.player, self.location_cache) + slot_data["PersonalItems"] = self.get_personal_items() slot_data["PyramidKeysGate"] = self.precalculated_weights.pyramid_keys_unlock slot_data["PresentGate"] = self.precalculated_weights.present_key_unlock slot_data["PastGate"] = self.precalculated_weights.past_key_unlock @@ -135,17 +112,17 @@ class TimespinnerWorld(World): return slot_data - def write_spoiler_header(self, spoiler_handle: TextIO): - if is_option_enabled(self.multiworld, self.player, "UnchainedKeys"): + def write_spoiler_header(self, spoiler_handle: TextIO) -> None: + if self.is_option_enabled("UnchainedKeys"): spoiler_handle.write(f'Modern Warp Beacon unlock: {self.precalculated_weights.present_key_unlock}\n') spoiler_handle.write(f'Timeworn Warp Beacon unlock: {self.precalculated_weights.past_key_unlock}\n') - if is_option_enabled(self.multiworld, self.player, "EnterSandman"): + if self.is_option_enabled("EnterSandman"): spoiler_handle.write(f'Mysterious Warp Beacon unlock: {self.precalculated_weights.time_key_unlock}\n') else: spoiler_handle.write(f'Twin Pyramid Keys unlock: {self.precalculated_weights.pyramid_keys_unlock}\n') - if is_option_enabled(self.multiworld, self.player, "RisingTides"): + if self.is_option_enabled("RisingTides"): flooded_areas: List[str] = [] if self.precalculated_weights.flood_basement: @@ -177,133 +154,131 @@ class TimespinnerWorld(World): spoiler_handle.write(f'Flooded Areas: {flooded_areas_string}\n') + def create_item(self, name: str) -> Item: + data = item_table[name] -def get_excluded_items(self: TimespinnerWorld, world: MultiWorld, player: int) -> Set[str]: - excluded_items: Set[str] = set() - - if is_option_enabled(world, player, "StartWithJewelryBox"): - excluded_items.add('Jewelry Box') - if is_option_enabled(world, player, "StartWithMeyef"): - excluded_items.add('Meyef') - if is_option_enabled(world, player, "QuickSeed"): - excluded_items.add('Talaria Attachment') - - if is_option_enabled(world, player, "UnchainedKeys"): - excluded_items.add('Twin Pyramid Key') - - if not is_option_enabled(world, player, "EnterSandman"): - excluded_items.add('Mysterious Warp Beacon') - else: - excluded_items.add('Timeworn Warp Beacon') - excluded_items.add('Modern Warp Beacon') - excluded_items.add('Mysterious Warp Beacon') - - for item in world.precollected_items[player]: - if item.name not in self.item_name_groups['UseItem']: - excluded_items.add(item.name) - - return excluded_items - - -def assign_starter_items(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]): - non_local_items = world.non_local_items[player].value - - local_starter_melee_weapons = tuple(item for item in starter_melee_weapons if item not in non_local_items) - if not local_starter_melee_weapons: - if 'Plasma Orb' in non_local_items: - raise Exception("Atleast one melee orb must be local") + if data.useful: + classification = ItemClassification.useful + elif data.progression: + classification = ItemClassification.progression + elif data.trap: + classification = ItemClassification.trap else: - local_starter_melee_weapons = ('Plasma Orb',) + classification = ItemClassification.filler + + item = Item(name, classification, data.code, self.player) - local_starter_spells = tuple(item for item in starter_spells if item not in non_local_items) - if not local_starter_spells: - if 'Lightwall' in non_local_items: - raise Exception("Atleast one spell must be local") - else: - local_starter_spells = ('Lightwall',) + if not item.advancement: + return item - assign_starter_item(world, player, excluded_items, locked_locations, 'Tutorial: Yo Momma 1', local_starter_melee_weapons) - assign_starter_item(world, player, excluded_items, locked_locations, 'Tutorial: Yo Momma 2', local_starter_spells) + if (name == 'Tablet' or name == 'Library Keycard V') and not self.is_option_enabled("DownloadableItems"): + item.classification = ItemClassification.filler + elif name == 'Oculus Ring' and not self.is_option_enabled("EyeSpy"): + item.classification = ItemClassification.filler + elif (name == 'Kobo' or name == 'Merchant Crow') and not self.is_option_enabled("GyreArchives"): + item.classification = ItemClassification.filler + elif name in {"Timeworn Warp Beacon", "Modern Warp Beacon", "Mysterious Warp Beacon"} \ + and not self.is_option_enabled("UnchainedKeys"): + item.classification = ItemClassification.filler - -def assign_starter_item(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str], - location: str, item_list: Tuple[str, ...]): - - item_name = world.random.choice(item_list) - - excluded_items.add(item_name) - - item = create_item_with_correct_settings(world, player, item_name) - - world.get_location(location, player).place_locked_item(item) - - locked_locations.append(location) - - -def get_item_pool(world: MultiWorld, player: int, excluded_items: Set[str]) -> List[Item]: - pool: List[Item] = [] - - for name, data in item_table.items(): - if name not in excluded_items: - for _ in range(data.count): - item = create_item_with_correct_settings(world, player, name) - pool.append(item) - - return pool - - -def fill_item_pool_with_dummy_items(self: TimespinnerWorld, world: MultiWorld, player: int, locked_locations: List[str], - location_cache: List[Location], pool: List[Item]): - for _ in range(len(location_cache) - len(locked_locations) - len(pool)): - item = create_item_with_correct_settings(world, player, self.get_filler_item_name()) - pool.append(item) - - -def create_item_with_correct_settings(world: MultiWorld, player: int, name: str) -> Item: - data = item_table[name] - - if data.useful: - classification = ItemClassification.useful - elif data.progression: - classification = ItemClassification.progression - elif data.trap: - classification = ItemClassification.trap - else: - classification = ItemClassification.filler - - item = Item(name, classification, data.code, player) - - if not item.advancement: return item - if (name == 'Tablet' or name == 'Library Keycard V') and not is_option_enabled(world, player, "DownloadableItems"): - item.classification = ItemClassification.filler - elif name == 'Oculus Ring' and not is_option_enabled(world, player, "EyeSpy"): - item.classification = ItemClassification.filler - elif (name == 'Kobo' or name == 'Merchant Crow') and not is_option_enabled(world, player, "GyreArchives"): - item.classification = ItemClassification.filler - elif name in {"Timeworn Warp Beacon", "Modern Warp Beacon", "Mysterious Warp Beacon"} \ - and not is_option_enabled(world, player, "UnchainedKeys"): - item.classification = ItemClassification.filler + def get_filler_item_name(self) -> str: + trap_chance: int = self.get_option_value("TrapChance") + enabled_traps: List[str] = self.get_option_value("Traps") - return item + if self.multiworld.random.random() < (trap_chance / 100) and enabled_traps: + return self.multiworld.random.choice(enabled_traps) + else: + return self.multiworld.random.choice(filler_items) + def get_excluded_items(self) -> Set[str]: + excluded_items: Set[str] = set() -def setup_events(player: int, locked_locations: List[str], location_cache: List[Location]): - for location in location_cache: - if location.address == EventId: - item = Item(location.name, ItemClassification.progression, EventId, player) + if self.is_option_enabled("StartWithJewelryBox"): + excluded_items.add('Jewelry Box') + if self.is_option_enabled("StartWithMeyef"): + excluded_items.add('Meyef') + if self.is_option_enabled("QuickSeed"): + excluded_items.add('Talaria Attachment') - locked_locations.append(location.name) + if self.is_option_enabled("UnchainedKeys"): + excluded_items.add('Twin Pyramid Key') - location.place_locked_item(item) + if not self.is_option_enabled("EnterSandman"): + excluded_items.add('Mysterious Warp Beacon') + else: + excluded_items.add('Timeworn Warp Beacon') + excluded_items.add('Modern Warp Beacon') + excluded_items.add('Mysterious Warp Beacon') + for item in self.multiworld.precollected_items[self.player]: + if item.name not in self.item_name_groups['UseItem']: + excluded_items.add(item.name) -def get_personal_items(player: int, locations: List[Location]) -> Dict[int, int]: - personal_items: Dict[int, int] = {} + return excluded_items - for location in locations: - if location.address and location.item and location.item.code and location.item.player == player: - personal_items[location.address] = location.item.code + def assign_starter_items(self, excluded_items: Set[str]) -> None: + non_local_items: Set[str] = self.multiworld.non_local_items[self.player].value - return personal_items + local_starter_melee_weapons = tuple(item for item in starter_melee_weapons if item not in non_local_items) + if not local_starter_melee_weapons: + if 'Plasma Orb' in non_local_items: + raise Exception("Atleast one melee orb must be local") + else: + local_starter_melee_weapons = ('Plasma Orb',) + + local_starter_spells = tuple(item for item in starter_spells if item not in non_local_items) + if not local_starter_spells: + if 'Lightwall' in non_local_items: + raise Exception("Atleast one spell must be local") + else: + local_starter_spells = ('Lightwall',) + + self.assign_starter_item(excluded_items, 'Tutorial: Yo Momma 1', local_starter_melee_weapons) + self.assign_starter_item(excluded_items, 'Tutorial: Yo Momma 2', local_starter_spells) + + def assign_starter_item(self, excluded_items: Set[str], location: str, item_list: Tuple[str, ...]) -> None: + item_name = self.multiworld.random.choice(item_list) + + excluded_items.add(item_name) + + item = self.create_item(item_name) + + self.multiworld.get_location(location, self.player).place_locked_item(item) + + def get_item_pool(self, excluded_items: Set[str]) -> List[Item]: + pool: List[Item] = [] + + for name, data in item_table.items(): + if name not in excluded_items: + for _ in range(data.count): + item = self.create_item(name) + pool.append(item) + + for _ in range(len(self.multiworld.get_unfilled_locations(self.player)) - len(pool)): + item = self.create_item(self.get_filler_item_name()) + pool.append(item) + + return pool + + def create_and_assign_event_items(self) -> None: + for location in self.multiworld.get_locations(self.player): + if location.address == EventId: + item = Item(location.name, ItemClassification.progression, EventId, self.player) + location.place_locked_item(item) + + def get_personal_items(self) -> Dict[int, int]: + personal_items: Dict[int, int] = {} + + for location in self.multiworld.get_locations(self.player): + if location.address and location.item and location.item.code and location.item.player == self.player: + personal_items[location.address] = location.item.code + + return personal_items + + def is_option_enabled(self, option: str) -> bool: + return is_option_enabled(self.multiworld, self.player, option) + + def get_option_value(self, option: str) -> Union[int, Dict, List]: + return get_option_value(self.multiworld, self.player, option) From d74c4c4c9432c012d614e4282b4450427199c4ff Mon Sep 17 00:00:00 2001 From: zig-for Date: Fri, 3 Mar 2023 23:23:52 -0800 Subject: [PATCH 017/172] Core: Remove ALTTP cruft from BaseClasses (#1451) --- BaseClasses.py | 167 ---------------- worlds/alttp/Bosses.py | 70 +++---- worlds/alttp/ItemPool.py | 5 +- worlds/alttp/OverworldGlitchRules.py | 23 +-- worlds/alttp/Rules.py | 263 +++++++++++++------------- worlds/alttp/StateHelpers.py | 137 ++++++++++++++ worlds/alttp/SubClasses.py | 7 +- worlds/alttp/UnderworldGlitchRules.py | 16 +- worlds/alttp/__init__.py | 3 +- 9 files changed, 339 insertions(+), 352 deletions(-) create mode 100644 worlds/alttp/StateHelpers.py diff --git a/BaseClasses.py b/BaseClasses.py index 5fa3b76988..16a0600595 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -16,7 +16,6 @@ import NetUtils import Options import Utils - class Group(TypedDict, total=False): name: str game: str @@ -760,169 +759,9 @@ class CollectionState(): found += self.prog_items[item_name, player] return found - def can_buy_unlimited(self, item: str, player: int) -> bool: - return any(shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(self) for - shop in self.multiworld.shops) - - def can_buy(self, item: str, player: int) -> bool: - return any(shop.region.player == player and shop.has(item) and shop.region.can_reach(self) for - shop in self.multiworld.shops) - def item_count(self, item: str, player: int) -> int: return self.prog_items[item, player] - def has_triforce_pieces(self, count: int, player: int) -> bool: - return self.item_count('Triforce Piece', player) + self.item_count('Power Star', player) >= count - - def has_crystals(self, count: int, player: int) -> bool: - found: int = 0 - for crystalnumber in range(1, 8): - found += self.prog_items[f"Crystal {crystalnumber}", player] - if found >= count: - return True - return False - - def can_lift_rocks(self, player: int): - return self.has('Power Glove', player) or self.has('Titans Mitts', player) - - def bottle_count(self, player: int) -> int: - return min(self.multiworld.difficulty_requirements[player].progressive_bottle_limit, - self.count_group("Bottles", player)) - - def has_hearts(self, player: int, count: int) -> int: - # Warning: This only considers items that are marked as advancement items - return self.heart_count(player) >= count - - def heart_count(self, player: int) -> int: - # Warning: This only considers items that are marked as advancement items - diff = self.multiworld.difficulty_requirements[player] - return min(self.item_count('Boss Heart Container', player), diff.boss_heart_container_limit) \ - + self.item_count('Sanctuary Heart Container', player) \ - + min(self.item_count('Piece of Heart', player), diff.heart_piece_limit) // 4 \ - + 3 # starting hearts - - def can_lift_heavy_rocks(self, player: int) -> bool: - return self.has('Titans Mitts', player) - - def can_extend_magic(self, player: int, smallmagic: int = 16, - fullrefill: bool = False): # This reflects the total magic Link has, not the total extra he has. - basemagic = 8 - if self.has('Magic Upgrade (1/4)', player): - basemagic = 32 - elif self.has('Magic Upgrade (1/2)', player): - basemagic = 16 - if self.can_buy_unlimited('Green Potion', player) or self.can_buy_unlimited('Blue Potion', player): - if self.multiworld.item_functionality[player] == 'hard' and not fullrefill: - basemagic = basemagic + int(basemagic * 0.5 * self.bottle_count(player)) - elif self.multiworld.item_functionality[player] == 'expert' and not fullrefill: - basemagic = basemagic + int(basemagic * 0.25 * self.bottle_count(player)) - else: - basemagic = basemagic + basemagic * self.bottle_count(player) - return basemagic >= smallmagic - - def can_kill_most_things(self, player: int, enemies: int = 5) -> bool: - return (self.has_melee_weapon(player) - or self.has('Cane of Somaria', player) - or (self.has('Cane of Byrna', player) and (enemies < 6 or self.can_extend_magic(player))) - or self.can_shoot_arrows(player) - or self.has('Fire Rod', player) - or (self.has('Bombs (10)', player) and enemies < 6)) - - def can_shoot_arrows(self, player: int) -> bool: - if self.multiworld.retro_bow[player]: - return (self.has('Bow', player) or self.has('Silver Bow', player)) and self.can_buy('Single Arrow', player) - return self.has('Bow', player) or self.has('Silver Bow', player) - - def can_get_good_bee(self, player: int) -> bool: - cave = self.multiworld.get_region('Good Bee Cave', player) - return ( - self.has_group("Bottles", player) and - self.has('Bug Catching Net', player) and - (self.has('Pegasus Boots', player) or (self.has_sword(player) and self.has('Quake', player))) and - cave.can_reach(self) and - self.is_not_bunny(cave, player) - ) - - def can_retrieve_tablet(self, player: int) -> bool: - return self.has('Book of Mudora', player) and (self.has_beam_sword(player) or - (self.multiworld.swordless[player] and - self.has("Hammer", player))) - - def has_sword(self, player: int) -> bool: - return self.has('Fighter Sword', player) \ - or self.has('Master Sword', player) \ - or self.has('Tempered Sword', player) \ - or self.has('Golden Sword', player) - - def has_beam_sword(self, player: int) -> bool: - return self.has('Master Sword', player) or self.has('Tempered Sword', player) or self.has('Golden Sword', - player) - - def has_melee_weapon(self, player: int) -> bool: - return self.has_sword(player) or self.has('Hammer', player) - - def has_fire_source(self, player: int) -> bool: - return self.has('Fire Rod', player) or self.has('Lamp', player) - - def can_melt_things(self, player: int) -> bool: - return self.has('Fire Rod', player) or \ - (self.has('Bombos', player) and - (self.multiworld.swordless[player] or - self.has_sword(player))) - - def can_avoid_lasers(self, player: int) -> bool: - return self.has('Mirror Shield', player) or self.has('Cane of Byrna', player) or self.has('Cape', player) - - def is_not_bunny(self, region: Region, player: int) -> bool: - if self.has('Moon Pearl', player): - return True - - return region.is_light_world if self.multiworld.mode[player] != 'inverted' else region.is_dark_world - - def can_reach_light_world(self, player: int) -> bool: - if True in [i.is_light_world for i in self.reachable_regions[player]]: - return True - return False - - def can_reach_dark_world(self, player: int) -> bool: - if True in [i.is_dark_world for i in self.reachable_regions[player]]: - return True - return False - - def has_misery_mire_medallion(self, player: int) -> bool: - return self.has(self.multiworld.required_medallions[player][0], player) - - def has_turtle_rock_medallion(self, player: int) -> bool: - return self.has(self.multiworld.required_medallions[player][1], player) - - def can_boots_clip_lw(self, player: int) -> bool: - if self.multiworld.mode[player] == 'inverted': - return self.has('Pegasus Boots', player) and self.has('Moon Pearl', player) - return self.has('Pegasus Boots', player) - - def can_boots_clip_dw(self, player: int) -> bool: - if self.multiworld.mode[player] != 'inverted': - return self.has('Pegasus Boots', player) and self.has('Moon Pearl', player) - return self.has('Pegasus Boots', player) - - def can_get_glitched_speed_lw(self, player: int) -> bool: - rules = [self.has('Pegasus Boots', player), any([self.has('Hookshot', player), self.has_sword(player)])] - if self.multiworld.mode[player] == 'inverted': - rules.append(self.has('Moon Pearl', player)) - return all(rules) - - def can_superbunny_mirror_with_sword(self, player: int) -> bool: - return self.has('Magic Mirror', player) and self.has_sword(player) - - def can_get_glitched_speed_dw(self, player: int) -> bool: - rules = [self.has('Pegasus Boots', player), any([self.has('Hookshot', player), self.has_sword(player)])] - if self.multiworld.mode[player] != 'inverted': - rules.append(self.has('Moon Pearl', player)) - return all(rules) - - def can_bomb_clip(self, region: Region, player: int) -> bool: - return self.is_not_bunny(region, player) and self.has('Pegasus Boots', player) - def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool: if location: self.locations_checked.add(location) @@ -958,12 +797,6 @@ class Region: exits: List[Entrance] locations: List[Location] dungeon: Optional[Dungeon] = None - shop: Optional = None - - # LttP specific. TODO: move to a LttPRegion - # will be set after making connections. - is_light_world: bool = False - is_dark_world: bool = False def __init__(self, name: str, player: int, multiworld: MultiWorld, hint: Optional[str] = None): self.name = name diff --git a/worlds/alttp/Bosses.py b/worlds/alttp/Bosses.py index 5f915a3342..51615ddc45 100644 --- a/worlds/alttp/Bosses.py +++ b/worlds/alttp/Bosses.py @@ -4,7 +4,7 @@ from typing import Optional, Union, List, Tuple, Callable, Dict from BaseClasses import Boss from Fill import FillError from .Options import LTTPBosses as Bosses - +from .StateHelpers import can_shoot_arrows, can_extend_magic, can_get_good_bee, has_sword, has_beam_sword, has_melee_weapon, has_fire_source def BossFactory(boss: str, player: int) -> Optional[Boss]: if boss in boss_table: @@ -16,33 +16,33 @@ def BossFactory(boss: str, player: int) -> Optional[Boss]: def ArmosKnightsDefeatRule(state, player: int) -> bool: # Magic amounts are probably a bit overkill return ( - state.has_melee_weapon(player) or - state.can_shoot_arrows(player) or - (state.has('Cane of Somaria', player) and state.can_extend_magic(player, 10)) or - (state.has('Cane of Byrna', player) and state.can_extend_magic(player, 16)) or - (state.has('Ice Rod', player) and state.can_extend_magic(player, 32)) or - (state.has('Fire Rod', player) and state.can_extend_magic(player, 32)) or + has_melee_weapon(state, player) or + can_shoot_arrows(state, player) or + (state.has('Cane of Somaria', player) and can_extend_magic(state, player, 10)) or + (state.has('Cane of Byrna', player) and can_extend_magic(state, player, 16)) or + (state.has('Ice Rod', player) and can_extend_magic(state, player, 32)) or + (state.has('Fire Rod', player) and can_extend_magic(state, player, 32)) or state.has('Blue Boomerang', player) or state.has('Red Boomerang', player)) def LanmolasDefeatRule(state, player: int) -> bool: return ( - state.has_melee_weapon(player) or + has_melee_weapon(state, player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Cane of Somaria', player) or state.has('Cane of Byrna', player) or - state.can_shoot_arrows(player)) + can_shoot_arrows(state, player)) def MoldormDefeatRule(state, player: int) -> bool: - return state.has_melee_weapon(player) + return has_melee_weapon(state, player) def HelmasaurKingDefeatRule(state, player: int) -> bool: # TODO: technically possible with the hammer - return state.has_sword(player) or state.can_shoot_arrows(player) + return has_sword(state, player) or can_shoot_arrows(state, player) def ArrghusDefeatRule(state, player: int) -> bool: @@ -51,28 +51,28 @@ def ArrghusDefeatRule(state, player: int) -> bool: # TODO: ideally we would have a check for bow and silvers, which combined with the # hookshot is enough. This is not coded yet because the silvers that only work in pyramid feature # makes this complicated - if state.has_melee_weapon(player): + if has_melee_weapon(state, player): return True - return ((state.has('Fire Rod', player) and (state.can_shoot_arrows(player) or state.can_extend_magic(player, + return ((state.has('Fire Rod', player) and (can_shoot_arrows(state, player) or can_extend_magic(state, player, 12))) or # assuming mostly gitting two puff with one shot - (state.has('Ice Rod', player) and (state.can_shoot_arrows(player) or state.can_extend_magic(player, 16)))) + (state.has('Ice Rod', player) and (can_shoot_arrows(state, player) or can_extend_magic(state, player, 16)))) def MothulaDefeatRule(state, player: int) -> bool: return ( - state.has_melee_weapon(player) or - (state.has('Fire Rod', player) and state.can_extend_magic(player, 10)) or + has_melee_weapon(state, player) or + (state.has('Fire Rod', player) and can_extend_magic(state, player, 10)) or # TODO: Not sure how much (if any) extend magic is needed for these two, since they only apply # to non-vanilla locations, so are harder to test, so sticking with what VT has for now: - (state.has('Cane of Somaria', player) and state.can_extend_magic(player, 16)) or - (state.has('Cane of Byrna', player) and state.can_extend_magic(player, 16)) or - state.can_get_good_bee(player) + (state.has('Cane of Somaria', player) and can_extend_magic(state, player, 16)) or + (state.has('Cane of Byrna', player) and can_extend_magic(state, player, 16)) or + can_get_good_bee(state, player) ) def BlindDefeatRule(state, player: int) -> bool: - return state.has_melee_weapon(player) or state.has('Cane of Somaria', player) or state.has('Cane of Byrna', player) + return has_melee_weapon(state, player) or state.has('Cane of Somaria', player) or state.has('Cane of Byrna', player) def KholdstareDefeatRule(state, player: int) -> bool: @@ -81,56 +81,56 @@ def KholdstareDefeatRule(state, player: int) -> bool: state.has('Fire Rod', player) or ( state.has('Bombos', player) and - (state.has_sword(player) or state.multiworld.swordless[player]) + (has_sword(state, player) or state.multiworld.swordless[player]) ) ) and ( - state.has_melee_weapon(player) or - (state.has('Fire Rod', player) and state.can_extend_magic(player, 20)) or + has_melee_weapon(state, player) or + (state.has('Fire Rod', player) and can_extend_magic(state, player, 20)) or ( state.has('Fire Rod', player) and state.has('Bombos', player) and state.multiworld.swordless[player] and - state.can_extend_magic(player, 16) + can_extend_magic(state, player, 16) ) ) ) def VitreousDefeatRule(state, player: int) -> bool: - return state.can_shoot_arrows(player) or state.has_melee_weapon(player) + return can_shoot_arrows(state, player) or has_melee_weapon(state, player) def TrinexxDefeatRule(state, player: int) -> bool: if not (state.has('Fire Rod', player) and state.has('Ice Rod', player)): return False return state.has('Hammer', player) or state.has('Tempered Sword', player) or state.has('Golden Sword', player) or \ - (state.has('Master Sword', player) and state.can_extend_magic(player, 16)) or \ - (state.has_sword(player) and state.can_extend_magic(player, 32)) + (state.has('Master Sword', player) and can_extend_magic(state, player, 16)) or \ + (has_sword(state, player) and can_extend_magic(state, player, 32)) def AgahnimDefeatRule(state, player: int) -> bool: - return state.has_sword(player) or state.has('Hammer', player) or state.has('Bug Catching Net', player) + return has_sword(state, player) or state.has('Hammer', player) or state.has('Bug Catching Net', player) def GanonDefeatRule(state, player: int) -> bool: if state.multiworld.swordless[player]: return state.has('Hammer', player) and \ - state.has_fire_source(player) and \ + has_fire_source(state, player) and \ state.has('Silver Bow', player) and \ - state.can_shoot_arrows(player) + can_shoot_arrows(state, player) - can_hurt = state.has_beam_sword(player) - common = can_hurt and state.has_fire_source(player) + can_hurt = has_beam_sword(state, player) + common = can_hurt and has_fire_source(state, player) # silverless ganon may be needed in anything higher than no glitches if state.multiworld.logic[player] != 'noglitches': # need to light torch a sufficient amount of times return common and (state.has('Tempered Sword', player) or state.has('Golden Sword', player) or ( - state.has('Silver Bow', player) and state.can_shoot_arrows(player)) or - state.has('Lamp', player) or state.can_extend_magic(player, 12)) + state.has('Silver Bow', player) and can_shoot_arrows(state, player)) or + state.has('Lamp', player) or can_extend_magic(state, player, 12)) else: - return common and state.has('Silver Bow', player) and state.can_shoot_arrows(player) + return common and state.has('Silver Bow', player) and can_shoot_arrows(state, player) boss_table: Dict[str, Tuple[str, Optional[Callable]]] = { diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index f7326092ec..80028c3fb3 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -10,6 +10,7 @@ from worlds.alttp.EntranceShuffle import connect_entrance from Fill import FillError from worlds.alttp.Items import ItemFactory, GetBeemizerItem from worlds.alttp.Options import smallkey_shuffle, compass_shuffle, bigkey_shuffle, map_shuffle +from .StateHelpers import has_triforce_pieces, has_melee_weapon # This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space. # Some basic items that various modes require are placed here, including pendants and crystals. Medallion requirements for the two relevant entrances are also decided. @@ -286,7 +287,7 @@ def generate_itempool(world): region = world.get_region('Light World', player) loc = ALttPLocation(player, "Murahdahla", parent=region) - loc.access_rule = lambda state: state.has_triforce_pieces(state.multiworld.treasure_hunt_count[player], player) + loc.access_rule = lambda state: has_triforce_pieces(state, player) region.locations.append(loc) world.clear_location_cache() @@ -327,7 +328,7 @@ def generate_itempool(world): for item in precollected_items: world.push_precollected(ItemFactory(item, player)) - if world.mode[player] == 'standard' and not world.state.has_melee_weapon(player): + if world.mode[player] == 'standard' and not has_melee_weapon(world.state, player): if "Link's Uncle" not in placed_items: found_sword = False found_bow = False diff --git a/worlds/alttp/OverworldGlitchRules.py b/worlds/alttp/OverworldGlitchRules.py index 705db7e7c0..f6c3ec8d14 100644 --- a/worlds/alttp/OverworldGlitchRules.py +++ b/worlds/alttp/OverworldGlitchRules.py @@ -4,6 +4,7 @@ Helper functions to deliver entrance/exit/region sets to OWG rules. from BaseClasses import Entrance +from .StateHelpers import can_lift_heavy_rocks, can_boots_clip_lw, can_boots_clip_dw, can_get_glitched_speed_dw def get_sword_required_superbunny_mirror_regions(): """ @@ -169,7 +170,7 @@ def get_boots_clip_exits_dw(inverted, player): yield ('Ganons Tower Ascent', 'Dark Death Mountain (West Bottom)', 'Dark Death Mountain (Top)') # This only gets you to the GT entrance yield ('Dark Death Mountain Glitched Bridge', 'Dark Death Mountain (West Bottom)', 'Dark Death Mountain (Top)') yield ('Turtle Rock (Top) Clip Spot', 'Dark Death Mountain (Top)', 'Turtle Rock (Top)') - yield ('Ice Palace Clip', 'South Dark World', 'Dark Lake Hylia Central Island', lambda state: state.can_boots_clip_dw(player) and state.has('Flippers', player)) + yield ('Ice Palace Clip', 'South Dark World', 'Dark Lake Hylia Central Island', lambda state: can_boots_clip_dw(state, player) and state.has('Flippers', player)) else: yield ('Dark Desert Teleporter Clip Spot', 'Dark Desert', 'Dark Desert Ledge') @@ -203,7 +204,7 @@ def get_mirror_offset_spots_lw(player): Mirror shenanigans placing a mirror portal with a broken camera """ yield ('Death Mountain Offset Mirror', 'Death Mountain', 'Light World') - yield ('Death Mountain Offset Mirror (Houlihan Exit)', 'Death Mountain', 'Hyrule Castle Ledge', lambda state: state.has('Magic Mirror', player) and state.can_boots_clip_dw(player) and state.has('Moon Pearl', player)) + yield ('Death Mountain Offset Mirror (Houlihan Exit)', 'Death Mountain', 'Hyrule Castle Ledge', lambda state: state.has('Magic Mirror', player) and can_boots_clip_dw(state, player) and state.has('Moon Pearl', player)) @@ -255,11 +256,11 @@ def overworld_glitch_connections(world, player): def overworld_glitches_rules(world, player): # Boots-accessible locations. - set_owg_connection_rules(player, world, get_boots_clip_exits_lw(world.mode[player] == 'inverted'), lambda state: state.can_boots_clip_lw(player)) - set_owg_connection_rules(player, world, get_boots_clip_exits_dw(world.mode[player] == 'inverted', player), lambda state: state.can_boots_clip_dw(player)) + set_owg_connection_rules(player, world, get_boots_clip_exits_lw(world.mode[player] == 'inverted'), lambda state: can_boots_clip_lw(state, player)) + set_owg_connection_rules(player, world, get_boots_clip_exits_dw(world.mode[player] == 'inverted', player), lambda state: can_boots_clip_dw(state, player)) # Glitched speed drops. - set_owg_connection_rules(player, world, get_glitched_speed_drops_dw(world.mode[player] == 'inverted'), lambda state: state.can_get_glitched_speed_dw(player)) + set_owg_connection_rules(player, world, get_glitched_speed_drops_dw(world.mode[player] == 'inverted'), lambda state: can_get_glitched_speed_dw(state, player)) # Dark Death Mountain Ledge Clip Spot also accessible with mirror. if world.mode[player] != 'inverted': add_alternate_rule(world.get_entrance('Dark Death Mountain Ledge Clip Spot', player), lambda state: state.has('Magic Mirror', player)) @@ -267,20 +268,20 @@ def overworld_glitches_rules(world, player): # Mirror clip spots. if world.mode[player] != 'inverted': set_owg_connection_rules(player, world, get_mirror_clip_spots_dw(), lambda state: state.has('Magic Mirror', player)) - set_owg_connection_rules(player, world, get_mirror_offset_spots_dw(), lambda state: state.has('Magic Mirror', player) and state.can_boots_clip_lw(player)) + set_owg_connection_rules(player, world, get_mirror_offset_spots_dw(), lambda state: state.has('Magic Mirror', player) and can_boots_clip_lw(state, player)) else: - set_owg_connection_rules(player, world, get_mirror_offset_spots_lw(player), lambda state: state.has('Magic Mirror', player) and state.can_boots_clip_dw(player)) + set_owg_connection_rules(player, world, get_mirror_offset_spots_lw(player), lambda state: state.has('Magic Mirror', player) and can_boots_clip_dw(state, player)) # Regions that require the boots and some other stuff. if world.mode[player] != 'inverted': - world.get_entrance('Turtle Rock Teleporter', player).access_rule = lambda state: (state.can_boots_clip_lw(player) or state.can_lift_heavy_rocks(player)) and state.has('Hammer', player) + world.get_entrance('Turtle Rock Teleporter', player).access_rule = lambda state: (can_boots_clip_lw(state, player) or can_lift_heavy_rocks(state, player)) and state.has('Hammer', player) add_alternate_rule(world.get_entrance('Waterfall of Wishing', player), lambda state: state.has('Moon Pearl', player) or state.has('Pegasus Boots', player)) else: add_alternate_rule(world.get_entrance('Waterfall of Wishing Cave', player), lambda state: state.has('Moon Pearl', player)) - world.get_entrance('Dark Desert Teleporter', player).access_rule = lambda state: (state.has('Flute', player) or state.has('Pegasus Boots', player)) and state.can_lift_heavy_rocks(player) - add_alternate_rule(world.get_entrance('Catfish Exit Rock', player), lambda state: state.can_boots_clip_dw(player)) - add_alternate_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: state.can_boots_clip_dw(player)) + world.get_entrance('Dark Desert Teleporter', player).access_rule = lambda state: (state.has('Flute', player) or state.has('Pegasus Boots', player)) and can_lift_heavy_rocks(state, player) + add_alternate_rule(world.get_entrance('Catfish Exit Rock', player), lambda state: can_boots_clip_dw(state, player)) + add_alternate_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: can_boots_clip_dw(state, player)) # Zora's Ledge via waterwalk setup. add_alternate_rule(world.get_location('Zora\'s Ledge', player), lambda state: state.has('Pegasus Boots', player)) diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index a31641d679..ff35c30554 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -2,17 +2,24 @@ import collections import logging from typing import Iterator, Set -from worlds.alttp import OverworldGlitchRules -from BaseClasses import MultiWorld, Entrance -from worlds.alttp.Items import ItemFactory, progression_items, item_name_groups, item_table -from worlds.alttp.OverworldGlitchRules import overworld_glitches_rules, no_logic_rules -from worlds.alttp.Regions import location_table -from worlds.alttp.UnderworldGlitchRules import underworld_glitches_rules -from worlds.alttp.Bosses import GanonDefeatRule -from worlds.generic.Rules import set_rule, add_rule, forbid_item, add_item_rule, item_in_locations, \ - item_name -from worlds.alttp.Options import smallkey_shuffle -from worlds.alttp.Regions import LTTPRegionType +from BaseClasses import Entrance, MultiWorld +from worlds.generic.Rules import (add_item_rule, add_rule, forbid_item, + item_in_locations, item_name, set_rule) + +from . import OverworldGlitchRules +from .Bosses import GanonDefeatRule +from .Items import ItemFactory, item_name_groups, item_table, progression_items +from .Options import smallkey_shuffle +from .OverworldGlitchRules import no_logic_rules, overworld_glitches_rules +from .Regions import LTTPRegionType, location_table +from .StateHelpers import (can_extend_magic, can_kill_most_things, + can_lift_heavy_rocks, can_lift_rocks, + can_melt_things, can_retrieve_tablet, + can_shoot_arrows, has_beam_sword, has_crystals, + has_fire_source, has_hearts, + has_misery_mire_medallion, has_sword, has_turtle_rock_medallion, + has_triforce_pieces) +from .UnderworldGlitchRules import underworld_glitches_rules def set_rules(world): @@ -76,7 +83,7 @@ def set_rules(world): if world.goal[player] == 'bosses': # require all bosses to beat ganon - add_rule(world.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player) and state.has('Beat Agahnim 1', player) and state.has('Beat Agahnim 2', player) and state.has_crystals(7, player)) + add_rule(world.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player) and state.has('Beat Agahnim 1', player) and state.has('Beat Agahnim 2', player) and has_crystals(state, 7, player)) elif world.goal[player] == 'ganon': # require aga2 to beat ganon add_rule(world.get_location('Ganon', player), lambda state: state.has('Beat Agahnim 2', player)) @@ -101,7 +108,7 @@ def set_rules(world): set_trock_key_rules(world, player) - set_rule(ganons_tower, lambda state: state.has_crystals(state.multiworld.crystals_needed_for_gt[player], player)) + set_rule(ganons_tower, lambda state: has_crystals(state, state.multiworld.crystals_needed_for_gt[player], player)) if world.mode[player] != 'inverted' and world.logic[player] in ['owglitches', 'hybridglitches', 'nologic']: add_rule(world.get_entrance('Ganons Tower', player), lambda state: state.multiworld.get_entrance('Ganons Tower Ascent', player).can_reach(state), 'or') @@ -199,7 +206,7 @@ def global_rules(world, player): set_rule(world.get_location('Dark Blacksmith Ruins', player), lambda state: state.has('Return Smith', player)) set_rule(world.get_location('Purple Chest', player), lambda state: state.has('Pick Up Purple Chest', player)) # Can S&Q with chest - set_rule(world.get_location('Ether Tablet', player), lambda state: state.can_retrieve_tablet(player)) + set_rule(world.get_location('Ether Tablet', player), lambda state: can_retrieve_tablet(state, player)) set_rule(world.get_location('Master Sword Pedestal', player), lambda state: state.has('Red Pendant', player) and state.has('Blue Pendant', player) and state.has('Green Pendant', player)) set_rule(world.get_location('Missing Smith', player), lambda state: state.has('Get Frog', player) and state.can_reach('Blacksmiths Hut', 'Region', player)) # Can't S&Q with smith @@ -212,11 +219,11 @@ def global_rules(world, player): set_rule(world.get_location('Spike Cave', player), lambda state: - state.has('Hammer', player) and state.can_lift_rocks(player) and - ((state.has('Cape', player) and state.can_extend_magic(player, 16, True)) or + state.has('Hammer', player) and can_lift_rocks(state, player) and + ((state.has('Cape', player) and can_extend_magic(state, player, 16, True)) or (state.has('Cane of Byrna', player) and - (state.can_extend_magic(player, 12, True) or - (state.multiworld.can_take_damage[player] and (state.has('Pegasus Boots', player) or state.has_hearts(player, 4)))))) + (can_extend_magic(state, player, 12, True) or + (state.multiworld.can_take_damage[player] and (state.has('Pegasus Boots', player) or has_hearts(state, player, 4)))))) ) set_rule(world.get_location('Hookshot Cave - Top Right', player), lambda state: state.has('Hookshot', player)) @@ -232,11 +239,11 @@ def global_rules(world, player): set_rule(world.get_entrance('Sewers Back Door', player), lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player)) set_rule(world.get_entrance('Agahnim 1', player), - lambda state: state.has_sword(player) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2)) + lambda state: has_sword(state, player) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2)) - set_rule(world.get_location('Castle Tower - Room 03', player), lambda state: state.can_kill_most_things(player, 8)) + set_rule(world.get_location('Castle Tower - Room 03', player), lambda state: can_kill_most_things(state, player, 8)) set_rule(world.get_location('Castle Tower - Dark Maze', player), - lambda state: state.can_kill_most_things(player, 8) and state._lttp_has_key('Small Key (Agahnims Tower)', + lambda state: can_kill_most_things(state, player, 8) and state._lttp_has_key('Small Key (Agahnims Tower)', player)) set_rule(world.get_location('Eastern Palace - Big Chest', player), @@ -248,14 +255,14 @@ def global_rules(world, player): set_rule(ep_prize, lambda state: state.has('Big Key (Eastern Palace)', player) and ep_prize.parent_region.dungeon.boss.can_defeat(state)) if not world.enemy_shuffle[player]: - add_rule(ep_boss, lambda state: state.can_shoot_arrows(player)) - add_rule(ep_prize, lambda state: state.can_shoot_arrows(player)) + add_rule(ep_boss, lambda state: can_shoot_arrows(state, player)) + add_rule(ep_prize, lambda state: can_shoot_arrows(state, player)) set_rule(world.get_location('Desert Palace - Big Chest', player), lambda state: state.has('Big Key (Desert Palace)', player)) set_rule(world.get_location('Desert Palace - Torch', player), lambda state: state.has('Pegasus Boots', player)) set_rule(world.get_entrance('Desert Palace East Wing', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player)) - set_rule(world.get_location('Desert Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and state.has_fire_source(player) and state.multiworld.get_location('Desert Palace - Prize', player).parent_region.dungeon.boss.can_defeat(state)) - set_rule(world.get_location('Desert Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and state.has_fire_source(player) and state.multiworld.get_location('Desert Palace - Boss', player).parent_region.dungeon.boss.can_defeat(state)) + set_rule(world.get_location('Desert Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and has_fire_source(state, player) and state.multiworld.get_location('Desert Palace - Prize', player).parent_region.dungeon.boss.can_defeat(state)) + set_rule(world.get_location('Desert Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and has_fire_source(state, player) and state.multiworld.get_location('Desert Palace - Boss', player).parent_region.dungeon.boss.can_defeat(state)) # logic patch to prevent placing a crystal in Desert that's required to reach the required keys if not (world.smallkey_shuffle[player] and world.bigkey_shuffle[player]): @@ -264,7 +271,7 @@ def global_rules(world, player): set_rule(world.get_entrance('Tower of Hera Small Key Door', player), lambda state: state._lttp_has_key('Small Key (Tower of Hera)', player) or item_name(state, 'Tower of Hera - Big Key Chest', player) == ('Small Key (Tower of Hera)', player)) set_rule(world.get_entrance('Tower of Hera Big Key Door', player), lambda state: state.has('Big Key (Tower of Hera)', player)) set_rule(world.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player)) - set_rule(world.get_location('Tower of Hera - Big Key Chest', player), lambda state: state.has_fire_source(player)) + set_rule(world.get_location('Tower of Hera - Big Key Chest', player), lambda state: has_fire_source(state, player)) if world.accessibility[player] != 'locations': set_always_allow(world.get_location('Tower of Hera - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Tower of Hera)' and item.player == player) @@ -292,18 +299,18 @@ def global_rules(world, player): set_rule(world.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player) or item_name(state, 'Skull Woods - Big Chest', player) == ('Big Key (Skull Woods)', player)) if world.accessibility[player] != 'locations': set_always_allow(world.get_location('Skull Woods - Big Chest', player), lambda state, item: item.name == 'Big Key (Skull Woods)' and item.player == player) - set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player) and state.has_sword(player)) # sword required for curtain + set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player) and has_sword(state, player)) # sword required for curtain - set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: state.can_melt_things(player)) + set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: can_melt_things(state, player)) set_rule(world.get_location('Ice Palace - Big Chest', player), lambda state: state.has('Big Key (Ice Palace)', player)) - set_rule(world.get_entrance('Ice Palace (Kholdstare)', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player) and state.has('Big Key (Ice Palace)', player) and (state._lttp_has_key('Small Key (Ice Palace)', player, 2) or (state.has('Cane of Somaria', player) and state._lttp_has_key('Small Key (Ice Palace)', player, 1)))) + set_rule(world.get_entrance('Ice Palace (Kholdstare)', player), lambda state: can_lift_rocks(state, player) and state.has('Hammer', player) and state.has('Big Key (Ice Palace)', player) and (state._lttp_has_key('Small Key (Ice Palace)', player, 2) or (state.has('Cane of Somaria', player) and state._lttp_has_key('Small Key (Ice Palace)', player, 1)))) set_rule(world.get_entrance('Ice Palace (East)', player), lambda state: (state.has('Hookshot', player) or ( item_in_locations(state, 'Big Key (Ice Palace)', player, [('Ice Palace - Spike Room', player), ('Ice Palace - Big Key Chest', player), ('Ice Palace - Map Chest', player)]) and state._lttp_has_key('Small Key (Ice Palace)', player))) and (state.multiworld.can_take_damage[player] or state.has('Hookshot', player) or state.has('Cape', player) or state.has('Cane of Byrna', player))) - set_rule(world.get_entrance('Ice Palace (East Top)', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player)) + set_rule(world.get_entrance('Ice Palace (East Top)', player), lambda state: can_lift_rocks(state, player) and state.has('Hammer', player)) - set_rule(world.get_entrance('Misery Mire Entrance Gap', player), lambda state: (state.has('Pegasus Boots', player) or state.has('Hookshot', player)) and (state.has_sword(player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Hammer', player) or state.has('Cane of Somaria', player) or state.can_shoot_arrows(player))) # need to defeat wizzrobes, bombs don't work ... + set_rule(world.get_entrance('Misery Mire Entrance Gap', player), lambda state: (state.has('Pegasus Boots', player) or state.has('Hookshot', player)) and (has_sword(state, player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Hammer', player) or state.has('Cane of Somaria', player) or can_shoot_arrows(state, player))) # need to defeat wizzrobes, bombs don't work ... set_rule(world.get_location('Misery Mire - Big Chest', player), lambda state: state.has('Big Key (Misery Mire)', player)) - set_rule(world.get_location('Misery Mire - Spike Chest', player), lambda state: (state.multiworld.can_take_damage[player] and state.has_hearts(player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player)) + set_rule(world.get_location('Misery Mire - Spike Chest', player), lambda state: (state.multiworld.can_take_damage[player] and has_hearts(state, player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player)) set_rule(world.get_entrance('Misery Mire Big Key Door', player), lambda state: state.has('Big Key (Misery Mire)', player)) # you can squander the free small key from the pot by opening the south door to the north west switch room, locking you out of accessing a color switch ... # big key gives backdoor access to that from the teleporter in the north west @@ -314,8 +321,8 @@ def global_rules(world, player): item_name(state, 'Misery Mire - Compass Chest', player) in [('Big Key (Misery Mire)', player)]) or ( item_name(state, 'Misery Mire - Big Key Chest', player) in [('Big Key (Misery Mire)', player)])) else state._lttp_has_key('Small Key (Misery Mire)', player, 3)) - set_rule(world.get_location('Misery Mire - Compass Chest', player), lambda state: state.has_fire_source(player)) - set_rule(world.get_location('Misery Mire - Big Key Chest', player), lambda state: state.has_fire_source(player)) + set_rule(world.get_location('Misery Mire - Compass Chest', player), lambda state: has_fire_source(state, player)) + set_rule(world.get_location('Misery Mire - Big Key Chest', player), lambda state: has_fire_source(state, player)) set_rule(world.get_entrance('Misery Mire (Vitreous)', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('Turtle Rock Entrance Gap', player), lambda state: state.has('Cane of Somaria', player)) @@ -335,10 +342,10 @@ def global_rules(world, player): set_rule(world.get_entrance('Turtle Rock (Trinexx)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4) and state.has('Big Key (Turtle Rock)', player) and state.has('Cane of Somaria', player)) if not world.enemy_shuffle[player]: - set_rule(world.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: state.can_shoot_arrows(player)) + set_rule(world.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: can_shoot_arrows(state, player)) set_rule(world.get_entrance('Palace of Darkness Hammer Peg Drop', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('Palace of Darkness Bridge Room', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 1)) # If we can reach any other small key door, we already have back door access to this area - set_rule(world.get_entrance('Palace of Darkness Big Key Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) and state.has('Big Key (Palace of Darkness)', player) and state.can_shoot_arrows(player) and state.has('Hammer', player)) + set_rule(world.get_entrance('Palace of Darkness Big Key Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) and state.has('Big Key (Palace of Darkness)', player) and can_shoot_arrows(state, player) and state.has('Hammer', player)) set_rule(world.get_entrance('Palace of Darkness (North)', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 4)) set_rule(world.get_location('Palace of Darkness - Big Chest', player), lambda state: state.has('Big Key (Palace of Darkness)', player)) @@ -399,9 +406,9 @@ def global_rules(world, player): lambda state: state.has('Big Key (Ganons Tower)', player)) else: set_rule(world.get_entrance('Ganons Tower Big Key Door', player), - lambda state: state.has('Big Key (Ganons Tower)', player) and state.can_shoot_arrows(player)) + lambda state: state.has('Big Key (Ganons Tower)', player) and can_shoot_arrows(state, player)) set_rule(world.get_entrance('Ganons Tower Torch Rooms', player), - lambda state: state.has_fire_source(player) and state.multiworld.get_entrance('Ganons Tower Torch Rooms', player).parent_region.dungeon.bosses['middle'].can_defeat(state)) + lambda state: has_fire_source(state, player) and state.multiworld.get_entrance('Ganons Tower Torch Rooms', player).parent_region.dungeon.bosses['middle'].can_defeat(state)) set_rule(world.get_location('Ganons Tower - Pre-Moldorm Chest', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3)) set_rule(world.get_entrance('Ganons Tower Moldorm Door', player), @@ -412,12 +419,12 @@ def global_rules(world, player): ganon = world.get_location('Ganon', player) set_rule(ganon, lambda state: GanonDefeatRule(state, player)) if world.goal[player] in ['ganontriforcehunt', 'localganontriforcehunt']: - add_rule(ganon, lambda state: state.has_triforce_pieces(state.multiworld.treasure_hunt_count[player], player)) + add_rule(ganon, lambda state: has_triforce_pieces(state, player)) elif world.goal[player] == 'ganonpedestal': add_rule(world.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player)) else: - add_rule(ganon, lambda state: state.has_crystals(state.multiworld.crystals_needed_for_ganon[player], player)) - set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has_beam_sword(player)) # need to damage ganon to get tiles to drop + add_rule(ganon, lambda state: has_crystals(state, state.multiworld.crystals_needed_for_ganon[player], player)) + set_rule(world.get_entrance('Ganon Drop', player), lambda state: has_beam_sword(state, player)) # need to damage ganon to get tiles to drop set_rule(world.get_location('Flute Activation Spot', player), lambda state: state.has('Flute', player)) @@ -426,51 +433,51 @@ def default_rules(world, player): """Default world rules when world state is not inverted.""" # overworld requirements set_rule(world.get_entrance('Kings Grave', player), lambda state: state.has('Pegasus Boots', player)) - set_rule(world.get_entrance('Kings Grave Outer Rocks', player), lambda state: state.can_lift_heavy_rocks(player)) - set_rule(world.get_entrance('Kings Grave Inner Rocks', player), lambda state: state.can_lift_heavy_rocks(player)) + set_rule(world.get_entrance('Kings Grave Outer Rocks', player), lambda state: can_lift_heavy_rocks(state, player)) + set_rule(world.get_entrance('Kings Grave Inner Rocks', player), lambda state: can_lift_heavy_rocks(state, player)) set_rule(world.get_entrance('Kings Grave Mirror Spot', player), lambda state: state.has('Moon Pearl', player) and state.has('Magic Mirror', player)) # Caution: If king's grave is releaxed at all to account for reaching it via a two way cave's exit in insanity mode, then the bomb shop logic will need to be updated (that would involve create a small ledge-like Region for it) set_rule(world.get_entrance('Bonk Fairy (Light)', player), lambda state: state.has('Pegasus Boots', player)) set_rule(world.get_entrance('Lumberjack Tree Tree', player), lambda state: state.has('Pegasus Boots', player) and state.has('Beat Agahnim 1', player)) set_rule(world.get_entrance('Bonk Rock Cave', player), lambda state: state.has('Pegasus Boots', player)) set_rule(world.get_entrance('Desert Palace Stairs', player), lambda state: state.has('Book of Mudora', player)) - set_rule(world.get_entrance('Sanctuary Grave', player), lambda state: state.can_lift_rocks(player)) - set_rule(world.get_entrance('20 Rupee Cave', player), lambda state: state.can_lift_rocks(player)) - set_rule(world.get_entrance('50 Rupee Cave', player), lambda state: state.can_lift_rocks(player)) - set_rule(world.get_entrance('Death Mountain Entrance Rock', player), lambda state: state.can_lift_rocks(player)) + set_rule(world.get_entrance('Sanctuary Grave', player), lambda state: can_lift_rocks(state, player)) + set_rule(world.get_entrance('20 Rupee Cave', player), lambda state: can_lift_rocks(state, player)) + set_rule(world.get_entrance('50 Rupee Cave', player), lambda state: can_lift_rocks(state, player)) + set_rule(world.get_entrance('Death Mountain Entrance Rock', player), lambda state: can_lift_rocks(state, player)) set_rule(world.get_entrance('Bumper Cave Entrance Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Flute Spot 1', player), lambda state: state.has('Activated Flute', player)) - set_rule(world.get_entrance('Lake Hylia Central Island Teleporter', player), lambda state: state.can_lift_heavy_rocks(player)) - set_rule(world.get_entrance('Dark Desert Teleporter', player), lambda state: state.has('Activated Flute', player) and state.can_lift_heavy_rocks(player)) - set_rule(world.get_entrance('East Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and state.has('Moon Pearl', player)) # bunny cannot use hammer - set_rule(world.get_entrance('South Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and state.has('Moon Pearl', player)) # bunny cannot use hammer - set_rule(world.get_entrance('Kakariko Teleporter', player), lambda state: ((state.has('Hammer', player) and state.can_lift_rocks(player)) or state.can_lift_heavy_rocks(player)) and state.has('Moon Pearl', player)) # bunny cannot lift bushes + set_rule(world.get_entrance('Lake Hylia Central Island Teleporter', player), lambda state: can_lift_heavy_rocks(state, player)) + set_rule(world.get_entrance('Dark Desert Teleporter', player), lambda state: state.has('Activated Flute', player) and can_lift_heavy_rocks(state, player)) + set_rule(world.get_entrance('East Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer + set_rule(world.get_entrance('South Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer + set_rule(world.get_entrance('Kakariko Teleporter', player), lambda state: ((state.has('Hammer', player) and can_lift_rocks(state, player)) or can_lift_heavy_rocks(state, player)) and state.has('Moon Pearl', player)) # bunny cannot lift bushes set_rule(world.get_location('Flute Spot', player), lambda state: state.has('Shovel', player)) set_rule(world.get_entrance('Bat Cave Drop Ledge', player), lambda state: state.has('Hammer', player)) set_rule(world.get_location('Zora\'s Ledge', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Waterfall of Wishing', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_location('Frog', player), lambda state: state.can_lift_heavy_rocks(player)) # will get automatic moon pearl requirement + set_rule(world.get_location('Frog', player), lambda state: can_lift_heavy_rocks(state, player)) # will get automatic moon pearl requirement set_rule(world.get_location('Potion Shop', player), lambda state: state.has('Mushroom', player)) - set_rule(world.get_entrance('Desert Palace Entrance (North) Rocks', player), lambda state: state.can_lift_rocks(player)) - set_rule(world.get_entrance('Desert Ledge Return Rocks', player), lambda state: state.can_lift_rocks(player)) # should we decide to place something that is not a dungeon end up there at some point - set_rule(world.get_entrance('Checkerboard Cave', player), lambda state: state.can_lift_rocks(player)) - set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has_beam_sword(player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle + set_rule(world.get_entrance('Desert Palace Entrance (North) Rocks', player), lambda state: can_lift_rocks(state, player)) + set_rule(world.get_entrance('Desert Ledge Return Rocks', player), lambda state: can_lift_rocks(state, player)) # should we decide to place something that is not a dungeon end up there at some point + set_rule(world.get_entrance('Checkerboard Cave', player), lambda state: can_lift_rocks(state, player)) + set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or has_beam_sword(state, player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle set_rule(world.get_entrance('Top of Pyramid', player), lambda state: state.has('Beat Agahnim 1', player)) set_rule(world.get_entrance('Old Man Cave Exit (West)', player), lambda state: False) # drop cannot be climbed up set_rule(world.get_entrance('Broken Bridge (West)', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Broken Bridge (East)', player), lambda state: state.has('Hookshot', player)) - set_rule(world.get_entrance('East Death Mountain Teleporter', player), lambda state: state.can_lift_heavy_rocks(player)) - set_rule(world.get_entrance('Fairy Ascension Rocks', player), lambda state: state.can_lift_heavy_rocks(player)) + set_rule(world.get_entrance('East Death Mountain Teleporter', player), lambda state: can_lift_heavy_rocks(state, player)) + set_rule(world.get_entrance('Fairy Ascension Rocks', player), lambda state: can_lift_heavy_rocks(state, player)) set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has('Mirror', player)) # can erase block set_rule(world.get_entrance('Death Mountain (Top)', player), lambda state: state.has('Hammer', player)) - set_rule(world.get_entrance('Turtle Rock Teleporter', player), lambda state: state.can_lift_heavy_rocks(player) and state.has('Hammer', player)) + set_rule(world.get_entrance('Turtle Rock Teleporter', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) set_rule(world.get_entrance('East Death Mountain (Top)', player), lambda state: state.has('Hammer', player)) - set_rule(world.get_entrance('Catfish Exit Rock', player), lambda state: state.can_lift_rocks(player)) - set_rule(world.get_entrance('Catfish Entrance Rock', player), lambda state: state.can_lift_rocks(player)) - set_rule(world.get_entrance('Northeast Dark World Broken Bridge Pass', player), lambda state: state.has('Moon Pearl', player) and (state.can_lift_rocks(player) or state.has('Hammer', player) or state.has('Flippers', player))) - set_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: state.has('Moon Pearl', player) and (state.can_lift_rocks(player) or state.has('Hammer', player))) + set_rule(world.get_entrance('Catfish Exit Rock', player), lambda state: can_lift_rocks(state, player)) + set_rule(world.get_entrance('Catfish Entrance Rock', player), lambda state: can_lift_rocks(state, player)) + set_rule(world.get_entrance('Northeast Dark World Broken Bridge Pass', player), lambda state: state.has('Moon Pearl', player) and (can_lift_rocks(state, player) or state.has('Hammer', player) or state.has('Flippers', player))) + set_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: state.has('Moon Pearl', player) and (can_lift_rocks(state, player) or state.has('Hammer', player))) set_rule(world.get_entrance('South Dark World Bridge', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Bonk Fairy (Dark)', player), lambda state: state.has('Moon Pearl', player) and state.has('Pegasus Boots', player)) set_rule(world.get_entrance('West Dark World Gap', player), lambda state: state.has('Moon Pearl', player) and state.has('Hookshot', player)) @@ -478,12 +485,12 @@ def default_rules(world, player): set_rule(world.get_entrance('Hyrule Castle Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Hyrule Castle Main Gate', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: (state.has('Moon Pearl', player) and state.has('Flippers', player) or state.has('Magic Mirror', player))) # Overworld Bunny Revival - set_rule(world.get_location('Bombos Tablet', player), lambda state: state.can_retrieve_tablet(player)) + set_rule(world.get_location('Bombos Tablet', player), lambda state: can_retrieve_tablet(state, player)) set_rule(world.get_entrance('Dark Lake Hylia Drop (South)', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # ToDo any fake flipper set up? set_rule(world.get_entrance('Dark Lake Hylia Ledge Fairy', player), lambda state: state.has('Moon Pearl', player)) # bomb required - set_rule(world.get_entrance('Dark Lake Hylia Ledge Spike Cave', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) + set_rule(world.get_entrance('Dark Lake Hylia Ledge Spike Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Village of Outcasts Heavy Rock', player), lambda state: state.has('Moon Pearl', player) and state.can_lift_heavy_rocks(player)) + set_rule(world.get_entrance('Village of Outcasts Heavy Rock', player), lambda state: state.has('Moon Pearl', player) and can_lift_heavy_rocks(state, player)) set_rule(world.get_entrance('Hype Cave', player), lambda state: state.has('Moon Pearl', player)) # bomb required set_rule(world.get_entrance('Brewery', player), lambda state: state.has('Moon Pearl', player)) # bomb required set_rule(world.get_entrance('Thieves Town', player), lambda state: state.has('Moon Pearl', player)) # bunny cannot pull @@ -497,26 +504,26 @@ def default_rules(world, player): set_rule(world.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('East Dark World River Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) set_rule(world.get_entrance('Graveyard Ledge Mirror Spot', player), lambda state: state.has('Moon Pearl', player) and state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Bumper Cave Entrance Rock', player), lambda state: state.has('Moon Pearl', player) and state.can_lift_rocks(player)) + set_rule(world.get_entrance('Bumper Cave Entrance Rock', player), lambda state: state.has('Moon Pearl', player) and can_lift_rocks(state, player)) set_rule(world.get_entrance('Bumper Cave Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Bat Cave Drop Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Dark World Hammer Peg Cave', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player)) - set_rule(world.get_entrance('Village of Outcasts Eastern Rocks', player), lambda state: state.has('Moon Pearl', player) and state.can_lift_heavy_rocks(player)) - set_rule(world.get_entrance('Peg Area Rocks', player), lambda state: state.has('Moon Pearl', player) and state.can_lift_heavy_rocks(player)) + set_rule(world.get_entrance('Village of Outcasts Eastern Rocks', player), lambda state: state.has('Moon Pearl', player) and can_lift_heavy_rocks(state, player)) + set_rule(world.get_entrance('Peg Area Rocks', player), lambda state: state.has('Moon Pearl', player) and can_lift_heavy_rocks(state, player)) set_rule(world.get_entrance('Village of Outcasts Pegs', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player)) set_rule(world.get_entrance('Grassy Lawn Pegs', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player)) set_rule(world.get_entrance('Bumper Cave Exit (Top)', player), lambda state: state.has('Cape', player)) set_rule(world.get_entrance('Bumper Cave Exit (Bottom)', player), lambda state: state.has('Cape', player) or state.has('Hookshot', player)) set_rule(world.get_entrance('Skull Woods Final Section', player), lambda state: state.has('Fire Rod', player) and state.has('Moon Pearl', player)) # bunny cannot use fire rod - set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has('Moon Pearl', player) and state.has_sword(player) and state.has_misery_mire_medallion(player)) # sword required to cast magic (!) + set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has('Moon Pearl', player) and has_sword(state, player) and has_misery_mire_medallion(state, player)) # sword required to cast magic (!) set_rule(world.get_entrance('Desert Ledge (Northeast) Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Desert Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Desert Palace Stairs Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Desert Palace Entrance (North) Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Spectacle Rock Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Hookshot Cave', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) + set_rule(world.get_entrance('Hookshot Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('East Death Mountain (Top) Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Mimic Cave Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) @@ -525,7 +532,7 @@ def default_rules(world, player): set_rule(world.get_entrance('Isolated Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Superbunny Cave Exit (Bottom)', player), lambda state: False) # Cannot get to bottom exit from top. Just exists for shuffling set_rule(world.get_entrance('Floating Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and state.has_sword(player) and state.has_turtle_rock_medallion(player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!) + set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and has_sword(state, player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!) set_rule(world.get_entrance('Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.open_pyramid[player].to_bool(world, player)) @@ -545,12 +552,12 @@ def inverted_rules(world, player): set_rule(world.get_entrance('Potion Shop Pier', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Light World Pier', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Kings Grave', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Kings Grave Outer Rocks', player), lambda state: state.can_lift_heavy_rocks(player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Kings Grave Inner Rocks', player), lambda state: state.can_lift_heavy_rocks(player) and state.has('Moon Pearl', player)) + set_rule(world.get_entrance('Kings Grave Outer Rocks', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(world.get_entrance('Kings Grave Inner Rocks', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Potion Shop Inner Bushes', player), lambda state: state.has('Moon Pearl', player)) set_rule(world.get_entrance('Potion Shop Outer Bushes', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Potion Shop Outer Rock', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Potion Shop Inner Rock', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) + set_rule(world.get_entrance('Potion Shop Outer Rock', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(world.get_entrance('Potion Shop Inner Rock', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Graveyard Cave Inner Bushes', player), lambda state: state.has('Moon Pearl', player)) set_rule(world.get_entrance('Graveyard Cave Outer Bushes', player), lambda state: state.has('Moon Pearl', player)) set_rule(world.get_entrance('Secret Passage Inner Bushes', player), lambda state: state.has('Moon Pearl', player)) @@ -560,23 +567,23 @@ def inverted_rules(world, player): set_rule(world.get_entrance('Lumberjack Tree Tree', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player) and state.has('Beat Agahnim 1', player)) set_rule(world.get_entrance('Bonk Rock Cave', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Desert Palace Stairs', player), lambda state: state.has('Book of Mudora', player)) # bunny can use book - set_rule(world.get_entrance('Sanctuary Grave', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('20 Rupee Cave', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('50 Rupee Cave', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Death Mountain Entrance Rock', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) + set_rule(world.get_entrance('Sanctuary Grave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(world.get_entrance('20 Rupee Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(world.get_entrance('50 Rupee Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(world.get_entrance('Death Mountain Entrance Rock', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Bumper Cave Entrance Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Dark Lake Hylia Central Island Teleporter', player), lambda state: state.can_lift_heavy_rocks(player)) - set_rule(world.get_entrance('Dark Desert Teleporter', player), lambda state: state.has('Activated Flute', player) and state.can_lift_heavy_rocks(player)) - set_rule(world.get_entrance('East Dark World Teleporter', player), lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and state.has('Moon Pearl', player)) # bunny cannot use hammer - set_rule(world.get_entrance('South Dark World Teleporter', player), lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and state.has('Moon Pearl', player)) # bunny cannot use hammer - set_rule(world.get_entrance('West Dark World Teleporter', player), lambda state: ((state.has('Hammer', player) and state.can_lift_rocks(player)) or state.can_lift_heavy_rocks(player)) and state.has('Moon Pearl', player)) + set_rule(world.get_entrance('Dark Lake Hylia Central Island Teleporter', player), lambda state: can_lift_heavy_rocks(state, player)) + set_rule(world.get_entrance('Dark Desert Teleporter', player), lambda state: state.has('Activated Flute', player) and can_lift_heavy_rocks(state, player)) + set_rule(world.get_entrance('East Dark World Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer + set_rule(world.get_entrance('South Dark World Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer + set_rule(world.get_entrance('West Dark World Teleporter', player), lambda state: ((state.has('Hammer', player) and can_lift_rocks(state, player)) or can_lift_heavy_rocks(state, player)) and state.has('Moon Pearl', player)) set_rule(world.get_location('Flute Spot', player), lambda state: state.has('Shovel', player) and state.has('Moon Pearl', player)) set_rule(world.get_location('Zora\'s Ledge', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Waterfall of Wishing Cave', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Northeast Light World Return', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) - set_rule(world.get_location('Frog', player), lambda state: state.can_lift_heavy_rocks(player) and (state.has('Moon Pearl', player) or state.has('Beat Agahnim 1', player)) or (state.can_reach('Light World', 'Region', player) and state.has('Magic Mirror', player))) # Need LW access using Mirror or Portal + set_rule(world.get_location('Frog', player), lambda state: can_lift_heavy_rocks(state, player) and (state.has('Moon Pearl', player) or state.has('Beat Agahnim 1', player)) or (state.can_reach('Light World', 'Region', player) and state.has('Magic Mirror', player))) # Need LW access using Mirror or Portal set_rule(world.get_location('Missing Smith', player), lambda state: state.has('Get Frog', player) and state.can_reach('Blacksmiths Hut', 'Region', player)) # Can't S&Q with smith set_rule(world.get_location('Blacksmith', player), lambda state: state.has('Return Smith', player)) set_rule(world.get_location('Magic Bat', player), lambda state: state.has('Magic Powder', player) and state.has('Moon Pearl', player)) @@ -591,52 +598,52 @@ def inverted_rules(world, player): set_rule(world.get_entrance('North Fairy Cave Drop', player), lambda state: state.has('Moon Pearl', player)) set_rule(world.get_entrance('Lost Woods Hideout Drop', player), lambda state: state.has('Moon Pearl', player)) set_rule(world.get_location('Potion Shop', player), lambda state: state.has('Mushroom', player) and (state.can_reach('Potion Shop Area', 'Region', player))) # new inverted region, need pearl for bushes or access to potion shop door/waterfall fairy - set_rule(world.get_entrance('Desert Palace Entrance (North) Rocks', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Desert Ledge Return Rocks', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) # should we decide to place something that is not a dungeon end up there at some point - set_rule(world.get_entrance('Checkerboard Cave', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) + set_rule(world.get_entrance('Desert Palace Entrance (North) Rocks', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(world.get_entrance('Desert Ledge Return Rocks', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # should we decide to place something that is not a dungeon end up there at some point + set_rule(world.get_entrance('Checkerboard Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Hyrule Castle Secret Entrance Drop', player), lambda state: state.has('Moon Pearl', player)) set_rule(world.get_entrance('Old Man Cave Exit (West)', player), lambda state: False) # drop cannot be climbed up set_rule(world.get_entrance('Broken Bridge (West)', player), lambda state: state.has('Hookshot', player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Broken Bridge (East)', player), lambda state: state.has('Hookshot', player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Dark Death Mountain Teleporter (East Bottom)', player), lambda state: state.can_lift_heavy_rocks(player)) - set_rule(world.get_entrance('Fairy Ascension Rocks', player), lambda state: state.can_lift_heavy_rocks(player) and state.has('Moon Pearl', player)) + set_rule(world.get_entrance('Dark Death Mountain Teleporter (East Bottom)', player), lambda state: can_lift_heavy_rocks(state, player)) + set_rule(world.get_entrance('Fairy Ascension Rocks', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has('Mirror', player)) # can erase block set_rule(world.get_entrance('Death Mountain (Top)', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Dark Death Mountain Teleporter (East)', player), lambda state: state.can_lift_heavy_rocks(player) and state.has('Hammer', player) and state.has('Moon Pearl', player)) # bunny cannot use hammer + set_rule(world.get_entrance('Dark Death Mountain Teleporter (East)', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Hammer', player) and state.has('Moon Pearl', player)) # bunny cannot use hammer set_rule(world.get_entrance('East Death Mountain (Top)', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player)) # bunny can not use hammer - set_rule(world.get_entrance('Catfish Entrance Rock', player), lambda state: state.can_lift_rocks(player)) - set_rule(world.get_entrance('Northeast Dark World Broken Bridge Pass', player), lambda state: ((state.can_lift_rocks(player) or state.has('Hammer', player)) or state.has('Flippers', player))) - set_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: (state.can_lift_rocks(player) or state.has('Hammer', player))) + set_rule(world.get_entrance('Catfish Entrance Rock', player), lambda state: can_lift_rocks(state, player)) + set_rule(world.get_entrance('Northeast Dark World Broken Bridge Pass', player), lambda state: ((can_lift_rocks(state, player) or state.has('Hammer', player)) or state.has('Flippers', player))) + set_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: (can_lift_rocks(state, player) or state.has('Hammer', player))) set_rule(world.get_entrance('South Dark World Bridge', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('Bonk Fairy (Dark)', player), lambda state: state.has('Pegasus Boots', player)) set_rule(world.get_entrance('West Dark World Gap', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_location('Bombos Tablet', player), lambda state: state.can_retrieve_tablet(player)) + set_rule(world.get_location('Bombos Tablet', player), lambda state: can_retrieve_tablet(state, player)) set_rule(world.get_entrance('Dark Lake Hylia Drop (South)', player), lambda state: state.has('Flippers', player)) # ToDo any fake flipper set up? set_rule(world.get_entrance('Dark Lake Hylia Ledge Pier', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_entrance('Dark Lake Hylia Ledge Spike Cave', player), lambda state: state.can_lift_rocks(player)) + set_rule(world.get_entrance('Dark Lake Hylia Ledge Spike Cave', player), lambda state: can_lift_rocks(state, player)) set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Flippers', player)) # Fake Flippers set_rule(world.get_entrance('Dark Lake Hylia Shallows', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_entrance('Village of Outcasts Heavy Rock', player), lambda state: state.can_lift_heavy_rocks(player)) + set_rule(world.get_entrance('Village of Outcasts Heavy Rock', player), lambda state: can_lift_heavy_rocks(state, player)) set_rule(world.get_entrance('East Dark World Bridge', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('East Dark World River Pier', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_entrance('Bumper Cave Entrance Rock', player), lambda state: state.can_lift_rocks(player)) + set_rule(world.get_entrance('Bumper Cave Entrance Rock', player), lambda state: can_lift_rocks(state, player)) set_rule(world.get_entrance('Bumper Cave Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Hammer Peg Area Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Dark World Hammer Peg Cave', player), lambda state: state.has('Hammer', player)) - set_rule(world.get_entrance('Village of Outcasts Eastern Rocks', player), lambda state: state.can_lift_heavy_rocks(player)) - set_rule(world.get_entrance('Peg Area Rocks', player), lambda state: state.can_lift_heavy_rocks(player)) + set_rule(world.get_entrance('Village of Outcasts Eastern Rocks', player), lambda state: can_lift_heavy_rocks(state, player)) + set_rule(world.get_entrance('Peg Area Rocks', player), lambda state: can_lift_heavy_rocks(state, player)) set_rule(world.get_entrance('Village of Outcasts Pegs', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('Grassy Lawn Pegs', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('Bumper Cave Exit (Top)', player), lambda state: state.has('Cape', player)) set_rule(world.get_entrance('Bumper Cave Exit (Bottom)', player), lambda state: state.has('Cape', player) or state.has('Hookshot', player)) set_rule(world.get_entrance('Skull Woods Final Section', player), lambda state: state.has('Fire Rod', player)) - set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has_sword(player) and state.has_misery_mire_medallion(player)) # sword required to cast magic (!) + set_rule(world.get_entrance('Misery Mire', player), lambda state: has_sword(state, player) and has_misery_mire_medallion(state, player)) # sword required to cast magic (!) - set_rule(world.get_entrance('Hookshot Cave', player), lambda state: state.can_lift_rocks(player)) + set_rule(world.get_entrance('Hookshot Cave', player), lambda state: can_lift_rocks(state, player)) set_rule(world.get_entrance('East Death Mountain Mirror Spot (Top)', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Death Mountain (Top) Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) @@ -646,7 +653,7 @@ def inverted_rules(world, player): set_rule(world.get_entrance('Dark Death Mountain Ledge Mirror Spot (West)', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Laser Bridge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) set_rule(world.get_entrance('Floating Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has_sword(player) and state.has_turtle_rock_medallion(player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!) + set_rule(world.get_entrance('Turtle Rock', player), lambda state: has_sword(state, player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!) # new inverted spots set_rule(world.get_entrance('Post Aga Teleporter', player), lambda state: state.has('Beat Agahnim 1', player)) @@ -687,7 +694,7 @@ def inverted_rules(world, player): def no_glitches_rules(world, player): """""" if world.mode[player] == 'inverted': - set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Moon Pearl', player) and (state.has('Flippers', player) or state.can_lift_rocks(player))) + set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Moon Pearl', player) and (state.has('Flippers', player) or can_lift_rocks(state, player))) set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to set_rule(world.get_entrance('Lake Hylia Island Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to set_rule(world.get_entrance('Lake Hylia Warp', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to @@ -698,7 +705,7 @@ def no_glitches_rules(world, player): set_rule(world.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('East Dark World Pier', player), lambda state: state.has('Flippers', player)) else: - set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Flippers', player) or state.can_lift_rocks(player)) + set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Flippers', player) or can_lift_rocks(state, player)) set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Flippers', player)) # can be fake flippered to set_rule(world.get_entrance('Hobo Bridge', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) @@ -820,19 +827,19 @@ def open_rules(world, player): def swordless_rules(world, player): - set_rule(world.get_entrance('Agahnim 1', player), lambda state: (state.has('Hammer', player) or state.has('Fire Rod', player) or state.can_shoot_arrows(player) or state.has('Cane of Somaria', player)) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2)) + set_rule(world.get_entrance('Agahnim 1', player), lambda state: (state.has('Hammer', player) or state.has('Fire Rod', player) or can_shoot_arrows(state, player) or state.has('Cane of Somaria', player)) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2)) set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player)) # no curtain set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player)) #in swordless mode bombos pads are present in the relevant parts of ice palace set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has('Hammer', player)) # need to damage ganon to get tiles to drop if world.mode[player] != 'inverted': set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has('Hammer', player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle - set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and state.has_turtle_rock_medallion(player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!) - set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has('Moon Pearl', player) and state.has_misery_mire_medallion(player)) # sword not required to use medallion for opening in swordless (!) + set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!) + set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has('Moon Pearl', player) and has_misery_mire_medallion(state, player)) # sword not required to use medallion for opening in swordless (!) else: # only need ddm access for aga tower in inverted - set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has_turtle_rock_medallion(player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!) - set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has_misery_mire_medallion(player)) # sword not required to use medallion for opening in swordless (!) + set_rule(world.get_entrance('Turtle Rock', player), lambda state: has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!) + set_rule(world.get_entrance('Misery Mire', player), lambda state: has_misery_mire_medallion(state, player)) # sword not required to use medallion for opening in swordless (!) def add_connection(parent_name, target_name, entrance_name, world, player): @@ -1083,7 +1090,7 @@ def set_big_bomb_rules(world, player): # returning via the eastern and southern teleporters needs the same items, so we use the southern teleporter for out routing. # crossing preg bridge already requires hammer so we just add the gloves to the requirement def southern_teleporter(state): - return state.can_lift_rocks(player) and cross_peg_bridge(state) + return can_lift_rocks(state, player) and cross_peg_bridge(state) # the basic routes assume you can reach eastern light world with the bomb. # you can then use the southern teleporter, or (if you have beaten Aga1) the hyrule castle gate warp @@ -1110,13 +1117,13 @@ def set_big_bomb_rules(world, player): #1. Mirror and basic routes #2. Go to south DW and then cross peg bridge: Need Mitts and hammer and moon pearl # -> (Mitts and CPB) or (M and BR) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.can_lift_heavy_rocks(player) and cross_peg_bridge(state)) or (state.has('Magic Mirror', player) and basic_routes(state))) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (can_lift_heavy_rocks(state, player) and cross_peg_bridge(state)) or (state.has('Magic Mirror', player) and basic_routes(state))) elif bombshop_entrance.name == 'Bumper Cave (Bottom)': #1. Mirror and Lift rock and basic_routes #2. Mirror and Flute and basic routes (can make difference if accessed via insanity or w/ mirror from connector, and then via hyrule castle gate, because no gloves are needed in that case) #3. Go to south DW and then cross peg bridge: Need Mitts and hammer and moon pearl # -> (Mitts and CPB) or (((G or Flute) and M) and BR)) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.can_lift_heavy_rocks(player) and cross_peg_bridge(state)) or (((state.can_lift_rocks(player) or state.has('Flute', player)) and state.has('Magic Mirror', player)) and basic_routes(state))) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (can_lift_heavy_rocks(state, player) and cross_peg_bridge(state)) or (((can_lift_rocks(state, player) or state.has('Flute', player)) and state.has('Magic Mirror', player)) and basic_routes(state))) elif bombshop_entrance.name in Southern_DW_entrances: #1. Mirror and enter via gate: Need mirror and Aga1 #2. cross peg bridge: Need hammer and moon pearl @@ -1144,7 +1151,7 @@ def set_big_bomb_rules(world, player): elif bombshop_entrance.name == 'Fairy Ascension Cave (Bottom)': # Same as East_LW_DM_entrances except navigation without BR requires Mitts # -> Flute and ((M and Hookshot and Mitts) or BR) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and ((state.has('Magic Mirror', player) and state.has('Hookshot', player) and state.can_lift_heavy_rocks(player)) or basic_routes(state))) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and ((state.has('Magic Mirror', player) and state.has('Hookshot', player) and can_lift_heavy_rocks(state, player)) or basic_routes(state))) elif bombshop_entrance.name in Castle_ledge_entrances: # 1. mirror on pyramid to castle ledge, grab bomb, return through mirror spot: Needs mirror # 2. flute then basic routes @@ -1160,7 +1167,7 @@ def set_big_bomb_rules(world, player): # 1. Lift rock then basic_routes # 2. flute then basic_routes # -> (Flute or G) and BR - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or state.can_lift_rocks(player)) and basic_routes(state)) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or can_lift_rocks(state, player)) and basic_routes(state)) elif bombshop_entrance.name == 'Graveyard Cave': # 1. flute then basic routes # 2. (has west dark world access) use existing mirror spot (required Pearl), mirror again off ledge @@ -1176,13 +1183,13 @@ def set_big_bomb_rules(world, player): # 2. walk down by hammering peg: needs hammer and pearl # 3. mirror and basic routes # -> (P and (H or Gloves)) or (M and BR) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Moon Pearl', player) and (state.has('Hammer', player) or state.can_lift_rocks(player))) or (state.has('Magic Mirror', player) and basic_routes(state))) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Moon Pearl', player) and (state.has('Hammer', player) or can_lift_rocks(state, player))) or (state.has('Magic Mirror', player) and basic_routes(state))) elif bombshop_entrance.name == 'Kings Grave': # same as the Normal_LW_entrances case except that the pre-existing mirror is only possible if you have mitts # (because otherwise mirror was used to reach the grave, so would cancel a pre-existing mirror spot) # to account for insanity, must consider a way to escape without a cave for basic_routes # -> (M and Mitts) or ((Mitts or Flute or (M and P and West Dark World access)) and BR) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.can_lift_heavy_rocks(player) and state.has('Magic Mirror', player)) or ((state.can_lift_heavy_rocks(player) or state.has('Activated Flute', player) or (state.can_reach('West Dark World', 'Region', player) and state.has('Moon Pearl', player) and state.has('Magic Mirror', player))) and basic_routes(state))) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (can_lift_heavy_rocks(state, player) and state.has('Magic Mirror', player)) or ((can_lift_heavy_rocks(state, player) or state.has('Activated Flute', player) or (state.can_reach('West Dark World', 'Region', player) and state.has('Moon Pearl', player) and state.has('Magic Mirror', player))) and basic_routes(state))) elif bombshop_entrance.name == 'Waterfall of Wishing': # same as the Normal_LW_entrances case except in insanity it's possible you could be here without Flippers which # means you need an escape route of either Flippers or Flute @@ -1329,7 +1336,7 @@ def set_inverted_big_bomb_rules(world, player): elif bombshop_entrance.name in Northern_DW_entrances: # You can just fly with the Flute, you can take a long walk with Mitts and Hammer, # or you can leave a Mirror portal nearby and then walk to the castle to Mirror again. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (state.can_lift_heavy_rocks(player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player))) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player))) elif bombshop_entrance.name in Southern_DW_entrances: # This is the same as north DW without the Mitts rock present. add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Hammer', player) or state.has('Activated Flute', player) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player))) @@ -1341,22 +1348,22 @@ def set_inverted_big_bomb_rules(world, player): add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player))) elif bombshop_entrance.name in LW_bush_entrances: # These entrances are behind bushes in LW so you need either Pearl or the tools to solve NDW bomb shop locations. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and (state.has('Activated Flute', player) or state.has('Moon Pearl', player) or (state.can_lift_heavy_rocks(player) and state.has('Hammer', player)))) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and (state.has('Activated Flute', player) or state.has('Moon Pearl', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)))) elif bombshop_entrance.name == 'Village of Outcasts Shop': # This is mostly the same as NDW but the Mirror path requires the Pearl, or using the Hammer - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (state.can_lift_heavy_rocks(player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player) and (state.has('Moon Pearl', player) or state.has('Hammer', player)))) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player) and (state.has('Moon Pearl', player) or state.has('Hammer', player)))) elif bombshop_entrance.name == 'Bumper Cave (Bottom)': # This is mostly the same as NDW but the Mirror path requires being able to lift a rock. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (state.can_lift_heavy_rocks(player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_lift_rocks(player) and state.can_reach('Light World', 'Region', player))) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and can_lift_rocks(state, player) and state.can_reach('Light World', 'Region', player))) elif bombshop_entrance.name == 'Old Man Cave (West)': # The three paths back are Mirror and DW walk, Mirror and Flute, or LW walk and then Mirror. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and ((state.can_lift_heavy_rocks(player) and state.has('Hammer', player)) or (state.can_lift_rocks(player) and state.has('Moon Pearl', player)) or state.has('Activated Flute', player))) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and ((can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (can_lift_rocks(state, player) and state.has('Moon Pearl', player)) or state.has('Activated Flute', player))) elif bombshop_entrance.name == 'Dark World Potion Shop': # You either need to Flute to 5 or cross the rock/hammer choice pass to the south. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or state.has('Hammer', player) or state.can_lift_rocks(player)) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or state.has('Hammer', player) or can_lift_rocks(state, player)) elif bombshop_entrance.name == 'Kings Grave': # Either lift the rock and walk to the castle to Mirror or Mirror immediately and Flute. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or state.can_lift_heavy_rocks(player)) and state.has('Magic Mirror', player)) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or can_lift_heavy_rocks(state, player)) and state.has('Magic Mirror', player)) elif bombshop_entrance.name == 'Waterfall of Wishing': # You absolutely must be able to swim to return it from here. add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player) and state.has('Magic Mirror', player)) @@ -1421,7 +1428,7 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool): if region.name == 'Swamp Palace (Entrance)': # Need to 0hp revive - not in logic return lambda state: state.has('Moon Pearl', player) if region.name == 'Tower of Hera (Bottom)': # Need to hit the crystal switch - return lambda state: state.has('Magic Mirror', player) and state.has_sword(player) or state.has('Moon Pearl', player) + return lambda state: state.has('Magic Mirror', player) and has_sword(state, player) or state.has('Moon Pearl', player) if region.name in OverworldGlitchRules.get_invalid_bunny_revival_dungeons(): return lambda state: state.has('Magic Mirror', player) or state.has('Moon Pearl', player) if region.type == LTTPRegionType.Dungeon: @@ -1459,7 +1466,7 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool): # For glitch rulesets, establish superbunny and revival rules. if world.logic[player] in ['minorglitches', 'owglitches', 'hybridglitches', 'nologic'] and entrance.name not in OverworldGlitchRules.get_invalid_bunny_revival_dungeons(): if region.name in OverworldGlitchRules.get_sword_required_superbunny_mirror_regions(): - possible_options.append(lambda state: path_to_access_rule(new_path, entrance) and state.has('Magic Mirror', player) and state.has_sword(player)) + possible_options.append(lambda state: path_to_access_rule(new_path, entrance) and state.has('Magic Mirror', player) and has_sword(state, player)) elif (region.name in OverworldGlitchRules.get_boots_required_superbunny_mirror_regions() or location is not None and location.name in OverworldGlitchRules.get_boots_required_superbunny_mirror_locations()): possible_options.append(lambda state: path_to_access_rule(new_path, entrance) and state.has('Magic Mirror', player) and state.has('Pegasus Boots', player)) diff --git a/worlds/alttp/StateHelpers.py b/worlds/alttp/StateHelpers.py new file mode 100644 index 0000000000..33cea8fbfb --- /dev/null +++ b/worlds/alttp/StateHelpers.py @@ -0,0 +1,137 @@ +from .SubClasses import LTTPRegion +from BaseClasses import CollectionState + +def is_not_bunny(state: CollectionState, region: LTTPRegion, player: int) -> bool: + if state.has('Moon Pearl', player): + return True + + return region.is_light_world if state.multiworld.mode[player] != 'inverted' else region.is_dark_world + +def can_bomb_clip(state: CollectionState, region: LTTPRegion, player: int) -> bool: + return is_not_bunny(state, region, player) and state.has('Pegasus Boots', player) + +def can_buy_unlimited(state: CollectionState, item: str, player: int) -> bool: + return any(shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(state) for + shop in state.multiworld.shops) + +def can_buy(state: CollectionState, item: str, player: int) -> bool: + return any(shop.region.player == player and shop.has(item) and shop.region.can_reach(state) for + shop in state.multiworld.shops) + +def can_shoot_arrows(state: CollectionState, player: int) -> bool: + if state.multiworld.retro_bow[player]: + return (state.has('Bow', player) or state.has('Silver Bow', player)) and can_buy(state, 'Single Arrow', player) + return state.has('Bow', player) or state.has('Silver Bow', player) + +def has_triforce_pieces(state: CollectionState, player: int) -> bool: + count = state.multiworld.treasure_hunt_count[player] + return state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= count + +def has_crystals(state: CollectionState, count: int, player: int) -> bool: + found = state.count_group("Crystals", player) + return found >= count + +def can_lift_rocks(state: CollectionState, player: int): + return state.has('Power Glove', player) or state.has('Titans Mitts', player) + +def can_lift_heavy_rocks(state: CollectionState, player: int) -> bool: + return state.has('Titans Mitts', player) + +def bottle_count(state: CollectionState, player: int) -> int: + return min(state.multiworld.difficulty_requirements[player].progressive_bottle_limit, + state.count_group("Bottles", player)) + +def has_hearts(state: CollectionState, player: int, count: int) -> int: + # Warning: This only considers items that are marked as advancement items + return heart_count(state, player) >= count + +def heart_count(state: CollectionState, player: int) -> int: + # Warning: This only considers items that are marked as advancement items + diff = state.multiworld.difficulty_requirements[player] + return min(state.item_count('Boss Heart Container', player), diff.boss_heart_container_limit) \ + + state.item_count('Sanctuary Heart Container', player) \ + + min(state.item_count('Piece of Heart', player), diff.heart_piece_limit) // 4 \ + + 3 # starting hearts + +def can_extend_magic(state: CollectionState, player: int, smallmagic: int = 16, + fullrefill: bool = False): # This reflects the total magic Link has, not the total extra he has. + basemagic = 8 + if state.has('Magic Upgrade (1/4)', player): + basemagic = 32 + elif state.has('Magic Upgrade (1/2)', player): + basemagic = 16 + if can_buy_unlimited(state, 'Green Potion', player) or can_buy_unlimited(state, 'Blue Potion', player): + if state.multiworld.item_functionality[player] == 'hard' and not fullrefill: + basemagic = basemagic + int(basemagic * 0.5 * bottle_count(state, player)) + elif state.multiworld.item_functionality[player] == 'expert' and not fullrefill: + basemagic = basemagic + int(basemagic * 0.25 * bottle_count(state, player)) + else: + basemagic = basemagic + basemagic * bottle_count(state, player) + return basemagic >= smallmagic + +def can_kill_most_things(state: CollectionState, player: int, enemies: int = 5) -> bool: + return (has_melee_weapon(state, player) + or state.has('Cane of Somaria', player) + or (state.has('Cane of Byrna', player) and (enemies < 6 or can_extend_magic(state, player))) + or can_shoot_arrows(state, player) + or state.has('Fire Rod', player) + or (state.has('Bombs (10)', player) and enemies < 6)) + +def can_get_good_bee(state: CollectionState, player: int) -> bool: + cave = state.multiworld.get_region('Good Bee Cave', player) + return ( + state.has_group("Bottles", player) and + state.has('Bug Catching Net', player) and + (state.has('Pegasus Boots', player) or (has_sword(state, player) and state.has('Quake', player))) and + cave.can_reach(state) and + is_not_bunny(state, cave, player) + ) + +def can_retrieve_tablet(state: CollectionState, player: int) -> bool: + return state.has('Book of Mudora', player) and (has_beam_sword(state, player) or + (state.multiworld.swordless[player] and + state.has("Hammer", player))) + +def has_sword(state: CollectionState, player: int) -> bool: + return state.has('Fighter Sword', player) \ + or state.has('Master Sword', player) \ + or state.has('Tempered Sword', player) \ + or state.has('Golden Sword', player) + +def has_beam_sword(state: CollectionState, player: int) -> bool: + return state.has('Master Sword', player) or state.has('Tempered Sword', player) or state.has('Golden Sword', + player) + +def has_melee_weapon(state: CollectionState, player: int) -> bool: + return has_sword(state, player) or state.has('Hammer', player) + +def has_fire_source(state: CollectionState, player: int) -> bool: + return state.has('Fire Rod', player) or state.has('Lamp', player) + +def can_melt_things(state: CollectionState, player: int) -> bool: + return state.has('Fire Rod', player) or \ + (state.has('Bombos', player) and + (state.multiworld.swordless[player] or + has_sword(state, player))) + +def has_misery_mire_medallion(state: CollectionState, player: int) -> bool: + return state.has(state.multiworld.required_medallions[player][0], player) + +def has_turtle_rock_medallion(state: CollectionState, player: int) -> bool: + return state.has(state.multiworld.required_medallions[player][1], player) + +def can_boots_clip_lw(state: CollectionState, player: int) -> bool: + if state.multiworld.mode[player] == 'inverted': + return state.has('Pegasus Boots', player) and state.has('Moon Pearl', player) + return state.has('Pegasus Boots', player) + +def can_boots_clip_dw(state: CollectionState, player: int) -> bool: + if state.multiworld.mode[player] != 'inverted': + return state.has('Pegasus Boots', player) and state.has('Moon Pearl', player) + return state.has('Pegasus Boots', player) + +def can_get_glitched_speed_dw(state: CollectionState, player: int) -> bool: + rules = [state.has('Pegasus Boots', player), any([state.has('Hookshot', player), has_sword(state, player)])] + if state.multiworld.mode[player] != 'inverted': + rules.append(state.has('Moon Pearl', player)) + return all(rules) \ No newline at end of file diff --git a/worlds/alttp/SubClasses.py b/worlds/alttp/SubClasses.py index 50f3ca47d9..5fc2aa0ba3 100644 --- a/worlds/alttp/SubClasses.py +++ b/worlds/alttp/SubClasses.py @@ -4,7 +4,6 @@ from enum import IntEnum from BaseClasses import Location, Item, ItemClassification, Region, MultiWorld - class ALttPLocation(Location): game: str = "A Link to the Past" crystal: bool @@ -81,6 +80,12 @@ class LTTPRegionType(IntEnum): class LTTPRegion(Region): type: LTTPRegionType + # will be set after making connections. + is_light_world: bool = False + is_dark_world: bool = False + + shop: Optional = None + def __init__(self, name: str, type_: LTTPRegionType, hint: str, player: int, multiworld: MultiWorld): super().__init__(name, player, multiworld, hint) self.type = type_ diff --git a/worlds/alttp/UnderworldGlitchRules.py b/worlds/alttp/UnderworldGlitchRules.py index f7e7736702..f3d78e365c 100644 --- a/worlds/alttp/UnderworldGlitchRules.py +++ b/worlds/alttp/UnderworldGlitchRules.py @@ -1,6 +1,8 @@ from BaseClasses import Entrance +from .SubClasses import LTTPRegion from worlds.generic.Rules import set_rule, add_rule +from .StateHelpers import can_bomb_clip, has_sword, has_beam_sword, has_fire_source, can_melt_things, has_misery_mire_medallion # We actually need the logic to properly "mark" these regions as Light or Dark world. # Therefore we need to make these connections during the normal link_entrances stage, rather than during set_rules. @@ -46,9 +48,9 @@ def dungeon_reentry_rules(world, player, clip: Entrance, dungeon_region: str, du if dungeon_entrance.name == 'Skull Woods Final Section': set_rule(clip, lambda state: False) # entrance doesn't exist until you fire rod it from the other side elif dungeon_entrance.name == 'Misery Mire': - add_rule(clip, lambda state: state.has_sword(player) and state.has_misery_mire_medallion(player)) # open the dungeon + add_rule(clip, lambda state: has_sword(state, player) and has_misery_mire_medallion(state, player)) # open the dungeon elif dungeon_entrance.name == 'Agahnims Tower': - add_rule(clip, lambda state: state.has('Cape', player) or state.has_beam_sword(player) or state.has('Beat Agahnim 1', player)) # kill/bypass barrier + add_rule(clip, lambda state: state.has('Cape', player) or has_beam_sword(state, player) or state.has('Beat Agahnim 1', player)) # kill/bypass barrier # Then we set a restriction on exiting the dungeon, so you can't leave unless you got in normally. add_rule(world.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state)) elif not fix_fake_worlds: # full, dungeonsfull; fixed dungeon exits, but no fake worlds fix @@ -66,21 +68,21 @@ def underworld_glitches_rules(world, player): # Ice Palace Entrance Clip # This is the easiest one since it's a simple internal clip. Just need to also add melting to freezor chest since it's otherwise assumed. - add_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: state.can_bomb_clip(world.get_region('Ice Palace (Entrance)', player), player), combine='or') - add_rule(world.get_location('Ice Palace - Freezor Chest', player), lambda state: state.can_melt_things(player)) + add_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: can_bomb_clip(state, world.get_region('Ice Palace (Entrance)', player), player), combine='or') + add_rule(world.get_location('Ice Palace - Freezor Chest', player), lambda state: can_melt_things(state, player)) # Kiki Skip kikiskip = world.get_entrance('Kiki Skip', player) - set_rule(kikiskip, lambda state: state.can_bomb_clip(kikiskip.parent_region, player)) + set_rule(kikiskip, lambda state: can_bomb_clip(state, kikiskip.parent_region, player)) dungeon_reentry_rules(world, player, kikiskip, 'Palace of Darkness (Entrance)', 'Palace of Darkness Exit') # Mire -> Hera -> Swamp # Using mire keys on other dungeon doors mire = world.get_region('Misery Mire (West)', player) - mire_clip = lambda state: state.can_reach('Misery Mire (West)', 'Region', player) and state.can_bomb_clip(mire, player) and state.has_fire_source(player) - hera_clip = lambda state: state.can_reach('Tower of Hera (Top)', 'Region', player) and state.can_bomb_clip(world.get_region('Tower of Hera (Top)', player), player) + mire_clip = lambda state: state.can_reach('Misery Mire (West)', 'Region', player) and can_bomb_clip(state, mire, player) and has_fire_source(state, player) + hera_clip = lambda state: state.can_reach('Tower of Hera (Top)', 'Region', player) and can_bomb_clip(state, world.get_region('Tower of Hera (Top)', player), player) add_rule(world.get_entrance('Tower of Hera Big Key Door', player), lambda state: mire_clip(state) and state.has('Big Key (Misery Mire)', player), combine='or') add_rule(world.get_entrance('Swamp Palace Small Key Door', player), lambda state: mire_clip(state), combine='or') add_rule(world.get_entrance('Swamp Palace (Center)', player), lambda state: mire_clip(state) or hera_clip(state), combine='or') diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 5f33b152b8..4121dbadf2 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -23,6 +23,7 @@ from .Rules import set_rules from .Shops import create_shops, ShopSlotFill, ShopType, price_rate_display, price_type_display_name from .SubClasses import ALttPItem, LTTPRegionType from worlds.AutoWorld import World, WebWorld, LogicMixin +from .StateHelpers import can_buy_unlimited lttp_logger = logging.getLogger("A Link to the Past") @@ -673,5 +674,5 @@ class ALttPLogic(LogicMixin): if self.multiworld.logic[player] == 'nologic': return True if self.multiworld.smallkey_shuffle[player] == smallkey_shuffle.option_universal: - return self.can_buy_unlimited('Small Key (Universal)', player) + return can_buy_unlimited(self, 'Small Key (Universal)', player) return self.prog_items[item, player] >= count From cd234fc04a7430c355980027acc9100476db0789 Mon Sep 17 00:00:00 2001 From: Jarno Date: Sat, 4 Mar 2023 16:31:44 +0100 Subject: [PATCH 018/172] Timespinner: Fixed Dry lake serene oddity (#1501) --- worlds/timespinner/Options.py | 2 +- worlds/timespinner/PreCalculatedWeights.py | 66 ++++++++++------------ worlds/timespinner/__init__.py | 4 +- 3 files changed, 32 insertions(+), 40 deletions(-) diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py index 0448e93dc4..5f4d230688 100644 --- a/worlds/timespinner/Options.py +++ b/worlds/timespinner/Options.py @@ -357,7 +357,7 @@ class RisingTidesOverrides(OptionDict): "CastleBasement": { "Dry": 66, "Flooded": 17, "FloodedWithSavePointAvailable": 17 }, "CastleCourtyard": { "Dry": 67, "Flooded": 33 }, "LakeDesolation": { "Dry": 67, "Flooded": 33 }, - "LakeSerene": { "Dry": 67, "Flooded": 33 }, + "LakeSerene": { "Dry": 33, "Flooded": 67 }, } diff --git a/worlds/timespinner/PreCalculatedWeights.py b/worlds/timespinner/PreCalculatedWeights.py index 8501ef73a0..514a17f8ac 100644 --- a/worlds/timespinner/PreCalculatedWeights.py +++ b/worlds/timespinner/PreCalculatedWeights.py @@ -1,7 +1,6 @@ from typing import Tuple, Dict, Union from BaseClasses import MultiWorld -from .Options import is_option_enabled, get_option_value - +from .Options import timespinner_options, is_option_enabled, get_option_value class PreCalculatedWeights: pyramid_keys_unlock: str @@ -24,21 +23,22 @@ class PreCalculatedWeights: weights_overrrides: Dict[str, Union[str, Dict[str, int]]] = self.get_flood_weights_overrides(world, player) self.flood_basement, self.flood_basement_high = \ - self.roll_flood_setting_with_available_save(world, player, weights_overrrides, "CastleBasement") - self.flood_xarion = self.roll_flood_setting(world, player, weights_overrrides, "Xarion") - self.flood_maw = self.roll_flood_setting(world, player, weights_overrrides, "Maw") - self.flood_pyramid_shaft = self.roll_flood_setting(world, player, weights_overrrides, "AncientPyramidShaft") - self.flood_pyramid_back = self.roll_flood_setting(world, player, weights_overrrides, "Sandman") - self.flood_moat = self.roll_flood_setting(world, player, weights_overrrides, "CastleMoat") - self.flood_courtyard = self.roll_flood_setting(world, player, weights_overrrides, "CastleCourtyard") - self.flood_lake_desolation = self.roll_flood_setting(world, player, weights_overrrides, "LakeDesolation") - self.dry_lake_serene = self.roll_flood_setting(world, player, weights_overrrides, "LakeSerene") + self.roll_flood_setting(world, player, weights_overrrides, "CastleBasement") + self.flood_xarion, _ = self.roll_flood_setting(world, player, weights_overrrides, "Xarion") + self.flood_maw, _ = self.roll_flood_setting(world, player, weights_overrrides, "Maw") + self.flood_pyramid_shaft, _ = self.roll_flood_setting(world, player, weights_overrrides, "AncientPyramidShaft") + self.flood_pyramid_back, _ = self.roll_flood_setting(world, player, weights_overrrides, "Sandman") + self.flood_moat, _ = self.roll_flood_setting(world, player, weights_overrrides, "CastleMoat") + self.flood_courtyard, _ = self.roll_flood_setting(world, player, weights_overrrides, "CastleCourtyard") + self.flood_lake_desolation, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeDesolation") + flood_lake_serene, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeSerene") + self.dry_lake_serene = not flood_lake_serene self.pyramid_keys_unlock, self.present_key_unlock, self.past_key_unlock, self.time_key_unlock = \ - self.get_pyramid_keys_unlock(world, player, self.flood_maw) + self.get_pyramid_keys_unlocks(world, player, self.flood_maw) - - def get_pyramid_keys_unlock(self, world: MultiWorld, player: int, is_maw_flooded: bool) -> Tuple[str, str, str, str]: + @staticmethod + def get_pyramid_keys_unlocks(world: MultiWorld, player: int, is_maw_flooded: bool) -> Tuple[str, str, str, str]: present_teleportation_gates: Tuple[str, ...] = ( "GateKittyBoss", "GateLeftLibrary", @@ -87,37 +87,29 @@ class PreCalculatedWeights: ) @staticmethod - def get_flood_weights_overrides( world: MultiWorld, player: int) -> Dict[str, Union[str, Dict[str, int]]]: + def get_flood_weights_overrides(world: MultiWorld, player: int) -> Dict[str, Union[str, Dict[str, int]]]: weights_overrides_option: Union[int, Dict[str, Union[str, Dict[str, int]]]] = \ get_option_value(world, player, "RisingTidesOverrides") - if weights_overrides_option == 0: - return {} + default_weights: Dict[str, Dict[str, int]] = timespinner_options["RisingTidesOverrides"].default + + if not weights_overrides_option: + weights_overrides_option = default_weights else: - return weights_overrides_option + for key, weights in default_weights.items(): + if not key in weights_overrides_option: + weights_overrides_option[key] = weights + + return weights_overrides_option @staticmethod - def roll_flood_setting(world: MultiWorld, player: int, weights: Dict[str, Union[Dict[str, int], str]], key: str) -> bool: - if not world or not is_option_enabled(world, player, "RisingTides"): - return False - - weights = weights[key] if key in weights else { "Dry": 67, "Flooded": 33 } - - if isinstance(weights, dict): - result: str = world.random.choices(list(weights.keys()), weights=list(map(int, weights.values())))[0] - else: - result: str = weights - - return result == "Flooded" - - @staticmethod - def roll_flood_setting_with_available_save(world: MultiWorld, player: int, - weights: Dict[str, Union[Dict[str, int], str]], key: str) -> Tuple[bool, bool]: + def roll_flood_setting(world: MultiWorld, player: int, + all_weights: Dict[str, Union[Dict[str, int], str]], key: str) -> Tuple[bool, bool]: if not world or not is_option_enabled(world, player, "RisingTides"): return False, False - weights = weights[key] if key in weights else {"Dry": 66, "Flooded": 17, "FloodedWithSavePointAvailable": 17} + weights: Union[Dict[str, int], str] = all_weights[key] if isinstance(weights, dict): result: str = world.random.choices(list(weights.keys()), weights=list(map(int, weights.values())))[0] @@ -127,6 +119,6 @@ class PreCalculatedWeights: if result == "Dry": return False, False elif result == "Flooded": - return True, False - elif result == "FloodedWithSavePointAvailable": return True, True + elif result == "FloodedWithSavePointAvailable": + return True, False diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index d31a4f1968..d9640727d7 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -144,8 +144,8 @@ class TimespinnerWorld(World): flooded_areas.append("Castle Courtyard") if self.precalculated_weights.flood_lake_desolation: flooded_areas.append("Lake Desolation") - if self.precalculated_weights.dry_lake_serene: - flooded_areas.append("Dry Lake Serene") + if not self.precalculated_weights.dry_lake_serene: + flooded_areas.append("Lake Serene") if len(flooded_areas) == 0: flooded_areas_string: str = "None" From 30b70b2055f4743773e3defccc4eb9a5279af231 Mon Sep 17 00:00:00 2001 From: recklesscoder <57289227+recklesscoder@users.noreply.github.com> Date: Sat, 4 Mar 2023 16:34:10 +0100 Subject: [PATCH 019/172] Misc collected fixes (#1497) --- CommonClient.py | 2 +- Generate.py | 2 +- Main.py | 3 +-- WebHostLib/static/assets/tracker.js | 2 ++ WebHostLib/templates/macros.html | 2 +- worlds/factorio/Technologies.py | 2 +- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 92f8d76a66..02dd55da98 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -63,7 +63,7 @@ class ClientCommandProcessor(CommandProcessor): def _cmd_received(self) -> bool: """List all received items""" - logger.info(f'{len(self.ctx.items_received)} received items:') + self.output(f'{len(self.ctx.items_received)} received items:') for index, item in enumerate(self.ctx.items_received, 1): self.output(f"{self.ctx.item_names[item.item]} from {self.ctx.player_names[item.player]}") return True diff --git a/Generate.py b/Generate.py index dadabd7ac6..afb34f11c6 100644 --- a/Generate.py +++ b/Generate.py @@ -107,7 +107,7 @@ def main(args=None, callback=ERmain): player_files = {} for file in os.scandir(args.player_files_path): fname = file.name - if file.is_file() and not file.name.startswith(".") and \ + if file.is_file() and not fname.startswith(".") and \ os.path.join(args.player_files_path, fname) not in {args.meta_file_path, args.weights_file_path}: path = os.path.join(args.player_files_path, fname) try: diff --git a/Main.py b/Main.py index 2ef61f5d38..ba1787b078 100644 --- a/Main.py +++ b/Main.py @@ -38,7 +38,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No world = MultiWorld(args.multi) logger = logging.getLogger() - world.set_seed(seed, args.race, str(args.outputname if args.outputname else world.seed)) + world.set_seed(seed, args.race, str(args.outputname) if args.outputname else None) world.plando_options = args.plando_options world.shuffle = args.shuffle.copy() @@ -53,7 +53,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No world.enemy_damage = args.enemy_damage.copy() world.beemizer_total_chance = args.beemizer_total_chance.copy() world.beemizer_trap_chance = args.beemizer_trap_chance.copy() - world.timer = args.timer.copy() world.countdown_start_time = args.countdown_start_time.copy() world.red_clock_time = args.red_clock_time.copy() world.blue_clock_time = args.blue_clock_time.copy() diff --git a/WebHostLib/static/assets/tracker.js b/WebHostLib/static/assets/tracker.js index 23e7f979a5..1a24b95c79 100644 --- a/WebHostLib/static/assets/tracker.js +++ b/WebHostLib/static/assets/tracker.js @@ -1,5 +1,7 @@ const adjustTableHeight = () => { const tablesContainer = document.getElementById('tables-container'); + if (!tablesContainer) + return; const upperDistance = tablesContainer.getBoundingClientRect().top; const containerHeight = window.innerHeight - upperDistance; diff --git a/WebHostLib/templates/macros.html b/WebHostLib/templates/macros.html index ba6f33a9d8..927ec10ffe 100644 --- a/WebHostLib/templates/macros.html +++ b/WebHostLib/templates/macros.html @@ -22,7 +22,7 @@ {% for patch in room.seed.slots|list|sort(attribute="player_id") %} {{ patch.player_id }} - {{ patch.player_name }} + {{ patch.player_name }} {{ patch.game }} {% if patch.game == "Minecraft" %} diff --git a/worlds/factorio/Technologies.py b/worlds/factorio/Technologies.py index 2db7f23dea..1c1939ee24 100644 --- a/worlds/factorio/Technologies.py +++ b/worlds/factorio/Technologies.py @@ -422,7 +422,7 @@ for root in sorted_rows: progressive = progressive_rows[root] assert all(tech in tech_table for tech in progressive), "declared a progressive technology without base technology" factorio_id += 1 - progressive_technology = Technology(root, technology_table[progressive_rows[root][0]].ingredients, factorio_id, + progressive_technology = Technology(root, technology_table[progressive[0]].ingredients, factorio_id, progressive, has_modifier=any(technology_table[tech].has_modifier for tech in progressive), unlocks=any(technology_table[tech].unlocks for tech in progressive)) From 96d7a3a64c485c02cfdd5b95c35123d5e1088a01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Bolduc?= <16137441+Jouramie@users.noreply.github.com> Date: Sat, 4 Mar 2023 12:34:51 -0500 Subject: [PATCH 020/172] Stardew Valley: Fix generation issue with Master Angler goal and vanilla tools (#1498) * - Can Catch every fish doesn't need fishing rods if they are not shuffled * add has_max_fishing_rod * add test for master angler + vanilla tools --------- Co-authored-by: Alex Gilbert --- worlds/stardew_valley/logic.py | 7 ++++++- worlds/stardew_valley/test/TestOptions.py | 8 ++++++++ worlds/stardew_valley/test/__init__.py | 8 +++++++- 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 worlds/stardew_valley/test/TestOptions.py diff --git a/worlds/stardew_valley/logic.py b/worlds/stardew_valley/logic.py index 79b2b63ddf..85a5bb08d4 100644 --- a/worlds/stardew_valley/logic.py +++ b/worlds/stardew_valley/logic.py @@ -946,11 +946,16 @@ class StardewLogic: return region_rule & season_rule & difficulty_rule def can_catch_every_fish(self) -> StardewRule: - rules = [self.has_skill_level("Fishing", 10), self.received("Progressive Fishing Rod", 4)] + rules = [self.has_skill_level("Fishing", 10), self.has_max_fishing_rod()] for fish in all_fish_items: rules.append(self.can_catch_fish(fish)) return _And(rules) + def has_max_fishing_rod(self) -> StardewRule: + if self.options[options.ToolProgression] == options.ToolProgression.option_progressive: + return self.received("Progressive Fishing Rod", 4) + return self.can_get_fishing_xp() + def can_cook(self) -> StardewRule: return self.has_house(1) or self.has_skill_level("Foraging", 9) diff --git a/worlds/stardew_valley/test/TestOptions.py b/worlds/stardew_valley/test/TestOptions.py new file mode 100644 index 0000000000..063d9c2be9 --- /dev/null +++ b/worlds/stardew_valley/test/TestOptions.py @@ -0,0 +1,8 @@ +from worlds.stardew_valley.test import SVTestBase + + +class TestMasterAnglerVanillaTools(SVTestBase): + options = { + "goal": "master_angler", + "tool_progression": "vanilla", + } diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index 1ddf037641..c9a8c74667 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -11,4 +11,10 @@ class SVTestBase(WorldTestBase): def world_setup(self, *args, **kwargs): super().world_setup(*args, **kwargs) - self.world = self.multiworld.worlds[self.player] + if self.constructed: + self.world = self.multiworld.worlds[self.player] + + @property + def run_default_tests(self) -> bool: + # world_setup is overridden, so it'd always run default tests when importing SVTestBase + return type(self) is not SVTestBase and super().run_default_tests From e78800d1bccccd5197354d6418ae11fc7688d5da Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Wed, 1 Mar 2023 22:43:59 +0100 Subject: [PATCH 021/172] Item Plando: make world selection deterministic --- Fill.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Fill.py b/Fill.py index ac3ae8fc6d..92b57af58b 100644 --- a/Fill.py +++ b/Fill.py @@ -840,8 +840,7 @@ def distribute_planned(world: MultiWorld) -> None: maxcount = placement['count']['target'] from_pool = placement['from_pool'] - candidates = list(location for location in world.get_unfilled_locations_for_players(locations, - worlds)) + candidates = list(world.get_unfilled_locations_for_players(locations, sorted(worlds))) world.random.shuffle(candidates) world.random.shuffle(items) count = 0 From 3a68ce3faa2b0e1b2a71ba581f85481e8a2eddd9 Mon Sep 17 00:00:00 2001 From: Joethepic <60947591+Joethepic@users.noreply.github.com> Date: Sun, 5 Mar 2023 03:08:32 -0600 Subject: [PATCH 022/172] PKMN: Make Exp All early (#1422) --- worlds/pokemon_rb/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index abe309222e..88813ad940 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -171,6 +171,7 @@ class PokemonRedBlueWorld(World): # damage being reduced by 1 which leads to a "not very effective" message appearing due to my changes # to the way effectiveness messages are generated. self.type_chart = sorted(chart, key=lambda matchup: -matchup[2]) + self.multiworld.early_items[self.player]["Exp. All"] = 1 def create_items(self) -> None: start_inventory = self.multiworld.start_inventory[self.player].value.copy() From efb2ab4505bcdb30ddb2771905f864f9b62df3cd Mon Sep 17 00:00:00 2001 From: Rosalie-A <61372066+Rosalie-A@users.noreply.github.com> Date: Sun, 5 Mar 2023 07:31:31 -0500 Subject: [PATCH 023/172] TLoZ: Implementing The Legend of Zelda (#1354) Co-authored-by: t3hf1gm3nt <59876300+t3hf1gm3nt@users.noreply.github.com> Co-authored-by: Alchav <59858495+Alchav@users.noreply.github.com> --- .gitignore | 1 + Launcher.py | 2 + README.md | 1 + Utils.py | 5 + Zelda1Client.py | 393 +++++++++++ data/lua/TLoZ/TheLegendOfZeldaConnector.lua | 702 ++++++++++++++++++++ data/lua/TLoZ/core.dll | Bin 0 -> 29184 bytes data/lua/TLoZ/json.lua | 380 +++++++++++ data/lua/TLoZ/socket.lua | 132 ++++ host.yaml | 11 +- worlds/tloz/ItemPool.py | 145 ++++ worlds/tloz/Items.py | 147 ++++ worlds/tloz/Locations.py | 350 ++++++++++ worlds/tloz/Options.py | 40 ++ worlds/tloz/Rom.py | 78 +++ worlds/tloz/Rules.py | 147 ++++ worlds/tloz/__init__.py | 313 +++++++++ worlds/tloz/docs/en_The Legend of Zelda.md | 43 ++ worlds/tloz/docs/multiworld_en.md | 104 +++ worlds/tloz/requirements.txt | 1 + worlds/tloz/z1_base_patch.bsdiff4 | Bin 0 -> 1648 bytes 21 files changed, 2994 insertions(+), 1 deletion(-) create mode 100644 Zelda1Client.py create mode 100644 data/lua/TLoZ/TheLegendOfZeldaConnector.lua create mode 100644 data/lua/TLoZ/core.dll create mode 100644 data/lua/TLoZ/json.lua create mode 100644 data/lua/TLoZ/socket.lua create mode 100644 worlds/tloz/ItemPool.py create mode 100644 worlds/tloz/Items.py create mode 100644 worlds/tloz/Locations.py create mode 100644 worlds/tloz/Options.py create mode 100644 worlds/tloz/Rom.py create mode 100644 worlds/tloz/Rules.py create mode 100644 worlds/tloz/__init__.py create mode 100644 worlds/tloz/docs/en_The Legend of Zelda.md create mode 100644 worlds/tloz/docs/multiworld_en.md create mode 100644 worlds/tloz/requirements.txt create mode 100644 worlds/tloz/z1_base_patch.bsdiff4 diff --git a/.gitignore b/.gitignore index e269202db9..c964b929f1 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ *.apm3 *.apmc *.apz5 +*.aptloz *.pyc *.pyd *.sfc diff --git a/Launcher.py b/Launcher.py index 27510a882d..c4d9b6fea0 100644 --- a/Launcher.py +++ b/Launcher.py @@ -148,6 +148,8 @@ components: Iterable[Component] = ( Component('FF1 Client', 'FF1Client'), # Pokémon Component('Pokemon Client', 'PokemonClient', file_identifier=SuffixIdentifier('.apred', '.apblue')), + # TLoZ + Component('Zelda 1 Client', 'Zelda1Client'), # ChecksFinder Component('ChecksFinder Client', 'ChecksFinderClient'), # Starcraft 2 diff --git a/README.md b/README.md index 8b72dcf685..9e6ed2b1eb 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Currently, the following games are supported: * Blasphemous * Wargroove * Stardew Valley +* The Legend of Zelda For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/Utils.py b/Utils.py index 098d5f01e7..3ec3d4ff56 100644 --- a/Utils.py +++ b/Utils.py @@ -310,6 +310,11 @@ def get_default_options() -> OptionsType: "lufia2ac_options": { "rom_file": "Lufia II - Rise of the Sinistrals (USA).sfc", }, + "tloz_options": { + "rom_file": "Legend of Zelda, The (U) (PRG0) [!].nes", + "rom_start": True, + "display_msgs": True, + }, "wargroove_options": { "root_directory": "C:/Program Files (x86)/Steam/steamapps/common/Wargroove" } diff --git a/Zelda1Client.py b/Zelda1Client.py new file mode 100644 index 0000000000..a325e4aebe --- /dev/null +++ b/Zelda1Client.py @@ -0,0 +1,393 @@ +# Based (read: copied almost wholesale and edited) off the FF1 Client. + +import asyncio +import copy +import json +import logging +import os +import subprocess +import time +import typing +from asyncio import StreamReader, StreamWriter +from typing import List + +import Utils +from Utils import async_start +from worlds import lookup_any_location_id_to_name +from CommonClient import CommonContext, server_loop, gui_enabled, console_loop, ClientCommandProcessor, logger, \ + get_base_parser + +from worlds.tloz.Items import item_game_ids +from worlds.tloz.Locations import location_ids +from worlds.tloz import Items, Locations, Rom + +SYSTEM_MESSAGE_ID = 0 + +CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart Zelda_connector.lua" +CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure Zelda_connector.lua is running" +CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart Zelda_connector.lua" +CONNECTION_TENTATIVE_STATUS = "Initial Connection Made" +CONNECTION_CONNECTED_STATUS = "Connected" +CONNECTION_INITIAL_STATUS = "Connection has not been initiated" + +DISPLAY_MSGS = True + +item_ids = item_game_ids +location_ids = location_ids +items_by_id = {id: item for item, id in item_ids.items()} +locations_by_id = {id: location for location, id in location_ids.items()} + + +class ZeldaCommandProcessor(ClientCommandProcessor): + + def _cmd_nes(self): + """Check NES Connection State""" + if isinstance(self.ctx, ZeldaContext): + logger.info(f"NES Status: {self.ctx.nes_status}") + + def _cmd_toggle_msgs(self): + """Toggle displaying messages in bizhawk""" + global DISPLAY_MSGS + DISPLAY_MSGS = not DISPLAY_MSGS + logger.info(f"Messages are now {'enabled' if DISPLAY_MSGS else 'disabled'}") + + +class ZeldaContext(CommonContext): + command_processor = ZeldaCommandProcessor + items_handling = 0b101 # get sent remote and starting items + # Infinite Hyrule compatibility + overworld_item = 0x5F + armos_item = 0x24 + + def __init__(self, server_address, password): + super().__init__(server_address, password) + self.bonus_items = [] + self.nes_streams: (StreamReader, StreamWriter) = None + self.nes_sync_task = None + self.messages = {} + self.locations_array = None + self.nes_status = CONNECTION_INITIAL_STATUS + self.game = 'The Legend of Zelda' + self.awaiting_rom = False + self.shop_slots_left = 0 + self.shop_slots_middle = 0 + self.shop_slots_right = 0 + self.shop_slots = [self.shop_slots_left, self.shop_slots_middle, self.shop_slots_right] + self.slot_data = dict() + + async def server_auth(self, password_requested: bool = False): + if password_requested and not self.password: + await super(ZeldaContext, self).server_auth(password_requested) + if not self.auth: + self.awaiting_rom = True + logger.info('Awaiting connection to NES to get Player information') + return + + await self.send_connect() + + def _set_message(self, msg: str, msg_id: int): + if DISPLAY_MSGS: + self.messages[(time.time(), msg_id)] = msg + + def on_package(self, cmd: str, args: dict): + if cmd == 'Connected': + self.slot_data = args.get("slot_data", {}) + asyncio.create_task(parse_locations(self.locations_array, self, True)) + elif cmd == 'Print': + msg = args['text'] + if ': !' not in msg: + self._set_message(msg, SYSTEM_MESSAGE_ID) + + def on_print_json(self, args: dict): + if self.ui: + self.ui.print_json(copy.deepcopy(args["data"])) + else: + text = self.jsontotextparser(copy.deepcopy(args["data"])) + logger.info(text) + relevant = args.get("type", None) in {"Hint", "ItemSend"} + if relevant: + item = args["item"] + # goes to this world + if self.slot_concerns_self(args["receiving"]): + relevant = True + # found in this world + elif self.slot_concerns_self(item.player): + relevant = True + # not related + else: + relevant = False + if relevant: + item = args["item"] + msg = self.raw_text_parser(copy.deepcopy(args["data"])) + self._set_message(msg, item.item) + + def run_gui(self): + from kvui import GameManager + + class ZeldaManager(GameManager): + logging_pairs = [ + ("Client", "Archipelago") + ] + base_title = "Archipelago Zelda 1 Client" + + self.ui = ZeldaManager(self) + self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + + +def get_payload(ctx: ZeldaContext): + current_time = time.time() + bonus_items = [item for item in ctx.bonus_items] + return json.dumps( + { + "items": [item.item for item in ctx.items_received], + "messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items() + if key[0] > current_time - 10}, + "shops": { + "left": ctx.shop_slots_left, + "middle": ctx.shop_slots_middle, + "right": ctx.shop_slots_right + }, + "bonusItems": bonus_items + } + ) + + +def reconcile_shops(ctx: ZeldaContext): + checked_location_names = [lookup_any_location_id_to_name[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] + right_slots = [shop for shop in shops if "Right" in shop] + for shop in left_slots: + ctx.shop_slots_left |= get_shop_bit_from_name(shop) + for shop in middle_slots: + ctx.shop_slots_middle |= get_shop_bit_from_name(shop) + for shop in right_slots: + ctx.shop_slots_right |= get_shop_bit_from_name(shop) + + +def get_shop_bit_from_name(location_name): + if "Potion" in location_name: + return Rom.potion_shop + elif "Arrow" in location_name: + return Rom.arrow_shop + elif "Shield" in location_name: + return Rom.shield_shop + elif "Ring" in location_name: + return Rom.ring_shop + elif "Candle" in location_name: + return Rom.candle_shop + elif "Take" in location_name: + return Rom.take_any + return 0 # this should never be hit + + +async def parse_locations(locations_array, ctx: ZeldaContext, force: bool, zone="None"): + if locations_array == ctx.locations_array and not force: + return + else: + # print("New values") + ctx.locations_array = locations_array + locations_checked = [] + location = None + for location in ctx.missing_locations: + location_name = lookup_any_location_id_to_name[location] + + if location_name in Locations.overworld_locations and zone == "overworld": + status = locations_array[Locations.major_location_offsets[location_name]] + if location_name == "Ocean Heart Container": + status = locations_array[ctx.overworld_item] + if location_name == "Armos Knights": + status = locations_array[ctx.armos_item] + if status & 0x10: + ctx.locations_checked.add(location) + locations_checked.append(location) + elif location_name in Locations.underworld1_locations and zone == "underworld1": + status = locations_array[Locations.floor_location_game_offsets_early[location_name]] + if status & 0x10: + ctx.locations_checked.add(location) + locations_checked.append(location) + elif location_name in Locations.underworld2_locations and zone == "underworld2": + status = locations_array[Locations.floor_location_game_offsets_late[location_name]] + if status & 0x10: + ctx.locations_checked.add(location) + locations_checked.append(location) + elif (location_name in Locations.shop_locations or "Take" in location_name) and zone == "caves": + shop_bit = get_shop_bit_from_name(location_name) + slot = 0 + context_slot = 0 + if "Left" in location_name: + slot = "slot1" + context_slot = 0 + elif "Middle" in location_name: + slot = "slot2" + context_slot = 1 + elif "Right" in location_name: + slot = "slot3" + context_slot = 2 + if locations_array[slot] & shop_bit > 0: + locations_checked.append(location) + ctx.shop_slots[context_slot] |= shop_bit + if locations_array["takeAnys"] and locations_array["takeAnys"] >= 4: + if "Take Any" in location_name: + short_name = None + if "Left" in location_name: + short_name = "TakeAnyLeft" + elif "Middle" in location_name: + short_name = "TakeAnyMiddle" + elif "Right" in location_name: + short_name = "TakeAnyRight" + if short_name is not None: + item_code = ctx.slot_data[short_name] + if item_code > 0: + ctx.bonus_items.append(item_code) + locations_checked.append(location) + if locations_checked: + await ctx.send_msgs([ + {"cmd": "LocationChecks", + "locations": locations_checked} + ]) + + +async def nes_sync_task(ctx: ZeldaContext): + logger.info("Starting nes connector. Use /nes for status information") + while not ctx.exit_event.is_set(): + error_status = None + if ctx.nes_streams: + (reader, writer) = ctx.nes_streams + msg = get_payload(ctx).encode() + writer.write(msg) + writer.write(b'\n') + try: + await asyncio.wait_for(writer.drain(), timeout=1.5) + try: + # Data will return a dict with up to two fields: + # 1. A keepalive response of the Players Name (always) + # 2. An array representing the memory values of the locations area (if in game) + data = await asyncio.wait_for(reader.readline(), timeout=5) + data_decoded = json.loads(data.decode()) + if data_decoded["overworldHC"] is not None: + ctx.overworld_item = data_decoded["overworldHC"] + if data_decoded["overworldPB"] is not None: + ctx.armos_item = data_decoded["overworldPB"] + if data_decoded['gameMode'] == 19 and ctx.finished_game == False: + await ctx.send_msgs([ + {"cmd": "StatusUpdate", + "status": 30} + ]) + ctx.finished_game = True + if ctx.game is not None and 'overworld' in data_decoded: + # Not just a keep alive ping, parse + asyncio.create_task(parse_locations(data_decoded['overworld'], ctx, False, "overworld")) + if ctx.game is not None and 'underworld1' in data_decoded: + asyncio.create_task(parse_locations(data_decoded['underworld1'], ctx, False, "underworld1")) + if ctx.game is not None and 'underworld2' in data_decoded: + asyncio.create_task(parse_locations(data_decoded['underworld2'], ctx, False, "underworld2")) + if ctx.game is not None and 'caves' in data_decoded: + asyncio.create_task(parse_locations(data_decoded['caves'], ctx, False, "caves")) + if not ctx.auth: + ctx.auth = ''.join([chr(i) for i in data_decoded['playerName'] if i != 0]) + if ctx.auth == '': + logger.info("Invalid ROM detected. No player name built into the ROM. Please regenerate" + "the ROM using the same link but adding your slot name") + if ctx.awaiting_rom: + await ctx.server_auth(False) + reconcile_shops(ctx) + except asyncio.TimeoutError: + logger.debug("Read Timed Out, Reconnecting") + error_status = CONNECTION_TIMING_OUT_STATUS + writer.close() + ctx.nes_streams = None + except ConnectionResetError as e: + logger.debug("Read failed due to Connection Lost, Reconnecting") + error_status = CONNECTION_RESET_STATUS + writer.close() + ctx.nes_streams = None + except TimeoutError: + logger.debug("Connection Timed Out, Reconnecting") + error_status = CONNECTION_TIMING_OUT_STATUS + writer.close() + ctx.nes_streams = None + except ConnectionResetError: + logger.debug("Connection Lost, Reconnecting") + error_status = CONNECTION_RESET_STATUS + writer.close() + ctx.nes_streams = None + if ctx.nes_status == CONNECTION_TENTATIVE_STATUS: + if not error_status: + logger.info("Successfully Connected to NES") + ctx.nes_status = CONNECTION_CONNECTED_STATUS + else: + ctx.nes_status = f"Was tentatively connected but error occured: {error_status}" + elif error_status: + ctx.nes_status = error_status + logger.info("Lost connection to nes and attempting to reconnect. Use /nes for status updates") + else: + try: + logger.debug("Attempting to connect to NES") + ctx.nes_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 52980), timeout=10) + ctx.nes_status = CONNECTION_TENTATIVE_STATUS + except TimeoutError: + logger.debug("Connection Timed Out, Trying Again") + ctx.nes_status = CONNECTION_TIMING_OUT_STATUS + continue + except ConnectionRefusedError: + logger.debug("Connection Refused, Trying Again") + ctx.nes_status = CONNECTION_REFUSED_STATUS + continue + + +if __name__ == '__main__': + # Text Mode to use !hint and such with games that have no text entry + Utils.init_logging("ZeldaClient") + + options = Utils.get_options() + DISPLAY_MSGS = options["tloz_options"]["display_msgs"] + + + async def run_game(romfile: str) -> None: + auto_start = typing.cast(typing.Union[bool, str], + Utils.get_options()["tloz_options"].get("rom_start", True)) + if auto_start is True: + import webbrowser + webbrowser.open(romfile) + elif isinstance(auto_start, str) and os.path.isfile(auto_start): + subprocess.Popen([auto_start, romfile], + stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + + async def main(args): + if args.diff_file: + import Patch + logging.info("Patch file was supplied. Creating nes rom..") + meta, romfile = Patch.create_rom_file(args.diff_file) + if "server" in meta: + args.connect = meta["server"] + logging.info(f"Wrote rom file to {romfile}") + async_start(run_game(romfile)) + ctx = ZeldaContext(args.connect, args.password) + ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + ctx.nes_sync_task = asyncio.create_task(nes_sync_task(ctx), name="NES Sync") + + await ctx.exit_event.wait() + ctx.server_address = None + + await ctx.shutdown() + + if ctx.nes_sync_task: + await ctx.nes_sync_task + + + import colorama + + parser = get_base_parser() + parser.add_argument('diff_file', default="", type=str, nargs="?", + help='Path to a Archipelago Binary Patch file') + args = parser.parse_args() + colorama.init() + + asyncio.run(main(args)) + colorama.deinit() diff --git a/data/lua/TLoZ/TheLegendOfZeldaConnector.lua b/data/lua/TLoZ/TheLegendOfZeldaConnector.lua new file mode 100644 index 0000000000..aee4412bc0 --- /dev/null +++ b/data/lua/TLoZ/TheLegendOfZeldaConnector.lua @@ -0,0 +1,702 @@ +--Shamelessly based off the FF1 lua + +local socket = require("socket") +local json = require('json') +local math = require('math') + +local STATE_OK = "Ok" +local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected" +local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made" +local STATE_UNINITIALIZED = "Uninitialized" + +local itemMessages = {} +local consumableStacks = nil +local prevstate = "" +local curstate = STATE_UNINITIALIZED +local zeldaSocket = nil +local frame = 0 +local gameMode = 0 + +local cave_index +local triforce_byte +local game_state + +local u8 = nil +local wU8 = nil +local isNesHawk = false + +local shopsChecked = {} +local shopSlotLeft = 0x0628 +local shopSlotMiddle = 0x0629 +local shopSlotRight = 0x062A + +--N.B.: you won't find these in a RAM map. They're flag values that the base patch derives from the cave ID. +local blueRingShopBit = 0x40 +local potionShopBit = 0x02 +local arrowShopBit = 0x08 +local candleShopBit = 0x10 +local shieldShopBit = 0x20 +local takeAnyCaveBit = 0x01 + + +local sword = 0x0657 +local bombs = 0x0658 +local maxBombs = 0x067C +local keys = 0x066E +local arrow = 0x0659 +local bow = 0x065A +local candle = 0x065B +local recorder = 0x065C +local food = 0x065D +local waterOfLife = 0x065E +local magicalRod = 0x065F +local raft = 0x0660 +local bookOfMagic = 0x0661 +local ring = 0x0662 +local stepladder = 0x0663 +local magicalKey = 0x0664 +local powerBracelet = 0x0665 +local letter = 0x0666 +local clockItem = 0x066C +local heartContainers = 0x066F +local partialHearts = 0x0670 +local triforceFragments = 0x0671 +local boomerang = 0x0674 +local magicalBoomerang = 0x0675 +local magicalShield = 0x0676 +local rupeesToAdd = 0x067D +local rupeesToSubtract = 0x067E +local itemsObtained = 0x0677 +local takeAnyCavesChecked = 0x0678 +local localTriforce = 0x0679 +local bonusItemsObtained = 0x067A + +itemAPids = { + ["Boomerang"] = 7100, + ["Bow"] = 7101, + ["Magical Boomerang"] = 7102, + ["Raft"] = 7103, + ["Stepladder"] = 7104, + ["Recorder"] = 7105, + ["Magical Rod"] = 7106, + ["Red Candle"] = 7107, + ["Book of Magic"] = 7108, + ["Magical Key"] = 7109, + ["Red Ring"] = 7110, + ["Silver Arrow"] = 7111, + ["Sword"] = 7112, + ["White Sword"] = 7113, + ["Magical Sword"] = 7114, + ["Heart Container"] = 7115, + ["Letter"] = 7116, + ["Magical Shield"] = 7117, + ["Candle"] = 7118, + ["Arrow"] = 7119, + ["Food"] = 7120, + ["Water of Life (Blue)"] = 7121, + ["Water of Life (Red)"] = 7122, + ["Blue Ring"] = 7123, + ["Triforce Fragment"] = 7124, + ["Power Bracelet"] = 7125, + ["Small Key"] = 7126, + ["Bomb"] = 7127, + ["Recovery Heart"] = 7128, + ["Five Rupees"] = 7129, + ["Rupee"] = 7130, + ["Clock"] = 7131, + ["Fairy"] = 7132 +} + +itemCodes = { + ["Boomerang"] = 0x1D, + ["Bow"] = 0x0A, + ["Magical Boomerang"] = 0x1E, + ["Raft"] = 0x0C, + ["Stepladder"] = 0x0D, + ["Recorder"] = 0x05, + ["Magical Rod"] = 0x10, + ["Red Candle"] = 0x07, + ["Book of Magic"] = 0x11, + ["Magical Key"] = 0x0B, + ["Red Ring"] = 0x13, + ["Silver Arrow"] = 0x09, + ["Sword"] = 0x01, + ["White Sword"] = 0x02, + ["Magical Sword"] = 0x03, + ["Heart Container"] = 0x1A, + ["Letter"] = 0x15, + ["Magical Shield"] = 0x1C, + ["Candle"] = 0x06, + ["Arrow"] = 0x08, + ["Food"] = 0x04, + ["Water of Life (Blue)"] = 0x1F, + ["Water of Life (Red)"] = 0x20, + ["Blue Ring"] = 0x12, + ["Triforce Fragment"] = 0x1B, + ["Power Bracelet"] = 0x14, + ["Small Key"] = 0x19, + ["Bomb"] = 0x00, + ["Recovery Heart"] = 0x22, + ["Five Rupees"] = 0x0F, + ["Rupee"] = 0x18, + ["Clock"] = 0x21, + ["Fairy"] = 0x23 +} + + +--Sets correct memory access functions based on whether NesHawk or QuickNES is loaded +local function defineMemoryFunctions() + local memDomain = {} + local domains = memory.getmemorydomainlist() + if domains[1] == "System Bus" then + --NesHawk + isNesHawk = true + memDomain["systembus"] = function() memory.usememorydomain("System Bus") end + memDomain["ram"] = function() memory.usememorydomain("RAM") end + memDomain["saveram"] = function() memory.usememorydomain("Battery RAM") end + memDomain["rom"] = function() memory.usememorydomain("PRG ROM") end + elseif domains[1] == "WRAM" then + --QuickNES + memDomain["systembus"] = function() memory.usememorydomain("System Bus") end + memDomain["ram"] = function() memory.usememorydomain("RAM") end + memDomain["saveram"] = function() memory.usememorydomain("WRAM") end + memDomain["rom"] = function() memory.usememorydomain("PRG ROM") end + end + return memDomain +end + +local memDomain = defineMemoryFunctions() +u8 = memory.read_u8 +wU8 = memory.write_u8 +uRange = memory.readbyterange + +itemIDNames = {} + +for key, value in pairs(itemAPids) do + itemIDNames[value] = key +end + + + +local function determineItem(array) + memdomain.ram() + currentItemsObtained = u8(itemsObtained) + +end + +local function gotSword() + local currentSword = u8(sword) + wU8(sword, math.max(currentSword, 1)) +end + +local function gotWhiteSword() + local currentSword = u8(sword) + wU8(sword, math.max(currentSword, 2)) +end + +local function gotMagicalSword() + wU8(sword, 3) +end + +local function gotBomb() + local currentBombs = u8(bombs) + local currentMaxBombs = u8(maxBombs) + wU8(bombs, math.min(currentBombs + 4, currentMaxBombs)) + wU8(0x505, 0x29) -- Fake bomb to show item get. +end + +local function gotArrow() + local currentArrow = u8(arrow) + wU8(arrow, math.max(currentArrow, 1)) +end + +local function gotSilverArrow() + wU8(arrow, 2) +end + +local function gotBow() + wU8(bow, 1) +end + +local function gotCandle() + local currentCandle = u8(candle) + wU8(candle, math.max(currentCandle, 1)) +end + +local function gotRedCandle() + wU8(candle, 2) +end + +local function gotRecorder() + wU8(recorder, 1) +end + +local function gotFood() + wU8(food, 1) +end + +local function gotWaterOfLifeBlue() + local currentWaterOfLife = u8(waterOfLife) + wU8(waterOfLife, math.max(currentWaterOfLife, 1)) +end + +local function gotWaterOfLifeRed() + wU8(waterOfLife, 2) +end + +local function gotMagicalRod() + wU8(magicalRod, 1) +end + +local function gotBookOfMagic() + wU8(bookOfMagic, 1) +end + +local function gotRaft() + wU8(raft, 1) +end + +local function gotBlueRing() + local currentRing = u8(ring) + wU8(ring, math.max(currentRing, 1)) + memDomain.saveram() + local currentTunicColor = u8(0x0B92) + if currentTunicColor == 0x29 then + wU8(0x0B92, 0x32) + wU8(0x0804, 0x32) + end +end + +local function gotRedRing() + wU8(ring, 2) + memDomain.saveram() + wU8(0x0B92, 0x16) + wU8(0x0804, 0x16) +end + +local function gotStepladder() + wU8(stepladder, 1) +end + +local function gotMagicalKey() + wU8(magicalKey, 1) +end + +local function gotPowerBracelet() + wU8(powerBracelet, 1) +end + +local function gotLetter() + wU8(letter, 1) +end + +local function gotHeartContainer() + local currentHeartContainers = bit.rshift(bit.band(u8(heartContainers), 0xF0), 4) + if currentHeartContainers < 16 then + currentHeartContainers = math.min(currentHeartContainers + 1, 16) + local currentHearts = bit.band(u8(heartContainers), 0x0F) + 1 + wU8(heartContainers, bit.lshift(currentHeartContainers, 4) + currentHearts) + end +end + +local function gotTriforceFragment() + local triforceByte = 0xFF + local newTriforceCount = u8(localTriforce) + 1 + wU8(localTriforce, newTriforceCount) +end + +local function gotBoomerang() + wU8(boomerang, 1) +end + +local function gotMagicalBoomerang() + wU8(magicalBoomerang, 1) +end + +local function gotMagicalShield() + wU8(magicalShield, 1) +end + +local function gotRecoveryHeart() + local currentHearts = bit.band(u8(heartContainers), 0x0F) + local currentHeartContainers = bit.rshift(bit.band(u8(heartContainers), 0xF0), 4) + if currentHearts < currentHeartContainers then + currentHearts = currentHearts + 1 + else + wU8(partialHearts, 0xFF) + end + currentHearts = bit.bor(bit.band(u8(heartContainers), 0xF0), currentHearts) + wU8(heartContainers, currentHearts) +end + +local function gotFairy() + local currentHearts = bit.band(u8(heartContainers), 0x0F) + local currentHeartContainers = bit.rshift(bit.band(u8(heartContainers), 0xF0), 4) + if currentHearts < currentHeartContainers then + currentHearts = currentHearts + 3 + if currentHearts > currentHeartContainers then + currentHearts = currentHeartContainers + wU8(partialHearts, 0xFF) + end + else + wU8(partialHearts, 0xFF) + end + currentHearts = bit.bor(bit.band(u8(heartContainers), 0xF0), currentHearts) + wU8(heartContainers, currentHearts) +end + +local function gotClock() + wU8(clockItem, 1) +end + +local function gotFiveRupees() + local currentRupeesToAdd = u8(rupeesToAdd) + wU8(rupeesToAdd, math.min(currentRupeesToAdd + 5, 255)) +end + +local function gotSmallKey() + wU8(keys, math.min(u8(keys) + 1, 9)) +end + +local function gotItem(item) + --Write itemCode to itemToLift + --Write 128 to itemLiftTimer + --Write 4 to sound effect queue + itemName = itemIDNames[item] + itemCode = itemCodes[itemName] + wU8(0x505, itemCode) + wU8(0x506, 128) + wU8(0x602, 4) + numberObtained = u8(itemsObtained) + 1 + wU8(itemsObtained, numberObtained) + if itemName == "Boomerang" then gotBoomerang() end + if itemName == "Bow" then gotBow() end + if itemName == "Magical Boomerang" then gotMagicalBoomerang() end + if itemName == "Raft" then gotRaft() end + if itemName == "Stepladder" then gotStepladder() end + if itemName == "Recorder" then gotRecorder() end + if itemName == "Magical Rod" then gotMagicalRod() end + if itemName == "Red Candle" then gotRedCandle() end + if itemName == "Book of Magic" then gotBookOfMagic() end + if itemName == "Magical Key" then gotMagicalKey() end + if itemName == "Red Ring" then gotRedRing() end + if itemName == "Silver Arrow" then gotSilverArrow() end + if itemName == "Sword" then gotSword() end + if itemName == "White Sword" then gotWhiteSword() end + if itemName == "Magical Sword" then gotMagicalSword() end + if itemName == "Heart Container" then gotHeartContainer() end + if itemName == "Letter" then gotLetter() end + if itemName == "Magical Shield" then gotMagicalShield() end + if itemName == "Candle" then gotCandle() end + if itemName == "Arrow" then gotArrow() end + if itemName == "Food" then gotFood() end + if itemName == "Water of Life (Blue)" then gotWaterOfLifeBlue() end + if itemName == "Water of Life (Red)" then gotWaterOfLifeRed() end + if itemName == "Blue Ring" then gotBlueRing() end + if itemName == "Triforce Fragment" then gotTriforceFragment() end + if itemName == "Power Bracelet" then gotPowerBracelet() end + if itemName == "Small Key" then gotSmallKey() end + if itemName == "Bomb" then gotBomb() end + if itemName == "Recovery Heart" then gotRecoveryHeart() end + if itemName == "Five Rupees" then gotFiveRupees() end + if itemName == "Fairy" then gotFairy() end + if itemName == "Clock" then gotClock() end +end + + +local function StateOKForMainLoop() + memDomain.ram() + local gameMode = u8(0x12) + return gameMode == 5 +end + +local function checkCaveItemObtained() + memDomain.ram() + local returnTable = {} + returnTable["slot1"] = u8(shopSlotLeft) + returnTable["slot2"] = u8(shopSlotMiddle) + returnTable["slot3"] = u8(shopSlotRight) + returnTable["takeAnys"] = u8(takeAnyCavesChecked) + return returnTable +end + +function table.empty (self) + for _, _ in pairs(self) do + return false + end + return true +end + +function slice (tbl, s, e) + local pos, new = 1, {} + for i = s + 1, e do + new[pos] = tbl[i] + pos = pos + 1 + end + return new +end + +local bizhawk_version = client.getversion() +local is23Or24Or25 = (bizhawk_version=="2.3.1") or (bizhawk_version:sub(1,3)=="2.4") or (bizhawk_version:sub(1,3)=="2.5") +local is26To28 = (bizhawk_version:sub(1,3)=="2.6") or (bizhawk_version:sub(1,3)=="2.7") or (bizhawk_version:sub(1,3)=="2.8") + +local function getMaxMessageLength() + if is23Or24Or25 then + return client.screenwidth()/11 + elseif is26To28 then + return client.screenwidth()/12 + end +end + +local function drawText(x, y, message, color) + if is23Or24Or25 then + gui.addmessage(message) + elseif is26To28 then + gui.drawText(x, y, message, color, 0xB0000000, 18, "Courier New", "middle", "bottom", nil, "client") + end +end + +local function clearScreen() + if is23Or24Or25 then + return + elseif is26To28 then + drawText(0, 0, "", "black") + end +end + +local function drawMessages() + if table.empty(itemMessages) then + clearScreen() + return + end + local y = 10 + found = false + maxMessageLength = getMaxMessageLength() + for k, v in pairs(itemMessages) do + if v["TTL"] > 0 then + message = v["message"] + while true do + drawText(5, y, message:sub(1, maxMessageLength), v["color"]) + y = y + 16 + + message = message:sub(maxMessageLength + 1, message:len()) + if message:len() == 0 then + break + end + end + newTTL = 0 + if is26To28 then + newTTL = itemMessages[k]["TTL"] - 1 + end + itemMessages[k]["TTL"] = newTTL + found = true + end + end + if found == false then + clearScreen() + end +end + +function generateOverworldLocationChecked() + memDomain.ram() + data = uRange(0x067E, 0x81) + data[0] = nil + return data +end + +function getHCLocation() + memDomain.rom() + data = u8(0x1789A) + return data +end + +function getPBLocation() + memDomain.rom() + data = u8(0x10CB2) + return data +end + +function generateUnderworld16LocationChecked() + memDomain.ram() + data = uRange(0x06FE, 0x81) + data[0] = nil + return data +end + +function generateUnderworld79LocationChecked() + memDomain.ram() + data = uRange(0x077E, 0x81) + data[0] = nil + return data +end + +function updateTriforceFragments() + memDomain.ram() + local triforceByte = 0xFF + totalTriforceCount = u8(localTriforce) + local currentPieces = bit.rshift(triforceByte, 8 - math.min(8, totalTriforceCount)) + wU8(triforceFragments, currentPieces) +end + +function processBlock(block) + if block ~= nil then + local msgBlock = block['messages'] + if msgBlock ~= nil then + for i, v in pairs(msgBlock) do + if itemMessages[i] == nil then + local msg = {TTL=450, message=v, color=0xFFFF0000} + itemMessages[i] = msg + end + end + end + local bonusItems = block["bonusItems"] + if bonusItems ~= nil and isInGame then + for i, item in ipairs(bonusItems) do + memDomain.ram() + if i > u8(bonusItemsObtained) then + if u8(0x505) == 0 then + gotItem(item) + wU8(itemsObtained, u8(itemsObtained) - 1) + wU8(bonusItemsObtained, u8(bonusItemsObtained) + 1) + end + end + end + end + local itemsBlock = block["items"] + memDomain.saveram() + isInGame = StateOKForMainLoop() + updateTriforceFragments() + if itemsBlock ~= nil and isInGame then + memDomain.ram() + --get item from item code + --get function from item + --do function + for i, item in ipairs(itemsBlock) do + memDomain.ram() + if u8(0x505) == 0 then + if i > u8(itemsObtained) then + gotItem(item) + end + end + end + end + local shopsBlock = block["shops"] + if shopsBlock ~= nil then + wU8(shopSlotLeft, bit.bor(u8(shopSlotLeft), shopsBlock["left"])) + wU8(shopSlotMiddle, bit.bor(u8(shopSlotMiddle), shopsBlock["middle"])) + wU8(shopSlotRight, bit.bor(u8(shopSlotRight), shopsBlock["right"])) + end + end +end + +function difference(a, b) + local aa = {} + for k,v in pairs(a) do aa[v]=true end + for k,v in pairs(b) do aa[v]=nil end + local ret = {} + local n = 0 + for k,v in pairs(a) do + if aa[v] then n=n+1 ret[n]=v end + end + return ret +end + +function receive() + l, e = zeldaSocket:receive() + if e == 'closed' then + if curstate == STATE_OK then + print("Connection closed") + end + curstate = STATE_UNINITIALIZED + return + elseif e == 'timeout' then + print("timeout") + return + elseif e ~= nil then + print(e) + curstate = STATE_UNINITIALIZED + return + end + processBlock(json.decode(l)) + + -- Determine Message to send back + memDomain.rom() + local playerName = uRange(0x1F, 0x10) + playerName[0] = nil + local retTable = {} + retTable["playerName"] = playerName + if StateOKForMainLoop() then + retTable["overworld"] = generateOverworldLocationChecked() + retTable["underworld1"] = generateUnderworld16LocationChecked() + retTable["underworld2"] = generateUnderworld79LocationChecked() + end + retTable["caves"] = checkCaveItemObtained() + memDomain.ram() + if gameMode ~= 19 then + gameMode = u8(0x12) + end + retTable["gameMode"] = gameMode + retTable["overworldHC"] = getHCLocation() + retTable["overworldPB"] = getPBLocation() + retTable["itemsObtained"] = u8(itemsObtained) + msg = json.encode(retTable).."\n" + local ret, error = zeldaSocket:send(msg) + if ret == nil then + print(error) + elseif curstate == STATE_INITIAL_CONNECTION_MADE then + curstate = STATE_TENTATIVELY_CONNECTED + elseif curstate == STATE_TENTATIVELY_CONNECTED then + print("Connected!") + itemMessages["(0,0)"] = {TTL=240, message="Connected", color="green"} + curstate = STATE_OK + end +end + +function main() + if (is23Or24Or25 or is26To28) == false then + print("Must use a version of bizhawk 2.3.1 or higher") + return + end + server, error = socket.bind('localhost', 52980) + + while true do + gui.drawEllipse(248, 9, 6, 6, "Black", "Yellow") + frame = frame + 1 + drawMessages() + if not (curstate == prevstate) then + -- console.log("Current state: "..curstate) + prevstate = curstate + end + if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then + if (frame % 60 == 0) then + gui.drawEllipse(248, 9, 6, 6, "Black", "Blue") + receive() + else + gui.drawEllipse(248, 9, 6, 6, "Black", "Green") + end + elseif (curstate == STATE_UNINITIALIZED) then + gui.drawEllipse(248, 9, 6, 6, "Black", "White") + if (frame % 60 == 0) then + gui.drawEllipse(248, 9, 6, 6, "Black", "Yellow") + + drawText(5, 8, "Waiting for client", 0xFFFF0000) + drawText(5, 32, "Please start Zelda1Client.exe", 0xFFFF0000) + + -- Advance so the messages are drawn + emu.frameadvance() + server:settimeout(2) + print("Attempting to connect") + local client, timeout = server:accept() + if timeout == nil then + -- print('Initial Connection Made') + curstate = STATE_INITIAL_CONNECTION_MADE + zeldaSocket = client + zeldaSocket:settimeout(0) + end + end + end + emu.frameadvance() + end +end + +main() \ No newline at end of file diff --git a/data/lua/TLoZ/core.dll b/data/lua/TLoZ/core.dll new file mode 100644 index 0000000000000000000000000000000000000000..3e9569571ab0947dcb7bcd789dc9c06c009d072d GIT binary patch literal 29184 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 B{xARl literal 0 HcmV?d00001 diff --git a/data/lua/TLoZ/json.lua b/data/lua/TLoZ/json.lua new file mode 100644 index 0000000000..0833bf6fb4 --- /dev/null +++ b/data/lua/TLoZ/json.lua @@ -0,0 +1,380 @@ +-- +-- json.lua +-- +-- Copyright (c) 2015 rxi +-- +-- This library is free software; you can redistribute it and/or modify it +-- under the terms of the MIT license. See LICENSE for details. +-- + +local json = { _version = "0.1.0" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\\\", + [ "\"" ] = "\\\"", + [ "\b" ] = "\\b", + [ "\f" ] = "\\f", + [ "\n" ] = "\\n", + [ "\r" ] = "\\r", + [ "\t" ] = "\\t", +} + +local escape_char_map_inv = { [ "\\/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return escape_char_map[c] or string.format("\\u%04x", c:byte()) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if val[1] ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + --local line_count = 1 + --local col_count = 1 + --for i = 1, idx - 1 do + -- col_count = col_count + 1 + -- if str:sub(i, i) == "\n" then + -- line_count = line_count + 1 + -- col_count = 1 + -- end + -- end + -- emu.message( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(3, 6), 16 ) + local n2 = tonumber( s:sub(9, 12), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local has_unicode_escape = false + local has_surrogate_escape = false + local has_escape = false + local last + for j = i + 1, #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + end + + if last == 92 then -- "\\" (escape char) + if x == 117 then -- "u" (unicode escape sequence) + local hex = str:sub(j + 1, j + 5) + if not hex:find("%x%x%x%x") then + decode_error(str, j, "invalid unicode escape in string") + end + if hex:find("^[dD][89aAbB]") then + has_surrogate_escape = true + else + has_unicode_escape = true + end + else + local c = string.char(x) + if not escape_chars[c] then + decode_error(str, j, "invalid escape char '" .. c .. "' in string") + end + has_escape = true + end + last = nil + + elseif x == 34 then -- '"' (end of string) + local s = str:sub(i + 1, j - 1) + if has_surrogate_escape then + s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape) + end + if has_unicode_escape then + s = s:gsub("\\u....", parse_unicode_escape) + end + if has_escape then + s = s:gsub("\\.", escape_char_map_inv) + end + return s, j + 1 + + else + last = x + end + end + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + return ( parse(str, next_char(str, 1, space_chars, true)) ) +end + + +return json \ No newline at end of file diff --git a/data/lua/TLoZ/socket.lua b/data/lua/TLoZ/socket.lua new file mode 100644 index 0000000000..a98e952115 --- /dev/null +++ b/data/lua/TLoZ/socket.lua @@ -0,0 +1,132 @@ +----------------------------------------------------------------------------- +-- LuaSocket helper module +-- Author: Diego Nehab +-- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $ +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module and import dependencies +----------------------------------------------------------------------------- +local base = _G +local string = require("string") +local math = require("math") +local socket = require("socket.core") +module("socket") + +----------------------------------------------------------------------------- +-- Exported auxiliar functions +----------------------------------------------------------------------------- +function connect(address, port, laddress, lport) + local sock, err = socket.tcp() + if not sock then return nil, err end + if laddress then + local res, err = sock:bind(laddress, lport, -1) + if not res then return nil, err end + end + local res, err = sock:connect(address, port) + if not res then return nil, err end + return sock +end + +function bind(host, port, backlog) + local sock, err = socket.tcp() + if not sock then return nil, err end + sock:setoption("reuseaddr", true) + local res, err = sock:bind(host, port) + if not res then return nil, err end + res, err = sock:listen(backlog) + if not res then return nil, err end + return sock +end + +try = newtry() + +function choose(table) + return function(name, opt1, opt2) + if base.type(name) ~= "string" then + name, opt1, opt2 = "default", name, opt1 + end + local f = table[name or "nil"] + if not f then base.error("unknown key (".. base.tostring(name) ..")", 3) + else return f(opt1, opt2) end + end +end + +----------------------------------------------------------------------------- +-- Socket sources and sinks, conforming to LTN12 +----------------------------------------------------------------------------- +-- create namespaces inside LuaSocket namespace +sourcet = {} +sinkt = {} + +BLOCKSIZE = 2048 + +sinkt["close-when-done"] = function(sock) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function(self, chunk, err) + if not chunk then + sock:close() + return 1 + else return sock:send(chunk) end + end + }) +end + +sinkt["keep-open"] = function(sock) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function(self, chunk, err) + if chunk then return sock:send(chunk) + else return 1 end + end + }) +end + +sinkt["default"] = sinkt["keep-open"] + +sink = choose(sinkt) + +sourcet["by-length"] = function(sock, length) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function() + if length <= 0 then return nil end + local size = math.min(socket.BLOCKSIZE, length) + local chunk, err = sock:receive(size) + if err then return nil, err end + length = length - string.len(chunk) + return chunk + end + }) +end + +sourcet["until-closed"] = function(sock) + local done + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function() + if done then return nil end + local chunk, err, partial = sock:receive(socket.BLOCKSIZE) + if not err then return chunk + elseif err == "closed" then + sock:close() + done = 1 + return partial + else return nil, err end + end + }) +end + + +sourcet["default"] = sourcet["until-closed"] + +source = choose(sourcet) diff --git a/host.yaml b/host.yaml index 78fff669e1..5d9ec56ee9 100644 --- a/host.yaml +++ b/host.yaml @@ -107,7 +107,7 @@ factorio_options: filter_item_sends: false # Whether to send chat messages from players on the Factorio server to Archipelago. bridge_chat_out: true -minecraft_options: +minecraft_options: forge_directory: "Minecraft Forge server" max_heap_size: "2G" # release channel, currently "release", or "beta" @@ -125,6 +125,15 @@ soe_options: rom_file: "Secret of Evermore (USA).sfc" ffr_options: display_msgs: true +tloz_options: + # File name of the Zelda 1 + rom_file: "Legend of Zelda, The (U) (PRG0) [!].nes" + # Set this to false to never autostart a rom (such as after patching) + # true for operating system default program + # Alternatively, a path to a program to open the .nes file with + rom_start: true + # Display message inside of Bizhawk + display_msgs: true dkc3_options: # File name of the DKC3 US rom rom_file: "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc" diff --git a/worlds/tloz/ItemPool.py b/worlds/tloz/ItemPool.py new file mode 100644 index 0000000000..21ceafdc42 --- /dev/null +++ b/worlds/tloz/ItemPool.py @@ -0,0 +1,145 @@ +from BaseClasses import ItemClassification +from .Locations import level_locations, all_level_locations, standard_level_locations, shop_locations + +# Swords are in starting_weapons +overworld_items = { + "Letter": 1, + "Power Bracelet": 1, + "Heart Container": 1, + "Sword": 1 +} + +# Bomb, Arrow, 1 Small Key and Red Water of Life are in guaranteed_shop_items +shop_items = { + "Magical Shield": 3, + "Food": 2, + "Small Key": 1, + "Candle": 1, + "Recovery Heart": 1, + "Blue Ring": 1, + "Water of Life (Blue)": 1 +} + +# Magical Rod and Red Candle are in starting_weapons, Triforce Fragments are added in its section of get_pool_core +major_dungeon_items = { + "Heart Container": 8, + "Bow": 1, + "Boomerang": 1, + "Magical Boomerang": 1, + "Raft": 1, + "Stepladder": 1, + "Recorder": 1, + "Magical Key": 1, + "Book of Magic": 1, + "Silver Arrow": 1, + "Red Ring": 1 +} + +minor_dungeon_items = { + "Bomb": 23, + "Small Key": 45, + "Five Rupees": 17 +} + +take_any_items = { + "Heart Container": 4 +} + +# Map/Compasses: 18 +# Reasoning: Adding some variety to the vanilla game. + +map_compass_replacements = { + "Fairy": 6, + "Clock": 3, + "Water of Life (Red)": 1, + "Water of Life (Blue)": 2, + "Bomb": 2, + "Small Key": 2, + "Five Rupees": 2 +} +basic_pool = { + item: overworld_items.get(item, 0) + shop_items.get(item, 0) + + major_dungeon_items.get(item, 0) + map_compass_replacements.get(item, 0) + for item in set(overworld_items) | set(shop_items) | set(major_dungeon_items) | set(map_compass_replacements) +} + +starting_weapons = ["Sword", "White Sword", "Magical Sword", "Magical Rod", "Red Candle"] +guaranteed_shop_items = ["Small Key", "Bomb", "Water of Life (Red)", "Arrow"] +starting_weapon_locations = ["Starting Sword Cave", "Letter Cave", "Armos Knights"] +dangerous_weapon_locations = [ + "Level 1 Compass", "Level 2 Bomb Drop (Keese)", "Level 3 Key Drop (Zols Entrance)", "Level 3 Compass"] + +def generate_itempool(tlozworld): + (pool, placed_items) = get_pool_core(tlozworld) + tlozworld.multiworld.itempool.extend([tlozworld.multiworld.create_item(item, tlozworld.player) for item in pool]) + for (location_name, item) in placed_items.items(): + location = tlozworld.multiworld.get_location(location_name, tlozworld.player) + location.place_locked_item(tlozworld.multiworld.create_item(item, tlozworld.player)) + if item == "Bomb": + location.item.classification = ItemClassification.progression + +def get_pool_core(world): + random = world.multiworld.random + + pool = [] + placed_items = {} + minor_items = dict(minor_dungeon_items) + + # Guaranteed Shop Items + reserved_store_slots = random.sample(shop_locations[0:9], 4) + for location, item in zip(reserved_store_slots, guaranteed_shop_items): + placed_items[location] = item + + # Starting Weapon + starting_weapon = random.choice(starting_weapons) + if world.multiworld.StartingPosition[world.player] == 0: + placed_items[starting_weapon_locations[0]] = starting_weapon + elif world.multiworld.StartingPosition[world.player] in [1, 2]: + if world.multiworld.StartingPosition[world.player] == 2: + for location in dangerous_weapon_locations: + if world.multiworld.ExpandedPool[world.player] or "Drop" not in location: + starting_weapon_locations.append(location) + placed_items[random.choice(starting_weapon_locations)] = starting_weapon + else: + pool.append(starting_weapon) + for other_weapons in starting_weapons: + if other_weapons != starting_weapon: + pool.append(other_weapons) + + # Triforce Fragments + fragment = "Triforce Fragment" + if world.multiworld.ExpandedPool[world.player]: + possible_level_locations = [location for location in all_level_locations + if location not in level_locations[8]] + else: + possible_level_locations = [location for location in standard_level_locations + if location not in level_locations[8]] + for level in range(1, 9): + if world.multiworld.TriforceLocations[world.player] == 0: + placed_items[f"Level {level} Triforce"] = fragment + elif world.multiworld.TriforceLocations[world.player] == 1: + placed_items[possible_level_locations.pop(random.randint(0, len(possible_level_locations) - 1))] = fragment + else: + pool.append(fragment) + + # Level 9 junk fill + if world.multiworld.ExpandedPool[world.player] > 0: + spots = random.sample(level_locations[8], len(level_locations[8]) // 2) + for spot in spots: + junk = random.choice(list(minor_items.keys())) + placed_items[spot] = junk + minor_items[junk] -= 1 + + # Finish Pool + final_pool = basic_pool + if world.multiworld.ExpandedPool[world.player]: + final_pool = { + item: basic_pool.get(item, 0) + minor_items.get(item, 0) + take_any_items.get(item, 0) + for item in set(basic_pool) | set(minor_items) | set(take_any_items) + } + final_pool["Five Rupees"] -= 1 + for item in final_pool.keys(): + for i in range(0, final_pool[item]): + pool.append(item) + + return pool, placed_items diff --git a/worlds/tloz/Items.py b/worlds/tloz/Items.py new file mode 100644 index 0000000000..d896d11d77 --- /dev/null +++ b/worlds/tloz/Items.py @@ -0,0 +1,147 @@ +from BaseClasses import ItemClassification +import typing +from typing import Dict + +progression = ItemClassification.progression +filler = ItemClassification.filler +useful = ItemClassification.useful +trap = ItemClassification.trap + + +class ItemData(typing.NamedTuple): + code: typing.Optional[int] + classification: ItemClassification + + +item_table: Dict[str, ItemData] = { + "Boomerang": ItemData(100, useful), + "Bow": ItemData(101, progression), + "Magical Boomerang": ItemData(102, useful), + "Raft": ItemData(103, progression), + "Stepladder": ItemData(104, progression), + "Recorder": ItemData(105, progression), + "Magical Rod": ItemData(106, progression), + "Red Candle": ItemData(107, progression), + "Book of Magic": ItemData(108, progression), + "Magical Key": ItemData(109, useful), + "Red Ring": ItemData(110, useful), + "Silver Arrow": ItemData(111, progression), + "Sword": ItemData(112, progression), + "White Sword": ItemData(113, progression), + "Magical Sword": ItemData(114, progression), + "Heart Container": ItemData(115, progression), + "Letter": ItemData(116, progression), + "Magical Shield": ItemData(117, useful), + "Candle": ItemData(118, progression), + "Arrow": ItemData(119, progression), + "Food": ItemData(120, progression), + "Water of Life (Blue)": ItemData(121, useful), + "Water of Life (Red)": ItemData(122, useful), + "Blue Ring": ItemData(123, useful), + "Triforce Fragment": ItemData(124, progression), + "Power Bracelet": ItemData(125, useful), + "Small Key": ItemData(126, filler), + "Bomb": ItemData(127, filler), + "Recovery Heart": ItemData(128, filler), + "Five Rupees": ItemData(129, filler), + "Rupee": ItemData(130, filler), + "Clock": ItemData(131, filler), + "Fairy": ItemData(132, filler) + +} + +item_game_ids = { + "Bomb": 0x00, + "Sword": 0x01, + "White Sword": 0x02, + "Magical Sword": 0x03, + "Food": 0x04, + "Recorder": 0x05, + "Candle": 0x06, + "Red Candle": 0x07, + "Arrow": 0x08, + "Silver Arrow": 0x09, + "Bow": 0x0A, + "Magical Key": 0x0B, + "Raft": 0x0C, + "Stepladder": 0x0D, + "Five Rupees": 0x0F, + "Magical Rod": 0x10, + "Book of Magic": 0x11, + "Blue Ring": 0x12, + "Red Ring": 0x13, + "Power Bracelet": 0x14, + "Letter": 0x15, + "Small Key": 0x19, + "Heart Container": 0x1A, + "Triforce Fragment": 0x1B, + "Magical Shield": 0x1C, + "Boomerang": 0x1D, + "Magical Boomerang": 0x1E, + "Water of Life (Blue)": 0x1F, + "Water of Life (Red)": 0x20, + "Recovery Heart": 0x22, + "Rupee": 0x18, + "Clock": 0x21, + "Fairy": 0x23 +} + +# Item prices are going to get a bit of a writeup here, because these are some seemingly arbitrary +# design decisions and future contributors may want to know how these were arrived at. + +# First, I based everything off of the Blue Ring. Since the Red Ring is twice as good as the Blue Ring, +# logic dictates it should cost twice as much. Since you can't make something cost 500 rupees, the only +# solution was to halve the price of the Blue Ring. Correspondingly, everything else sold in shops was +# also cut in half. + +# Then, I decided on a factor for swords. Since each sword does double the damage of its predecessor, each +# one should be at least double. Since the sword saves so much time when upgraded (as, unlike other items, +# you don't need to switch to it), I wanted a bit of a premium on upgrades. Thus, a 4x multiplier was chosen, +# allowing the basic Sword to stay cheap while making the Magical Sword be a hefty upgrade you'll +# feel the price of. + +# Since arrows do the same amount of damage as the White Sword and silver arrows are the same with the Magical Sword. +# they were given corresponding costs. + +# Utility items were based on the prices of the shield, keys, and food. Broadly useful utility items should cost more, +# while limited use utility items should cost less. After eyeballing those, a few editorial decisions were made as +# deliberate thumbs on the scale of game balance. Those exceptions will be noted below. In general, prices were chosen +# based on how a player would feel spending that amount of money as opposed to how useful an item actually is. + +item_prices = { + "Bomb": 10, + "Sword": 10, + "White Sword": 40, + "Magical Sword": 160, + "Food": 30, + "Recorder": 45, + "Candle": 30, + "Red Candle": 60, + "Arrow": 40, + "Silver Arrow": 160, + "Bow": 40, + "Magical Key": 250, # Replacing all small keys commands a high premium + "Raft": 80, + "Stepladder": 80, + "Five Rupees": 255, # This could cost anything above 5 Rupees and be fine, but 255 is the funniest + "Magical Rod": 100, # White Sword with forever beams should cost at least more than the White Sword itself + "Book of Magic": 60, + "Blue Ring": 125, + "Red Ring": 250, + "Power Bracelet": 25, + "Letter": 20, + "Small Key": 40, + "Heart Container": 80, + "Triforce Fragment": 200, # Since I couldn't make Zelda 1 track shop purchases, this is how to discourage repeat + # Triforce purchases. The punishment for endless Rupee grinding to avoid searching out + # Triforce pieces is that you're doing endless Rupee grinding to avoid playing the game + "Magical Shield": 45, + "Boomerang": 5, + "Magical Boomerang": 20, + "Water of Life (Blue)": 20, + "Water of Life (Red)": 34, + "Recovery Heart": 5, + "Rupee": 50, + "Clock": 0, + "Fairy": 10 +} diff --git a/worlds/tloz/Locations.py b/worlds/tloz/Locations.py new file mode 100644 index 0000000000..bbd22abad4 --- /dev/null +++ b/worlds/tloz/Locations.py @@ -0,0 +1,350 @@ +from . import Rom + +major_locations = [ + "Starting Sword Cave", + "White Sword Pond", + "Magical Sword Grave", + "Take Any Item Left", + "Take Any Item Middle", + "Take Any Item Right", + "Armos Knights", + "Ocean Heart Container", + "Letter Cave", +] + +level_locations = [ + [ + "Level 1 Item (Bow)", "Level 1 Item (Boomerang)", "Level 1 Map", "Level 1 Compass", "Level 1 Boss", + "Level 1 Triforce", "Level 1 Key Drop (Keese Entrance)", "Level 1 Key Drop (Stalfos Middle)", + "Level 1 Key Drop (Moblins)", "Level 1 Key Drop (Stalfos Water)", + "Level 1 Key Drop (Stalfos Entrance)", "Level 1 Key Drop (Wallmasters)", + ], + [ + "Level 2 Item (Magical Boomerang)", "Level 2 Map", "Level 2 Compass", "Level 2 Boss", "Level 2 Triforce", + "Level 2 Key Drop (Ropes West)", "Level 2 Key Drop (Moldorms)", + "Level 2 Key Drop (Ropes Middle)", "Level 2 Key Drop (Ropes Entrance)", + "Level 2 Bomb Drop (Keese)", "Level 2 Bomb Drop (Moblins)", + "Level 2 Rupee Drop (Gels)", + ], + [ + "Level 3 Item (Raft)", "Level 3 Map", "Level 3 Compass", "Level 3 Boss", "Level 3 Triforce", + "Level 3 Key Drop (Zols and Keese West)", "Level 3 Key Drop (Keese North)", + "Level 3 Key Drop (Zols Central)", "Level 3 Key Drop (Zols South)", + "Level 3 Key Drop (Zols Entrance)", "Level 3 Bomb Drop (Darknuts West)", + "Level 3 Bomb Drop (Keese Corridor)", "Level 3 Bomb Drop (Darknuts Central)", + "Level 3 Rupee Drop (Zols and Keese East)" + ], + [ + "Level 4 Item (Stepladder)", "Level 4 Map", "Level 4 Compass", "Level 4 Boss", "Level 4 Triforce", + "Level 4 Key Drop (Keese Entrance)", "Level 4 Key Drop (Keese Central)", + "Level 4 Key Drop (Zols)", "Level 4 Key Drop (Keese North)", + ], + [ + "Level 5 Item (Recorder)", "Level 5 Map", "Level 5 Compass", "Level 5 Boss", "Level 5 Triforce", + "Level 5 Key Drop (Keese North)", "Level 5 Key Drop (Gibdos North)", + "Level 5 Key Drop (Gibdos Central)", "Level 5 Key Drop (Pols Voice Entrance)", + "Level 5 Key Drop (Gibdos Entrance)", "Level 5 Key Drop (Gibdos, Keese, and Pols Voice)", + "Level 5 Key Drop (Zols)", "Level 5 Bomb Drop (Gibdos)", + "Level 5 Bomb Drop (Dodongos)", "Level 5 Rupee Drop (Zols)", + ], + [ + "Level 6 Item (Magical Rod)", "Level 6 Map", "Level 6 Compass", "Level 6 Boss", "Level 6 Triforce", + "Level 6 Key Drop (Wizzrobes Entrance)", "Level 6 Key Drop (Keese)", + "Level 6 Key Drop (Wizzrobes North Island)", "Level 6 Key Drop (Wizzrobes North Stream)", + "Level 6 Key Drop (Vires)", "Level 6 Bomb Drop (Wizzrobes)", + "Level 6 Rupee Drop (Wizzrobes)" + ], + [ + "Level 7 Item (Red Candle)", "Level 7 Map", "Level 7 Compass", "Level 7 Boss", "Level 7 Triforce", + "Level 7 Key Drop (Ropes)", "Level 7 Key Drop (Goriyas)", "Level 7 Key Drop (Stalfos)", + "Level 7 Key Drop (Moldorms)", "Level 7 Bomb Drop (Goriyas South)", "Level 7 Bomb Drop (Keese and Spikes)", + "Level 7 Bomb Drop (Moldorms South)", "Level 7 Bomb Drop (Moldorms North)", + "Level 7 Bomb Drop (Goriyas North)", "Level 7 Bomb Drop (Dodongos)", + "Level 7 Bomb Drop (Digdogger)", "Level 7 Rupee Drop (Goriyas Central)", + "Level 7 Rupee Drop (Dodongos)", "Level 7 Rupee Drop (Goriyas North)", + ], + [ + "Level 8 Item (Magical Key)", "Level 8 Map", "Level 8 Compass", "Level 8 Item (Book of Magic)", "Level 8 Boss", + "Level 8 Triforce", "Level 8 Key Drop (Darknuts West)", + "Level 8 Key Drop (Darknuts Far West)", "Level 8 Key Drop (Pols Voice South)", + "Level 8 Key Drop (Pols Voice and Keese)", "Level 8 Key Drop (Darknuts Central)", + "Level 8 Key Drop (Keese and Zols Entrance)", "Level 8 Bomb Drop (Darknuts North)", + "Level 8 Bomb Drop (Darknuts East)", "Level 8 Bomb Drop (Pols Voice North)", + "Level 8 Rupee Drop (Manhandla Entrance West)", "Level 8 Rupee Drop (Manhandla Entrance North)", + "Level 8 Rupee Drop (Darknuts and Gibdos)", + ], + [ + "Level 9 Item (Silver Arrow)", "Level 9 Item (Red Ring)", + "Level 9 Map", "Level 9 Compass", + "Level 9 Key Drop (Patra Southwest)", "Level 9 Key Drop (Like Likes and Zols East)", + "Level 9 Key Drop (Wizzrobes and Bubbles East)", "Level 9 Key Drop (Wizzrobes East Island)", + "Level 9 Bomb Drop (Blue Lanmolas)", "Level 9 Bomb Drop (Gels Lake)", + "Level 9 Bomb Drop (Like Likes and Zols Corridor)", "Level 9 Bomb Drop (Patra Northeast)", + "Level 9 Bomb Drop (Vires)", "Level 9 Rupee Drop (Wizzrobes West Island)", + "Level 9 Rupee Drop (Red Lanmolas)", "Level 9 Rupee Drop (Keese Southwest)", + "Level 9 Rupee Drop (Keese Central Island)", "Level 9 Rupee Drop (Wizzrobes Central)", + "Level 9 Rupee Drop (Wizzrobes North Island)", "Level 9 Rupee Drop (Gels East)" + ] +] + +all_level_locations = [] +for level in level_locations: + for location in level: + all_level_locations.append(location) + +standard_level_locations = [] +for level in level_locations: + for location in level: + if "Drop" not in location: + standard_level_locations.append(location) + +shop_locations = [ + "Arrow Shop Item Left", "Arrow Shop Item Middle", "Arrow Shop Item Right", + "Candle Shop Item Left", "Candle Shop Item Middle", "Candle Shop Item Right", + "Blue Ring Shop Item Left", "Blue Ring Shop Item Middle", "Blue Ring Shop Item Right", + "Shield Shop Item Left", "Shield Shop Item Middle", "Shield Shop Item Right", + "Potion Shop Item Left", "Potion Shop Item Middle", "Potion Shop Item Right" +] + +food_locations = [ + "Level 7 Map", "Level 7 Boss", "Level 7 Triforce", "Level 7 Key Drop (Goriyas)", + "Level 7 Bomb Drop (Moldorms North)", "Level 7 Bomb Drop (Goriyas North)", + "Level 7 Bomb Drop (Dodongos)", "Level 7 Rupee Drop (Goriyas North)" +] + +floor_location_game_offsets_early = { + "Level 1 Item (Bow)": 0x7F, + "Level 1 Item (Boomerang)": 0x44, + "Level 1 Map": 0x43, + "Level 1 Compass": 0x54, + "Level 1 Boss": 0x35, + "Level 1 Triforce": 0x36, + "Level 1 Key Drop (Keese Entrance)": 0x72, + "Level 1 Key Drop (Moblins)": 0x23, + "Level 1 Key Drop (Stalfos Water)": 0x33, + "Level 1 Key Drop (Stalfos Entrance)": 0x74, + "Level 1 Key Drop (Stalfos Middle)": 0x53, + "Level 1 Key Drop (Wallmasters)": 0x45, + "Level 2 Item (Magical Boomerang)": 0x4F, + "Level 2 Map": 0x5F, + "Level 2 Compass": 0x6F, + "Level 2 Boss": 0x0E, + "Level 2 Triforce": 0x0D, + "Level 2 Key Drop (Ropes West)": 0x6C, + "Level 2 Key Drop (Moldorms)": 0x3E, + "Level 2 Key Drop (Ropes Middle)": 0x4E, + "Level 2 Key Drop (Ropes Entrance)": 0x7E, + "Level 2 Bomb Drop (Keese)": 0x3F, + "Level 2 Bomb Drop (Moblins)": 0x1E, + "Level 2 Rupee Drop (Gels)": 0x2F, + "Level 3 Item (Raft)": 0x0F, + "Level 3 Map": 0x4C, + "Level 3 Compass": 0x5A, + "Level 3 Boss": 0x4D, + "Level 3 Triforce": 0x3D, + "Level 3 Key Drop (Zols and Keese West)": 0x49, + "Level 3 Key Drop (Keese North)": 0x2A, + "Level 3 Key Drop (Zols Central)": 0x4B, + "Level 3 Key Drop (Zols South)": 0x6B, + "Level 3 Key Drop (Zols Entrance)": 0x7B, + "Level 3 Bomb Drop (Darknuts West)": 0x69, + "Level 3 Bomb Drop (Keese Corridor)": 0x4A, + "Level 3 Bomb Drop (Darknuts Central)": 0x5B, + "Level 3 Rupee Drop (Zols and Keese East)": 0x5D, + "Level 4 Item (Stepladder)": 0x60, + "Level 4 Map": 0x21, + "Level 4 Compass": 0x62, + "Level 4 Boss": 0x13, + "Level 4 Triforce": 0x03, + "Level 4 Key Drop (Keese Entrance)": 0x70, + "Level 4 Key Drop (Keese Central)": 0x51, + "Level 4 Key Drop (Zols)": 0x40, + "Level 4 Key Drop (Keese North)": 0x01, + "Level 5 Item (Recorder)": 0x04, + "Level 5 Map": 0x46, + "Level 5 Compass": 0x37, + "Level 5 Boss": 0x24, + "Level 5 Triforce": 0x14, + "Level 5 Key Drop (Keese North)": 0x16, + "Level 5 Key Drop (Gibdos North)": 0x26, + "Level 5 Key Drop (Gibdos Central)": 0x47, + "Level 5 Key Drop (Pols Voice Entrance)": 0x77, + "Level 5 Key Drop (Gibdos Entrance)": 0x66, + "Level 5 Key Drop (Gibdos, Keese, and Pols Voice)": 0x27, + "Level 5 Key Drop (Zols)": 0x55, + "Level 5 Bomb Drop (Gibdos)": 0x65, + "Level 5 Bomb Drop (Dodongos)": 0x56, + "Level 5 Rupee Drop (Zols)": 0x57, + "Level 6 Item (Magical Rod)": 0x75, + "Level 6 Map": 0x19, + "Level 6 Compass": 0x68, + "Level 6 Boss": 0x1C, + "Level 6 Triforce": 0x0C, + "Level 6 Key Drop (Wizzrobes Entrance)": 0x7A, + "Level 6 Key Drop (Keese)": 0x58, + "Level 6 Key Drop (Wizzrobes North Island)": 0x29, + "Level 6 Key Drop (Wizzrobes North Stream)": 0x1A, + "Level 6 Key Drop (Vires)": 0x2D, + "Level 6 Bomb Drop (Wizzrobes)": 0x3C, + "Level 6 Rupee Drop (Wizzrobes)": 0x28 +} + +floor_location_game_ids_early = {} +floor_location_game_ids_late = {} +for key, value in floor_location_game_offsets_early.items(): + floor_location_game_ids_early[key] = value + Rom.first_quest_dungeon_items_early + +floor_location_game_offsets_late = { + "Level 7 Item (Red Candle)": 0x4A, + "Level 7 Map": 0x18, + "Level 7 Compass": 0x5A, + "Level 7 Boss": 0x2A, + "Level 7 Triforce": 0x2B, + "Level 7 Key Drop (Ropes)": 0x78, + "Level 7 Key Drop (Goriyas)": 0x0A, + "Level 7 Key Drop (Stalfos)": 0x6D, + "Level 7 Key Drop (Moldorms)": 0x3A, + "Level 7 Bomb Drop (Goriyas South)": 0x69, + "Level 7 Bomb Drop (Keese and Spikes)": 0x68, + "Level 7 Bomb Drop (Moldorms South)": 0x7A, + "Level 7 Bomb Drop (Moldorms North)": 0x0B, + "Level 7 Bomb Drop (Goriyas North)": 0x1B, + "Level 7 Bomb Drop (Dodongos)": 0x0C, + "Level 7 Bomb Drop (Digdogger)": 0x6C, + "Level 7 Rupee Drop (Goriyas Central)": 0x38, + "Level 7 Rupee Drop (Dodongos)": 0x58, + "Level 7 Rupee Drop (Goriyas North)": 0x09, + "Level 8 Item (Magical Key)": 0x0F, + "Level 8 Item (Book of Magic)": 0x6F, + "Level 8 Map": 0x2E, + "Level 8 Compass": 0x5F, + "Level 8 Boss": 0x3C, + "Level 8 Triforce": 0x2C, + "Level 8 Key Drop (Darknuts West)": 0x5C, + "Level 8 Key Drop (Darknuts Far West)": 0x4B, + "Level 8 Key Drop (Pols Voice South)": 0x4C, + "Level 8 Key Drop (Pols Voice and Keese)": 0x5D, + "Level 8 Key Drop (Darknuts Central)": 0x5E, + "Level 8 Key Drop (Keese and Zols Entrance)": 0x7F, + "Level 8 Bomb Drop (Darknuts North)": 0x0E, + "Level 8 Bomb Drop (Darknuts East)": 0x3F, + "Level 8 Bomb Drop (Pols Voice North)": 0x1D, + "Level 8 Rupee Drop (Manhandla Entrance West)": 0x7D, + "Level 8 Rupee Drop (Manhandla Entrance North)": 0x6E, + "Level 8 Rupee Drop (Darknuts and Gibdos)": 0x4E, + "Level 9 Item (Silver Arrow)": 0x4F, + "Level 9 Item (Red Ring)": 0x00, + "Level 9 Map": 0x27, + "Level 9 Compass": 0x35, + "Level 9 Key Drop (Patra Southwest)": 0x61, + "Level 9 Key Drop (Like Likes and Zols East)": 0x56, + "Level 9 Key Drop (Wizzrobes and Bubbles East)": 0x47, + "Level 9 Key Drop (Wizzrobes East Island)": 0x57, + "Level 9 Bomb Drop (Blue Lanmolas)": 0x11, + "Level 9 Bomb Drop (Gels Lake)": 0x23, + "Level 9 Bomb Drop (Like Likes and Zols Corridor)": 0x25, + "Level 9 Bomb Drop (Patra Northeast)": 0x16, + "Level 9 Bomb Drop (Vires)": 0x37, + "Level 9 Rupee Drop (Wizzrobes West Island)": 0x40, + "Level 9 Rupee Drop (Red Lanmolas)": 0x12, + "Level 9 Rupee Drop (Keese Southwest)": 0x62, + "Level 9 Rupee Drop (Keese Central Island)": 0x34, + "Level 9 Rupee Drop (Wizzrobes Central)": 0x44, + "Level 9 Rupee Drop (Wizzrobes North Island)": 0x15, + "Level 9 Rupee Drop (Gels East)": 0x26 +} + +for key, value in floor_location_game_offsets_late.items(): + floor_location_game_ids_late[key] = value + Rom.first_quest_dungeon_items_late + +dungeon_items = {**floor_location_game_ids_early, **floor_location_game_ids_late} + +shop_location_ids = { + "Arrow Shop Item Left": 0x18637, + "Arrow Shop Item Middle": 0x18638, + "Arrow Shop Item Right": 0x18639, + "Candle Shop Item Left": 0x1863A, + "Candle Shop Item Middle": 0x1863B, + "Candle Shop Item Right": 0x1863C, + "Shield Shop Item Left": 0x1863D, + "Shield Shop Item Middle": 0x1863E, + "Shield Shop Item Right": 0x1863F, + "Blue Ring Shop Item Left": 0x18640, + "Blue Ring Shop Item Middle": 0x18641, + "Blue Ring Shop Item Right": 0x18642, + "Potion Shop Item Left": 0x1862E, + "Potion Shop Item Middle": 0x1862F, + "Potion Shop Item Right": 0x18630 +} + +shop_price_location_ids = { + "Arrow Shop Item Left": 0x18673, + "Arrow Shop Item Middle": 0x18674, + "Arrow Shop Item Right": 0x18675, + "Candle Shop Item Left": 0x18676, + "Candle Shop Item Middle": 0x18677, + "Candle Shop Item Right": 0x18678, + "Shield Shop Item Left": 0x18679, + "Shield Shop Item Middle": 0x1867A, + "Shield Shop Item Right": 0x1867B, + "Blue Ring Shop Item Left": 0x1867C, + "Blue Ring Shop Item Middle": 0x1867D, + "Blue Ring Shop Item Right": 0x1867E, + "Potion Shop Item Left": 0x1866A, + "Potion Shop Item Middle": 0x1866B, + "Potion Shop Item Right": 0x1866C +} + +secret_money_ids = { + "Secret Money 1": 0x18680, + "Secret Money 2": 0x18683, + "Secret Money 3": 0x18686 +} + +major_location_ids = { + "Starting Sword Cave": 0x18611, + "White Sword Pond": 0x18617, + "Magical Sword Grave": 0x1861A, + "Letter Cave": 0x18629, + "Take Any Item Left": 0x18613, + "Take Any Item Middle": 0x18614, + "Take Any Item Right": 0x18615, + "Armos Knights": 0x10D05, + "Ocean Heart Container": 0x1789A +} + +major_location_offsets = { + "Starting Sword Cave": 0x77, + "White Sword Pond": 0x0A, + "Magical Sword Grave": 0x21, + "Letter Cave": 0x0E, + # "Take Any Item Left": 0x7B, + # "Take Any Item Middle": 0x2C, + # "Take Any Item Right": 0x47, + "Armos Knights": 0x24, + "Ocean Heart Container": 0x5F +} + +overworld_locations = [ + "Starting Sword Cave", + "White Sword Pond", + "Magical Sword Grave", + "Letter Cave", + "Armos Knights", + "Ocean Heart Container" +] + +underworld1_locations = [*floor_location_game_offsets_early.keys()] + +underworld2_locations = [*floor_location_game_offsets_late.keys()] + +#cave_locations = ["Take Any Item Left", "Take Any Item Middle", "Take Any Item Right"] + [*shop_locations] + +location_table_base = [x for x in major_locations] + \ + [y for y in all_level_locations] + \ + [z for z in shop_locations] +location_table = {} +for i, location in enumerate(location_table_base): + location_table[location] = i + +location_ids = {**dungeon_items, **shop_location_ids, **major_location_ids} diff --git a/worlds/tloz/Options.py b/worlds/tloz/Options.py new file mode 100644 index 0000000000..47eb9509aa --- /dev/null +++ b/worlds/tloz/Options.py @@ -0,0 +1,40 @@ +import typing +from Options import Option, DefaultOnToggle, Choice + + +class ExpandedPool(DefaultOnToggle): + """Puts room clear drops into the pool of items and locations.""" + display_name = "Expanded Item Pool" + + +class TriforceLocations(Choice): + """Where Triforce fragments can be located. Note that Triforce pieces + obtained in a dungeon will heal and warp you out, while overworld Triforce pieces obtained will appear to have + no immediate effect. This is normal.""" + display_name = "Triforce Locations" + option_vanilla = 0 + option_dungeons = 1 + option_anywhere = 2 + + +class StartingPosition(Choice): + """How easy is the start of the game. + Safe means a weapon is guaranteed in Starting Sword Cave. + Unsafe means that a weapon is guaranteed between Starting Sword Cave, Letter Cave, and Armos Knight. + Dangerous adds these level locations to the unsafe pool (if they exist): +# Level 1 Compass, Level 2 Bomb Drop (Keese), Level 3 Key Drop (Zols Entrance), Level 3 Compass + Very Dangerous is the same as dangerous except it doesn't guarantee a weapon. It will only mean progression + will be there in single player seeds. In multi worlds, however, this means all bets are off and after checking + the dangerous spots, you could be stuck until someone sends you a weapon""" + display_name = "Starting Position" + option_safe = 0 + option_unsafe = 1 + option_dangerous = 2 + option_very_dangerous = 3 + + +tloz_options: typing.Dict[str, type(Option)] = { + "ExpandedPool": ExpandedPool, + "TriforceLocations": TriforceLocations, + "StartingPosition": StartingPosition +} diff --git a/worlds/tloz/Rom.py b/worlds/tloz/Rom.py new file mode 100644 index 0000000000..0eaf5855d1 --- /dev/null +++ b/worlds/tloz/Rom.py @@ -0,0 +1,78 @@ +import zlib +import os + +import Utils +from Patch import APDeltaPatch + +NA10CHECKSUM = 'D7AE93DF' +ROM_PLAYER_LIMIT = 65535 +ROM_NAME = 0x10 +bit_positions = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80] +candle_shop = bit_positions[5] +arrow_shop = bit_positions[4] +potion_shop = bit_positions[1] +shield_shop = bit_positions[6] +ring_shop = bit_positions[7] +take_any = bit_positions[2] +first_quest_dungeon_items_early = 0x18910 +first_quest_dungeon_items_late = 0x18C10 +game_mode = 0x12 +sword = 0x0657 +bombs = 0x0658 +arrow = 0x0659 +bow = 0x065A +candle = 0x065B +recorder = 0x065C +food = 0x065D +potion = 0x065E +magical_rod = 0x065F +raft = 0x0660 +book_of_magic = 0x0661 +ring = 0x0662 +stepladder = 0x0663 +magical_key = 0x0664 +power_bracelet = 0x0665 +letter = 0x0666 +heart_containers = 0x066F +triforce_fragments = 0x0671 +boomerang = 0x0674 +magical_boomerang = 0x0675 +magical_shield = 0x0676 +rupees_to_add = 0x067D + + + + +class TLoZDeltaPatch(APDeltaPatch): + checksum = NA10CHECKSUM + hash = NA10CHECKSUM + game = "The Legend of Zelda" + patch_file_ending = ".aptloz" + result_file_ending = ".nes" + + @classmethod + def get_source_data(cls) -> bytes: + return get_base_rom_bytes() + + +def get_base_rom_bytes(file_name: str = "") -> bytes: + base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None) + if not base_rom_bytes: + file_name = get_base_rom_path(file_name) + base_rom_bytes = bytes(Utils.read_snes_rom(open(file_name, "rb"))) + + basechecksum = str(hex(zlib.crc32(base_rom_bytes))).upper()[2:] + if NA10CHECKSUM != basechecksum: + raise Exception('Supplied Base Rom does not match known CRC-32 for NA (1.0) release. ' + 'Get the correct game and version, then dump it') + get_base_rom_bytes.base_rom_bytes = base_rom_bytes + return base_rom_bytes + + +def get_base_rom_path(file_name: str = "") -> str: + options = Utils.get_options() + if not file_name: + file_name = options["tloz_options"]["rom_file"] + if not os.path.exists(file_name): + file_name = Utils.local_path(file_name) + return file_name \ No newline at end of file diff --git a/worlds/tloz/Rules.py b/worlds/tloz/Rules.py new file mode 100644 index 0000000000..631a23d067 --- /dev/null +++ b/worlds/tloz/Rules.py @@ -0,0 +1,147 @@ +from typing import TYPE_CHECKING + +from ..generic.Rules import add_rule +from .Locations import food_locations, shop_locations +from .ItemPool import dangerous_weapon_locations + +if TYPE_CHECKING: + from . import TLoZWorld + +def set_rules(tloz_world: "TLoZWorld"): + player = tloz_world.player + world = tloz_world.multiworld + + # Boss events for a nicer spoiler log play through + for level in range(1, 9): + boss = world.get_location(f"Level {level} Boss", player) + boss_event = world.get_location(f"Level {level} Boss Status", player) + status = tloz_world.create_event(f"Boss {level} Defeated") + boss_event.place_locked_item(status) + add_rule(boss_event, lambda state, b=boss: state.can_reach(b, "Location", player)) + + # No dungeons without weapons except for the dangerous weapon locations if we're dangerous, no unsafe dungeons + for i, level in enumerate(tloz_world.levels[1:10]): + for location in level.locations: + if world.StartingPosition[player] < 1 or location.name not in dangerous_weapon_locations: + add_rule(world.get_location(location.name, player), + lambda state: state.has_group("weapons", player)) + if i > 0: # Don't need an extra heart for Level 1 + add_rule(world.get_location(location.name, player), + lambda state, hearts=i: state.has("Heart Container", player, hearts) or + (state.has("Blue Ring", player) and + state.has("Heart Container", player, int(hearts / 2))) or + (state.has("Red Ring", player) and + state.has("Heart Container", player, int(hearts / 4))) + + ) + # No requiring anything in a shop until we can farm for money + for location in shop_locations: + add_rule(world.get_location(location, player), + lambda state: state.has_group("weapons", player)) + + # Everything from 4 on up has dark rooms + for level in tloz_world.levels[4:]: + for location in level.locations: + add_rule(world.get_location(location.name, player), + lambda state: state.has_group("candles", player) + or (state.has("Magical Rod", player) and state.has("Book", player))) + + # Everything from 5 on up has gaps + for level in tloz_world.levels[5:]: + for location in level.locations: + add_rule(world.get_location(location.name, player), + lambda state: state.has("Stepladder", player)) + + add_rule(world.get_location("Level 5 Boss", player), + lambda state: state.has("Recorder", player)) + + add_rule(world.get_location("Level 6 Boss", player), + lambda state: state.has("Bow", player) and state.has_group("arrows", player)) + + add_rule(world.get_location("Level 7 Item (Red Candle)", player), + lambda state: state.has("Recorder", player)) + add_rule(world.get_location("Level 7 Boss", player), + lambda state: state.has("Recorder", player)) + if world.ExpandedPool[player]: + add_rule(world.get_location("Level 7 Key Drop (Stalfos)", player), + lambda state: state.has("Recorder", player)) + add_rule(world.get_location("Level 7 Bomb Drop (Digdogger)", player), + lambda state: state.has("Recorder", player)) + add_rule(world.get_location("Level 7 Rupee Drop (Dodongos)", player), + lambda state: state.has("Recorder", player)) + + for location in food_locations: + if world.ExpandedPool[player] or "Drop" not in location: + add_rule(world.get_location(location, player), + lambda state: state.has("Food", player)) + + add_rule(world.get_location("Level 8 Item (Magical Key)", player), + lambda state: state.has("Bow", player) and state.has_group("arrows", player)) + if world.ExpandedPool[player]: + add_rule(world.get_location("Level 8 Bomb Drop (Darknuts North)", player), + lambda state: state.has("Bow", player) and state.has_group("arrows", player)) + + for location in tloz_world.levels[9].locations: + add_rule(world.get_location(location.name, player), + lambda state: state.has("Triforce Fragment", player, 8) and + state.has_group("swords", player)) + + # Yes we are looping this range again for Triforce locations. No I can't add it to the boss event loop + for level in range(1, 9): + add_rule(world.get_location(f"Level {level} Triforce", player), + lambda state, l=level: state.has(f"Boss {l} Defeated", player)) + + # Sword, raft, and ladder spots + add_rule(world.get_location("White Sword Pond", player), + lambda state: state.has("Heart Container", player, 2)) + add_rule(world.get_location("Magical Sword Grave", player), + lambda state: state.has("Heart Container", player, 9)) + + stepladder_locations = ["Ocean Heart Container", "Level 4 Triforce", "Level 4 Boss", "Level 4 Map"] + stepladder_locations_expanded = ["Level 4 Key Drop (Keese North)"] + for location in stepladder_locations: + add_rule(world.get_location(location, player), + lambda state: state.has("Stepladder", player)) + if world.ExpandedPool[player]: + for location in stepladder_locations_expanded: + add_rule(world.get_location(location, player), + lambda state: state.has("Stepladder", player)) + + if world.StartingPosition[player] != 2: + # Don't allow Take Any Items until we can actually get in one + if world.ExpandedPool[player]: + add_rule(world.get_location("Take Any Item Left", player), + lambda state: state.has_group("candles", player) or + state.has("Raft", player)) + add_rule(world.get_location("Take Any Item Middle", player), + lambda state: state.has_group("candles", player) or + state.has("Raft", player)) + add_rule(world.get_location("Take Any Item Right", player), + lambda state: state.has_group("candles", player) or + state.has("Raft", player)) + for location in tloz_world.levels[4].locations: + add_rule(world.get_location(location.name, player), + lambda state: state.has("Raft", player) or state.has("Recorder", player)) + for location in tloz_world.levels[7].locations: + add_rule(world.get_location(location.name, player), + lambda state: state.has("Recorder", player)) + for location in tloz_world.levels[8].locations: + add_rule(world.get_location(location.name, player), + lambda state: state.has("Bow", player)) + + add_rule(world.get_location("Potion Shop Item Left", player), + lambda state: state.has("Letter", player)) + add_rule(world.get_location("Potion Shop Item Middle", player), + lambda state: state.has("Letter", player)) + add_rule(world.get_location("Potion Shop Item Right", player), + lambda state: state.has("Letter", player)) + + add_rule(world.get_location("Shield Shop Item Left", player), + lambda state: state.has_group("candles", player) or + state.has("Bomb", player)) + add_rule(world.get_location("Shield Shop Item Middle", player), + lambda state: state.has_group("candles", player) or + state.has("Bomb", player)) + add_rule(world.get_location("Shield Shop Item Right", player), + lambda state: state.has_group("candles", player) or + state.has("Bomb", player)) \ No newline at end of file diff --git a/worlds/tloz/__init__.py b/worlds/tloz/__init__.py new file mode 100644 index 0000000000..3304569cbd --- /dev/null +++ b/worlds/tloz/__init__.py @@ -0,0 +1,313 @@ +import logging +import os +import threading +import pkgutil +from typing import NamedTuple, Union, Dict, Any + +import bsdiff4 + +import Utils +from BaseClasses import Item, Location, Region, Entrance, MultiWorld, ItemClassification, Tutorial +from .ItemPool import generate_itempool, starting_weapons, dangerous_weapon_locations +from .Items import item_table, item_prices, item_game_ids +from .Locations import location_table, level_locations, major_locations, shop_locations, all_level_locations, \ + standard_level_locations, shop_price_location_ids, secret_money_ids, location_ids, food_locations +from .Options import tloz_options +from .Rom import TLoZDeltaPatch, get_base_rom_path, first_quest_dungeon_items_early, first_quest_dungeon_items_late +from .Rules import set_rules +from worlds.AutoWorld import World, WebWorld +from worlds.generic.Rules import add_rule + + +class TLoZWeb(WebWorld): + theme = "stone" + setup = Tutorial( + "Multiworld Setup Tutorial", + "A guide to setting up The Legend of Zelda for Archipelago on your computer.", + "English", + "multiworld_en.md", + "multiworld/en", + ["Rosalie and Figment"] + ) + + tutorials = [setup] + + +class TLoZWorld(World): + """ + The Legend of Zelda needs almost no introduction. Gather the eight fragments of the + Triforce of Courage, enter Death Mountain, defeat Ganon, and rescue Princess Zelda. + This randomizer shuffles all the items in the game around, leading to a new adventure + every time. + """ + option_definitions = tloz_options + game = "The Legend of Zelda" + topology_present = False + data_version = 1 + base_id = 7000 + web = TLoZWeb() + + item_name_to_id = {name: data.code for name, data in item_table.items()} + location_name_to_id = location_table + + item_name_groups = { + 'weapons': starting_weapons, + 'swords': { + "Sword", "White Sword", "Magical Sword" + }, + "candles": { + "Candle", "Red Candle" + }, + "arrows": { + "Arrow", "Silver Arrow" + } + } + + for k, v in item_name_to_id.items(): + item_name_to_id[k] = v + base_id + + for k, v in location_name_to_id.items(): + if v is not None: + location_name_to_id[k] = v + base_id + + def __init__(self, world: MultiWorld, player: int): + super().__init__(world, player) + self.generator_in_use = threading.Event() + self.rom_name_available_event = threading.Event() + self.levels = None + self.filler_items = None + + def create_item(self, name: str): + return TLoZItem(name, item_table[name].classification, self.item_name_to_id[name], self.player) + + def create_event(self, event: str): + return TLoZItem(event, ItemClassification.progression, None, self.player) + + def create_location(self, name, id, parent, event=False): + return_location = TLoZLocation(self.player, name, id, parent) + return_location.event = event + return return_location + + def create_regions(self): + menu = Region("Menu", self.player, self.multiworld) + overworld = Region("Overworld", self.player, self.multiworld) + self.levels = [None] # Yes I'm making a one-indexed array in a zero-indexed language. I hate me too. + for i in range(1, 10): + level = Region(f"Level {i}", self.player, self.multiworld) + self.levels.append(level) + new_entrance = Entrance(self.player, f"Level {i}", overworld) + new_entrance.connect(level) + overworld.exits.append(new_entrance) + self.multiworld.regions.append(level) + + for i, level in enumerate(level_locations): + for location in level: + if self.multiworld.ExpandedPool[self.player] or "Drop" not in location: + self.levels[i + 1].locations.append( + self.create_location(location, self.location_name_to_id[location], self.levels[i + 1])) + + for level in range(1, 9): + boss_event = self.create_location(f"Level {level} Boss Status", None, + self.multiworld.get_region(f"Level {level}", self.player), + True) + boss_event.show_in_spoiler = False + self.levels[level].locations.append(boss_event) + + for location in major_locations: + if self.multiworld.ExpandedPool[self.player] or "Take Any" not in location: + overworld.locations.append( + self.create_location(location, self.location_name_to_id[location], overworld)) + + for location in shop_locations: + overworld.locations.append( + self.create_location(location, self.location_name_to_id[location], overworld)) + + ganon = self.create_location("Ganon", None, self.multiworld.get_region("Level 9", self.player)) + zelda = self.create_location("Zelda", None, self.multiworld.get_region("Level 9", self.player)) + ganon.show_in_spoiler = False + zelda.show_in_spoiler = False + self.levels[9].locations.append(ganon) + self.levels[9].locations.append(zelda) + begin_game = Entrance(self.player, "Begin Game", menu) + menu.exits.append(begin_game) + begin_game.connect(overworld) + self.multiworld.regions.append(menu) + self.multiworld.regions.append(overworld) + + set_rules = set_rules + + def generate_basic(self): + ganon = self.multiworld.get_location("Ganon", self.player) + ganon.place_locked_item(self.create_event("Triforce of Power")) + add_rule(ganon, lambda state: state.has("Silver Arrow", self.player) and state.has("Bow", self.player)) + + self.multiworld.get_location("Zelda", self.player).place_locked_item(self.create_event("Rescued Zelda!")) + add_rule(self.multiworld.get_location("Zelda", self.player), + lambda state: ganon in state.locations_checked) + + self.multiworld.completion_condition[self.player] = lambda state: state.has("Rescued Zelda!", self.player) + generate_itempool(self) + + def apply_base_patch(self, rom): + # The base patch source is on a different repo, so here's the summary of changes: + # Remove Triforce check for recorder, so you can always warp. + # Remove level check for Triforce Fragments (and maps and compasses, but this won't matter) + # Replace some code with a jump to free space + # Check if we're picking up a Triforce Fragment. If so, increment the local count + # In either case, we do the instructions we overwrote with the jump and then return to normal flow + # Remove map/compass check so they're always on + # Removing a bit from the boss roars flags, so we can have more dungeon items. This allows us to + # go past 0x1F items for dungeon items. + base_patch_location = os.path.dirname(__file__) + "/z1_base_patch.bsdiff4" + with open(base_patch_location, "rb") as base_patch: + rom_data = bsdiff4.patch(rom.read(), base_patch.read()) + rom_data = bytearray(rom_data) + # Set every item to the new nothing value, but keep room flags. Type 2 boss roars should + # become type 1 boss roars, so we at least keep the sound of roaring where it should be. + for i in range(0, 0x7F): + item = rom_data[first_quest_dungeon_items_early + i] + if item & 0b00100000: + rom_data[first_quest_dungeon_items_early + i] = item & 0b11011111 + rom_data[first_quest_dungeon_items_early + i] = item | 0b01000000 + if item & 0b00011111 == 0b00000011: # Change all Item 03s to Item 3F, the proper "nothing" + rom_data[first_quest_dungeon_items_early + i] = item | 0b00111111 + + item = rom_data[first_quest_dungeon_items_late + i] + if item & 0b00100000: + rom_data[first_quest_dungeon_items_late + i] = item & 0b11011111 + rom_data[first_quest_dungeon_items_late + i] = item | 0b01000000 + if item & 0b00011111 == 0b00000011: + rom_data[first_quest_dungeon_items_late + i] = item | 0b00111111 + return rom_data + + def apply_randomizer(self): + with open(get_base_rom_path(), 'rb') as rom: + rom_data = self.apply_base_patch(rom) + # Write each location's new data in + for location in self.multiworld.get_filled_locations(self.player): + # Zelda and Ganon aren't real locations + if location.name == "Ganon" or location.name == "Zelda": + continue + + # Neither are boss defeat events + if "Status" in location.name: + continue + + item = location.item.name + # Remote items are always going to look like Rupees. + if location.item.player != self.player: + item = "Rupee" + + item_id = item_game_ids[item] + location_id = location_ids[location.name] + + # Shop prices need to be set + if location.name in shop_locations: + if location.name[-5:] == "Right": + # Final item in stores has bit 6 and 7 set. It's what marks the cave a shop. + item_id = item_id | 0b11000000 + price_location = shop_price_location_ids[location.name] + item_price = item_prices[item] + if item == "Rupee": + item_class = location.item.classification + if item_class == ItemClassification.progression: + item_price = item_price * 2 + elif item_class == ItemClassification.useful: + item_price = item_price // 2 + elif item_class == ItemClassification.filler: + item_price = item_price // 2 + elif item_class == ItemClassification.trap: + item_price = item_price * 2 + rom_data[price_location] = item_price + if location.name == "Take Any Item Right": + # Same story as above: bit 6 is what makes this a Take Any cave + item_id = item_id | 0b01000000 + rom_data[location_id] = item_id + + # We shuffle the tiers of rupee caves. Caves that shared a value before still will. + secret_caves = self.multiworld.per_slot_randoms[self.player].sample(sorted(secret_money_ids), 3) + secret_cave_money_amounts = [20, 50, 100] + for i, amount in enumerate(secret_cave_money_amounts): + # Giving approximately double the money to keep grinding down + amount = amount * self.multiworld.per_slot_randoms[self.player].triangular(1.5, 2.5) + secret_cave_money_amounts[i] = int(amount) + for i, cave in enumerate(secret_caves): + rom_data[secret_money_ids[cave]] = secret_cave_money_amounts[i] + return rom_data + + def generate_output(self, output_directory: str): + try: + patched_rom = self.apply_randomizer() + outfilebase = 'AP_' + self.multiworld.seed_name + outfilepname = f'_P{self.player}' + outfilepname += f"_{self.multiworld.get_file_safe_player_name(self.player).replace(' ', '_')}" + outputFilename = os.path.join(output_directory, f'{outfilebase}{outfilepname}.nes') + self.rom_name_text = f'LOZ{Utils.__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed:11}\0' + self.romName = bytearray(self.rom_name_text, 'utf8')[:0x20] + self.romName.extend([0] * (0x20 - len(self.romName))) + self.rom_name = self.romName + patched_rom[0x10:0x30] = self.romName + self.playerName = bytearray(self.multiworld.player_name[self.player], 'utf8')[:0x20] + self.playerName.extend([0] * (0x20 - len(self.playerName))) + patched_rom[0x30:0x50] = self.playerName + patched_filename = os.path.join(output_directory, outputFilename) + with open(patched_filename, 'wb') as patched_rom_file: + patched_rom_file.write(patched_rom) + patch = TLoZDeltaPatch(os.path.splitext(outputFilename)[0] + TLoZDeltaPatch.patch_file_ending, + player=self.player, + player_name=self.multiworld.player_name[self.player], + patched_path=outputFilename) + patch.write() + os.unlink(patched_filename) + finally: + self.rom_name_available_event.set() + + def modify_multidata(self, multidata: dict): + import base64 + self.rom_name_available_event.wait() + new_name = base64.b64encode(bytes(self.rom_name)).decode() + multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]] + + def get_filler_item_name(self) -> str: + if self.filler_items is None: + self.filler_items = [item for item in item_table if item_table[item].classification == ItemClassification.filler] + return self.multiworld.random.choice(self.filler_items) + + def fill_slot_data(self) -> Dict[str, Any]: + if self.multiworld.ExpandedPool[self.player]: + take_any_left = self.multiworld.get_location("Take Any Item Left", self.player).item + take_any_middle = self.multiworld.get_location("Take Any Item Middle", self.player).item + take_any_right = self.multiworld.get_location("Take Any Item Right", self.player).item + if take_any_left.player == self.player: + take_any_left = take_any_left.code + else: + take_any_left = -1 + if take_any_middle.player == self.player: + take_any_middle = take_any_middle.code + else: + take_any_middle = -1 + if take_any_right.player == self.player: + take_any_right = take_any_right.code + else: + take_any_right = -1 + + slot_data = { + "TakeAnyLeft": take_any_left, + "TakeAnyMiddle": take_any_middle, + "TakeAnyRight": take_any_right + } + else: + slot_data = { + "TakeAnyLeft": -1, + "TakeAnyMiddle": -1, + "TakeAnyRight": -1 + } + return slot_data + + +class TLoZItem(Item): + game = 'The Legend of Zelda' + + +class TLoZLocation(Location): + game = 'The Legend of Zelda' \ No newline at end of file diff --git a/worlds/tloz/docs/en_The Legend of Zelda.md b/worlds/tloz/docs/en_The Legend of Zelda.md new file mode 100644 index 0000000000..e443c9b953 --- /dev/null +++ b/worlds/tloz/docs/en_The Legend of Zelda.md @@ -0,0 +1,43 @@ +# The Legend of Zelda (NES) + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +config file. + +## What does randomization do to this game? + +All acquirable pickups (except maps and compasses) are shuffled among each other. Logic is in place to ensure both +that the game is still completable, and that players aren't forced to enter dungeons under-geared. + +Shops can contain any item in the game, with prices added for the items unavailable in stores. Rupee caves are worth +more while shops cost less, making shop routing and money management important without requiring mindless grinding. + +## What items and locations get shuffled? + +In general, all item pickups in the game. More formally: + +- Every inventory item. +- Every item found in the five kinds of shops. +- Optionally, Triforce Fragments can be shuffled to be within dungeons, or anywhere. +- Optionally, enemy-held items and dungeon floor items can be included in the shuffle, along with their slots +- Maps and compasses have been replaced with bonus items, including Clocks and Fairies. + +## What items from The Legend of Zelda can appear in other players' worlds? + +All items can appear in other players' worlds. + +## What does another world's item look like in The Legend of Zelda? + +All local items appear as normal. All remote items, no matter the game they originate from, will take on the appearance +of a single Rupee. These single Rupees will have variable prices in shops: progression and trap items will cost more, +filler and useful items will cost less, and uncategorized items will be in the middle. + +## Are there any other changes made? + +- The map and compass for each dungeon start already acquired, and other items can be found in their place. +- The Recorder will warp you between all eight levels regardless of Triforce count + - It's possible for this to be your route to level 4! +- Pressing Select will cycle through your inventory. +- Shop purchases are tracked within sessions, indicated by the item being elevated from its normal position. +- What slots from a Take Any Cave have been chosen are similarly tracked. \ No newline at end of file diff --git a/worlds/tloz/docs/multiworld_en.md b/worlds/tloz/docs/multiworld_en.md new file mode 100644 index 0000000000..d3aa0afb1d --- /dev/null +++ b/worlds/tloz/docs/multiworld_en.md @@ -0,0 +1,104 @@ +# The Legend of Zelda (NES) Multiworld Setup Guide + +## Required Software + +- The Zelda1Client + - Bundled with Archipelago: [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) +- The BizHawk emulator. Versions 2.3.1 and higher are supported. Version 2.7 is recommended + - [BizHawk Official Website](http://tasvideos.org/BizHawk.html) + +## Installation Procedures + +1. Download and install the latest version of Archipelago. + - On Windows, download Setup.Archipelago..exe and run it. +2. Assign Bizhawk version 2.3.1 or higher as your default program for launching `.nes` files. + - Extract your Bizhawk folder to your Desktop, or somewhere you will remember. Below are optional additional steps + for loading ROMs more conveniently. + 1. Right-click on a ROM file and select **Open with...** + 2. Check the box next to **Always use this app to open .nes files**. + 3. Scroll to the bottom of the list and click the grey text **Look for another App on this PC**. + 4. Browse for `EmuHawk.exe` located inside your Bizhawk folder (from step 1) and click **Open**. + +## Create a Config (.yaml) File + +### What is a config file and why do I need one? + +See the guide on setting up a basic YAML at the Archipelago setup +guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) + +### Where do I get a config file? + +The Player Settings page on the website allows you to configure your personal settings and export a config file from +them. Player settings page: [The Legend of Zelda Player Settings Page](/games/The%20Legen%20of%20Zelda/player-settings) + +### Verifying your config file + +If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML +validator page: [YAML Validation page](/mysterycheck) + +## Generating a Single-Player Game + +1. Navigate to the Player Settings page, configure your options, and click the "Generate Game" button. + - Player Settings page: [The Legend of Zelda Player Settings Page](/games/The%20Legen%20of%20Zelda/player-settings) +2. You will be presented with a "Seed Info" page. +3. Click the "Create New Room" link. +4. You will be presented with a server page, from which you can download your patch file. +5. Double-click on your patch file, and the Zelda 1 Client will launch automatically, create your ROM from the + patch file, and open your emulator for you. +6. Since this is a single-player game, you will no longer need the client, so feel free to close it. + +## Joining a MultiWorld Game + +### Obtain your patch file and create your ROM + +When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done, +the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch +files. Your patch file should have a `.aptloz` extension. + +Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically launch the +client, and will also create your ROM in the same place as your patch file. + + +## Running the Client Program and Connecting to the Server + +Once the Archipelago server has been hosted: + +1. Navigate to your Archipelago install folder and run `ArchipelagoZelda1Client.exe`. +2. Notice the `/connect command` on the server hosting page. (It should look like `/connect archipelago.gg:*****` + where ***** are numbers) +3. Type the connect command into the client OR add the port to the pre-populated address on the top bar (it should + already say `archipelago.gg`) and click `connect`. + +### Running Your Game and Connecting to the Client Program + +1. Open Bizhawk 2.3.1 or higher and load your ROM OR click your ROM file if it is already associated with the + extension `*.nes`. +2. Click on the Tools menu and click on **Lua Console**. +3. Click the folder button to open a new Lua script. (CTL-O or **Script** -> **Open Script**) +4. Navigate to the location you installed Archipelago to. Open `data/lua/TLOZ/tloz_connector.lua`. + 1. If it gives a `NLua.Exceptions.LuaScriptException: .\socket.lua:13: module 'socket.core' not found:` exception + close your emulator entirely, restart it and re-run these steps. + 2. If it says `Must use a version of bizhawk 2.3.1 or higher`, double-check your Bizhawk version by clicking ** + Help** -> **About**. + +## Play the game + +When the client shows both NES and server are connected, you are good to go. You can check the connection status of the +NES at any time by running `/nes`. + +### Other Client Commands + +All other commands may be found on the [Archipelago Server and Client Commands Guide.](/tutorial/Archipelago/commands/en) +. + +## Known Issues + +- Triforce Fragments and Heart Containers may be purchased multiple times. It is up to you if you wish to take advantage +of this; logic will not account for or require purchasing any slot more than once. Remote items, no matter what they +are, will always only be sent once. +- Obtaining a remote item will move the location of any existing item in that room. Should this make an item +inaccessible, simply exit and re-enter the room. This can be used to obtain the Ocean Heart Container item without the +stepladder; logic does not account for this. +- Whether you've purchased from a shop is tracked via Archipelago between sessions: if you revisit a single player game, +none of your shop pruchase statuses will be remembered. If you want them to be, connect to the client and server like +you would in a multiplayer game. \ No newline at end of file diff --git a/worlds/tloz/requirements.txt b/worlds/tloz/requirements.txt new file mode 100644 index 0000000000..d1f50ea5e9 --- /dev/null +++ b/worlds/tloz/requirements.txt @@ -0,0 +1 @@ +bsdiff4>=1.2.2 \ No newline at end of file diff --git a/worlds/tloz/z1_base_patch.bsdiff4 b/worlds/tloz/z1_base_patch.bsdiff4 new file mode 100644 index 0000000000000000000000000000000000000000..1231c8699481ef3d89202bcb5ac7c8dc4ed83eca GIT binary patch literal 1648 zcmYL}e^k=<8pl6hKth8=@kgR|fgxeDLj09p?R-&0aFQZeLAs1k5s$N@X3IJg{6Xr~ zgw(F!Hp$Fil$vR#sjF!m{IRB~x0PC+mMgWpTGP5Kr*l8<+3TO@eV+52=RD6puV5jY z$KiN0^1+|%vG}v%0pgDkERp+rvcxo@xHR^ML;wWqGw=EelxDyNmKzhf5CmWVEHMyF z0uTvsONHATw`oq&lz`Zv4T_8cz?m)IW)j}N*UcC1Pa5H_Y6H{m!U&76+OlPHp)%AZgCZk@vdQH;K@k(Y|FW0Mgm-+k zi@x*~YvDr;Rz0}_)hj26r9p7EJ{u0asIY2SOuiI28P>GWq}D|W43Gh!l{kRh_;e!z zZvfB@8iBh(4*ufz?%*F8MU;AfjjbYZEQ{)C5JS(v+@jG493^fmOG(yvPms0srC3`9 zwZ2}VkW1x3BmlVZ8dy&6JdY3c=?vAo zNiI97^w7kU?}|!9VDPW2q(o@aS3sJSfSFtGWi5QKXfqMrCB5jv3F-SyIR%h zxVrq5NDK+NgYwy+m(_zpx55IqqB?uX!bVucNMRBA)%!V>B+6D%+`vEkaB6FAq&Rt( zy*oqqBENSEDs)5&(w}fKtgJ21_b_GWoGNa%H7oHdr2=nT#o62r=N`%RRfN!$x!$&U zYIlG53~PWsms*ri+A;k}Oo{Yi(}~~_(a!504icbfKg3X3@o0IL^+V)j>h>4;6&n4E zS1Rvvp3XDD4Xbh^*;=&YNzq+a#ZgAd@Xnou#U9amMWaUE(T%W74ZR>1*tm-YJ)Lri z@hBik{?(AhqiV$EZZ;6NT^E5y@etveuN4F)Z!-%q94&2e8$xjZ6T^0y!ak8U_B0mK z<3GCI*zA!GU{N&GFFON)C$pQ*@mse`W|AgvpI>jEoN+*TL{kzxDWuvZFPwdoz=Kzc zADm@8F1hjAyh4BaB`F2-4^hUpBwhdTT+;#s!OtajK+ziK>Z z*~9A_3#xg&bdH`2R9Qaz zuUK1h9KMJwYI{MDzA|1GaLi$^A!oO%{`bzeRzaBE7zP`U zA{V@I`Mi^Bf4@p~ur*fOg~s&t9LN#j+$&SJ|I*iZl(`Dv1)EGqJiVgew-Ek zQL*n~-@(oSV(rg)oP|w&*D9-S=Vrg*ejlIqNy@?V{~O!cK^wdF&S9RC-a*ucw5LV} z>}&qG&XD?iOMu_ZbXtQ`3)VL0tZS-Ky0ZHMD{C#IRvmNuea^|&57Wud9<#mPi_(&1 zaijt_H zb40oCi<$I*rMIV39wwh!%}N+|Jlfxz;q>-Kezhrb9@)b^_Un&lmk1?KVIfQ^}siYN70x<6rFGjxNe=RBb#>AwGDM2 z-|Z`29DRvYg;f@t`PI{gr1*VS#Kw~~rh@Xd#5`JN;4T~?W-8>;y(|3_M~r91R1Iz9 zz`A34=$sLye*G&=aX1_S5D)+)5RZT`xv(El!pB$uwt-OwkV(-a zHO57 nBP?pjsd<+KmYco}h+noV_*IK2yIqJ2aM}Eq%dO|_<&56|)Muy1 literal 0 HcmV?d00001 From 08c17c83d440c416fdf87704e9b678b5f6291556 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 5 Mar 2023 14:10:05 +0100 Subject: [PATCH 024/172] Setup: auto download SNI (#1312) Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- setup.py | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/setup.py b/setup.py index 509981da37..df2d43ec6a 100644 --- a/setup.py +++ b/setup.py @@ -7,6 +7,11 @@ import sys import sysconfig import typing import zipfile +import urllib.request +import io +import json +import threading +import platform from collections.abc import Iterable from hashlib import sha3_512 from pathlib import Path @@ -49,6 +54,68 @@ apworlds: set = { "Timespinner", } + +def download_SNI(): + print("Updating SNI") + machine_to_go = { + "x86_64": "amd64", + "aarch64": "arm64", + "armv7l": "arm" + } + platform_name = platform.system().lower() + machine_name = platform.machine().lower() + # force amd64 on macos until we have universal2 sni, otherwise resolve to GOARCH + machine_name = "amd64" if platform_name == "darwin" else machine_to_go.get(machine_name, machine_name) + with urllib.request.urlopen("https://api.github.com/repos/alttpo/sni/releases/latest") as request: + data = json.load(request) + files = data["assets"] + + source_url = None + + for file in files: + download_url: str = file["browser_download_url"] + machine_match = download_url.rsplit("-", 1)[1].split(".", 1)[0] == machine_name + if platform_name in download_url and machine_match: + # prefer "many" builds + if "many" in download_url: + source_url = download_url + break + source_url = download_url + + if source_url and source_url.endswith(".zip"): + with urllib.request.urlopen(source_url) as download: + with zipfile.ZipFile(io.BytesIO(download.read()), "r") as zf: + for member in zf.infolist(): + zf.extract(member, path="SNI") + print(f"Downloaded SNI from {source_url}") + + elif source_url and (source_url.endswith(".tar.xz") or source_url.endswith(".tar.gz")): + import tarfile + mode = "r:xz" if source_url.endswith(".tar.xz") else "r:gz" + with urllib.request.urlopen(source_url) as download: + sni_dir = None + with tarfile.open(fileobj=io.BytesIO(download.read()), mode=mode) as tf: + for member in tf.getmembers(): + if member.name.startswith("/") or "../" in member.name: + raise ValueError(f"Unexpected file '{member.name}' in {source_url}") + elif member.isdir() and not sni_dir: + sni_dir = member.name + elif member.isfile() and not sni_dir or not member.name.startswith(sni_dir): + raise ValueError(f"Expected folder before '{member.name}' in {source_url}") + elif member.isfile() and sni_dir: + tf.extract(member) + # sadly SNI is in its own folder on non-windows, so we need to rename + shutil.rmtree("SNI", True) + os.rename(sni_dir, "SNI") + print(f"Downloaded SNI from {source_url}") + + elif source_url: + print(f"Don't know how to extract SNI from {source_url}") + + else: + print(f"No SNI found for system spec {platform_name} {machine_name}") + + if os.path.exists("X:/pw.txt"): print("Using signtool") with open("X:/pw.txt", encoding="utf-8-sig") as f: @@ -174,6 +241,10 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE): print("Created Manifest") def run(self): + # start downloading sni asap + sni_thread = threading.Thread(target=download_SNI, name="SNI Downloader") + sni_thread.start() + # pre build steps print(f"Outputting to: {self.buildfolder}") os.makedirs(self.buildfolder, exist_ok=True) @@ -185,6 +256,9 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE): self.buildtime = datetime.datetime.utcnow() super().run() + # need to finish download before copying + sni_thread.join() + # include_files seems to not be done automatically. implement here for src, dst in self.include_files: print(f"copying {src} -> {self.buildfolder / dst}") From 227d59ecfb9b701de6ae6acabd32bc381cd3aad3 Mon Sep 17 00:00:00 2001 From: 0rganics Date: Sun, 5 Mar 2023 13:17:04 +0000 Subject: [PATCH 025/172] WebHost: Add a ChecksFinder tracker (#1333) Co-authored-by: Chris Wilson --- .../static/assets/checksfinderTracker.js | 49 +++++++++++ .../static/styles/checksfinderTracker.css | 30 +++++++ WebHostLib/templates/checksfinderTracker.html | 35 ++++++++ WebHostLib/tracker.py | 81 ++++++++++++++++++- 4 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 WebHostLib/static/assets/checksfinderTracker.js create mode 100644 WebHostLib/static/styles/checksfinderTracker.css create mode 100644 WebHostLib/templates/checksfinderTracker.html diff --git a/WebHostLib/static/assets/checksfinderTracker.js b/WebHostLib/static/assets/checksfinderTracker.js new file mode 100644 index 0000000000..61cf1e1559 --- /dev/null +++ b/WebHostLib/static/assets/checksfinderTracker.js @@ -0,0 +1,49 @@ +window.addEventListener('load', () => { + // Reload tracker every 60 seconds + const url = window.location; + setInterval(() => { + const ajax = new XMLHttpRequest(); + ajax.onreadystatechange = () => { + if (ajax.readyState !== 4) { return; } + + // Create a fake DOM using the returned HTML + const domParser = new DOMParser(); + const fakeDOM = domParser.parseFromString(ajax.responseText, 'text/html'); + + // Update item tracker + document.getElementById('inventory-table').innerHTML = fakeDOM.getElementById('inventory-table').innerHTML; + // Update only counters in the location-table + let counters = document.getElementsByClassName('counter'); + const fakeCounters = fakeDOM.getElementsByClassName('counter'); + for (let i = 0; i < counters.length; i++) { + counters[i].innerHTML = fakeCounters[i].innerHTML; + } + }; + ajax.open('GET', url); + ajax.send(); +}, 60000) + + // Collapsible advancement sections + const categories = document.getElementsByClassName("location-category"); + for (let i = 0; i < categories.length; i++) { + let hide_id = categories[i].id.split('-')[0]; + if (hide_id == 'Total') { + continue; + } + categories[i].addEventListener('click', function() { + // Toggle the advancement list + document.getElementById(hide_id).classList.toggle("hide"); + // Change text of the header + const tab_header = document.getElementById(hide_id+'-header').children[0]; + const orig_text = tab_header.innerHTML; + let new_text; + if (orig_text.includes("▼")) { + new_text = orig_text.replace("▼", "▲"); + } + else { + new_text = orig_text.replace("▲", "▼"); + } + tab_header.innerHTML = new_text; + }); + } +}); diff --git a/WebHostLib/static/styles/checksfinderTracker.css b/WebHostLib/static/styles/checksfinderTracker.css new file mode 100644 index 0000000000..e0cde61241 --- /dev/null +++ b/WebHostLib/static/styles/checksfinderTracker.css @@ -0,0 +1,30 @@ +#player-tracker-wrapper{ + margin: 0; +} + +#inventory-table{ + padding: 8px 10px 2px 6px; + background-color: #42b149; + border-radius: 4px; + border: 2px solid black; +} + +#inventory-table tr.column-headers td { + font-size: 1rem; + padding: 0 5rem 0 0; +} + +#inventory-table td{ + padding: 0 0.5rem 0.5rem; + font-family: LexendDeca-Light, monospace; + font-size: 2.5rem; + color: #ffffff; +} + +#inventory-table td img{ + vertical-align: middle; +} + +.hide { + display: none; +} diff --git a/WebHostLib/templates/checksfinderTracker.html b/WebHostLib/templates/checksfinderTracker.html new file mode 100644 index 0000000000..5df77f5e74 --- /dev/null +++ b/WebHostLib/templates/checksfinderTracker.html @@ -0,0 +1,35 @@ + + + + {{ player_name }}'s Tracker + + + + + +