449 lines
17 KiB
Python
449 lines
17 KiB
Python
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
|