From 6ddfbdf709410929921c76ff3a840e5ae72d0386 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Wed, 3 Mar 2021 02:20:37 -0800 Subject: [PATCH 01/12] Allow pre-rolling yaml settings, and re-using the exact same pre-rolled settings later, for different actual seeds. --- MultiMystery.py | 6 ++++++ Mystery.py | 30 ++++++++++++++++++++++++++---- Utils.py | 2 ++ host.yaml | 5 +++++ 4 files changed, 39 insertions(+), 4 deletions(-) diff --git a/MultiMystery.py b/MultiMystery.py index 237e432b..f78a6b66 100644 --- a/MultiMystery.py +++ b/MultiMystery.py @@ -61,6 +61,8 @@ if __name__ == "__main__": player_name = multi_mystery_options["player_name"] 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"] + use_pre_rolled = multi_mystery_options["use_pre_rolled"] teams = multi_mystery_options["teams"] rom_file = options["general_options"]["rom_file"] host = options["server_options"]["host"] @@ -119,6 +121,10 @@ if __name__ == "__main__": 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" + if use_pre_rolled: + command += " --use_pre_rolled" logging.info(command) import time diff --git a/Mystery.py b/Mystery.py index d6d55bde..72fcaac4 100644 --- a/Mystery.py +++ b/Mystery.py @@ -12,7 +12,7 @@ from BaseClasses import PlandoItem, PlandoConnection ModuleUpdate.update() import Bosses -from Utils import parse_yaml +from Utils import parse_yaml, unsafe_parse_yaml from Rom import Sprite from EntranceRandomizer import parse_arguments from Main import main as ERmain @@ -37,6 +37,8 @@ def mystery_argparse(): parser.add_argument('--teams', default=1, type=lambda value: max(int(value), 1)) parser.add_argument('--create_spoiler', action='store_true') parser.add_argument('--skip_playthrough', action='store_true') + parser.add_argument('--pre_roll', action='store_true') + parser.add_argument('--use_pre_rolled', action='store_true') parser.add_argument('--rom') parser.add_argument('--enemizercli') parser.add_argument('--outputpath') @@ -93,7 +95,7 @@ def main(args=None, callback=ERmain): if path: try: if path not in weights_cache: - weights_cache[path] = get_weights(path) + weights_cache[path] = get_weights(path, args.use_pre_rolled) print(f"P{player} Weights: {path} >> " f"{get_choice('description', weights_cache[path], 'No description specified')}") @@ -180,6 +182,23 @@ def main(args=None, callback=ERmain): settings.sprite): logging.warning( f"Warning: The chosen sprite, \"{settings.sprite}\", for yaml \"{path}\", does not exist.") + if args.pre_roll: + import yaml + if path == args.weights: + settings.name = f"Player{player}" + elif not settings.name: + settings.name = os.path.split(path)[-1].split(".")[0] + + if "-" not in settings.shuffle and settings.shuffle != "vanilla": + settings.shuffle += f"-{random.randint(0, 2 ** 64)}" + + if "-" not in settings.door_shuffle and settings.door_shuffle != "vanilla": + settings.door_shuffle += f"-{random.randint(0, 2 ** 64)}" + + pre_rolled = dict() + pre_rolled["pre_rolled"] = settings + with open(os.path.join(args.outputpath if args.outputpath else ".", f"{os.path.split(path)[-1].split('.')[0]}_pre_rolled_{seedname}.yaml"), "wt") as f: + yaml.dump(pre_rolled, f) for k, v in vars(settings).items(): if v is not None: getattr(erargs, k)[player] = v @@ -223,7 +242,7 @@ def main(args=None, callback=ERmain): callback(erargs, seed) -def get_weights(path): +def get_weights(path, use_pre_rolled=False): try: if urllib.parse.urlparse(path).scheme: yaml = str(urllib.request.urlopen(path).read(), "utf-8") @@ -233,7 +252,7 @@ def get_weights(path): except Exception as e: raise Exception(f"Failed to read weights ({path})") from e - return parse_yaml(yaml) + return unsafe_parse_yaml(yaml) if use_pre_rolled else parse_yaml(yaml) def interpret_on_off(value): @@ -349,6 +368,9 @@ def roll_triggers(weights: dict) -> dict: return weights def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("bosses"))): + if "pre_rolled" in weights: + return weights["pre_rolled"] + if "linked_options" in weights: weights = roll_linked_options(weights) diff --git a/Utils.py b/Utils.py index 758f0058..489f7995 100644 --- a/Utils.py +++ b/Utils.py @@ -213,6 +213,8 @@ def get_default_options() -> dict: "players": 0, "weights_file_path": "weights.yaml", "meta_file_path": "meta.yaml", + "pre_roll": False, + "use_pre_rolled": False, "player_name": "", "create_spoiler": 1, "zip_roms": 0, diff --git a/host.yaml b/host.yaml index bccfc9e5..933f393f 100644 --- a/host.yaml +++ b/host.yaml @@ -66,6 +66,11 @@ multi_mystery_options: weights_file_path: "weights.yaml" # Meta file name, within the stated player_files_path location 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. + pre_roll: false + # Option to use pre-rolled settings. If not enabled, attempts to use a pre-rolled yaml WILL fail with + # "Please fix your yaml." + use_pre_rolled: false # Automatically launches {player_name}.yaml's ROM file using the OS's default program once generation completes. (likely your emulator) # Does nothing if the name is not found # Example: player_name = "Berserker" From 475d39932c5a6221d4aefb44c80fdbb891138bee Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Wed, 3 Mar 2021 13:02:55 -0800 Subject: [PATCH 02/12] main doesn't have doors to roll. --- Mystery.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/Mystery.py b/Mystery.py index 72fcaac4..d2a0fb09 100644 --- a/Mystery.py +++ b/Mystery.py @@ -192,9 +192,6 @@ def main(args=None, callback=ERmain): if "-" not in settings.shuffle and settings.shuffle != "vanilla": settings.shuffle += f"-{random.randint(0, 2 ** 64)}" - if "-" not in settings.door_shuffle and settings.door_shuffle != "vanilla": - settings.door_shuffle += f"-{random.randint(0, 2 ** 64)}" - pre_rolled = dict() pre_rolled["pre_rolled"] = settings with open(os.path.join(args.outputpath if args.outputpath else ".", f"{os.path.split(path)[-1].split('.')[0]}_pre_rolled_{seedname}.yaml"), "wt") as f: From 280f3938edff8916646f9aaa9aa79b399ba48406 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Wed, 3 Mar 2021 17:47:35 -0500 Subject: [PATCH 03/12] Add Triforce HUD options to weighted settings page - Added triforce hud options - Fix a bug causing weighted settings to always fail to load (git blame forthcoming) - Update ws_version to prompt users to update - Who is Murahalda? (corrected to Murahadala) --- .../static/static/weightedSettings.json | 63 ++++++++++--------- .../static/static/weightedSettings.yaml | 7 ++- playerSettings.yaml | 6 +- 3 files changed, 41 insertions(+), 35 deletions(-) diff --git a/WebHostLib/static/static/weightedSettings.json b/WebHostLib/static/static/weightedSettings.json index f1f7706c..a9bd1fca 100644 --- a/WebHostLib/static/static/weightedSettings.json +++ b/WebHostLib/static/static/weightedSettings.json @@ -1602,37 +1602,6 @@ } } }, - "triforcehud": { - "keyString": "rom.triforcehud", - "friendlyName": "Triforce Hud Options", - "description": "Hide the triforce hud in certain circumstances.", - "inputType": "range", - "subOptions": { - "normal": { - "keyString": "rom.triforcehud.normal", - "friendlyName": "Normal", - "description": "Always displays HUD as usual.", - "defaultValue": 0 - }, - "hide_goal": { - "keyString": "rom.triforcehud.hide_goal", - "friendlyName": "Hide Goal", - "description": "Hide Triforce Hud elements until a single triforce piece is acquired or spoken to Murahadala", - "defaultValue": 50 - }, - "hide_total": { - "keyString": "rom.triforcehud.hide_required", - "friendlyName": "Hide Required Total", - "description": "Hide total amount needed to win the game (unless spoken to Murahadala)", - "defaultValue": 0 - }, - "hide_both": { - "keyString": "rom.triforcehud.hide_both", - "friendlyName": "Hide Both", - "description": "Hide both of the above options", - "defaultValue": 0 - } - }, "reduceflashing": { "keyString": "rom.reduceflashing", "friendlyName": "Full-Screen Flashing Effects", @@ -1673,6 +1642,38 @@ } } }, + "triforcehud": { + "keyString": "rom.triforcehud", + "friendlyName": "Triforce Hunt HUD Options", + "description": "Hide the triforce hud in certain circumstances.", + "inputType": "range", + "subOptions": { + "normal": { + "keyString": "rom.triforcehud.normal", + "friendlyName": "Always Show", + "description": "Always display HUD", + "defaultValue": 50 + }, + "hide_goal": { + "keyString": "rom.triforcehud.hide_goal", + "friendlyName": "Hide HUD", + "description": "Hide Triforce HUD elements until a single triforce piece is acquired or you speak to Murahadala", + "defaultValue": 0 + }, + "hide_total": { + "keyString": "rom.triforcehud.hide_required", + "friendlyName": "Hide Total", + "description": "Hide total triforce pieces needed to win the game until you speak with Murahadala", + "defaultValue": 0 + }, + "hide_both": { + "keyString": "rom.triforcehud.hide_both", + "friendlyName": "Hide HUD Total", + "description": "Combination of Hide HUD and Hide Total", + "defaultValue": 0 + } + } + }, "menuspeed": { "keyString": "menuspeed", "friendlyName": "Menu Speed", diff --git a/WebHostLib/static/static/weightedSettings.yaml b/WebHostLib/static/static/weightedSettings.yaml index 657c32f6..d309f205 100644 --- a/WebHostLib/static/static/weightedSettings.yaml +++ b/WebHostLib/static/static/weightedSettings.yaml @@ -20,7 +20,7 @@ # For use with the weighted-settings page on the website. Changing this value will cause all users to be prompted # to update their settings. The version number should match the current released version number, and the revision # should be updated manually by whoever edits this file. -ws_version: 4.0.1 rev1 +ws_version: 4.1.0 rev0 description: Template Name # Used to describe your yaml. Useful if you have multiple files name: YourName # Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit @@ -363,6 +363,11 @@ rom: quickswap: # Enable switching items by pressing the L+R shoulder buttons on: 50 off: 0 + triforcehud: # Disable visibility of the triforce hud unless collecting a piece or speaking to Murahadala + normal: 50 # original behavior (always visible) + hide_goal: 0 # hide counter until a piece is collected or speaking to Murahadala + hide_required: 0 # Always visible, but required amount is invisible until determined by Murahadala + hide_both: 0 # Hide both under above circumstances reduceflashing: # Reduces instances of flashing such as lightning attacks, weather, ether and more. on: 50 off: 0 diff --git a/playerSettings.yaml b/playerSettings.yaml index 9148d7a9..67d4fd58 100644 --- a/playerSettings.yaml +++ b/playerSettings.yaml @@ -404,10 +404,10 @@ rom: quickswap: # Enable switching items by pressing the L+R shoulder buttons on: 50 off: 0 - triforcehud: # Disable visibility of the triforce hud unless collecting a piece or speaking to Murahalda + triforcehud: # Disable visibility of the triforce hud unless collecting a piece or speaking to Murahadala normal: 0 # original behavior (always visible) - hide_goal: 50 # hide counter until a piece is collected or speaking to Murahalda - hide_required: 0 # Always visible, but required amount is invisible until determined by Murahalda + hide_goal: 50 # hide counter until a piece is collected or speaking to Murahadala + hide_required: 0 # Always visible, but required amount is invisible until determined by Murahadala hide_both: 0 # Hide both under above circumstances reduceflashing: # Reduces instances of flashing such as lightning attacks, weather, ether and more. on: 50 From f130829c0cbaccf29bfc5c7449251beda309429d Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 4 Mar 2021 08:10:30 +0100 Subject: [PATCH 04/12] Massively speed up progression balancing for very large multiworlds Several times faster was observed in testing. 10+ hours to less than 2 in the last sample. --- Fill.py | 78 +++++++++++++++++++++++++++++++++------------------------ 1 file changed, 45 insertions(+), 33 deletions(-) diff --git a/Fill.py b/Fill.py index 0e797be1..26b4f5f8 100644 --- a/Fill.py +++ b/Fill.py @@ -42,9 +42,10 @@ def fill_restrictive(world, base_state: CollectionState, locations, itempool, si for item_to_place in items_to_place: perform_access_check = True if world.accessibility[item_to_place.player] == 'none': - perform_access_check = not world.has_beaten_game(maximum_exploration_state, item_to_place.player) if single_player_placement else not has_beaten_game + perform_access_check = not world.has_beaten_game(maximum_exploration_state, + item_to_place.player) if single_player_placement else not has_beaten_game for location in locations: - if (not single_player_placement or location.player == item_to_place.player)\ + if (not single_player_placement or location.player == item_to_place.player) \ and location.can_fill(maximum_exploration_state, item_to_place, perform_access_check): spot_to_fill = location break @@ -70,6 +71,7 @@ def fill_restrictive(world, base_state: CollectionState, locations, itempool, si itempool.extend(unplaced_items) + def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None): # If not passed in, then get a shuffled list of locations to fill in if not fill_locations: @@ -241,15 +243,14 @@ def balance_multiworld_progression(world): else: logging.info(f'Balancing multiworld progression for {len(balanceable_players)} Players.') state = CollectionState(world) - checked_locations = [] - unchecked_locations = world.get_locations().copy() - world.random.shuffle(unchecked_locations) + checked_locations = set() + unchecked_locations = set(world.get_locations()) reachable_locations_count = {player: 0 for player in world.player_ids} def get_sphere_locations(sphere_state, locations): sphere_state.sweep_for_events(key_only=True, locations=locations) - return [loc for loc in locations if sphere_state.can_reach(loc)] + return {loc for loc in locations if sphere_state.can_reach(loc)} while True: sphere_locations = get_sphere_locations(state, unchecked_locations) @@ -259,14 +260,14 @@ def balance_multiworld_progression(world): if checked_locations: threshold = max(reachable_locations_count.values()) - 20 - balancing_players = [player for player, reachables in reachable_locations_count.items() if - reachables < threshold and player in balanceable_players] + balancing_players = {player for player, reachables in reachable_locations_count.items() if + reachables < threshold and player in balanceable_players} if balancing_players: balancing_state = state.copy() balancing_unchecked_locations = unchecked_locations.copy() balancing_reachables = reachable_locations_count.copy() balancing_sphere = sphere_locations.copy() - candidate_items = collections.defaultdict(list) + candidate_items = collections.defaultdict(set) while True: for location in balancing_sphere: if location.event: @@ -274,7 +275,7 @@ def balance_multiworld_progression(world): player = location.item.player # only replace items that end up in another player's world if not location.locked and player in balancing_players and location.player != player: - candidate_items[player].append(location) + candidate_items[player].add(location) balancing_sphere = get_sphere_locations(balancing_state, balancing_unchecked_locations) for location in balancing_sphere: balancing_unchecked_locations.remove(location) @@ -284,10 +285,10 @@ def balance_multiworld_progression(world): break elif not balancing_sphere: raise RuntimeError('Not all required items reachable. Something went terribly wrong here.') - unlocked_locations = collections.defaultdict(list) + unlocked_locations = collections.defaultdict(set) for l in unchecked_locations: if l not in balancing_unchecked_locations: - unlocked_locations[l.player].append(l) + unlocked_locations[l.player].add(l) items_to_replace = [] for player in balancing_players: locations_to_test = unlocked_locations[player] @@ -297,7 +298,6 @@ def balance_multiworld_progression(world): reducing_state = state.copy() for location in itertools.chain((l for l in items_to_replace if l.item.player == player), items_to_test): - reducing_state.collect(location.item, True, location) reducing_state.sweep_for_events(locations=locations_to_test) @@ -311,33 +311,40 @@ def balance_multiworld_progression(world): items_to_replace.append(testing) replaced_items = False - replacement_locations = [l for l in checked_locations if not l.event and not l.locked] + + # sort then shuffle to maintain deterministic behaviour, + # while allowing use of set for better algorithm growth behaviour elsewhere + replacement_locations = sorted(l for l in checked_locations if not l.event and not l.locked) + world.random.shuffle(replacement_locations) + items_to_replace.sort() + world.random.shuffle(items_to_replace) + while replacement_locations and items_to_replace: - new_location = replacement_locations.pop() old_location = items_to_replace.pop() - - while not new_location.can_fill(state, old_location.item, False) or ( - new_location.item and not old_location.can_fill(state, new_location.item, False)): - replacement_locations.insert(0, new_location) - new_location = replacement_locations.pop() - - swap_location_item(old_location, new_location) - logging.debug(f"Progression balancing moved {new_location.item} to {new_location}, " - f"displacing {old_location.item} into {old_location}") - state.collect(new_location.item, True, new_location) - replaced_items = True + for new_location in replacement_locations: + if new_location.can_fill(state, old_location.item, False) and \ + old_location.can_fill(state, new_location.item, False): + replacement_locations.remove(new_location) + swap_location_item(old_location, new_location) + logging.debug(f"Progression balancing moved {new_location.item} to {new_location}, " + f"displacing {old_location.item} into {old_location}") + state.collect(new_location.item, True, new_location) + replaced_items = True + break + else: + logging.warning(f"Could not Progression Balance {old_location.item}") if replaced_items: - unlocked = [fresh for player in balancing_players for fresh in unlocked_locations[player]] + unlocked = {fresh for player in balancing_players for fresh in unlocked_locations[player]} for location in get_sphere_locations(state, unlocked): unchecked_locations.remove(location) reachable_locations_count[location.player] += 1 - sphere_locations.append(location) + sphere_locations.add(location) for location in sphere_locations: if location.event: state.collect(location.item, True, location) - checked_locations.extend(sphere_locations) + checked_locations |= sphere_locations if world.has_beaten_game(state): break @@ -378,7 +385,8 @@ def distribute_planned(world): set(world.player_ids) - {player}) if location.item_rule(item) ) if not unfilled: - placement.failed(f"Could not find a world with an unfilled location {placement.location}", FillError) + placement.failed(f"Could not find a world with an unfilled location {placement.location}", + FillError) continue target_world = world.random.choice(unfilled).player @@ -389,18 +397,22 @@ def distribute_planned(world): set(world.player_ids)) if location.item_rule(item) ) if not unfilled: - placement.failed(f"Could not find a world with an unfilled location {placement.location}", FillError) + placement.failed(f"Could not find a world with an unfilled location {placement.location}", + FillError) continue target_world = world.random.choice(unfilled).player elif type(target_world) == int: # target world by player id if target_world not in range(1, world.players + 1): - placement.failed(f"Cannot place item in world {target_world} as it is not in range of (1, {world.players})", ValueError) + placement.failed( + f"Cannot place item in world {target_world} as it is not in range of (1, {world.players})", + ValueError) continue else: # find world by name if target_world not in world_name_lookup: - placement.failed(f"Cannot place item to {target_world}'s world as that world does not exist.", ValueError) + placement.failed(f"Cannot place item to {target_world}'s world as that world does not exist.", + ValueError) continue target_world = world_name_lookup[target_world] From 2b553cd1c5c04dd3db3efb24bcc80a3038b18c53 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 4 Mar 2021 08:44:07 +0100 Subject: [PATCH 05/12] Give security warning --- host.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/host.yaml b/host.yaml index 933f393f..558d5e05 100644 --- a/host.yaml +++ b/host.yaml @@ -70,6 +70,7 @@ multi_mystery_options: pre_roll: false # Option to use pre-rolled settings. If not enabled, attempts to use a pre-rolled yaml WILL fail with # "Please fix your yaml." + # Warning: only use this to load files you've created yourself. use_pre_rolled: false # Automatically launches {player_name}.yaml's ROM file using the OS's default program once generation completes. (likely your emulator) # Does nothing if the name is not found From 60e032510d635468acef10b140a48085114691c8 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Fri, 5 Mar 2021 02:50:40 -0800 Subject: [PATCH 06/12] Make pre_rolled safer by converting namespace/plandoitems/plandoconnections to/from dict. --- BaseClasses.py | 12 ++++++++++++ Mystery.py | 24 ++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index e226c453..76714117 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1508,8 +1508,20 @@ class PlandoItem(NamedTuple): else: self.warn(warning) + def to_dict(self): + return {"item": self.item, + "location": self.location, + "world": self.world, + "from_pool": self.from_pool, + "force": self.force} + class PlandoConnection(NamedTuple): entrance: str exit: str direction: str # entrance, exit or both + + def to_dict(self): + return {"entrance": self.entrance, + "exit": self.exit, + "direction": self.direction} diff --git a/Mystery.py b/Mystery.py index d2a0fb09..eb423dce 100644 --- a/Mystery.py +++ b/Mystery.py @@ -193,7 +193,13 @@ def main(args=None, callback=ERmain): settings.shuffle += f"-{random.randint(0, 2 ** 64)}" pre_rolled = dict() - pre_rolled["pre_rolled"] = settings + 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_{seedname}.yaml"), "wt") as f: yaml.dump(pre_rolled, f) for k, v in vars(settings).items(): @@ -366,7 +372,21 @@ def roll_triggers(weights: dict) -> dict: def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("bosses"))): if "pre_rolled" in weights: - return weights["pre_rolled"] + pre_rolled = weights["pre_rolled"] + if isinstance(pre_rolled, argparse.Namespace): + return pre_rolled # Still accept old format pre-rolled, but only with unsafe loading. + + 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 "plando_connections" in pre_rolled: + pre_rolled["plando_connections"] = [PlandoConnection(connection["entrance"], + connection["exit"], + connection["direction"]) for connection in pre_rolled["plando_connections"]] + return argparse.Namespace(**pre_rolled) if "linked_options" in weights: weights = roll_linked_options(weights) From d0d995b3a4f9f6e41bf60ed0e8b02172cc714eee Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Fri, 5 Mar 2021 11:05:57 -0800 Subject: [PATCH 07/12] Use the more convenient _asdict() function of named tuples. --- BaseClasses.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 76714117..64453fd8 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1509,11 +1509,7 @@ class PlandoItem(NamedTuple): self.warn(warning) def to_dict(self): - return {"item": self.item, - "location": self.location, - "world": self.world, - "from_pool": self.from_pool, - "force": self.force} + return self._asdict() class PlandoConnection(NamedTuple): @@ -1522,6 +1518,4 @@ class PlandoConnection(NamedTuple): direction: str # entrance, exit or both def to_dict(self): - return {"entrance": self.entrance, - "exit": self.exit, - "direction": self.direction} + return self._asdict() From d09a03aace5a90887ab123789787194131081b89 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Fri, 5 Mar 2021 11:32:24 -0800 Subject: [PATCH 08/12] Rip out unsafe yaml loading. --- MultiMystery.py | 3 --- Mystery.py | 14 +++++++------- Utils.py | 1 - host.yaml | 6 ++---- 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/MultiMystery.py b/MultiMystery.py index f78a6b66..d569d2a0 100644 --- a/MultiMystery.py +++ b/MultiMystery.py @@ -62,7 +62,6 @@ if __name__ == "__main__": 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"] - use_pre_rolled = multi_mystery_options["use_pre_rolled"] teams = multi_mystery_options["teams"] rom_file = options["general_options"]["rom_file"] host = options["server_options"]["host"] @@ -123,8 +122,6 @@ if __name__ == "__main__": command += f" --weights {weights_file_path}" if pre_roll: command += " --pre_roll" - if use_pre_rolled: - command += " --use_pre_rolled" logging.info(command) import time diff --git a/Mystery.py b/Mystery.py index eb423dce..98f07230 100644 --- a/Mystery.py +++ b/Mystery.py @@ -12,7 +12,7 @@ from BaseClasses import PlandoItem, PlandoConnection ModuleUpdate.update() import Bosses -from Utils import parse_yaml, unsafe_parse_yaml +from Utils import parse_yaml from Rom import Sprite from EntranceRandomizer import parse_arguments from Main import main as ERmain @@ -38,7 +38,6 @@ def mystery_argparse(): parser.add_argument('--create_spoiler', action='store_true') parser.add_argument('--skip_playthrough', action='store_true') parser.add_argument('--pre_roll', action='store_true') - parser.add_argument('--use_pre_rolled', action='store_true') parser.add_argument('--rom') parser.add_argument('--enemizercli') parser.add_argument('--outputpath') @@ -95,7 +94,7 @@ def main(args=None, callback=ERmain): if path: try: if path not in weights_cache: - weights_cache[path] = get_weights(path, args.use_pre_rolled) + weights_cache[path] = get_weights(path) print(f"P{player} Weights: {path} >> " f"{get_choice('description', weights_cache[path], 'No description specified')}") @@ -193,14 +192,15 @@ def main(args=None, callback=ERmain): settings.shuffle += f"-{random.randint(0, 2 ** 64)}" pre_rolled = dict() + pre_rolled["original_seed_number"] = seed + pre_rolled["original_seed_name"] = seedname 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_{seedname}.yaml"), "wt") as f: + with open(os.path.join(args.outputpath if args.outputpath else ".", f"{os.path.split(path)[-1].split('.')[0]}_pre_rolled.yaml"), "wt") as f: yaml.dump(pre_rolled, f) for k, v in vars(settings).items(): if v is not None: @@ -245,7 +245,7 @@ def main(args=None, callback=ERmain): callback(erargs, seed) -def get_weights(path, use_pre_rolled=False): +def get_weights(path): try: if urllib.parse.urlparse(path).scheme: yaml = str(urllib.request.urlopen(path).read(), "utf-8") @@ -255,7 +255,7 @@ def get_weights(path, use_pre_rolled=False): except Exception as e: raise Exception(f"Failed to read weights ({path})") from e - return unsafe_parse_yaml(yaml) if use_pre_rolled else parse_yaml(yaml) + return parse_yaml(yaml) def interpret_on_off(value): diff --git a/Utils.py b/Utils.py index 489f7995..e5e0090d 100644 --- a/Utils.py +++ b/Utils.py @@ -214,7 +214,6 @@ def get_default_options() -> dict: "weights_file_path": "weights.yaml", "meta_file_path": "meta.yaml", "pre_roll": False, - "use_pre_rolled": False, "player_name": "", "create_spoiler": 1, "zip_roms": 0, diff --git a/host.yaml b/host.yaml index 558d5e05..eb1d3875 100644 --- a/host.yaml +++ b/host.yaml @@ -67,11 +67,9 @@ multi_mystery_options: # Meta file name, within the stated player_files_path location 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 - # Option to use pre-rolled settings. If not enabled, attempts to use a pre-rolled yaml WILL fail with - # "Please fix your yaml." - # Warning: only use this to load files you've created yourself. - use_pre_rolled: false # Automatically launches {player_name}.yaml's ROM file using the OS's default program once generation completes. (likely your emulator) # Does nothing if the name is not found # Example: player_name = "Berserker" From 43643870da17693042428e2d385688dd6dbc88d2 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Fri, 5 Mar 2021 11:40:24 -0800 Subject: [PATCH 09/12] Remove the last bit of code that required unsafe yaml loading. --- Mystery.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Mystery.py b/Mystery.py index 98f07230..200f832e 100644 --- a/Mystery.py +++ b/Mystery.py @@ -373,8 +373,6 @@ def roll_triggers(weights: dict) -> dict: def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("bosses"))): if "pre_rolled" in weights: pre_rolled = weights["pre_rolled"] - if isinstance(pre_rolled, argparse.Namespace): - return pre_rolled # Still accept old format pre-rolled, but only with unsafe loading. if "plando_items" in pre_rolled: pre_rolled["plando_items"] = [PlandoItem(item["item"], From 8c809095518fc174f7d6b127d5fb917ca5a13240 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Fri, 5 Mar 2021 13:38:04 -0800 Subject: [PATCH 10/12] Correctly attribute items to each player. --- WebHostLib/tracker.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index e207c81d..891f2fb8 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -352,8 +352,9 @@ def getPlayerTracker(tracker: UUID, team: int, player: int): # Add items to player inventory for (ms_team, ms_player), locations_checked in room.multisave.get("location_checks", {}): + # logging.info(f"{ms_team}, {ms_player}, {locations_checked}") # Skip teams and players not matching the request - if ms_team != (team - 1) or ms_player != player: + if ms_team != (team - 1): continue # If the player does not have the item, do nothing @@ -362,7 +363,10 @@ def getPlayerTracker(tracker: UUID, team: int, player: int): continue item, recipient = locations[location, ms_player] - attribute_item_solo(inventory, item) + if recipient == player: + attribute_item_solo(inventory, item) + if ms_player != player: + continue checks_done[player_location_to_area[location]] += 1 checks_done["Total"] += 1 From 28fd8274b26166d4c9a9cf7c149e223c3b313b5f Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Fri, 5 Mar 2021 13:47:27 -0800 Subject: [PATCH 11/12] Set max progressive items. --- WebHostLib/tracker.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 891f2fb8..f707cd2e 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -397,7 +397,7 @@ def getPlayerTracker(tracker: UUID, team: int, player: int): sword_acquired = False sword_names = ['Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'] if "Progressive Sword" in acquired_items: - sword_url = icons[sword_names[inventory[progressive_items["Progressive Sword"]] - 1]] + sword_url = icons[sword_names[max(inventory[progressive_items["Progressive Sword"]], 4) - 1]] sword_acquired = True else: for sword in reversed(sword_names): @@ -410,7 +410,7 @@ def getPlayerTracker(tracker: UUID, team: int, player: int): gloves_acquired = False glove_names = ["Power Glove", "Titan Mitts"] if "Progressive Glove" in acquired_items: - gloves_url = icons[glove_names[inventory[progressive_items["Progressive Glove"]] - 1]] + gloves_url = icons[glove_names[max(inventory[progressive_items["Progressive Glove"]], 2) - 1]] gloves_acquired = True else: for glove in reversed(glove_names): @@ -423,7 +423,7 @@ def getPlayerTracker(tracker: UUID, team: int, player: int): bow_acquired = False bow_names = ["Bow", "Silver Bow"] if "Progressive Bow" in acquired_items: - bow_url = icons[bow_names[inventory[progressive_items["Progressive Bow"]] - 1]] + bow_url = icons[bow_names[max(inventory[progressive_items["Progressive Bow"]], 2) - 1]] bow_acquired = True else: for bow in reversed(bow_names): @@ -435,7 +435,7 @@ def getPlayerTracker(tracker: UUID, team: int, player: int): mail_url = icons["Green Mail"] mail_names = ["Blue Mail", "Red Mail"] if "Progressive Mail" in acquired_items: - mail_url = icons[mail_names[inventory[progressive_items["Progressive Mail"]] - 1]] + mail_url = icons[mail_names[max(inventory[progressive_items["Progressive Mail"]], 2) - 1]] else: for mail in reversed(mail_names): if mail in acquired_items: @@ -446,7 +446,7 @@ def getPlayerTracker(tracker: UUID, team: int, player: int): shield_acquired = False shield_names = ["Blue Shield", "Red Shield", "Mirror Shield"] if "Progressive Shield" in acquired_items: - shield_url = icons[shield_names[inventory[progressive_items["Progressive Shield"]] - 1]] + shield_url = icons[shield_names[max(inventory[progressive_items["Progressive Shield"]], 3) - 1]] shield_acquired = True else: for shield in reversed(shield_names): From 94b25112cd4986671db04874a4c854eb8726b2cd Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Fri, 5 Mar 2021 13:56:55 -0800 Subject: [PATCH 12/12] Whoops, min, not max. --- WebHostLib/tracker.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index f707cd2e..173ab5e4 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -397,7 +397,7 @@ def getPlayerTracker(tracker: UUID, team: int, player: int): sword_acquired = False sword_names = ['Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'] if "Progressive Sword" in acquired_items: - sword_url = icons[sword_names[max(inventory[progressive_items["Progressive Sword"]], 4) - 1]] + sword_url = icons[sword_names[min(inventory[progressive_items["Progressive Sword"]], 4) - 1]] sword_acquired = True else: for sword in reversed(sword_names): @@ -410,7 +410,7 @@ def getPlayerTracker(tracker: UUID, team: int, player: int): gloves_acquired = False glove_names = ["Power Glove", "Titan Mitts"] if "Progressive Glove" in acquired_items: - gloves_url = icons[glove_names[max(inventory[progressive_items["Progressive Glove"]], 2) - 1]] + gloves_url = icons[glove_names[min(inventory[progressive_items["Progressive Glove"]], 2) - 1]] gloves_acquired = True else: for glove in reversed(glove_names): @@ -423,7 +423,7 @@ def getPlayerTracker(tracker: UUID, team: int, player: int): bow_acquired = False bow_names = ["Bow", "Silver Bow"] if "Progressive Bow" in acquired_items: - bow_url = icons[bow_names[max(inventory[progressive_items["Progressive Bow"]], 2) - 1]] + bow_url = icons[bow_names[min(inventory[progressive_items["Progressive Bow"]], 2) - 1]] bow_acquired = True else: for bow in reversed(bow_names): @@ -435,7 +435,7 @@ def getPlayerTracker(tracker: UUID, team: int, player: int): mail_url = icons["Green Mail"] mail_names = ["Blue Mail", "Red Mail"] if "Progressive Mail" in acquired_items: - mail_url = icons[mail_names[max(inventory[progressive_items["Progressive Mail"]], 2) - 1]] + mail_url = icons[mail_names[min(inventory[progressive_items["Progressive Mail"]], 2) - 1]] else: for mail in reversed(mail_names): if mail in acquired_items: @@ -446,7 +446,7 @@ def getPlayerTracker(tracker: UUID, team: int, player: int): shield_acquired = False shield_names = ["Blue Shield", "Red Shield", "Mirror Shield"] if "Progressive Shield" in acquired_items: - shield_url = icons[shield_names[max(inventory[progressive_items["Progressive Shield"]], 3) - 1]] + shield_url = icons[shield_names[min(inventory[progressive_items["Progressive Shield"]], 3) - 1]] shield_acquired = True else: for shield in reversed(shield_names):