Item Plando Support

This commit is contained in:
Fabian Dill 2021-01-02 12:49:43 +01:00
parent 3d53adf45c
commit f3b6be2b20
9 changed files with 171 additions and 74 deletions

View File

@ -5,7 +5,7 @@ from enum import Enum, unique
import logging import logging
import json import json
from collections import OrderedDict, Counter, deque from collections import OrderedDict, Counter, deque
from typing import Union, Optional, List, Set, Dict from typing import Union, Optional, List, Set, Dict, NamedTuple
import secrets import secrets
import random import random
@ -16,7 +16,7 @@ from Items import item_name_groups
class World(object): class World(object):
debug_types = False debug_types = False
player_names: list player_names: Dict[int, List[str]]
_region_cache: dict _region_cache: dict
difficulty_requirements: dict difficulty_requirements: dict
required_medallions: dict required_medallions: dict
@ -135,6 +135,7 @@ class World(object):
set_player_attr('sprite_pool', []) set_player_attr('sprite_pool', [])
set_player_attr('dark_room_logic', "lamp") set_player_attr('dark_room_logic', "lamp")
set_player_attr('restrict_dungeon_item_on_boss', False) set_player_attr('restrict_dungeon_item_on_boss', False)
set_player_attr('plando_items', [])
def secure(self): def secure(self):
self.random = secrets.SystemRandom() self.random = secrets.SystemRandom()
@ -1037,6 +1038,9 @@ class Item(object):
self.world = None self.world = None
self.player = player self.player = player
def __eq__(self, other):
return self.name == other.name and self.player == other.player
@property @property
def crystal(self) -> bool: def crystal(self) -> bool:
return self.type == 'Crystal' return self.type == 'Crystal'
@ -1452,3 +1456,10 @@ 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))
class PlandoItem(NamedTuple):
item: str
location: str
world: Union[bool, str] = False # False -> own world, True -> not own world
from_pool: bool = True # if item should be removed from item pool

View File

@ -355,6 +355,10 @@ def parse_arguments(argv, no_defaults=False):
parser.add_argument(f'--p{player}', default=defval(''), help=argparse.SUPPRESS) parser.add_argument(f'--p{player}', default=defval(''), help=argparse.SUPPRESS)
ret = parser.parse_args(argv) ret = parser.parse_args(argv)
# cannot be set through CLI currently
ret.plando_items = {}
ret.glitch_boots = not ret.disable_glitch_boots ret.glitch_boots = not ret.disable_glitch_boots
if ret.timer == "none": if ret.timer == "none":
ret.timer = False ret.timer = False
@ -382,9 +386,10 @@ def parse_arguments(argv, no_defaults=False):
'shufflebosses', 'enemy_shuffle', 'enemy_health', 'enemy_damage', 'shufflepots', 'shufflebosses', 'enemy_shuffle', 'enemy_health', 'enemy_damage', 'shufflepots',
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor',
'heartbeep', "skip_progression_balancing", "triforce_pieces_available", 'heartbeep', "skip_progression_balancing", "triforce_pieces_available",
"triforce_pieces_required", "shop_shuffle", "triforce_pieces_required", "shop_shuffle", "plando_items",
'remote_items', 'progressive', 'dungeon_counters', 'glitch_boots', 'killable_thieves', 'remote_items', 'progressive', 'dungeon_counters', 'glitch_boots', 'killable_thieves',
'tile_shuffle', 'bush_shuffle', 'shuffle_prizes', 'sprite_pool', 'dark_room_logic', 'restrict_dungeon_item_on_boss', 'tile_shuffle', 'bush_shuffle', 'shuffle_prizes', 'sprite_pool', 'dark_room_logic',
'restrict_dungeon_item_on_boss',
'hud_palettes', 'sword_palettes', 'shield_palettes', 'link_palettes']: 'hud_palettes', 'sword_palettes', 'shield_palettes', 'link_palettes']:
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
if player == 1: if player == 1:

10
Fill.py
View File

