Archipelago/Generate.py

644 lines
31 KiB
Python
Raw Normal View History

from __future__ import annotations
import argparse
import logging
import os
import random
import string
import urllib.parse
import urllib.request
from collections import Counter, ChainMap
from typing import Dict, Tuple, Callable, Any, Union
import ModuleUpdate
ModuleUpdate.update()
import Utils
from worlds.alttp import Options as LttPOptions
from worlds.generic import PlandoConnection
from Utils import parse_yamls, version_tuple, __version__, tuplize_version, get_options, user_path
2020-10-24 03:38:56 +00:00
from worlds.alttp.EntranceRandomizer import parse_arguments
from Main import main as ERmain
from BaseClasses import seeddigits, get_seed, PlandoOptions
2021-03-14 07:38:02 +00:00
import Options
from worlds.alttp.Text import TextTable
2021-06-15 12:11:46 +00:00
from worlds.AutoWorld import AutoWorldRegister
import copy
2020-06-29 01:59:16 +00:00
def mystery_argparse():
options = get_options()
defaults = options["generator"]
def resolve_path(path: str, resolver: Callable[[str], str]) -> str:
2022-03-31 03:08:15 +00:00
return path if os.path.isabs(path) else resolver(path)
parser = argparse.ArgumentParser(description="CMD Generation Interface, defaults come from host.yaml.")
2022-03-31 03:08:15 +00:00
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')
2022-03-31 03:08:15 +00:00
parser.add_argument('--player_files_path', default=resolve_path(defaults["player_files_path"], user_path),
help="Input directory for player files.")
parser.add_argument('--seed', help='Define seed number to generate.', type=int)
2021-10-21 06:15:47 +00:00
parser.add_argument('--multi', default=defaults["players"], type=lambda value: max(int(value), 1))
parser.add_argument('--spoiler', type=int, default=defaults["spoiler"])
2022-03-31 03:08:15 +00:00
parser.add_argument('--outputpath', default=resolve_path(options["general_options"]["output_path"], user_path),
help="Path to output folder. Absolute or relative to cwd.") # absolute or relative to cwd
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_level', default='info', help='Sets log level')
2021-10-21 06:15:47 +00:00
parser.add_argument('--yaml_output', default=0, type=lambda value: max(int(value), 0),
2020-04-25 00:25:46 +00:00
help='Output rolled mystery results to yaml up to specified number (made for async multiworld)')
parser.add_argument('--plando', default=defaults["plando_options"],
2021-01-02 11:49:43 +00:00
help='List of options that can be set manually. Can be combined, for example "bosses, items"')
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: PlandoOptions = PlandoOptions.from_option_string(args.plando)
return args, options
2020-06-29 01:59:16 +00:00
2021-06-15 12:11:46 +00:00
def get_seed_name(random_source) -> str:
return f"{random_source.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits)
2020-06-29 01:59:16 +00:00
2021-06-15 12:11:46 +00:00
2020-09-13 15:15:49 +00:00
def main(args=None, callback=ERmain):
2020-06-29 01:59:16 +00:00
if not args:
args, options = mystery_argparse()
seed = get_seed(args.seed)
2023-05-18 13:52:55 +00:00
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level)
random.seed(seed)
seed_name = get_seed_name(random)
if args.race:
2023-05-18 13:52:55 +00:00
logging.info("Race mode enabled. Using non-deterministic random source.")
random.seed() # reset to time-based random source
weights_cache: Dict[str, Tuple[Any, ...]] = {}
if args.weights_file_path and os.path.exists(args.weights_file_path):
2020-02-18 08:14:31 +00:00
try:
weights_cache[args.weights_file_path] = read_weights_yamls(args.weights_file_path)
2020-02-18 08:14:31 +00:00
except Exception as e:
raise ValueError(f"File {args.weights_file_path} is destroyed. Please fix your yaml.") from e
2023-05-18 13:52:55 +00:00
logging.info(f"Weights: {args.weights_file_path} >> "
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):
2020-02-18 08:14:31 +00:00
try:
meta_weights = read_weights_yamls(args.meta_file_path)[-1]
2020-02-18 08:14:31 +00:00
except Exception as e:
raise ValueError(f"File {args.meta_file_path} is destroyed. Please fix your yaml.") from e
2023-05-18 13:52:55 +00:00
logging.info(f"Meta: {args.meta_file_path} >> {get_choice('meta_description', meta_weights)}")
try: # meta description allows us to verify that the file named meta.yaml is intentionally a meta file
del(meta_weights["meta_description"])
except Exception as e:
raise ValueError("No meta description found for meta.yaml. Unable to verify.") from e
2020-02-18 08:14:31 +00:00
if args.samesettings:
raise Exception("Cannot mix --samesettings with --meta")
else:
meta_weights = None
player_id = 1
player_files = {}
for file in os.scandir(args.player_files_path):
fname = file.name
2023-03-04 15:34:10 +00:00
if file.is_file() and not fname.startswith(".") 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)
2020-03-05 01:31:26 +00:00
try:
weights_cache[fname] = read_weights_yamls(path)
2020-03-05 01:31:26 +00:00
except Exception as e:
raise ValueError(f"File {fname} is destroyed. Please fix your yaml.") from e
# sort dict for consistent results across platforms:
weights_cache = {key: value for key, value in sorted(weights_cache.items())}
for filename, yaml_data in weights_cache.items():
if filename not in {args.meta_file_path, args.weights_file_path}:
for yaml in yaml_data:
2023-05-18 13:52:55 +00:00
logging.info(f"P{player_id} Weights: {filename} >> "
f"{get_choice('description', yaml, 'No description specified')}")
player_files[player_id] = filename
player_id += 1
args.multi = max(player_id - 1, args.multi)
2023-05-18 13:52:55 +00:00
logging.info(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, "
f"{seed_name} Seed {seed} with plando: {args.plando}")
if not weights_cache:
2023-05-18 13:52:55 +00:00
raise Exception(f"No weights found. "
f"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.plando_options = args.plando
erargs.glitch_triforce = options["generator"]["glitch_triforce_room"]
erargs.spoiler = args.spoiler
erargs.race = args.race
erargs.outputname = seed_name
erargs.outputpath = args.outputpath
settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None)
for fname, yamls in weights_cache.items()}
2020-02-18 08:14:31 +00:00
if meta_weights:
2021-11-21 17:09:06 +00:00
for category_name, category_dict in meta_weights.items():
for key in category_dict:
option = roll_meta_option(key, category_name, category_dict)
2021-11-21 17:09:06 +00:00
if option is not None:
for path in weights_cache:
for yaml in weights_cache[path]:
if category_name is None:
for category in yaml:
if category in AutoWorldRegister.world_types and key in Options.common_options:
yaml[category][key] = option
elif category_name not in yaml:
logging.warning(f"Meta: Category {category_name} is not present in {path}.")
else:
yaml[category_name][key] = option
player_path_cache = {}
for player in range(1, args.multi + 1):
player_path_cache[player] = player_files.get(player, args.weights_file_path)
name_counter = Counter()
2021-03-14 07:38:02 +00:00
erargs.player_settings = {}
player = 1
while player <= args.multi:
2020-02-18 08:14:31 +00:00
path = player_path_cache[player]
if path:
2020-01-12 16:03:30 +00:00
try:
settings: Tuple[argparse.Namespace, ...] = settings_cache[path] if settings_cache[path] else \
tuple(roll_settings(yaml, args.plando) for yaml in weights_cache[path])
for settingsObject in settings:
for k, v in vars(settingsObject).items():
if v is not None:
try:
getattr(erargs, k)[player] = v
except AttributeError:
setattr(erargs, k, {player: v})
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
2020-02-18 08:14:31 +00:00
except Exception as e:
raise ValueError(f"File {path} is destroyed. Please fix your yaml.") from e
else:
raise RuntimeError(f'No weights specified for player {player}')
if len(set(name.lower() for name in erargs.name.values())) != len(erargs.name):
raise Exception(f"Names have to be unique. Names: {Counter(name.lower() for name in erargs.name.values())}")
2020-04-25 00:24:37 +00:00
if args.yaml_output:
import yaml
important = {}
for option, player_settings in vars(erargs).items():
if type(player_settings) == dict:
if all(type(value) != list for value in player_settings.values()):
if len(player_settings.values()) > 1:
important[option] = {player: value for player, value in player_settings.items() if
player <= args.yaml_output}
else:
logging.debug(f"No player settings defined for option '{option}'")
2020-04-25 00:24:37 +00:00
else:
if player_settings != "": # is not empty name
important[option] = player_settings
else:
logging.debug(f"No player settings defined for option '{option}'")
if args.outputpath:
os.makedirs(args.outputpath, exist_ok=True)
with open(os.path.join(args.outputpath if args.outputpath else ".", f"generate_{seed_name}.yaml"), "wt") as f:
2020-04-25 00:24:37 +00:00
yaml.dump(important, f)
2020-02-23 16:06:44 +00:00
callback(erargs, seed)
2020-02-23 16:06:44 +00:00
def read_weights_yamls(path) -> Tuple[Any, ...]:
try:
2022-03-31 06:22:01 +00:00
if urllib.parse.urlparse(path).scheme in ('https', 'file'):
yaml = str(urllib.request.urlopen(path).read(), "utf-8-sig")
2020-04-10 04:41:32 +00:00
else:
with open(path, 'rb') as f:
yaml = str(f.read(), "utf-8-sig")
except Exception as e:
2020-09-18 04:03:04 +00:00
raise Exception(f"Failed to read weights ({path})") from e
return tuple(parse_yamls(yaml))
def interpret_on_off(value) -> bool:
return {"on": True, "off": False}.get(value, value)
2020-02-23 16:06:44 +00:00
def convert_to_on_off(value) -> str:
2020-01-20 02:29:13 +00:00
return {True: "on", False: "off"}.get(value, value)
2020-02-23 16:06:44 +00:00
def get_choice_legacy(option, root, value=None) -> Any:
2020-02-18 08:14:31 +00:00
if option not in root:
return value
if type(root[option]) is list:
return interpret_on_off(random.choices(root[option])[0])
2020-02-18 08:14:31 +00:00
if type(root[option]) is not dict:
return interpret_on_off(root[option])
if not root[option]:
return value
2020-06-19 02:21:52 +00:00
if any(root[option].values()):
2020-06-18 15:48:33 +00:00
return interpret_on_off(
random.choices(list(root[option].keys()), weights=list(map(int, root[option].values())))[0])
2020-12-31 12:23:32 +00:00
raise RuntimeError(f"All options specified in \"{option}\" are weighted as zero.")
2020-02-23 16:06:44 +00:00
def get_choice(option, root, value=None) -> Any:
if option not in root:
return value
if type(root[option]) is list:
return random.choices(root[option])[0]
if type(root[option]) is not dict:
return root[option]
if not root[option]:
return value
if any(root[option].values()):
return random.choices(list(root[option].keys()), weights=list(map(int, root[option].values())))[0]
raise RuntimeError(f"All options specified in \"{option}\" are weighted as zero.")
class SafeDict(dict):
def __missing__(self, key):
return '{' + key + '}'
def handle_name(name: str, player: int, name_counter: Counter):
name_counter[name.lower()] += 1
number = name_counter[name.lower()]
new_name = "%".join([x.replace("%number%", "{number}").replace("%player%", "{player}") for x in name.split("%%")])
new_name = string.Formatter().vformat(new_name, (), SafeDict(number=number,
NUMBER=(number if number > 1 else ''),
player=player,
PLAYER=(player if player > 1 else '')))
new_name = new_name.strip()[:16]
2021-05-11 21:08:50 +00:00
if new_name == "Archipelago":
raise Exception(f"You cannot name yourself \"{new_name}\"")
return new_name
2020-02-23 16:06:44 +00:00
def prefer_int(input_data: str) -> Union[str, int]:
try:
return int(input_data)
except:
return input_data
2020-12-31 12:23:32 +00:00
goals = {
'ganon': 'ganon',
'crystals': 'crystals',
'bosses': 'bosses',
'pedestal': 'pedestal',
'ganon_pedestal': 'ganonpedestal',
'triforce_hunt': 'triforcehunt',
'local_triforce_hunt': 'localtriforcehunt',
'ganon_triforce_hunt': 'ganontriforcehunt',
'local_ganon_triforce_hunt': 'localganontriforcehunt',
'ice_rod_hunt': 'icerodhunt',
}
2021-04-03 13:06:32 +00:00
def roll_percentage(percentage: Union[int, float]) -> bool:
2021-01-02 11:49:43 +00:00
"""Roll a percentage chance.
percentage is expected to be in range [0, 100]"""
return random.random() < (float(percentage) / 100)
def update_weights(weights: dict, new_weights: dict, type: str, name: str) -> dict:
logging.debug(f'Applying {new_weights}')
new_options = set(new_weights) - set(weights)
weights.update(new_weights)
if new_options:
for new_option in new_options:
logging.warning(f'{type} Suboption "{new_option}" of "{name}" did not '
f'overwrite a root option. '
f'This is probably in error.')
return weights
def roll_meta_option(option_key, game: str, category_dict: Dict) -> Any:
if not game:
return get_choice(option_key, category_dict)
if game in AutoWorldRegister.world_types:
game_world = AutoWorldRegister.world_types[game]
options = ChainMap(game_world.option_definitions, Options.per_game_common_options)
if option_key in options:
if options[option_key].supports_weighting:
return get_choice(option_key, category_dict)
return category_dict[option_key]
if game == "A Link to the Past": # TODO wow i hate this
if option_key in {"glitches_required", "dark_room_logic", "entrance_shuffle", "goals", "triforce_pieces_mode",
"triforce_pieces_percentage", "triforce_pieces_available", "triforce_pieces_extra",
"triforce_pieces_required", "shop_shuffle", "mode", "item_pool", "item_functionality",
"boss_shuffle", "enemy_damage", "enemy_health", "timer", "countdown_start_time",
"red_clock_time", "blue_clock_time", "green_clock_time", "dungeon_counters", "shuffle_prizes",
"misery_mire_medallion", "turtle_rock_medallion", "sprite_pool", "sprite",
"random_sprite_on_event"}:
return get_choice(option_key, category_dict)
raise Exception(f"Error generating meta option {option_key} for {game}.")
2021-02-13 13:03:23 +00:00
def roll_linked_options(weights: dict) -> dict:
weights = copy.deepcopy(weights) # make sure we don't write back to other weights sets in same_settings
2021-02-13 13:03:23 +00:00
for option_set in weights["linked_options"]:
if "name" not in option_set:
raise ValueError("One of your linked options does not have a name.")
try:
if roll_percentage(option_set["percentage"]):
logging.debug(f"Linked option {option_set['name']} triggered.")
2021-06-15 12:11:46 +00:00
new_options = option_set["options"]
for category_name, category_options in new_options.items():
currently_targeted_weights = weights
if category_name:
currently_targeted_weights = currently_targeted_weights[category_name]
update_weights(currently_targeted_weights, category_options, "Linked", option_set["name"])
2021-02-13 13:03:23 +00:00
else:
logging.debug(f"linked option {option_set['name']} skipped.")
except Exception as e:
raise ValueError(f"Linked option {option_set['name']} is destroyed. "
f"Please fix your linked option.") from e
return weights
2021-01-02 11:49:43 +00:00
def roll_triggers(weights: dict, triggers: list) -> dict:
weights = copy.deepcopy(weights) # make sure we don't write back to other weights sets in same_settings
weights["_Generator_Version"] = Utils.__version__
for i, option_set in enumerate(triggers):
2021-02-13 13:03:23 +00:00
try:
2021-06-15 12:11:46 +00:00
currently_targeted_weights = weights
category = option_set.get("option_category", None)
if category:
currently_targeted_weights = currently_targeted_weights[category]
2021-02-13 13:03:23 +00:00
key = get_choice("option_name", option_set)
2021-06-15 12:11:46 +00:00
if key not in currently_targeted_weights:
logging.warning(f'Specified option name {option_set["option_name"]} did not '
f'match with a root option. '
f'This is probably in error.')
2021-02-13 13:03:23 +00:00
trigger_result = get_choice("option_result", option_set)
2021-06-15 12:11:46 +00:00
result = get_choice(key, currently_targeted_weights)
currently_targeted_weights[key] = result
2021-02-13 13:03:23 +00:00
if result == trigger_result and roll_percentage(get_choice("percentage", option_set, 100)):
2021-06-15 12:11:46 +00:00
for category_name, category_options in option_set["options"].items():
currently_targeted_weights = weights
if category_name:
currently_targeted_weights = currently_targeted_weights[category_name]
update_weights(currently_targeted_weights, category_options, "Triggered", option_set["option_name"])
2021-02-13 13:03:23 +00:00
except Exception as e:
2021-06-15 12:11:46 +00:00
raise ValueError(f"Your trigger number {i + 1} is destroyed. "
2021-02-13 13:03:23 +00:00
f"Please fix your triggers.") from e
return weights
def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option), plando_options: PlandoOptions):
if option_key in game_weights:
try:
if not option.supports_weighting:
player_option = option.from_any(game_weights[option_key])
else:
player_option = option.from_any(get_choice(option_key, game_weights))
setattr(ret, option_key, player_option)
except Exception as e:
raise Exception(f"Error generating option {option_key} in {ret.game}") from e
else:
core: new freetext and textchoice options (#728) * add freetext and freetextchoice options * fix textchoice. create plando_bosses bool so worlds can check if boss plando is enabled * remove strange unneccessary \ escapes * lttp: rip boss plando out of core * fix broken text methods so they read the data correctly * revert `None` key in boss_shuffle_options. fix failing tests * lttp: rewrite boss plando * lttp: rewrite boss shuffle * add generic verification step and allow options to set a plando module * add default typing to plando_options set * use PlandoSettings intflag for lttp boss plando * fix plandosettings boss flag check * minor lttp init cleanup * make suggested changes. account for "random" existing within plando boss options * override eq operator * Please document me! * Forgot to mention it supports plando * remove auto_display_name * Throw warning alerting user to which shuffle is being used if plando is off. Set the remaining boss shuffle in init and boss placement cleanup * move the convoluted string matching to `from_text` * remove unneccessary text lowering and actually turn off plando option when it's disabled * typing * strong typing for verify method and reorder * typing is your friend * log warning correctly * 3.8 support :( * also list apparently * rip out old boss shuffle spoiler code * verification step for plando bosses and locations * update plando guide to reference new supported behavior * empty string is not `None`. remove unneccessary error throw * Fix bad ordering * validate boss_shuffle only contains a normal boss option at the end * get random choice from a list dummy * >:( Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * minor textchoice cleanup Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-09-17 00:55:33 +00:00
player_option.verify(AutoWorldRegister.world_types[ret.game], ret.name, plando_options)
else:
setattr(ret, option_key, option.from_any(option.default)) # call the from_any here to support default "random"
def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.bosses):
2020-04-25 00:24:37 +00:00
if "linked_options" in weights:
2021-02-13 13:03:23 +00:00
weights = roll_linked_options(weights)
if "triggers" in weights:
weights = roll_triggers(weights, weights["triggers"])
2020-04-25 00:24:37 +00:00
requirements = weights.get("requires", {})
if requirements:
version = requirements.get("version", __version__)
if tuplize_version(version) > version_tuple:
raise Exception(f"Settings reports required version of generator is at least {version}, "
f"however generator is of version {__version__}")
required_plando_options = PlandoOptions.from_option_string(requirements.get("plando", ""))
if required_plando_options not in plando_options:
if required_plando_options:
raise Exception(f"Settings reports required plando module {str(required_plando_options)}, "
f"which is not enabled.")
2021-02-13 13:03:23 +00:00
ret = argparse.Namespace()
for option_key in Options.per_game_common_options:
if option_key in weights and option_key not in Options.common_options:
raise Exception(f"Option {option_key} has to be in a game's section, not on its own.")
ret.game = get_choice("game", weights)
if ret.game not in weights:
raise Exception(f"No game options for selected game \"{ret.game}\" found.")
2021-07-12 11:54:47 +00:00
world_type = AutoWorldRegister.world_types[ret.game]
game_weights = weights[ret.game]
2021-07-14 13:24:34 +00:00
if "triggers" in game_weights:
weights = roll_triggers(weights, game_weights["triggers"])
game_weights = weights[ret.game]
ret.name = get_choice('name', weights)
for option_key, option in Options.common_options.items():
setattr(ret, option_key, option.from_any(get_choice(option_key, weights, option.default)))
if ret.game in AutoWorldRegister.world_types:
for option_key, option in world_type.option_definitions.items():
core: new freetext and textchoice options (#728) * add freetext and freetextchoice options * fix textchoice. create plando_bosses bool so worlds can check if boss plando is enabled * remove strange unneccessary \ escapes * lttp: rip boss plando out of core * fix broken text methods so they read the data correctly * revert `None` key in boss_shuffle_options. fix failing tests * lttp: rewrite boss plando * lttp: rewrite boss shuffle * add generic verification step and allow options to set a plando module * add default typing to plando_options set * use PlandoSettings intflag for lttp boss plando * fix plandosettings boss flag check * minor lttp init cleanup * make suggested changes. account for "random" existing within plando boss options * override eq operator * Please document me! * Forgot to mention it supports plando * remove auto_display_name * Throw warning alerting user to which shuffle is being used if plando is off. Set the remaining boss shuffle in init and boss placement cleanup * move the convoluted string matching to `from_text` * remove unneccessary text lowering and actually turn off plando option when it's disabled * typing * strong typing for verify method and reorder * typing is your friend * log warning correctly * 3.8 support :( * also list apparently * rip out old boss shuffle spoiler code * verification step for plando bosses and locations * update plando guide to reference new supported behavior * empty string is not `None`. remove unneccessary error throw * Fix bad ordering * validate boss_shuffle only contains a normal boss option at the end * get random choice from a list dummy * >:( Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * minor textchoice cleanup Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-09-17 00:55:33 +00:00
handle_option(ret, game_weights, option_key, option, plando_options)
for option_key, option in Options.per_game_common_options.items():
# skip setting this option if already set from common_options, defaulting to root option
if option_key not in world_type.option_definitions and \
(option_key not in Options.common_options or option_key in game_weights):
core: new freetext and textchoice options (#728) * add freetext and freetextchoice options * fix textchoice. create plando_bosses bool so worlds can check if boss plando is enabled * remove strange unneccessary \ escapes * lttp: rip boss plando out of core * fix broken text methods so they read the data correctly * revert `None` key in boss_shuffle_options. fix failing tests * lttp: rewrite boss plando * lttp: rewrite boss shuffle * add generic verification step and allow options to set a plando module * add default typing to plando_options set * use PlandoSettings intflag for lttp boss plando * fix plandosettings boss flag check * minor lttp init cleanup * make suggested changes. account for "random" existing within plando boss options * override eq operator * Please document me! * Forgot to mention it supports plando * remove auto_display_name * Throw warning alerting user to which shuffle is being used if plando is off. Set the remaining boss shuffle in init and boss placement cleanup * move the convoluted string matching to `from_text` * remove unneccessary text lowering and actually turn off plando option when it's disabled * typing * strong typing for verify method and reorder * typing is your friend * log warning correctly * 3.8 support :( * also list apparently * rip out old boss shuffle spoiler code * verification step for plando bosses and locations * update plando guide to reference new supported behavior * empty string is not `None`. remove unneccessary error throw * Fix bad ordering * validate boss_shuffle only contains a normal boss option at the end * get random choice from a list dummy * >:( Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * minor textchoice cleanup Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-09-17 00:55:33 +00:00
handle_option(ret, game_weights, option_key, option, plando_options)
if PlandoOptions.items in plando_options:
2022-01-20 18:34:17 +00:00
ret.plando_items = game_weights.get("plando_items", [])
2021-11-20 21:36:57 +00:00
if ret.game == "Minecraft" or ret.game == "Ocarina of Time":
# bad hardcoded behavior to make this work for now
ret.plando_connections = []
if PlandoOptions.connections in plando_options:
options = game_weights.get("plando_connections", [])
for placement in options:
if roll_percentage(get_choice("percentage", placement, 100)):
ret.plando_connections.append(PlandoConnection(
get_choice("entrance", placement),
get_choice("exit", placement),
2021-11-20 21:36:57 +00:00
get_choice("direction", placement)
))
elif ret.game == "A Link to the Past":
roll_alttp_settings(ret, game_weights, plando_options)
2021-03-14 07:38:02 +00:00
else:
raise Exception(f"Unsupported game {ret.game}")
2021-03-14 07:38:02 +00:00
return ret
2021-03-14 07:38:02 +00:00
def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
2021-09-01 16:18:43 +00:00
if "dungeon_items" in weights and get_choice_legacy('dungeon_items', weights, "none") != "none":
raise Exception(f"dungeon_items key in A Link to the Past was removed, but is present in these weights as {get_choice_legacy('dungeon_items', weights, False)}.")
glitches_required = get_choice_legacy('glitches_required', weights)
if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'hybrid_major_glitches', 'minor_glitches']:
logging.warning("Only NMG, OWG, HMG and No Logic supported")
glitches_required = 'none'
2020-06-10 17:02:11 +00:00
ret.logic = {None: 'noglitches', 'none': 'noglitches', 'no_logic': 'nologic', 'overworld_glitches': 'owglitches',
'minor_glitches': 'minorglitches', 'hybrid_major_glitches': 'hybridglitches'}[
2020-04-25 00:24:37 +00:00
glitches_required]
ret.dark_room_logic = get_choice_legacy("dark_room_logic", weights, "lamp")
2020-10-07 21:19:16 +00:00
if not ret.dark_room_logic: # None/False
ret.dark_room_logic = "none"
if ret.dark_room_logic == "sconces":
ret.dark_room_logic = "torches"
if ret.dark_room_logic not in {"lamp", "torches", "none"}:
raise ValueError(f"Unknown Dark Room Logic: \"{ret.dark_room_logic}\"")
entrance_shuffle = get_choice_legacy('entrance_shuffle', weights, 'vanilla')
if entrance_shuffle.startswith('none-'):
ret.shuffle = 'vanilla'
else:
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
goal = get_choice_legacy('goals', weights, 'ganon')
ret.goal = goals[goal]
extra_pieces = get_choice_legacy('triforce_pieces_mode', weights, 'available')
2020-06-17 08:02:54 +00:00
ret.triforce_pieces_required = LttPOptions.TriforcePieces.from_any(get_choice_legacy('triforce_pieces_required', weights, 20))
# sum a percentage to required
if extra_pieces == 'percentage':
percentage = max(100, float(get_choice_legacy('triforce_pieces_percentage', weights, 150))) / 100
ret.triforce_pieces_available = int(round(ret.triforce_pieces_required * percentage, 0))
# vanilla mode (specify how many pieces are)
elif extra_pieces == 'available':
ret.triforce_pieces_available = LttPOptions.TriforcePieces.from_any(
get_choice_legacy('triforce_pieces_available', weights, 30))
# required pieces + fixed extra
elif extra_pieces == 'extra':
extra_pieces = max(0, int(get_choice_legacy('triforce_pieces_extra', weights, 10)))
ret.triforce_pieces_available = ret.triforce_pieces_required + extra_pieces
# change minimum to required pieces to avoid problems
2020-10-07 21:19:16 +00:00
ret.triforce_pieces_available = min(max(ret.triforce_pieces_required, int(ret.triforce_pieces_available)), 90)
ret.shop_shuffle = get_choice_legacy('shop_shuffle', weights, '')
if not ret.shop_shuffle:
2020-08-23 19:38:21 +00:00
ret.shop_shuffle = ''
ret.mode = get_choice_legacy("mode", weights)
ret.difficulty = get_choice_legacy('item_pool', weights)
ret.item_functionality = get_choice_legacy('item_functionality', weights)
ret.enemy_damage = {None: 'default',
'default': 'default',
'shuffled': 'shuffled',
'random': 'chaos', # to be removed
'chaos': 'chaos',
}[get_choice_legacy('enemy_damage', weights)]
ret.enemy_health = get_choice_legacy('enemy_health', weights)
2020-03-04 12:55:03 +00:00
ret.timer = {'none': False,
None: False,
False: False,
'timed': 'timed',
'timed_ohko': 'timed-ohko',
'ohko': 'ohko',
'timed_countdown': 'timed-countdown',
'display': 'display'}[get_choice_legacy('timer', weights, False)]
ret.countdown_start_time = int(get_choice_legacy('countdown_start_time', weights, 10))
ret.red_clock_time = int(get_choice_legacy('red_clock_time', weights, -2))
ret.blue_clock_time = int(get_choice_legacy('blue_clock_time', weights, 2))
ret.green_clock_time = int(get_choice_legacy('green_clock_time', weights, 4))
ret.dungeon_counters = get_choice_legacy('dungeon_counters', weights, 'default')
ret.shuffle_prizes = get_choice_legacy('shuffle_prizes', weights, "g")
2020-09-20 02:35:45 +00:00
ret.required_medallions = [get_choice_legacy("misery_mire_medallion", weights, "random"),
get_choice_legacy("turtle_rock_medallion", weights, "random")]
for index, medallion in enumerate(ret.required_medallions):
2021-03-14 07:38:02 +00:00
ret.required_medallions[index] = {"ether": "Ether", "quake": "Quake", "bombos": "Bombos", "random": "random"} \
.get(medallion.lower(), None)
if not ret.required_medallions[index]:
raise Exception(f"unknown Medallion {medallion} for {'misery mire' if index == 0 else 'turtle rock'}")
2021-01-02 15:44:58 +00:00
ret.plando_texts = {}
if PlandoOptions.texts in plando_options:
tt = TextTable()
tt.removeUnwantedText()
2021-01-02 15:44:58 +00:00
options = weights.get("plando_texts", [])
for placement in options:
if roll_percentage(get_choice_legacy("percentage", placement, 100)):
at = str(get_choice_legacy("at", placement))
if at not in tt:
raise Exception(f"No text target \"{at}\" found.")
ret.plando_texts[at] = str(get_choice_legacy("text", placement))
2021-01-02 15:44:58 +00:00
2021-01-02 21:41:03 +00:00
ret.plando_connections = []
if PlandoOptions.connections in plando_options:
2021-01-02 21:41:03 +00:00
options = weights.get("plando_connections", [])
for placement in options:
if roll_percentage(get_choice_legacy("percentage", placement, 100)):
2021-01-02 21:41:03 +00:00
ret.plando_connections.append(PlandoConnection(
get_choice_legacy("entrance", placement),
get_choice_legacy("exit", placement),
get_choice_legacy("direction", placement, "both")
2021-01-02 21:41:03 +00:00
))
2021-06-15 19:15:57 +00:00
ret.sprite_pool = weights.get('sprite_pool', [])
ret.sprite = get_choice_legacy('sprite', weights, "Link")
2021-06-15 19:15:57 +00:00
if 'random_sprite_on_event' in weights:
randomoneventweights = weights['random_sprite_on_event']
if get_choice_legacy('enabled', randomoneventweights, False):
2021-06-15 19:15:57 +00:00
ret.sprite = 'randomon'
ret.sprite += '-hit' if get_choice_legacy('on_hit', randomoneventweights, True) else ''
ret.sprite += '-enter' if get_choice_legacy('on_enter', randomoneventweights, False) else ''
ret.sprite += '-exit' if get_choice_legacy('on_exit', randomoneventweights, False) else ''
ret.sprite += '-slash' if get_choice_legacy('on_slash', randomoneventweights, False) else ''
ret.sprite += '-item' if get_choice_legacy('on_item', randomoneventweights, False) else ''
ret.sprite += '-bonk' if get_choice_legacy('on_bonk', randomoneventweights, False) else ''
ret.sprite = 'randomonall' if get_choice_legacy('on_everything', randomoneventweights, False) else ret.sprite
2021-06-15 19:15:57 +00:00
ret.sprite = 'randomonnone' if ret.sprite == 'randomon' else ret.sprite
if (not ret.sprite_pool or get_choice_legacy('use_weighted_sprite_pool', randomoneventweights, False)) \
2021-06-15 19:15:57 +00:00
and 'sprite' in weights: # Use sprite as a weighted sprite pool, if a sprite pool is not already defined.
for key, value in weights['sprite'].items():
if key.startswith('random'):
ret.sprite_pool += ['random'] * int(value)
else:
ret.sprite_pool += [key] * int(value)
if __name__ == '__main__':
2021-08-01 23:36:04 +00:00
import atexit
confirmation = atexit.register(input, "Press enter to close.")
main()
# in case of error-free exit should not need confirmation
atexit.unregister(confirmation)