[Core] Allow multiple worlds in one yaml (#428)
This commit is contained in:
parent
8e68aa0ccd
commit
618bdfc917
105
Generate.py
105
Generate.py
|
@ -3,7 +3,7 @@ import logging
|
||||||
import random
|
import random
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import typing
|
from typing import Set, Dict, Tuple, Callable, Any, Union
|
||||||
import os
|
import os
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
import string
|
import string
|
||||||
|
@ -15,7 +15,7 @@ ModuleUpdate.update()
|
||||||
import Utils
|
import Utils
|
||||||
from worlds.alttp import Options as LttPOptions
|
from worlds.alttp import Options as LttPOptions
|
||||||
from worlds.generic import PlandoConnection
|
from worlds.generic import PlandoConnection
|
||||||
from Utils import parse_yaml, version_tuple, __version__, tuplize_version, get_options, local_path, user_path
|
from Utils import parse_yamls, version_tuple, __version__, tuplize_version, get_options, local_path, user_path
|
||||||
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 BaseClasses import seeddigits, get_seed
|
from BaseClasses import seeddigits, get_seed
|
||||||
|
@ -32,7 +32,7 @@ def mystery_argparse():
|
||||||
options = get_options()
|
options = get_options()
|
||||||
defaults = options["generator"]
|
defaults = options["generator"]
|
||||||
|
|
||||||
def resolve_path(path: str, resolver: typing.Callable[[str], str]) -> str:
|
def resolve_path(path: str, resolver: Callable[[str], str]) -> str:
|
||||||
return path if os.path.isabs(path) else resolver(path)
|
return path if os.path.isabs(path) else resolver(path)
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="CMD Generation Interface, defaults come from host.yaml.")
|
parser = argparse.ArgumentParser(description="CMD Generation Interface, defaults come from host.yaml.")
|
||||||
|
@ -64,7 +64,7 @@ def mystery_argparse():
|
||||||
args.weights_file_path = os.path.join(args.player_files_path, 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):
|
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.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: Set[str] = {arg.strip().lower() for arg in args.plando.split(",")}
|
||||||
return args, options
|
return args, options
|
||||||
|
|
||||||
|
|
||||||
|
@ -83,21 +83,21 @@ def main(args=None, callback=ERmain):
|
||||||
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: Dict[str, Tuple[Any, ...]] = {}
|
||||||
if args.weights_file_path and os.path.exists(args.weights_file_path):
|
if args.weights_file_path and os.path.exists(args.weights_file_path):
|
||||||
try:
|
try:
|
||||||
weights_cache[args.weights_file_path] = read_weights_yaml(args.weights_file_path)
|
weights_cache[args.weights_file_path] = read_weights_yamls(args.weights_file_path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"File {args.weights_file_path} 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_file_path} >> "
|
print(f"Weights: {args.weights_file_path} >> "
|
||||||
f"{get_choice('description', weights_cache[args.weights_file_path], 'No description specified')}")
|
f"{get_choice('description', weights_cache[args.weights_file_path][-1], 'No description specified')}")
|
||||||
|
|
||||||
if args.meta_file_path and os.path.exists(args.meta_file_path):
|
if args.meta_file_path and os.path.exists(args.meta_file_path):
|
||||||
try:
|
try:
|
||||||
weights_cache[args.meta_file_path] = read_weights_yaml(args.meta_file_path)
|
weights_cache[args.meta_file_path] = read_weights_yamls(args.meta_file_path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"File {args.meta_file_path} 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_file_path]
|
meta_weights = weights_cache[args.meta_file_path][-1]
|
||||||
print(f"Meta: {args.meta_file_path} >> {get_choice('meta_description', meta_weights)}")
|
print(f"Meta: {args.meta_file_path} >> {get_choice('meta_description', meta_weights)}")
|
||||||
del(meta_weights["meta_description"])
|
del(meta_weights["meta_description"])
|
||||||
if args.samesettings:
|
if args.samesettings:
|
||||||
|
@ -111,14 +111,15 @@ def main(args=None, callback=ERmain):
|
||||||
if file.is_file() and os.path.join(args.player_files_path, fname) not in {args.meta_file_path, args.weights_file_path}:
|
if file.is_file() and os.path.join(args.player_files_path, fname) not in {args.meta_file_path, args.weights_file_path}:
|
||||||
path = os.path.join(args.player_files_path, fname)
|
path = os.path.join(args.player_files_path, fname)
|
||||||
try:
|
try:
|
||||||
weights_cache[fname] = read_weights_yaml(path)
|
weights_cache[fname] = read_weights_yamls(path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"File {fname} is destroyed. Please fix your yaml.") from e
|
raise ValueError(f"File {fname} is destroyed. Please fix your yaml.") from e
|
||||||
else:
|
else:
|
||||||
print(f"P{player_id} Weights: {fname} >> "
|
for yaml in weights_cache[fname]:
|
||||||
f"{get_choice('description', weights_cache[fname], 'No description specified')}")
|
print(f"P{player_id} Weights: {fname} >> "
|
||||||
player_files[player_id] = fname
|
f"{get_choice('description', yaml, 'No description specified')}")
|
||||||
player_id += 1
|
player_files[player_id] = fname
|
||||||
|
player_id += 1
|
||||||
|
|
||||||
args.multi = max(player_id-1, args.multi)
|
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} with plando: "
|
print(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, {seed_name} Seed {seed} with plando: "
|
||||||
|
@ -141,8 +142,9 @@ def main(args=None, callback=ERmain):
|
||||||
erargs.sm_rom = args.sm_rom
|
erargs.sm_rom = args.sm_rom
|
||||||
erargs.enemizercli = args.enemizercli
|
erargs.enemizercli = args.enemizercli
|
||||||
|
|
||||||
settings_cache = {k: (roll_settings(v, args.plando) if args.samesettings else None)
|
settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
|
||||||
for k, v in weights_cache.items()}
|
{fname: ( tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None)
|
||||||
|
for fname, yamls 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] = player_files.get(player, args.weights_file_path)
|
player_path_cache[player] = player_files.get(player, args.weights_file_path)
|
||||||
|
@ -153,38 +155,45 @@ def main(args=None, callback=ERmain):
|
||||||
option = get_choice(key, category_dict)
|
option = get_choice(key, category_dict)
|
||||||
if option is not None:
|
if option is not None:
|
||||||
for player, path in player_path_cache.items():
|
for player, path in player_path_cache.items():
|
||||||
if category_name is None:
|
for yaml in weights_cache[path]:
|
||||||
weights_cache[path][key] = option
|
if category_name is None:
|
||||||
elif category_name not in weights_cache[path]:
|
yaml[key] = option
|
||||||
logging.warning(f"Meta: Category {category_name} is not present in {path}.")
|
elif category_name not in yaml:
|
||||||
else:
|
logging.warning(f"Meta: Category {category_name} is not present in {path}.")
|
||||||
weights_cache[path][category_name][key] = option
|
else:
|
||||||
|
yaml[category_name][key] = option
|
||||||
|
|
||||||
name_counter = Counter()
|
name_counter = Counter()
|
||||||
erargs.player_settings = {}
|
erargs.player_settings = {}
|
||||||
for player in range(1, args.multi + 1):
|
|
||||||
|
player = 1
|
||||||
|
while player <= args.multi:
|
||||||
path = player_path_cache[player]
|
path = player_path_cache[player]
|
||||||
if path:
|
if path:
|
||||||
try:
|
try:
|
||||||
settings = settings_cache[path] if settings_cache[path] else \
|
settings: Tuple[argparse.Namespace, ...] = settings_cache[path] if settings_cache[path] else \
|
||||||
roll_settings(weights_cache[path], args.plando)
|
tuple(roll_settings(yaml, args.plando) for yaml in weights_cache[path])
|
||||||
for k, v in vars(settings).items():
|
for settingsObject in settings:
|
||||||
if v is not None:
|
for k, v in vars(settingsObject).items():
|
||||||
try:
|
if v is not None:
|
||||||
getattr(erargs, k)[player] = v
|
try:
|
||||||
except AttributeError:
|
getattr(erargs, k)[player] = v
|
||||||
setattr(erargs, k, {player: v})
|
except AttributeError:
|
||||||
except Exception as e:
|
setattr(erargs, k, {player: v})
|
||||||
raise Exception(f"Error setting {k} to {v} for player {player}") from e
|
except Exception as e:
|
||||||
|
raise Exception(f"Error setting {k} to {v} for player {player}") from e
|
||||||
|
|
||||||
|
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]
|
||||||
|
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
|
||||||
|
|
||||||
|
player += 1
|
||||||
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 {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_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]
|
|
||||||
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
|
|
||||||
|
|
||||||
if len(set(erargs.name.values())) != len(erargs.name):
|
if len(set(erargs.name.values())) != len(erargs.name):
|
||||||
raise Exception(f"Names have to be unique. Names: {Counter(erargs.name.values())}")
|
raise Exception(f"Names have to be unique. Names: {Counter(erargs.name.values())}")
|
||||||
|
@ -214,7 +223,7 @@ def main(args=None, callback=ERmain):
|
||||||
callback(erargs, seed)
|
callback(erargs, seed)
|
||||||
|
|
||||||
|
|
||||||
def read_weights_yaml(path):
|
def read_weights_yamls(path) -> Tuple[Any, ...]:
|
||||||
try:
|
try:
|
||||||
if urllib.parse.urlparse(path).scheme in ('https', 'file'):
|
if urllib.parse.urlparse(path).scheme in ('https', 'file'):
|
||||||
yaml = str(urllib.request.urlopen(path).read(), "utf-8-sig")
|
yaml = str(urllib.request.urlopen(path).read(), "utf-8-sig")
|
||||||
|
@ -224,7 +233,7 @@ def read_weights_yaml(path):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(f"Failed to read weights ({path})") from e
|
raise Exception(f"Failed to read weights ({path})") from e
|
||||||
|
|
||||||
return parse_yaml(yaml)
|
return tuple(parse_yamls(yaml))
|
||||||
|
|
||||||
|
|
||||||
def interpret_on_off(value) -> bool:
|
def interpret_on_off(value) -> bool:
|
||||||
|
@ -235,7 +244,7 @@ def convert_to_on_off(value) -> str:
|
||||||
return {True: "on", False: "off"}.get(value, value)
|
return {True: "on", False: "off"}.get(value, value)
|
||||||
|
|
||||||
|
|
||||||
def get_choice_legacy(option, root, value=None) -> typing.Any:
|
def get_choice_legacy(option, root, value=None) -> Any:
|
||||||
if option not in root:
|
if option not in root:
|
||||||
return value
|
return value
|
||||||
if type(root[option]) is list:
|
if type(root[option]) is list:
|
||||||
|
@ -250,7 +259,7 @@ def get_choice_legacy(option, root, value=None) -> typing.Any:
|
||||||
raise RuntimeError(f"All options specified in \"{option}\" are weighted as zero.")
|
raise RuntimeError(f"All options specified in \"{option}\" are weighted as zero.")
|
||||||
|
|
||||||
|
|
||||||
def get_choice(option, root, value=None) -> typing.Any:
|
def get_choice(option, root, value=None) -> Any:
|
||||||
if option not in root:
|
if option not in root:
|
||||||
return value
|
return value
|
||||||
if type(root[option]) is list:
|
if type(root[option]) is list:
|
||||||
|
@ -283,16 +292,16 @@ def handle_name(name: str, player: int, name_counter: Counter):
|
||||||
return new_name
|
return new_name
|
||||||
|
|
||||||
|
|
||||||
def prefer_int(input_data: str) -> typing.Union[str, int]:
|
def prefer_int(input_data: str) -> Union[str, int]:
|
||||||
try:
|
try:
|
||||||
return int(input_data)
|
return int(input_data)
|
||||||
except:
|
except:
|
||||||
return input_data
|
return input_data
|
||||||
|
|
||||||
|
|
||||||
available_boss_names: typing.Set[str] = {boss.lower() for boss in Bosses.boss_table if boss not in
|
available_boss_names: Set[str] = {boss.lower() for boss in Bosses.boss_table if boss not in
|
||||||
{'Agahnim', 'Agahnim2', 'Ganon'}}
|
{'Agahnim', 'Agahnim2', 'Ganon'}}
|
||||||
available_boss_locations: typing.Set[str] = {f"{loc.lower()}{f' {level}' if level else ''}" for loc, level in
|
available_boss_locations: Set[str] = {f"{loc.lower()}{f' {level}' if level else ''}" for loc, level in
|
||||||
Bosses.boss_location_table}
|
Bosses.boss_location_table}
|
||||||
|
|
||||||
boss_shuffle_options = {None: 'none',
|
boss_shuffle_options = {None: 'none',
|
||||||
|
@ -317,7 +326,7 @@ goals = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def roll_percentage(percentage: typing.Union[int, float]) -> bool:
|
def roll_percentage(percentage: Union[int, float]) -> bool:
|
||||||
"""Roll a percentage chance.
|
"""Roll a percentage chance.
|
||||||
percentage is expected to be in range [0, 100]"""
|
percentage is expected to be in range [0, 100]"""
|
||||||
return random.random() < (float(percentage) / 100)
|
return random.random() < (float(percentage) / 100)
|
||||||
|
@ -387,7 +396,7 @@ def roll_triggers(weights: dict, triggers: list) -> dict:
|
||||||
return weights
|
return weights
|
||||||
|
|
||||||
|
|
||||||
def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str:
|
def get_plando_bosses(boss_shuffle: str, plando_options: Set[str]) -> str:
|
||||||
if boss_shuffle in boss_shuffle_options:
|
if boss_shuffle in boss_shuffle_options:
|
||||||
return boss_shuffle_options[boss_shuffle]
|
return boss_shuffle_options[boss_shuffle]
|
||||||
elif "bosses" in plando_options:
|
elif "bosses" in plando_options:
|
||||||
|
@ -439,7 +448,7 @@ def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str,
|
||||||
setattr(ret, option_key, option(option.default))
|
setattr(ret, option_key, option(option.default))
|
||||||
|
|
||||||
|
|
||||||
def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("bosses",))):
|
def roll_settings(weights: dict, plando_options: Set[str] = frozenset(("bosses",))):
|
||||||
if "linked_options" in weights:
|
if "linked_options" in weights:
|
||||||
weights = roll_linked_options(weights)
|
weights = roll_linked_options(weights)
|
||||||
|
|
||||||
|
|
3
Utils.py
3
Utils.py
|
@ -28,7 +28,7 @@ class Version(typing.NamedTuple):
|
||||||
__version__ = "0.3.2"
|
__version__ = "0.3.2"
|
||||||
version_tuple = tuplize_version(__version__)
|
version_tuple = tuplize_version(__version__)
|
||||||
|
|
||||||
from yaml import load, dump, SafeLoader
|
from yaml import load, load_all, dump, SafeLoader
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from yaml import CLoader as Loader
|
from yaml import CLoader as Loader
|
||||||
|
@ -159,6 +159,7 @@ class UniqueKeyLoader(SafeLoader):
|
||||||
|
|
||||||
|
|
||||||
parse_yaml = functools.partial(load, Loader=UniqueKeyLoader)
|
parse_yaml = functools.partial(load, Loader=UniqueKeyLoader)
|
||||||
|
parse_yamls = functools.partial(load_all, Loader=UniqueKeyLoader)
|
||||||
unsafe_parse_yaml = functools.partial(load, Loader=Loader)
|
unsafe_parse_yaml = functools.partial(load, Loader=Loader)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ def allowed_file(filename):
|
||||||
|
|
||||||
|
|
||||||
from Generate import roll_settings
|
from Generate import roll_settings
|
||||||
from Utils import parse_yaml
|
from Utils import parse_yamls
|
||||||
|
|
||||||
|
|
||||||
@app.route('/check', methods=['GET', 'POST'])
|
@app.route('/check', methods=['GET', 'POST'])
|
||||||
|
@ -68,14 +68,19 @@ def roll_options(options: Dict[str, Union[dict, str]]) -> Tuple[Dict[str, Union[
|
||||||
for filename, text in options.items():
|
for filename, text in options.items():
|
||||||
try:
|
try:
|
||||||
if type(text) is dict:
|
if type(text) is dict:
|
||||||
yaml_data = text
|
yaml_datas = (text, )
|
||||||
else:
|
else:
|
||||||
yaml_data = parse_yaml(text)
|
yaml_datas = tuple(parse_yamls(text))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results[filename] = f"Failed to parse YAML data in {filename}: {e}"
|
results[filename] = f"Failed to parse YAML data in {filename}: {e}"
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
rolled_results[filename] = roll_settings(yaml_data,
|
if len(yaml_datas) == 1:
|
||||||
|
rolled_results[filename] = roll_settings(yaml_datas[0],
|
||||||
|
plando_options={"bosses", "items", "connections", "texts"})
|
||||||
|
else:
|
||||||
|
for i, yaml_data in enumerate(yaml_datas):
|
||||||
|
rolled_results[f"{filename}/{i + 1}"] = roll_settings(yaml_data,
|
||||||
plando_options={"bosses", "items", "connections", "texts"})
|
plando_options={"bosses", "items", "connections", "texts"})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results[filename] = f"Failed to generate mystery in {filename}: {e}"
|
results[filename] = f"Failed to generate mystery in {filename}: {e}"
|
||||||
|
|
Loading…
Reference in New Issue