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(<game>) now replaces <game>_player_ids
Python venv should now work properly
This commit is contained in:
Fabian Dill 2021-07-21 18:08:15 +02:00
parent 47f7ec16c0
commit 2fc4006dfa
21 changed files with 305 additions and 645 deletions

View File

@ -6,7 +6,7 @@ import logging
import json import json
import functools import functools
from collections import OrderedDict, Counter, deque from collections import OrderedDict, Counter, deque
from typing import * from typing import List, Dict, Optional, Set, Iterable, Union
import secrets import secrets
import random import random
@ -14,14 +14,14 @@ import random
class MultiWorld(): class MultiWorld():
debug_types = False debug_types = False
player_names: Dict[int, List[str]] player_names: Dict[int, List[str]]
_region_cache: dict _region_cache: Dict[int, Dict[str, Region]]
difficulty_requirements: dict difficulty_requirements: dict
required_medallions: dict required_medallions: dict
dark_room_logic: Dict[int, str] dark_room_logic: Dict[int, str]
restrict_dungeon_item_on_boss: Dict[int, bool] restrict_dungeon_item_on_boss: Dict[int, bool]
plando_texts: List[Dict[str, str]] plando_texts: List[Dict[str, str]]
plando_items: List[PlandoItem] plando_items: List
plando_connections: List[PlandoConnection] plando_connections: List
er_seeds: Dict[int, str] er_seeds: Dict[int, str]
worlds: Dict[int, "AutoWorld.World"] worlds: Dict[int, "AutoWorld.World"]
is_race: bool = False is_race: bool = False
@ -157,22 +157,9 @@ class MultiWorld():
def player_ids(self): def player_ids(self):
return tuple(range(1, self.players + 1)) return tuple(range(1, self.players + 1))
# Todo: make these automatic, or something like get_players_for_game(game_name) @functools.lru_cache()
@functools.cached_property def get_game_players(self, game_name: str):
def alttp_player_ids(self): return tuple(player for player in self.player_ids if self.game[player] == game_name)
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")
def get_name_string_for_object(self, obj) -> str: 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)})' 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) self.worlds[item.player].collect(ret, item)
if keys: 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] world = self.worlds[p]
from worlds.alttp.Items import ItemFactory from worlds.alttp.Items import ItemFactory
for item in ItemFactory( for item in ItemFactory(
@ -1226,7 +1213,7 @@ class Spoiler(object):
def parse_data(self): def parse_data(self):
self.medallions = OrderedDict() 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'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] 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'] shopdata['item_{}'.format(index)] += ", {} - {}".format(item['replacement'], item['replacement_price']) if item['replacement_price'] else item['replacement']
self.shops.append(shopdata) 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)] = OrderedDict()
self.bosses[str(player)]["Eastern Palace"] = self.world.get_dungeon("Eastern Palace", player).boss.name 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 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] 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') 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): for team in range(self.world.teams):
outfile.write('%s%s\n' % ( outfile.write('%s%s\n' % (
f"Hash - {self.world.player_names[player][team]} (Team {team + 1}): " if 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])) self.hashes[player, team]))
outfile.write('Logic: %s\n' % self.metadata['logic'][player]) outfile.write('Logic: %s\n' % self.metadata['logic'][player])
@ -1473,10 +1460,10 @@ class Spoiler(object):
outfile.write('\n\nMedallions:\n') outfile.write('\n\nMedallions:\n')
for dungeon, medallion in self.medallions.items(): for dungeon, medallion in self.medallions.items():
outfile.write(f'\n{dungeon}: {medallion}') outfile.write(f'\n{dungeon}: {medallion}')
factorio_players = self.world.get_game_players("Factorio")
if self.world.factorio_player_ids: if factorio_players:
outfile.write('\n\nRecipes:\n') 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) name = self.world.get_player_names(player)
for recipe in self.world.worlds[player].custom_recipes.values(): for recipe in self.world.worlds[player].custom_recipes.values():
outfile.write(f"\n{recipe.name} ({name}): {recipe.ingredients} -> {recipe.products}") 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\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)) 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': if self.world.boss_shuffle[player] != 'none':
bossmap = self.bosses[str(player)] if self.world.players > 1 else self.bosses 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') 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))) path_listings.append("{}\n {}".format(location, "\n => ".join(path_lines)))
outfile.write('\n'.join(path_listings)) outfile.write('\n'.join(path_listings))
from worlds.generic import PlandoItem, PlandoConnection

