import argparse import copy import json import os import textwrap import shlex import sys from source.classes.BabelFish import BabelFish from Utils import update_deprecated_args from source.classes.CustomSettings import CustomSettings class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter): def _get_help_string(self, action): return textwrap.dedent(action.help) def parse_cli(argv, no_defaults=False): def defval(value): return value if not no_defaults else None # get settings settings = parse_settings() lang = "en" fish = BabelFish(lang=lang) # we need to know how many players we have first # also if we're loading our own settings file, we should do that now parser = argparse.ArgumentParser(add_help=False) parser.add_argument('--settingsfile', help="input json file of settings", type=str) parser.add_argument('--multi', default=defval(settings["multi"]), type=lambda value: min(max(int(value), 1), 255)) parser.add_argument('--customizer', help='input yaml file for customizations', type=str) parser.add_argument('--print_template_yaml', help='print example yaml for current settings', default=False, action="store_true") parser.add_argument('--print_custom_yaml', help='print example plando yaml for current settings and placements', default=False, action="store_true") parser.add_argument('--mystery', dest="mystery", default=False, action="store_true") multiargs, _ = parser.parse_known_args(argv) if multiargs.settingsfile: settings = apply_settings_file(settings, multiargs.settingsfile) player_num = multiargs.multi if multiargs.customizer: custom = CustomSettings() custom.load_yaml(multiargs.customizer) cp = custom.determine_players() if cp: player_num = cp parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) # get args args = [] with open(os.path.join("resources", "app", "cli", "args.json")) as argsFile: args = json.load(argsFile) for arg in args: argdata = args[arg] argname = "--" + arg argatts = {} argatts["help"] = "(default: %(default)s)" if "action" in argdata: argatts["action"] = argdata["action"] if "choices" in argdata: argatts["choices"] = argdata["choices"] argatts["const"] = argdata["choices"][0] argatts["default"] = argdata["choices"][0] argatts["nargs"] = "?" if arg in settings: default = settings[arg] if "type" in argdata and argdata["type"] == "bool": default = settings[arg] != 0 argatts["default"] = defval(default) arghelp = fish.translate("cli", "help", arg) if "help" in argdata and argdata["help"] == "suppress": argatts["help"] = argparse.SUPPRESS elif not isinstance(arghelp, str): argatts["help"] = '\n'.join(arghelp).replace("\\'", "'") else: argatts["help"] = arghelp + " " + argatts["help"] parser.add_argument(argname, **argatts) parser.add_argument('--seed', default=defval(int(settings["seed"]) if settings["seed"] != "" and settings["seed"] is not None else None), help="\n".join(fish.translate("cli", "help", "seed")), type=int) parser.add_argument('--count', default=defval(int(settings["count"]) if settings["count"] != "" and settings["count"] is not None else 1), help="\n".join(fish.translate("cli", "help", "count")), type=int) parser.add_argument('--tries', default=defval(int(settings["tries"]) if settings["tries"] != "" and settings["tries"] is not None else 1), help="\n".join(fish.translate("cli", "help", "tries")), type=int) parser.add_argument('--customitemarray', default={}, help=argparse.SUPPRESS) # included for backwards compatibility parser.add_argument('--multi', default=defval(settings["multi"]), type=lambda value: min(max(int(value), 1), 255)) parser.add_argument('--securerandom', default=defval(settings["securerandom"]), action='store_true') parser.add_argument('--teams', default=defval(1), type=lambda value: max(int(value), 1)) parser.add_argument('--settingsfile', dest="filename", help="input json file of settings", type=str) parser.add_argument('--customizer', dest="customizer", help='input yaml file for customizations', type=str) parser.add_argument('--print_template_yaml', dest="print_template_yaml", default=False, action="store_true") parser.add_argument('--print_custom_yaml', dest="print_custom_yaml", default=False, action="store_true") if player_num: for player in range(1, player_num + 1): parser.add_argument(f'--p{player}', default=defval(''), help=argparse.SUPPRESS) ret = parser.parse_args(argv) if ret.keysanity: ret.mapshuffle, ret.compassshuffle, ret.keyshuffle, ret.bigkeyshuffle = 'wild' * 4 if ret.keydropshuffle: ret.dropshuffle = 'keys' if ret.dropshuffle == 'none' else ret.dropshuffle ret.pottery = 'keys' if ret.pottery == 'none' else ret.pottery if ret.retro or ret.mode == 'retro': if ret.bow_mode == 'progressive': ret.bow_mode = 'retro' elif ret.bow_mode == 'silvers': ret.bow_mode = 'retro_silvers' ret.take_any = 'random' if ret.take_any == 'none' else ret.take_any ret.keyshuffle = 'universal' if player_num: defaults = copy.deepcopy(ret) for player in range(1, player_num + 1): playerargs = parse_cli(shlex.split(getattr(ret, f"p{player}")), True) if playerargs.filename: playersettings = apply_settings_file({}, playerargs.filename) for k, v in playersettings.items(): setattr(playerargs, k, v) for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', 'ow_shuffle', 'ow_terrain', 'ow_crossed', 'ow_keepsimilar', 'ow_mixed', 'ow_whirlpool', 'ow_fluteshuffle', 'flute_mode', 'bow_mode', 'take_any', 'boots_hint', 'shuffle_followers', 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'ganon_item', 'openpyramid', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'prizeshuffle', 'startinventory', 'usestartinventory', 'bombbag', 'shuffleganon', 'overworld_map', 'restrict_boss_items', 'triforce_max_difference', 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max', 'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'shuffletavern', 'skullwoods', 'linked_drops', 'pseudoboots', 'mirrorscroll', 'dark_rooms', 'damage_challenge', 'shuffle_damage_table', 'crystal_book', 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters', 'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots', 'ow_palettes', 'uw_palettes', 'sprite', 'triforce_gfx', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep', 'remote_items', 'shopsanity', 'dropshuffle', 'pottery', 'keydropshuffle', 'mixed_travel', 'standardize_palettes', 'code', 'reduce_flashing', 'shuffle_sfx', 'shuffle_sfxinstruments', 'shuffle_songinstruments', 'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors', 'door_type_mode', 'bonk_drops', 'trap_door_mode', 'key_logic_algorithm', 'door_self_loops', 'any_enemy_logic', 'aga_randomness', 'money_balance']: value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) if player == 1: setattr(ret, name, {1: value}) else: getattr(ret, name)[player] = value return ret def apply_settings_file(settings, settings_path): if os.path.exists(settings_path): with open(settings_path) as json_file: data = json.load(json_file) for k, v in data.items(): settings[k] = v return settings def parse_settings(): # set default settings settings = { "lang": "en", "retro": False, "bombbag": False, "mode": "open", "boots_hint": False, "logic": "noglitches", "goal": "ganon", "crystals_gt": "7", "crystals_ganon": "7", "ganon_item": "silver", "swords": "random", "flute_mode": "normal", "bow_mode": "progressive", "difficulty": "normal", "item_functionality": "normal", "timer": "none", "progressive": "on", "accessibility": "items", "algorithm": "balanced", "mystery": False, "suppress_meta": False, "restrict_boss_items": "none", # Shuffle Ganon defaults to TRUE "openpyramid": "auto", "shuffleganon": True, "ow_shuffle": "vanilla", "ow_terrain": False, "ow_crossed": "none", "ow_keepsimilar": False, "ow_mixed": False, "ow_whirlpool": False, "ow_fluteshuffle": "vanilla", "shuffle_followers": False, "bonk_drops": False, "shuffle": "vanilla", "shufflelinks": False, "shuffletavern": True, 'skullwoods': 'original', 'linked_drops': 'unset', "overworld_map": "default", "take_any": "none", "pseudoboots": False, "mirrorscroll": False, "dark_rooms": "require_lamp", "damage_challenge": "normal", "shuffle_damage_table": "vanilla", "crystal_book": False, "shuffleenemies": "none", "shufflebosses": "none", "enemy_damage": "default", "enemy_health": "default", 'any_enemy_logic': 'allow_all', "shopsanity": False, "keydropshuffle": False, "dropshuffle": "none", "pottery": "none", "colorizepots": True, "shufflepots": False, "mapshuffle": "none", "compassshuffle": "none", "keyshuffle": "none", "bigkeyshuffle": "none", "prizeshuffle": "none", "keysanity": False, "door_shuffle": "vanilla", "intensity": 3, "door_type_mode": "original", "trap_door_mode": "optional", "key_logic_algorithm": "partial", "decoupledoors": False, "door_self_loops": False, "experimental": False, "dungeon_counters": "default", "mixed_travel": "prevent", "standardize_palettes": "standardize", 'aga_randomness': True, 'money_balance': 100, "triforce_pool": 0, "triforce_goal": 0, "triforce_pool_min": 0, "triforce_pool_max": 0, "triforce_goal_min": 0, "triforce_goal_max": 0, "triforce_min_difference": 0, "triforce_max_difference": 10000, "code": "", "multi": 1, "names": "", "securerandom": False, "hints": False, "disablemusic": False, "quickswap": False, "heartcolor": "red", "heartbeep": "normal", "sprite": None, "triforce_gfx": None, "fastmenu": "normal", "ow_palettes": "default", "uw_palettes": "default", "reduce_flashing": False, "shuffle_sfx": False, "shuffle_sfxinstruments": False, "shuffle_songinstruments": False, "msu_resume": False, "collection_rate": False, 'spoiler': 'full', # Playthrough defaults to TRUE # ROM defaults to TRUE "calc_playthrough": True, "create_rom": True, "bps": False, "usestartinventory": False, "custom": False, "rom": os.path.join(".", "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"), "patch": os.path.join(".", "Patch File.bps"), "seed": "", "count": 1, "tries": 1, "startinventory": "", 'beemizer': '0', "remote_items": False, "race": False, "customitemarray": { "bow": 0, "progressivebow": 2, "boomerang": 1, "redmerang": 1, "hookshot": 1, "mushroom": 1, "powder": 1, "firerod": 1, "icerod": 1, "bombos": 1, "ether": 1, "quake": 1, "lamp": 1, "hammer": 1, "shovel": 1, "flute": 1, "bugnet": 1, "book": 1, "bottle": 4, "somaria": 1, "byrna": 1, "cape": 1, "mirror": 1, "boots": 1, "powerglove": 0, "titansmitt": 0, "progressiveglove": 2, "flippers": 1, "pearl": 1, "heartpiece": 24, "heartcontainer": 10, "sancheart": 1, "sword1": 0, "sword2": 0, "sword3": 0, "sword4": 0, "progressivesword": 4, "shield1": 0, "shield2": 0, "shield3": 0, "progressiveshield": 3, "mail2": 0, "mail3": 0, "progressivemail": 2, "halfmagic": 1, "quartermagic": 0, "bombsplus5": 0, "bombsplus10": 0, "arrowsplus5": 0, "arrowsplus10": 0, "arrow1": 1, "arrow10": 12, "bomb1": 0, "bomb3": 16, "bomb10": 1, "rupee1": 2, "rupee5": 4, "rupee20": 28, "rupee50": 7, "rupee100": 1, "rupee300": 5, "blueclock": 0, "greenclock": 0, "redclock": 0, "silversupgrade": 0, "generickeys": 0, "triforcepieces": 0, "triforcepiecesgoal": 0, "triforce": 0, "rupoor": 0, "rupoorcost": 10 }, "randomSprite": False, "outputpath": os.path.join("."), "settingsonload": "saved", "outputname": "", "startinventoryarray": {}, "notes": "" } # read saved settings file if it exists and set these settings_path = os.path.join(".", "resources", "user", "settings.json") settings = apply_settings_file(settings, settings_path) if settings["settingsonload"] == "saved": settings_path = os.path.join(".", "resources", "user", "saved.json") settings = apply_settings_file(settings, settings_path) elif settings["settingsonload"] == "lastused": settings_path = os.path.join(".", "resources", "user", "last.json") settings = apply_settings_file(settings, settings_path) return settings # Priority fallback is: # 1: CLI # 2: Settings file(s) # 3: Canned defaults def get_args_priority(settings_args, gui_args, cli_args): args = {} args["settings"] = parse_settings() if settings_args is None else settings_args args["gui"] = gui_args args["cli"] = cli_args args["load"] = args["settings"] if args["gui"] is not None: for k in args["gui"]: if k not in args["load"] or args["load"][k] != args["gui"]: args["load"][k] = args["gui"][k] if args["cli"] is None: args["cli"] = {} cli = vars(parse_cli(None)) for k, v in cli.items(): if isinstance(v, dict) and 1 in v: args["cli"][k] = v[1] else: args["cli"][k] = v args["cli"] = argparse.Namespace(**args["cli"]) cli = vars(args["cli"]) for k in vars(args["cli"]): load_doesnt_have_key = k not in args["load"] cli_val = cli[k] if isinstance(cli_val, dict) and 1 in cli_val: cli_val = cli_val[1] different_val = (k in args["load"] and k in cli) and (str(args["load"][k]) != str(cli_val)) cli_has_empty_dict = k in cli and isinstance(cli_val, dict) and len(cli_val) == 0 if load_doesnt_have_key or different_val: if not cli_has_empty_dict: args["load"][k] = cli_val newArgs = {} for key in ["settings", "gui", "cli", "load"]: if args[key]: if isinstance(args[key], dict): newArgs[key] = argparse.Namespace(**args[key]) else: newArgs[key] = args[key] else: newArgs[key] = args[key] newArgs[key] = update_deprecated_args(newArgs[key]) args = newArgs return args