def create_dungeons(world): for dungeon_info in dungeon_table: name = dungeon_info['name'] hint = dungeon_info['hint'] if 'hint' in dungeon_info else name font_color = dungeon_info['font_color'] if 'font_color' in dungeon_info else 'White' if world.settings.logic_rules == 'glitched': if not world.dungeon_mq[name]: dungeon_json = os.path.join(data_path('Glitched World'), name + '.json') else: dungeon_json = os.path.join(data_path('Glitched World'), name + ' MQ.json') else: if not world.dungeon_mq[name]: dungeon_json = os.path.join(data_path('World'), name + '.json') else: dungeon_json = os.path.join(data_path('World'), name + ' MQ.json') world.load_regions_from_json(dungeon_json) boss_keys = ItemFactory(['Boss Key (%s)' % name] * dungeon_info['boss_key']) if not world.dungeon_mq[dungeon_info['name']]: small_keys = ItemFactory(['Small Key (%s)' % name] * dungeon_info['small_key']) else: small_keys = ItemFactory(['Small Key (%s)' % name] * dungeon_info['small_key_mq']) dungeon_items = ItemFactory(['Map (%s)' % name, 'Compass (%s)' % name] * dungeon_info['dungeon_item']) if world.settings.shuffle_mapcompass in ['any_dungeon', 'overworld']: for item in dungeon_items: item.priority = True world.dungeons.append(Dungeon(world, name, hint, font_color, boss_keys, small_keys, dungeon_items))
def generate_tunic_icon(color): with open(data_path('icons/grey.tiff'), 'rb') as grey_fil, open(data_path('icons/belt.tiff'), 'rb') as belt_fil: grey = list(grey_fil.read()) belt = list(belt_fil.read()) return add_belt(add_hue(grey, color, True), belt, True)[154:]
def create_dungeons(world): for dungeon_info in dungeon_table: name = dungeon_info['name'] hint = dungeon_info['hint'] if 'hint' in dungeon_info else name if world.settings.logic_rules == 'glitched': if not world.dungeon_mq[name]: dungeon_json = os.path.join(data_path('Glitched World'), name + '.json') else: dungeon_json = os.path.join(data_path('Glitched World'), name + ' MQ.json') else: if not world.dungeon_mq[name]: dungeon_json = os.path.join(data_path('World'), name + '.json') else: dungeon_json = os.path.join(data_path('World'), name + ' MQ.json') world.load_regions_from_json(dungeon_json) boss_keys = ItemFactory(['Boss Key (%s)' % name] * dungeon_info['boss_key']) if not world.dungeon_mq[dungeon_info['name']]: small_keys = ItemFactory(['Small Key (%s)' % name] * dungeon_info['small_key']) else: small_keys = ItemFactory(['Small Key (%s)' % name] * dungeon_info['small_key_mq']) dungeon_items = ItemFactory( ['Map (%s)' % name, 'Compass (%s)' % name] * dungeon_info['dungeon_item']) world.dungeons.append( Dungeon(world, name, hint, boss_keys, small_keys, dungeon_items))
def generate(input_data): settings = getSettings(input_data) for trick in SettingsList.logic_tricks.values(): settings.__dict__[trick['name']] = trick['name'] in settings.allowed_tricks worlds = [] for i in range(0, settings.world_count): worlds.append(World(i, settings)) worlds[-1].ensure_tod_access = False for id, world in enumerate(worlds): if settings.logic_rules == 'glitched': overworld_data = os.path.join(data_path('Glitched World'), 'Overworld.json') else: overworld_data = os.path.join(data_path('World'), 'Overworld.json') # Compile the json rules based on settings world.load_regions_from_json(overworld_data) create_dungeons(world) world.create_internal_locations() # Populate drop items drop_locations = list(filter(lambda loc: loc.type == 'Drop', world.get_locations())) for drop_location in drop_locations: item = ItemPool.droplocations[drop_location.name] world.push_item(drop_location, ItemFactory(item, world)) drop_location.locked = True return world
def __init__(self, settings, patch=True): self.__last_address = None file = settings.rom decomp_file = 'ZOOTDEC.z64' os.chdir(local_path()) with open(data_path('generated/symbols.json'), 'r') as stream: symbols = json.load(stream) self.symbols = { name: int(addr, 16) for name, addr in symbols.items() } if file == '': # if not specified, try to read from the previously decompressed rom file = decomp_file try: self.read_rom(file) except FileNotFoundError: # could not find the decompressed rom either raise FileNotFoundError('Must specify path to base ROM') else: self.read_rom(file) # decompress rom, or check if it's already decompressed self.decompress_rom_file(file, decomp_file) # Add file to maximum size self.buffer.extend(bytearray([0x00] * (0x4000000 - len(self.buffer)))) self.original = copy.copy(self.buffer) self.changed_address = {} self.changed_dma = {} self.force_patch = []
def __init__(self, settings, patch=True): self.last_address = None file = settings.rom decomp_file = 'ZOOTDEC.z64' os.chdir(local_path()) with open(data_path('symbols.json'), 'r') as stream: symbols = json.load(stream) self.symbols = { name: int(addr, 16) for name, addr in symbols.items() } try: # Read decompressed file if it exists self.read_rom(decomp_file) # This is mainly for validation testing, but just in case... self.decompress_rom_file(decomp_file, decomp_file) except Exception as ex: # No decompressed file, instead read Input ROM self.read_rom(file) self.decompress_rom_file(file, decomp_file) # Add file to maximum size self.buffer.extend(bytearray([0x00] * (0x4000000 - len(self.buffer))))
def build_world_graphs(settings, window=dummy_window()): logger = logging.getLogger('') worlds = [] for i in range(0, settings.world_count): worlds.append(World(i, settings)) window.update_status('Creating the Worlds') for id, world in enumerate(worlds): logger.info('Generating World %d.' % (id + 1)) window.update_progress(0 + 1 * (id + 1) / settings.world_count) logger.info('Creating Overworld') if settings.logic_rules == 'glitched': overworld_data = os.path.join(data_path('Glitched World'), 'Overworld.json') else: overworld_data = os.path.join(data_path('World'), 'Overworld.json') # Compile the json rules based on settings world.load_regions_from_json(overworld_data) create_dungeons(world) world.create_internal_locations() if settings.shopsanity != 'off': world.random_shop_prices() world.set_scrub_prices() window.update_progress(0 + 4 * (id + 1) / settings.world_count) logger.info('Calculating Access Rules.') set_rules(world) window.update_progress(0 + 5 * (id + 1) / settings.world_count) logger.info('Generating Item Pool.') generate_itempool(world) set_shop_rules(world) set_drop_location_names(world) world.fill_bosses() if settings.triforce_hunt: settings.distribution.configure_triforce_hunt(worlds) logger.info('Setting Entrances.') set_entrances(worlds) return worlds
def patch_magic_colors(rom, settings, log, symbols): # patch magic colors magic = [ ('Magic Meter Color', 'magic_color', symbols["CFG_MAGIC_COLOR"], ([0x154C654, 0x154CFB4], [0x154C65C, 0x154CFBC])), # GI Model DList colors ] magic_color_list = get_magic_colors() for magic_color, magic_setting, symbol, model_addresses in magic: magic_option = settings.__dict__[magic_setting] # Handle Plando if log.src_dict.get('ui_colors', {}).get(magic_color, {}).get('color', ''): magic_option = log.src_dict['ui_colors'][magic_color]['color'] if magic_option == 'Random Choice': magic_option = random.choice(magic_color_list) if magic_option == 'Completely Random': color = generate_random_color() elif magic_option in magic_colors: color = list(magic_colors[magic_option]) else: color = hex_to_color(magic_option) magic_option = 'Custom' rom.write_int16s(symbol, color) if magic_option != 'Green' and settings.correct_model_colors: patch_model_colors(rom, color, model_addresses) icon.patch_overworld_icon( rom, color, 0xF45650, data_path('icons/magicSmallExtras.raw')) # Overworld Small Pot icon.patch_overworld_icon( rom, color, 0xF47650, data_path('icons/magicLargeExtras.raw')) # Overworld Big Pot else: patch_model_colors(rom, None, model_addresses) icon.patch_overworld_icon(rom, None, 0xF45650) icon.patch_overworld_icon(rom, None, 0xF47650) log.ui_colors[magic_color] = CollapseDict({ ':option': magic_option, 'color': color_to_hex(color), })
def load_aliases(): j = read_json(data_path('LogicHelpers.json')) for s, repl in j.items(): if '(' in s: rule, args = s[:-1].split('(', 1) args = [re.compile(r'\b%s\b' % a.strip()) for a in args.split(',')] else: rule = s args = () rule_aliases[rule] = (args, repl) nonaliases = escaped_items.keys() - rule_aliases.keys()
def buildBingoHintList(boardURL): try: if len(boardURL) > 256: raise URLError(f"URL too large {len(boardURL)}") with urllib.request.urlopen(boardURL + "/board") as board: if board.length and 0 < board.length < 4096: goalList = board.read() else: raise HTTPError(f"Board of invalid size {board.length}") except (URLError, HTTPError) as e: logger = logging.getLogger('') logger.info( f"Could not retrieve board info. Using default bingo hints instead: {e}" ) genericBingo = read_json(data_path('Bingo/generic_bingo_hints.json')) return genericBingo['settings']['item_hints'] # Goal list returned from Bingosync is a sequential list of all of the goals on the bingo board, starting at top-left and moving to the right. # Each goal is a dictionary with attributes for name, slot, and colours. The only one we use is the name goalList = [goal['name'] for goal in json.loads(goalList)] goalHintRequirements = read_json(data_path('Bingo/bingo_goals.json')) hintsToAdd = {} for goal in goalList: # Using 'get' here ensures some level of forward compatibility, where new goals added to randomiser bingo won't # cause the generator to crash (though those hints won't have item hints for them) requirements = goalHintRequirements.get(goal, {}) if len(requirements) != 0: for item in requirements: hintsToAdd[item] = max(hintsToAdd.get(item, 0), requirements[item]['count']) # Items to be hinted need to be included in the item_hints list once for each instance you want hinted # (e.g. if you want all three strength upgrades to be hintes it needs to be in the list three times) hints = [] for key, value in hintsToAdd.items(): for _ in range(value): hints.append(key) return hints
def guiMain(): try: version_check("Node", "8.0.0", "https://nodejs.org/en/download/") version_check("NPM", "3.5.2", "https://nodejs.org/en/download/") except VersionError as ex: print(ex.args[0]) webbrowser.open(ex.args[1]) return web_version = '--web' in sys.argv if '--skip-settingslist' not in sys.argv: CreateJSON(data_path('generated/settings_list.json'), web_version) args = ["node", "run.js", "release", "python", sys.executable] subprocess.Popen(args,shell=False,cwd=local_path("GUI"))
def patch_magic_colors(rom, settings, log, symbols): # patch magic colors magic = [ ('Magic Meter Color', settings.magic_color, symbols["CFG_MAGIC_COLOR"], ([0x154C654, 0x154CFB4], [0x154C65C, 0x154CFBC])), # GI Model DList colors ] magic_color_list = get_magic_colors() for magic_color, magic_option, symbol, model_addresses in magic: if magic_option == 'Random Choice': magic_option = random.choice(magic_color_list) if magic_option == 'Completely Random': color = [ random.getrandbits(8), random.getrandbits(8), random.getrandbits(8) ] elif magic_option in magic_colors: color = list(magic_colors[magic_option]) else: color = list(int(magic_option[i:i + 2], 16) for i in (0, 2, 4)) magic_option = 'Custom' rom.write_int16s(symbol, color) if settings.correct_model_colors and magic_option != 'Green': patch_model_colors(rom, color, model_addresses) icon.patch_overworld_icon( rom, color, 0xF45650, data_path('icons/magicSmallExtras.raw')) # Overworld Small Pot icon.patch_overworld_icon( rom, color, 0xF47650, data_path('icons/magicLargeExtras.raw')) # Overworld Big Pot log.magic_colors[magic_color] = dict( option=magic_option, color=''.join(['{:02X}'.format(c) for c in color]))
def __init__(self, file=None): super().__init__([]) self.original = None self.changed_address = {} self.changed_dma = {} self.force_patch = [] if file is None: return decomp_file = local_path('ZOOTDEC.z64') os.chdir(local_path()) with open(data_path('generated/symbols.json'), 'r') as stream: symbols = json.load(stream) self.symbols = { name: int(addr, 16) for name, addr in symbols.items() } if file == '': # if not specified, try to read from the previously decompressed rom file = decomp_file try: self.read_rom(file) except FileNotFoundError: # could not find the decompressed rom either raise FileNotFoundError('Must specify path to base ROM') else: self.read_rom(file) # decompress rom, or check if it's already decompressed self.decompress_rom_file(file, decomp_file) # Add file to maximum size self.buffer.extend(bytearray([0x00] * (0x4000000 - len(self.buffer)))) self.original = self.copy() # Add version number to header. self.write_bytes(0x35, get_version_bytes(__version__)) self.force_patch.extend([0x35, 0x36, 0x37])
def __init__(self, settings, patch=True): self.__last_address = None file = settings.rom decomp_file = 'ZOOTDEC.z64' os.chdir(local_path()) with open(data_path('generated/symbols.json'), 'r') as stream: symbols = json.load(stream) self.symbols = { name: int(addr, 16) for name, addr in symbols.items() } self.read_rom(file) self.decompress_rom_file(file, decomp_file) # Add file to maximum size self.buffer.extend(bytearray([0x00] * (0x4000000 - len(self.buffer)))) self.original = copy.copy(self.buffer) self.changed_address = {} self.changed_dma = {}
def get_json(): with open(data_path('mqu.json'), 'r') as stream: data = json.load(stream) return data
def process_sequences(rom, sequences, target_sequences, disabled_source_sequences, disabled_target_sequences, ids, seq_type='bgm'): # Process vanilla music data for bgm in ids: # Get sequence metadata name = bgm[0] cosmetic_name = name type = rom.read_int16(0xB89AE8 + (bgm[1] * 0x10)) instrument_set = rom.read_byte(0xB89911 + 0xDD + (bgm[1] * 2)) id = bgm[1] # Create new sequences seq = TableEntry(name, cosmetic_name, type, instrument_set, vanilla_id=id) target = TableEntry(name, cosmetic_name, type, instrument_set, replaces=id) # Special handling for file select/fairy fountain if seq.vanilla_id != 0x57 and cosmetic_name not in disabled_source_sequences: sequences.append(seq) if cosmetic_name not in disabled_target_sequences: target_sequences.append(target) # If present, load the file containing custom music to exclude try: with open(os.path.join(data_path(), u'custom_music_exclusion.txt')) as excl_in: seq_exclusion_list = excl_in.readlines() seq_exclusion_list = [ seq.rstrip() for seq in seq_exclusion_list if seq[0] != '#' ] seq_exclusion_list = [ seq for seq in seq_exclusion_list if seq.endswith('.meta') ] except FileNotFoundError: seq_exclusion_list = [] # Process music data in data/Music/ # Each sequence requires a valid .seq sequence file and a .meta metadata file # Current .meta format: Cosmetic Name\nInstrument Set\nPool for dirpath, _, filenames in os.walk(u'./data/Music', followlinks=True): for fname in filenames: # Skip if included in exclusion file if fname in seq_exclusion_list: continue # Find meta file and check if corresponding seq file exists if fname.endswith('.meta') and os.path.isfile( os.path.join(dirpath, fname.split('.')[0] + '.seq')): # Read meta info try: with open(os.path.join(dirpath, fname), 'r') as stream: lines = stream.readlines() # Strip newline(s) lines = [line.rstrip() for line in lines] except FileNotFoundError as ex: raise FileNotFoundError('No meta file for: "' + fname + '". This should never happen') # Create new sequence, checking third line for correct type if (len(lines) > 2 and (lines[2].lower() == seq_type.lower() or lines[2] == '')) or (len(lines) <= 2 and seq_type == 'bgm'): seq = TableEntry(os.path.join(dirpath, fname.split('.')[0]), lines[0], instrument_set=int(lines[1], 16)) if seq.instrument_set < 0x00 or seq.instrument_set > 0x25: raise Exception( 'Sequence instrument must be in range [0x00, 0x25]' ) if seq.cosmetic_name not in disabled_source_sequences: sequences.append(seq) return sequences, target_sequences
def get_settings_from_command_line_args(): parser = argparse.ArgumentParser( formatter_class=ArgumentDefaultsHelpFormatter) parser.add_argument('--gui', help='Launch the GUI', action='store_true') parser.add_argument('--loglevel', default='info', const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.') parser.add_argument( '--settings_string', help= 'Provide sharable settings using a settings string. This will override all flags that it specifies.' ) parser.add_argument( '--convert_settings', help= 'Only convert the specified settings to a settings string. If a settings string is specified output the used settings instead.', action='store_true') parser.add_argument( '--settings', help='Use the specified settings file to use for generation') parser.add_argument( '--settings_preset', help= "Use the given preset for base settings. Anything defined in the --settings file or the --settings_string will override the preset." ) parser.add_argument('--seed', help='Generate the specified seed.') parser.add_argument('--no_log', help='Suppresses the generation of a log file.', action='store_true') parser.add_argument( '--output_settings', help= 'Always outputs a settings.json file even when spoiler is enabled.', action='store_true') args = parser.parse_args() settings_base = {} if args.settings_preset: presetsFiles = [data_path('presets_default.json')] + sorted( os.path.join(data_path('Presets'), fn) for fn in os.listdir(data_path('Presets')) if fn.endswith('.json')) for fn in presetsFiles: with open(fn, encoding='utf-8') as f: presets = json.load(f) if args.settings_preset in presets: settings_base.update(presets[args.settings_preset]) break else: sys.stderr.write( f'ERROR:No preset found with name {args.settings_preset!r}\n') sys.exit(1) if args.settings == '-': settings_base.update(json.loads(sys.stdin.read())) elif args.settings or not settings_base: # avoid implicitly using settings.sav with presets settingsFile = local_path(args.settings or 'settings.sav') try: with open(settingsFile, encoding='utf-8') as f: settings_base.update(json.load(f)) except Exception as ex: if args.settings is not None: raise ex settings = Settings(settings_base) settings.output_settings = args.output_settings if args.settings_string is not None: settings.update_with_settings_string(args.settings_string) if args.seed is not None: settings.update_seed(args.seed) if args.convert_settings: if args.settings_string is not None: print(json.dumps(settings.to_json())) else: print(settings.get_settings_string()) sys.exit(0) return settings, args.gui, args.loglevel, args.no_log
def generate(settings, window): logger = logging.getLogger('') worlds = [] for i in range(0, settings.world_count): worlds.append(World(i, settings)) window.update_status('Creating the Worlds') for id, world in enumerate(worlds): logger.info('Generating World %d.' % (id + 1)) window.update_progress(0 + 1 * (id + 1) / settings.world_count) logger.info('Creating Overworld') if settings.logic_rules == 'glitched': overworld_data = os.path.join(data_path('Glitched World'), 'Overworld.json') else: overworld_data = os.path.join(data_path('World'), 'Overworld.json') # Compile the json rules based on settings world.load_regions_from_json(overworld_data) create_dungeons(world) world.create_internal_locations() if settings.shopsanity != 'off': world.random_shop_prices() world.set_scrub_prices() window.update_progress(0 + 4 * (id + 1) / settings.world_count) logger.info('Calculating Access Rules.') set_rules(world) window.update_progress(0 + 5 * (id + 1) / settings.world_count) logger.info('Generating Item Pool.') generate_itempool(world) set_shop_rules(world) set_drop_location_names(world) world.fill_bosses() if settings.triforce_hunt: settings.distribution.configure_triforce_hunt(worlds) logger.info('Setting Entrances.') set_entrances(worlds) window.update_status('Placing the Items') logger.info('Fill the world.') distribute_items_restrictive(window, worlds) window.update_progress(35) spoiler = Spoiler(worlds) if settings.create_spoiler: window.update_status('Calculating Spoiler Data') logger.info('Calculating playthrough.') create_playthrough(spoiler) window.update_progress(50) if settings.create_spoiler or settings.hints != 'none': window.update_status('Calculating Hint Data') logger.info('Calculating hint data.') State.update_required_items(spoiler) buildGossipHints(spoiler, worlds) window.update_progress(55) spoiler.build_file_hash() return spoiler
def main(settings, window=dummy_window()): start = time.process_time() logger = logging.getLogger('') worlds = [] allowed_tricks = {} for trick in logic_tricks.values(): settings.__dict__[ trick['name']] = trick['name'] in settings.allowed_tricks settings.load_distribution() # we load the rom before creating the seed so that error get caught early if settings.compress_rom == 'None' and not settings.create_spoiler: raise Exception( '`No Output` must have spoiler enabled to produce anything.') if settings.compress_rom != 'None': window.update_status('Loading ROM') rom = Rom(settings.rom) if not settings.world_count: settings.world_count = 1 if settings.world_count < 1 or settings.world_count > 255: raise Exception('World Count must be between 1 and 255') if settings.player_num > settings.world_count or settings.player_num < 1: if settings.compress_rom not in ['None', 'Patch']: raise Exception('Player Num must be between 1 and %d' % settings.world_count) else: settings.player_num = 1 logger.info('OoT Randomizer Version %s - Seed: %s\n\n', __version__, settings.seed) settings.remove_disabled() random.seed(settings.numeric_seed) settings.resolve_random_settings() for i in range(0, settings.world_count): worlds.append(World(settings)) window.update_status('Creating the Worlds') for id, world in enumerate(worlds): world.id = id world.distribution = settings.distribution.world_dists[id] logger.info('Generating World %d.' % id) window.update_progress(0 + 1 * (id + 1) / settings.world_count) logger.info('Creating Overworld') # Determine MQ Dungeons dungeon_pool = list(world.dungeon_mq) dist_num_mq = world.distribution.configure_dungeons( world, dungeon_pool) if world.mq_dungeons_random: for dungeon in dungeon_pool: world.dungeon_mq[dungeon] = random.choice([True, False]) world.mq_dungeons = list(world.dungeon_mq.values()).count(True) else: mqd_picks = random.sample(dungeon_pool, world.mq_dungeons - dist_num_mq) for dung in mqd_picks: world.dungeon_mq[dung] = True if settings.logic_rules == 'glitched': overworld_data = os.path.join(data_path('Glitched World'), 'Overworld.json') else: overworld_data = os.path.join(data_path('World'), 'Overworld.json') world.load_regions_from_json(overworld_data) create_dungeons(world) if settings.shopsanity != 'off': world.random_shop_prices() world.set_scrub_prices() window.update_progress(0 + 4 * (id + 1) / settings.world_count) logger.info('Calculating Access Rules.') set_rules(world) window.update_progress(0 + 5 * (id + 1) / settings.world_count) logger.info('Generating Item Pool.') generate_itempool(world) set_shop_rules(world) set_drop_location_names(world) logger.info('Setting Entrances.') set_entrances(worlds) window.update_status('Placing the Items') logger.info('Fill the world.') distribute_items_restrictive(window, worlds) window.update_progress(35) spoiler = Spoiler(worlds) cosmetics_log = None if settings.create_spoiler: window.update_status('Calculating Spoiler Data') logger.info('Calculating playthrough.') create_playthrough(spoiler) window.update_progress(50) if settings.create_spoiler or settings.hints != 'none': window.update_status('Calculating Hint Data') State.update_required_items(spoiler) for world in worlds: world.update_useless_areas(spoiler) buildGossipHints(spoiler, world) window.update_progress(55) spoiler.build_file_hash() logger.info('Patching ROM.') settings_string_hash = hashlib.sha1( settings.settings_string.encode('utf-8')).hexdigest().upper()[:5] if settings.output_file: outfilebase = settings.output_file elif settings.world_count > 1: outfilebase = 'OoT_%s_%s_W%d' % (settings_string_hash, settings.seed, settings.world_count) else: outfilebase = 'OoT_%s_%s' % (settings_string_hash, settings.seed) output_dir = default_output_path(settings.output_dir) if settings.compress_rom == 'Patch': rng_state = random.getstate() file_list = [] window.update_progress(65) for world in worlds: if settings.world_count > 1: window.update_status('Patching ROM: Player %d' % (world.id + 1)) patchfilename = '%sP%d.zpf' % (outfilebase, world.id + 1) else: window.update_status('Patching ROM') patchfilename = '%s.zpf' % outfilebase random.setstate(rng_state) patch_rom(spoiler, world, rom, outfilebase) cosmetics_log = patch_cosmetics(settings, rom) window.update_progress(65 + 20 * (world.id + 1) / settings.world_count) window.update_status('Creating Patch File') output_path = os.path.join(output_dir, patchfilename) file_list.append(patchfilename) create_patch_file(rom, output_path) rom.restore() window.update_progress(65 + 30 * (world.id + 1) / settings.world_count) if settings.create_cosmetics_log and cosmetics_log: window.update_status('Creating Cosmetics Log') if settings.world_count > 1: cosmetics_log_filename = "%sP%d_Cosmetics.txt" % ( outfilebase, world.id + 1) else: cosmetics_log_filename = '%s_Cosmetics.txt' % outfilebase cosmetics_log.to_file( os.path.join(output_dir, cosmetics_log_filename)) file_list.append(cosmetics_log_filename) cosmetics_log = None if settings.world_count > 1: window.update_status('Creating Patch Archive') output_path = os.path.join(output_dir, '%s.zpfz' % outfilebase) with zipfile.ZipFile(output_path, mode="w") as patch_archive: for file in file_list: file_path = os.path.join(output_dir, file) patch_archive.write(file_path, file.replace(outfilebase, ''), compress_type=zipfile.ZIP_DEFLATED) for file in file_list: os.remove(os.path.join(output_dir, file)) logger.info("Created patchfile at: %s" % output_path) window.update_progress(95) elif settings.compress_rom != 'None': window.update_status('Patching ROM') patch_rom(spoiler, worlds[settings.player_num - 1], rom, outfilebase) cosmetics_log = patch_cosmetics(settings, rom) window.update_progress(65) window.update_status('Saving Uncompressed ROM') if settings.world_count > 1: filename = "%sP%d.z64" % (outfilebase, settings.player_num) else: filename = '%s.z64' % outfilebase output_path = os.path.join(output_dir, filename) rom.write_to_file(output_path) if settings.compress_rom == 'True': window.update_status('Compressing ROM') logger.info('Compressing ROM.') if is_bundled(): compressor_path = "." else: compressor_path = "Compress" if platform.system() == 'Windows': if 8 * struct.calcsize("P") == 64: compressor_path += "\\Compress.exe" else: compressor_path += "\\Compress32.exe" elif platform.system() == 'Linux': if platform.uname()[4] == 'aarch64' or platform.uname( )[4] == 'arm64': compressor_path += "/Compress_ARM64" else: compressor_path += "/Compress" elif platform.system() == 'Darwin': compressor_path += "/Compress.out" else: compressor_path = "" logger.info('OS not supported for compression') output_compress_path = output_path[:output_path. rfind('.')] + '-comp.z64' if compressor_path != "": run_process( window, logger, [compressor_path, output_path, output_compress_path]) os.remove(output_path) logger.info("Created compessed rom at: %s" % output_compress_path) else: logger.info("Created uncompessed rom at: %s" % output_path) window.update_progress(95) for world in worlds: for info in setting_infos: world.settings.__dict__[info.name] = world.__dict__[info.name] settings.distribution.update_spoiler(spoiler) if settings.create_spoiler: window.update_status('Creating Spoiler Log') spoiler_path = os.path.join(output_dir, '%s_Spoiler.json' % outfilebase) settings.distribution.to_file(spoiler_path) logger.info("Created spoiler log at: %s" % ('%s_Spoiler.json' % outfilebase)) else: window.update_status('Creating Settings Log') settings_path = os.path.join(output_dir, '%s_Settings.json' % outfilebase) settings.distribution.to_file(settings_path) logger.info("Created settings log at: %s" % ('%s_Settings.json' % outfilebase)) if settings.create_cosmetics_log and cosmetics_log: window.update_status('Creating Cosmetics Log') if settings.world_count > 1 and not settings.output_file: filename = "%sP%d_Cosmetics.txt" % (outfilebase, settings.player_num) else: filename = '%s_Cosmetics.txt' % outfilebase cosmetic_path = os.path.join(output_dir, filename) cosmetics_log.to_file(cosmetic_path) logger.info("Created cosmetic log at: %s" % cosmetic_path) window.update_progress(100) if cosmetics_log and cosmetics_log.error: window.update_status( 'Success: Rom patched successfully. Some cosmetics could not be applied.' ) else: window.update_status('Success: Rom patched successfully') logger.info('Done. Enjoy.') logger.debug('Total Time: %s', time.process_time() - start) return worlds[settings.player_num - 1]
def HintDistFiles(): return [os.path.join(data_path('Hints/'), d) for d in defaultHintDists] + [ os.path.join(data_path('Hints/'), d) for d in sorted(os.listdir(data_path('Hints/'))) if d.endswith('.json') and d not in defaultHintDists ]
def buildWorldGossipHints(spoiler, world, checkedLocations=None): # rebuild hint exclusion list hintExclusions(world, clear_cache=True) world.barren_dungeon = 0 world.woth_dungeon = 0 search = Search.max_explore([w.state for w in spoiler.worlds]) for stone in gossipLocations.values(): stone.reachable = ( search.spot_access(world.get_location(stone.location)) and search.state_list[world.id].guarantee_hint()) if checkedLocations is None: checkedLocations = set() stoneIDs = list(gossipLocations.keys()) world.distribution.configure_gossip(spoiler, stoneIDs) if 'disabled' in world.hint_dist_user: for stone_name in world.hint_dist_user['disabled']: try: stone_id = gossipLocations_reversemap[stone_name] except KeyError: raise ValueError(f'Gossip stone location "{stone_name}" is not valid') stoneIDs.remove(stone_id) (gossip_text, _) = get_junk_hint(spoiler, world, checkedLocations) spoiler.hints[world.id][stone_id] = gossip_text stoneGroups = [] if 'groups' in world.hint_dist_user: for group_names in world.hint_dist_user['groups']: group = [] for stone_name in group_names: try: stone_id = gossipLocations_reversemap[stone_name] except KeyError: raise ValueError(f'Gossip stone location "{stone_name}" is not valid') stoneIDs.remove(stone_id) group.append(stone_id) stoneGroups.append(group) # put the remaining locations into singleton groups stoneGroups.extend([[id] for id in stoneIDs]) random.shuffle(stoneGroups) # Create list of items for which we want hints. If Bingosync URL is supplied, include items specific to that bingo. # If not (or if the URL is invalid), use generic bingo hints if world.hint_dist == "bingo": bingoDefaults = read_json(data_path('Bingo/generic_bingo_hints.json')) if world.bingosync_url is not None and world.bingosync_url.startswith("https://bingosync.com/"): # Verify that user actually entered a bingosync URL logger = logging.getLogger('') logger.info("Got Bingosync URL. Building board-specific goals.") world.item_hints = buildBingoHintList(world.bingosync_url) else: world.item_hints = bingoDefaults['settings']['item_hints'] if world.tokensanity in ("overworld", "all") and "Suns Song" not in world.item_hints: world.item_hints.append("Suns Song") if world.shopsanity != "off" and "Progressive Wallet" not in world.item_hints: world.item_hints.append("Progressive Wallet") # Load hint distro from distribution file or pre-defined settings # # 'fixed' key is used to mimic the tournament distribution, creating a list of fixed hint types to fill # Once the fixed hint type list is exhausted, weighted random choices are taken like all non-tournament sets # This diverges from the tournament distribution where leftover stones are filled with sometimes hints (or random if no sometimes locations remain to be hinted) sorted_dist = {} type_count = 1 hint_dist = OrderedDict({}) fixed_hint_types = [] max_order = 0 for hint_type in world.hint_dist_user['distribution']: if world.hint_dist_user['distribution'][hint_type]['order'] > 0: hint_order = int(world.hint_dist_user['distribution'][hint_type]['order']) sorted_dist[hint_order] = hint_type if max_order < hint_order: max_order = hint_order type_count = type_count + 1 if (type_count - 1) < max_order: raise Exception("There are gaps in the custom hint orders. Please revise your plando file to remove them.") for i in range(1, type_count): hint_type = sorted_dist[i] if world.hint_dist_user['distribution'][hint_type]['copies'] > 0: fixed_num = world.hint_dist_user['distribution'][hint_type]['fixed'] hint_weight = world.hint_dist_user['distribution'][hint_type]['weight'] else: logging.getLogger('').warning("Hint copies is zero for type %s. Assuming this hint type should be disabled.", hint_type) fixed_num = 0 hint_weight = 0 hint_dist[hint_type] = (hint_weight, world.hint_dist_user['distribution'][hint_type]['copies']) hint_dist.move_to_end(hint_type) fixed_hint_types.extend([hint_type] * int(fixed_num)) hint_types, hint_prob = zip(*hint_dist.items()) hint_prob, _ = zip(*hint_prob) # Add required location hints, only if hint copies > 0 if hint_dist['always'][1] > 0: alwaysLocations = getHintGroup('always', world) for hint in alwaysLocations: location = world.get_location(hint.name) checkedLocations.add(hint.name) if location.item.name in bingoBottlesForHints and world.hint_dist == 'bingo': always_item = 'Bottle' else: always_item = location.item.name if always_item in world.item_hints: world.item_hints.remove(always_item) if location.name in world.hint_text_overrides: location_text = world.hint_text_overrides[location.name] else: location_text = getHint(location.name, world.clearer_hints).text if '#' not in location_text: location_text = '#%s#' % location_text item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text add_hint(spoiler, world, stoneGroups, GossipText('%s #%s#.' % (location_text, item_text), ['Green', 'Red']), hint_dist['always'][1], location, force_reachable=True) logging.getLogger('').debug('Placed always hint for %s.', location.name) # Add trial hints, only if hint copies > 0 if hint_dist['trial'][1] > 0: if world.trials_random and world.trials == 6: add_hint(spoiler, world, stoneGroups, GossipText("#Ganon's Tower# is protected by a powerful barrier.", ['Pink']), hint_dist['trial'][1], force_reachable=True) elif world.trials_random and world.trials == 0: add_hint(spoiler, world, stoneGroups, GossipText("Sheik dispelled the barrier around #Ganon's Tower#.", ['Yellow']), hint_dist['trial'][1], force_reachable=True) elif world.trials < 6 and world.trials > 3: for trial,skipped in world.skipped_trials.items(): if skipped: add_hint(spoiler, world, stoneGroups,GossipText("the #%s Trial# was dispelled by Sheik." % trial, ['Yellow']), hint_dist['trial'][1], force_reachable=True) elif world.trials <= 3 and world.trials > 0: for trial,skipped in world.skipped_trials.items(): if not skipped: add_hint(spoiler, world, stoneGroups, GossipText("the #%s Trial# protects Ganon's Tower." % trial, ['Pink']), hint_dist['trial'][1], force_reachable=True) # Add user-specified hinted item locations if using a built-in hint distribution # Raise error if hint copies is zero if len(world.item_hints) > 0 and world.hint_dist_user['named_items_required']: if hint_dist['named-item'][1] == 0: raise Exception('User-provided item hints were requested, but copies per named-item hint is zero') else: for i in range(0, len(world.item_hints)): hint = get_specific_item_hint(spoiler, world, checkedLocations) if hint == None: raise Exception('No valid hints for user-provided item') else: gossip_text, location = hint place_ok = add_hint(spoiler, world, stoneGroups, gossip_text, hint_dist['named-item'][1], location) if not place_ok: raise Exception('Not enough gossip stones for user-provided item hints') hint_types = list(hint_types) hint_prob = list(hint_prob) hint_counts = {} custom_fixed = True while stoneGroups: if fixed_hint_types: hint_type = fixed_hint_types.pop(0) copies = hint_dist[hint_type][1] if copies > len(stoneGroups): # Quiet to avoid leaking information. logging.getLogger('').debug(f'Not enough gossip stone locations ({len(stoneGroups)} groups) for fixed hint type {hint_type} with {copies} copies, proceeding with available stones.') copies = len(stoneGroups) else: custom_fixed = False # Make sure there are enough stones left for each hint type num_types = len(hint_types) hint_types = list(filter(lambda htype: hint_dist[htype][1] <= len(stoneGroups), hint_types)) new_num_types = len(hint_types) if new_num_types == 0: raise Exception('Not enough gossip stone locations for remaining weighted hint types.') elif new_num_types < num_types: hint_prob = [] for htype in hint_types: hint_prob.append(hint_dist[htype][0]) try: # Weight the probabilities such that hints that are over the expected proportion # will be drawn less, and hints that are under will be drawn more. # This tightens the variance quite a bit. The variance can be adjusted via the power weighted_hint_prob = [] for w1_type, w1_prob in zip(hint_types, hint_prob): p = w1_prob if p != 0: # If the base prob is 0, then it's 0 for w2_type, w2_prob in zip(hint_types, hint_prob): if w2_prob != 0: # If the other prob is 0, then it has no effect # Raising this term to a power greater than 1 will decrease variance # Conversely, a power less than 1 will increase variance p = p * (((hint_counts.get(w2_type, 0) / w2_prob) + 1) / ((hint_counts.get(w1_type, 0) / w1_prob) + 1)) weighted_hint_prob.append(p) hint_type = random_choices(hint_types, weights=weighted_hint_prob)[0] copies = hint_dist[hint_type][1] except IndexError: raise Exception('Not enough valid hints to fill gossip stone locations.') hint = hint_func[hint_type](spoiler, world, checkedLocations) if hint == None: index = hint_types.index(hint_type) hint_prob[index] = 0 # Zero out the probability in the base distribution in case the probability list is modified # to fit hint types in remaining gossip stones hint_dist[hint_type] = (0.0, copies) else: gossip_text, location = hint place_ok = add_hint(spoiler, world, stoneGroups, gossip_text, copies, location) if place_ok: hint_counts[hint_type] = hint_counts.get(hint_type, 0) + 1 if location is None: logging.getLogger('').debug('Placed %s hint.', hint_type) else: logging.getLogger('').debug('Placed %s hint for %s.', hint_type, location.name) if not place_ok and custom_fixed: logging.getLogger('').debug('Failed to place %s fixed hint for %s.', hint_type, location.name) fixed_hint_types.insert(0, hint_type)
def HintDistFiles(): return [os.path.join(data_path('Hints/'), d) for d in os.listdir(data_path('Hints/')) if d.endswith('.json')]
def set_icon(window): er16 = tk.PhotoImage(file=data_path('ER16.gif')) er32 = tk.PhotoImage(file=data_path('ER32.gif')) er48 = tk.PhotoImage(file=data_path('ER48.gif')) window.tk.call('wm', 'iconphoto', window._w, er16, er32, er48) # pylint: disable=protected-access
def settingToJsonMain(): web_version = '--web' in sys.argv CreateJSON(data_path('generated/settings_list.json'), web_version)
def guiMain(settings=None): frames = {} mainWindow = Tk() mainWindow.wm_title("OoT Randomizer %s" % ESVersion) mainWindow.resizable(False, False) set_icon(mainWindow) notebook = ttk.Notebook(mainWindow) frames['rom_tab'] = ttk.Frame(notebook) frames['rules_tab'] = ttk.Frame(notebook) frames['logic_tab'] = ttk.Frame(notebook) frames['other_tab'] = ttk.Frame(notebook) frames['cosmetic_tab'] = ttk.Frame(notebook) frames['SFX_tab'] = ttk.Frame(notebook) frames['cosmetic_tab_left'] = Frame(frames['cosmetic_tab']) frames['cosmetic_tab_right'] = Frame(frames['cosmetic_tab']) notebook.add(frames['rom_tab'], text='ROM Options') notebook.add(frames['rules_tab'], text='Main Rules') notebook.add(frames['logic_tab'], text='Detailed Logic') notebook.add(frames['other_tab'], text='Other') notebook.add(frames['cosmetic_tab'], text='Cosmetic') notebook.add(frames['SFX_tab'], text='SFX') ####################### # Randomizer controls # ####################### # Hold the results of the user's decisions here guivars = {} widgets = {} presets = {} # Hierarchy ############ #Rules Tab frames['open'] = LabelFrame(frames['rules_tab'], text='Open', labelanchor=NW) frames['world'] = LabelFrame(frames['rules_tab'], text='World', labelanchor=NW) frames['shuffle'] = LabelFrame(frames['rules_tab'], text='Shuffle', labelanchor=NW) # Logic tab frames['checks'] = LabelFrame(frames['logic_tab'], text='Adult Trade Sequence', labelanchor=NW) frames['tricks'] = LabelFrame(frames['logic_tab'], text='Lens of Truth', labelanchor=NW) #Other Tab frames['convenience'] = LabelFrame(frames['other_tab'], text='Timesavers', labelanchor=NW) frames['other'] = LabelFrame(frames['other_tab'], text='Misc', labelanchor=NW) #Cosmetic tab frames['cosmetic'] = LabelFrame(frames['cosmetic_tab_left'], text='General', labelanchor=NW) frames['sword_trails']= LabelFrame(frames['cosmetic_tab_left'], text='Sword Trail Colors',labelanchor=NW) frames['ui_colors']= LabelFrame(frames['cosmetic_tab_left'], text='UI Colors', labelanchor=NW) frames['tunic_colors']= LabelFrame(frames['cosmetic_tab_right'], text='Tunic Colors', labelanchor=NW) frames['navi_colors']= LabelFrame(frames['cosmetic_tab_right'], text='Navi Colors', labelanchor=NW) frames['gauntlet_colors']= LabelFrame(frames['cosmetic_tab_right'], text='Gauntlet Colors', labelanchor=NW) #Cosmetic tab frames['sfx'] = LabelFrame(frames['SFX_tab'], text='General', labelanchor=NW) frames['menu_sfx'] = LabelFrame(frames['SFX_tab'], text='Menu', labelanchor=NW) frames['npc_sfx'] = LabelFrame(frames['SFX_tab'], text='NPC', labelanchor=NW) # Shared def toggle_widget(widget, enabled): widget_type = widget.winfo_class() if widget_type == 'Frame' or widget_type == 'TFrame' or widget_type == 'Labelframe': if widget_type == 'Labelframe': widget.configure(fg='Black'if enabled else 'Grey') for child in widget.winfo_children(): toggle_widget(child, enabled) else: if widget_type == 'TCombobox': widget.configure(state= 'readonly' if enabled else 'disabled') else: widget.configure(state= 'normal' if enabled else 'disabled') if widget_type == 'Scale': widget.configure(fg='Black'if enabled else 'Grey') def show_settings(*event): settings = guivars_to_settings(guivars) settings_string_var.set( settings.get_settings_string() ) # Update any dependencies for info in setting_infos: dep_met = settings.check_dependency(info.name) if info.name in widgets: toggle_widget(widgets[info.name], dep_met) if info.type == list: widgets[info.name].delete(0, END) widgets[info.name].insert(0, *guivars[info.name]) if info.type != list and info.name in guivars and guivars[info.name].get() == 'Custom Color': color = colorchooser.askcolor() if color == (None, None): color = ((0,0,0),'#000000') guivars[info.name].set('Custom (' + color[1] + ')') if info.type != list and info.name in guivars and guivars[info.name].get() == 'Custom Navi Color': innerColor = colorchooser.askcolor(title='Pick an Inner Core color.') if innerColor == (None, None): innerColor = ((0,0,0),'#000000') outerColor = colorchooser.askcolor(title='Pick an Outer Glow color.') if outerColor == (None, None): outerColor = ((0,0,0),'#000000') guivars[info.name].set('Custom (%s %s)' % (innerColor[1], outerColor[1])) update_generation_type() versionCheckFrame = Frame(frames['rom_tab']) versionCheckFrame.pack(side=BOTTOM, anchor=NW, fill=X) fileDialogFrame = Frame(frames['rom_tab']) romDialogFrame = Frame(fileDialogFrame) baseRomLabel = Label(romDialogFrame, text='Base ROM') guivars['rom'] = StringVar(value='') romEntry = Entry(romDialogFrame, textvariable=guivars['rom'], width=50) def RomSelect(): rom = filedialog.askopenfilename(filetypes=[("ROM Files", (".z64", ".n64")), ("All Files", "*")]) if rom != '': guivars['rom'].set(rom) romSelectButton = Button(romDialogFrame, text='Browse', command=RomSelect, width=10) baseRomLabel.pack(side=LEFT, padx=(34,0)) romEntry.pack(side=LEFT, padx=3) romSelectButton.pack(side=LEFT) romDialogFrame.pack() fileDialogFrame.pack(side=TOP, anchor=W, padx=5, pady=(5,1)) def output_dir_select(): rom = filedialog.askdirectory(initialdir = default_output_path(guivars['output_dir'].get())) if rom != '': guivars['output_dir'].set(rom) outputDialogFrame = Frame(frames['rom_tab']) outputDirLabel = Label(outputDialogFrame, text='Output Directory') guivars['output_dir'] = StringVar(value='') outputDirEntry = Entry(outputDialogFrame, textvariable=guivars['output_dir'], width=50) outputDirButton = Button(outputDialogFrame, text='Browse', command=output_dir_select, width=10) outputDirLabel.pack(side=LEFT, padx=(3,0)) outputDirEntry.pack(side=LEFT, padx=3) outputDirButton.pack(side=LEFT) outputDialogFrame.pack(side=TOP, anchor=W, pady=3) distFileDialogFrame = Frame(frames['rom_tab']) distFileLabel = Label(distFileDialogFrame, text='Distribution File') guivars['distribution_file'] = StringVar(value='') distFileEntry = Entry(distFileDialogFrame, textvariable=guivars['distribution_file'], width=50) def DistFileSelect(): distFile = filedialog.askopenfilename(filetypes=[("JSON Files", (".json")), ("All Files", "*")]) if distFile != '': guivars['distribution_file'].set(distFile) distFileSelectButton = Button(distFileDialogFrame, text='Browse', command=DistFileSelect, width=10) distFileLabel.pack(side=LEFT, padx=(9,0)) distFileEntry.pack(side=LEFT, padx=3) distFileSelectButton.pack(side=LEFT) distFileDialogFrame.pack(side=TOP, anchor=W, pady=3) countDialogFrame = Frame(frames['rom_tab']) countLabel = Label(countDialogFrame, text='Generation Count') guivars['count'] = StringVar() widgets['count'] = Spinbox(countDialogFrame, from_=1, to=100, textvariable=guivars['count'], width=3) def open_readme(): open_file('https://wiki.ootrandomizer.com/index.php?title=Main_Page') openReadmeButton = Button(countDialogFrame, text='Open Wiki Page', command=open_readme) openReadmeButton.pack(side=RIGHT, padx=5) def open_output(): open_file(default_output_path(guivars['output_dir'].get())) openOutputButton = Button(countDialogFrame, text='Open Output Directory', command=open_output) openOutputButton.pack(side=RIGHT, padx=5) countLabel.pack(side=LEFT) widgets['count'].pack(side=LEFT, padx=2) countDialogFrame.pack(side=TOP, anchor=W, padx=5, pady=(1,1)) # Build gui ############ for info in setting_infos: if 'group' in info.gui_params: if info.gui_params['widget'] == 'Checkbutton': # Determine the initial value of the checkbox default_value = 1 if info.choices[info.default] == 'checked' else 0 # Create a variable to access the box's state guivars[info.name] = IntVar(value=default_value) # Create the checkbox widgets[info.name] = Checkbutton(frames[info.gui_params['group']], text=info.gui_params['text'], variable=guivars[info.name], justify=LEFT, wraplength=220, command=show_settings) widgets[info.name].pack(expand=False, anchor=W) elif info.gui_params['widget'] == 'Combobox': # Create the variable to store the user's decision guivars[info.name] = StringVar(value=info.choices[info.default]) # Create the option menu widgets[info.name] = Frame(frames[info.gui_params['group']]) dropdown = ttk.Combobox(widgets[info.name], textvariable=guivars[info.name], values=list(map(lambda choice: info.choices[choice], info.choice_list)), state='readonly', width=36) dropdown.bind("<<ComboboxSelected>>", show_settings) dropdown.pack(side=BOTTOM, anchor=W) # Label the option if 'text' in info.gui_params: label = Label(widgets[info.name], text=info.gui_params['text']) label.pack(side=LEFT, anchor=W, padx=5) # Pack the frame widgets[info.name].pack(expand=False, side=TOP, anchor=W, padx=3, pady=3) elif info.gui_params['widget'] == 'Radiobutton': # Create the variable to store the user's decision guivars[info.name] = StringVar(value=info.choices[info.default]) # Create the option menu widgets[info.name] = LabelFrame(frames[info.gui_params['group']], text=info.gui_params.get('text', info.name), labelanchor=NW) # Setup orientation side = TOP anchor = W if "horizontal" in info.gui_params and info.gui_params["horizontal"]: side = LEFT anchor = N # Add the radio buttons for option in map(lambda choice: info.choices[choice], info.choice_list): radio_button = Radiobutton(widgets[info.name], text=option, value=option, variable=guivars[info.name], justify=LEFT, wraplength=220, indicatoron=False, command=show_settings) radio_button.pack(expand=True, side=side, anchor=anchor) # Pack the frame widgets[info.name].pack(expand=False, side=TOP, anchor=W, padx=3, pady=3) elif info.gui_params['widget'] == 'Scale': # Create the variable to store the user's decision guivars[info.name] = IntVar(value=info.choices[info.default]) # Create the option menu widgets[info.name] = Frame(frames[info.gui_params['group']]) minval = info.gui_params.get('min', 0) maxval = info.gui_params.get('max', 100) stepval = info.gui_params.get('step', 1) scale = Scale(widgets[info.name], variable=guivars[info.name], from_=minval, to=maxval, tickinterval=stepval, resolution=stepval, showvalue=0, orient=HORIZONTAL, sliderlength=15, length=235, command=show_settings) scale.pack(side=BOTTOM, anchor=W) # Label the option if 'text' in info.gui_params: label = Label(widgets[info.name], text=info.gui_params['text']) label.pack(side=LEFT, anchor=W, padx=5) # Pack the frame widgets[info.name].pack(expand=False, side=TOP, anchor=W, padx=3, pady=3) elif info.gui_params['widget'] == 'Entry': # Create the variable to store the user's decision guivars[info.name] = StringVar(value=info.default) # Create the option menu widgets[info.name] = Frame(frames[info.gui_params['group']]) if 'validate' in info.gui_params: entry = ValidatingEntry(widgets[info.name], command=show_settings, validate=info.gui_params['validate'], textvariable=guivars[info.name], width=35) else: entry = Entry(widgets[info.name], textvariable=guivars[info.name], width=36) entry.pack(side=BOTTOM, anchor=W) # Label the option if 'text' in info.gui_params: label = Label(widgets[info.name], text=info.gui_params['text']) label.pack(side=LEFT, anchor=W, padx=5) # Pack the frame widgets[info.name].pack(expand=False, side=TOP, anchor=W, padx=3, pady=3) elif info.gui_params['widget'] == 'SearchBox' or info.gui_params['widget'] == 'FilteredSearchBox': filtered = (info.gui_params['widget'] == 'FilteredSearchBox') search_frame = LabelFrame(frames[info.gui_params['group']], text=info.gui_params.get('text', info.name), labelanchor=NW) if filtered: filter_frame = Frame(search_frame) widgets[info.name + '_filterlabel'] = Label(filter_frame, text="Filter: ") widgets[info.name + '_filterlabel'].pack(side=LEFT, anchor=W) widgets[info.name + '_entry'] = SearchBox(search_frame, list(map(lambda choice: info.choices[choice], info.choice_list)), width=78) widgets[info.name + '_filter'] = SearchBoxFilterControl(filter_frame, widgets[info.name + '_entry'], info.gui_params['filterdata'], width=50) widgets[info.name + '_filter'].pack(expand=False, side=LEFT, anchor=W) filter_frame.pack(expand=False, side=TOP, anchor=W, padx=3, pady=3) widgets[info.name + '_entry'].pack(expand=False, side=TOP, anchor=W) else: widgets[info.name + '_entry'] = SearchBox(search_frame, list(map(lambda choice: info.choices[choice], info.choice_list)), width=78) widgets[info.name + '_entry'].pack(expand=False, side=TOP, anchor=W, padx=3, pady=3) list_frame = Frame(search_frame) scrollbar = Scrollbar(list_frame, orient=VERTICAL) widgets[info.name] = Listbox(list_frame, width=78, height=7, yscrollcommand=scrollbar.set) guivars[info.name] = list(info.default) scrollbar.config(command=widgets[info.name].yview) scrollbar.pack(side=RIGHT, fill=Y) widgets[info.name].pack(side=LEFT) list_frame.pack(expand=False, side=TOP, anchor=W, padx=3, pady=3) if 'entry_tooltip' in info.gui_params: ToolTips.register(widgets[info.name + '_entry'], info.gui_params['entry_tooltip']) if filtered: ToolTips.register(widgets[info.name + '_filter'], info.gui_params['entry_tooltip']) if 'list_tooltip' in info.gui_params: ToolTips.register(widgets[info.name], info.gui_params['list_tooltip']) def get_lambda(function, *args): return lambda: function(*args) def add_list_selected(name): new_location = widgets[name +'_entry'].get() if new_location in widgets[name +'_entry'].options and new_location not in widgets[name].get(0, END): widgets[name].insert(END, new_location) guivars[name].append(new_location) show_settings() def remove_list_selected(name): location = widgets[name].get(ACTIVE) widgets[name].delete(ACTIVE) guivars[name].remove(location) show_settings() def add_list_all(name): for new_location in widgets[name + '_entry'].options: if new_location not in widgets[name].get(0, END): widgets[name].insert(END, new_location) guivars[name].append(new_location) show_settings() def remove_list_all(name): items = list(widgets[name].get(0, END)) widgets[name].delete(0, END) guivars[name] = [] for item in (x for x in items if x not in widgets[name + '_entry'].options): widgets[name].insert(END, item) guivars[name].append(item) show_settings() def clear_list_all(name): widgets[name].delete(0, END) guivars[name] = [] show_settings() list_button_frame = Frame(search_frame) list_add = Button(list_button_frame, width=10, text='Add', command=get_lambda(add_list_selected, info.name)) list_add.pack(side=LEFT, anchor=N, padx=3, pady=3) list_remove = Button(list_button_frame, width=10, text='Remove', command=get_lambda(remove_list_selected, info.name)) list_remove.pack(side=LEFT, anchor=N, padx=3, pady=3) list_add = Button(list_button_frame, width=10, text='All', command=get_lambda(add_list_all, info.name)) list_add.pack(side=LEFT, anchor=N, padx=3, pady=3) list_remove = Button(list_button_frame, width=10, text='None', command=get_lambda(remove_list_all, info.name)) list_remove.pack(side=LEFT, anchor=N, padx=3, pady=3) if filtered: list_clear = Button(list_button_frame, width=10, text='Clear', command=get_lambda(clear_list_all, info.name)) list_clear.pack(side=LEFT, anchor=N, padx=3, pady=3) list_button_frame.pack(expand=False, side=TOP, padx=3, pady=3) # pack the frame search_frame.pack(expand=False, side=TOP, anchor=W, padx=3, pady=3) if 'tooltip' in info.gui_params: ToolTips.register(widgets[info.name], info.gui_params['tooltip']) # Pack the hierarchy frames['shuffle'].pack(fill=BOTH, expand=True, anchor=N, side=RIGHT, pady=(5,1)) frames['open'].pack( fill=BOTH, expand=True, anchor=W, side=TOP, pady=(5,1)) frames['world'].pack( fill=BOTH, expand=True, anchor=W, side=BOTTOM, pady=(5,1)) # Logic tab frames['checks'].pack(fill=BOTH, expand=True, anchor=N, side=LEFT, pady=(5,1)) frames['tricks'].pack(fill=BOTH, expand=True, anchor=N, side=LEFT, pady=(5,1)) # Other Tab frames['convenience'].pack(fill=BOTH, expand=True, anchor=N, side=LEFT, pady=(5,1)) frames['other'].pack( fill=BOTH, expand=True, anchor=N, side=LEFT, pady=(5,1)) # Cosmetics tab frames['cosmetic'].pack( fill=BOTH, expand=True, anchor=W, side=TOP) frames['cosmetic_tab_left'].pack( fill=BOTH, expand=True, anchor=W, side=LEFT) frames['cosmetic_tab_right'].pack(fill=BOTH, expand=True, anchor=W, side=RIGHT) # Cosmetics tab - Left Side frames['sword_trails'].pack( fill=BOTH, expand=True, anchor=W, side=TOP) frames['ui_colors'].pack( fill=BOTH, expand=True, anchor=W, side=BOTTOM) # Cosmetics tab - Right Side frames['tunic_colors'].pack(fill=BOTH, expand=True, anchor=N, side=TOP) frames['navi_colors'].pack( fill=BOTH, expand=True, anchor=W, side=TOP) frames['gauntlet_colors'].pack(fill=BOTH, expand=True, anchor=W, side=BOTTOM) #SFX tab frames['sfx'].pack( fill=BOTH, expand=True, anchor=N, side=LEFT, pady=(5,1)) frames['menu_sfx'].pack( fill=BOTH, expand=False, anchor=W, side=TOP, pady=(5,1)) frames['npc_sfx'].pack(fill=BOTH, expand=True, anchor=W, side=BOTTOM, pady=(5,1)) notebook.pack(fill=BOTH, expand=True, padx=5, pady=5) #Multi-World widgets['multiworld'] = LabelFrame(frames['rom_tab'], text='Multi-World Generation') countLabel = Label( widgets['multiworld'], wraplength=250, justify=LEFT, text='''This is used for co-op generations. \ Increasing Player Count will drastically \ increase the generation time. \ \nFor more information, see: \ ''' ) hyperLabel = Label(widgets['multiworld'], wraplength=250, justify=LEFT, text='https://github.com/TestRunnerSRL/\nbizhawk-co-op\n', fg='blue', cursor='hand2') hyperLabel.bind("<Button-1>", lambda event: webbrowser.open_new(r"https://github.com/TestRunnerSRL/bizhawk-co-op")) countLabel.pack(side=TOP, anchor=W, padx=5, pady=0) hyperLabel.pack(side=TOP, anchor=W, padx=5, pady=0) worldCountFrame = Frame(widgets['multiworld']) countLabel = Label(worldCountFrame, text='Player Count') guivars['world_count'] = StringVar() widgets['world_count'] = Spinbox(worldCountFrame, from_=1, to=255, textvariable=guivars['world_count'], width=3) guivars['world_count'].trace('w', show_settings) countLabel.pack(side=LEFT) widgets['world_count'].pack(side=LEFT, padx=2) worldCountFrame.pack(side=LEFT, anchor=N, padx=10, pady=(1,5)) playerNumFrame = Frame(widgets['multiworld']) countLabel = Label(playerNumFrame, text='Player ID') guivars['player_num'] = StringVar() widgets['player_num'] = Spinbox(playerNumFrame, from_=1, to=255, textvariable=guivars['player_num'], width=3) countLabel.pack(side=LEFT) widgets['player_num'].pack(side=LEFT, padx=2) ToolTips.register(widgets['player_num'], 'Generate for specific Player.') playerNumFrame.pack(side=LEFT, anchor=N, padx=10, pady=(1,5)) widgets['multiworld'].pack(side=LEFT, fill=BOTH, anchor=NW, padx=5, pady=5) # Settings Presets Functions def import_setting_preset(): if guivars['settings_preset'].get() == '[New Preset]': messagebox.showerror("Invalid Preset", "You must select an existing preset!") return # Get cosmetic settings old_settings = guivars_to_settings(guivars) new_settings = {setting.name: old_settings.__dict__[setting.name] for setting in filter(lambda s: not (s.shared and s.bitwidth > 0), setting_infos)} preset = presets[guivars['settings_preset'].get()] new_settings.update(preset) settings = Settings(new_settings) settings.seed = guivars['seed'].get() settings_to_guivars(settings, guivars) show_settings() def add_settings_preset(): preset_name = guivars['settings_preset'].get() if preset_name == '[New Preset]': preset_name = simpledialog.askstring("New Preset", "Enter a new preset name:") if not preset_name or preset_name in presets or preset_name == '[New Preset]': messagebox.showerror("Invalid Preset", "You must enter a new preset name!") return elif presets[preset_name].get('locked', False): messagebox.showerror("Invalid Preset", "You cannot modify a locked preset!") return else: if messagebox.askquestion("Overwrite Preset", 'Are you sure you want to overwrite the "%s" preset?' % preset_name) != 'yes': return settings = guivars_to_settings(guivars) preset = {setting.name: settings.__dict__[setting.name] for setting in filter(lambda s: s.shared and s.bitwidth > 0, setting_infos)} presets[preset_name] = preset guivars['settings_preset'].set(preset_name) update_preset_dropdown() save_presets() def remove_setting_preset(): preset_name = guivars['settings_preset'].get() if preset_name == '[New Preset]': messagebox.showerror("Invalid Preset", "You must select an existing preset!") return elif presets[preset_name].get('locked', False): messagebox.showerror("Invalid Preset", "You cannot modify a locked preset!") return confirm = messagebox.askquestion('Remove Setting Preset', 'Are you sure you want to remove the setting preset "%s"?' % preset_name) if confirm != 'yes': return del presets[preset_name] guivars['settings_preset'].set('[New Preset]') update_preset_dropdown() save_presets() def update_preset_dropdown(): widgets['settings_preset']['values'] = ['[New Preset]'] + list(presets.keys()) def save_presets(): presets_file = local_path('presets.sav') with open(presets_file, 'w') as outfile: preset_json = {name: preset for name,preset in presets.items() if not preset.get('locked')} json.dump(preset_json, outfile, indent=4) # Settings Presets widgets['settings_presets'] = LabelFrame(frames['rom_tab'], text='Settings Presets') countLabel = Label( widgets['settings_presets'], wraplength=250, justify=LEFT, text='''Presets are settings that can be saved\ and loaded from. Loading a preset\ will overwrite all settings that affect\ the seed.\ \n\ ''' ) countLabel.pack(side=TOP, anchor=W, padx=5, pady=0) selectPresetFrame = Frame(widgets['settings_presets']) guivars['settings_preset'] = StringVar(value='[New Preset]') widgets['settings_preset'] = ttk.Combobox(selectPresetFrame, textvariable=guivars['settings_preset'], values=['[New Preset]'], state='readonly', width=33) widgets['settings_preset'].pack(side=BOTTOM, anchor=W) ToolTips.register(widgets['settings_preset'], 'Select a setting preset to apply.') widgets['settings_preset'].pack(side=LEFT, padx=(5, 0)) selectPresetFrame.pack(side=TOP, anchor=W, padx=5, pady=(1,5)) buttonPresetFrame = Frame(widgets['settings_presets']) importPresetButton = Button(buttonPresetFrame, text='Load', width=9, command=import_setting_preset) addPresetButton = Button(buttonPresetFrame, text='Save', width=9, command=add_settings_preset) removePresetButton = Button(buttonPresetFrame, text='Remove', width=9, command=remove_setting_preset) importPresetButton.pack(side=LEFT, anchor=W, padx=2) addPresetButton.pack(side=LEFT, anchor=W, padx=2) removePresetButton.pack(side=LEFT, anchor=W, padx=2) buttonPresetFrame.pack(side=TOP, anchor=W, padx=5, pady=(1,5)) widgets['settings_presets'].pack(side=RIGHT, fill=BOTH, anchor=NW, padx=5, pady=5) # Create the generation menu def update_generation_type(event=None): settings = guivars_to_settings(guivars) if generation_notebook.tab(generation_notebook.select())['text'] == 'Generate From Seed': notebook.tab(1, state="normal") if guivars['logic_rules'].get() == 'Glitchless': notebook.tab(2, state="normal") else: notebook.tab(2, state="disabled") notebook.tab(3, state="normal") notebook.tab(4, state="normal") notebook.tab(5, state="normal") toggle_widget(widgets['world_count'], settings.check_dependency('world_count')) toggle_widget(widgets['create_spoiler'], settings.check_dependency('create_spoiler')) toggle_widget(widgets['count'], settings.check_dependency('count')) toggle_widget(widgets['settings_presets'], True) else: notebook.tab(1, state="disabled") notebook.tab(2, state="disabled") notebook.tab(3, state="disabled") if guivars['repatch_cosmetics'].get(): notebook.tab(4, state="normal") notebook.tab(5, state="normal") toggle_widget(widgets['create_cosmetics_log'], settings.check_dependency('create_cosmetics_log')) else: notebook.tab(4, state="disabled") notebook.tab(5, state="disabled") toggle_widget(widgets['create_cosmetics_log'], False) toggle_widget(widgets['world_count'], False) toggle_widget(widgets['create_spoiler'], False) toggle_widget(widgets['count'], False) toggle_widget(widgets['settings_presets'], False) generation_notebook = ttk.Notebook(mainWindow) frames['gen_from_seed'] = ttk.Frame(generation_notebook) frames['gen_from_file'] = ttk.Frame(generation_notebook) generation_notebook.add(frames['gen_from_seed'], text='Generate From Seed') generation_notebook.add(frames['gen_from_file'], text='Generate From File') generation_notebook.bind("<<NotebookTabChanged>>", show_settings) # From seed tab def import_settings(event=None): try: settings = guivars_to_settings(guivars) text = settings_string_var.get().upper() settings.seed = guivars['seed'].get() settings.update_with_settings_string(text) settings_to_guivars(settings, guivars) show_settings() except Exception as e: messagebox.showerror(title="Error", message="Invalid settings string") def copy_settings(event=None): mainWindow.clipboard_clear() new_clip = settings_string_var.get().upper() mainWindow.clipboard_append(new_clip) mainWindow.update() settingsFrame = Frame(frames['gen_from_seed']) settings_string_var = StringVar() widgets['setting_string'] = Entry(settingsFrame, textvariable=settings_string_var, width=44) label = Label(settingsFrame, text="Settings String", width=13, anchor=E) widgets['copy_settings'] = Button(settingsFrame, text='Copy', width=5, command=copy_settings) widgets['import_settings'] = Button(settingsFrame, text='Import', width=7, command=import_settings) label.pack(side=LEFT, anchor=W, padx=5) widgets['setting_string'].pack(side=LEFT, anchor=W) widgets['copy_settings'].pack(side=LEFT, anchor=W, padx=(5, 0)) widgets['import_settings'].pack(side=LEFT, anchor=W) settingsFrame.pack(fill=BOTH, anchor=W, padx=5, pady=(10,0)) def multiple_run(settings, window): orig_seed = settings.seed for i in range(settings.count): settings.update_seed(orig_seed + '-' + str(i)) window.update_title("Generating Seed %s...%d/%d" % (settings.seed, i+1, settings.count)) main(settings, window) def generateRom(): settings = guivars_to_settings(guivars) if settings.count: BackgroundTaskProgress(mainWindow, "Generating Seed %s..." % settings.seed, multiple_run, settings) else: BackgroundTaskProgress(mainWindow, "Generating Seed %s..." % settings.seed, main, settings) generateSeedFrame = Frame(frames['gen_from_seed']) seedLabel = Label(generateSeedFrame, text='Seed', width=13, anchor=E) generateButton = Button(generateSeedFrame, text='Generate!', width=14, command=generateRom) guivars['seed'] = StringVar() widgets['seed'] = Entry(generateSeedFrame, textvariable=guivars['seed'], width=44) seedLabel.pack(side=LEFT, padx=5) widgets['seed'].pack(side=LEFT) generateButton.pack(side=LEFT, padx=(5, 0)) generateSeedFrame.pack(side=BOTTOM, anchor=W, padx=5, pady=10) # From file tab patchDialogFrame = Frame(frames['gen_from_file']) patchFileLabel = Label(patchDialogFrame, text='Patch File') guivars['patch_file'] = StringVar(value='') patchEntry = Entry(patchDialogFrame, textvariable=guivars['patch_file'], width=52) def PatchSelect(): patch_file = filedialog.askopenfilename(filetypes=[("Patch File Archive", "*.zpfz *.zpf"), ("All Files", "*")]) if patch_file != '': guivars['patch_file'].set(patch_file) patchSelectButton = Button(patchDialogFrame, text='Select File', command=PatchSelect, width=14) patchFileLabel.pack(side=LEFT, padx=(5,0)) patchEntry.pack(side=LEFT, padx=3) patchSelectButton.pack(side=LEFT) patchDialogFrame.pack(side=TOP, anchor=W, padx=5, pady=(10,5)) def generateFromFile(): settings = guivars_to_settings(guivars) BackgroundTaskProgress(mainWindow, "Generating From File %s..." % os.path.basename(settings.patch_file), from_patch_file, settings) patchCosmeticsAndGenerateFrame = Frame(frames['gen_from_file']) guivars['repatch_cosmetics'] = IntVar() widgets['repatch_cosmetics'] = Checkbutton(patchCosmeticsAndGenerateFrame, text='Update Cosmetics', variable=guivars['repatch_cosmetics'], justify=LEFT, wraplength=220, command=show_settings) widgets['repatch_cosmetics'].pack(side=LEFT, padx=5, anchor=W) generateFileButton = Button(patchCosmeticsAndGenerateFrame, text='Generate!', width=14, command=generateFromFile) generateFileButton.pack(side=RIGHT, anchor=E) patchCosmeticsAndGenerateFrame.pack(side=BOTTOM, fill=BOTH, expand=True, pady=(0,10), padx=(0, 10)) generation_notebook.pack(fill=BOTH, expand=True, padx=5, pady=5) guivars['checked_version'] = StringVar() guivars['cosmetics_only'] = IntVar() if settings is not None: # Load values from commandline args settings_to_guivars(settings, guivars) else: # Try to load saved settings settingsFile = local_path('settings.sav') try: with open(settingsFile) as f: settings = Settings( json.load(f) ) except: settings = Settings({}) settings_to_guivars(settings, guivars) guivars['seed'].set("") presets = {} for file in [data_path('presets_default.json')] \ + [local_path(f) for f in os.listdir(local_path()) if f.startswith('presets_') and f.endswith('.sav')] \ + [local_path('presets.sav')]: try: with open(file) as f: presets_temp = json.load(f) if file != local_path('presets.sav'): for preset in presets_temp.values(): preset['locked'] = True presets.update(presets_temp) except: pass update_preset_dropdown() show_settings() def gui_check_version(): task = BackgroundTask(mainWindow, check_version, guivars['checked_version'].get()) while task.running: mainWindow.update() if task.status: versionCheckLabel = LabelFrame(versionCheckFrame, text="New Version Available!") versionCheckText = Label(versionCheckLabel, justify=LEFT, text=task.status[(task.status.find(' ')+1):]) versionCheckLink = Label(versionCheckLabel, justify=LEFT, text='Click here and download the current version.', fg='blue', cursor='hand2') versionCheckLink.bind("<Button-1>", lambda event: webbrowser.open_new(r"https://github.com/TestRunnerSRL/OoT-Randomizer/tree/Dev")) versionCheckText.pack(anchor=NW, padx=5, pady=0) versionCheckLink.pack(anchor=NW, padx=5, pady=0) versionCheckLabel.pack(anchor=NW, fill=X, expand="yes", padx=5, pady=5) mainWindow.after(1000, gui_check_version) mainWindow.mainloop() # Save settings on close settings_file = local_path('settings.sav') with open(settings_file, 'w') as outfile: settings = guivars_to_settings(guivars) del settings.__dict__["distribution"] del settings.__dict__["seed"] del settings.__dict__["numeric_seed"] del settings.__dict__["check_version"] if "locked" in settings.__dict__: del settings.__dict__["locked"] json.dump(settings.__dict__, outfile, indent=4) save_presets()
def get_preset_files(): return [data_path('presets_default.json')] + sorted( os.path.join(data_path('Presets'), fn) for fn in os.listdir(data_path('Presets')) if fn.endswith('.json'))