View File

@ -3,9 +3,10 @@ import typing
import collections import collections
import itertools import itertools
from BaseClasses import CollectionState, PlandoItem, Location, MultiWorld from BaseClasses import CollectionState, Location, MultiWorld
from worlds.alttp.Items import ItemFactory from worlds.alttp.Items import ItemFactory
from worlds.alttp.Regions import key_drop_data from worlds.alttp.Regions import key_drop_data
from worlds.generic import PlandoItem
class FillError(RuntimeError): class FillError(RuntimeError):
@ -91,7 +92,7 @@ def distribute_items_restrictive(world: MultiWorld, gftower_trash=False, fill_lo
standard_keyshuffle_players = set() standard_keyshuffle_players = set()
# fill in gtower locations with trash first # 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 \ if not gftower_trash or not world.ganonstower_vanilla[player] or \
world.logic[player] in {'owglitches', 'hybridglitches', "nologic"}: world.logic[player] in {'owglitches', 'hybridglitches', "nologic"}:
gtower_trash_count = 0 gtower_trash_count = 0

View File

@ -14,7 +14,7 @@ from worlds.generic import PlandoItem, PlandoConnection
ModuleUpdate.update() 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 worlds.alttp.EntranceRandomizer import parse_arguments
from Main import main as ERmain from Main import main as ERmain
from Main import get_seed, seeddigits from Main import get_seed, seeddigits
@ -28,40 +28,38 @@ from worlds.AutoWorld import AutoWorldRegister
categories = set(AutoWorldRegister.world_types) categories = set(AutoWorldRegister.world_types)
def mystery_argparse(): def mystery_argparse():
parser = argparse.ArgumentParser(add_help=False) options = get_options()
parser.add_argument('--multi', default=1, type=lambda value: min(max(int(value), 1), 255)) defaults = options["generator"]
multiargs, _ = parser.parse_known_args()
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser(description="CMD Generation Interface, defaults come from host.yaml.")
parser.add_argument('--weights', 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') 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', parser.add_argument('--samesettings', help='Rolls settings per weights file rather than per player',
action='store_true') 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('--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('--teams', default=1, type=lambda value: max(int(value), 1))
parser.add_argument('--create_spoiler', action='store_true') parser.add_argument('--spoiler', type=int, default=defaults["spoiler"])
parser.add_argument('--skip_playthrough', action='store_true') parser.add_argument('--rom', default=options["lttp_options"]["rom_file"], help="Path to the 1.0 JP LttP Baserom.")
parser.add_argument('--pre_roll', action='store_true') parser.add_argument('--enemizercli', default=defaults["enemizer_path"])
parser.add_argument('--rom') parser.add_argument('--outputpath', default=options["general_options"]["output_path"])
parser.add_argument('--enemizercli') parser.add_argument('--race', action='store_true', default=defaults["race"])
parser.add_argument('--outputpath') parser.add_argument('--meta_file_path', default=defaults["meta_file_path"])
parser.add_argument('--glitch_triforce', action='store_true')
parser.add_argument('--race', action='store_true')
parser.add_argument('--meta', default=None)
parser.add_argument('--log_output_path', help='Path to store output log') 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('--log_level', default='info', help='Sets log level')
parser.add_argument('--create_diff', action="store_true")
parser.add_argument('--yaml_output', default=0, type=lambda value: min(max(int(value), 0), 255), 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)') 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"') 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() 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(",")} args.plando: typing.Set[str] = {arg.strip().lower() for arg in args.plando.split(",")}
return args return args, options
def get_seed_name(random): def get_seed_name(random):
@ -70,106 +68,93 @@ def get_seed_name(random):
def main(args=None, callback=ERmain): def main(args=None, callback=ERmain):
if not args: if not args:
args = mystery_argparse() args, options = mystery_argparse()
seed = get_seed(args.seed) seed = get_seed(args.seed)
random.seed(seed) random.seed(seed)
seed_name = args.seed_name if args.seed_name else get_seed_name(random) seed_name = get_seed_name(random)
print(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, {seed_name} Seed {seed}")
if args.race: if args.race:
random.seed() # reset to time-based random source random.seed() # reset to time-based random source
weights_cache = {} weights_cache = {}
if args.weights: if args.weights_file_path and os.path.exists(args.weights_file_path):
try: 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: except Exception as e:
raise ValueError(f"File {args.weights} is destroyed. Please fix your yaml.") from e raise ValueError(f"File {args.weights_file_path} is destroyed. Please fix your yaml.") from e
print(f"Weights: {args.weights} >> " print(f"Weights: {args.weights_file_path} >> "
f"{get_choice('description', weights_cache[args.weights], 'No description specified')}") f"{get_choice('description', weights_cache[args.weights_file_path], 'No description specified')}")
if args.meta:
if args.meta_file_path and os.path.exists(args.meta_file_path):
try: 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: except Exception as e:
raise ValueError(f"File {args.meta} is destroyed. Please fix your yaml.") from e raise ValueError(f"File {args.meta_file_path} is destroyed. Please fix your yaml.") from e
meta_weights = weights_cache[args.meta] meta_weights = weights_cache[args.meta_file_path]
print(f"Meta: {args.meta} >> {get_choice('meta_description', meta_weights, 'No description specified')}") print(f"Meta: {args.meta_file_path} >> {get_choice('meta_description', meta_weights, 'No description specified')}")
if args.samesettings: if args.samesettings:
raise Exception("Cannot mix --samesettings with --meta") raise Exception("Cannot mix --samesettings with --meta")
else:
for player in range(1, args.multi + 1): meta_weights = None
path = getattr(args, f'p{player}') player_id = 1
if path: 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: try:
if path not in weights_cache: weights_cache[fname] = read_weights_yaml(path)
weights_cache[path] = read_weights_yaml(path)
print(f"P{player} Weights: {path} >> "
f"{get_choice('description', weights_cache[path], 'No description specified')}")
except Exception as e: 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 = parse_arguments(['--multi', str(args.multi)])
erargs.seed = seed erargs.seed = seed
erargs.name = {x: "" for x in range(1, args.multi + 1)} # only so it can be overwrittin in mystery 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_spoiler = args.spoiler > 0
erargs.create_diff = args.create_diff erargs.glitch_triforce = options["generator"]["glitch_triforce_room"]
erargs.glitch_triforce = args.glitch_triforce
erargs.race = args.race erargs.race = args.race
erargs.skip_playthrough = args.skip_playthrough erargs.skip_playthrough = args.spoiler == 0
erargs.outputname = seed_name erargs.outputname = seed_name
erargs.outputpath = args.outputpath erargs.outputpath = args.outputpath
erargs.teams = args.teams erargs.teams = args.teams
# set up logger # set up logger
if args.loglevel: if args.log_level:
erargs.loglevel = args.loglevel erargs.loglevel = args.log_level
loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[ loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[
erargs.loglevel] erargs.loglevel]
if args.log_output_path: 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) 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")) filename=os.path.join(args.log_output_path, f"{seed}.log"))
else: else:
logging.basicConfig(format='%(message)s', level=loglevel) logging.basicConfig(format='%(message)s', level=loglevel, force=True)
if args.rom:
erargs.rom = args.rom
if args.enemizercli: erargs.rom = args.rom
erargs.enemizercli = args.enemizercli erargs.enemizercli = args.enemizercli
settings_cache = {k: (roll_settings(v, args.plando) if args.samesettings else None) settings_cache = {k: (roll_settings(v, args.plando) if args.samesettings else None)
for k, v in weights_cache.items()} for k, v in weights_cache.items()}
player_path_cache = {} player_path_cache = {}
for player in range(1, args.multi + 1): 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(): for player, path in player_path_cache.items():
weights_cache[path].setdefault("meta_ignore", []) weights_cache[path].setdefault("meta_ignore", [])
meta_weights = weights_cache[args.meta]
for key in meta_weights: for key in meta_weights:
option = get_choice(key, meta_weights) option = get_choice(key, meta_weights)
if option is not None: if option is not None:
@ -188,31 +173,6 @@ def main(args=None, callback=ERmain):
try: try:
settings = settings_cache[path] if settings_cache[path] else \ settings = settings_cache[path] if settings_cache[path] else \
roll_settings(weights_cache[path], args.plando) 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(): for k, v in vars(settings).items():
if v is not None: if v is not None:
try: try:
@ -223,7 +183,7 @@ def main(args=None, callback=ERmain):
raise ValueError(f"File {path} is destroyed. Please fix your yaml.") from e raise ValueError(f"File {path} is destroyed. Please fix your yaml.") from e
else: else:
raise RuntimeError(f'No weights specified for player {player}') 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}" erargs.name[player] = f"Player{player}"
elif not erargs.name[player]: # if name was not specified, generate it from filename 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] 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",))): 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: if "linked_options" in weights:
weights = roll_linked_options(weights) weights = roll_linked_options(weights)

