diff --git a/MultiMystery.py b/MultiMystery.py index 4587bde0..c658dfe3 100644 --- a/MultiMystery.py +++ b/MultiMystery.py @@ -40,6 +40,7 @@ if __name__ == "__main__": zip_spoiler = multi_mystery_options["zip_spoiler"] zip_multidata = multi_mystery_options["zip_multidata"] player_name = multi_mystery_options["player_name"] + meta_file_path = multi_mystery_options["meta_file_path"] py_version = f"{sys.version_info.major}.{sys.version_info.minor}" @@ -51,7 +52,8 @@ if __name__ == "__main__": player_files = [] os.makedirs(player_files_path, exist_ok=True) for file in os.listdir(player_files_path): - if file.lower().endswith(".yaml"): + lfile = file.lower() + if lfile.endswith(".yaml") and lfile != meta_file_path.lower(): player_files.append(file) print(f"Player {file[:-5]} found.") player_count = len(player_files) @@ -75,7 +77,15 @@ if __name__ == "__main__": command = f"{basemysterycommand} --multi {len(player_files)} {player_string} " \ f"--names {','.join(player_names)} --enemizercli {enemizer_path} " \ - f"--outputpath {output_path}" + " --create_spoiler" if create_spoiler else "" + " --race" if race else "" + f"--outputpath {output_path}" + + if create_spoiler: + command += " --create_spoiler" + if race: + command += " --race" + if os.path.exists(os.path.join(player_files_path, meta_file_path)): + command += f" --meta {os.path.join(player_files_path, meta_file_path)}" + print(command) import time start = time.perf_counter() diff --git a/MultiServer.py b/MultiServer.py index d5d4bd6f..d23ccabe 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -448,7 +448,7 @@ async def process_client_cmd(ctx: Context, client: Client, cmd, args): def set_password(ctx : Context, password): ctx.password = password - logging.warning('Password set to ' + password if password is not None else 'Password disabled') + logging.warning('Password set to ' + password if password else 'Password disabled') async def console(ctx : Context): while True: diff --git a/Mystery.py b/Mystery.py index 31d1c6ec..5c5f57f7 100644 --- a/Mystery.py +++ b/Mystery.py @@ -3,7 +3,7 @@ import logging import random import urllib.request import urllib.parse -import functools +import typing import os import ModuleUpdate @@ -36,6 +36,8 @@ def main(): parser.add_argument('--enemizercli') parser.add_argument('--outputpath') parser.add_argument('--race', action='store_true') + parser.add_argument('--meta', default=None) + for player in range(1, multiargs.multi + 1): parser.add_argument(f'--p{player}', help=argparse.SUPPRESS) args = parser.parse_args() @@ -47,23 +49,34 @@ def main(): seed = args.seed random.seed(seed) - seedname = f'M{random.randint(0, 999999999)}' + seedname = "M"+(f"{random.randint(0, 999999999)}".zfill(9)) print(f"Generating mystery for {args.multi} player{'s' if args.multi > 1 else ''}, {seedname} Seed {seed}") weights_cache = {} if args.weights: - weights_cache[args.weights] = get_weights(args.weights) + try: + weights_cache[args.weights] = get_weights(args.weights) + except Exception as e: + raise ValueError(f"File {args.weights} is destroyed. Please fix your yaml.") from e print(f"Weights: {args.weights} >> {weights_cache[args.weights]['description']}") + if args.meta: + try: + weights_cache[args.meta] = get_weights(args.meta) + except Exception as e: + raise ValueError(f"File {args.weights} is destroyed. Please fix your yaml.") from e + print(f"Meta: {args.meta} >> {weights_cache[args.meta]['meta_description']}") + if args.samesettings: + raise Exception("Cannot mix --samesettings with --meta") + for player in range(1, args.multi + 1): path = getattr(args, f'p{player}') if path: if path not in weights_cache: try: weights_cache[path] = get_weights(path) - except: - raise ValueError(f"File {path} is destroyed. Please fix your yaml.") + except Exception as e: + raise ValueError(f"File {path} is destroyed. Please fix your yaml.") from e print(f"P{player} Weights: {path} >> {weights_cache[path]['description']}") - erargs = parse_arguments(['--multi', str(args.multi)]) erargs.seed = seed erargs.name = {x+1: name for x,name in enumerate(args.names.split(","))} @@ -78,17 +91,31 @@ def main(): erargs.enemizercli = args.enemizercli settings_cache = {k: (roll_settings(v) if args.samesettings else None) for k, v in weights_cache.items()} + player_path_cache = {} + for player in range(1, args.multi + 1): + player_path_cache[player] = getattr(args, f'p{player}') if getattr(args, f'p{player}') else args.weights + + if args.meta: + for player, path in player_path_cache.items(): + weights_cache[path].setdefault("meta_ignore", []) + meta_weights = weights_cache[args.meta] + for key in meta_weights: + option = get_choice(key, meta_weights) + if option is not None: + for player, path in player_path_cache.items(): + if key not in weights_cache[path]["meta_ignore"]: + weights_cache[path][key] = option for player in range(1, args.multi + 1): - path = getattr(args, f'p{player}') if getattr(args, f'p{player}') else args.weights + path = player_path_cache[player] if path: try: settings = settings_cache[path] if settings_cache[path] else roll_settings(weights_cache[path]) for k, v in vars(settings).items(): if v is not None: getattr(erargs, k)[player] = v - except: - raise ValueError(f"File {path} is destroyed. Please fix your yaml.") + except Exception as e: + raise ValueError(f"File {path} is destroyed. Please fix your yaml.") from e else: raise RuntimeError(f'No weights specified for player {player}') # set up logger @@ -118,22 +145,22 @@ def interpret_on_off(value): def convert_to_on_off(value): return {True: "on", False: "off"}.get(value, value) -def roll_settings(weights): - def get_choice(option, root=weights): - if option not in root: - return None - if type(root[option]) is not dict: - return interpret_on_off(root[option]) - if not root[option]: - return None - return interpret_on_off( - random.choices(list(root[option].keys()), weights=list(map(int, root[option].values())))[0]) +def get_choice(option, root) -> typing.Any: + if option not in root: + return None + if type(root[option]) is not dict: + return interpret_on_off(root[option]) + if not root[option]: + return None + return interpret_on_off( + random.choices(list(root[option].keys()), weights=list(map(int, root[option].values())))[0]) +def roll_settings(weights): ret = argparse.Namespace() - ret.name = get_choice('name') + ret.name = get_choice('name', weights) if ret.name: ret.name = ret.name.replace(" ", "-").replace("_", "-") - glitches_required = get_choice('glitches_required') + glitches_required = get_choice('glitches_required', weights) if glitches_required not in ['none', 'no_logic']: print("Only NMG and No Logic supported") glitches_required = 'none' @@ -142,7 +169,7 @@ def roll_settings(weights): # item_placement = get_choice('item_placement') # not supported in ER - dungeon_items = get_choice('dungeon_items') + dungeon_items = get_choice('dungeon_items', weights) if dungeon_items == 'full' or dungeon_items == True: dungeon_items = 'mcsb' elif dungeon_items == 'standard': @@ -150,17 +177,17 @@ def roll_settings(weights): elif not dungeon_items: dungeon_items = "" - ret.mapshuffle = get_choice('map_shuffle') if 'map_shuffle' in weights else 'm' in dungeon_items - ret.compassshuffle = get_choice('compass_shuffle') if 'compass_shuffle' in weights else 'c' in dungeon_items - ret.keyshuffle = get_choice('smallkey_shuffle') if 'smallkey_shuffle' in weights else 's' in dungeon_items - ret.bigkeyshuffle = get_choice('bigkey_shuffle') if 'bigkey_shuffle' in weights else 'b' in dungeon_items + ret.mapshuffle = get_choice('map_shuffle', weights) if 'map_shuffle' in weights else 'm' in dungeon_items + ret.compassshuffle = get_choice('compass_shuffle', weights) if 'compass_shuffle' in weights else 'c' in dungeon_items + ret.keyshuffle = get_choice('smallkey_shuffle', weights) if 'smallkey_shuffle' in weights else 's' in dungeon_items + ret.bigkeyshuffle = get_choice('bigkey_shuffle', weights) if 'bigkey_shuffle' in weights else 'b' in dungeon_items - ret.accessibility = get_choice('accessibility') + ret.accessibility = get_choice('accessibility', weights) - entrance_shuffle = get_choice('entrance_shuffle') + entrance_shuffle = get_choice('entrance_shuffle', weights) ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla' - goal = get_choice('goals') + goal = get_choice('goals', weights) ret.goal = {'ganon': 'ganon', 'fast_ganon': 'crystals', 'dungeons': 'dungeons', @@ -169,57 +196,57 @@ def roll_settings(weights): }[goal] ret.openpyramid = goal == 'fast_ganon' - ret.crystals_gt = get_choice('tower_open') + ret.crystals_gt = get_choice('tower_open', weights) - ret.crystals_ganon = get_choice('ganon_open') + ret.crystals_ganon = get_choice('ganon_open', weights) - ret.mode = get_choice('world_state') + ret.mode = get_choice('world_state', weights) if ret.mode == 'retro': ret.mode = 'open' ret.retro = True - ret.hints = get_choice('hints') + ret.hints = get_choice('hints', weights) ret.swords = {'randomized': 'random', 'assured': 'assured', 'vanilla': 'vanilla', 'swordless': 'swordless' - }[get_choice('weapons')] + }[get_choice('weapons', weights)] - ret.difficulty = get_choice('item_pool') + ret.difficulty = get_choice('item_pool', weights) - ret.item_functionality = get_choice('item_functionality') + ret.item_functionality = get_choice('item_functionality', weights) ret.shufflebosses = {'none': 'none', 'simple': 'basic', 'full': 'normal', 'random': 'chaos' - }[get_choice('boss_shuffle')] + }[get_choice('boss_shuffle', weights)] ret.shuffleenemies = {'none': 'none', 'shuffled': 'shuffled', 'random': 'chaos' - }[get_choice('enemy_shuffle')] + }[get_choice('enemy_shuffle', weights)] ret.enemy_damage = {'default': 'default', 'shuffled': 'shuffled', 'random': 'chaos' - }[get_choice('enemy_damage')] + }[get_choice('enemy_damage', weights)] - ret.enemy_health = get_choice('enemy_health') + ret.enemy_health = get_choice('enemy_health', weights) - ret.shufflepots = get_choice('pot_shuffle') + ret.shufflepots = get_choice('pot_shuffle', weights) - ret.beemizer = int(get_choice('beemizer')) if 'beemizer' in weights else 0 + ret.beemizer = int(get_choice('beemizer', weights)) if 'beemizer' in weights else 0 ret.timer = {'none': 'none', 'timed': 'timed', 'timed_ohko': 'timed-ohko', 'ohko': 'ohko', 'timed_countdown': 'timed-countdown', - 'display': 'display'}[get_choice('timer')] if 'timer' in weights.keys() else 'none' + 'display': 'display'}[get_choice('timer', weights)] if 'timer' in weights.keys() else 'none' - ret.progressive = convert_to_on_off(get_choice('progressive')) if "progressive" in weights else 'on' + ret.progressive = convert_to_on_off(get_choice('progressive', weights)) if "progressive" in weights else 'on' inventoryweights = weights.get('startinventory', {}) startitems = [] for item in inventoryweights.keys(): diff --git a/host.yaml b/host.yaml index 81f734fc..1c49d40d 100644 --- a/host.yaml +++ b/host.yaml @@ -22,6 +22,8 @@ multi_mystery_options: enemizer_path: "EnemizerCLI/EnemizerCLI.Core.exe" #folder from which the player yaml files are pulled from player_files_path: "Players" +#meta file name, within players folder + meta_file_path: "meta.yaml" #automatically launches {player_name}.yaml's ROM file using the OS's default program once generation completes. (likely your emulator) #does nothing if the name is not found #example: player_name = "Berserker" diff --git a/meta.yaml b/meta.yaml new file mode 100644 index 00000000..fe6f8e48 --- /dev/null +++ b/meta.yaml @@ -0,0 +1,36 @@ +#this file has to be in the Players folder to take effect. +meta_description: Meta file with the intention of having similar-length roms for a hopefully better experience +goals: + ganon: 10 + fast_ganon: 30 + dungeons: 5 + pedestal: 5 + triforce-hunt: 1 + null: 0 # maintain individual goals +world_state: + standard: 5 + open: 5 + inverted: 5 + retro: 5 + null: 10 # maintain individual world states +tower_open: + '0': 8 + '1': 7 + '2': 6 + '3': 5 + '4': 4 + '5': 3 + '6': 2 + '7': 1 + random: 5 +ganon_open: + '0': 3 + '1': 4 + '2': 5 + '3': 6 + '4': 7 + '5': 8 + '6': 9 + '7': 10 + random: 5 +#do not use meta rom options at this time. \ No newline at end of file