From 2fc4006dfa79dc3c51f451aae7f6d9e048f9b989 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 21 Jul 2021 18:08:15 +0200 Subject: [PATCH] RIP: MultiMystery and Mystery, now there's just Generate Other changes: host.yaml Multi Mystery options were moved and changed generate_output now has an output_directory argument MultiWorld.get_game_players() now replaces _player_ids Python venv should now work properly --- BaseClasses.py | 47 +-- Fill.py | 5 +- Mystery.py => Generate.py | 207 ++++------- Main.py | 345 +++++++++--------- MultiMystery.py | 226 ------------ MultiServer.py | 1 + Utils.py | 15 +- WebHostLib/check.py | 2 +- WebHostLib/generate.py | 3 +- WebHostLib/lttpsprites.py | 1 + WebHostLib/static/assets/weightedSettings.js | 3 + .../static/assets/zelda3/player-settings.js | 3 + host.yaml | 47 +-- setup.py | 11 +- worlds/AutoWorld.py | 16 +- worlds/__init__.py | 4 - worlds/alttp/EntranceRandomizer.py | 2 - worlds/alttp/ItemPool.py | 2 +- worlds/factorio/Mod.py | 4 +- worlds/hk/__init__.py | 4 +- worlds/minecraft/__init__.py | 2 +- 21 files changed, 305 insertions(+), 645 deletions(-) rename Mystery.py => Generate.py (81%) delete mode 100644 MultiMystery.py diff --git a/BaseClasses.py b/BaseClasses.py index 269f988e..95172286 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -6,7 +6,7 @@ import logging import json import functools from collections import OrderedDict, Counter, deque -from typing import * +from typing import List, Dict, Optional, Set, Iterable, Union import secrets import random @@ -14,14 +14,14 @@ import random class MultiWorld(): debug_types = False player_names: Dict[int, List[str]] - _region_cache: dict + _region_cache: Dict[int, Dict[str, Region]] difficulty_requirements: dict required_medallions: dict dark_room_logic: Dict[int, str] restrict_dungeon_item_on_boss: Dict[int, bool] plando_texts: List[Dict[str, str]] - plando_items: List[PlandoItem] - plando_connections: List[PlandoConnection] + plando_items: List + plando_connections: List er_seeds: Dict[int, str] worlds: Dict[int, "AutoWorld.World"] is_race: bool = False @@ -157,22 +157,9 @@ class MultiWorld(): def player_ids(self): return tuple(range(1, self.players + 1)) - # Todo: make these automatic, or something like get_players_for_game(game_name) - @functools.cached_property - def alttp_player_ids(self): - return tuple(player for player in range(1, self.players + 1) if self.game[player] == "A Link to the Past") - - @functools.cached_property - def hk_player_ids(self): - return tuple(player for player in range(1, self.players + 1) if self.game[player] == "Hollow Knight") - - @functools.cached_property - def factorio_player_ids(self): - return tuple(player for player in range(1, self.players + 1) if self.game[player] == "Factorio") - - @functools.cached_property - def minecraft_player_ids(self): - return tuple(player for player in range(1, self.players + 1) if self.game[player] == "Minecraft") + @functools.lru_cache() + def get_game_players(self, game_name: str): + return tuple(player for player in self.player_ids if self.game[player] == game_name) def get_name_string_for_object(self, obj) -> str: return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})' @@ -241,7 +228,7 @@ class MultiWorld(): self.worlds[item.player].collect(ret, item) if keys: - for p in self.alttp_player_ids: + for p in self.get_game_players("A Link to the Past"): world = self.worlds[p] from worlds.alttp.Items import ItemFactory for item in ItemFactory( @@ -1226,7 +1213,7 @@ class Spoiler(object): def parse_data(self): self.medallions = OrderedDict() - for player in self.world.alttp_player_ids: + for player in self.world.get_game_players("A Link to the Past"): self.medallions[f'Misery Mire ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][0] self.medallions[f'Turtle Rock ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][1] @@ -1282,7 +1269,7 @@ class Spoiler(object): shopdata['item_{}'.format(index)] += ", {} - {}".format(item['replacement'], item['replacement_price']) if item['replacement_price'] else item['replacement'] self.shops.append(shopdata) - for player in self.world.alttp_player_ids: + for player in self.world.get_game_players("A Link to the Past"): self.bosses[str(player)] = OrderedDict() self.bosses[str(player)]["Eastern Palace"] = self.world.get_dungeon("Eastern Palace", player).boss.name self.bosses[str(player)]["Desert Palace"] = self.world.get_dungeon("Desert Palace", player).boss.name @@ -1396,11 +1383,11 @@ class Spoiler(object): res = getattr(self.world, f_option)[player] outfile.write(f'{f_option+":":33}{bool_to_text(res) if type(res) == Options.Toggle else res.get_option_name()}\n') - if player in self.world.alttp_player_ids: + if player in self.world.get_game_players("A Link to the Past"): for team in range(self.world.teams): outfile.write('%s%s\n' % ( f"Hash - {self.world.player_names[player][team]} (Team {team + 1}): " if - (player in self.world.alttp_player_ids and self.world.teams > 1) else 'Hash: ', + (player in self.world.get_game_players("A Link to the Past") and self.world.teams > 1) else 'Hash: ', self.hashes[player, team])) outfile.write('Logic: %s\n' % self.metadata['logic'][player]) @@ -1473,10 +1460,10 @@ class Spoiler(object): outfile.write('\n\nMedallions:\n') for dungeon, medallion in self.medallions.items(): outfile.write(f'\n{dungeon}: {medallion}') - - if self.world.factorio_player_ids: + factorio_players = self.world.get_game_players("Factorio") + if factorio_players: outfile.write('\n\nRecipes:\n') - for player in self.world.factorio_player_ids: + for player in factorio_players: name = self.world.get_player_names(player) for recipe in self.world.worlds[player].custom_recipes.values(): outfile.write(f"\n{recipe.name} ({name}): {recipe.ingredients} -> {recipe.products}") @@ -1492,7 +1479,7 @@ class Spoiler(object): outfile.write('\n\nShops:\n\n') outfile.write('\n'.join("{} [{}]\n {}".format(shop['location'], shop['type'], "\n ".join(item for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if item)) for shop in self.shops)) - for player in self.world.alttp_player_ids: + for player in self.world.get_game_players("A Link to the Past"): if self.world.boss_shuffle[player] != 'none': bossmap = self.bosses[str(player)] if self.world.players > 1 else self.bosses outfile.write(f'\n\nBosses{(f" ({self.world.get_player_names(player)})" if self.world.players > 1 else "")}:\n') @@ -1516,5 +1503,3 @@ class Spoiler(object): path_listings.append("{}\n {}".format(location, "\n => ".join(path_lines))) outfile.write('\n'.join(path_listings)) - -from worlds.generic import PlandoItem, PlandoConnection \ No newline at end of file diff --git a/Fill.py b/Fill.py index 5402d871..95953695 100644 --- a/Fill.py +++ b/Fill.py @@ -3,9 +3,10 @@ import typing import collections import itertools -from BaseClasses import CollectionState, PlandoItem, Location, MultiWorld +from BaseClasses import CollectionState, Location, MultiWorld from worlds.alttp.Items import ItemFactory from worlds.alttp.Regions import key_drop_data +from worlds.generic import PlandoItem class FillError(RuntimeError): @@ -91,7 +92,7 @@ def distribute_items_restrictive(world: MultiWorld, gftower_trash=False, fill_lo standard_keyshuffle_players = set() # fill in gtower locations with trash first - for player in world.alttp_player_ids: + for player in world.get_game_players("A Link to the Past"): if not gftower_trash or not world.ganonstower_vanilla[player] or \ world.logic[player] in {'owglitches', 'hybridglitches', "nologic"}: gtower_trash_count = 0 diff --git a/Mystery.py b/Generate.py similarity index 81% rename from Mystery.py rename to Generate.py index 608b9be8..2e38dd56 100644 --- a/Mystery.py +++ b/Generate.py @@ -14,7 +14,7 @@ from worlds.generic import PlandoItem, PlandoConnection ModuleUpdate.update() -from Utils import parse_yaml, version_tuple, __version__, tuplize_version +from Utils import parse_yaml, version_tuple, __version__, tuplize_version, get_options from worlds.alttp.EntranceRandomizer import parse_arguments from Main import main as ERmain from Main import get_seed, seeddigits @@ -28,40 +28,38 @@ from worlds.AutoWorld import AutoWorldRegister categories = set(AutoWorldRegister.world_types) def mystery_argparse(): - parser = argparse.ArgumentParser(add_help=False) - parser.add_argument('--multi', default=1, type=lambda value: min(max(int(value), 1), 255)) - multiargs, _ = parser.parse_known_args() + options = get_options() + defaults = options["generator"] - parser = argparse.ArgumentParser() - parser.add_argument('--weights', + parser = argparse.ArgumentParser(description="CMD Generation Interface, defaults come from host.yaml.") + parser.add_argument('--weights_file_path', default = defaults["weights_file_path"], help='Path to the weights file to use for rolling game settings, urls are also valid') parser.add_argument('--samesettings', help='Rolls settings per weights file rather than per player', action='store_true') + parser.add_argument('--player_files_path', default=defaults["player_files_path"], + help="Input directory for player files.") parser.add_argument('--seed', help='Define seed number to generate.', type=int) - parser.add_argument('--multi', default=1, type=lambda value: min(max(int(value), 1), 255)) + parser.add_argument('--multi', default=defaults["players"], type=lambda value: min(max(int(value), 1), 255)) parser.add_argument('--teams', default=1, type=lambda value: max(int(value), 1)) - parser.add_argument('--create_spoiler', action='store_true') - parser.add_argument('--skip_playthrough', action='store_true') - parser.add_argument('--pre_roll', action='store_true') - parser.add_argument('--rom') - parser.add_argument('--enemizercli') - parser.add_argument('--outputpath') - parser.add_argument('--glitch_triforce', action='store_true') - parser.add_argument('--race', action='store_true') - parser.add_argument('--meta', default=None) + parser.add_argument('--spoiler', type=int, default=defaults["spoiler"]) + parser.add_argument('--rom', default=options["lttp_options"]["rom_file"], help="Path to the 1.0 JP LttP Baserom.") + parser.add_argument('--enemizercli', default=defaults["enemizer_path"]) + parser.add_argument('--outputpath', default=options["general_options"]["output_path"]) + parser.add_argument('--race', action='store_true', default=defaults["race"]) + parser.add_argument('--meta_file_path', default=defaults["meta_file_path"]) parser.add_argument('--log_output_path', help='Path to store output log') - parser.add_argument('--loglevel', default='info', help='Sets log level') - parser.add_argument('--create_diff', action="store_true") + parser.add_argument('--log_level', default='info', help='Sets log level') parser.add_argument('--yaml_output', default=0, type=lambda value: min(max(int(value), 0), 255), help='Output rolled mystery results to yaml up to specified number (made for async multiworld)') - parser.add_argument('--plando', default="bosses", + parser.add_argument('--plando', default=defaults["plando_options"], help='List of options that can be set manually. Can be combined, for example "bosses, items"') - parser.add_argument('--seed_name') - for player in range(1, multiargs.multi + 1): - parser.add_argument(f'--p{player}', help=argparse.SUPPRESS) args = parser.parse_args() + if not os.path.isabs(args.weights_file_path): + args.weights_file_path = os.path.join(args.player_files_path, args.weights_file_path) + if not os.path.isabs(args.meta_file_path): + args.meta_file_path = os.path.join(args.player_files_path, args.meta_file_path) args.plando: typing.Set[str] = {arg.strip().lower() for arg in args.plando.split(",")} - return args + return args, options def get_seed_name(random): @@ -70,106 +68,93 @@ def get_seed_name(random): def main(args=None, callback=ERmain): if not args: - args = mystery_argparse() + args, options = mystery_argparse() seed = get_seed(args.seed) random.seed(seed) - seed_name = args.seed_name if args.seed_name else get_seed_name(random) - print(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, {seed_name} Seed {seed}") + seed_name = get_seed_name(random) if args.race: random.seed() # reset to time-based random source weights_cache = {} - if args.weights: + if args.weights_file_path and os.path.exists(args.weights_file_path): try: - weights_cache[args.weights] = read_weights_yaml(args.weights) + weights_cache[args.weights_file_path] = read_weights_yaml(args.weights_file_path) except Exception as e: - raise ValueError(f"File {args.weights} is destroyed. Please fix your yaml.") from e - print(f"Weights: {args.weights} >> " - f"{get_choice('description', weights_cache[args.weights], 'No description specified')}") - if args.meta: + raise ValueError(f"File {args.weights_file_path} is destroyed. Please fix your yaml.") from e + print(f"Weights: {args.weights_file_path} >> " + f"{get_choice('description', weights_cache[args.weights_file_path], 'No description specified')}") + + if args.meta_file_path and os.path.exists(args.meta_file_path): try: - weights_cache[args.meta] = read_weights_yaml(args.meta) + weights_cache[args.meta_file_path] = read_weights_yaml(args.meta_file_path) except Exception as e: - raise ValueError(f"File {args.meta} is destroyed. Please fix your yaml.") from e - meta_weights = weights_cache[args.meta] - print(f"Meta: {args.meta} >> {get_choice('meta_description', meta_weights, 'No description specified')}") + raise ValueError(f"File {args.meta_file_path} is destroyed. Please fix your yaml.") from e + meta_weights = weights_cache[args.meta_file_path] + print(f"Meta: {args.meta_file_path} >> {get_choice('meta_description', meta_weights, 'No description specified')}") 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: + else: + meta_weights = None + player_id = 1 + player_files = {} + for file in os.scandir(args.player_files_path): + fname = file.name + if file.is_file() and fname not in {args.meta_file_path, args.weights_file_path}: + path = os.path.join(args.player_files_path, fname) try: - if path not in weights_cache: - weights_cache[path] = read_weights_yaml(path) - print(f"P{player} Weights: {path} >> " - f"{get_choice('description', weights_cache[path], 'No description specified')}") - + weights_cache[fname] = read_weights_yaml(path) except Exception as e: - raise ValueError(f"File {path} is destroyed. Please fix your yaml.") from e + raise ValueError(f"File {fname} is destroyed. Please fix your yaml.") from e + else: + print(f"P{player_id} Weights: {fname} >> " + f"{get_choice('description', weights_cache[fname], 'No description specified')}") + player_files[player_id] = fname + player_id += 1 + + args.multi = max(player_id-1, args.multi) + print(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, {seed_name} Seed {seed}") + + if not weights_cache: + raise Exception(f"No weights found. Provide a general weights file ({args.weights_file_path}) or individual player files. " + f"A mix is also permitted.") erargs = parse_arguments(['--multi', str(args.multi)]) erargs.seed = seed erargs.name = {x: "" for x in range(1, args.multi + 1)} # only so it can be overwrittin in mystery - erargs.create_spoiler = args.create_spoiler - erargs.create_diff = args.create_diff - erargs.glitch_triforce = args.glitch_triforce + erargs.create_spoiler = args.spoiler > 0 + erargs.glitch_triforce = options["generator"]["glitch_triforce_room"] erargs.race = args.race - erargs.skip_playthrough = args.skip_playthrough + erargs.skip_playthrough = args.spoiler == 0 erargs.outputname = seed_name erargs.outputpath = args.outputpath erargs.teams = args.teams # set up logger - if args.loglevel: - erargs.loglevel = args.loglevel + if args.log_level: + erargs.loglevel = args.log_level loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[ erargs.loglevel] if args.log_output_path: - import sys - class LoggerWriter(object): - def __init__(self, writer): - self._writer = writer - self._msg = '' - - def write(self, message): - self._msg = self._msg + message - while '\n' in self._msg: - pos = self._msg.find('\n') - self._writer(self._msg[:pos]) - self._msg = self._msg[pos + 1:] - - def flush(self): - if self._msg != '': - self._writer(self._msg) - self._msg = '' - - log = logging.getLogger("stderr") - log.addHandler(logging.StreamHandler()) - sys.stderr = LoggerWriter(log.error) os.makedirs(args.log_output_path, exist_ok=True) - logging.basicConfig(format='%(message)s', level=loglevel, + logging.basicConfig(format='%(message)s', level=loglevel, force=True, filename=os.path.join(args.log_output_path, f"{seed}.log")) else: - logging.basicConfig(format='%(message)s', level=loglevel) - if args.rom: - erargs.rom = args.rom + logging.basicConfig(format='%(message)s', level=loglevel, force=True) - if args.enemizercli: - erargs.enemizercli = args.enemizercli + erargs.rom = args.rom + erargs.enemizercli = args.enemizercli settings_cache = {k: (roll_settings(v, args.plando) 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 + player_path_cache[player] = player_files.get(player, args.weights_file_path) - if args.meta: + if meta_weights: 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: @@ -188,31 +173,6 @@ def main(args=None, callback=ERmain): try: settings = settings_cache[path] if settings_cache[path] else \ roll_settings(weights_cache[path], args.plando) - if args.pre_roll: - import yaml - if path == args.weights: - settings.name = f"Player{player}" - elif not settings.name: - settings.name = os.path.splitext(os.path.split(path)[-1])[0] - - if "-" not in settings.shuffle and settings.shuffle != "vanilla": - settings.shuffle += f"-{random.randint(0, 2 ** 64)}" - - pre_rolled = dict() - pre_rolled["original_seed_number"] = seed - pre_rolled["original_seed_name"] = seed_name - pre_rolled["pre_rolled"] = vars(settings).copy() - if "plando_items" in pre_rolled["pre_rolled"]: - pre_rolled["pre_rolled"]["plando_items"] = [item.to_dict() for item in - pre_rolled["pre_rolled"]["plando_items"]] - if "plando_connections" in pre_rolled["pre_rolled"]: - pre_rolled["pre_rolled"]["plando_connections"] = [connection.to_dict() for connection in - pre_rolled["pre_rolled"][ - "plando_connections"]] - - with open(os.path.join(args.outputpath if args.outputpath else ".", - f"{os.path.split(path)[-1].split('.')[0]}_pre_rolled.yaml"), "wt") as f: - yaml.dump(pre_rolled, f) for k, v in vars(settings).items(): if v is not None: try: @@ -223,7 +183,7 @@ def main(args=None, callback=ERmain): raise ValueError(f"File {path} is destroyed. Please fix your yaml.") from e else: raise RuntimeError(f'No weights specified for player {player}') - if path == args.weights: # if name came from the weights file, just use base player name + if path == args.weights_file_path: # if name came from the weights file, just use base player name erargs.name[player] = f"Player{player}" elif not erargs.name[player]: # if name was not specified, generate it from filename erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0] @@ -453,37 +413,6 @@ def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("bosses",))): - if "pre_rolled" in weights: - pre_rolled = weights["pre_rolled"] - - if "plando_items" in pre_rolled: - pre_rolled["plando_items"] = [PlandoItem(item["item"], - item["location"], - item["world"], - item["from_pool"], - item["force"]) for item in pre_rolled["plando_items"]] - if "items" not in plando_options and pre_rolled["plando_items"]: - raise Exception("Item Plando is turned off. Reusing this pre-rolled setting not permitted.") - - if "plando_connections" in pre_rolled: - pre_rolled["plando_connections"] = [PlandoConnection(connection["entrance"], - connection["exit"], - connection["direction"]) for connection in - pre_rolled["plando_connections"]] - if "connections" not in plando_options and pre_rolled["plando_connections"]: - raise Exception("Connection Plando is turned off. Reusing this pre-rolled setting not permitted.") - - if "bosses" not in plando_options: - try: - pre_rolled["shufflebosses"] = get_plando_bosses(pre_rolled["shufflebosses"], plando_options) - except Exception as ex: - raise Exception("Boss Plando is turned off. Reusing this pre-rolled setting not permitted.") from ex - - if pre_rolled.get("plando_texts") and "texts" not in plando_options: - raise Exception("Text Plando is turned off. Reusing this pre-rolled setting not permitted.") - - return argparse.Namespace(**pre_rolled) - if "linked_options" in weights: weights = roll_linked_options(weights) diff --git a/Main.py b/Main.py index 6abbdc37..e1bc1089 100644 --- a/Main.py +++ b/Main.py @@ -6,6 +6,8 @@ import time import zlib import concurrent.futures import pickle +import tempfile +import zipfile from typing import Dict, Tuple from BaseClasses import MultiWorld, CollectionState, Region, Item @@ -128,7 +130,7 @@ def main(args, seed=None): AutoWorld.call_all(world, "generate_early") # system for sharing ER layouts - for player in world.alttp_player_ids: + for player in world.get_game_players("A Link to the Past"): world.er_seeds[player] = str(world.random.randint(0, 2 ** 64)) if "-" in world.shuffle[player]: @@ -160,7 +162,7 @@ def main(args, seed=None): world.player_names[player].append(name) logger.info('') - for player in world.alttp_player_ids: + for player in world.get_game_players("A Link to the Past"): world.difficulty_requirements[player] = difficulties[world.difficulty[player]] for player in world.player_ids: @@ -168,7 +170,7 @@ def main(args, seed=None): world.push_precollected(world.create_item(item_name, player)) for player in world.player_ids: - if player in world.alttp_player_ids: + if player in world.get_game_players("A Link to the Past"): # enforce pre-defined local items. if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]: world.local_items[player].add('Triforce Piece') @@ -195,7 +197,7 @@ def main(args, seed=None): AutoWorld.call_all(world, "create_regions") - for player in world.alttp_player_ids: + for player in world.get_game_players("A Link to the Past"): if world.open_pyramid[player] == 'goal': world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} @@ -220,7 +222,7 @@ def main(args, seed=None): logger.info('Shuffling the World about.') - for player in world.alttp_player_ids: + for player in world.get_game_players("A Link to the Past"): if world.logic[player] not in ["noglitches", "minorglitches"] and world.shuffle[player] in \ {"vanilla", "dungeonssimple", "dungeonsfull", "simple", "restricted", "full"}: world.fix_fake_world[player] = False @@ -241,7 +243,7 @@ def main(args, seed=None): logger.info('Generating Item Pool.') - for player in world.alttp_player_ids: + for player in world.get_game_players("A Link to the Past"): generate_itempool(world, player) logger.info('Calculating Access Rules.') @@ -251,7 +253,7 @@ def main(args, seed=None): AutoWorld.call_all(world, "set_rules") - for player in world.alttp_player_ids: + for player in world.get_game_players("A Link to the Past"): set_rules(world, player) for player in world.player_ids: @@ -301,7 +303,7 @@ def main(args, seed=None): outfilebase = 'AP_' + world.seed_name rom_names = [] - def _gen_rom(team: int, player: int): + def _gen_rom(team: int, player: int, output_directory:str): use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player] or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default' or world.shufflepots[player] or world.bush_shuffle[player] @@ -390,180 +392,184 @@ def main(args, seed=None): "-prog_" + outfilestuffs["progressive"] if outfilestuffs["progressive"] in ['off', 'random'] else "", # A "-nohints" if not outfilestuffs["hints"] == "True" else "") # B ) if not args.outputname else '' - rompath = output_path(f'{outfilebase}{outfilepname}{outfilesuffix}.sfc') + rompath = os.path.join(output_directory, f'{outfilebase}{outfilepname}{outfilesuffix}.sfc') rom.write_to_file(rompath, hide_enemizer=True) - if args.create_diff: - Patch.create_patch_file(rompath, player=player, player_name=world.player_names[player][team]) + Patch.create_patch_file(rompath, player=player, player_name=world.player_names[player][team]) + os.unlink(rompath) return player, team, bytes(rom.name) pool = concurrent.futures.ThreadPoolExecutor() - check_accessibility_task = pool.submit(world.fulfills_accessibility) + output = tempfile.TemporaryDirectory() + with output as temp_dir: + check_accessibility_task = pool.submit(world.fulfills_accessibility) + rom_futures = [] + output_file_futures = [] + for team in range(world.teams): + for player in world.get_game_players("A Link to the Past"): + rom_futures.append(pool.submit(_gen_rom, team, player, temp_dir)) + for player in world.player_ids: + output_file_futures.append(pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir)) - rom_futures = [] - output_file_futures = [] - for team in range(world.teams): - for player in world.alttp_player_ids: - rom_futures.append(pool.submit(_gen_rom, team, player)) - for player in world.player_ids: - output_file_futures.append(pool.submit(AutoWorld.call_single, world, "generate_output", player)) + def get_entrance_to_region(region: Region): + for entrance in region.entrances: + if entrance.parent_region.type in (RegionType.DarkWorld, RegionType.LightWorld, RegionType.Generic): + return entrance + for entrance in region.entrances: # BFS might be better here, trying DFS for now. + return get_entrance_to_region(entrance.parent_region) - def get_entrance_to_region(region: Region): - for entrance in region.entrances: - if entrance.parent_region.type in (RegionType.DarkWorld, RegionType.LightWorld, RegionType.Generic): - return entrance - for entrance in region.entrances: # BFS might be better here, trying DFS for now. - return get_entrance_to_region(entrance.parent_region) + # collect ER hint info + er_hint_data = {player: {} for player in world.get_game_players("A Link to the Past") if + world.shuffle[player] != "vanilla" or world.retro[player]} + from worlds.alttp.Regions import RegionType + for region in world.regions: + if region.player in er_hint_data and region.locations: + main_entrance = get_entrance_to_region(region) + for location in region.locations: + if type(location.address) == int: # skips events and crystals + if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name: + er_hint_data[region.player][location.address] = main_entrance.name - # collect ER hint info - er_hint_data = {player: {} for player in world.alttp_player_ids if - world.shuffle[player] != "vanilla" or world.retro[player]} - from worlds.alttp.Regions import RegionType - for region in world.regions: - if region.player in er_hint_data and region.locations: - main_entrance = get_entrance_to_region(region) - for location in region.locations: - if type(location.address) == int: # skips events and crystals - if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name: - er_hint_data[region.player][location.address] = main_entrance.name + ordered_areas = ('Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace', + 'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace', + 'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total") - ordered_areas = ('Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace', - 'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace', - 'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total") + checks_in_area = {player: {area: list() for area in ordered_areas} + for player in range(1, world.players + 1)} - checks_in_area = {player: {area: list() for area in ordered_areas} - for player in range(1, world.players + 1)} + for player in range(1, world.players + 1): + checks_in_area[player]["Total"] = 0 - for player in range(1, world.players + 1): - checks_in_area[player]["Total"] = 0 + for location in [loc for loc in world.get_filled_locations() if type(loc.address) is int]: + main_entrance = get_entrance_to_region(location.parent_region) + if location.game != "A Link to the Past": + checks_in_area[location.player]["Light World"].append(location.address) + elif location.parent_region.dungeon: + dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower', + 'Inverted Ganons Tower': 'Ganons Tower'} \ + .get(location.parent_region.dungeon.name, location.parent_region.dungeon.name) + checks_in_area[location.player][dungeonname].append(location.address) + elif main_entrance.parent_region.type == RegionType.LightWorld: + checks_in_area[location.player]["Light World"].append(location.address) + elif main_entrance.parent_region.type == RegionType.DarkWorld: + checks_in_area[location.player]["Dark World"].append(location.address) + checks_in_area[location.player]["Total"] += 1 - for location in [loc for loc in world.get_filled_locations() if type(loc.address) is int]: - main_entrance = get_entrance_to_region(location.parent_region) - if location.game != "A Link to the Past": - checks_in_area[location.player]["Light World"].append(location.address) - elif location.parent_region.dungeon: - dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower', - 'Inverted Ganons Tower': 'Ganons Tower'} \ - .get(location.parent_region.dungeon.name, location.parent_region.dungeon.name) - checks_in_area[location.player][dungeonname].append(location.address) - elif main_entrance.parent_region.type == RegionType.LightWorld: - checks_in_area[location.player]["Light World"].append(location.address) - elif main_entrance.parent_region.type == RegionType.DarkWorld: - checks_in_area[location.player]["Dark World"].append(location.address) - checks_in_area[location.player]["Total"] += 1 + oldmancaves = [] + takeanyregions = ["Old Man Sword Cave", "Take-Any #1", "Take-Any #2", "Take-Any #3", "Take-Any #4"] + for index, take_any in enumerate(takeanyregions): + for region in [world.get_region(take_any, player) for player in range(1, world.players + 1) if + world.retro[player]]: + item = world.create_item(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'], + region.player) + player = region.player + location_id = SHOP_ID_START + total_shop_slots + index - oldmancaves = [] - takeanyregions = ["Old Man Sword Cave", "Take-Any #1", "Take-Any #2", "Take-Any #3", "Take-Any #4"] - for index, take_any in enumerate(takeanyregions): - for region in [world.get_region(take_any, player) for player in range(1, world.players + 1) if - world.retro[player]]: - item = world.create_item(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'], - region.player) - player = region.player - location_id = SHOP_ID_START + total_shop_slots + index + main_entrance = get_entrance_to_region(region) + if main_entrance.parent_region.type == RegionType.LightWorld: + checks_in_area[player]["Light World"].append(location_id) + else: + checks_in_area[player]["Dark World"].append(location_id) + checks_in_area[player]["Total"] += 1 - main_entrance = get_entrance_to_region(region) - if main_entrance.parent_region.type == RegionType.LightWorld: - checks_in_area[player]["Light World"].append(location_id) + er_hint_data[player][location_id] = main_entrance.name + oldmancaves.append(((location_id, player), (item.code, player))) + + FillDisabledShopSlots(world) + + def write_multidata(roms, outputs): + import base64 + import NetUtils + for future in roms: + rom_name = future.result() + rom_names.append(rom_name) + slot_data = {} + client_versions = {} + minimum_versions = {"server": (0, 1, 1), "clients": client_versions} + games = {} + for slot in world.player_ids: + client_versions[slot] = world.worlds[slot].get_required_client_version() + games[slot] = world.game[slot] + connect_names = {base64.b64encode(rom_name).decode(): (team, slot) for + slot, team, rom_name in rom_names} + precollected_items = {player: [] for player in range(1, world.players + 1)} + for item in world.precollected_items: + precollected_items[item.player].append(item.code) + precollected_hints = {player: set() for player in range(1, world.players + 1)} + # for now special case Factorio tech_tree_information + sending_visible_players = set() + for player in world.get_game_players("Factorio"): + if world.tech_tree_information[player].value == 2: + sending_visible_players.add(player) + + for i, team in enumerate(parsed_names): + for player, name in enumerate(team, 1): + if player not in world.get_game_players("A Link to the Past"): + connect_names[name] = (i, player) + + for slot in world.player_ids: + slot_data[slot] = world.worlds[slot].fill_slot_data() + + locations_data: Dict[int, Dict[int, Tuple[int, int]]] = {player: {} for player in world.player_ids} + for location in world.get_filled_locations(): + if type(location.address) == int: + locations_data[location.player][location.address] = location.item.code, location.item.player + if location.player in sending_visible_players and location.item.player != location.player: + hint = NetUtils.Hint(location.item.player, location.player, location.address, + location.item.code, False) + precollected_hints[location.player].add(hint) + precollected_hints[location.item.player].add(hint) + elif location.item.name in args.start_hints[location.item.player]: + hint = NetUtils.Hint(location.item.player, location.player, location.address, + location.item.code, False, + er_hint_data.get(location.player, {}).get(location.address, "")) + precollected_hints[location.player].add(hint) + precollected_hints[location.item.player].add(hint) + + multidata = zlib.compress(pickle.dumps({ + "slot_data": slot_data, + "games": games, + "names": parsed_names, + "connect_names": connect_names, + "remote_items": {player for player in world.player_ids if + world.worlds[player].remote_items}, + "locations": locations_data, + "checks_in_area": checks_in_area, + "server_options": get_options()["server_options"], + "er_hint_data": er_hint_data, + "precollected_items": precollected_items, + "precollected_hints": precollected_hints, + "version": tuple(version_tuple), + "tags": ["AP"], + "minimum_versions": minimum_versions, + "seed_name": world.seed_name + }), 9) + + with open(os.path.join(temp_dir, '%s.archipelago' % outfilebase), 'wb') as f: + f.write(bytes([1])) # version of format + f.write(multidata) + for future in outputs: + future.result() # collect errors if they occured + + multidata_task = pool.submit(write_multidata, rom_futures, output_file_futures) + if not check_accessibility_task.result(): + if not world.can_beat_game(): + raise Exception("Game appears as unbeatable. Aborting.") else: - checks_in_area[player]["Dark World"].append(location_id) - checks_in_area[player]["Total"] += 1 - - er_hint_data[player][location_id] = main_entrance.name - oldmancaves.append(((location_id, player), (item.code, player))) - - FillDisabledShopSlots(world) - - def write_multidata(roms, outputs): - import base64 - import NetUtils - for future in roms: - rom_name = future.result() - rom_names.append(rom_name) - slot_data = {} - client_versions = {} - minimum_versions = {"server": (0, 1, 1), "clients": client_versions} - games = {} - for slot in world.player_ids: - client_versions[slot] = world.worlds[slot].get_required_client_version() - games[slot] = world.game[slot] - connect_names = {base64.b64encode(rom_name).decode(): (team, slot) for - slot, team, rom_name in rom_names} - precollected_items = {player: [] for player in range(1, world.players + 1)} - for item in world.precollected_items: - precollected_items[item.player].append(item.code) - precollected_hints = {player: set() for player in range(1, world.players + 1)} - # for now special case Factorio tech_tree_information - sending_visible_players = set() - for player in world.factorio_player_ids: - if world.tech_tree_information[player].value == 2: - sending_visible_players.add(player) - - for i, team in enumerate(parsed_names): - for player, name in enumerate(team, 1): - if player not in world.alttp_player_ids: - connect_names[name] = (i, player) - if world.hk_player_ids: - for slot in world.hk_player_ids: - slot_data[slot] = AutoWorld.call_single(world, "fill_slot_data", slot) - for slot in world.minecraft_player_ids: - slot_data[slot] = AutoWorld.call_single(world, "fill_slot_data", slot) - - locations_data: Dict[int, Dict[int, Tuple[int, int]]] = {player: {} for player in world.player_ids} - for location in world.get_filled_locations(): - if type(location.address) == int: - locations_data[location.player][location.address] = (location.item.code, location.item.player) - if location.player in sending_visible_players and location.item.player != location.player: - hint = NetUtils.Hint(location.item.player, location.player, location.address, - location.item.code, False) - precollected_hints[location.player].add(hint) - precollected_hints[location.item.player].add(hint) - elif location.item.name in args.start_hints[location.item.player]: - hint = NetUtils.Hint(location.item.player, location.player, location.address, - location.item.code, False, - er_hint_data.get(location.player, {}).get(location.address, "")) - precollected_hints[location.player].add(hint) - precollected_hints[location.item.player].add(hint) - - multidata = zlib.compress(pickle.dumps({ - "slot_data": slot_data, - "games": games, - "names": parsed_names, - "connect_names": connect_names, - "remote_items": {player for player in world.player_ids if - world.worlds[player].remote_items}, - "locations": locations_data, - "checks_in_area": checks_in_area, - "server_options": get_options()["server_options"], - "er_hint_data": er_hint_data, - "precollected_items": precollected_items, - "precollected_hints": precollected_hints, - "version": tuple(version_tuple), - "tags": ["AP"], - "minimum_versions": minimum_versions, - "seed_name": world.seed_name - }), 9) - - with open(output_path('%s.archipelago' % outfilebase), 'wb') as f: - f.write(bytes([1])) # version of format - f.write(multidata) - for future in outputs: - future.result() # collect errors if they occured - - multidata_task = pool.submit(write_multidata, rom_futures, output_file_futures) - if not check_accessibility_task.result(): - if not world.can_beat_game(): - raise Exception("Game appears as unbeatable. Aborting.") - else: - logger.warning("Location Accessibility requirements not fulfilled.") - if multidata_task: - multidata_task.result() # retrieve exception if one exists - pool.shutdown() # wait for all queued tasks to complete - if not args.skip_playthrough: - logger.info('Calculating playthrough.') - create_playthrough(world) - if args.create_spoiler: # needs spoiler.hashes to be filled, that depend on rom_futures being done - world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase)) + logger.warning("Location Accessibility requirements not fulfilled.") + if multidata_task: + multidata_task.result() # retrieve exception if one exists + pool.shutdown() # wait for all queued tasks to complete + if not args.skip_playthrough: + logger.info('Calculating playthrough.') + create_playthrough(world) + if args.create_spoiler: # needs spoiler.hashes to be filled, that depend on rom_futures being done + world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase)) + logger.info('Creating final archive.') + with zipfile.ZipFile(output_path(f"AP_{world.seed_name}.zip"), mode="w", compression=zipfile.ZIP_LZMA, + compresslevel=9) as zf: + for file in os.scandir(temp_dir): + zf.write(os.path.join(temp_dir, file), arcname=file.name) logger.info('Done. Enjoy. Total Time: %s', time.perf_counter() - start) return world @@ -581,7 +587,8 @@ def create_playthrough(world): while sphere_candidates: state.sweep_for_events(key_only=True) - # build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres + # build up spheres of collection radius. + # Everything in each sphere is independent from each other in dependencies and only depends on lower spheres sphere = {location for location in sphere_candidates if state.can_reach(location)} @@ -682,7 +689,7 @@ def create_playthrough(world): world.spoiler.paths.update( {str(location): get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere if location.player == player}) - if player in world.alttp_player_ids: + if player in world.get_game_players("A Link to the Past"): for path in dict(world.spoiler.paths).values(): if any(exit == 'Pyramid Fairy' for (_, exit) in path): if world.mode[player] != 'inverted': diff --git a/MultiMystery.py b/MultiMystery.py deleted file mode 100644 index 84b78e0c..00000000 --- a/MultiMystery.py +++ /dev/null @@ -1,226 +0,0 @@ -import os -import subprocess -import sys -import threading -import concurrent.futures -import argparse -import logging -import random -from shutil import which - - -def feedback(text: str): - logging.info(text) - input("Press Enter to ignore and probably crash.") - - -if __name__ == "__main__": - logging.basicConfig(format='%(message)s', level=logging.INFO) - try: - import ModuleUpdate - - ModuleUpdate.update() - - parser = argparse.ArgumentParser(add_help=False) - parser.add_argument('--disable_autohost', action='store_true') - args = parser.parse_args() - - from Utils import get_public_ipv4, get_options - from Mystery import get_seed_name - - options = get_options() - - multi_mystery_options = options["multi_mystery_options"] - output_path = options["general_options"]["output_path"] - enemizer_path = multi_mystery_options["enemizer_path"] - player_files_path = multi_mystery_options["player_files_path"] - target_player_count = multi_mystery_options["players"] - glitch_triforce = multi_mystery_options["glitch_triforce_room"] - race = multi_mystery_options["race"] - plando_options = multi_mystery_options["plando_options"] - create_spoiler = multi_mystery_options["create_spoiler"] - zip_roms = multi_mystery_options["zip_roms"] - zip_diffs = multi_mystery_options["zip_diffs"] - zip_apmcs = multi_mystery_options["zip_apmcs"] - zip_spoiler = multi_mystery_options["zip_spoiler"] - zip_multidata = multi_mystery_options["zip_multidata"] - zip_format = multi_mystery_options["zip_format"] - # zip_password = multi_mystery_options["zip_password"] not at this time - meta_file_path = multi_mystery_options["meta_file_path"] - weights_file_path = multi_mystery_options["weights_file_path"] - pre_roll = multi_mystery_options["pre_roll"] - teams = multi_mystery_options["teams"] - rom_file = options["lttp_options"]["rom_file"] - host = options["server_options"]["host"] - port = options["server_options"]["port"] - - py_version = f"{sys.version_info.major}.{sys.version_info.minor}" - - if not os.path.exists(enemizer_path): - feedback( - f"Enemizer not found at {enemizer_path}, please adjust the path in MultiMystery.py's config or put Enemizer in the default location.") - if not os.path.exists(rom_file): - feedback(f"Base rom is expected as {rom_file} in the Multiworld root folder please place/rename it there.") - player_files = [] - os.makedirs(player_files_path, exist_ok=True) - for file in os.listdir(player_files_path): - lfile = file.lower() - if lfile.endswith(".yaml") and lfile != meta_file_path.lower() and lfile != weights_file_path.lower(): - player_files.append(file) - logging.info(f"Found player's file {file}.") - - player_string = "" - for i, file in enumerate(player_files, 1): - player_string += f"--p{i} \"{os.path.join(player_files_path, file)}\" " - - if os.path.exists("ArchipelagoMystery.exe"): - basemysterycommand = "ArchipelagoMystery.exe" # compiled windows - elif os.path.exists("ArchipelagoMystery"): - basemysterycommand = "./ArchipelagoMystery" # compiled linux - elif which('py'): - basemysterycommand = f"py -{py_version} Mystery.py" # source windows - else: - basemysterycommand = f"python3 Mystery.py" # source others - - weights_file_path = os.path.join(player_files_path, weights_file_path) - if os.path.exists(weights_file_path): - target_player_count = max(len(player_files), target_player_count) - else: - target_player_count = len(player_files) - - if target_player_count == 0: - feedback(f"No player files found. Please put them in a {player_files_path} folder.") - else: - logging.info(f"{target_player_count} Players found.") - seed_name = get_seed_name(random) - command = f"{basemysterycommand} --multi {target_player_count} {player_string} " \ - f"--rom \"{rom_file}\" --enemizercli \"{enemizer_path}\" " \ - f"--outputpath \"{output_path}\" --teams {teams} --plando \"{plando_options}\" " \ - f"--seed_name {seed_name}" - - if create_spoiler: - command += " --create_spoiler" - if create_spoiler == 2: - command += " --skip_playthrough" - if zip_diffs: - command += " --create_diff" - if glitch_triforce: - command += " --glitch_triforce" - 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)}" - if os.path.exists(weights_file_path): - command += f" --weights {weights_file_path}" - if pre_roll: - command += " --pre_roll" - - logging.info(command) - import time - - start = time.perf_counter() - text = subprocess.check_output(command, shell=True).decode() - logging.info(f"Took {time.perf_counter() - start:.3f} seconds to generate multiworld.") - - multidataname = f"AP_{seed_name}.archipelago" - spoilername = f"AP_{seed_name}_Spoiler.txt" - romfilename = "" - - if any((zip_roms, zip_multidata, zip_spoiler, zip_diffs, zip_apmcs)): - import zipfile - - compression = {1: zipfile.ZIP_DEFLATED, - 2: zipfile.ZIP_LZMA, - 3: zipfile.ZIP_BZIP2}[zip_format] - - typical_zip_ending = {1: "zip", - 2: "7z", - 3: "bz2"}[zip_format] - - ziplock = threading.Lock() - - - def pack_file(file: str): - with ziplock: - zf.write(os.path.join(output_path, file), file) - logging.info(f"Packed {file} into zipfile {zipname}") - - - def remove_zipped_file(file: str): - os.remove(os.path.join(output_path, file)) - logging.info(f"Removed {file} which is now present in the zipfile") - - - zipname = os.path.join(output_path, f"AP_{seed_name}.{typical_zip_ending}") - - logging.info(f"Creating zipfile {zipname}") - ipv4 = (host if host else get_public_ipv4()) + ":" + str(port) - - - def _handle_sfc_file(file: str): - if zip_roms: - pack_file(file) - if zip_roms == 2: - remove_zipped_file(file) - - - def _handle_diff_file(file: str): - if zip_diffs > 0: - pack_file(file) - if zip_diffs == 2: - remove_zipped_file(file) - - - def _handle_apmc_file(file: str): - if zip_apmcs: - pack_file(file) - if zip_apmcs == 2: - remove_zipped_file(file) - - - with concurrent.futures.ThreadPoolExecutor() as pool: - futures = [] - files = os.listdir(output_path) - with zipfile.ZipFile(zipname, "w", compression=compression, compresslevel=9) as zf: - for file in files: - if seed_name in file: - if file.endswith(".sfc"): - futures.append(pool.submit(_handle_sfc_file, file)) - elif file.endswith(".apbp"): - futures.append(pool.submit(_handle_diff_file, file)) - elif file.endswith(".apmc"): - futures.append(pool.submit(_handle_apmc_file, file)) - # just handle like a diff file for now - elif file.endswith(".zip"): - futures.append(pool.submit(_handle_diff_file, file)) - - if zip_multidata and os.path.exists(os.path.join(output_path, multidataname)): - pack_file(multidataname) - if zip_multidata == 2: - remove_zipped_file(multidataname) - - if zip_spoiler and create_spoiler: - pack_file(spoilername) - if zip_spoiler == 2: - remove_zipped_file(spoilername) - - for future in futures: - future.result() # make sure we close the zip AFTER any packing is done - - if not args.disable_autohost: - if os.path.exists(os.path.join(output_path, multidataname)): - if os.path.exists("ArchipelagoServer.exe"): - baseservercommand = ["ArchipelagoServer.exe"] # compiled windows - elif os.path.exists("ArchipelagoServer"): - baseservercommand = ["./ArchipelagoServer"] # compiled linux - elif which('py'): - baseservercommand = ["py", f"-{py_version}", "MultiServer.py"] # source windows - else: - baseservercommand = ["python3", "MultiServer.py"] # source others - # don't have a mac to test that. If you try to run compiled on mac, good luck. - subprocess.call(baseservercommand + ["--multidata", os.path.join(output_path, multidataname)]) - except: - import traceback - - traceback.print_exc() - input("Press enter to close") diff --git a/MultiServer.py b/MultiServer.py index 56bc6c83..d7fd17ba 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -1050,6 +1050,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): "players": ctx.get_players_package(), "missing_locations": get_missing_checks(ctx, client), "checked_locations": get_checked_checks(ctx, client), + # get is needed for old multidata that was sparsely populated "slot_data": ctx.slot_data.get(client.slot, {}) }] items = get_received_items(ctx, client.team, client.slot) diff --git a/Utils.py b/Utils.py index b6e03e58..ef446bca 100644 --- a/Utils.py +++ b/Utils.py @@ -200,27 +200,16 @@ def get_default_options() -> dict: "compatibility": 2, "log_network": 0 }, - "multi_mystery_options": { + "generator": { "teams": 1, "enemizer_path": "EnemizerCLI/EnemizerCLI.Core.exe", "player_files_path": "Players", "players": 0, "weights_file_path": "weights.yaml", "meta_file_path": "meta.yaml", - "pre_roll": False, - "create_spoiler": 1, - "zip_roms": 0, - "zip_diffs": 2, - "zip_apmcs": 1, - "zip_spoiler": 0, - "zip_multidata": 1, - "zip_format": 1, + "spoiler": 2, "glitch_triforce_room": 1, "race": 0, - "cpu_threads": 0, - "max_attempts": 0, - "take_first_working": False, - "keep_all_seeds": False, "log_output_path": "Output Logs", "log_level": None, "plando_options": "bosses", diff --git a/WebHostLib/check.py b/WebHostLib/check.py index 86d79b02..f391938f 100644 --- a/WebHostLib/check.py +++ b/WebHostLib/check.py @@ -12,7 +12,7 @@ def allowed_file(filename): return filename.endswith(('.txt', ".yaml", ".zip")) -from Mystery import roll_settings +from Generate import roll_settings from Utils import parse_yaml diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index d879555e..0c270cc7 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -9,7 +9,7 @@ from flask import request, flash, redirect, url_for, session, render_template from worlds.alttp.EntranceRandomizer import parse_arguments from Main import main as ERmain from Main import get_seed, seeddigits -from Mystery import handle_name +from Generate import handle_name import pickle from .models import * @@ -80,7 +80,6 @@ def gen_game(gen_options, race=False, owner=None, sid=None): erargs.outputname = seedname erargs.outputpath = target.name erargs.teams = 1 - erargs.create_diff = True name_counter = Counter() for player, (playerfile, settings) in enumerate(gen_options.items(), 1): diff --git a/WebHostLib/lttpsprites.py b/WebHostLib/lttpsprites.py index 12d98247..81eb9c2d 100644 --- a/WebHostLib/lttpsprites.py +++ b/WebHostLib/lttpsprites.py @@ -3,6 +3,7 @@ import threading import json from Utils import local_path +from worlds.alttp.Rom import Sprite def update_sprites_lttp(): diff --git a/WebHostLib/static/assets/weightedSettings.js b/WebHostLib/static/assets/weightedSettings.js index fbbbd7dd..185b16c6 100644 --- a/WebHostLib/static/assets/weightedSettings.js +++ b/WebHostLib/static/assets/weightedSettings.js @@ -476,6 +476,9 @@ const generateGame = (raceMode = false) => { }).catch((error) => { const userMessage = document.getElementById('user-message'); userMessage.innerText = 'Something went wrong and your game could not be generated.'; + if (error.response.data.text) { + userMessage.innerText += ' ' + error.response.data.text; + } userMessage.classList.add('visible'); window.scrollTo(0, 0); console.error(error); diff --git a/WebHostLib/static/assets/zelda3/player-settings.js b/WebHostLib/static/assets/zelda3/player-settings.js index 199ef6bf..f915260e 100644 --- a/WebHostLib/static/assets/zelda3/player-settings.js +++ b/WebHostLib/static/assets/zelda3/player-settings.js @@ -173,6 +173,9 @@ const generateGame = (raceMode = false) => { }).catch((error) => { const userMessage = document.getElementById('user-message'); userMessage.innerText = 'Something went wrong and your game could not be generated.'; + if (error.response.data.text) { + userMessage.innerText += ' ' + error.response.data.text; + } userMessage.classList.add('visible'); window.scrollTo(0, 0); console.error(error); diff --git a/host.yaml b/host.yaml index a3fab015..0f63d80f 100644 --- a/host.yaml +++ b/host.yaml @@ -43,10 +43,10 @@ server_options: compatibility: 2 # log all server traffic, mostly for dev use log_network: 0 -# Options for MultiMystery.py -multi_mystery_options: +# Options for Generation +generator: # Teams - # Note that there is currently no way to supply names for teams 2+ through MultiMystery + # Note that this feature is TODO: to move it to dynamic creation on server, not during generation teams: 1 # Location of your Enemizer CLI, available here: https://github.com/Ijwu/Enemizer/releases enemizer_path: "EnemizerCLI/EnemizerCLI.Core.exe" @@ -57,51 +57,20 @@ multi_mystery_options: # general weights file, within the stated player_files_path location # gets used if players is higher than the amount of per-player files found to fill remaining slots weights_file_path: "weights.yaml" - # Meta file name, within the stated player_files_path location + # Meta file name, within the stated player_files_path location, TODO: re-implement this meta_file_path: "meta.yaml" - # Option to pre-roll a yaml that will be used to roll future seeds with the exact same settings every single time. - # If using a pre-rolled yaml fails with "Please fix your yaml.", please file a bug report including both the original yaml - # as well as the generated pre-rolled yaml. - pre_roll: false # Create a spoiler file # 0 -> None - # 1 -> Full spoiler - # 2 -> Spoiler without playthrough - create_spoiler: 1 - # Zip the resulting roms - # 0 -> Don't - # 1 -> Create a zip - # 2 -> Create a zip and delete the ROMs that will be in it, except the hosts (requires player_name to be set correctly) - zip_roms: 0 - # Zip diffs - # -1 -> Create them without zipping - # 2 -> Delete the non-zipped one. - zip_diffs: 2 - # Zip apmc files for Minecraft - # 0 -> Don't zip - # 1 -> Create a zip - # 2 -> Create a zip and delete apmc files inside of it - zip_apmcs: 1 - # Zip spoiler log - # 1 -> Include the spoiler log in the zip - # 2 -> Delete the non-zipped one - zip_spoiler: 0 - # Zip multidata - # 1 -> Include the multidata file in the zip - # 2 -> Delete the non-zipped one, which also means the server won't autostart - zip_multidata: 1 - # Zip algorithm - # 1 -> Zip is recommended for patch files - # 2 -> 7z is recommended for roms. All of them get the job done. - # 3 -> bz2 - zip_format: 1 + # 1 -> Spoiler without playthrough + # 2 -> Full spoiler + spoiler: 2 # Glitch to Triforce room from Ganon # When disabled, you have to have a weapon that can hurt ganon (master sword or swordless/easy item functionality + hammer) # and have completed the goal required for killing ganon to be able to access the triforce room. # 1 -> Enabled. # 0 -> Disabled (except in no-logic) glitch_triforce_room: 1 - # Create encrypted race roms + # Create encrypted race roms and flag games as race mode race: 0 # List of options that can be plando'd. Can be combined, for example "bosses, items" # Available options: bosses, items, texts, connections diff --git a/setup.py b/setup.py index 2cccb2e1..a6152904 100644 --- a/setup.py +++ b/setup.py @@ -56,11 +56,12 @@ def manifest_creation(folder): print("Created Manifest") -scripts = {"LttPClient.py": "ArchipelagoLttPClient", - "MultiMystery.py": "ArchipelagoMultiMystery", - "MultiServer.py": "ArchipelagoServer", - "Mystery.py": "ArchipelagoMystery", - "LttPAdjuster.py": "ArchipelagoLttPAdjuster"} +scripts = { + "LttPClient.py": "ArchipelagoLttPClient", + "MultiServer.py": "ArchipelagoServer", + "Generate.py": "ArchipelagoGenerate", + "LttPAdjuster.py": "ArchipelagoLttPAdjuster" +} exes = [] diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 16603a29..ca2529ac 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -21,6 +21,7 @@ class AutoWorldRegister(type): AutoWorldRegister.world_types[dct["game"]] = new_class return new_class + class AutoLogicRegister(type): def __new__(cls, name, bases, dct): new_class = super().__new__(cls, name, bases, dct) @@ -31,14 +32,15 @@ class AutoLogicRegister(type): setattr(CollectionState, item_name, function) return new_class -def call_single(world: MultiWorld, method_name: str, player: int): + +def call_single(world: MultiWorld, method_name: str, player: int, *args): method = getattr(world.worlds[player], method_name) - return method() + return method(*args) -def call_all(world: MultiWorld, method_name: str): +def call_all(world: MultiWorld, method_name: str, *args): for player in world.player_ids: - call_single(world, method_name, player) + call_single(world, method_name, player, *args) class World(metaclass=AutoWorldRegister): @@ -93,11 +95,15 @@ class World(metaclass=AutoWorldRegister): def generate_basic(self): pass - def generate_output(self): + def generate_output(self, output_directory: str): """This method gets called from a threadpool, do not use world.random here. If you need any last-second randomization, use MultiWorld.slot_seeds[slot] instead.""" pass + def fill_slot_data(self): + """Fill in the slot_data field in the Connected network package.""" + return {} + def get_required_client_version(self) -> Tuple[int, int, int]: return 0, 0, 3 diff --git a/worlds/__init__.py b/worlds/__init__.py index 2d053f9d..4760a3c6 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -32,7 +32,3 @@ network_data_package = { "version": sum(world.data_version for world in AutoWorldRegister.world_types.values()), "games": games, } - -import json -with open("datapackagegroups.json", "w") as f: - json.dump(network_data_package, f, indent=4) \ No newline at end of file diff --git a/worlds/alttp/EntranceRandomizer.py b/worlds/alttp/EntranceRandomizer.py index d76c074d..6dcc0a1b 100644 --- a/worlds/alttp/EntranceRandomizer.py +++ b/worlds/alttp/EntranceRandomizer.py @@ -337,8 +337,6 @@ def parse_arguments(argv, no_defaults=False): parser.add_argument('--game', default="A Link to the Past") parser.add_argument('--race', default=defval(False), action='store_true') parser.add_argument('--outputname') - parser.add_argument('--create_diff', default=defval(False), action='store_true', help='''\ - create a binary patch file from which the randomized rom can be recreated using MultiClient.''') parser.add_argument('--disable_glitch_boots', default=defval(False), action='store_true', help='''\ turns off starting with Pegasus Boots in glitched modes.''') parser.add_argument('--start_hints') diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index 7cd83dad..45103836 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -511,7 +511,7 @@ def create_dynamic_shop_locations(world, player): def fill_prizes(world, attempts=15): all_state = world.get_all_state(keys=True) - for player in world.alttp_player_ids: + for player in world.get_game_players("A Link to the Past"): crystals = ItemFactory(['Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5', 'Crystal 6'], player) crystal_locations = [world.get_location('Turtle Rock - Prize', player), world.get_location('Eastern Palace - Prize', player), world.get_location('Desert Palace - Prize', player), world.get_location('Tower of Hera - Prize', player), world.get_location('Palace of Darkness - Prize', player), world.get_location('Thieves\' Town - Prize', player), world.get_location('Skull Woods - Prize', player), world.get_location('Swamp Palace - Prize', player), world.get_location('Ice Palace - Prize', player), diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index c587eafe..388138c3 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -43,7 +43,7 @@ recipe_time_scales = { Options.RecipeTime.option_vanilla: None } -def generate_mod(world): +def generate_mod(world, output_directory: str): player = world.player multiworld = world.world global data_final_template, locale_template, control_template, data_template @@ -92,7 +92,7 @@ def generate_mod(world): data_template_code = data_template.render(**template_data) data_final_fixes_code = data_final_template.render(**template_data) - mod_dir = Utils.output_path(mod_name) + "_" + Utils.__version__ + mod_dir = os.path.join(output_directory, mod_name + "_" + Utils.__version__) en_locale_dir = os.path.join(mod_dir, "locale", "en") os.makedirs(en_locale_dir, exist_ok=True) shutil.copytree(Utils.local_path("data", "factorio", "mod"), mod_dir, dirs_exist_ok=True) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 8321ca55..5a920ee7 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -72,15 +72,12 @@ class HKWorld(World): def set_rules(self): set_rules(self.world, self.player) - def create_regions(self): create_regions(self.world, self.player) - def generate_output(self): pass # Hollow Knight needs no output files - def fill_slot_data(self): slot_data = {} for option_name in self.options: @@ -92,6 +89,7 @@ class HKWorld(World): item_data = item_table[name] return HKItem(name, item_data.advancement, item_data.id, item_data.type, self.player) + def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None): ret = Region(name, None, name, player) ret.world = world diff --git a/worlds/minecraft/__init__.py b/worlds/minecraft/__init__.py index faddaf46..bfc6ca24 100644 --- a/worlds/minecraft/__init__.py +++ b/worlds/minecraft/__init__.py @@ -78,7 +78,7 @@ class MinecraftWorld(World): self.world.regions += [MCRegion(*r) for r in mc_regions] link_minecraft_structures(self.world, self.player) - def generate_output(self): + def generate_output(self, output_directory: str): import json from base64 import b64encode from Utils import output_path