345
Main.py
View File

@ -6,6 +6,8 @@ import time
import zlib import zlib
import concurrent.futures import concurrent.futures
import pickle import pickle
import tempfile
import zipfile
from typing import Dict, Tuple from typing import Dict, Tuple
from BaseClasses import MultiWorld, CollectionState, Region, Item from BaseClasses import MultiWorld, CollectionState, Region, Item
@ -128,7 +130,7 @@ def main(args, seed=None):
AutoWorld.call_all(world, "generate_early") AutoWorld.call_all(world, "generate_early")
# system for sharing ER layouts # 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)) world.er_seeds[player] = str(world.random.randint(0, 2 ** 64))
if "-" in world.shuffle[player]: if "-" in world.shuffle[player]:
@ -160,7 +162,7 @@ def main(args, seed=None):
world.player_names[player].append(name) world.player_names[player].append(name)
logger.info('') 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]] world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
for player in world.player_ids: for player in world.player_ids:
@ -168,7 +170,7 @@ def main(args, seed=None):
world.push_precollected(world.create_item(item_name, player)) world.push_precollected(world.create_item(item_name, player))
for player in world.player_ids: 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. # enforce pre-defined local items.
if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]: if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]:
world.local_items[player].add('Triforce Piece') world.local_items[player].add('Triforce Piece')
@ -195,7 +197,7 @@ def main(args, seed=None):
AutoWorld.call_all(world, "create_regions") 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': if world.open_pyramid[player] == 'goal':
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt',
'localganontriforcehunt', 'ganonpedestal'} 'localganontriforcehunt', 'ganonpedestal'}
@ -220,7 +222,7 @@ def main(args, seed=None):
logger.info('Shuffling the World about.') 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 \ if world.logic[player] not in ["noglitches", "minorglitches"] and world.shuffle[player] in \
{"vanilla", "dungeonssimple", "dungeonsfull", "simple", "restricted", "full"}: {"vanilla", "dungeonssimple", "dungeonsfull", "simple", "restricted", "full"}:
world.fix_fake_world[player] = False world.fix_fake_world[player] = False
@ -241,7 +243,7 @@ def main(args, seed=None):
logger.info('Generating Item Pool.') 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) generate_itempool(world, player)
logger.info('Calculating Access Rules.') logger.info('Calculating Access Rules.')
@ -251,7 +253,7 @@ def main(args, seed=None):
AutoWorld.call_all(world, "set_rules") 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) set_rules(world, player)
for player in world.player_ids: for player in world.player_ids:
@ -301,7 +303,7 @@ def main(args, seed=None):
outfilebase = 'AP_' + world.seed_name outfilebase = 'AP_' + world.seed_name
rom_names = [] 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] 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.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
or world.shufflepots[player] or world.bush_shuffle[player] 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 "-prog_" + outfilestuffs["progressive"] if outfilestuffs["progressive"] in ['off', 'random'] else "", # A
"-nohints" if not outfilestuffs["hints"] == "True" else "") # B "-nohints" if not outfilestuffs["hints"] == "True" else "") # B
) if not args.outputname else '' ) 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) 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) return player, team, bytes(rom.name)
pool = concurrent.futures.ThreadPoolExecutor() 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 = [] def get_entrance_to_region(region: Region):
output_file_futures = [] for entrance in region.entrances:
for team in range(world.teams): if entrance.parent_region.type in (RegionType.DarkWorld, RegionType.LightWorld, RegionType.Generic):
for player in world.alttp_player_ids: return entrance
rom_futures.append(pool.submit(_gen_rom, team, player)) for entrance in region.entrances: # BFS might be better here, trying DFS for now.
for player in world.player_ids: return get_entrance_to_region(entrance.parent_region)
output_file_futures.append(pool.submit(AutoWorld.call_single, world, "generate_output", player))
def get_entrance_to_region(region: Region): # collect ER hint info
for entrance in region.entrances: er_hint_data = {player: {} for player in world.get_game_players("A Link to the Past") if
if entrance.parent_region.type in (RegionType.DarkWorld, RegionType.LightWorld, RegionType.Generic): world.shuffle[player] != "vanilla" or world.retro[player]}
return entrance from worlds.alttp.Regions import RegionType
for entrance in region.entrances: # BFS might be better here, trying DFS for now. for region in world.regions:
return get_entrance_to_region(entrance.parent_region) 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 ordered_areas = ('Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace',
er_hint_data = {player: {} for player in world.alttp_player_ids if 'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace',
world.shuffle[player] != "vanilla" or world.retro[player]} 'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total")
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', checks_in_area = {player: {area: list() for area in ordered_areas}
'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace', for player in range(1, world.players + 1)}
'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):
for player in range(1, world.players + 1)} checks_in_area[player]["Total"] = 0
for player in range(1, world.players + 1): for location in [loc for loc in world.get_filled_locations() if type(loc.address) is int]:
checks_in_area[player]["Total"] = 0 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]: oldmancaves = []
main_entrance = get_entrance_to_region(location.parent_region) takeanyregions = ["Old Man Sword Cave", "Take-Any #1", "Take-Any #2", "Take-Any #3", "Take-Any #4"]
if location.game != "A Link to the Past": for index, take_any in enumerate(takeanyregions):
checks_in_area[location.player]["Light World"].append(location.address) for region in [world.get_region(take_any, player) for player in range(1, world.players + 1) if
elif location.parent_region.dungeon: world.retro[player]]:
dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower', item = world.create_item(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'],
'Inverted Ganons Tower': 'Ganons Tower'} \ region.player)
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name) player = region.player
checks_in_area[location.player][dungeonname].append(location.address) location_id = SHOP_ID_START + total_shop_slots + index
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 = [] main_entrance = get_entrance_to_region(region)
takeanyregions = ["Old Man Sword Cave", "Take-Any #1", "Take-Any #2", "Take-Any #3", "Take-Any #4"] if main_entrance.parent_region.type == RegionType.LightWorld:
for index, take_any in enumerate(takeanyregions): checks_in_area[player]["Light World"].append(location_id)
for region in [world.get_region(take_any, player) for player in range(1, world.players + 1) if else:
world.retro[player]]: checks_in_area[player]["Dark World"].append(location_id)
item = world.create_item(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'], checks_in_area[player]["Total"] += 1
region.player)
player = region.player
location_id = SHOP_ID_START + total_shop_slots + index
main_entrance = get_entrance_to_region(region) er_hint_data[player][location_id] = main_entrance.name
if main_entrance.parent_region.type == RegionType.LightWorld: oldmancaves.append(((location_id, player), (item.code, player)))
checks_in_area[player]["Light World"].append(location_id)
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: else:
checks_in_area[player]["Dark World"].append(location_id) logger.warning("Location Accessibility requirements not fulfilled.")
checks_in_area[player]["Total"] += 1 if multidata_task:
multidata_task.result() # retrieve exception if one exists
er_hint_data[player][location_id] = main_entrance.name pool.shutdown() # wait for all queued tasks to complete
oldmancaves.append(((location_id, player), (item.code, player))) if not args.skip_playthrough:
logger.info('Calculating playthrough.')
FillDisabledShopSlots(world) create_playthrough(world)
if args.create_spoiler: # needs spoiler.hashes to be filled, that depend on rom_futures being done
def write_multidata(roms, outputs): world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))
import base64 logger.info('Creating final archive.')
import NetUtils with zipfile.ZipFile(output_path(f"AP_{world.seed_name}.zip"), mode="w", compression=zipfile.ZIP_LZMA,
for future in roms: compresslevel=9) as zf:
rom_name = future.result() for file in os.scandir(temp_dir):
rom_names.append(rom_name) zf.write(os.path.join(temp_dir, file), arcname=file.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.info('Done. Enjoy. Total Time: %s', time.perf_counter() - start) logger.info('Done. Enjoy. Total Time: %s', time.perf_counter() - start)
return world return world
@ -581,7 +587,8 @@ def create_playthrough(world):
while sphere_candidates: while sphere_candidates:
state.sweep_for_events(key_only=True) 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)} sphere = {location for location in sphere_candidates if state.can_reach(location)}
@ -682,7 +689,7 @@ def create_playthrough(world):
world.spoiler.paths.update( world.spoiler.paths.update(
{str(location): get_path(state, location.parent_region) for sphere in collection_spheres for location in {str(location): get_path(state, location.parent_region) for sphere in collection_spheres for location in
sphere if location.player == player}) 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(): for path in dict(world.spoiler.paths).values():
if any(exit == 'Pyramid Fairy' for (_, exit) in path): if any(exit == 'Pyramid Fairy' for (_, exit) in path):
if world.mode[player] != 'inverted': if world.mode[player] != 'inverted':

View File

@ -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")

View File

@ -1050,6 +1050,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
"players": ctx.get_players_package(), "players": ctx.get_players_package(),
"missing_locations": get_missing_checks(ctx, client), "missing_locations": get_missing_checks(ctx, client),
"checked_locations": get_checked_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, {}) "slot_data": ctx.slot_data.get(client.slot, {})
}] }]
items = get_received_items(ctx, client.team, client.slot) items = get_received_items(ctx, client.team, client.slot)