@ -54,7 +54,8 @@ def fill_restrictive(world, base_state: CollectionState, locations, itempool, si
for location in region.locations: for location in region.locations:
if location.item and not location.event: if location.item and not location.event:
placements.append(location) placements.append(location)
# fill in name of world for item
item_to_place.world = world
raise FillError(f'No more spots to place {item_to_place}, locations {locations} are invalid. ' raise FillError(f'No more spots to place {item_to_place}, locations {locations} are invalid. '
f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}') f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}')
@ -128,9 +129,12 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
world.random.shuffle(fill_locations) world.random.shuffle(fill_locations)
# Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots # Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots
standard_keyshuffle_players = {player for player, mode in world.mode.items() if mode == 'standard' and
world.keyshuffle[player] is True}
if standard_keyshuffle_players:
progitempool.sort( progitempool.sort(
key=lambda item: 1 if item.name == 'Small Key (Hyrule Castle)' and world.mode[item.player] == 'standard' and key=lambda item: 1 if item.name == 'Small Key (Hyrule Castle)' and
world.keyshuffle[item.player] else 0) item.player in standard_keyshuffle_players else 0)
fill_restrictive(world, world.state, fill_locations, progitempool) fill_restrictive(world, world.state, fill_locations, progitempool)

48
Main.py
View File

@ -9,7 +9,7 @@ import time
import zlib import zlib
import concurrent.futures import concurrent.futures
from BaseClasses import World, CollectionState, Item, Region, Location, Shop from BaseClasses import World, CollectionState, Item, Region, Location, PlandoItem
from Items import ItemFactory, item_table, item_name_groups from Items import ItemFactory, item_table, item_name_groups
from Regions import create_regions, create_shops, mark_light_world_regions, lookup_vanilla_location_to_entrance from Regions import create_regions, create_shops, mark_light_world_regions, lookup_vanilla_location_to_entrance
from InvertedRegions import create_inverted_regions, mark_dark_world_regions from InvertedRegions import create_inverted_regions, mark_dark_world_regions
@ -87,6 +87,7 @@ def main(args, seed=None):
world.shuffle_prizes = args.shuffle_prizes.copy() world.shuffle_prizes = args.shuffle_prizes.copy()
world.sprite_pool = args.sprite_pool.copy() world.sprite_pool = args.sprite_pool.copy()
world.dark_room_logic = args.dark_room_logic.copy() world.dark_room_logic = args.dark_room_logic.copy()
world.plando_items = args.plando_items.copy()
world.restrict_dungeon_item_on_boss = args.restrict_dungeon_item_on_boss.copy() world.restrict_dungeon_item_on_boss = args.restrict_dungeon_item_on_boss.copy()
world.rom_seeds = {player: random.Random(world.random.randint(0, 999999999)) for player in range(1, world.players + 1)} world.rom_seeds = {player: random.Random(world.random.randint(0, 999999999)) for player in range(1, world.players + 1)}
@ -172,10 +173,49 @@ def main(args, seed=None):
fill_prizes(world) fill_prizes(world)
logger.info("Running Item Plando")
world_name_lookup = {world.player_names[player_id][0]: player_id for player_id in world.player_ids}
for player in world.player_ids:
placement: PlandoItem
for placement in world.plando_items[player]:
target_world: int = placement.world
if target_world is False or world.players == 1:
target_world = player # in own world
elif target_world is True: # in any other world
target_world = player
while target_world == player:
target_world = world.random.randint(1, world.players + 1)
elif target_world is None: # any random world
target_world = world.random.randint(1, world.players + 1)
elif type(target_world) == int: # target world by player id
pass
else: # find world by name
target_world = world_name_lookup[target_world]
location = world.get_location(placement.location, target_world)
if location.item:
raise Exception(f"Cannot place item into already filled location {location}.")
item = ItemFactory(placement.item, player)
if placement.from_pool:
try:
world.itempool.remove(item)
except ValueError:
logger.warning(f"Could not remove {item} from pool as it's already missing from it.")
if location.can_fill(world.state, item, False):
world.push_item(location, item, collect=False)
location.event = True # flag location to be checked during fill
location.locked = True
logger.debug(f"Plando placed {item} at {location}")
else:
raise Exception(f"Can't place {item} at {location} due to fill condition not met.")
logger.info('Placing Dungeon Items.') logger.info('Placing Dungeon Items.')
shuffled_locations = None if args.algorithm in ['balanced', 'vt26'] or any(
if args.algorithm in ['balanced', 'vt26'] or any(list(args.mapshuffle.values()) + list(args.compassshuffle.values()) + list(args.mapshuffle.values()) + list(args.compassshuffle.values()) +
list(args.keyshuffle.values()) + list(args.bigkeyshuffle.values())): list(args.keyshuffle.values()) + list(args.bigkeyshuffle.values())):
fill_dungeons_restrictive(world) fill_dungeons_restrictive(world)
else: else:
@ -188,7 +228,7 @@ def main(args, seed=None):
elif args.algorithm == 'vt25': elif args.algorithm == 'vt25':
distribute_items_restrictive(world, False) distribute_items_restrictive(world, False)
elif args.algorithm == 'vt26': elif args.algorithm == 'vt26':
distribute_items_restrictive(world, True, shuffled_locations) distribute_items_restrictive(world, True)
elif args.algorithm == 'balanced': elif args.algorithm == 'balanced':
distribute_items_restrictive(world, True) distribute_items_restrictive(world, True)

View File

@ -50,6 +50,7 @@ if __name__ == "__main__":
player_files_path = multi_mystery_options["player_files_path"] player_files_path = multi_mystery_options["player_files_path"]
target_player_count = multi_mystery_options["players"] target_player_count = multi_mystery_options["players"]
race = multi_mystery_options["race"] race = multi_mystery_options["race"]
plando_options = multi_mystery_options["plando_options"]
create_spoiler = multi_mystery_options["create_spoiler"] create_spoiler = multi_mystery_options["create_spoiler"]
zip_roms = multi_mystery_options["zip_roms"] zip_roms = multi_mystery_options["zip_roms"]
zip_diffs = multi_mystery_options["zip_diffs"] zip_diffs = multi_mystery_options["zip_diffs"]
@ -104,7 +105,7 @@ if __name__ == "__main__":
command = f"{basemysterycommand} --multi {target_player_count} {player_string} " \ command = f"{basemysterycommand} --multi {target_player_count} {player_string} " \
f"--rom \"{rom_file}\" --enemizercli \"{enemizer_path}\" " \ f"--rom \"{rom_file}\" --enemizercli \"{enemizer_path}\" " \
f"--outputpath \"{output_path}\" --teams {teams}" f"--outputpath \"{output_path}\" --teams {teams} --plando \"{plando_options}\""
if create_spoiler: if create_spoiler:
command += " --create_spoiler" command += " --create_spoiler"

View File

@ -7,6 +7,7 @@ import typing
import os import os
import ModuleUpdate import ModuleUpdate
from BaseClasses import PlandoItem
ModuleUpdate.update() ModuleUpdate.update()
@ -44,10 +45,13 @@ def mystery_argparse():
parser.add_argument('--create_diff', action="store_true") 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",
help='List of options that can be set manually. Can be combined, for example "bosses, items"')
for player in range(1, multiargs.multi + 1): for player in range(1, multiargs.multi + 1):
parser.add_argument(f'--p{player}', help=argparse.SUPPRESS) parser.add_argument(f'--p{player}', help=argparse.SUPPRESS)
args = parser.parse_args() args = parser.parse_args()
args.plando: typing.Set[str] = {arg.strip().lower() for arg in args.plando.split(",")}
return args return args
@ -144,7 +148,8 @@ def main(args=None, callback=ERmain):
if args.enemizercli: if args.enemizercli:
erargs.enemizercli = args.enemizercli erargs.enemizercli = args.enemizercli
settings_cache = {k: (roll_settings(v) if args.samesettings else None) for k, v in weights_cache.items()} settings_cache = {k: (roll_settings(v, args.plando) if args.samesettings else None)
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] = getattr(args, f'p{player}') if getattr(args, f'p{player}') else args.weights
@ -167,7 +172,8 @@ def main(args=None, callback=ERmain):
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 roll_settings(weights_cache[path]) settings = settings_cache[path] if settings_cache[path] else \
roll_settings(weights_cache[path], args.plando)
if settings.sprite and not os.path.isfile(settings.sprite) and not Sprite.get_sprite_from_name( if settings.sprite and not os.path.isfile(settings.sprite) and not Sprite.get_sprite_from_name(
settings.sprite): settings.sprite):
logging.warning( logging.warning(
@ -275,7 +281,13 @@ boss_shuffle_options = {None: 'none',
} }
def roll_settings(weights): def roll_percentage(percentage: typing.Union[int, float]) -> bool:
"""Roll a percentage chance.
percentage is expected to be in range [0, 100]"""
return random.random() < (float(percentage) / 100)
def roll_settings(weights, plando_options: typing.Set[str] = frozenset(("bosses"))):
ret = argparse.Namespace() ret = argparse.Namespace()
if "linked_options" in weights: if "linked_options" in weights:
weights = weights.copy() # make sure we don't write back to other weights sets in same_settings weights = weights.copy() # make sure we don't write back to other weights sets in same_settings
@ -283,7 +295,7 @@ def roll_settings(weights):
if "name" not in option_set: if "name" not in option_set:
raise ValueError("One of your linked options does not have a name.") raise ValueError("One of your linked options does not have a name.")
try: try:
if random.random() < (float(option_set["percentage"]) / 100): if roll_percentage(option_set["percentage"]):
logging.debug(f"Linked option {option_set['name']} triggered.") logging.debug(f"Linked option {option_set['name']} triggered.")
logging.debug(f'Applying {option_set["options"]}') logging.debug(f'Applying {option_set["options"]}')
new_options = set(option_set["options"]) - set(weights) new_options = set(option_set["options"]) - set(weights)
@ -415,7 +427,7 @@ def roll_settings(weights):
if boss_shuffle in boss_shuffle_options: if boss_shuffle in boss_shuffle_options:
ret.shufflebosses = boss_shuffle_options[boss_shuffle] ret.shufflebosses = boss_shuffle_options[boss_shuffle]
else: elif "bosses" in plando_options:
options = boss_shuffle.lower().split(";") options = boss_shuffle.lower().split(";")
remainder_shuffle = "none" # vanilla remainder_shuffle = "none" # vanilla
bosses = [] bosses = []
@ -427,6 +439,8 @@ def roll_settings(weights):
else: else:
bosses.append(boss) bosses.append(boss)
ret.shufflebosses = ";".join(bosses + [remainder_shuffle]) ret.shufflebosses = ";".join(bosses + [remainder_shuffle])
else:
raise Exception(f"Boss Shuffle {boss_shuffle} is unknown and boss plando is turned off.")
ret.enemy_shuffle = {'none': False, ret.enemy_shuffle = {'none': False,
'shuffled': 'shuffled', 'shuffled': 'shuffled',
@ -534,6 +548,17 @@ def roll_settings(weights):
ret.non_local_items = ",".join(ret.non_local_items) ret.non_local_items = ",".join(ret.non_local_items)
ret.plando_items = []
if "items" in plando_options:
options = weights.get("plando_items", [])
for placement in options:
if roll_percentage(get_choice("percentage", placement, 100)):
item = get_choice("item", placement)
location = get_choice("location", placement)
from_pool = get_choice("from_pool", placement, True)
location_world = get_choice("world", placement, False)
ret.plando_items.append(PlandoItem(item, location, location_world, from_pool))
if 'rom' in weights: if 'rom' in weights:
romweights = weights['rom'] romweights = weights['rom']

112
Utils.py
View File

@ -1,10 +1,12 @@
from __future__ import annotations from __future__ import annotations
import typing import typing
def tuplize_version(version: str) -> typing.Tuple[int, ...]: def tuplize_version(version: str) -> typing.Tuple[int, ...]:
return Version(*(int(piece, 10) for piece in version.split("."))) return Version(*(int(piece, 10) for piece in version.split(".")))
class Version(typing.NamedTuple): class Version(typing.NamedTuple):
major: int major: int
minor: int minor: int
@ -42,11 +44,11 @@ def int32_as_bytes(value):
def pc_to_snes(value): def pc_to_snes(value):
return ((value<<1) & 0x7F0000)|(value & 0x7FFF)|0x8000 return ((value << 1) & 0x7F0000) | (value & 0x7FFF) | 0x8000
def snes_to_pc(value): def snes_to_pc(value):
return ((value & 0x7F0000)>>1)|(value & 0x7FFF) return ((value & 0x7F0000) >> 1) | (value & 0x7FFF)
def parse_player_names(names, players, teams): def parse_player_names(names, players, teams):
@ -87,6 +89,7 @@ def local_path(*path):
return os.path.join(local_path.cached_path, *path) return os.path.join(local_path.cached_path, *path)
local_path.cached_path = None local_path.cached_path = None
@ -98,8 +101,10 @@ def output_path(*path):
os.makedirs(os.path.dirname(path), exist_ok=True) os.makedirs(os.path.dirname(path), exist_ok=True)
return path return path
output_path.cached_path = None output_path.cached_path = None
def open_file(filename): def open_file(filename):
if sys.platform == 'win32': if sys.platform == 'win32':
os.startfile(filename) os.startfile(filename)
@ -107,9 +112,10 @@ def open_file(filename):
open_command = 'open' if sys.platform == 'darwin' else 'xdg-open' open_command = 'open' if sys.platform == 'darwin' else 'xdg-open'
subprocess.call([open_command, filename]) subprocess.call([open_command, filename])
def close_console(): def close_console():
if sys.platform == 'win32': if sys.platform == 'win32':
#windows # windows
import ctypes.wintypes import ctypes.wintypes
try: try:
ctypes.windll.kernel32.FreeConsole() ctypes.windll.kernel32.FreeConsole()
@ -143,6 +149,7 @@ class Hint(typing.NamedTuple):
def __hash__(self): def __hash__(self):
return hash((self.receiving_player, self.finding_player, self.location, self.item, self.entrance)) return hash((self.receiving_player, self.finding_player, self.location, self.item, self.entrance))
def get_public_ipv4() -> str: def get_public_ipv4() -> str:
import socket import socket
import urllib.request import urllib.request
@ -158,6 +165,7 @@ def get_public_ipv4() -> str:
pass # we could be offline, in a local game, so no point in erroring out pass # we could be offline, in a local game, so no point in erroring out
return ip return ip
def get_public_ipv6() -> str: def get_public_ipv6() -> str:
import socket import socket
import urllib.request import urllib.request
@ -173,56 +181,56 @@ def get_public_ipv6() -> str:
def get_default_options() -> dict: def get_default_options() -> dict:
if not hasattr(get_default_options, "options"): if not hasattr(get_default_options, "options"):
options = dict()
# Refer to host.yaml for comments as to what all these options mean. # Refer to host.yaml for comments as to what all these options mean.
generaloptions = dict() options = {
generaloptions["rom_file"] = "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc" "general_options": {
generaloptions["qusb2snes"] = "QUsb2Snes\\QUsb2Snes.exe" "rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
generaloptions["rom_start"] = True "qusb2snes": "QUsb2Snes\\QUsb2Snes.exe",
generaloptions["output_path"] = "output" "rom_start": True,
options["general_options"] = generaloptions "output_path": "output",
},
"server_options": {
"host": None,
"port": 38281,
"password": None,
"multidata": None,
"savefile": None,
"disable_save": False,
"loglevel": "info",
"server_password": None,
"disable_item_cheat": False,
"location_check_points": 1,
"hint_cost": 1000,
"forfeit_mode": "goal",
"remaining_mode": "goal",
"auto_shutdown": 0,
"compatibility": 2,
},
"multi_mystery_options": {
"teams": 1,
"enemizer_path": "EnemizerCLI/EnemizerCLI.Core.exe",
"player_files_path": "Players",
"players": 0,
"weights_file_path": "weights.yaml",
"meta_file_path": "meta.yaml",
"player_name": "",
"create_spoiler": 1,
"zip_roms": 0,
"zip_diffs": 2,
"zip_spoiler": 0,
"zip_multidata": 1,
"zip_format": 1,
"race": 0,
"cpu_threads": 0,
"max_attempts": 0,
"take_first_working": False,
"keep_all_seeds": False,
"log_output_path": "Output Logs",
"log_level": None,
"plando_options": "bosses",
}
}
serveroptions = dict()
serveroptions["host"] = None
serveroptions["port"] = 38281
serveroptions["password"] = None
serveroptions["multidata"] = None
serveroptions["savefile"] = None
serveroptions["disable_save"] = False
serveroptions["loglevel"] = "info"
serveroptions["server_password"] = None
serveroptions["disable_item_cheat"] = False
serveroptions["location_check_points"] = 1
serveroptions["hint_cost"] = 1000
serveroptions["forfeit_mode"] = "goal"
serveroptions["remaining_mode"] = "goal"
serveroptions["auto_shutdown"] = 0
serveroptions["compatibility"] = 2
options["server_options"] = serveroptions
multimysteryoptions = dict()
multimysteryoptions["teams"] = 1
multimysteryoptions["enemizer_path"] = "EnemizerCLI/EnemizerCLI.Core.exe"
multimysteryoptions["player_files_path"] = "Players"
multimysteryoptions["players"] = 0
multimysteryoptions["weights_file_path"] = "weights.yaml"
multimysteryoptions["meta_file_path"] = "meta.yaml"
multimysteryoptions["player_name"] = ""
multimysteryoptions["create_spoiler"] = 1
multimysteryoptions["zip_roms"] = 0
multimysteryoptions["zip_diffs"] = 2
multimysteryoptions["zip_spoiler"] = 0
multimysteryoptions["zip_multidata"] = 1
multimysteryoptions["zip_format"] = 1
multimysteryoptions["race"] = 0
multimysteryoptions["cpu_threads"] = 0
multimysteryoptions["max_attempts"] = 0
multimysteryoptions["take_first_working"] = False
multimysteryoptions["keep_all_seeds"] = False
multimysteryoptions["log_output_path"] = "Output Logs"
multimysteryoptions["log_level"] = None
options["multi_mystery_options"] = multimysteryoptions
get_default_options.options = options get_default_options.options = options
return get_default_options.options return get_default_options.options
@ -254,6 +262,7 @@ def update_options(src: dict, dest: dict, filename: str, keys: list) -> dict:
dest[key] = update_options(value, dest[key], filename, new_keys) dest[key] = update_options(value, dest[key], filename, new_keys)
return dest return dest
def get_options() -> dict: def get_options() -> dict:
if not hasattr(get_options, "options"): if not hasattr(get_options, "options"):
locations = ("options.yaml", "host.yaml", locations = ("options.yaml", "host.yaml",
@ -350,7 +359,6 @@ def get_adjuster_settings(romfile: str) -> typing.Tuple[str, bool]:
return romfile, False return romfile, False
class ReceivedItem(typing.NamedTuple): class ReceivedItem(typing.NamedTuple):
item: int item: int
location: int location: int

View File

@ -73,7 +73,7 @@ def roll_options(options: Dict[str, Union[dict, str]]) -> Tuple[Dict[str, Union[
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) rolled_results[filename] = roll_settings(yaml_data, plando_options={"bosses"})
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}"
else: else:

View File

@ -99,3 +99,6 @@ multi_mystery_options:
zip_format: 1 zip_format: 1
# Create encrypted race roms # Create encrypted race roms
race: 0 race: 0
# List of options that can be plando'd. Can be combined, for example "bosses, items"
# Available options: bosses
plando_options: "bosses"