def homepage(): """ Render the homepage template on the / route """ a = Logic() print(a.run()) return render_template('home/index.html', title="Welcome")
def __init__(self, session, request, cache): self.session = session self.request = request self.cache = cache # to compute hardrooms/hellruns Logic.factory('vanilla') self.vars = self.request.vars
def __init__(self, session, request, cache): self.session = session self.request = request self.cache = cache # required for GraphUtils access to access points Logic.factory('vanilla') self.vars = self.request.vars
def __init__(self, options: Options, progress_callback=dummy_progress_callback): self.options = options self.progress_callback = progress_callback self.dry_run = bool(self.options['dry-run']) # TODO: maybe make paths configurable? # exe root path is where the executable is self.exe_root_path = Path('.').resolve() # this is where all assets/read only files are self.rando_root_path = RANDO_ROOT_PATH if not self.dry_run: self.actual_extract_path = self.exe_root_path / 'actual-extract' self.modified_extract_path = self.exe_root_path / 'modified-extract' self.oarc_cache_path = self.exe_root_path / 'oarc' self.no_logs = False self.seed = self.options['seed'] if self.seed == -1: self.seed = random.randint(0,1000000) self.rng = random.Random() self.rng.seed(self.seed) self.entrance_connections = OrderedDict([ ("Dungeon Entrance In Deep Woods", "Skyview"), ("Dungeon Entrance In Eldin Volcano", "Earth Temple"), ("Dungeon Entrance In Lanayru Desert", "Lanayru Mining Facility"), ("Dungeon Entrance In Lake Floria", "Ancient Cistern"), ("Dungeon Entrance In Sand Sea", "Sandship"), ("Dungeon Entrance In Volcano Summit", "Fire Sanctuary"), ("Dungeon Entrance On Skyloft", "Skykeep"), ]) # self.starting_items = (x.strip() for x in self.options['starting_items'] # self.starting_items: List[str] = list(filter(lambda x: x != '', self.starting_items)) self.starting_items = [] self.required_dungeons = self.rng.sample(constants.POTENTIALLY_REQUIRED_DUNGEONS, k=self.options['required-dungeon-count']) # make the order always consistent self.required_dungeons = [dungeon for dungeon in constants.POTENTIALLY_REQUIRED_DUNGEONS if dungeon in self.required_dungeons] if not self.options['randomize-tablets']: self.starting_items.append('Emerald Tablet') self.starting_items.append('Ruby Tablet') self.starting_items.append('Amber Tablet') if not self.options['swordless']: self.starting_items.append('Progressive Sword') self.starting_items.append('Progressive Sword') # if not self.options.get('randomize-sailcloth',False): # self.starting_items.append('Sailcloth') self.banned_types = self.options['banned-types'] self.race_mode_banned_locations = [] self.non_required_dungeons = [dungeon for dungeon in constants.POTENTIALLY_REQUIRED_DUNGEONS if not dungeon in self.required_dungeons] self.logic = Logic(self)
def update_total_progress_locations(self): options = OrderedDict() for option_name in OPTIONS: options[option_name] = self.get_option_value(option_name) num_progress_locations = Logic.get_num_progression_locations_static(self.cached_item_locations, options) text = "Where Should Progress Items Appear? (Selected: %d Possible Progression Locations)" % num_progress_locations self.ui.groupBox.setTitle(text)
def __init__(self, player=0, maxDiff=sys.maxsize, onlyBossLeft=False): self._items = {} self._counts = {} self.player = player self.maxDiff = maxDiff self.onlyBossLeft = onlyBossLeft # cache related self.cacheKey = 0 self.computeItemsPositions() Cache.reset() Logic.factory('vanilla') self.helpers = Logic.HelpersGraph(self) self.doorsManager = DoorsManager() self.createFacadeFunctions() self.createKnowsFunctions(player) self.resetItems()
def __init__(self, output, logic): self.interactive = True self.errorMsg = "" self.checkDuplicateMajor = False self.vcr = None self.log = utils.log.get('Solver') self.outputFileName = output self.firstLogFile = None Logic.factory(logic) self.locations = Logic.locations (self.locsAddressName, self.locsWeb2Internal) = self.initLocsAddressName() self.transWeb2Internal = self.initTransitionsName() Conf.difficultyTarget = infinity # no time limitation self.runtimeLimit_s = 0
def create_logic(db, user_id): logging.debug( "creating logic for user id {user_id}".format(user_id=user_id)) user = db.get_user_by_user_id(user_id=user_id) logging.debug("user is {user}".format(user=user)) doist = TodoistAPIWrapper(token=user.token) duration_setter = DurationSetter(doist) logging.debug("user mode is {user_mode}".format(user_mode=user.mode)) logic = Logic(ds=duration_setter, doist=doist, mode=modes[user.mode](doist=doist)) return logic
def __init__(self): self._items = {} self._counts = {} # cache related self.cacheKey = 0 self.computeItemsPositions() Cache.reset() self.helpers = Logic.HelpersGraph(self) self.doorsManager = DoorsManager() self.createFacadeFunctions() self.createKnowsFunctions() self.resetItems()
def __init__(self, logic=Logic()): # initiate screen self.root = Tk() self.root.title('SPACEGame') self.root.geometry(constants.SCREEN) self.root.configure(background='black') # create frames self.f_menu = Frame(master=self.root, width=100, height=400, bg='black') self.f_resources = Frame(master=self.root, width=300, height=100, bg='black') self.f_screen = Frame(master=self.root, width=300, height=300, bg='black') self.f_menu.pack(side=LEFT) self.f_resources.pack() self.f_screen.pack() # initiate game self.logic = logic # create_player self.logic.world.create_player('gon') self.player = self.logic.world.players[0] self.village = self.player.villages[0] # Available Resources self.resources = Resources(self.f_resources, self.village, 0, 1) # create MENU self.menu = Menu(root=self.root, menu=self.f_menu, screen=self.f_screen, world=self.logic.world, village=self.village, resources=self.resources, row_i=0, column_i=0) self.root.mainloop()
class Randomizer: def __init__(self, options: Options, progress_callback=dummy_progress_callback): self.options = options self.progress_callback = progress_callback self.dry_run = bool(self.options['dry-run']) # TODO: maybe make paths configurable? # exe root path is where the executable is self.exe_root_path = Path('.').resolve() # this is where all assets/read only files are self.rando_root_path = RANDO_ROOT_PATH if not self.dry_run: self.actual_extract_path = self.exe_root_path / 'actual-extract' self.modified_extract_path = self.exe_root_path / 'modified-extract' self.oarc_cache_path = self.exe_root_path / 'oarc' self.no_logs = self.options['no-spoiler-log'] self.seed = self.options['seed'] if self.seed == -1: self.seed = random.randint(0, 1000000) self.randomizer_hash = self._get_rando_hash() self.rng = random.Random() self.rng.seed(self.seed) if self.no_logs: self.rng.randint(0, 100) dungeons = [ "Skyview", "Earth Temple", "Lanayru Mining Facility", "Ancient Cistern", "Sandship", "Fire Sanctuary" ] if self.options['randomize-entrances'] == 'None': dungeons.append('Skykeep') dungeons.reverse() else: if self.options['randomize-entrances'] == 'Dungeons': self.rng.shuffle(dungeons) dungeons.append('Skykeep') dungeons.reverse() else: dungeons.append('Skykeep') self.rng.shuffle(dungeons) self.entrance_connections = OrderedDict([ ("Dungeon Entrance In Deep Woods", dungeons.pop()), ("Dungeon Entrance In Eldin Volcano", dungeons.pop()), ("Dungeon Entrance In Lanayru Desert", dungeons.pop()), ("Dungeon Entrance In Lake Floria", dungeons.pop()), ("Dungeon Entrance In Sand Sea", dungeons.pop()), ("Dungeon Entrance In Volcano Summit", dungeons.pop()), ("Dungeon Entrance On Skyloft", dungeons.pop()), ]) assert len(dungeons) == 0, 'Not all dungeons linked to an entrance' # self.starting_items = (x.strip() for x in self.options['starting_items'] # self.starting_items: List[str] = list(filter(lambda x: x != '', self.starting_items)) self.starting_items = [] self.required_dungeons = self.rng.sample( constants.POTENTIALLY_REQUIRED_DUNGEONS, k=self.options['required-dungeon-count']) # make the order always consistent self.required_dungeons = [ dungeon for dungeon in constants.POTENTIALLY_REQUIRED_DUNGEONS if dungeon in self.required_dungeons ] tablets = ['Emerald Tablet', 'Ruby Tablet', 'Amber Tablet'] self.starting_items.extend( self.rng.sample(tablets, k=self.options['starting-tablet-count'])) if not self.options['swordless']: self.starting_items.append('Progressive Sword') self.starting_items.append('Progressive Sword') # if not self.options.get('randomize-sailcloth',False): # self.starting_items.append('Sailcloth') if self.options['start-with-pouch']: self.starting_items.append('Progressive Pouch') self.banned_types = self.options['banned-types'] self.race_mode_banned_locations = [] self.non_required_dungeons = [ dungeon for dungeon in constants.POTENTIALLY_REQUIRED_DUNGEONS if not dungeon in self.required_dungeons ] rupoor_mode = self.options['rupoor-mode'] if rupoor_mode != 'Off': if rupoor_mode == 'Added': logic.item_types.CONSUMABLE_ITEMS += ['Rupoor'] * 15 else: self.rng.shuffle(logic.item_types.CONSUMABLE_ITEMS) replace_end_index = len(logic.item_types.CONSUMABLE_ITEMS) if rupoor_mode == 'Rupoor Mayhem': replace_end_index /= 2 for i in range(int(replace_end_index)): logic.item_types.CONSUMABLE_ITEMS[i] = 'Rupoor' self.logic = Logic(self) # self.logic.set_prerandomization_item_location("Skyloft - Beedle Second 100 Rupee Item", "Rare Treasure") # self.logic.set_prerandomization_item_location("Skyloft - Beedle Third 100 Rupee Item", "Rare Treasure") # self.logic.set_prerandomization_item_location("Skyloft - Beedle 1000 Rupee Item", "Rare Treasure") # self.logic.set_prerandomization_item_location("Skyloft - Fledge", "Progressive Sword") # self.logic.set_prerandomization_item_location("Skyloft - Owlan's Shield", "Bow") # self.logic.set_prerandomization_item_location("Skyloft - Bazaar Potion Lady", "Progressive Sword") # self.logic.set_prerandomization_item_location("Skyloft - Shed normal chest", "Potion Medal") # self.logic.set_prerandomization_item_location("Skyloft - Skyloft Archer minigame", "Heart Medal") # self.logic.set_prerandomization_item_location("Skyloft - Baby Rattle", "Sea Chart") # self.logic.set_prerandomization_item_location("Skyloft - Practice Sword", "Progressive Sword") def _get_rando_hash(self): # hash of seed, options, version current_hash = hashlib.md5() current_hash.update(str(self.seed).encode('ASCII')) current_hash.update(self.options.get_permalink().encode('ASCII')) current_hash.update(VERSION.encode('ASCII')) with open(RANDO_ROOT_PATH / 'names.txt') as f: names = [s.strip() for s in f.readlines()] hash_random = random.Random() hash_random.seed(current_hash.digest()) return ' '.join(hash_random.choice(names) for _ in range(3)) def check_valid_directory_setup(self): # catch common errors with directory setup if not self.actual_extract_path.is_dir(): raise StartupException( "ERROR: directory actual-extract doesn't exist! Make sure you have the ISO extracted into that directory" ) if not self.modified_extract_path.is_dir(): raise StartupException( "ERROR: directory modified-extract doesn't exist! Make sure you have the contents of actual-extract copied over to modified-extract" ) if not (self.actual_extract_path / 'DATA').is_dir(): raise StartupException( "ERROR: directory actual-extract doesn't contain a DATA directory! Make sure you have the ISO properly extracted into actual-extract" ) if not (self.modified_extract_path / 'DATA').is_dir(): raise StartupException( "ERROR: directory 'DATA' in modified-extract doesn't exist! Make sure you have the contents of actual-extract copied over to modified-extract" ) if not (self.modified_extract_path / 'DATA' / 'files' / 'COPYDATE_CODE_2011-09-28_153155').exists(): raise StartupException( "ERROR: the randomizer only supports NTSC-U 1.00") def get_total_progress_steps(self): if self.dry_run: return 2 else: return 2 + GAMEPATCH_TOTAL_STEP_COUNT def set_progress_callback(self, progress_callback: Callable[[str], None]): self.progress_callback = progress_callback def randomize(self): self.progress_callback('randomizing items...') self.logic.randomize_items() if self.no_logs: self.progress_callback('writing anti spoiler log...') else: self.progress_callback('writing spoiler log...') if self.options['json']: self.write_spoiler_log_json() else: self.write_spoiler_log() if not self.dry_run: GamePatcher(self).do_all_gamepatches() self.progress_callback('patching done') def write_spoiler_log(self): spoiler_log = self.get_log_header() if self.no_logs: # We still calculate progression spheres even if we're not going to write them anywhere to catch more errors in testing. self.calculate_playthrough_progression_spheres() spoiler_log_output_path = self.options['output-folder'] / ( "SS Random %s - Anti Spoiler Log.txt" % self.seed) with spoiler_log_output_path.open('w') as f: f.write(spoiler_log) return if len(self.starting_items) > 0: spoiler_log += "\n\nStarting items:\n " spoiler_log += "\n ".join(self.starting_items) spoiler_log += "\n\n\n" # Write required dungeons for i, dungeon in enumerate(self.required_dungeons): spoiler_log += f"Required Dungeon {i+1}: " + dungeon + '\n' spoiler_log += "\n\n" # Write progression spheres. spoiler_log += "Playthrough:\n" progression_spheres = self.calculate_playthrough_progression_spheres() all_progression_sphere_locations = [ loc for locs in progression_spheres for loc in locs ] zones, max_location_name_length = self.get_zones_and_max_location_name_len( all_progression_sphere_locations) format_string = " %-" + str(max_location_name_length + 1) + "s %s\n" for i, progression_sphere in enumerate(progression_spheres): # skip single gratitude crystals progression_sphere = [ loc for loc in progression_sphere if loc == 'Past - Demise' or self.logic.done_item_locations[loc] != 'Gratitude Crystal' ] spoiler_log += "%d:\n" % (i + 1) for zone_name, locations_in_zone in zones.items(): if not any(loc for (loc, _) in locations_in_zone if loc in progression_sphere): # No locations in this zone are used in this sphere. continue spoiler_log += " %s:\n" % zone_name for (location_name, specific_location_name) in locations_in_zone: if location_name in progression_sphere: if location_name == "Past - Demise": item_name = "Defeat Demise" else: item_name = self.logic.done_item_locations[ location_name] spoiler_log += format_string % ( specific_location_name + ":", item_name) spoiler_log += "\n\n\n" # Write item locations. spoiler_log += "All item locations:\n" zones, max_location_name_length = self.get_zones_and_max_location_name_len( self.logic.done_item_locations) format_string = " %-" + str(max_location_name_length + 1) + "s %s\n" for zone_name, locations_in_zone in zones.items(): spoiler_log += zone_name + ":\n" for (location_name, specific_location_name) in locations_in_zone: item_name = self.logic.done_item_locations[location_name] # skip single gratitude crystals, since they are forced vanilla if item_name == 'Gratitude Crystal': continue spoiler_log += format_string % (specific_location_name + ":", item_name) spoiler_log += "\n\n\n" # Write dungeon/secret cave entrances. spoiler_log += "Entrances:\n" for entrance_name, dungeon_or_cave_name in self.entrance_connections.items( ): spoiler_log += " %-48s %s\n" % (entrance_name + ":", dungeon_or_cave_name) spoiler_log += "\n\n\n" spoiler_log_output_path = self.options['output-folder'] / ( "SS Random %s - Spoiler Log.txt" % self.seed) with spoiler_log_output_path.open('w') as f: f.write(spoiler_log) def write_spoiler_log_json(self): spoiler_log = self.get_log_header_json() if self.no_logs: # We still calculate progression spheres even if we're not going to write them anywhere to catch more errors in testing. self.calculate_playthrough_progression_spheres() spoiler_log_output_path = self.options['output-folder'] / ( "SS Random %s - Anti Spoiler Log.json" % self.seed) with spoiler_log_output_path.open('w') as f: json.dump(spoiler_log, f, indent=2) return spoiler_log['starting-items'] = self.starting_items spoiler_log['required-dungeons'] = self.required_dungeons spoiler_log[ 'playthrough'] = self.calculate_playthrough_progression_spheres() spoiler_log['item-locations'] = self.logic.done_item_locations spoiler_log['entrances'] = self.entrance_connections spoiler_log_output_path = self.options['output-folder'] / ( "SS Random %s - Spoiler Log.json" % self.seed) with spoiler_log_output_path.open('w') as f: json.dump(spoiler_log, f, indent=2) def get_log_header_json(self): header_dict = OrderedDict() header_dict['version'] = VERSION header_dict['permalink'] = self.options.get_permalink() header_dict['seed'] = self.seed header_dict['hash'] = self.randomizer_hash non_disabled_options = [ name for name in self.options.options if (self.options[name] not in [False, [], {}, OrderedDict()] or OPTIONS[name]['type'] == 'int') and OPTIONS[name].get('permalink', True) == True ] header_dict['options'] = OrderedDict() for option_name in non_disabled_options: header_dict['options'][option_name] = self.options[option_name] return header_dict def get_log_header(self): header = "" header += "Skyward Sword Randomizer Version %s\n" % VERSION header += "Permalink: %s\n" % self.options.get_permalink() header += "Seed: %s\n" % self.seed header += "Hash : %s\n" % self.randomizer_hash header += "Options selected:\n" non_disabled_options = [ name for name in self.options.options if (self.options[name] not in [False, [], {}, OrderedDict()] or OPTIONS[name]['type'] == 'int') and OPTIONS[name].get('permalink', True) == True ] option_strings = [] for option_name in non_disabled_options: if isinstance(self.options[option_name], bool): option_strings.append(" %s" % option_name) else: value = self.options[option_name] option_strings.append(" %s: %s" % (option_name, value)) header += "\n".join(option_strings) return header def calculate_playthrough_progression_spheres(self): progression_spheres = [] logic = Logic(self) previously_accessible_locations = [] game_beatable = False while logic.unplaced_progress_items: progress_items_in_this_sphere = OrderedDict() accessible_locations = logic.get_accessible_remaining_locations() assert len(accessible_locations) >= len( previously_accessible_locations) locations_in_this_sphere = [ loc for loc in accessible_locations if loc not in previously_accessible_locations ] if not locations_in_this_sphere: raise Exception("Failed to calculate progression spheres") if not self.options.get("keylunacy"): # If the player gained access to any small keys, we need to give them the keys without counting that as a new sphere. newly_accessible_predetermined_item_locations = [ loc for loc in locations_in_this_sphere if loc in self.logic.prerandomization_item_locations ] newly_accessible_small_key_locations = [ loc for loc in newly_accessible_predetermined_item_locations if self.logic.prerandomization_item_locations[loc]. endswith(" Small Key") ] if newly_accessible_small_key_locations: for small_key_location_name in newly_accessible_small_key_locations: item_name = self.logic.prerandomization_item_locations[ small_key_location_name] assert item_name.endswith(" Small Key") logic.add_owned_item(item_name) previously_accessible_locations += newly_accessible_small_key_locations continue # Redo this loop iteration with the small key locations no longer being considered 'remaining'. for location_name in locations_in_this_sphere: item_name = self.logic.done_item_locations[location_name] if item_name in logic.all_progress_items: progress_items_in_this_sphere[location_name] = item_name if not game_beatable: game_beatable = logic.check_requirement_met( "Can Reach and Defeat Demise") if game_beatable: progress_items_in_this_sphere[ "Past - Demise"] = "Defeat Demise" progression_spheres.append(progress_items_in_this_sphere) for location_name, item_name in progress_items_in_this_sphere.items( ): if item_name == "Defeat Demise": continue logic.add_owned_item(item_name) previously_accessible_locations = accessible_locations if not game_beatable: # If the game wasn't already beatable on a previous progression sphere but it is now we add one final one just for this. game_beatable = logic.check_requirement_met( "Can Reach and Defeat Demise") if game_beatable: final_progression_sphere = OrderedDict([ ("Past - Demise", "Defeat Demise"), ]) progression_spheres.append(final_progression_sphere) return progression_spheres def get_zones_and_max_location_name_len(self, locations): zones = OrderedDict() max_location_name_length = 0 for location_name in locations: zone_name, specific_location_name = self.logic.split_location_name_by_zone( location_name) if zone_name not in zones: zones[zone_name] = [] zones[zone_name].append((location_name, specific_location_name)) if len(specific_location_name) > max_location_name_length: max_location_name_length = len(specific_location_name) return (zones, max_location_name_length)
def __init__(self, seed, clean_iso_path, randomized_output_folder, options, permalink=None, cmd_line_args=[]): self.randomized_output_folder = randomized_output_folder self.options = options self.seed = seed self.permalink = permalink self.dry_run = ("-dry" in cmd_line_args) self.disassemble = ("-disassemble" in cmd_line_args) self.export_disc_to_folder = ("-exportfolder" in cmd_line_args) self.no_logs = ("-nologs" in cmd_line_args) self.bulk_test = ("-bulk" in cmd_line_args) if self.bulk_test: self.dry_run = True self.no_logs = True self.integer_seed = self.convert_string_to_integer_md5(self.seed) self.rng = self.get_new_rng() self.arcs_by_path = {} self.jpcs_by_path = {} self.raw_files_by_path = {} if not self.dry_run: self.verify_supported_version(clean_iso_path) self.gcm = GCM(clean_iso_path) self.gcm.read_entire_disc() self.chart_list = self.get_arc("files/res/Msg/fmapres.arc").get_file("cmapdat.bin") self.bmg = self.get_arc("files/res/Msg/bmgres.arc").get_file("zel_00.bmg") if self.disassemble: self.disassemble_all_code() self.read_text_file_lists() # Starting items. This list is read by the Logic when initializing your currently owned items list. self.starting_items = [ "Wind Waker", "Wind's Requiem", "Ballad of Gales", "Song of Passing", "Hero's Shield", "Boat's Sail", ] if self.options.get("sword_mode") == "Start with Sword": self.starting_items.append("Progressive Sword") # Add starting Triforce Shards. num_starting_triforce_shards = int(self.options.get("num_starting_triforce_shards", 0)) for i in range(num_starting_triforce_shards): self.starting_items.append("Triforce Shard %d" % (i+1)) # Default dungeon entrances to be used if dungeon entrance randomizer is not on. self.dungeon_entrances = OrderedDict([ ("Dungeon Entrance On Dragon Roost Island", "Dragon Roost Cavern"), ("Dungeon Entrance In Forest Haven Sector", "Forbidden Woods"), ("Dungeon Entrance In Tower of the Gods Sector", "Tower of the Gods"), ("Dungeon Entrance On Headstone Island", "Earth Temple"), ("Dungeon Entrance On Gale Isle", "Wind Temple"), ]) self.dungeon_island_locations = OrderedDict([ ("Dragon Roost Cavern", "Dragon Roost Island"), ("Forbidden Woods", "Forest Haven"), ("Tower of the Gods", "Tower of the Gods"), ("Earth Temple", "Headstone Island"), ("Wind Temple", "Gale Isle"), ]) # Default starting island (Outset) if the starting island randomizer is not on. self.starting_island_index = 44 # Default charts for each island. self.island_number_to_chart_name = OrderedDict([ (1, "Treasure Chart 25"), (2, "Treasure Chart 7"), (3, "Treasure Chart 24"), (4, "Triforce Chart 2"), (5, "Treasure Chart 11"), (6, "Triforce Chart 7"), (7, "Treasure Chart 13"), (8, "Treasure Chart 41"), (9, "Treasure Chart 29"), (10, "Treasure Chart 22"), (11, "Treasure Chart 18"), (12, "Treasure Chart 30"), (13, "Treasure Chart 39"), (14, "Treasure Chart 19"), (15, "Treasure Chart 8"), (16, "Treasure Chart 2"), (17, "Treasure Chart 10"), (18, "Treasure Chart 26"), (19, "Treasure Chart 3"), (20, "Treasure Chart 37"), (21, "Treasure Chart 27"), (22, "Treasure Chart 38"), (23, "Triforce Chart 1"), (24, "Treasure Chart 21"), (25, "Treasure Chart 6"), (26, "Treasure Chart 14"), (27, "Treasure Chart 34"), (28, "Treasure Chart 5"), (29, "Treasure Chart 28"), (30, "Treasure Chart 35"), (31, "Triforce Chart 3"), (32, "Triforce Chart 6"), (33, "Treasure Chart 1"), (34, "Treasure Chart 20"), (35, "Treasure Chart 36"), (36, "Treasure Chart 23"), (37, "Treasure Chart 12"), (38, "Treasure Chart 16"), (39, "Treasure Chart 4"), (40, "Treasure Chart 17"), (41, "Treasure Chart 31"), (42, "Triforce Chart 5"), (43, "Treasure Chart 9"), (44, "Triforce Chart 4"), (45, "Treasure Chart 40"), (46, "Triforce Chart 8"), (47, "Treasure Chart 15"), (48, "Treasure Chart 32"), (49, "Treasure Chart 33"), ]) self.custom_model_name = "Link" self.logic = Logic(self) num_progress_locations = self.logic.get_num_progression_locations() num_progress_items = self.logic.get_num_progression_items() if num_progress_locations < num_progress_items: error_message = "Not enough progress locations to place all progress items.\n\n" error_message += "Total progress items: %d\n" % num_progress_items error_message += "Progress locations with current options: %d\n\n" % num_progress_locations error_message += "You need to check more of the progress location options in order to give the randomizer enough space to place all the items." raise TooFewProgressionLocationsError(error_message) # We need to determine if the user's selected options result in a dungeons-only-start. # Dungeons-only-start meaning that the only locations accessible at the start of the run are dungeon locations. # e.g. If the user selects Dungeons, Expensive Purchases, and Sunken Treasures, the dungeon locations are the only ones the player can check first. # We need to distinguish this situation because it can cause issues for the randomizer's item placement logic. self.logic.temporarily_make_dungeon_entrance_macros_impossible() accessible_undone_locations = self.logic.get_accessible_remaining_locations(for_progression=True) if len(accessible_undone_locations) == 0: self.dungeons_only_start = True else: self.dungeons_only_start = False self.logic.update_dungeon_entrance_macros() # Reset the dungeon entrance macros.
def control(logic, event): if event.char == 'w': logic.move_pacman((0, -1)) if event.char == 'd': logic.move_pacman((1, 0)) if event.char == 's': logic.move_pacman((0, 1)) if event.char == 'a': logic.move_pacman((-1, 0)) if __name__ == '__main__': root = Tk() Logic.set_thread_mode() logic = Logic.load_file('world.txt') info = logic.get_state() W = 800 H = 800 w = logic.field.width h = logic.field.height dx = W / w dy = H / h c = Canvas(root, width=W, height=H, bg='white') c.pack() root.bind('<Key>', lambda event: control(logic, event)) def dyn(): info = logic.get_state() dynamic(c, info)
class Randomizer: def __init__(self, options): self.options = options self.dry_run = bool(options.get('dry-run', False)) # TODO: maybe make paths configurable? if not self.dry_run: self.actual_extract_path = Path(__file__).parent / 'actual-extract' self.modified_extract_path = Path( __file__).parent / 'modified-extract' self.oarc_cache_path = Path(__file__).parent / 'oarc' # catch common errors with directory setup if not self.actual_extract_path.is_dir(): raise StartupException( "ERROR: directory actual-extract doesn't exist! Make sure you have the ISO extracted into that directory" ) if not self.modified_extract_path.is_dir(): raise StartupException( "ERROR: directory modified-extract doesn't exist! Make sure you have the contents of actual-extract copied over to modified-extract" ) if not (self.actual_extract_path / 'DATA').is_dir(): raise StartupException( "ERROR: directory actual-extract doesn't contain a DATA directory! Make sure you have the ISO properly extracted into actual-extract" ) if not (self.modified_extract_path / 'DATA').is_dir(): raise StartupException( "ERROR: directory 'DATA' in modified-extract doesn't exist! Make sure you have the contents of actual-extract copied over to modified-extract" ) if not (self.modified_extract_path / 'DATA' / 'files' / 'COPYDATE_CODE_2011-09-28_153155').exists(): raise StartupException( "ERROR: the randomizer only supports E1.00") self.options = options self.no_logs = False self.seed = options.get('seed', -1) if self.seed == -1: self.seed = random.randint(0, 1000000) self.rng = random.Random() self.rng.seed(self.seed) self.entrance_connections = OrderedDict([ ("Dungeon Entrance In Deep Woods", "Skyview"), ("Dungeon Entrance In Eldin Volcano", "Earth Temple"), ("Dungeon Entrance In Lanayru Desert", "Lanayru Mining Facility"), ("Dungeon Entrance In Lake Floria", "Ancient Cistern"), ("Dungeon Entrance In Sand Sea", "Sandship"), ("Dungeon Entrance In Volcano Summit", "Fire Sanctuary"), ("Dungeon Entrance On Skyloft", "Skykeep"), ]) self.starting_items = ( x.strip() for x in self.options.get('starting_items', '').split(',')) self.starting_items: List[str] = list( filter(lambda x: x != '', self.starting_items)) self.required_dungeons = self.rng.sample( constants.POTENTIALLY_REQUIRED_DUNGEONS, k=2) if not self.options.get('randomize-tablets', False): self.starting_items.append('Emerald Tablet') self.starting_items.append('Ruby Tablet') self.starting_items.append('Amber Tablet') if not self.options.get('swordless', False): self.starting_items.append('Progressive Sword') self.starting_items.append('Progressive Sword') # if not self.options.get('randomize-sailcloth',False): # self.starting_items.append('Sailcloth') self.banned_types = self.options.get('banned-types', '').split(',') self.banned_types = [ x.strip() for x in self.banned_types if x.strip() != '' ] unknown_types = [ x for x in self.banned_types if not x in constants.ALL_TYPES ] if len(unknown_types) > 0: print(f"ERROR: unknown banned type(s): {unknown_types}") self.race_mode_banned_locations = [] self.logic = Logic(self) self.non_required_dungeons = [ dungeon for dungeon in constants.POTENTIALLY_REQUIRED_DUNGEONS if not dungeon in self.required_dungeons ] if self.options.get('empty-unrequired-dungeons', False): for location_name in self.logic.item_locations: zone, _ = Logic.split_location_name_by_zone(location_name) if zone in self.non_required_dungeons: self.race_mode_banned_locations.append(location_name) # checks outside dungeons that require dungeons: if 'Lanayru Mining Facility' in self.non_required_dungeons: self.race_mode_banned_locations.append( 'Skyloft - Fledge Crystals') elif 'Skyview' in self.non_required_dungeons: # TODO: check again with entrance rando self.race_mode_banned_locations.append( 'Sky - Lumpy Pumpkin Roof Goddess Chest') self.race_mode_banned_locations.append( 'Sealed Grounds - Gorko Goddess Wall Reward') # self.logic.set_prerandomization_item_location("Skyloft - Fledge", "Bomb Bag") # self.logic.set_prerandomization_item_location("Skyloft - Skyloft Owlan's Shield", "Goddess Harp") # self.logic.set_prerandomization_item_location("Skyloft - Skyloft above waterfall", "Farore's Courage") # self.logic.set_prerandomization_item_location("Skyloft - Shed normal chest", "Potion Medal") # self.logic.set_prerandomization_item_location("Skyloft - Skyloft Archer minigame", "Heart Medal") # self.logic.set_prerandomization_item_location("Skyloft - Baby Rattle", "Sea Chart") # self.logic.set_prerandomization_item_location("Skyloft - Training Hall chest", "Lanayru Song of the Hero Part") def randomize(self): self.logic.randomize_items() self.write_spoiler_log() if not self.dry_run: do_gamepatches(self) # print('Required dungeons: '+(', '.join(self.required_dungeons))) def write_spoiler_log(self): if self.no_logs: # We still calculate progression spheres even if we're not going to write them anywhere to catch more errors in testing. self.calculate_playthrough_progression_spheres() return spoiler_log = self.get_log_header() # Write required dungeons spoiler_log += "Required Dungeon 1: " + self.required_dungeons[0] + '\n' spoiler_log += "Required Dungeon 2: " + self.required_dungeons[1] spoiler_log += "\n\n\n" # Write progression spheres. spoiler_log += "Playthrough:\n" progression_spheres = self.calculate_playthrough_progression_spheres() all_progression_sphere_locations = [ loc for locs in progression_spheres for loc in locs ] zones, max_location_name_length = self.get_zones_and_max_location_name_len( all_progression_sphere_locations) format_string = " %-" + str(max_location_name_length + 1) + "s %s\n" for i, progression_sphere in enumerate(progression_spheres): # skip single gratitude crystals progression_sphere = [ loc for loc in progression_sphere if loc == 'Past - Demise' or self.logic.done_item_locations[loc] != 'Gratitude Crystal' ] spoiler_log += "%d:\n" % (i + 1) for zone_name, locations_in_zone in zones.items(): if not any(loc for (loc, _) in locations_in_zone if loc in progression_sphere): # No locations in this zone are used in this sphere. continue spoiler_log += " %s:\n" % zone_name for (location_name, specific_location_name) in locations_in_zone: if location_name in progression_sphere: if location_name == "Past - Demise": item_name = "Defeat Demise" else: item_name = self.logic.done_item_locations[ location_name] spoiler_log += format_string % ( specific_location_name + ":", item_name) spoiler_log += "\n\n\n" # Write item locations. spoiler_log += "All item locations:\n" zones, max_location_name_length = self.get_zones_and_max_location_name_len( self.logic.done_item_locations) format_string = " %-" + str(max_location_name_length + 1) + "s %s\n" for zone_name, locations_in_zone in zones.items(): spoiler_log += zone_name + ":\n" for (location_name, specific_location_name) in locations_in_zone: item_name = self.logic.done_item_locations[location_name] # skip single gratitude crystals, since they are forced vanilla if item_name == 'Gratitude Crystals': continue spoiler_log += format_string % (specific_location_name + ":", item_name) spoiler_log += "\n\n\n" # Write dungeon/secret cave entrances. spoiler_log += "Entrances:\n" for entrance_name, dungeon_or_cave_name in self.entrance_connections.items( ): spoiler_log += " %-48s %s\n" % (entrance_name + ":", dungeon_or_cave_name) spoiler_log += "\n\n\n" spoiler_log_output_path = Path('.') / ( "SS Random %s - Spoiler Log.txt" % self.seed) with spoiler_log_output_path.open('w') as f: f.write(spoiler_log) def get_log_header(self): header = "" header += "Skyward Sword Randomizer Version %s\n" % VERSION # if self.permalink: # header += "Permalink: %s\n" % self.permalink header += "Seed: %s\n" % self.seed header += "Options selected:\n " non_disabled_options = [ name for name in self.options if self.options[name] not in [False, [], {}, OrderedDict()] and not name in ["dry-run", "invisible-sword", "seed"] ] option_strings = [] for option_name in non_disabled_options: if isinstance(self.options[option_name], bool): option_strings.append(option_name) else: value = self.options[option_name] option_strings.append(" %s: %s" % (option_name, value)) header += "\n".join(option_strings) header += "\n\n\n" return header def calculate_playthrough_progression_spheres(self): progression_spheres = [] logic = Logic(self) previously_accessible_locations = [] game_beatable = False while logic.unplaced_progress_items: progress_items_in_this_sphere = OrderedDict() accessible_locations = logic.get_accessible_remaining_locations() assert len(accessible_locations) >= len( previously_accessible_locations) locations_in_this_sphere = [ loc for loc in accessible_locations if loc not in previously_accessible_locations ] if not locations_in_this_sphere: raise Exception("Failed to calculate progression spheres") if not self.options.get("keylunacy"): # If the player gained access to any small keys, we need to give them the keys without counting that as a new sphere. newly_accessible_predetermined_item_locations = [ loc for loc in locations_in_this_sphere if loc in self.logic.prerandomization_item_locations ] newly_accessible_small_key_locations = [ loc for loc in newly_accessible_predetermined_item_locations if self.logic.prerandomization_item_locations[loc]. endswith(" Small Key") ] if newly_accessible_small_key_locations: for small_key_location_name in newly_accessible_small_key_locations: item_name = self.logic.prerandomization_item_locations[ small_key_location_name] assert item_name.endswith(" Small Key") logic.add_owned_item(item_name) previously_accessible_locations += newly_accessible_small_key_locations continue # Redo this loop iteration with the small key locations no longer being considered 'remaining'. for location_name in locations_in_this_sphere: item_name = self.logic.done_item_locations[location_name] if item_name in logic.all_progress_items: progress_items_in_this_sphere[location_name] = item_name if not game_beatable: game_beatable = logic.check_requirement_met( "Can Reach and Defeat Demise") if game_beatable: progress_items_in_this_sphere[ "Past - Demise"] = "Defeat Demise" progression_spheres.append(progress_items_in_this_sphere) for location_name, item_name in progress_items_in_this_sphere.items( ): if item_name == "Defeat Demise": continue logic.add_owned_item(item_name) previously_accessible_locations = accessible_locations if not game_beatable: # If the game wasn't already beatable on a previous progression sphere but it is now we add one final one just for this. game_beatable = logic.check_requirement_met( "Can Reach and Defeat Demise") if game_beatable: final_progression_sphere = OrderedDict([ ("Past - Demise", "Defeat Demise"), ]) progression_spheres.append(final_progression_sphere) return progression_spheres def get_zones_and_max_location_name_len(self, locations): zones = OrderedDict() max_location_name_length = 0 for location_name in locations: zone_name, specific_location_name = self.logic.split_location_name_by_zone( location_name) if zone_name not in zones: zones[zone_name] = [] zones[zone_name].append((location_name, specific_location_name)) if len(specific_location_name) > max_location_name_length: max_location_name_length = len(specific_location_name) return (zones, max_location_name_length)
def loadRom(self, rom, interactive=False, magic=None, startAP=None): # startAP param is only use for seedless if rom == None: # TODO::add a --logic parameter for seedless Logic.factory('varia') self.romFileName = 'seedless' self.majorsSplit = 'Full' self.areaRando = True self.bossRando = True self.escapeRando = False self.escapeTimer = "03:00" self.startAP = startAP RomPatches.setDefaultPatches(startAP) self.startArea = getAccessPoint(startAP).Start['solveArea'] # in seedless load all the vanilla transitions self.areaTransitions = vanillaTransitions[:] self.bossTransitions = vanillaBossesTransitions[:] self.escapeTransition = [vanillaEscapeTransitions[0]] # in seedless we allow mixing of area and boss transitions self.hasMixedTransitions = True self.curGraphTransitions = self.bossTransitions + self.areaTransitions + self.escapeTransition self.locations = Logic.locations for loc in self.locations: loc.itemName = 'Nothing' # set doors related to default patches DoorsManager.setDoorsColor() self.doorsRando = False else: self.romFileName = rom self.romLoader = RomLoader.factory(rom, magic) Logic.factory(self.romLoader.readLogic()) self.romLoader.readNothingId() self.locations = Logic.locations self.majorsSplit = self.romLoader.assignItems(self.locations) (self.startAP, self.startArea, startPatches) = self.romLoader.getStartAP() (self.areaRando, self.bossRando, self.escapeRando) = self.romLoader.loadPatches() RomPatches.ActivePatches += startPatches self.escapeTimer = self.romLoader.getEscapeTimer() self.doorsRando = self.romLoader.loadDoorsColor() if interactive == False: print( "ROM {} majors: {} area: {} boss: {} escape: {} patches: {} activePatches: {}" .format(rom, self.majorsSplit, self.areaRando, self.bossRando, self.escapeRando, sorted(self.romLoader.getPatches()), sorted(RomPatches.ActivePatches))) else: print( "majors: {} area: {} boss: {} escape: {} activepatches: {}" .format(self.majorsSplit, self.areaRando, self.bossRando, self.escapeRando, sorted(RomPatches.ActivePatches))) (self.areaTransitions, self.bossTransitions, self.escapeTransition, self.hasMixedTransitions) = self.romLoader.getTransitions() if interactive == True and self.debug == False: # in interactive area mode we build the graph as we play along if self.areaRando == True and self.bossRando == True: self.curGraphTransitions = [] elif self.areaRando == True: self.curGraphTransitions = self.bossTransitions[:] elif self.bossRando == True: self.curGraphTransitions = self.areaTransitions[:] else: self.curGraphTransitions = self.bossTransitions + self.areaTransitions if self.escapeRando == False: self.curGraphTransitions += self.escapeTransition else: self.curGraphTransitions = self.bossTransitions + self.areaTransitions + self.escapeTransition self.smbm = SMBoolManager() self.areaGraph = AccessGraph(Logic.accessPoints, self.curGraphTransitions) # store at each step how many locations are available self.nbAvailLocs = [] if self.log.getEffectiveLevel() == logging.DEBUG: self.log.debug("Display items at locations:") for loc in self.locations: self.log.debug('{:>50}: {:>16}'.format(loc.Name, loc.itemName))
def __init__(self, seed, clean_iso_path, randomized_output_folder, options, permalink=None, dry_run=False): self.randomized_output_folder = randomized_output_folder self.options = options self.seed = seed self.permalink = permalink self.dry_run = dry_run self.integer_seed = int( hashlib.md5(self.seed.encode('utf-8')).hexdigest(), 16) self.rng = Random() self.rng.seed(self.integer_seed) self.arcs_by_path = {} self.jpcs_by_path = {} self.raw_files_by_path = {} if not self.dry_run: self.verify_supported_version(clean_iso_path) self.gcm = GCM(clean_iso_path) self.gcm.read_entire_disc() self.chart_list = self.get_arc( "files/res/Msg/fmapres.arc").get_file("cmapdat.bin") self.bmg = self.get_arc("files/res/Msg/bmgres.arc").get_file( "zel_00.bmg") self.read_text_file_lists() # Starting items. This list is read by the Logic when initializing your currently owned items list. self.starting_items = [ "Wind Waker", "Wind's Requiem", "Ballad of Gales", "Progressive Sword", "Hero's Shield", "Boat's Sail", ] # Add starting Triforce Shards. num_starting_triforce_shards = int( self.options.get("num_starting_triforce_shards", 0)) for i in range(num_starting_triforce_shards): self.starting_items.append("Triforce Shard %d" % (i + 1)) # Default dungeon entrances to be used if dungeon entrance randomizer is not on. self.dungeon_entrances = OrderedDict([ ("Dungeon Entrance On Dragon Roost Island", "Dragon Roost Cavern"), ("Dungeon Entrance In Forest Haven Sector", "Forbidden Woods"), ("Dungeon Entrance In Tower of the Gods Sector", "Tower of the Gods"), ("Dungeon Entrance On Headstone Island", "Earth Temple"), ("Dungeon Entrance On Gale Isle", "Wind Temple"), ]) self.dungeon_island_locations = OrderedDict([ ("Dragon Roost Cavern", "Dragon Roost Island"), ("Forbidden Woods", "Forest Haven"), ("Tower of the Gods", "Tower of the Gods"), ("Earth Temple", "Headstone Island"), ("Wind Temple", "Gale Isle"), ]) # Default starting island (Outset) if the starting island randomizer is not on. self.starting_island_index = 44 # Default charts for each island. self.island_number_to_chart_name = OrderedDict([ (1, "Treasure Chart 25"), (2, "Treasure Chart 7"), (3, "Treasure Chart 24"), (4, "Triforce Chart 2"), (5, "Treasure Chart 11"), (6, "Triforce Chart 7"), (7, "Treasure Chart 13"), (8, "Treasure Chart 41"), (9, "Treasure Chart 29"), (10, "Treasure Chart 22"), (11, "Treasure Chart 18"), (12, "Treasure Chart 30"), (13, "Treasure Chart 39"), (14, "Treasure Chart 19"), (15, "Treasure Chart 8"), (16, "Treasure Chart 2"), (17, "Treasure Chart 10"), (18, "Treasure Chart 26"), (19, "Treasure Chart 3"), (20, "Treasure Chart 37"), (21, "Treasure Chart 27"), (22, "Treasure Chart 38"), (23, "Triforce Chart 1"), (24, "Treasure Chart 21"), (25, "Treasure Chart 6"), (26, "Treasure Chart 14"), (27, "Treasure Chart 34"), (28, "Treasure Chart 5"), (29, "Treasure Chart 28"), (30, "Treasure Chart 35"), (31, "Triforce Chart 3"), (32, "Triforce Chart 6"), (33, "Treasure Chart 1"), (34, "Treasure Chart 20"), (35, "Treasure Chart 36"), (36, "Treasure Chart 23"), (37, "Treasure Chart 12"), (38, "Treasure Chart 16"), (39, "Treasure Chart 4"), (40, "Treasure Chart 17"), (41, "Treasure Chart 31"), (42, "Triforce Chart 5"), (43, "Treasure Chart 9"), (44, "Triforce Chart 4"), (45, "Treasure Chart 40"), (46, "Triforce Chart 8"), (47, "Treasure Chart 15"), (48, "Treasure Chart 32"), (49, "Treasure Chart 33"), ]) self.logic = Logic(self) num_progress_locations = self.logic.get_num_progression_locations() num_progress_items = self.logic.get_num_progression_items() if num_progress_locations < num_progress_items: error_message = "Not enough progress locations to place all progress items.\n\n" error_message += "Total progress items: %d\n" % num_progress_items error_message += "Progress locations with current options: %d\n\n" % num_progress_locations error_message += "You need to check more of the progress location options in order to give the randomizer enough space to place all the items." raise TooFewProgressionLocationsError(error_message)
class Randomizer: def __init__(self, seed, clean_iso_path, randomized_output_folder, options, permalink=None, dry_run=False): self.randomized_output_folder = randomized_output_folder self.options = options self.seed = seed self.permalink = permalink self.dry_run = dry_run self.integer_seed = int( hashlib.md5(self.seed.encode('utf-8')).hexdigest(), 16) self.rng = Random() self.rng.seed(self.integer_seed) self.arcs_by_path = {} self.jpcs_by_path = {} self.raw_files_by_path = {} if not self.dry_run: self.verify_supported_version(clean_iso_path) self.gcm = GCM(clean_iso_path) self.gcm.read_entire_disc() self.chart_list = self.get_arc( "files/res/Msg/fmapres.arc").get_file("cmapdat.bin") self.bmg = self.get_arc("files/res/Msg/bmgres.arc").get_file( "zel_00.bmg") self.read_text_file_lists() # Starting items. This list is read by the Logic when initializing your currently owned items list. self.starting_items = [ "Wind Waker", "Wind's Requiem", "Ballad of Gales", "Progressive Sword", "Hero's Shield", "Boat's Sail", ] # Add starting Triforce Shards. num_starting_triforce_shards = int( self.options.get("num_starting_triforce_shards", 0)) for i in range(num_starting_triforce_shards): self.starting_items.append("Triforce Shard %d" % (i + 1)) # Default dungeon entrances to be used if dungeon entrance randomizer is not on. self.dungeon_entrances = OrderedDict([ ("Dungeon Entrance On Dragon Roost Island", "Dragon Roost Cavern"), ("Dungeon Entrance In Forest Haven Sector", "Forbidden Woods"), ("Dungeon Entrance In Tower of the Gods Sector", "Tower of the Gods"), ("Dungeon Entrance On Headstone Island", "Earth Temple"), ("Dungeon Entrance On Gale Isle", "Wind Temple"), ]) self.dungeon_island_locations = OrderedDict([ ("Dragon Roost Cavern", "Dragon Roost Island"), ("Forbidden Woods", "Forest Haven"), ("Tower of the Gods", "Tower of the Gods"), ("Earth Temple", "Headstone Island"), ("Wind Temple", "Gale Isle"), ]) # Default starting island (Outset) if the starting island randomizer is not on. self.starting_island_index = 44 # Default charts for each island. self.island_number_to_chart_name = OrderedDict([ (1, "Treasure Chart 25"), (2, "Treasure Chart 7"), (3, "Treasure Chart 24"), (4, "Triforce Chart 2"), (5, "Treasure Chart 11"), (6, "Triforce Chart 7"), (7, "Treasure Chart 13"), (8, "Treasure Chart 41"), (9, "Treasure Chart 29"), (10, "Treasure Chart 22"), (11, "Treasure Chart 18"), (12, "Treasure Chart 30"), (13, "Treasure Chart 39"), (14, "Treasure Chart 19"), (15, "Treasure Chart 8"), (16, "Treasure Chart 2"), (17, "Treasure Chart 10"), (18, "Treasure Chart 26"), (19, "Treasure Chart 3"), (20, "Treasure Chart 37"), (21, "Treasure Chart 27"), (22, "Treasure Chart 38"), (23, "Triforce Chart 1"), (24, "Treasure Chart 21"), (25, "Treasure Chart 6"), (26, "Treasure Chart 14"), (27, "Treasure Chart 34"), (28, "Treasure Chart 5"), (29, "Treasure Chart 28"), (30, "Treasure Chart 35"), (31, "Triforce Chart 3"), (32, "Triforce Chart 6"), (33, "Treasure Chart 1"), (34, "Treasure Chart 20"), (35, "Treasure Chart 36"), (36, "Treasure Chart 23"), (37, "Treasure Chart 12"), (38, "Treasure Chart 16"), (39, "Treasure Chart 4"), (40, "Treasure Chart 17"), (41, "Treasure Chart 31"), (42, "Triforce Chart 5"), (43, "Treasure Chart 9"), (44, "Triforce Chart 4"), (45, "Treasure Chart 40"), (46, "Triforce Chart 8"), (47, "Treasure Chart 15"), (48, "Treasure Chart 32"), (49, "Treasure Chart 33"), ]) self.logic = Logic(self) num_progress_locations = self.logic.get_num_progression_locations() num_progress_items = self.logic.get_num_progression_items() if num_progress_locations < num_progress_items: error_message = "Not enough progress locations to place all progress items.\n\n" error_message += "Total progress items: %d\n" % num_progress_items error_message += "Progress locations with current options: %d\n\n" % num_progress_locations error_message += "You need to check more of the progress location options in order to give the randomizer enough space to place all the items." raise TooFewProgressionLocationsError(error_message) def randomize(self): options_completed = 0 yield ("Modifying game code...", options_completed) if not self.dry_run: self.apply_necessary_tweaks() if self.options.get("swift_sail"): tweaks.make_sail_behave_like_swift_sail(self) if self.options.get("instant_text_boxes"): tweaks.make_all_text_instant(self) if self.options.get("reveal_full_sea_chart"): tweaks.apply_patch(self, "reveal_sea_chart") if self.options.get("add_shortcut_warps_between_dungeons"): tweaks.add_inter_dungeon_warp_pots(self) options_completed += 1 yield ("Randomizing...", options_completed) if self.options.get("randomize_charts"): charts.randomize_charts(self) if self.options.get("randomize_starting_island"): starting_island.randomize_starting_island(self) if self.options.get("randomize_dungeon_entrances"): dungeon_entrances.randomize_dungeon_entrances(self) items.randomize_items(self) options_completed += 2 yield ("Saving items...", options_completed) if not self.dry_run: items.write_changed_items(self) if not self.dry_run: self.apply_necessary_post_randomization_tweaks() options_completed += 7 yield ("Saving randomized ISO...", options_completed) if not self.dry_run: self.save_randomized_iso() options_completed += 9 yield ("Writing logs...", options_completed) self.write_spoiler_log() self.write_non_spoiler_log() yield ("Done", -1) def apply_necessary_tweaks(self): tweaks.apply_patch(self, "custom_funcs") tweaks.apply_patch(self, "necessary_fixes") tweaks.skip_wakeup_intro_and_start_at_dock(self) tweaks.start_ship_at_outset(self) tweaks.fix_deku_leaf_model(self) tweaks.allow_all_items_to_be_field_items(self) tweaks.remove_shop_item_forced_uniqueness_bit(self) tweaks.remove_forsaken_fortress_2_cutscenes(self) tweaks.make_items_progressive(self) tweaks.add_ganons_tower_warp_to_ff2(self) tweaks.add_chest_in_place_medli_grappling_hook_gift(self) tweaks.add_chest_in_place_queen_fairy_cutscene(self) #tweaks.add_cube_to_earth_temple_first_room(self) tweaks.add_more_magic_jars_to_dungeons(self) tweaks.remove_title_and_ending_videos(self) tweaks.modify_title_screen_logo(self) tweaks.update_game_name_icon_and_banners(self) tweaks.allow_dungeon_items_to_appear_anywhere(self) #tweaks.remove_ballad_of_gales_warp_in_cutscene(self) tweaks.fix_shop_item_y_offsets(self) tweaks.shorten_zephos_event(self) tweaks.update_korl_dialogue(self) tweaks.set_num_starting_triforce_shards(self) tweaks.add_pirate_ship_to_windfall(self) tweaks.remove_makar_kidnapping_event(self) tweaks.increase_player_movement_speeds(self) tweaks.add_chart_number_to_item_get_messages(self) tweaks.change_starting_clothes(self) customizer.replace_link_model(self) customizer.change_player_clothes_color(self) def apply_necessary_post_randomization_tweaks(self): tweaks.update_shop_item_descriptions(self) tweaks.update_auction_item_names(self) tweaks.update_sinking_ships_item_names(self) tweaks.update_item_names_in_letter_advertising_rock_spire_shop(self) tweaks.update_savage_labyrinth_hint_tablet(self) tweaks.update_fishmen_hints(self) def verify_supported_version(self, clean_iso_path): if not os.path.isfile(clean_iso_path): raise Exception("Clean WW ISO does not exist: %s" % clean_iso_path) with open(clean_iso_path, "rb") as f: game_id = try_read_str(f, 0, 6) if game_id != "GZLE01": if game_id and game_id.startswith("GZL"): raise Exception( "Invalid version of Wind Waker. Only the USA version is supported by this randomizer." ) else: raise Exception( "Invalid game given as the clean ISO. You must specify a Wind Waker ISO (USA version)." ) def read_text_file_lists(self): # Get item names. self.item_names = {} self.item_name_to_id = {} with open(os.path.join(DATA_PATH, "item_names.txt"), "r") as f: matches = re.findall(r"^([0-9a-f]{2}) - (.+)$", f.read(), re.IGNORECASE | re.MULTILINE) for item_id, item_name in matches: if item_name: item_id = int(item_id, 16) self.item_names[item_id] = item_name if item_name in self.item_name_to_id: raise Exception("Duplicate item name: " + item_name) self.item_name_to_id[item_name] = item_id # Get stage and island names for debug purposes. self.stage_names = {} with open(os.path.join(DATA_PATH, "stage_names.txt"), "r") as f: while True: stage_folder = f.readline() if not stage_folder: break stage_name = f.readline() self.stage_names[stage_folder.strip()] = stage_name.strip() self.island_names = {} self.island_number_to_name = {} with open(os.path.join(DATA_PATH, "island_names.txt"), "r") as f: while True: room_arc_name = f.readline() if not room_arc_name: break island_name = f.readline().strip() self.island_names[room_arc_name.strip()] = island_name island_number = int( re.search(r"Room(\d+)", room_arc_name).group(1)) self.island_number_to_name[island_number] = island_name self.item_ids_without_a_field_model = [] with open(os.path.join(DATA_PATH, "items_without_field_models.txt"), "r") as f: matches = re.findall(r"^([0-9a-f]{2}) ", f.read(), re.IGNORECASE | re.MULTILINE) for item_id in matches: if item_name: item_id = int(item_id, 16) self.item_ids_without_a_field_model.append(item_id) self.arc_name_pointers = {} with open( os.path.join(DATA_PATH, "item_resource_arc_name_pointers.txt"), "r") as f: matches = re.findall(r"^([0-9a-f]{2}) ([0-9a-f]{8}) ", f.read(), re.IGNORECASE | re.MULTILINE) for item_id, arc_name_pointer in matches: item_id = int(item_id, 16) arc_name_pointer = int(arc_name_pointer, 16) self.arc_name_pointers[item_id] = arc_name_pointer self.icon_name_pointer = {} with open( os.path.join(DATA_PATH, "item_resource_icon_name_pointers.txt"), "r") as f: matches = re.findall(r"^([0-9a-f]{2}) ([0-9a-f]{8}) ", f.read(), re.IGNORECASE | re.MULTILINE) for item_id, icon_name_pointer in matches: item_id = int(item_id, 16) icon_name_pointer = int(icon_name_pointer, 16) self.icon_name_pointer[item_id] = icon_name_pointer self.custom_symbols = {} with open(os.path.join(ASM_PATH, "custom_symbols.txt"), "r") as f: matches = re.findall(r"^([0-9a-f]{8}) (\S+)", f.read(), re.IGNORECASE | re.MULTILINE) for symbol_address, symbol_name in matches: self.custom_symbols[symbol_name] = int(symbol_address, 16) with open(os.path.join(DATA_PATH, "progress_item_hints.txt"), "r") as f: self.progress_item_hints = yaml.load(f) with open(os.path.join(DATA_PATH, "island_name_hints.txt"), "r") as f: self.island_name_hints = yaml.load(f) def get_arc(self, arc_path): arc_path = arc_path.replace("\\", "/") if arc_path in self.arcs_by_path: return self.arcs_by_path[arc_path] else: data = self.gcm.read_file_data(arc_path) arc = RARC(data) self.arcs_by_path[arc_path] = arc return arc def get_jpc(self, jpc_path): jpc_path = jpc_path.replace("\\", "/") if jpc_path in self.jpcs_by_path: return self.jpcs_by_path[jpc_path] else: data = self.gcm.read_file_data(jpc_path) jpc = JPC(data) self.jpcs_by_path[jpc_path] = jpc return jpc def get_raw_file(self, file_path): file_path = file_path.replace("\\", "/") if file_path in self.raw_files_by_path: return self.raw_files_by_path[file_path] else: if file_path.startswith("files/rels/"): rel_name = os.path.basename(file_path) rels_arc = self.get_arc("files/RELS.arc") rel_file_entry = rels_arc.get_file_entry(rel_name) else: rel_file_entry = None if rel_file_entry: rel_file_entry.decompress_data_if_necessary() data = rel_file_entry.data else: data = self.gcm.read_file_data(file_path) if try_read_str(data, 0, 4) == "Yaz0": data = Yaz0Decompressor.decompress(data) self.raw_files_by_path[file_path] = data return data def replace_arc(self, arc_path, new_data): if arc_path not in self.gcm.files_by_path: raise Exception("Cannot replace RARC that doesn't exist: " + arc_path) arc = RARC(new_data) self.arcs_by_path[arc_path] = arc def replace_raw_file(self, file_path, new_data): if file_path not in self.gcm.files_by_path: raise Exception("Cannot replace file that doesn't exist: " + file_path) self.raw_files_by_path[file_path] = new_data def save_randomized_iso(self): self.bmg.save_changes() changed_files = {} for file_path, data in self.raw_files_by_path.items(): if file_path.startswith("files/rels/"): rel_name = os.path.basename(file_path) rels_arc = self.get_arc("files/RELS.arc") rel_file_entry = rels_arc.get_file_entry(rel_name) if rel_file_entry: # Modify the RELS.arc entry for this rel. rel_file_entry.data = data continue changed_files[file_path] = data for arc_path, arc in self.arcs_by_path.items(): arc.save_changes() changed_files[arc_path] = arc.data for jpc_path, jpc in self.jpcs_by_path.items(): jpc.save_changes() changed_files[jpc_path] = jpc.data output_file_path = os.path.join(self.randomized_output_folder, "WW Random %s.iso" % self.seed) self.gcm.export_iso_with_changed_files(output_file_path, changed_files) def calculate_playthrough_progression_spheres(self): progression_spheres = [] logic = Logic(self) previously_accessible_locations = [] game_beatable = False while logic.unplaced_progress_items: progress_items_in_this_sphere = OrderedDict() accessible_locations = logic.get_accessible_remaining_locations() locations_in_this_sphere = [ loc for loc in accessible_locations if loc not in previously_accessible_locations ] if not locations_in_this_sphere: raise Exception("Failed to calculate progression spheres") if not self.options.get("keylunacy"): # If the player gained access to any small keys, we need to give them the keys without counting that as a new sphere. newly_accessible_dungeon_item_locations = [ loc for loc in locations_in_this_sphere if loc in self.logic.prerandomization_dungeon_item_locations ] newly_accessible_small_key_locations = [ loc for loc in newly_accessible_dungeon_item_locations if self.logic.prerandomization_dungeon_item_locations[loc]. endswith(" Small Key") ] if newly_accessible_small_key_locations: for small_key_location_name in newly_accessible_small_key_locations: item_name = self.logic.prerandomization_dungeon_item_locations[ small_key_location_name] assert item_name.endswith(" Small Key") logic.add_owned_item(item_name) previously_accessible_locations += newly_accessible_small_key_locations continue # Redo this loop iteration with the small key locations no longer being considered 'remaining'. for location_name in locations_in_this_sphere: item_name = self.logic.done_item_locations[location_name] if item_name in logic.all_progress_items: progress_items_in_this_sphere[location_name] = item_name if not game_beatable: game_beatable = logic.check_requirement_met( "Can Reach and Defeat Ganondorf") if game_beatable: progress_items_in_this_sphere[ "Ganon's Tower - Rooftop"] = "Defeat Ganondorf" progression_spheres.append(progress_items_in_this_sphere) for location_name, item_name in progress_items_in_this_sphere.items( ): if item_name == "Defeat Ganondorf": continue logic.add_owned_item(item_name) for group_name, item_names in logic.progress_item_groups.items(): entire_group_is_owned = all( item_name in logic.currently_owned_items for item_name in item_names) if entire_group_is_owned and group_name in logic.unplaced_progress_items: logic.unplaced_progress_items.remove(group_name) previously_accessible_locations = accessible_locations if not game_beatable: # If the game wasn't already beatable on a previous progression sphere but it is now we add one final one just for this. game_beatable = logic.check_requirement_met( "Can Reach and Defeat Ganondorf") if game_beatable: final_progression_sphere = OrderedDict([ ("Ganon's Tower - Rooftop", "Defeat Ganondorf"), ]) progression_spheres.append(final_progression_sphere) return progression_spheres def get_log_header(self): header = "" header += "Wind Waker Randomizer Version %s\n" % VERSION if self.permalink: header += "Permalink: %s\n" % self.permalink header += "Seed: %s\n" % self.seed header += "Options selected:\n " non_disabled_options = [ name for name in self.options if self.options[name] != False ] option_strings = [] for option_name in non_disabled_options: if isinstance(self.options[option_name], bool): option_strings.append(option_name) else: option_strings.append("%s: %s" % (option_name, self.options[option_name])) header += ", ".join(option_strings) header += "\n\n\n" return header def get_zones_and_max_location_name_len(self, locations): zones = OrderedDict() max_location_name_length = 0 for location_name in locations: zone_name, specific_location_name = self.logic.split_location_name_by_zone( location_name) if zone_name not in zones: zones[zone_name] = [] zones[zone_name].append((location_name, specific_location_name)) if len(specific_location_name) > max_location_name_length: max_location_name_length = len(specific_location_name) return (zones, max_location_name_length) def write_non_spoiler_log(self): log_str = self.get_log_header() progress_locations, nonprogress_locations = self.logic.get_progress_and_non_progress_locations( ) zones, max_location_name_length = self.get_zones_and_max_location_name_len( self.logic.done_item_locations) format_string = " %s\n" # Write progress item locations. log_str += "### Locations that may or may not have progress items in them on this run:\n" for zone_name, locations_in_zone in zones.items(): if not any(loc for (loc, _) in locations_in_zone if loc in progress_locations): # No progress locations for this zone. continue log_str += zone_name + ":\n" for (location_name, specific_location_name) in locations_in_zone: if location_name in progress_locations: item_name = self.logic.done_item_locations[location_name] log_str += format_string % specific_location_name log_str += "\n\n" # Write nonprogress item locations. log_str += "### Locations that cannot have progress items in them on this run:\n" for zone_name, locations_in_zone in zones.items(): if not any(loc for (loc, _) in locations_in_zone if loc in nonprogress_locations): # No nonprogress locations for this zone. continue log_str += zone_name + ":\n" for (location_name, specific_location_name) in locations_in_zone: if location_name in nonprogress_locations: item_name = self.logic.done_item_locations[location_name] log_str += format_string % specific_location_name nonspoiler_log_output_path = os.path.join( self.randomized_output_folder, "WW Random %s - Non-Spoiler Log.txt" % self.seed) with open(nonspoiler_log_output_path, "w") as f: f.write(log_str) def write_spoiler_log(self): spoiler_log = self.get_log_header() # Write progression spheres. spoiler_log += "Playthrough:\n" progression_spheres = self.calculate_playthrough_progression_spheres() all_progression_sphere_locations = [ loc for locs in progression_spheres for loc in locs ] zones, max_location_name_length = self.get_zones_and_max_location_name_len( all_progression_sphere_locations) format_string = " %-" + str(max_location_name_length + 1) + "s %s\n" for i, progression_sphere in enumerate(progression_spheres): spoiler_log += "%d:\n" % (i + 1) for zone_name, locations_in_zone in zones.items(): if not any(loc for (loc, _) in locations_in_zone if loc in progression_sphere): # No locations in this zone are used in this sphere. continue spoiler_log += " %s:\n" % zone_name for (location_name, specific_location_name) in locations_in_zone: if location_name in progression_sphere: if location_name == "Ganon's Tower - Rooftop": item_name = "Defeat Ganondorf" else: item_name = self.logic.done_item_locations[ location_name] spoiler_log += format_string % ( specific_location_name + ":", item_name) spoiler_log += "\n\n\n" # Write item locations. spoiler_log += "All item locations:\n" zones, max_location_name_length = self.get_zones_and_max_location_name_len( self.logic.done_item_locations) format_string = " %-" + str(max_location_name_length + 1) + "s %s\n" for zone_name, locations_in_zone in zones.items(): spoiler_log += zone_name + ":\n" for (location_name, specific_location_name) in locations_in_zone: item_name = self.logic.done_item_locations[location_name] spoiler_log += format_string % (specific_location_name + ":", item_name) spoiler_log += "\n\n\n" # Write starting island. spoiler_log += "Starting island: " spoiler_log += self.island_number_to_name[self.starting_island_index] spoiler_log += "\n" spoiler_log += "\n\n\n" # Write dungeon entrances. spoiler_log += "Dungeon entrances:\n" for entrance_name, dungeon_name in self.dungeon_entrances.items(): spoiler_log += " %-45s %s\n" % (entrance_name + ":", dungeon_name) spoiler_log += "\n\n\n" # Write treasure charts. spoiler_log += "Charts:\n" chart_name_to_island_number = {} for island_number in range(1, 49 + 1): chart_name = self.logic.macros["Chart for Island %d" % island_number][0] chart_name_to_island_number[chart_name] = island_number for chart_number in range(1, 49 + 1): if chart_number <= 8: chart_name = "Triforce Chart %d" % chart_number else: chart_name = "Treasure Chart %d" % (chart_number - 8) island_number = chart_name_to_island_number[chart_name] island_name = self.island_number_to_name[island_number] spoiler_log += " %-18s %s\n" % (chart_name + ":", island_name) spoiler_log_output_path = os.path.join( self.randomized_output_folder, "WW Random %s - Spoiler Log.txt" % self.seed) with open(spoiler_log_output_path, "w") as f: f.write(spoiler_log) def write_error_log(self, error_message): error_log_str = self.get_log_header() error_log_str += error_message error_log_output_path = os.path.join( self.randomized_output_folder, "WW Random %s - Error Log.txt" % self.seed) with open(error_log_output_path, "w") as f: f.write(error_log_str) def disassemble_all_code(self): from asm.disassemble import disassemble_all_code disassemble_all_code(self)
def __init__(self, options): self.options = options self.dry_run = bool(options.get('dry-run', False)) # TODO: maybe make paths configurable? if not self.dry_run: self.actual_extract_path = Path(__file__).parent / 'actual-extract' self.modified_extract_path = Path( __file__).parent / 'modified-extract' self.oarc_cache_path = Path(__file__).parent / 'oarc' # catch common errors with directory setup if not self.actual_extract_path.is_dir(): raise StartupException( "ERROR: directory actual-extract doesn't exist! Make sure you have the ISO extracted into that directory" ) if not self.modified_extract_path.is_dir(): raise StartupException( "ERROR: directory modified-extract doesn't exist! Make sure you have the contents of actual-extract copied over to modified-extract" ) if not (self.actual_extract_path / 'DATA').is_dir(): raise StartupException( "ERROR: directory actual-extract doesn't contain a DATA directory! Make sure you have the ISO properly extracted into actual-extract" ) if not (self.modified_extract_path / 'DATA').is_dir(): raise StartupException( "ERROR: directory 'DATA' in modified-extract doesn't exist! Make sure you have the contents of actual-extract copied over to modified-extract" ) if not (self.modified_extract_path / 'DATA' / 'files' / 'COPYDATE_CODE_2011-09-28_153155').exists(): raise StartupException( "ERROR: the randomizer only supports E1.00") self.options = options self.no_logs = False self.seed = options.get('seed', -1) if self.seed == -1: self.seed = random.randint(0, 1000000) self.rng = random.Random() self.rng.seed(self.seed) self.entrance_connections = OrderedDict([ ("Dungeon Entrance In Deep Woods", "Skyview"), ("Dungeon Entrance In Eldin Volcano", "Earth Temple"), ("Dungeon Entrance In Lanayru Desert", "Lanayru Mining Facility"), ("Dungeon Entrance In Lake Floria", "Ancient Cistern"), ("Dungeon Entrance In Sand Sea", "Sandship"), ("Dungeon Entrance In Volcano Summit", "Fire Sanctuary"), ("Dungeon Entrance On Skyloft", "Skykeep"), ]) self.starting_items = ( x.strip() for x in self.options.get('starting_items', '').split(',')) self.starting_items: List[str] = list( filter(lambda x: x != '', self.starting_items)) self.required_dungeons = self.rng.sample( constants.POTENTIALLY_REQUIRED_DUNGEONS, k=2) if not self.options.get('randomize-tablets', False): self.starting_items.append('Emerald Tablet') self.starting_items.append('Ruby Tablet') self.starting_items.append('Amber Tablet') if not self.options.get('swordless', False): self.starting_items.append('Progressive Sword') self.starting_items.append('Progressive Sword') # if not self.options.get('randomize-sailcloth',False): # self.starting_items.append('Sailcloth') self.banned_types = self.options.get('banned-types', '').split(',') self.banned_types = [ x.strip() for x in self.banned_types if x.strip() != '' ] unknown_types = [ x for x in self.banned_types if not x in constants.ALL_TYPES ] if len(unknown_types) > 0: print(f"ERROR: unknown banned type(s): {unknown_types}") self.race_mode_banned_locations = [] self.logic = Logic(self) self.non_required_dungeons = [ dungeon for dungeon in constants.POTENTIALLY_REQUIRED_DUNGEONS if not dungeon in self.required_dungeons ] if self.options.get('empty-unrequired-dungeons', False): for location_name in self.logic.item_locations: zone, _ = Logic.split_location_name_by_zone(location_name) if zone in self.non_required_dungeons: self.race_mode_banned_locations.append(location_name) # checks outside dungeons that require dungeons: if 'Lanayru Mining Facility' in self.non_required_dungeons: self.race_mode_banned_locations.append( 'Skyloft - Fledge Crystals') elif 'Skyview' in self.non_required_dungeons: # TODO: check again with entrance rando self.race_mode_banned_locations.append( 'Sky - Lumpy Pumpkin Roof Goddess Chest') self.race_mode_banned_locations.append( 'Sealed Grounds - Gorko Goddess Wall Reward')
def loadRom(self, rom, interactive=False, magic=None, startLocation=None): self.scavengerOrder = [] self.plandoScavengerOrder = [] # startLocation param is only use for seedless if rom == None: # TODO::add a --logic parameter for seedless Logic.factory('vanilla') self.romFileName = 'seedless' self.majorsSplit = 'Full' self.masterMajorsSplit = 'Full' self.areaRando = True self.bossRando = True self.escapeRando = False self.escapeTimer = "03:00" self.startLocation = startLocation RomPatches.setDefaultPatches(startLocation) self.startArea = getAccessPoint(startLocation).Start['solveArea'] # in seedless load all the vanilla transitions self.areaTransitions = vanillaTransitions[:] self.bossTransitions = vanillaBossesTransitions[:] self.escapeTransition = [vanillaEscapeTransitions[0]] # in seedless we allow mixing of area and boss transitions self.hasMixedTransitions = True self.curGraphTransitions = self.bossTransitions + self.areaTransitions + self.escapeTransition self.locations = Logic.locations for loc in self.locations: loc.itemName = 'Nothing' # set doors related to default patches DoorsManager.setDoorsColor() self.doorsRando = False self.hasNothing = False self.objectives.setVanilla() self.tourian = 'Vanilla' else: self.romFileName = rom self.romLoader = RomLoader.factory(rom, magic) Logic.factory(self.romLoader.readLogic()) self.locations = Logic.locations (self.majorsSplit, self.masterMajorsSplit) = self.romLoader.assignItems( self.locations) (self.startLocation, self.startArea, startPatches) = self.romLoader.getStartAP() if not GraphUtils.isStandardStart( self.startLocation) and self.majorsSplit != 'Full': # update major/chozo locs in non standard start self.romLoader.updateSplitLocs(self.majorsSplit, self.locations) (self.areaRando, self.bossRando, self.escapeRando, hasObjectives, self.tourian) = self.romLoader.loadPatches() RomPatches.ActivePatches += startPatches self.escapeTimer = self.romLoader.getEscapeTimer() self.doorsRando = self.romLoader.loadDoorsColor() self.hasNothing = self.checkLocsForNothing() if self.majorsSplit == 'Scavenger': self.scavengerOrder = self.romLoader.loadScavengerOrder( self.locations) if hasObjectives: self.romLoader.loadObjectives(self.objectives) else: if self.majorsSplit == "Scavenger": self.objectives.setScavengerHunt() self.objectives.tourianRequired = not self.romLoader.hasPatch( 'Escape_Trigger') else: self.objectives.setVanilla() self.majorUpgrades = self.romLoader.loadMajorUpgrades() self.splitLocsByArea = self.romLoader.getSplitLocsByArea( self.locations) self.objectives.setSolverMode(self) if interactive == False: print( "ROM {} majors: {} area: {} boss: {} escape: {} patches: {} activePatches: {}" .format(rom, self.majorsSplit, self.areaRando, self.bossRando, self.escapeRando, sorted(self.romLoader.getPatches()), sorted(RomPatches.ActivePatches))) else: print( "majors: {} area: {} boss: {} escape: {} activepatches: {}" .format(self.majorsSplit, self.areaRando, self.bossRando, self.escapeRando, sorted(RomPatches.ActivePatches))) (self.areaTransitions, self.bossTransitions, self.escapeTransition, self.hasMixedTransitions) = self.romLoader.getTransitions( self.tourian) if interactive == True and self.debug == False: # in interactive area mode we build the graph as we play along if self.areaRando == True and self.bossRando == True: self.curGraphTransitions = [] elif self.areaRando == True: self.curGraphTransitions = self.bossTransitions[:] elif self.bossRando == True: self.curGraphTransitions = self.areaTransitions[:] else: self.curGraphTransitions = self.bossTransitions + self.areaTransitions if self.escapeRando == False: self.curGraphTransitions += self.escapeTransition else: self.curGraphTransitions = self.bossTransitions + self.areaTransitions + self.escapeTransition self.smbm = SMBoolManager() self.buildGraph() # store at each step how many locations are available self.nbAvailLocs = [] if self.log.getEffectiveLevel() == logging.DEBUG: self.log.debug("Display items at locations:") for loc in self.locations: self.log.debug('{:>50}: {:>16}'.format(loc.Name, loc.itemName))
class Randomizer: def __init__(self, seed, clean_iso_path, randomized_output_folder, options, permalink=None, cmd_line_args=OrderedDict()): self.randomized_output_folder = randomized_output_folder self.options = options self.seed = seed self.permalink = permalink self.dry_run = ("-dry" in cmd_line_args) self.disassemble = ("-disassemble" in cmd_line_args) self.export_disc_to_folder = ("-exportfolder" in cmd_line_args) self.no_logs = ("-nologs" in cmd_line_args) self.bulk_test = ("-bulk" in cmd_line_args) if self.bulk_test: self.dry_run = True self.no_logs = True self.print_used_flags = ("-printflags" in cmd_line_args) self.test_room_args = None if "-test" in cmd_line_args: args = cmd_line_args["-test"] if args is not None: stage, room, spawn = args.split(",") self.test_room_args = { "stage": stage, "room": int(room), "spawn": int(spawn) } seed_string = self.seed if not self.options.get("generate_spoiler_log"): seed_string += SEED_KEY self.integer_seed = self.convert_string_to_integer_md5(seed_string) self.rng = self.get_new_rng() self.arcs_by_path = {} self.jpcs_by_path = {} self.raw_files_by_path = {} self.read_text_file_lists() if not self.dry_run: if not os.path.isfile(clean_iso_path): raise InvalidCleanISOError("Clean WW ISO does not exist: %s" % clean_iso_path) self.verify_supported_version(clean_iso_path) self.gcm = GCM(clean_iso_path) self.gcm.read_entire_disc() try: self.chart_list = self.get_arc( "files/res/Msg/fmapres.arc").get_file("cmapdat.bin") except InvalidOffsetError: # An invalid offset error when reading fmapres.arc seems to happen when the user has a corrupted clean ISO. # The reason for this is unknown, but when this happens check the ISO's MD5 and if it's wrong say so in an error message. self.verify_correct_clean_iso_md5(clean_iso_path) # But if the ISO's MD5 is correct just raise the normal offset error. raise self.bmg = self.get_arc("files/res/Msg/bmgres.arc").get_file( "zel_00.bmg") if self.disassemble: self.disassemble_all_code() if self.print_used_flags: stage_searcher.print_all_used_item_pickup_flags(self) stage_searcher.print_all_used_chest_open_flags(self) stage_searcher.print_all_event_flags_used_by_stb_cutscenes( self) # Starting items. This list is read by the Logic when initializing your currently owned items list. self.starting_items = [ "Wind Waker", "Wind's Requiem", "Ballad of Gales", "Song of Passing", "Hero's Shield", "Boat's Sail", ] self.starting_items += self.options.get("starting_gear", []) if self.options.get("sword_mode") == "Start with Sword": self.starting_items.append("Progressive Sword") # Add starting Triforce Shards. num_starting_triforce_shards = int( self.options.get("num_starting_triforce_shards", 0)) for i in range(num_starting_triforce_shards): self.starting_items.append("Triforce Shard %d" % (i + 1)) # Default entrances connections to be used if the entrance randomizer is not on. self.entrance_connections = OrderedDict([ ("Dungeon Entrance On Dragon Roost Island", "Dragon Roost Cavern"), ("Dungeon Entrance In Forest Haven Sector", "Forbidden Woods"), ("Dungeon Entrance In Tower of the Gods Sector", "Tower of the Gods"), ("Dungeon Entrance On Headstone Island", "Earth Temple"), ("Dungeon Entrance On Gale Isle", "Wind Temple"), ("Secret Cave Entrance on Outset Island", "Savage Labyrinth"), ("Secret Cave Entrance on Dragon Roost Island", "Dragon Roost Island Secret Cave"), ("Secret Cave Entrance on Fire Mountain", "Fire Mountain Secret Cave"), ("Secret Cave Entrance on Ice Ring Isle", "Ice Ring Isle Secret Cave"), ("Secret Cave Entrance on Private Oasis", "Cabana Labyrinth"), ("Secret Cave Entrance on Needle Rock Isle", "Needle Rock Isle Secret Cave"), ("Secret Cave Entrance on Angular Isles", "Angular Isles Secret Cave"), ("Secret Cave Entrance on Boating Course", "Boating Course Secret Cave"), ("Secret Cave Entrance on Stone Watcher Island", "Stone Watcher Island Secret Cave"), ("Secret Cave Entrance on Overlook Island", "Overlook Island Secret Cave"), ("Secret Cave Entrance on Bird's Peak Rock", "Bird's Peak Rock Secret Cave"), ("Secret Cave Entrance on Pawprint Isle", "Pawprint Isle Chuchu Cave"), ("Secret Cave Entrance on Pawprint Isle Side Isle", "Pawprint Isle Wizzrobe Cave"), ("Secret Cave Entrance on Diamond Steppe Island", "Diamond Steppe Island Warp Maze Cave"), ("Secret Cave Entrance on Bomb Island", "Bomb Island Secret Cave"), ("Secret Cave Entrance on Rock Spire Isle", "Rock Spire Isle Secret Cave"), ("Secret Cave Entrance on Shark Island", "Shark Island Secret Cave"), ("Secret Cave Entrance on Cliff Plateau Isles", "Cliff Plateau Isles Secret Cave"), ("Secret Cave Entrance on Horseshoe Island", "Horseshoe Island Secret Cave"), ("Secret Cave Entrance on Star Island", "Star Island Secret Cave"), ]) self.dungeon_and_cave_island_locations = OrderedDict([ ("Dragon Roost Cavern", "Dragon Roost Island"), ("Forbidden Woods", "Forest Haven"), ("Tower of the Gods", "Tower of the Gods"), ("Earth Temple", "Headstone Island"), ("Wind Temple", "Gale Isle"), ("Secret Cave Entrance on Outset Island", "Outset Island"), ("Secret Cave Entrance on Dragon Roost Island", "Dragon Roost Island"), ("Secret Cave Entrance on Fire Mountain", "Fire Mountain"), ("Secret Cave Entrance on Ice Ring Isle", "Ice Ring Isle"), ("Secret Cave Entrance on Private Oasis", "Private Oasis"), ("Secret Cave Entrance on Needle Rock Isle", "Needle Rock Isle"), ("Secret Cave Entrance on Angular Isles", "Angular Isles"), ("Secret Cave Entrance on Boating Course", "Boating Course"), ("Secret Cave Entrance on Stone Watcher Island", "Stone Watcher Island"), ("Secret Cave Entrance on Overlook Island", "Overlook Island"), ("Secret Cave Entrance on Bird's Peak Rock", "Bird's Peak Rock"), ("Secret Cave Entrance on Pawprint Isle", "Pawprint Isle"), ("Secret Cave Entrance on Pawprint Isle Side Isle", "Pawprint Isle"), ("Secret Cave Entrance on Diamond Steppe Island", "Diamond Steppe Island"), ("Secret Cave Entrance on Bomb Island", "Bomb Island"), ("Secret Cave Entrance on Rock Spire Isle", "Rock Spire Isle"), ("Secret Cave Entrance on Shark Island", "Shark Island"), ("Secret Cave Entrance on Cliff Plateau Isles", "Cliff Plateau Isles"), ("Secret Cave Entrance on Horseshoe Island", "Horseshoe Island"), ("Secret Cave Entrance on Star Island", "Star Island"), ]) # Default starting island (Outset) if the starting island randomizer is not on. self.starting_island_index = 44 # Default charts for each island. self.island_number_to_chart_name = OrderedDict([ (1, "Treasure Chart 25"), (2, "Treasure Chart 7"), (3, "Treasure Chart 24"), (4, "Triforce Chart 2"), (5, "Treasure Chart 11"), (6, "Triforce Chart 7"), (7, "Treasure Chart 13"), (8, "Treasure Chart 41"), (9, "Treasure Chart 29"), (10, "Treasure Chart 22"), (11, "Treasure Chart 18"), (12, "Treasure Chart 30"), (13, "Treasure Chart 39"), (14, "Treasure Chart 19"), (15, "Treasure Chart 8"), (16, "Treasure Chart 2"), (17, "Treasure Chart 10"), (18, "Treasure Chart 26"), (19, "Treasure Chart 3"), (20, "Treasure Chart 37"), (21, "Treasure Chart 27"), (22, "Treasure Chart 38"), (23, "Triforce Chart 1"), (24, "Treasure Chart 21"), (25, "Treasure Chart 6"), (26, "Treasure Chart 14"), (27, "Treasure Chart 34"), (28, "Treasure Chart 5"), (29, "Treasure Chart 28"), (30, "Treasure Chart 35"), (31, "Triforce Chart 3"), (32, "Triforce Chart 6"), (33, "Treasure Chart 1"), (34, "Treasure Chart 20"), (35, "Treasure Chart 36"), (36, "Treasure Chart 23"), (37, "Treasure Chart 12"), (38, "Treasure Chart 16"), (39, "Treasure Chart 4"), (40, "Treasure Chart 17"), (41, "Treasure Chart 31"), (42, "Triforce Chart 5"), (43, "Treasure Chart 9"), (44, "Triforce Chart 4"), (45, "Treasure Chart 40"), (46, "Triforce Chart 8"), (47, "Treasure Chart 15"), (48, "Treasure Chart 32"), (49, "Treasure Chart 33"), ]) # This list will hold the randomly selected dungeon boss locations that are required in race mode. # If race mode is not on, this list will remain empty. self.race_mode_required_locations = [] # This list will hold the dungeon names of the race mode required locations. # If race mode is not on, this list will remain empty. self.race_mode_required_dungeons = [] # This list will hold all item location names that should not have any items in them in race mode. # If race mode is not on, this list will remain empty. self.race_mode_banned_locations = [] self.custom_model_name = "Link" self.logic = Logic(self) num_progress_locations = self.logic.get_num_progression_locations() num_progress_items = self.logic.get_num_progression_items() if num_progress_locations < num_progress_items: error_message = "Not enough progress locations to place all progress items.\n\n" error_message += "Total progress items: %d\n" % num_progress_items error_message += "Progress locations with current options: %d\n\n" % num_progress_locations error_message += "You need to check more of the progress location options in order to give the randomizer enough space to place all the items." raise TooFewProgressionLocationsError(error_message) # We need to determine if the user's selected options result in a dungeons-only-start. # Dungeons-only-start meaning that the only locations accessible at the start of the run are dungeon locations. # e.g. If the user selects Dungeons, Expensive Purchases, and Sunken Treasures, the dungeon locations are the only ones the player can check first. # We need to distinguish this situation because it can cause issues for the randomizer's item placement logic (specifically when placing keys in DRC). self.logic.temporarily_make_dungeon_entrance_macros_impossible() accessible_undone_locations = self.logic.get_accessible_remaining_locations( for_progression=True) if len(accessible_undone_locations) == 0: self.dungeons_only_start = True else: self.dungeons_only_start = False self.logic.update_entrance_connection_macros( ) # Reset the dungeon entrance macros. # Also determine if these options result in a dungeons-and-caves-only-start. # Dungeons-and-caves-only-start means the only locations accessible at the start of the run are dungeon or secret cave locations. # This situation can also cause issues for the item placement logic (specifically when placing the first item of the run). self.logic.temporarily_make_entrance_macros_impossible() accessible_undone_locations = self.logic.get_accessible_remaining_locations( for_progression=True) if len(accessible_undone_locations) == 0: self.dungeons_and_caves_only_start = True else: self.dungeons_and_caves_only_start = False self.logic.update_entrance_connection_macros( ) # Reset the entrance macros. def randomize(self): options_completed = 0 yield ("Modifying game code...", options_completed) if not self.dry_run: self.apply_necessary_tweaks() if self.options.get("swift_sail"): tweaks.make_sail_behave_like_swift_sail(self) if self.options.get("instant_text_boxes"): tweaks.make_all_text_instant(self) if self.options.get("reveal_full_sea_chart"): tweaks.apply_patch(self, "reveal_sea_chart") if self.options.get("add_shortcut_warps_between_dungeons"): tweaks.add_inter_dungeon_warp_pots(self) if self.options.get("invert_camera_x_axis"): tweaks.apply_patch(self, "invert_camera_x_axis") tweaks.update_skip_rematch_bosses_game_variable(self) tweaks.update_sword_mode_game_variable(self) if self.options.get("sword_mode") == "Swordless": tweaks.apply_patch(self, "swordless") tweaks.update_text_for_swordless(self) if self.options.get("randomize_entrances") not in [ "Disabled", None, "Dungeons" ]: tweaks.disable_ice_ring_isle_and_fire_mountain_effects_indoors( self) tweaks.update_starting_gear(self) if self.options.get("disable_tingle_chests_with_tingle_bombs"): tweaks.apply_patch(self, "disable_tingle_bombs_on_tingle_chests") if self.test_room_args is not None: tweaks.test_room(self) options_completed += 1 yield ("Randomizing...", options_completed) if self.options.get("randomize_charts"): charts.randomize_charts(self) if self.options.get("randomize_starting_island"): starting_island.randomize_starting_island(self) if self.options.get("randomize_entrances") not in ["Disabled", None]: entrances.randomize_entrances(self) if self.options.get("randomize_bgm"): bgm.randomize_bgm(self) items.randomize_items(self) options_completed += 2 yield ("Saving items...", options_completed) if not self.dry_run: items.write_changed_items(self) if not self.dry_run: self.apply_necessary_post_randomization_tweaks() options_completed += 7 yield ("Saving randomized ISO...", options_completed) if not self.dry_run: self.save_randomized_iso() options_completed += 9 yield ("Writing logs...", options_completed) if self.options.get("generate_spoiler_log"): self.write_spoiler_log() self.write_non_spoiler_log() yield ("Done", -1) def apply_necessary_tweaks(self): tweaks.apply_patch(self, "custom_funcs") tweaks.apply_patch(self, "necessary_fixes") tweaks.skip_wakeup_intro_and_start_at_dock(self) tweaks.start_ship_at_outset(self) tweaks.fix_deku_leaf_model(self) tweaks.allow_all_items_to_be_field_items(self) tweaks.remove_shop_item_forced_uniqueness_bit(self) tweaks.remove_forsaken_fortress_2_cutscenes(self) tweaks.make_items_progressive(self) tweaks.add_ganons_tower_warp_to_ff2(self) tweaks.add_chest_in_place_medli_grappling_hook_gift(self) tweaks.add_chest_in_place_queen_fairy_cutscene(self) #tweaks.add_cube_to_earth_temple_first_room(self) tweaks.add_more_magic_jars(self) tweaks.remove_title_and_ending_videos(self) tweaks.modify_title_screen_logo(self) tweaks.update_game_name_icon_and_banners(self) tweaks.allow_dungeon_items_to_appear_anywhere(self) #tweaks.remove_ballad_of_gales_warp_in_cutscene(self) tweaks.fix_shop_item_y_offsets(self) tweaks.shorten_zephos_event(self) tweaks.update_korl_dialogue(self) tweaks.set_num_starting_triforce_shards(self) tweaks.add_pirate_ship_to_windfall(self) tweaks.remove_makar_kidnapping_event(self) tweaks.increase_player_movement_speeds(self) tweaks.add_chart_number_to_item_get_messages(self) tweaks.increase_grapple_animation_speed(self) tweaks.increase_block_moving_animation(self) tweaks.increase_misc_animations(self) tweaks.shorten_auction_intro_event(self) tweaks.disable_invisible_walls(self) tweaks.add_hint_signs(self) tweaks.prevent_door_boulder_softlocks(self) tweaks.update_tingle_statue_item_get_funcs(self) tweaks.apply_patch(self, "tingle_chests_without_tuner") tweaks.make_tingle_statue_reward_rupee_rainbow_colored(self) tweaks.show_seed_hash_on_name_entry_screen(self) tweaks.fix_ghost_ship_chest_crash(self) tweaks.implement_key_bag(self) tweaks.add_chest_in_place_of_jabun_cutscene(self) tweaks.add_chest_in_place_of_master_sword(self) tweaks.update_beedle_spoil_selling_text(self) tweaks.fix_totg_warp_out_spawn_pos(self) tweaks.remove_phantom_ganon_requirement_from_eye_reefs(self) customizer.replace_link_model(self) tweaks.change_starting_clothes(self) customizer.change_player_clothes_color(self) def apply_necessary_post_randomization_tweaks(self): tweaks.update_shop_item_descriptions(self) tweaks.update_auction_item_names(self) tweaks.update_battlesquid_item_names(self) tweaks.update_item_names_in_letter_advertising_rock_spire_shop(self) tweaks.update_savage_labyrinth_hint_tablet(self) tweaks.update_randomly_chosen_hints(self) tweaks.show_quest_markers_on_sea_chart_for_dungeons( self, dungeon_names=self.race_mode_required_dungeons) tweaks.prevent_fire_mountain_lava_softlock(self) def verify_supported_version(self, clean_iso_path): with open(clean_iso_path, "rb") as f: game_id = try_read_str(f, 0, 6) if game_id != "GZLE01": if game_id and game_id.startswith("GZL"): raise InvalidCleanISOError( "Invalid version of Wind Waker. Only the USA version is supported by this randomizer." ) else: raise InvalidCleanISOError( "Invalid game given as the clean ISO. You must specify a Wind Waker ISO (USA version)." ) def verify_correct_clean_iso_md5(self, clean_iso_path): md5 = hashlib.md5() with open(clean_iso_path, "rb") as f: while True: chunk = f.read(1024 * 1024) if not chunk: break md5.update(chunk) integer_md5 = int(md5.hexdigest(), 16) if integer_md5 != CLEAN_WIND_WAKER_ISO_MD5: raise InvalidCleanISOError( "Invalid clean Wind Waker ISO. Your ISO may be corrupted.\n\nCorrect ISO MD5 hash: %x\nYour ISO's MD5 hash: %x" % (CLEAN_WIND_WAKER_ISO_MD5, integer_md5)) def read_text_file_lists(self): # Get item names. self.item_names = {} self.item_name_to_id = {} with open(os.path.join(DATA_PATH, "item_names.txt"), "r") as f: matches = re.findall(r"^([0-9a-f]{2}) - (.+)$", f.read(), re.IGNORECASE | re.MULTILINE) for item_id, item_name in matches: if item_name: item_id = int(item_id, 16) self.item_names[item_id] = item_name if item_name in self.item_name_to_id: raise Exception("Duplicate item name: " + item_name) self.item_name_to_id[item_name] = item_id # Get stage and island names for debug purposes. self.stage_names = {} with open(os.path.join(DATA_PATH, "stage_names.txt"), "r") as f: while True: stage_folder = f.readline() if not stage_folder: break stage_name = f.readline() self.stage_names[stage_folder.strip()] = stage_name.strip() self.island_names = {} self.island_number_to_name = {} self.island_name_to_number = {} with open(os.path.join(DATA_PATH, "island_names.txt"), "r") as f: while True: room_arc_name = f.readline() if not room_arc_name: break island_name = f.readline().strip() self.island_names[room_arc_name.strip()] = island_name island_number = int( re.search(r"Room(\d+)", room_arc_name).group(1)) self.island_number_to_name[island_number] = island_name self.island_name_to_number[island_name] = island_number self.item_ids_without_a_field_model = [] with open(os.path.join(DATA_PATH, "items_without_field_models.txt"), "r") as f: matches = re.findall(r"^([0-9a-f]{2}) ", f.read(), re.IGNORECASE | re.MULTILINE) for item_id in matches: if item_name: item_id = int(item_id, 16) self.item_ids_without_a_field_model.append(item_id) self.arc_name_pointers = {} with open( os.path.join(DATA_PATH, "item_resource_arc_name_pointers.txt"), "r") as f: matches = re.findall(r"^([0-9a-f]{2}) ([0-9a-f]{8}) ", f.read(), re.IGNORECASE | re.MULTILINE) for item_id, arc_name_pointer in matches: item_id = int(item_id, 16) arc_name_pointer = int(arc_name_pointer, 16) self.arc_name_pointers[item_id] = arc_name_pointer self.icon_name_pointer = {} with open( os.path.join(DATA_PATH, "item_resource_icon_name_pointers.txt"), "r") as f: matches = re.findall(r"^([0-9a-f]{2}) ([0-9a-f]{8}) ", f.read(), re.IGNORECASE | re.MULTILINE) for item_id, icon_name_pointer in matches: item_id = int(item_id, 16) icon_name_pointer = int(icon_name_pointer, 16) self.icon_name_pointer[item_id] = icon_name_pointer self.custom_symbols = {} with open(os.path.join(ASM_PATH, "custom_symbols.txt"), "r") as f: matches = re.findall(r"^([0-9a-f]{8}) (\S+)", f.read(), re.IGNORECASE | re.MULTILINE) for symbol_address, symbol_name in matches: self.custom_symbols[symbol_name] = int(symbol_address, 16) with open(os.path.join(DATA_PATH, "progress_item_hints.txt"), "r") as f: self.progress_item_hints = yaml.safe_load(f) with open(os.path.join(DATA_PATH, "island_name_hints.txt"), "r") as f: self.island_name_hints = yaml.safe_load(f) def get_arc(self, arc_path): arc_path = arc_path.replace("\\", "/") if arc_path in self.arcs_by_path: return self.arcs_by_path[arc_path] else: data = self.gcm.read_file_data(arc_path) arc = RARC(data) self.arcs_by_path[arc_path] = arc return arc def get_jpc(self, jpc_path): jpc_path = jpc_path.replace("\\", "/") if jpc_path in self.jpcs_by_path: return self.jpcs_by_path[jpc_path] else: data = self.gcm.read_file_data(jpc_path) jpc = JPC(data) self.jpcs_by_path[jpc_path] = jpc return jpc def get_raw_file(self, file_path): file_path = file_path.replace("\\", "/") if file_path in self.raw_files_by_path: return self.raw_files_by_path[file_path] else: if file_path.startswith("files/rels/"): rel_name = os.path.basename(file_path) rels_arc = self.get_arc("files/RELS.arc") rel_file_entry = rels_arc.get_file_entry(rel_name) else: rel_file_entry = None if rel_file_entry: rel_file_entry.decompress_data_if_necessary() data = rel_file_entry.data else: data = self.gcm.read_file_data(file_path) if try_read_str(data, 0, 4) == "Yaz0": data = Yaz0.decompress(data) self.raw_files_by_path[file_path] = data return data def replace_arc(self, arc_path, new_data): if arc_path not in self.gcm.files_by_path: raise Exception("Cannot replace RARC that doesn't exist: " + arc_path) arc = RARC(new_data) self.arcs_by_path[arc_path] = arc def replace_raw_file(self, file_path, new_data): if file_path not in self.gcm.files_by_path: raise Exception("Cannot replace file that doesn't exist: " + file_path) self.raw_files_by_path[file_path] = new_data def add_new_raw_file(self, file_path, new_data): if file_path.lower() in self.gcm.files_by_path_lowercase: raise Exception( "Cannot add a new file that has the same path and name as an existing one: " + file_path) self.raw_files_by_path[file_path] = new_data def save_randomized_iso(self): self.bmg.save_changes() changed_files = {} for file_path, data in self.raw_files_by_path.items(): if file_path.startswith("files/rels/"): rel_name = os.path.basename(file_path) rels_arc = self.get_arc("files/RELS.arc") rel_file_entry = rels_arc.get_file_entry(rel_name) if rel_file_entry: # Modify the RELS.arc entry for this rel. rel_file_entry.data = data continue changed_files[file_path] = data for arc_path, arc in self.arcs_by_path.items(): for file_name, instantiated_file in arc.instantiated_object_files.items( ): if file_name == "event_list.dat": instantiated_file.save_changes() arc.save_changes() changed_files[arc_path] = arc.data for jpc_path, jpc in self.jpcs_by_path.items(): jpc.save_changes() changed_files[jpc_path] = jpc.data if self.export_disc_to_folder: output_folder_path = os.path.join(self.randomized_output_folder, "WW Random %s" % self.seed) self.gcm.export_disc_to_folder_with_changed_files( output_folder_path, changed_files) else: output_file_path = os.path.join(self.randomized_output_folder, "WW Random %s.iso" % self.seed) self.gcm.export_disc_to_iso_with_changed_files( output_file_path, changed_files) def convert_string_to_integer_md5(self, string): return int(hashlib.md5(string.encode('utf-8')).hexdigest(), 16) def get_new_rng(self): rng = Random() rng.seed(self.integer_seed) if not self.options.get("generate_spoiler_log"): for i in range(1, 100): rng.getrandbits(i) return rng def calculate_playthrough_progression_spheres(self): progression_spheres = [] logic = Logic(self) previously_accessible_locations = [] game_beatable = False while logic.unplaced_progress_items: progress_items_in_this_sphere = OrderedDict() accessible_locations = logic.get_accessible_remaining_locations() locations_in_this_sphere = [ loc for loc in accessible_locations if loc not in previously_accessible_locations ] if not locations_in_this_sphere: raise Exception("Failed to calculate progression spheres") if not self.options.get("keylunacy"): # If the player gained access to any small keys, we need to give them the keys without counting that as a new sphere. newly_accessible_predetermined_item_locations = [ loc for loc in locations_in_this_sphere if loc in self.logic.prerandomization_item_locations ] newly_accessible_small_key_locations = [ loc for loc in newly_accessible_predetermined_item_locations if self.logic.prerandomization_item_locations[loc]. endswith(" Small Key") ] if newly_accessible_small_key_locations: for small_key_location_name in newly_accessible_small_key_locations: item_name = self.logic.prerandomization_item_locations[ small_key_location_name] assert item_name.endswith(" Small Key") logic.add_owned_item(item_name) previously_accessible_locations += newly_accessible_small_key_locations continue # Redo this loop iteration with the small key locations no longer being considered 'remaining'. for location_name in locations_in_this_sphere: item_name = self.logic.done_item_locations[location_name] if item_name in logic.all_progress_items: progress_items_in_this_sphere[location_name] = item_name if not game_beatable: game_beatable = logic.check_requirement_met( "Can Reach and Defeat Ganondorf") if game_beatable: progress_items_in_this_sphere[ "Ganon's Tower - Rooftop"] = "Defeat Ganondorf" progression_spheres.append(progress_items_in_this_sphere) for location_name, item_name in progress_items_in_this_sphere.items( ): if item_name == "Defeat Ganondorf": continue logic.add_owned_item(item_name) for group_name, item_names in logic.progress_item_groups.items(): entire_group_is_owned = all( item_name in logic.currently_owned_items for item_name in item_names) if entire_group_is_owned and group_name in logic.unplaced_progress_items: logic.unplaced_progress_items.remove(group_name) previously_accessible_locations = accessible_locations if not game_beatable: # If the game wasn't already beatable on a previous progression sphere but it is now we add one final one just for this. game_beatable = logic.check_requirement_met( "Can Reach and Defeat Ganondorf") if game_beatable: final_progression_sphere = OrderedDict([ ("Ganon's Tower - Rooftop", "Defeat Ganondorf"), ]) progression_spheres.append(final_progression_sphere) return progression_spheres def get_log_header(self): header = "" header += "Wind Waker Randomizer Version %s\n" % VERSION if self.permalink: header += "Permalink: %s\n" % self.permalink header += "Seed: %s\n" % self.seed header += "Options selected:\n " non_disabled_options = [ name for name in self.options if self.options[name] not in [False, [], {}, OrderedDict()] and name != "randomized_gear" # Just takes up space ] option_strings = [] for option_name in non_disabled_options: if isinstance(self.options[option_name], bool): option_strings.append(option_name) else: if option_name == "custom_colors": # Only show non-default colors. default_colors = customizer.get_default_colors(self) value = OrderedDict() for custom_color_name, custom_color_value in self.options[ option_name].items(): if custom_color_value != default_colors[ custom_color_name]: value[custom_color_name] = custom_color_value if value == OrderedDict(): # No colors changed from default, don't show it at all. continue else: value = self.options[option_name] option_strings.append("%s: %s" % (option_name, value)) header += ", ".join(option_strings) header += "\n\n\n" return header def get_zones_and_max_location_name_len(self, locations): zones = OrderedDict() max_location_name_length = 0 for location_name in locations: zone_name, specific_location_name = self.logic.split_location_name_by_zone( location_name) if zone_name not in zones: zones[zone_name] = [] zones[zone_name].append((location_name, specific_location_name)) if len(specific_location_name) > max_location_name_length: max_location_name_length = len(specific_location_name) return (zones, max_location_name_length) def write_non_spoiler_log(self): if self.no_logs: return log_str = self.get_log_header() progress_locations, nonprogress_locations = self.logic.get_progress_and_non_progress_locations( ) zones, max_location_name_length = self.get_zones_and_max_location_name_len( self.logic.done_item_locations) format_string = " %s\n" # Write progress item locations. log_str += "### Locations that may or may not have progress items in them on this run:\n" for zone_name, locations_in_zone in zones.items(): if not any(loc for (loc, _) in locations_in_zone if loc in progress_locations): # No progress locations for this zone. continue log_str += zone_name + ":\n" for (location_name, specific_location_name) in locations_in_zone: if location_name in progress_locations: item_name = self.logic.done_item_locations[location_name] log_str += format_string % specific_location_name log_str += "\n\n" # Write nonprogress item locations. log_str += "### Locations that cannot have progress items in them on this run:\n" for zone_name, locations_in_zone in zones.items(): if not any(loc for (loc, _) in locations_in_zone if loc in nonprogress_locations): # No nonprogress locations for this zone. continue log_str += zone_name + ":\n" for (location_name, specific_location_name) in locations_in_zone: if location_name in nonprogress_locations: item_name = self.logic.done_item_locations[location_name] log_str += format_string % specific_location_name nonspoiler_log_output_path = os.path.join( self.randomized_output_folder, "WW Random %s - Non-Spoiler Log.txt" % self.seed) with open(nonspoiler_log_output_path, "w") as f: f.write(log_str) def write_spoiler_log(self): if self.no_logs: # We still calculate progression spheres even if we're not going to write them anywhere to catch more errors in testing. self.calculate_playthrough_progression_spheres() return spoiler_log = self.get_log_header() # Write progression spheres. spoiler_log += "Playthrough:\n" progression_spheres = self.calculate_playthrough_progression_spheres() all_progression_sphere_locations = [ loc for locs in progression_spheres for loc in locs ] zones, max_location_name_length = self.get_zones_and_max_location_name_len( all_progression_sphere_locations) format_string = " %-" + str(max_location_name_length + 1) + "s %s\n" for i, progression_sphere in enumerate(progression_spheres): spoiler_log += "%d:\n" % (i + 1) for zone_name, locations_in_zone in zones.items(): if not any(loc for (loc, _) in locations_in_zone if loc in progression_sphere): # No locations in this zone are used in this sphere. continue spoiler_log += " %s:\n" % zone_name for (location_name, specific_location_name) in locations_in_zone: if location_name in progression_sphere: if location_name == "Ganon's Tower - Rooftop": item_name = "Defeat Ganondorf" else: item_name = self.logic.done_item_locations[ location_name] spoiler_log += format_string % ( specific_location_name + ":", item_name) spoiler_log += "\n\n\n" # Write item locations. spoiler_log += "All item locations:\n" zones, max_location_name_length = self.get_zones_and_max_location_name_len( self.logic.done_item_locations) format_string = " %-" + str(max_location_name_length + 1) + "s %s\n" for zone_name, locations_in_zone in zones.items(): spoiler_log += zone_name + ":\n" for (location_name, specific_location_name) in locations_in_zone: item_name = self.logic.done_item_locations[location_name] spoiler_log += format_string % (specific_location_name + ":", item_name) spoiler_log += "\n\n\n" # Write starting island. spoiler_log += "Starting island: " spoiler_log += self.island_number_to_name[self.starting_island_index] spoiler_log += "\n" spoiler_log += "\n\n\n" # Write dungeon/secret cave entrances. spoiler_log += "Entrances:\n" for entrance_name, dungeon_or_cave_name in self.entrance_connections.items( ): spoiler_log += " %-48s %s\n" % (entrance_name + ":", dungeon_or_cave_name) spoiler_log += "\n\n\n" # Write treasure charts. spoiler_log += "Charts:\n" chart_name_to_island_number = {} for island_number in range(1, 49 + 1): chart_name = self.logic.macros["Chart for Island %d" % island_number][0] chart_name_to_island_number[chart_name] = island_number for chart_number in range(1, 49 + 1): if chart_number <= 8: chart_name = "Triforce Chart %d" % chart_number else: chart_name = "Treasure Chart %d" % (chart_number - 8) island_number = chart_name_to_island_number[chart_name] island_name = self.island_number_to_name[island_number] spoiler_log += " %-18s %s\n" % (chart_name + ":", island_name) spoiler_log_output_path = os.path.join( self.randomized_output_folder, "WW Random %s - Spoiler Log.txt" % self.seed) with open(spoiler_log_output_path, "w") as f: f.write(spoiler_log) def write_error_log(self, error_message): if self.no_logs: return error_log_str = self.get_log_header() error_log_str += error_message error_log_output_path = os.path.join( self.randomized_output_folder, "WW Random %s - Error Log.txt" % self.seed) with open(error_log_output_path, "w") as f: f.write(error_log_str) def disassemble_all_code(self): from asm.disassemble import disassemble_all_code disassemble_all_code(self)
def __init__(self, options: Options, progress_callback=dummy_progress_callback): self.options = options self.progress_callback = progress_callback self.dry_run = bool(self.options['dry-run']) # TODO: maybe make paths configurable? # exe root path is where the executable is self.exe_root_path = Path('.').resolve() # this is where all assets/read only files are self.rando_root_path = RANDO_ROOT_PATH if not self.dry_run: self.actual_extract_path = self.exe_root_path / 'actual-extract' self.modified_extract_path = self.exe_root_path / 'modified-extract' self.oarc_cache_path = self.exe_root_path / 'oarc' self.no_logs = self.options['no-spoiler-log'] self.seed = self.options['seed'] if self.seed == -1: self.seed = random.randint(0, 1000000) self.randomizer_hash = self._get_rando_hash() self.rng = random.Random() self.rng.seed(self.seed) if self.no_logs: self.rng.randint(0, 100) dungeons = [ "Skyview", "Earth Temple", "Lanayru Mining Facility", "Ancient Cistern", "Sandship", "Fire Sanctuary" ] if self.options['randomize-entrances'] == 'None': dungeons.append('Skykeep') dungeons.reverse() else: if self.options['randomize-entrances'] == 'Dungeons': self.rng.shuffle(dungeons) dungeons.append('Skykeep') dungeons.reverse() else: dungeons.append('Skykeep') self.rng.shuffle(dungeons) self.entrance_connections = OrderedDict([ ("Dungeon Entrance In Deep Woods", dungeons.pop()), ("Dungeon Entrance In Eldin Volcano", dungeons.pop()), ("Dungeon Entrance In Lanayru Desert", dungeons.pop()), ("Dungeon Entrance In Lake Floria", dungeons.pop()), ("Dungeon Entrance In Sand Sea", dungeons.pop()), ("Dungeon Entrance In Volcano Summit", dungeons.pop()), ("Dungeon Entrance On Skyloft", dungeons.pop()), ]) assert len(dungeons) == 0, 'Not all dungeons linked to an entrance' # self.starting_items = (x.strip() for x in self.options['starting_items'] # self.starting_items: List[str] = list(filter(lambda x: x != '', self.starting_items)) self.starting_items = [] self.required_dungeons = self.rng.sample( constants.POTENTIALLY_REQUIRED_DUNGEONS, k=self.options['required-dungeon-count']) # make the order always consistent self.required_dungeons = [ dungeon for dungeon in constants.POTENTIALLY_REQUIRED_DUNGEONS if dungeon in self.required_dungeons ] tablets = ['Emerald Tablet', 'Ruby Tablet', 'Amber Tablet'] self.starting_items.extend( self.rng.sample(tablets, k=self.options['starting-tablet-count'])) if not self.options['swordless']: self.starting_items.append('Progressive Sword') self.starting_items.append('Progressive Sword') # if not self.options.get('randomize-sailcloth',False): # self.starting_items.append('Sailcloth') if self.options['start-with-pouch']: self.starting_items.append('Progressive Pouch') self.banned_types = self.options['banned-types'] self.race_mode_banned_locations = [] self.non_required_dungeons = [ dungeon for dungeon in constants.POTENTIALLY_REQUIRED_DUNGEONS if not dungeon in self.required_dungeons ] rupoor_mode = self.options['rupoor-mode'] if rupoor_mode != 'Off': if rupoor_mode == 'Added': logic.item_types.CONSUMABLE_ITEMS += ['Rupoor'] * 15 else: self.rng.shuffle(logic.item_types.CONSUMABLE_ITEMS) replace_end_index = len(logic.item_types.CONSUMABLE_ITEMS) if rupoor_mode == 'Rupoor Mayhem': replace_end_index /= 2 for i in range(int(replace_end_index)): logic.item_types.CONSUMABLE_ITEMS[i] = 'Rupoor' self.logic = Logic(self)
import sys from PyQt5.QtWidgets import QApplication from utils import json_hook import json from ui.login import LoginWindow from ui.room import RoomWindow from ui.game import GameWindow from logic.logic import Logic if __name__ == '__main__': app = QApplication([]) logic = Logic() login_window = LoginWindow() login_window.username_signal.connect(logic.check_username) # valid username signal: logic.username_valid_signal.connect(login_window.close) # invalid username signal: logic.username_invalid_signal.connect(login_window.invalid_username) room_window = RoomWindow() # signal emitted when username is valid in login_window logic.username_valid_signal.connect(room_window.show) # signal emitted when new player joins the room: logic.new_player_signal.connect(room_window.new_player) # signal emitted when game starts: logic.start_game_signal.connect(room_window.close) game_window = GameWindow()
if use_ui: try: from parser.ui import Ui except: use_ui = False else: try: import readline #input history except: pass from parser.parser import Parser from logic.logic import Logic from logging.logging import Logging logic = Logic() logging = Logging(logic) parser = Parser(logic, logging) if len(sys.argv) > 1 and sys.argv[1] != "-cli": logging.start() parser.default_script() sleep(0.1) #wait for pybroker connection parser.print(">".join(sys.argv[1:])) parser.parse(" ".join(sys.argv[1:])) if sys.argv[1] == "log": print("Press enter to end logging") input() elif use_ui: ui = Ui()
from utils.doorsmanager import DoorsManager from logic.logic import Logic from utils.objectives import Objectives import utils.log import utils.db as db # we need to know the logic before doing anything else def getLogic(): # check if --logic is there logic = 'vanilla' for i, param in enumerate(sys.argv): if param == '--logic' and i+1 < len(sys.argv): logic = sys.argv[i+1] return logic Logic.factory(getLogic()) defaultMultiValues = getDefaultMultiValues() speeds = defaultMultiValues['progressionSpeed'] energyQties = defaultMultiValues['energyQty'] progDiffs = defaultMultiValues['progressionDifficulty'] morphPlacements = defaultMultiValues['morphPlacement'] majorsSplits = defaultMultiValues['majorsSplit'] gravityBehaviours = defaultMultiValues['gravityBehaviour'] objectives = defaultMultiValues['objective'] def randomMulti(args, param, defaultMultiValues): value = args[param] isRandom = False if value == "random": isRandom = True
def __init__(self, options: Options, progress_callback=dummy_progress_callback): self.options = options # hack: if shops are vanilla, disable them as banned types because of bug net and progressive pouches if self.options['shop-mode'] == 'Vanilla': banned_types = self.options['banned-types'] for unban_shop_item in ['beedle', 'cheap', 'medium', 'expensive']: if unban_shop_item in banned_types: banned_types.remove(unban_shop_item) self.options.set_option('banned-types', banned_types) self.progress_callback = progress_callback self.dry_run = bool(self.options['dry-run']) # TODO: maybe make paths configurable? # exe root path is where the executable is self.exe_root_path = Path('.').resolve() # this is where all assets/read only files are self.rando_root_path = RANDO_ROOT_PATH if not self.dry_run: self.actual_extract_path = self.exe_root_path / 'actual-extract' self.modified_extract_path = self.exe_root_path / 'modified-extract' self.oarc_cache_path = self.exe_root_path / 'oarc' self.no_logs = self.options['no-spoiler-log'] self.seed = self.options['seed'] if self.seed == -1: self.seed = random.randint(0, 1000000) self.options.set_option('seed', self.seed) self.randomizer_hash = self._get_rando_hash() self.rng = random.Random() self.rng.seed(self.seed) if self.no_logs: self.rng.randint(0, 100) dungeons = [ "Skyview", "Earth Temple", "Lanayru Mining Facility", "Ancient Cistern", "Sandship", "Fire Sanctuary" ] if self.options['randomize-entrances'] == 'None': dungeons.append('Sky Keep') dungeons.reverse() else: if self.options['randomize-entrances'] == 'Dungeons': self.rng.shuffle(dungeons) dungeons.append('Sky Keep') dungeons.reverse() else: dungeons.append('Sky Keep') self.rng.shuffle(dungeons) self.entrance_connections = OrderedDict([ ("Dungeon Entrance in Deep Woods", dungeons.pop()), ("Dungeon Entrance in Eldin Volcano", dungeons.pop()), ("Dungeon Entrance in Lanayru Desert", dungeons.pop()), ("Dungeon Entrance in Lake Floria", dungeons.pop()), ("Dungeon Entrance in Sand Sea", dungeons.pop()), ("Dungeon Entrance in Volcano Summit", dungeons.pop()), ("Dungeon Entrance on Skyloft", dungeons.pop()), ]) assert len(dungeons) == 0, 'Not all dungeons linked to an entrance' # self.starting_items = (x.strip() for x in self.options['starting_items'] # self.starting_items: List[str] = list(filter(lambda x: x != '', self.starting_items)) self.starting_items = [] self.required_dungeons = self.rng.sample( constants.POTENTIALLY_REQUIRED_DUNGEONS, k=self.options['required-dungeon-count']) # make the order always consistent self.required_dungeons = [ dungeon for dungeon in constants.POTENTIALLY_REQUIRED_DUNGEONS if dungeon in self.required_dungeons ] tablets = ['Emerald Tablet', 'Ruby Tablet', 'Amber Tablet'] self.starting_items.extend( self.rng.sample(tablets, k=self.options['starting-tablet-count'])) if not self.options['swordless']: self.starting_items.append('Progressive Sword') self.starting_items.append('Progressive Sword') # if not self.options.get('randomize-sailcloth',False): # self.starting_items.append('Sailcloth') if self.options['start-with-pouch']: self.starting_items.append('Progressive Pouch') self.banned_types = self.options['banned-types'] self.race_mode_banned_locations = [] self.non_required_dungeons = [ dungeon for dungeon in constants.POTENTIALLY_REQUIRED_DUNGEONS if not dungeon in self.required_dungeons ] self.logic = Logic(self) # self.logic.set_prerandomization_item_location("Beedle - Second 100 Rupee Item", "Rare Treasure") # self.logic.set_prerandomization_item_location("Beedle - Third 100 Rupee Item", "Rare Treasure") # self.logic.set_prerandomization_item_location("Beedle - 1000 Rupee Item", "Rare Treasure") self.hints = Hints(self.logic)
from rando.Items import ItemManager from logic.smboolmanager import SMBoolManager from logic.logic import Logic import random import utils.log fun = ['HiJump', 'SpeedBooster', 'Plasma', 'ScrewAttack', 'Wave', 'Spazer', 'SpringBall'] maxLocs = 109 bossesItems = ['Kraid', 'Phantoon', 'Draygon', 'Ridley', 'MotherBrain', 'SporeSpawn', 'Crocomire', 'Botwoon', 'GoldenTorizo'] if __name__ == "__main__": # utils.log.init(True) # debug mode utils.log.init(False) logger = utils.log.get('ItemsTest') Logic.factory('vanilla') sm = SMBoolManager() with open("itemStats.csv", "w") as csvOut: csvOut.write("nLocs;energyQty;minorQty;nFun;strictMinors;MissProb;SuperProb;PowerProb;split;nItems;nTanks;nTanksTotal;nMinors;nMissiles;nSupers;nPowers;MissAccuracy;SuperAccuracy;PowerAccuracy;AmmoAccuracy\n") for i in range(10000): logger.debug('SEED ' + str(i)) if (i+1) % 100 == 0: print(i+1) isVanilla = random.random() < 0.5 strictMinors = bool(random.getrandbits(1)) minQty = 100 energyQty = 'vanilla' forbidden = [] if not isVanilla: minQty = random.randint(1, 99) r = random.random()
def calculate_playthrough_progression_spheres(self): progression_spheres = [] logic = Logic(self) previously_accessible_locations = [] game_beatable = False while logic.unplaced_progress_items: progress_items_in_this_sphere = OrderedDict() accessible_locations = logic.get_accessible_remaining_locations() locations_in_this_sphere = [ loc for loc in accessible_locations if loc not in previously_accessible_locations ] if not locations_in_this_sphere: raise Exception("Failed to calculate progression spheres") if not self.options.get("keylunacy"): # If the player gained access to any small keys, we need to give them the keys without counting that as a new sphere. newly_accessible_predetermined_item_locations = [ loc for loc in locations_in_this_sphere if loc in self.logic.prerandomization_item_locations ] newly_accessible_small_key_locations = [ loc for loc in newly_accessible_predetermined_item_locations if self.logic.prerandomization_item_locations[loc]. endswith(" Small Key") ] if newly_accessible_small_key_locations: for small_key_location_name in newly_accessible_small_key_locations: item_name = self.logic.prerandomization_item_locations[ small_key_location_name] assert item_name.endswith(" Small Key") logic.add_owned_item(item_name) previously_accessible_locations += newly_accessible_small_key_locations continue # Redo this loop iteration with the small key locations no longer being considered 'remaining'. for location_name in locations_in_this_sphere: item_name = self.logic.done_item_locations[location_name] if item_name in logic.all_progress_items: progress_items_in_this_sphere[location_name] = item_name if not game_beatable: game_beatable = logic.check_requirement_met( "Can Reach and Defeat Ganondorf") if game_beatable: progress_items_in_this_sphere[ "Ganon's Tower - Rooftop"] = "Defeat Ganondorf" progression_spheres.append(progress_items_in_this_sphere) for location_name, item_name in progress_items_in_this_sphere.items( ): if item_name == "Defeat Ganondorf": continue logic.add_owned_item(item_name) for group_name, item_names in logic.progress_item_groups.items(): entire_group_is_owned = all( item_name in logic.currently_owned_items for item_name in item_names) if entire_group_is_owned and group_name in logic.unplaced_progress_items: logic.unplaced_progress_items.remove(group_name) previously_accessible_locations = accessible_locations if not game_beatable: # If the game wasn't already beatable on a previous progression sphere but it is now we add one final one just for this. game_beatable = logic.check_requirement_met( "Can Reach and Defeat Ganondorf") if game_beatable: final_progression_sphere = OrderedDict([ ("Ganon's Tower - Rooftop", "Defeat Ganondorf"), ]) progression_spheres.append(final_progression_sphere) return progression_spheres
def __init__(self, cmd_line_args=[]): super(WWRandomizerWindow, self).__init__() self.ui = Ui_MainWindow() self.ui.setupUi(self) self.cmd_line_args = cmd_line_args self.bulk_test = ("-bulk" in cmd_line_args) self.custom_color_selector_buttons = OrderedDict() self.custom_color_selector_hex_inputs = OrderedDict() self.custom_colors = OrderedDict() self.initialize_custom_player_model_list() self.preserve_default_settings() self.cached_item_locations = Logic.load_and_parse_item_locations() self.load_settings() self.ui.clean_iso_path.editingFinished.connect(self.update_settings) self.ui.output_folder.editingFinished.connect(self.update_settings) self.ui.seed.editingFinished.connect(self.update_settings) self.ui.clean_iso_path_browse_button.clicked.connect(self.browse_for_clean_iso) self.ui.output_folder_browse_button.clicked.connect(self.browse_for_output_folder) self.ui.permalink.textEdited.connect(self.permalink_modified) self.ui.custom_player_model.currentIndexChanged.connect(self.custom_model_changed) self.ui.player_in_casual_clothes.clicked.connect(self.custom_model_changed) for option_name in OPTIONS: widget = getattr(self.ui, option_name) if isinstance(widget, QAbstractButton): widget.clicked.connect(self.update_settings) elif isinstance(widget, QComboBox): widget.currentIndexChanged.connect(self.update_settings) else: raise Exception("Option widget is invalid: %s" % option_name) self.ui.generate_seed_button.clicked.connect(self.generate_seed) self.ui.randomize_button.clicked.connect(self.randomize) self.ui.reset_settings_to_default.clicked.connect(self.reset_settings_to_default) self.ui.about_button.clicked.connect(self.open_about) for option_name in OPTIONS: getattr(self.ui, option_name).installEventFilter(self) label_for_option = getattr(self.ui, "label_for_" + option_name, None) if label_for_option: label_for_option.installEventFilter(self) self.set_option_description(None) self.update_settings() self.setWindowTitle("Wind Waker Randomizer %s" % VERSION) icon_path = os.path.join(ASSETS_PATH, "icon.ico") self.setWindowIcon(QIcon(icon_path)) self.show() self.update_checker_thread = UpdateCheckerThread() self.update_checker_thread.finished_checking_for_updates.connect(self.show_update_check_results) self.update_checker_thread.start()
def __init__(self, seed, clean_iso_path, randomized_output_folder, options, permalink=None, cmd_line_args=OrderedDict()): self.randomized_output_folder = randomized_output_folder self.options = options self.seed = seed self.permalink = permalink self.dry_run = ("-dry" in cmd_line_args) self.disassemble = ("-disassemble" in cmd_line_args) self.export_disc_to_folder = ("-exportfolder" in cmd_line_args) self.no_logs = ("-nologs" in cmd_line_args) self.bulk_test = ("-bulk" in cmd_line_args) if self.bulk_test: self.dry_run = True self.no_logs = True self.print_used_flags = ("-printflags" in cmd_line_args) self.test_room_args = None if "-test" in cmd_line_args: args = cmd_line_args["-test"] if args is not None: stage, room, spawn = args.split(",") self.test_room_args = { "stage": stage, "room": int(room), "spawn": int(spawn) } seed_string = self.seed if not self.options.get("generate_spoiler_log"): seed_string += SEED_KEY self.integer_seed = self.convert_string_to_integer_md5(seed_string) self.rng = self.get_new_rng() self.arcs_by_path = {} self.jpcs_by_path = {} self.raw_files_by_path = {} self.read_text_file_lists() if not self.dry_run: if not os.path.isfile(clean_iso_path): raise InvalidCleanISOError("Clean WW ISO does not exist: %s" % clean_iso_path) self.verify_supported_version(clean_iso_path) self.gcm = GCM(clean_iso_path) self.gcm.read_entire_disc() try: self.chart_list = self.get_arc( "files/res/Msg/fmapres.arc").get_file("cmapdat.bin") except InvalidOffsetError: # An invalid offset error when reading fmapres.arc seems to happen when the user has a corrupted clean ISO. # The reason for this is unknown, but when this happens check the ISO's MD5 and if it's wrong say so in an error message. self.verify_correct_clean_iso_md5(clean_iso_path) # But if the ISO's MD5 is correct just raise the normal offset error. raise self.bmg = self.get_arc("files/res/Msg/bmgres.arc").get_file( "zel_00.bmg") if self.disassemble: self.disassemble_all_code() if self.print_used_flags: stage_searcher.print_all_used_item_pickup_flags(self) stage_searcher.print_all_used_chest_open_flags(self) stage_searcher.print_all_event_flags_used_by_stb_cutscenes( self) # Starting items. This list is read by the Logic when initializing your currently owned items list. self.starting_items = [ "Wind Waker", "Wind's Requiem", "Ballad of Gales", "Song of Passing", "Hero's Shield", "Boat's Sail", ] self.starting_items += self.options.get("starting_gear", []) if self.options.get("sword_mode") == "Start with Sword": self.starting_items.append("Progressive Sword") # Add starting Triforce Shards. num_starting_triforce_shards = int( self.options.get("num_starting_triforce_shards", 0)) for i in range(num_starting_triforce_shards): self.starting_items.append("Triforce Shard %d" % (i + 1)) # Default entrances connections to be used if the entrance randomizer is not on. self.entrance_connections = OrderedDict([ ("Dungeon Entrance On Dragon Roost Island", "Dragon Roost Cavern"), ("Dungeon Entrance In Forest Haven Sector", "Forbidden Woods"), ("Dungeon Entrance In Tower of the Gods Sector", "Tower of the Gods"), ("Dungeon Entrance On Headstone Island", "Earth Temple"), ("Dungeon Entrance On Gale Isle", "Wind Temple"), ("Secret Cave Entrance on Outset Island", "Savage Labyrinth"), ("Secret Cave Entrance on Dragon Roost Island", "Dragon Roost Island Secret Cave"), ("Secret Cave Entrance on Fire Mountain", "Fire Mountain Secret Cave"), ("Secret Cave Entrance on Ice Ring Isle", "Ice Ring Isle Secret Cave"), ("Secret Cave Entrance on Private Oasis", "Cabana Labyrinth"), ("Secret Cave Entrance on Needle Rock Isle", "Needle Rock Isle Secret Cave"), ("Secret Cave Entrance on Angular Isles", "Angular Isles Secret Cave"), ("Secret Cave Entrance on Boating Course", "Boating Course Secret Cave"), ("Secret Cave Entrance on Stone Watcher Island", "Stone Watcher Island Secret Cave"), ("Secret Cave Entrance on Overlook Island", "Overlook Island Secret Cave"), ("Secret Cave Entrance on Bird's Peak Rock", "Bird's Peak Rock Secret Cave"), ("Secret Cave Entrance on Pawprint Isle", "Pawprint Isle Chuchu Cave"), ("Secret Cave Entrance on Pawprint Isle Side Isle", "Pawprint Isle Wizzrobe Cave"), ("Secret Cave Entrance on Diamond Steppe Island", "Diamond Steppe Island Warp Maze Cave"), ("Secret Cave Entrance on Bomb Island", "Bomb Island Secret Cave"), ("Secret Cave Entrance on Rock Spire Isle", "Rock Spire Isle Secret Cave"), ("Secret Cave Entrance on Shark Island", "Shark Island Secret Cave"), ("Secret Cave Entrance on Cliff Plateau Isles", "Cliff Plateau Isles Secret Cave"), ("Secret Cave Entrance on Horseshoe Island", "Horseshoe Island Secret Cave"), ("Secret Cave Entrance on Star Island", "Star Island Secret Cave"), ]) self.dungeon_and_cave_island_locations = OrderedDict([ ("Dragon Roost Cavern", "Dragon Roost Island"), ("Forbidden Woods", "Forest Haven"), ("Tower of the Gods", "Tower of the Gods"), ("Earth Temple", "Headstone Island"), ("Wind Temple", "Gale Isle"), ("Secret Cave Entrance on Outset Island", "Outset Island"), ("Secret Cave Entrance on Dragon Roost Island", "Dragon Roost Island"), ("Secret Cave Entrance on Fire Mountain", "Fire Mountain"), ("Secret Cave Entrance on Ice Ring Isle", "Ice Ring Isle"), ("Secret Cave Entrance on Private Oasis", "Private Oasis"), ("Secret Cave Entrance on Needle Rock Isle", "Needle Rock Isle"), ("Secret Cave Entrance on Angular Isles", "Angular Isles"), ("Secret Cave Entrance on Boating Course", "Boating Course"), ("Secret Cave Entrance on Stone Watcher Island", "Stone Watcher Island"), ("Secret Cave Entrance on Overlook Island", "Overlook Island"), ("Secret Cave Entrance on Bird's Peak Rock", "Bird's Peak Rock"), ("Secret Cave Entrance on Pawprint Isle", "Pawprint Isle"), ("Secret Cave Entrance on Pawprint Isle Side Isle", "Pawprint Isle"), ("Secret Cave Entrance on Diamond Steppe Island", "Diamond Steppe Island"), ("Secret Cave Entrance on Bomb Island", "Bomb Island"), ("Secret Cave Entrance on Rock Spire Isle", "Rock Spire Isle"), ("Secret Cave Entrance on Shark Island", "Shark Island"), ("Secret Cave Entrance on Cliff Plateau Isles", "Cliff Plateau Isles"), ("Secret Cave Entrance on Horseshoe Island", "Horseshoe Island"), ("Secret Cave Entrance on Star Island", "Star Island"), ]) # Default starting island (Outset) if the starting island randomizer is not on. self.starting_island_index = 44 # Default charts for each island. self.island_number_to_chart_name = OrderedDict([ (1, "Treasure Chart 25"), (2, "Treasure Chart 7"), (3, "Treasure Chart 24"), (4, "Triforce Chart 2"), (5, "Treasure Chart 11"), (6, "Triforce Chart 7"), (7, "Treasure Chart 13"), (8, "Treasure Chart 41"), (9, "Treasure Chart 29"), (10, "Treasure Chart 22"), (11, "Treasure Chart 18"), (12, "Treasure Chart 30"), (13, "Treasure Chart 39"), (14, "Treasure Chart 19"), (15, "Treasure Chart 8"), (16, "Treasure Chart 2"), (17, "Treasure Chart 10"), (18, "Treasure Chart 26"), (19, "Treasure Chart 3"), (20, "Treasure Chart 37"), (21, "Treasure Chart 27"), (22, "Treasure Chart 38"), (23, "Triforce Chart 1"), (24, "Treasure Chart 21"), (25, "Treasure Chart 6"), (26, "Treasure Chart 14"), (27, "Treasure Chart 34"), (28, "Treasure Chart 5"), (29, "Treasure Chart 28"), (30, "Treasure Chart 35"), (31, "Triforce Chart 3"), (32, "Triforce Chart 6"), (33, "Treasure Chart 1"), (34, "Treasure Chart 20"), (35, "Treasure Chart 36"), (36, "Treasure Chart 23"), (37, "Treasure Chart 12"), (38, "Treasure Chart 16"), (39, "Treasure Chart 4"), (40, "Treasure Chart 17"), (41, "Treasure Chart 31"), (42, "Triforce Chart 5"), (43, "Treasure Chart 9"), (44, "Triforce Chart 4"), (45, "Treasure Chart 40"), (46, "Triforce Chart 8"), (47, "Treasure Chart 15"), (48, "Treasure Chart 32"), (49, "Treasure Chart 33"), ]) # This list will hold the randomly selected dungeon boss locations that are required in race mode. # If race mode is not on, this list will remain empty. self.race_mode_required_locations = [] # This list will hold the dungeon names of the race mode required locations. # If race mode is not on, this list will remain empty. self.race_mode_required_dungeons = [] # This list will hold all item location names that should not have any items in them in race mode. # If race mode is not on, this list will remain empty. self.race_mode_banned_locations = [] self.custom_model_name = "Link" self.logic = Logic(self) num_progress_locations = self.logic.get_num_progression_locations() num_progress_items = self.logic.get_num_progression_items() if num_progress_locations < num_progress_items: error_message = "Not enough progress locations to place all progress items.\n\n" error_message += "Total progress items: %d\n" % num_progress_items error_message += "Progress locations with current options: %d\n\n" % num_progress_locations error_message += "You need to check more of the progress location options in order to give the randomizer enough space to place all the items." raise TooFewProgressionLocationsError(error_message) # We need to determine if the user's selected options result in a dungeons-only-start. # Dungeons-only-start meaning that the only locations accessible at the start of the run are dungeon locations. # e.g. If the user selects Dungeons, Expensive Purchases, and Sunken Treasures, the dungeon locations are the only ones the player can check first. # We need to distinguish this situation because it can cause issues for the randomizer's item placement logic (specifically when placing keys in DRC). self.logic.temporarily_make_dungeon_entrance_macros_impossible() accessible_undone_locations = self.logic.get_accessible_remaining_locations( for_progression=True) if len(accessible_undone_locations) == 0: self.dungeons_only_start = True else: self.dungeons_only_start = False self.logic.update_entrance_connection_macros( ) # Reset the dungeon entrance macros. # Also determine if these options result in a dungeons-and-caves-only-start. # Dungeons-and-caves-only-start means the only locations accessible at the start of the run are dungeon or secret cave locations. # This situation can also cause issues for the item placement logic (specifically when placing the first item of the run). self.logic.temporarily_make_entrance_macros_impossible() accessible_undone_locations = self.logic.get_accessible_remaining_locations( for_progression=True) if len(accessible_undone_locations) == 0: self.dungeons_and_caves_only_start = True else: self.dungeons_and_caves_only_start = False self.logic.update_entrance_connection_macros( ) # Reset the entrance macros.
def __init__(self, cmd_line_args=OrderedDict()): super(WWRandomizerWindow, self).__init__() self.ui = Ui_MainWindow() self.ui.setupUi(self) self.cmd_line_args = cmd_line_args self.bulk_test = ("-bulk" in cmd_line_args) self.custom_color_selector_buttons = OrderedDict() self.custom_color_selector_hex_inputs = OrderedDict() self.custom_colors = OrderedDict() self.initialize_custom_player_model_list() self.ui.add_gear.clicked.connect(self.add_to_starting_gear) self.randomized_gear_model = QStringListModel() self.randomized_gear_model.setStringList(INVENTORY_ITEMS.copy()) self.filtered_rgear = ModelFilterOut() self.filtered_rgear.setSourceModel(self.randomized_gear_model) self.ui.randomized_gear.setModel(self.filtered_rgear) self.ui.remove_gear.clicked.connect(self.remove_from_starting_gear) self.starting_gear_model = QStringListModel() self.ui.starting_gear.setModel(self.starting_gear_model) self.preserve_default_settings() self.cached_item_locations = Logic.load_and_parse_item_locations() self.load_settings() self.ui.clean_iso_path.editingFinished.connect(self.update_settings) self.ui.output_folder.editingFinished.connect(self.update_settings) self.ui.seed.editingFinished.connect(self.update_settings) self.ui.clean_iso_path_browse_button.clicked.connect( self.browse_for_clean_iso) self.ui.output_folder_browse_button.clicked.connect( self.browse_for_output_folder) self.ui.permalink.textEdited.connect(self.permalink_modified) self.ui.custom_player_model.currentIndexChanged.connect( self.custom_model_changed) self.ui.player_in_casual_clothes.clicked.connect( self.custom_model_changed) for option_name in OPTIONS: widget = getattr(self.ui, option_name) if isinstance(widget, QAbstractButton): widget.clicked.connect(self.update_settings) elif isinstance(widget, QComboBox): widget.currentIndexChanged.connect(self.update_settings) elif isinstance(widget, QListView): pass else: raise Exception("Option widget is invalid: %s" % option_name) self.ui.generate_seed_button.clicked.connect(self.generate_seed) self.ui.randomize_button.clicked.connect(self.randomize) self.ui.reset_settings_to_default.clicked.connect( self.reset_settings_to_default) self.ui.about_button.clicked.connect(self.open_about) for option_name in OPTIONS: getattr(self.ui, option_name).installEventFilter(self) label_for_option = getattr(self.ui, "label_for_" + option_name, None) if label_for_option: label_for_option.installEventFilter(self) self.set_option_description(None) self.update_settings() self.setWindowTitle("Wind Waker Randomizer %s" % VERSION) icon_path = os.path.join(ASSETS_PATH, "icon.ico") self.setWindowIcon(QIcon(icon_path)) # Hide unfinished options from the GUI (still accessible via settings.txt). self.ui.randomize_bgm.hide() self.show() if not IS_RUNNING_FROM_SOURCE: self.update_checker_thread = UpdateCheckerThread() self.update_checker_thread.finished_checking_for_updates.connect( self.show_update_check_results) self.update_checker_thread.start() else: self.ui.update_checker_label.setText( "(Running from source, skipping release update check.)")