View File

@ -200,27 +200,16 @@ def get_default_options() -> dict:
"compatibility": 2, "compatibility": 2,
"log_network": 0 "log_network": 0
}, },
"multi_mystery_options": { "generator": {
"teams": 1, "teams": 1,
"enemizer_path": "EnemizerCLI/EnemizerCLI.Core.exe", "enemizer_path": "EnemizerCLI/EnemizerCLI.Core.exe",
"player_files_path": "Players", "player_files_path": "Players",
"players": 0, "players": 0,
"weights_file_path": "weights.yaml", "weights_file_path": "weights.yaml",
"meta_file_path": "meta.yaml", "meta_file_path": "meta.yaml",
"pre_roll": False, "spoiler": 2,
"create_spoiler": 1,
"zip_roms": 0,
"zip_diffs": 2,
"zip_apmcs": 1,
"zip_spoiler": 0,
"zip_multidata": 1,
"zip_format": 1,
"glitch_triforce_room": 1, "glitch_triforce_room": 1,
"race": 0, "race": 0,
"cpu_threads": 0,
"max_attempts": 0,
"take_first_working": False,
"keep_all_seeds": False,
"log_output_path": "Output Logs", "log_output_path": "Output Logs",
"log_level": None, "log_level": None,
"plando_options": "bosses", "plando_options": "bosses",

View File

@ -12,7 +12,7 @@ def allowed_file(filename):
return filename.endswith(('.txt', ".yaml", ".zip")) return filename.endswith(('.txt', ".yaml", ".zip"))
from Mystery import roll_settings from Generate import roll_settings
from Utils import parse_yaml from Utils import parse_yaml

View File

@ -9,7 +9,7 @@ from flask import request, flash, redirect, url_for, session, render_template
from worlds.alttp.EntranceRandomizer import parse_arguments from worlds.alttp.EntranceRandomizer import parse_arguments
from Main import main as ERmain from Main import main as ERmain
from Main import get_seed, seeddigits from Main import get_seed, seeddigits
from Mystery import handle_name from Generate import handle_name
import pickle import pickle
from .models import * from .models import *
@ -80,7 +80,6 @@ def gen_game(gen_options, race=False, owner=None, sid=None):
erargs.outputname = seedname erargs.outputname = seedname
erargs.outputpath = target.name erargs.outputpath = target.name
erargs.teams = 1 erargs.teams = 1
erargs.create_diff = True
name_counter = Counter() name_counter = Counter()
for player, (playerfile, settings) in enumerate(gen_options.items(), 1): for player, (playerfile, settings) in enumerate(gen_options.items(), 1):

View File

@ -3,6 +3,7 @@ import threading
import json import json
from Utils import local_path from Utils import local_path
from worlds.alttp.Rom import Sprite
def update_sprites_lttp(): def update_sprites_lttp():

View File

@ -476,6 +476,9 @@ const generateGame = (raceMode = false) => {
}).catch((error) => { }).catch((error) => {
const userMessage = document.getElementById('user-message'); const userMessage = document.getElementById('user-message');
userMessage.innerText = 'Something went wrong and your game could not be generated.'; 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'); userMessage.classList.add('visible');
window.scrollTo(0, 0); window.scrollTo(0, 0);
console.error(error); console.error(error);

View File

@ -173,6 +173,9 @@ const generateGame = (raceMode = false) => {
}).catch((error) => { }).catch((error) => {
const userMessage = document.getElementById('user-message'); const userMessage = document.getElementById('user-message');
userMessage.innerText = 'Something went wrong and your game could not be generated.'; 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'); userMessage.classList.add('visible');
window.scrollTo(0, 0); window.scrollTo(0, 0);
console.error(error); console.error(error);

View File

@ -43,10 +43,10 @@ server_options:
compatibility: 2 compatibility: 2
# log all server traffic, mostly for dev use # log all server traffic, mostly for dev use
log_network: 0 log_network: 0
# Options for MultiMystery.py # Options for Generation
multi_mystery_options: generator:
# Teams # 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 teams: 1
# Location of your Enemizer CLI, available here: https://github.com/Ijwu/Enemizer/releases # Location of your Enemizer CLI, available here: https://github.com/Ijwu/Enemizer/releases
enemizer_path: "EnemizerCLI/EnemizerCLI.Core.exe" enemizer_path: "EnemizerCLI/EnemizerCLI.Core.exe"
@ -57,51 +57,20 @@ multi_mystery_options:
# general weights file, within the stated player_files_path location # 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 # gets used if players is higher than the amount of per-player files found to fill remaining slots
weights_file_path: "weights.yaml" 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" 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 # Create a spoiler file
# 0 -> None # 0 -> None
# 1 -> Full spoiler # 1 -> Spoiler without playthrough
# 2 -> Spoiler without playthrough # 2 -> Full spoiler
create_spoiler: 1 spoiler: 2
# 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
# Glitch to Triforce room from Ganon # 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) # 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. # and have completed the goal required for killing ganon to be able to access the triforce room.
# 1 -> Enabled. # 1 -> Enabled.
# 0 -> Disabled (except in no-logic) # 0 -> Disabled (except in no-logic)
glitch_triforce_room: 1 glitch_triforce_room: 1
# Create encrypted race roms # Create encrypted race roms and flag games as race mode
race: 0 race: 0
# List of options that can be plando'd. Can be combined, for example "bosses, items" # List of options that can be plando'd. Can be combined, for example "bosses, items"
# Available options: bosses, items, texts, connections # Available options: bosses, items, texts, connections

View File

@ -56,11 +56,12 @@ def manifest_creation(folder):
print("Created Manifest") print("Created Manifest")
scripts = {"LttPClient.py": "ArchipelagoLttPClient", scripts = {
"MultiMystery.py": "ArchipelagoMultiMystery", "LttPClient.py": "ArchipelagoLttPClient",
"MultiServer.py": "ArchipelagoServer", "MultiServer.py": "ArchipelagoServer",
"Mystery.py": "ArchipelagoMystery", "Generate.py": "ArchipelagoGenerate",
"LttPAdjuster.py": "ArchipelagoLttPAdjuster"} "LttPAdjuster.py": "ArchipelagoLttPAdjuster"
}
exes = [] exes = []

View File

@ -21,6 +21,7 @@ class AutoWorldRegister(type):
AutoWorldRegister.world_types[dct["game"]] = new_class AutoWorldRegister.world_types[dct["game"]] = new_class
return new_class return new_class
class AutoLogicRegister(type): class AutoLogicRegister(type):
def __new__(cls, name, bases, dct): def __new__(cls, name, bases, dct):
new_class = super().__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) setattr(CollectionState, item_name, function)
return new_class 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) 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: for player in world.player_ids:
call_single(world, method_name, player) call_single(world, method_name, player, *args)
class World(metaclass=AutoWorldRegister): class World(metaclass=AutoWorldRegister):
@ -93,11 +95,15 @@ class World(metaclass=AutoWorldRegister):
def generate_basic(self): def generate_basic(self):
pass 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. """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.""" If you need any last-second randomization, use MultiWorld.slot_seeds[slot] instead."""
pass 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]: def get_required_client_version(self) -> Tuple[int, int, int]:
return 0, 0, 3 return 0, 0, 3

View File

@ -32,7 +32,3 @@ network_data_package = {
"version": sum(world.data_version for world in AutoWorldRegister.world_types.values()), "version": sum(world.data_version for world in AutoWorldRegister.world_types.values()),
"games": games, "games": games,
} }
import json
with open("datapackagegroups.json", "w") as f:
json.dump(network_data_package, f, indent=4)

View File

@ -337,8 +337,6 @@ def parse_arguments(argv, no_defaults=False):
parser.add_argument('--game', default="A Link to the Past") parser.add_argument('--game', default="A Link to the Past")
parser.add_argument('--race', default=defval(False), action='store_true') parser.add_argument('--race', default=defval(False), action='store_true')
parser.add_argument('--outputname') 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='''\ parser.add_argument('--disable_glitch_boots', default=defval(False), action='store_true', help='''\
turns off starting with Pegasus Boots in glitched modes.''') turns off starting with Pegasus Boots in glitched modes.''')
parser.add_argument('--start_hints') parser.add_argument('--start_hints')

View File

@ -511,7 +511,7 @@ def create_dynamic_shop_locations(world, player):
def fill_prizes(world, attempts=15): def fill_prizes(world, attempts=15):
all_state = world.get_all_state(keys=True) 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) 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), 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), 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),

View File

@ -43,7 +43,7 @@ recipe_time_scales = {
Options.RecipeTime.option_vanilla: None Options.RecipeTime.option_vanilla: None
} }
def generate_mod(world): def generate_mod(world, output_directory: str):
player = world.player player = world.player
multiworld = world.world multiworld = world.world
global data_final_template, locale_template, control_template, data_template 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_template_code = data_template.render(**template_data)
data_final_fixes_code = data_final_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") en_locale_dir = os.path.join(mod_dir, "locale", "en")
os.makedirs(en_locale_dir, exist_ok=True) os.makedirs(en_locale_dir, exist_ok=True)
shutil.copytree(Utils.local_path("data", "factorio", "mod"), mod_dir, dirs_exist_ok=True) shutil.copytree(Utils.local_path("data", "factorio", "mod"), mod_dir, dirs_exist_ok=True)

View File

@ -72,15 +72,12 @@ class HKWorld(World):
def set_rules(self): def set_rules(self):
set_rules(self.world, self.player) set_rules(self.world, self.player)
def create_regions(self): def create_regions(self):
create_regions(self.world, self.player) create_regions(self.world, self.player)
def generate_output(self): def generate_output(self):
pass # Hollow Knight needs no output files pass # Hollow Knight needs no output files
def fill_slot_data(self): def fill_slot_data(self):
slot_data = {} slot_data = {}
for option_name in self.options: for option_name in self.options:
@ -92,6 +89,7 @@ class HKWorld(World):
item_data = item_table[name] item_data = item_table[name]
return HKItem(name, item_data.advancement, item_data.id, item_data.type, self.player) 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): def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
ret = Region(name, None, name, player) ret = Region(name, None, name, player)
ret.world = world ret.world = world

View File

@ -78,7 +78,7 @@ class MinecraftWorld(World):
self.world.regions += [MCRegion(*r) for r in mc_regions] self.world.regions += [MCRegion(*r) for r in mc_regions]
link_minecraft_structures(self.world, self.player) link_minecraft_structures(self.world, self.player)
def generate_output(self): def generate_output(self, output_directory: str):
import json import json
from base64 import b64encode from base64 import b64encode
from Utils import output_path from Utils import output_path