diff --git a/BaseClasses.py b/BaseClasses.py index 7d2f9183..00d35c96 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -2359,6 +2359,62 @@ class Spoiler(object): else: self.doorTypes[(doorNames, player)] = OrderedDict([('player', player), ('doorNames', doorNames), ('type', type)]) + def parse_meta(self): + from Main import __version__ as ERVersion + + self.startinventory = list(map(str, self.world.precollected_items)) + self.metadata = {'version': ERVersion, + 'logic': self.world.logic, + 'mode': self.world.mode, + 'retro': self.world.retro, + 'bombbag': self.world.bombbag, + 'weapons': self.world.swords, + 'goal': self.world.goal, + 'shuffle': self.world.shuffle, + 'shuffleganon': self.world.shuffle_ganon, + 'shufflelinks': self.world.shufflelinks, + 'overworld_map': self.world.overworld_map, + 'door_shuffle': self.world.doorShuffle, + 'intensity': self.world.intensity, + 'dungeon_counters': self.world.dungeon_counters, + 'item_pool': self.world.difficulty, + 'item_functionality': self.world.difficulty_adjustments, + 'gt_crystals': self.world.crystals_needed_for_gt, + 'ganon_crystals': self.world.crystals_needed_for_ganon, + 'open_pyramid': self.world.open_pyramid, + 'accessibility': self.world.accessibility, + 'restricted_boss_items': self.world.restrict_boss_items, + 'hints': self.world.hints, + 'mapshuffle': self.world.mapshuffle, + 'compassshuffle': self.world.compassshuffle, + 'keyshuffle': self.world.keyshuffle, + 'bigkeyshuffle': self.world.bigkeyshuffle, + 'boss_shuffle': self.world.boss_shuffle, + 'enemy_shuffle': self.world.enemy_shuffle, + 'enemy_health': self.world.enemy_health, + 'enemy_damage': self.world.enemy_damage, + 'players': self.world.players, + 'teams': self.world.teams, + 'experimental': self.world.experimental, + 'dropshuffle': self.world.dropshuffle, + 'pottery': self.world.pottery, + 'potshuffle': self.world.potshuffle, + 'shopsanity': self.world.shopsanity, + 'pseudoboots': self.world.pseudoboots, + 'triforcegoal': self.world.treasure_hunt_count, + 'triforcepool': self.world.treasure_hunt_total, + 'code': {p: Settings.make_code(self.world, p) for p in range(1, self.world.players + 1)} + } + + for p in range(1, self.world.players + 1): + from ItemList import set_default_triforce + if self.world.custom and p in self.world.customitemarray: + self.metadata['triforcegoal'][p], self.metadata['triforcepool'][p] = set_default_triforce(self.metadata['goal'][p], self.world.customitemarray[p]["triforcepiecesgoal"], self.world.customitemarray[p]["triforcepieces"]) + else: + custom_goal = self.world.treasure_hunt_count[p] if isinstance(self.world.treasure_hunt_count, dict) else self.world.treasure_hunt_count + custom_total = self.world.treasure_hunt_total[p] if isinstance(self.world.treasure_hunt_total, dict) else self.world.treasure_hunt_total + self.metadata['triforcegoal'][p], self.metadata['triforcepool'][p] = set_default_triforce(self.metadata['goal'][p], custom_goal, custom_total) + def parse_data(self): self.medallions = OrderedDict() if self.world.players == 1: @@ -2378,8 +2434,6 @@ class Spoiler(object): self.bottles[f'Waterfall Bottle ({self.world.get_player_names(player)})'] = self.world.bottle_refills[player][0] self.bottles[f'Pyramid Bottle ({self.world.get_player_names(player)})'] = self.world.bottle_refills[player][1] - self.startinventory = list(map(str, self.world.precollected_items)) - self.locations = OrderedDict() listed_locations = set() @@ -2450,47 +2504,6 @@ class Spoiler(object): for portal in self.world.dungeon_portals[player]: self.set_lobby(portal.name, portal.door.name, player) - from Main import __version__ as ERVersion - self.metadata = {'version': ERVersion, - 'logic': self.world.logic, - 'mode': self.world.mode, - 'retro': self.world.retro, - 'bombbag': self.world.bombbag, - 'weapons': self.world.swords, - 'goal': self.world.goal, - 'shuffle': self.world.shuffle, - 'shufflelinks': self.world.shufflelinks, - 'door_shuffle': self.world.doorShuffle, - 'intensity': self.world.intensity, - 'item_pool': self.world.difficulty, - 'item_functionality': self.world.difficulty_adjustments, - 'gt_crystals': self.world.crystals_needed_for_gt, - 'ganon_crystals': self.world.crystals_needed_for_ganon, - 'open_pyramid': self.world.open_pyramid, - 'accessibility': self.world.accessibility, - 'restricted_boss_items': self.world.restrict_boss_items, - 'hints': self.world.hints, - 'mapshuffle': self.world.mapshuffle, - 'compassshuffle': self.world.compassshuffle, - 'keyshuffle': self.world.keyshuffle, - 'bigkeyshuffle': self.world.bigkeyshuffle, - 'boss_shuffle': self.world.boss_shuffle, - 'enemy_shuffle': self.world.enemy_shuffle, - 'enemy_health': self.world.enemy_health, - 'enemy_damage': self.world.enemy_damage, - 'players': self.world.players, - 'teams': self.world.teams, - 'experimental': self.world.experimental, - 'dropshuffle': self.world.dropshuffle, - 'pottery': self.world.pottery, - 'potshuffle': self.world.potshuffle, - 'shopsanity': self.world.shopsanity, - 'pseudoboots': self.world.pseudoboots, - 'triforcegoal': self.world.treasure_hunt_count, - 'triforcepool': self.world.treasure_hunt_total, - 'code': {p: Settings.make_code(self.world, p) for p in range(1, self.world.players + 1)} - } - def to_json(self): self.parse_data() out = OrderedDict() @@ -2513,22 +2526,29 @@ class Spoiler(object): return json.dumps(out) - def to_file(self, filename): + def mystery_meta_to_file(self, filename): + self.parse_meta() + with open(filename, 'w') as outfile: + outfile.write('ALttP Dungeon Randomizer Version %s - Seed: %s\n\n' % (self.metadata['version'], self.world.seed)) + for player in range(1, self.world.players + 1): + if self.world.players > 1: + outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_names(player))) + outfile.write('Logic: %s\n' % self.metadata['logic'][player]) + + def meta_to_file(self, filename): def yn(flag): return 'Yes' if flag else 'No' - self.parse_data() + line_width = 35 + self.parse_meta() with open(filename, 'w') as outfile: - outfile.write('ALttP Entrance Randomizer Version %s - Seed: %s\n\n' % (self.metadata['version'], self.world.seed)) + outfile.write('ALttP Dungeon Randomizer Version %s - Seed: %s\n\n' % (self.metadata['version'], self.world.seed)) outfile.write('Filling Algorithm: %s\n' % self.world.algorithm) outfile.write('Players: %d\n' % self.world.players) outfile.write('Teams: %d\n' % self.world.teams) for player in range(1, self.world.players + 1): if self.world.players > 1: outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_names(player))) - if len(self.hashes) > 0: - for team in range(self.world.teams): - outfile.write('%s%s\n' % (f"Hash - {self.world.player_names[player][team]} (Team {team+1}): " if self.world.teams > 1 else 'Hash: ', self.hashes[player, team])) outfile.write(f'Settings Code: {self.metadata["code"][player]}\n') outfile.write('Logic: %s\n' % self.metadata['logic'][player]) outfile.write('Mode: %s\n' % self.metadata['mode'][player]) @@ -2538,23 +2558,30 @@ class Spoiler(object): if self.metadata['goal'][player] in ['triforcehunt', 'trinity']: outfile.write('Triforce Pieces Required: %s\n' % self.metadata['triforcegoal'][player]) outfile.write('Triforce Pieces Total: %s\n' % self.metadata['triforcepool'][player]) + outfile.write('Crystals required for GT: %s\n' % (str(self.world.crystals_gt_orig[player]))) + outfile.write('Crystals required for Ganon: %s\n' % (str(self.world.crystals_ganon_orig[player]))) + outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player]) + outfile.write(f"Restricted Boss Items: {self.metadata['restricted_boss_items'][player]}\n") outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player]) outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player]) + outfile.write(f"Shopsanity: {yn(self.metadata['shopsanity'][player])}\n") + outfile.write(f"Bombbag: {yn(self.metadata['bombbag'][player])}\n") + outfile.write(f"Pseudoboots: {yn(self.metadata['pseudoboots'][player])}\n") outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player]) - outfile.write(f"Link's House Shuffled: {yn(self.metadata['shufflelinks'])}\n") + if self.metadata['shuffle'][player] != 'vanilla': + outfile.write(f"Link's House Shuffled: {yn(self.metadata['shufflelinks'])}\n") + outfile.write(f"GT/Ganon Shuffled: {yn(self.metadata['shuffleganon'])}\n") + outfile.write(f"Overworld Map: {self.metadata['overworld_map'][player]}\n") + if self.metadata['goal'][player] != 'trinity': + outfile.write('Pyramid hole pre-opened: %s\n' % ('Yes' if self.metadata['open_pyramid'][player] else 'No')) outfile.write('Door Shuffle: %s\n' % self.metadata['door_shuffle'][player]) - outfile.write('Intensity: %s\n' % self.metadata['intensity'][player]) + if self.metadata['door_shuffle'][player] != 'vanilla': + outfile.write(f"Intensity: {self.metadata['intensity'][player]}\n") + outfile.write(f"Experimental: {yn(self.metadata['experimental'][player])}\n") + outfile.write(f"Dungeon Counters: {self.metadata['dungeon_counters'][player]}\n") outfile.write(f"Drop Shuffle: {yn(self.metadata['dropshuffle'][player])}\n") outfile.write(f"Pottery Mode: {self.metadata['pottery'][player]}\n") outfile.write(f"Pot Shuffle (Legacy): {yn(self.metadata['potshuffle'][player])}\n") - addition = ' (Random)' if self.world.crystals_gt_orig[player] == 'random' else '' - outfile.write('Crystals required for GT: %s\n' % (str(self.metadata['gt_crystals'][player]) + addition)) - addition = ' (Random)' if self.world.crystals_ganon_orig[player] == 'random' else '' - outfile.write('Crystals required for Ganon: %s\n' % (str(self.metadata['ganon_crystals'][player]) + addition)) - if self.metadata['goal'][player] != 'trinity': - outfile.write('Pyramid hole pre-opened: %s\n' % ('Yes' if self.metadata['open_pyramid'][player] else 'No')) - outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player]) - outfile.write(f"Restricted Boss Items: {self.metadata['restricted_boss_items'][player]}\n") outfile.write('Map shuffle: %s\n' % ('Yes' if self.metadata['mapshuffle'][player] else 'No')) outfile.write('Compass shuffle: %s\n' % ('Yes' if self.metadata['compassshuffle'][player] else 'No')) outfile.write('Small Key shuffle: %s\n' % ('Yes' if self.metadata['keyshuffle'][player] else 'No')) @@ -2564,10 +2591,62 @@ class Spoiler(object): outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'][player]) outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'][player]) outfile.write(f"Hints: {yn(self.metadata['hints'][player])}\n") - outfile.write(f"Experimental: {yn(self.metadata['experimental'][player])}\n") - outfile.write(f"Shopsanity: {yn(self.metadata['shopsanity'][player])}\n") - outfile.write(f"Bombbag: {yn(self.metadata['bombbag'][player])}\n") - outfile.write(f"Pseudoboots: {yn(self.metadata['pseudoboots'][player])}\n") + + if self.startinventory: + outfile.write('Starting Inventory:'.ljust(line_width)) + outfile.write('\n'.ljust(line_width+1).join(self.startinventory) + '\n') + + def hashes_to_file(self, filename): + with open(filename, 'r') as infile: + contents = infile.readlines() + + def insert(lines, i, value): + lines.insert(i, value) + i += 1 + return i + + idx = 2 + if self.world.players > 1: + idx = insert(contents, idx, 'Hashes:') + for player in range(1, self.world.players + 1): + if self.world.players > 1: + idx = insert(contents, idx, f'\nPlayer {player}: {self.world.get_player_names(player)}\n') + if len(self.hashes) > 0: + for team in range(self.world.teams): + player_name = self.world.player_names[player][team] + label = f"Hash - {player_name} (Team {team+1}): " if self.world.teams > 1 else 'Hash: ' + idx = insert(contents, idx, f'{label}{self.hashes[player, team]}\n') + if self.world.players > 1: + insert(contents, idx, '\n') # return value ignored here, if you want to add more lines + + with open(filename, "w") as f: + contents = "".join(contents) + f.write(contents) + + def to_file(self, filename): + self.parse_data() + with open(filename, 'a') as outfile: + line_width = 35 + + outfile.write('\nRequirements:\n\n') + for dungeon, medallion in self.medallions.items(): + outfile.write(f'{dungeon}: {medallion} Medallion\n') + for player in range(1, self.world.players + 1): + player_name = '' if self.world.players == 1 else str(' (Player ' + str(player) + ')') + if self.world.crystals_gt_orig[player] == 'random': + outfile.write(str('Crystals Required for GT' + player_name + ':').ljust(line_width) + '%s\n' % (str(self.metadata['gt_crystals'][player]))) + if self.world.crystals_ganon_orig[player] == 'random': + outfile.write(str('Crystals Required for Ganon' + player_name + ':').ljust(line_width) + '%s\n' % (str(self.metadata['ganon_crystals'][player]))) + + outfile.write('\n\nBottle Refills:\n\n') + for fairy, bottle in self.bottles.items(): + outfile.write(f'{fairy}: {bottle}\n') + + if self.entrances: + # entrances: To/From overworld; Checking w/ & w/out "Exit" and translating accordingly + outfile.write('\nEntrances:\n\n') + outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' if self.world.players > 1 else '', self.world.fish.translate("meta","entrances",entry['entrance']), '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', self.world.fish.translate("meta","entrances",entry['exit'])) for entry in self.entrances.values()])) + if self.doors: outfile.write('\n\nDoors:\n\n') outfile.write('\n'.join( @@ -2588,19 +2667,6 @@ class Spoiler(object): # doorTypes: Small Key, Bombable, Bonkable outfile.write('\n\nDoor Types:\n\n') outfile.write('\n'.join(['%s%s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '', self.world.fish.translate("meta","doors",entry['doorNames']), self.world.fish.translate("meta","doorTypes",entry['type'])) for entry in self.doorTypes.values()])) - if self.entrances: - # entrances: To/From overworld; Checking w/ & w/out "Exit" and translating accordingly - outfile.write('\n\nEntrances:\n\n') - outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' if self.world.players > 1 else '', self.world.fish.translate("meta","entrances",entry['entrance']), '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', self.world.fish.translate("meta","entrances",entry['exit'])) for entry in self.entrances.values()])) - outfile.write('\n\nMedallions:\n') - for dungeon, medallion in self.medallions.items(): - outfile.write(f'\n{dungeon}: {medallion} Medallion') - outfile.write('\n\nBottle Refills:\n') - for fairy, bottle in self.bottles.items(): - outfile.write(f'\n{fairy}: {bottle}') - if self.startinventory: - outfile.write('\n\nStarting Inventory:\n\n') - outfile.write('\n'.join(self.startinventory)) # locations: Change up location names; in the instance of a location with multiple sections, it'll try to translate the room name # items: Item names @@ -2618,6 +2684,8 @@ class Spoiler(object): outfile.write(f'\n\nBosses ({self.world.get_player_names(player)}):\n\n') outfile.write('\n'.join([f'{x}: {y}' for x, y in bossmap.items() if y not in ['Agahnim', 'Agahnim 2', 'Ganon']])) + def playthrough_to_file(self, filename): + with open(filename, 'a') as outfile: # locations: Change up location names; in the instance of a location with multiple sections, it'll try to translate the room name # items: Item names outfile.write('\n\nPlaythrough:\n\n') diff --git a/DungeonRandomizer.py b/DungeonRandomizer.py index d24e81d6..cf0f73bc 100755 --- a/DungeonRandomizer.py +++ b/DungeonRandomizer.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 -import argparse -import copy +if __name__ == '__main__': + from source.meta.check_requirements import check_requirements + check_requirements(console=True) + import os import logging import RaceRandom as random -import textwrap -import shlex import sys from source.classes.BabelFish import BabelFish diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 0d3f502b..9398121a 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -2551,6 +2551,8 @@ mandatory_connections = [('Links House S&Q', 'Links House'), ('Lake Hylia Central Island Pier', 'Lake Hylia Central Island'), ('Lake Hylia Central Island Teleporter', 'Dark Lake Hylia Central Island'), ('Zoras River', 'Zoras River'), + ('Zora Waterfall Entryway', 'Zora Waterfall Entryway'), + ('Zora Waterfall Water Drop', 'Light World'), ('Kings Grave Outer Rocks', 'Kings Grave Area'), ('Kings Grave Inner Rocks', 'Light World'), ('Kings Grave Mirror Spot', 'Kings Grave Area'), diff --git a/Gui.py b/Gui.py index 9eab4aac..d8ebf356 100755 --- a/Gui.py +++ b/Gui.py @@ -1,3 +1,7 @@ +if __name__ == '__main__': + from source.meta.check_requirements import check_requirements + check_requirements() + import json import os import sys diff --git a/Main.py b/Main.py index fc31adb5..0728fcaf 100644 --- a/Main.py +++ b/Main.py @@ -33,7 +33,7 @@ from source.overworld.EntranceShuffle2 import link_entrances_new from source.tools.BPS import create_bps_from_data from source.classes.CustomSettings import CustomSettings -__version__ = '1.0.2.5-w' +__version__ = '1.0.2.6-w' from source.classes.BabelFish import BabelFish @@ -144,6 +144,8 @@ def main(args, seed=None, fish=None): world.settings = CustomSettings() world.settings.create_from_world(world) + outfilebase = f'DR_{args.outputname if args.outputname else world.seed}' + for player in range(1, world.players + 1): world.difficulty_requirements[player] = difficulties[world.difficulty[player]] @@ -157,6 +159,13 @@ def main(args, seed=None, fish=None): if item: world.push_precollected(item) + if args.create_spoiler and not args.jsonout: + logger.info(world.fish.translate("cli", "cli", "create.meta")) + world.spoiler.meta_to_file(output_path(f'{outfilebase}_Spoiler.txt')) + if args.mystery: + world.spoiler.mystery_meta_to_file(output_path(f'{outfilebase}_meta.txt')) + + for player in range(1, world.players + 1): if world.mode[player] != 'inverted': create_regions(world, player) else: @@ -288,7 +297,6 @@ def main(args, seed=None, fish=None): balance_money_progression(world) ensure_good_pots(world, True) - outfilebase = f'DR_{args.outputname if args.outputname else world.seed}' if args.print_custom_yaml: world.settings.record_item_placements(world) world.settings.write_to_file(output_path(f'{outfilebase}_custom.yaml')) @@ -367,6 +375,14 @@ def main(args, seed=None, fish=None): with open(output_path('%s_multidata' % outfilebase), 'wb') as f: f.write(multidata) + if args.mystery: + world.spoiler.hashes_to_file(output_path(f'{outfilebase}_meta.txt')) + elif args.create_spoiler and not args.jsonout: + world.spoiler.hashes_to_file(output_path(f'{outfilebase}_Spoiler.txt')) + if args.create_spoiler and not args.jsonout: + logger.info(world.fish.translate("cli", "cli", "patching.spoiler")) + world.spoiler.to_file(output_path(f'{outfilebase}_Spoiler.txt')) + if not args.skip_playthrough: logger.info(world.fish.translate("cli","cli","calc.playthrough")) create_playthrough(world) @@ -379,7 +395,7 @@ def main(args, seed=None, fish=None): with open(output_path('%s_Spoiler.json' % outfilebase), 'w') as outfile: outfile.write(world.spoiler.to_json()) else: - world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase)) + world.spoiler.playthrough_to_file(output_path(f'{outfilebase}_Spoiler.txt')) YES = world.fish.translate("cli","cli","yes") NO = world.fish.translate("cli","cli","no") diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5938a27d..b95a0c6d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -166,11 +166,17 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o #### Customizer +* Fixed an issue with lite/lean ER not generating * Fixed up the GUI selection of the customizer file. * Fixed up the item_pool section to skip a lot of pool manipulations. Key items will be added (like the bow) if not detected. Extra dungeon items can be added to the pool and will be confined to the dungeon if possible (and not shuffled). If the pool isn't full, junk items are added to the pool to fill it out. #### Volatile +* 1.0.2.6 + * Fix for Zelda (or any follower) going to the maiden cell supertile and the boss is not Blind. The follower will not despawn unless the boss is Blind, then the maiden will spawn as normal. + * Added a check for package requirements before running code. GUI and console both for better error messages. Thanks to mtrethewey for the idea. + * Refactored spoiler to generate in stages for better error collection. A meta file will be generated additionally for mystery seeds. Some random settings moved later in the spoiler to have the meta section at the top not spoil certain things. (GT/Ganon requirements.) Thanks to codemann and OWR for most of this work. + * Fix for Waterfall of Wishing logic in open. You must have flippers to exit the Waterfall (or moon pearl in glitched modes that allow minor glitches in logic) * 1.0.2.5 * Some textual changes for hints (capitalization standardization) * Item will be highlighted in red if experimental is on diff --git a/Regions.py b/Regions.py index 29d61bf9..c0ef0364 100644 --- a/Regions.py +++ b/Regions.py @@ -15,7 +15,7 @@ def create_regions(world, player): 'Bonk Rock Cave', 'Library', 'Potion Shop', 'Two Brothers House (East)', 'Desert Palace Stairs', 'Eastern Palace', 'Master Sword Meadow', 'Sanctuary', 'Sanctuary Grave', 'Death Mountain Entrance Rock', 'Flute Spot 1', 'Dark Desert Teleporter', 'East Hyrule Teleporter', 'South Hyrule Teleporter', 'Kakariko Teleporter', 'Elder House (East)', 'Elder House (West)', 'North Fairy Cave', 'North Fairy Cave Drop', 'Lost Woods Gamble', 'Snitch Lady (East)', 'Snitch Lady (West)', 'Tavern (Front)', - 'Bush Covered House', 'Light World Bomb Hut', 'Kakariko Shop', 'Long Fairy Cave', 'Good Bee Cave', '20 Rupee Cave', 'Cave Shop (Lake Hylia)', 'Waterfall of Wishing', 'Hyrule Castle Main Gate', + 'Bush Covered House', 'Light World Bomb Hut', 'Kakariko Shop', 'Long Fairy Cave', 'Good Bee Cave', '20 Rupee Cave', 'Cave Shop (Lake Hylia)', 'Zora Waterfall Entryway', 'Hyrule Castle Main Gate', 'Bonk Fairy (Light)', '50 Rupee Cave', 'Fortune Teller (Light)', 'Lake Hylia Fairy', 'Light Hype Fairy', 'Desert Fairy', 'Lumberjack House', 'Lake Hylia Fortune Teller', 'Kakariko Gamble Game', 'Top of Pyramid']), create_lw_region(player, 'Death Mountain Entrance', None, ['Old Man Cave (West)', 'Death Mountain Entrance Drop']), create_lw_region(player, 'Lake Hylia Central Island', None, ['Capacity Upgrade', 'Lake Hylia Central Island Teleporter']), @@ -28,6 +28,7 @@ def create_regions(world, player): create_cave_region(player, 'Blinds Hideout (Top)', 'a bounty of five items', ["Blind's Hideout - Top"]), create_cave_region(player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit', ['Link\'s Uncle', 'Secret Passage'], ['Hyrule Castle Secret Entrance Exit']), create_lw_region(player, 'Zoras River', ['King Zora', 'Zora\'s Ledge']), + create_lw_region(player, 'Zora Waterfall Entryway', None, ['Waterfall of Wishing', 'Zora Waterfall Water Drop']), create_cave_region(player, 'Waterfall of Wishing', 'a cave with two chests', ['Waterfall Fairy - Left', 'Waterfall Fairy - Right']), create_lw_region(player, 'Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']), create_cave_region(player, 'Kings Grave', 'a cave with a chest', ['King\'s Tomb']), diff --git a/Rom.py b/Rom.py index 23d5f5d2..f6bb7b93 100644 --- a/Rom.py +++ b/Rom.py @@ -353,6 +353,9 @@ def patch_enemizer(world, player, rom, local_rom, enemizercli, random_sprite_on_ 0xad, 0x3, 0x4, 0x29, 0x20, 0xf0, 0x1d]) rom.write_byte(0x200101, 0) # Do not close boss room door on entry. rom.write_byte(0x1B0101, 0) # Do not close boss room door on entry. (for Ijwu's enemizer) + else: + rom.write_byte(0x04DE83, 0xB3) # maiden is now something else + if random_sprite_on_hit: _populate_sprite_table() diff --git a/Rules.py b/Rules.py index e10b7d83..8eacdacc 100644 --- a/Rules.py +++ b/Rules.py @@ -793,6 +793,9 @@ def default_rules(world, 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)) # can be fake flippered into, but is in weird state inside that might prevent you from doing things. Can be improved in future Todo + # flippers or pearl to leave (via fake flippers) + set_rule(world.get_entrance('Zora Waterfall Water Drop', player), + lambda state: state.has('Flippers', player) or state.has_Pearl(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('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)) @@ -1032,6 +1035,8 @@ def inverted_rules(world, player): def no_glitches_rules(world, player): if world.mode[player] != 'inverted': add_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Flippers', player) or state.can_lift_rocks(player)) + add_rule(world.get_entrance('Zora Waterfall Entryway', player), lambda state: state.has('Flippers', player)) + add_rule(world.get_entrance('Zora Waterfall Water Drop', player), lambda state: state.has('Flippers', player)) add_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Flippers', player)) # can be fake flippered to add_rule(world.get_entrance('Hobo Bridge', player), lambda state: state.has('Flippers', player)) add_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has_Pearl(player) and state.has('Flippers', player)) diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 4e805b12..427e90aa 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -38,6 +38,7 @@ "cannot.reach.required": "Not all required items reachable. Something went terribly wrong here.", "patching.rom": "Patching ROM", "patching.spoiler": "Creating Spoiler", + "create.meta": "Creating Meta Info", "calc.playthrough": "Calculating Playthrough", "made.rom": "Patched ROM: %s", "made.playthrough": "Printed Playthrough: %s", diff --git a/source/meta/check_requirements.py b/source/meta/check_requirements.py new file mode 100644 index 00000000..680dfe8f --- /dev/null +++ b/source/meta/check_requirements.py @@ -0,0 +1,41 @@ +import importlib.util +import webbrowser +from tkinter import Tk, Label, Button, Frame + + +def check_requirements(console=False): + check_packages = {'aenum': 'aenum', + 'fast-enum': 'fast_enum', + 'python-bps-continued': 'bps', + 'colorama': 'colorama', + 'aioconsole' : 'aioconsole', + 'websockets' : 'websockets', + 'pyyaml': 'yaml'} + missing = [] + for package, import_name in check_packages.items(): + spec = importlib.util.find_spec(import_name) + if spec is None: + missing.append(package) + if len(missing) > 0: + packages = ','.join(missing) + if console: + import logging + logger = logging.getLogger('') + logger.error('You need to install the following python packages:') + logger.error(f'{packages}') + logger.error('See the step about "Installing Platform-specific dependencies":') + logger.error('https://github.com/aerinon/ALttPDoorRandomizer/blob/DoorDev/docs/BUILDING.md') + else: + master = Tk() + master.title('Error') + frame = Frame(master) + frame.pack(expand=True, padx =50, pady=50) + Label(frame, text='You need to install the following python packages:').pack() + Label(frame, text=f'{packages}').pack() + Label(frame, text='See the step about "Installing Platform-specific dependencies":').pack() + url = 'https://github.com/aerinon/ALttPDoorRandomizer/blob/DoorDev/docs/BUILDING.md' + link = Label(frame, fg='blue', cursor='hand2', text=url) + link.pack() + link.bind('', lambda e: webbrowser.open_new_tab(url)) + Button(master, text='Ok', command=master.destroy).pack() + master.mainloop() diff --git a/source/overworld/EntranceShuffle2.py b/source/overworld/EntranceShuffle2.py index 20578cc5..24760925 100644 --- a/source/overworld/EntranceShuffle2.py +++ b/source/overworld/EntranceShuffle2.py @@ -947,7 +947,7 @@ def find_entrances_and_exits(avail_pool, entrance_pool): if item in avail_pool.entrances: entrances.append(item) if item in entrance_map and entrance_map[item] in avail_pool.exits: - if item in ['Links House Exit', 'Inverted Links House Exit']: + if entrance_map[item] in ['Links House Exit', 'Inverted Links House Exit']: targets.append('Chris Houlihan Room Exit') targets.append(entrance_map[item]) elif item in single_entrance_map and single_entrance_map[item] in avail_pool.exits: @@ -1837,6 +1837,8 @@ mandatory_connections = [('Links House S&Q', 'Links House'), ('Lake Hylia Central Island Pier', 'Lake Hylia Central Island'), ('Lake Hylia Central Island Teleporter', 'Dark Lake Hylia Central Island'), ('Zoras River', 'Zoras River'), + ('Zora Waterfall Entryway', 'Zora Waterfall Entryway'), + ('Zora Waterfall Water Drop', 'Light World'), ('Kings Grave Outer Rocks', 'Kings Grave Area'), ('Kings Grave Inner Rocks', 'Light World'), ('Kings Grave Mirror Spot', 'Kings Grave Area'), diff --git a/test/customizer/std_maidencell_hc.yaml b/test/customizer/std_maidencell_hc.yaml new file mode 100644 index 00000000..61e6fa11 --- /dev/null +++ b/test/customizer/std_maidencell_hc.yaml @@ -0,0 +1,26 @@ +meta: +# seed: 872603231 + seed: 222336029 +settings: + 1: + door_shuffle: crossed +# door_shuffle: vanilla + mode: standard + shufflebosses: unique +doors: + 1: + doors: + Hyrule Dungeon Cellblock Up Stairs: Thieves Basement Block Up Stairs +bosses: + 1: + Thieves Town: Moldorm +#start_inventory: +# 1: +# - Titans Mitts +# - Ocarina +# - Moon Pearl +# - Tempered Sword +# - Boss Heart Container +# - Boss Heart Container +# - Boss Heart Container +# - Boss Heart Container