diff --git a/AdventureClient.py b/AdventureClient.py index d2f4e734ac..06e4d60dad 100644 --- a/AdventureClient.py +++ b/AdventureClient.py @@ -115,11 +115,12 @@ class AdventureContext(CommonContext): msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}" self._set_message(msg, SYSTEM_MESSAGE_ID) elif cmd == "Retrieved": - self.freeincarnates_used = args["keys"][f"adventure_{self.auth}_freeincarnates_used"] - if self.freeincarnates_used is None: - self.freeincarnates_used = 0 - self.freeincarnates_used += self.freeincarnate_pending - self.send_pending_freeincarnates() + if f"adventure_{self.auth}_freeincarnates_used" in args["keys"]: + self.freeincarnates_used = args["keys"][f"adventure_{self.auth}_freeincarnates_used"] + if self.freeincarnates_used is None: + self.freeincarnates_used = 0 + self.freeincarnates_used += self.freeincarnate_pending + self.send_pending_freeincarnates() elif cmd == "SetReply": if args["key"] == f"adventure_{self.auth}_freeincarnates_used": self.freeincarnates_used = args["value"] diff --git a/BaseClasses.py b/BaseClasses.py index 7965eb8b0d..dddcfc3a6f 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -252,15 +252,20 @@ class MultiWorld(): range(1, self.players + 1)} def set_options(self, args: Namespace) -> None: + # TODO - remove this section once all worlds use options dataclasses + all_keys: Set[str] = {key for player in self.player_ids for key in + AutoWorld.AutoWorldRegister.world_types[self.game[player]].options_dataclass.type_hints} + for option_key in all_keys: + option = Utils.DeprecateDict(f"Getting options from multiworld is now deprecated. " + f"Please use `self.options.{option_key}` instead.") + option.update(getattr(args, option_key, {})) + setattr(self, option_key, option) + for player in self.player_ids: world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]] self.worlds[player] = world_type(self, player) self.worlds[player].random = self.per_slot_randoms[player] - for option_key in world_type.options_dataclass.type_hints: - option_values = getattr(args, option_key, {}) - setattr(self, option_key, option_values) - # TODO - remove this loop once all worlds use options dataclasses - options_dataclass: typing.Type[Options.PerGameCommonOptions] = self.worlds[player].options_dataclass + options_dataclass: typing.Type[Options.PerGameCommonOptions] = world_type.options_dataclass self.worlds[player].options = options_dataclass(**{option_key: getattr(args, option_key)[player] for option_key in options_dataclass.type_hints}) @@ -491,7 +496,7 @@ class MultiWorld(): else: return all((self.has_beaten_game(state, p) for p in range(1, self.players + 1))) - def can_beat_game(self, starting_state: Optional[CollectionState] = None): + def can_beat_game(self, starting_state: Optional[CollectionState] = None) -> bool: if starting_state: if self.has_beaten_game(starting_state): return True @@ -504,7 +509,7 @@ class MultiWorld(): and location.item.advancement and location not in state.locations_checked} while prog_locations: - sphere = set() + sphere: Set[Location] = set() # build up spheres of collection radius. # Everything in each sphere is independent from each other in dependencies and only depends on lower spheres for location in prog_locations: @@ -524,12 +529,19 @@ class MultiWorld(): return False - def get_spheres(self): + def get_spheres(self) -> Iterator[Set[Location]]: + """ + yields a set of locations for each logical sphere + + If there are unreachable locations, the last sphere of reachable + locations is followed by an empty set, and then a set of all of the + unreachable locations. + """ state = CollectionState(self) locations = set(self.get_filled_locations()) while locations: - sphere = set() + sphere: Set[Location] = set() for location in locations: if location.can_reach(state): diff --git a/Fill.py b/Fill.py index 342c155079..525d27d338 100644 --- a/Fill.py +++ b/Fill.py @@ -550,7 +550,7 @@ def flood_items(world: MultiWorld) -> None: break -def balance_multiworld_progression(world: MultiWorld) -> None: +def balance_multiworld_progression(multiworld: MultiWorld) -> None: # A system to reduce situations where players have no checks remaining, popularly known as "BK mode." # Overall progression balancing algorithm: # Gather up all locations in a sphere. @@ -558,28 +558,28 @@ def balance_multiworld_progression(world: MultiWorld) -> None: # If other players are below the threshold value, swap progression in this sphere into earlier spheres, # which gives more locations available by this sphere. balanceable_players: typing.Dict[int, float] = { - player: world.worlds[player].options.progression_balancing / 100 - for player in world.player_ids - if world.worlds[player].options.progression_balancing > 0 + player: multiworld.worlds[player].options.progression_balancing / 100 + for player in multiworld.player_ids + if multiworld.worlds[player].options.progression_balancing > 0 } if not balanceable_players: logging.info('Skipping multiworld progression balancing.') else: logging.info(f'Balancing multiworld progression for {len(balanceable_players)} Players.') logging.debug(balanceable_players) - state: CollectionState = CollectionState(world) + state: CollectionState = CollectionState(multiworld) checked_locations: typing.Set[Location] = set() - unchecked_locations: typing.Set[Location] = set(world.get_locations()) + unchecked_locations: typing.Set[Location] = set(multiworld.get_locations()) total_locations_count: typing.Counter[int] = Counter( location.player - for location in world.get_locations() + for location in multiworld.get_locations() if not location.locked ) reachable_locations_count: typing.Dict[int, int] = { player: 0 - for player in world.player_ids - if total_locations_count[player] and len(world.get_filled_locations(player)) != 0 + for player in multiworld.player_ids + if total_locations_count[player] and len(multiworld.get_filled_locations(player)) != 0 } balanceable_players = { player: balanceable_players[player] @@ -658,7 +658,7 @@ def balance_multiworld_progression(world: MultiWorld) -> None: balancing_unchecked_locations.remove(location) if not location.locked: balancing_reachables[location.player] += 1 - if world.has_beaten_game(balancing_state) or all( + if multiworld.has_beaten_game(balancing_state) or all( item_percentage(player, reachables) >= threshold_percentages[player] for player, reachables in balancing_reachables.items() if player in threshold_percentages): @@ -675,7 +675,7 @@ def balance_multiworld_progression(world: MultiWorld) -> None: locations_to_test = unlocked_locations[player] items_to_test = list(candidate_items[player]) items_to_test.sort() - world.random.shuffle(items_to_test) + multiworld.random.shuffle(items_to_test) while items_to_test: testing = items_to_test.pop() reducing_state = state.copy() @@ -687,8 +687,8 @@ def balance_multiworld_progression(world: MultiWorld) -> None: reducing_state.sweep_for_events(locations=locations_to_test) - if world.has_beaten_game(balancing_state): - if not world.has_beaten_game(reducing_state): + if multiworld.has_beaten_game(balancing_state): + if not multiworld.has_beaten_game(reducing_state): items_to_replace.append(testing) else: reduced_sphere = get_sphere_locations(reducing_state, locations_to_test) @@ -696,33 +696,32 @@ def balance_multiworld_progression(world: MultiWorld) -> None: if p < threshold_percentages[player]: items_to_replace.append(testing) - replaced_items = False + old_moved_item_count = moved_item_count # 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) + multiworld.random.shuffle(replacement_locations) items_to_replace.sort() - world.random.shuffle(items_to_replace) + multiworld.random.shuffle(items_to_replace) # Start swapping items. Since we swap into earlier spheres, no need for accessibility checks. while replacement_locations and items_to_replace: old_location = items_to_replace.pop() - for new_location in replacement_locations: + for i, new_location in enumerate(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) + replacement_locations.pop(i) 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}") moved_item_count += 1 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: + if old_moved_item_count < moved_item_count: logging.debug(f"Moved {moved_item_count} items so far\n") unlocked = {fresh for player in balancing_players for fresh in unlocked_locations[player]} for location in get_sphere_locations(state, unlocked): @@ -736,7 +735,7 @@ def balance_multiworld_progression(world: MultiWorld) -> None: state.collect(location.item, True, location) checked_locations |= sphere_locations - if world.has_beaten_game(state): + if multiworld.has_beaten_game(state): break elif not sphere_locations: logging.warning("Progression Balancing ran out of paths.") diff --git a/Main.py b/Main.py index b64650478b..8dac8f7d20 100644 --- a/Main.py +++ b/Main.py @@ -117,6 +117,17 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No for item_name, count in world.start_inventory_from_pool.setdefault(player, StartInventoryPool({})).value.items(): for _ in range(count): world.push_precollected(world.create_item(item_name, player)) + # remove from_pool items also from early items handling, as starting is plenty early. + early = world.early_items[player].get(item_name, 0) + if early: + world.early_items[player][item_name] = max(0, early-count) + remaining_count = count-early + if remaining_count > 0: + local_early = world.early_local_items[player].get(item_name, 0) + if local_early: + world.early_items[player][item_name] = max(0, local_early - remaining_count) + del local_early + del early logger.info('Creating World.') AutoWorld.call_all(world, "create_regions") diff --git a/Utils.py b/Utils.py index 5955e92432..f6e4a9ab60 100644 --- a/Utils.py +++ b/Utils.py @@ -779,6 +779,25 @@ def deprecate(message: str): import warnings warnings.warn(message) + +class DeprecateDict(dict): + log_message: str + should_error: bool + + def __init__(self, message, error: bool = False) -> None: + self.log_message = message + self.should_error = error + super().__init__() + + def __getitem__(self, item: Any) -> Any: + if self.should_error: + deprecate(self.log_message) + elif __debug__: + import warnings + warnings.warn(self.log_message) + return super().__getitem__(item) + + def _extend_freeze_support() -> None: """Extend multiprocessing.freeze_support() to also work on Non-Windows for spawn.""" # upstream issue: https://github.com/python/cpython/issues/76327 diff --git a/WebHostLib/check.py b/WebHostLib/check.py index 4db2ec2ce3..e739dda02d 100644 --- a/WebHostLib/check.py +++ b/WebHostLib/check.py @@ -1,3 +1,4 @@ +import os import zipfile import base64 from typing import Union, Dict, Set, Tuple @@ -6,13 +7,7 @@ from flask import request, flash, redirect, url_for, render_template from markupsafe import Markup from WebHostLib import app - -banned_zip_contents = (".sfc",) - - -def allowed_file(filename): - return filename.endswith(('.txt', ".yaml", ".zip")) - +from WebHostLib.upload import allowed_options, allowed_options_extensions, banned_file from Generate import roll_settings, PlandoOptions from Utils import parse_yamls @@ -51,33 +46,41 @@ def mysterycheck(): def get_yaml_data(files) -> Union[Dict[str, str], str, Markup]: options = {} for uploaded_file in files: - # if user does not select file, browser also - # submit an empty part without filename - if uploaded_file.filename == '': - return 'No selected file' + if banned_file(uploaded_file.filename): + return ("Uploaded data contained a rom file, which is likely to contain copyrighted material. " + "Your file was deleted.") + # If the user does not select file, the browser will still submit an empty string without a file name. + elif uploaded_file.filename == "": + return "No selected file." elif uploaded_file.filename in options: - return f'Conflicting files named {uploaded_file.filename} submitted' - elif uploaded_file and allowed_file(uploaded_file.filename): + return f"Conflicting files named {uploaded_file.filename} submitted." + elif uploaded_file and allowed_options(uploaded_file.filename): if uploaded_file.filename.endswith(".zip"): + if not zipfile.is_zipfile(uploaded_file): + return f"Uploaded file {uploaded_file.filename} is not a valid .zip file and cannot be opened." - with zipfile.ZipFile(uploaded_file, 'r') as zfile: - infolist = zfile.infolist() + uploaded_file.seek(0) # offset from is_zipfile check + with zipfile.ZipFile(uploaded_file, "r") as zfile: + for file in zfile.infolist(): + # Remove folder pathing from str (e.g. "__MACOSX/" folder paths from archives created by macOS). + base_filename = os.path.basename(file.filename) - if any(file.filename.endswith(".archipelago") for file in infolist): - return Markup("Error: Your .zip file contains an .archipelago file. " - 'Did you mean to host a game?') - - for file in infolist: - if file.filename.endswith(banned_zip_contents): - return ("Uploaded data contained a rom file, " - "which is likely to contain copyrighted material. " - "Your file was deleted.") - elif file.filename.endswith((".yaml", ".json", ".yml", ".txt")): + if base_filename.endswith(".archipelago"): + return Markup("Error: Your .zip file contains an .archipelago file. " + 'Did you mean to host a game?') + elif base_filename.endswith(".zip"): + return "Nested .zip files inside a .zip are not supported." + elif banned_file(base_filename): + return ("Uploaded data contained a rom file, which is likely to contain copyrighted " + "material. Your file was deleted.") + # Ignore dot-files. + elif not base_filename.startswith(".") and allowed_options(base_filename): options[file.filename] = zfile.open(file, "r").read() else: options[uploaded_file.filename] = uploaded_file.read() + if not options: - return "Did not find a .yaml file to process." + return f"Did not find any valid files to process. Accepted formats: {allowed_options_extensions}" return options diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt index 654104252c..62707d78cf 100644 --- a/WebHostLib/requirements.txt +++ b/WebHostLib/requirements.txt @@ -5,5 +5,5 @@ Flask-Caching>=2.1.0 Flask-Compress>=1.14 Flask-Limiter>=3.5.0 bokeh>=3.1.1; python_version <= '3.8' -bokeh>=3.2.2; python_version >= '3.9' +bokeh>=3.3.2; python_version >= '3.9' markupsafe>=2.1.3 diff --git a/WebHostLib/upload.py b/WebHostLib/upload.py index e7ac033913..8f01294eac 100644 --- a/WebHostLib/upload.py +++ b/WebHostLib/upload.py @@ -19,7 +19,22 @@ from worlds.Files import AutoPatchRegister from . import app from .models import Seed, Room, Slot, GameDataPackage -banned_zip_contents = (".sfc", ".z64", ".n64", ".sms", ".gb") +banned_extensions = (".sfc", ".z64", ".n64", ".nes", ".smc", ".sms", ".gb", ".gbc", ".gba") +allowed_options_extensions = (".yaml", ".json", ".yml", ".txt", ".zip") +allowed_generation_extensions = (".archipelago", ".zip") + + +def allowed_options(filename: str) -> bool: + return filename.endswith(allowed_options_extensions) + + +def allowed_generation(filename: str) -> bool: + return filename.endswith(allowed_generation_extensions) + + +def banned_file(filename: str) -> bool: + return filename.endswith(banned_extensions) + def process_multidata(compressed_multidata, files={}): decompressed_multidata = MultiServer.Context.decompress(compressed_multidata) @@ -61,8 +76,8 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s if not owner: owner = session["_id"] infolist = zfile.infolist() - if all(file.filename.endswith((".yaml", ".yml")) or file.is_dir() for file in infolist): - flash(Markup("Error: Your .zip file only contains .yaml files. " + if all(allowed_options(file.filename) or file.is_dir() for file in infolist): + flash(Markup("Error: Your .zip file only contains options files. " 'Did you mean to generate a game?')) return @@ -73,7 +88,7 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s # Load files. for file in infolist: handler = AutoPatchRegister.get_handler(file.filename) - if file.filename.endswith(banned_zip_contents): + if banned_file(file.filename): return "Uploaded data contained a rom file, which is likely to contain copyrighted material. " \ "Your file was deleted." @@ -136,35 +151,34 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s flash("No multidata was found in the zip file, which is required.") -@app.route('/uploads', methods=['GET', 'POST']) +@app.route("/uploads", methods=["GET", "POST"]) def uploads(): - if request.method == 'POST': - # check if the post request has the file part - if 'file' not in request.files: - flash('No file part') + if request.method == "POST": + # check if the POST request has a file part. + if "file" not in request.files: + flash("No file part in POST request.") else: - file = request.files['file'] - # if user does not select file, browser also - # submit an empty part without filename - if file.filename == '': - flash('No selected file') - elif file and allowed_file(file.filename): - if zipfile.is_zipfile(file): - with zipfile.ZipFile(file, 'r') as zfile: + uploaded_file = request.files["file"] + # If the user does not select file, the browser will still submit an empty string without a file name. + if uploaded_file.filename == "": + flash("No selected file.") + elif uploaded_file and allowed_generation(uploaded_file.filename): + if zipfile.is_zipfile(uploaded_file): + with zipfile.ZipFile(uploaded_file, "r") as zfile: try: res = upload_zip_to_db(zfile) except VersionException: flash(f"Could not load multidata. Wrong Version detected.") else: - if type(res) == str: + if res is str: return res elif res: return redirect(url_for("view_seed", seed=res.id)) else: - file.seek(0) # offset from is_zipfile check + uploaded_file.seek(0) # offset from is_zipfile check # noinspection PyBroadException try: - multidata = file.read() + multidata = uploaded_file.read() slots, multidata = process_multidata(multidata) except Exception as e: flash(f"Could not load multidata. File may be corrupted or incompatible. ({e})") @@ -182,7 +196,3 @@ def user_content(): rooms = select(room for room in Room if room.owner == session["_id"]) seeds = select(seed for seed in Seed if seed.owner == session["_id"]) return render_template("userContent.html", rooms=rooms, seeds=seeds) - - -def allowed_file(filename): - return filename.endswith(('.archipelago', ".zip")) diff --git a/data/lua/connector_bizhawk_generic.lua b/data/lua/connector_bizhawk_generic.lua index c4e729300d..eff400cb03 100644 --- a/data/lua/connector_bizhawk_generic.lua +++ b/data/lua/connector_bizhawk_generic.lua @@ -585,7 +585,7 @@ else -- misaligned, so for GB and GBC we explicitly set the callback on -- vblank instead. -- https://github.com/TASEmulators/BizHawk/issues/3711 - if emu.getsystemid() == "GB" or emu.getsystemid() == "GBC" then + if emu.getsystemid() == "GB" or emu.getsystemid() == "GBC" or emu.getsystemid() == "SGB" then event.onmemoryexecute(tick, 0x40, "tick", "System Bus") else event.onframeend(tick) diff --git a/requirements.txt b/requirements.txt index 7d93928bb5..0db55a8035 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,13 @@ colorama>=0.4.5 -websockets>=11.0.3 +websockets>=12.0 PyYAML>=6.0.1 jellyfish>=1.0.3 jinja2>=3.1.2 schema>=0.7.5 -kivy>=2.2.0 +kivy>=2.2.1 bsdiff4>=1.2.4 platformdirs>=4.0.0 certifi>=2023.11.17 -cython>=3.0.5 +cython>=3.0.6 cymem>=2.0.8 orjson>=3.9.10 \ No newline at end of file diff --git a/test/general/test_ids.py b/test/general/test_ids.py index 4edfb8d994..98c41b67b1 100644 --- a/test/general/test_ids.py +++ b/test/general/test_ids.py @@ -1,5 +1,8 @@ import unittest -from worlds.AutoWorld import AutoWorldRegister + +from Fill import distribute_items_restrictive +from worlds.AutoWorld import AutoWorldRegister, call_all +from . import setup_solo_multiworld class TestIDs(unittest.TestCase): @@ -66,3 +69,34 @@ class TestIDs(unittest.TestCase): for gamename, world_type in AutoWorldRegister.world_types.items(): with self.subTest(game=gamename): self.assertEqual(len(world_type.location_id_to_name), len(world_type.location_name_to_id)) + + def test_postgen_datapackage(self): + """Generates a solo multiworld and checks that the datapackage is still valid""" + for gamename, world_type in AutoWorldRegister.world_types.items(): + with self.subTest(game=gamename): + multiworld = setup_solo_multiworld(world_type) + distribute_items_restrictive(multiworld) + call_all(multiworld, "post_fill") + datapackage = world_type.get_data_package_data() + for item_group, item_names in datapackage["item_name_groups"].items(): + self.assertIsInstance(item_group, str, + f"item_name_group names should be strings: {item_group}") + for item_name in item_names: + self.assertIsInstance(item_name, str, + f"{item_name}, in group {item_group} is not a string") + for loc_group, loc_names in datapackage["location_name_groups"].items(): + self.assertIsInstance(loc_group, str, + f"location_name_group names should be strings: {loc_group}") + for loc_name in loc_names: + self.assertIsInstance(loc_name, str, + f"{loc_name}, in group {loc_group} is not a string") + for item_name, item_id in datapackage["item_name_to_id"].items(): + self.assertIsInstance(item_name, str, + f"{item_name} is not a valid item name for item_name_to_id") + self.assertIsInstance(item_id, int, + f"{item_id} for {item_name} should be an int") + for loc_name, loc_id in datapackage["location_name_to_id"].items(): + self.assertIsInstance(loc_name, str, + f"{loc_name} is not a valid item name for location_name_to_id") + self.assertIsInstance(loc_id, int, + f"{loc_id} for {loc_name} should be an int") diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 5d0533e068..f56c39f690 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -77,6 +77,10 @@ class AutoWorldRegister(type): # create missing options_dataclass from legacy option_definitions # TODO - remove this once all worlds use options dataclasses if "options_dataclass" not in dct and "option_definitions" in dct: + # TODO - switch to deprecate after a version + if __debug__: + from warnings import warn + warn("Assigning options through option_definitions is now deprecated. Use options_dataclass instead.") dct["options_dataclass"] = make_dataclass(f"{name}Options", dct["option_definitions"].items(), bases=(PerGameCommonOptions,)) diff --git a/worlds/alttp/Dungeons.py b/worlds/alttp/Dungeons.py index a68acf7288..b456174f39 100644 --- a/worlds/alttp/Dungeons.py +++ b/worlds/alttp/Dungeons.py @@ -264,7 +264,7 @@ def fill_dungeons_restrictive(multiworld: MultiWorld): if loc in all_state_base.events: all_state_base.events.remove(loc) - fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True, + fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True, allow_excluded=True, name="LttP Dungeon Items") diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index 88a2d899fc..1c3f3e44f7 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -682,8 +682,6 @@ def get_pool_core(world, player: int): key_location = world.random.choice(key_locations) place_item(key_location, "Small Key (Universal)") pool = pool[:-3] - if world.key_drop_shuffle[player]: - pass # pool.extend([item_to_place] * (len(key_drop_data) - 1)) return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, additional_pieces_to_place) diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index 469f4f82ee..8a04f87afa 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -136,7 +136,8 @@ def mirrorless_path_to_castle_courtyard(world, player): def set_defeat_dungeon_boss_rule(location): # Lambda required to defer evaluation of dungeon.boss since it will change later if boss shuffle is used - set_rule(location, lambda state: location.parent_region.dungeon.boss.can_defeat(state)) + add_rule(location, lambda state: location.parent_region.dungeon.boss.can_defeat(state)) + def set_always_allow(spot, rule): spot.always_allow = rule diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 32667249f2..3f380d0037 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -289,12 +289,17 @@ class ALTTPWorld(World): self.waterfall_fairy_bottle_fill = self.random.choice(bottle_options) self.pyramid_fairy_bottle_fill = self.random.choice(bottle_options) - if multiworld.mode[player] == 'standard' \ - and multiworld.smallkey_shuffle[player] \ - and multiworld.smallkey_shuffle[player] != smallkey_shuffle.option_universal \ - and multiworld.smallkey_shuffle[player] != smallkey_shuffle.option_own_dungeons \ - and multiworld.smallkey_shuffle[player] != smallkey_shuffle.option_start_with: - self.multiworld.local_early_items[self.player]["Small Key (Hyrule Castle)"] = 1 + if multiworld.mode[player] == 'standard': + if multiworld.smallkey_shuffle[player]: + if (multiworld.smallkey_shuffle[player] not in + (smallkey_shuffle.option_universal, smallkey_shuffle.option_own_dungeons, + smallkey_shuffle.option_start_with)): + self.multiworld.local_early_items[self.player]["Small Key (Hyrule Castle)"] = 1 + self.multiworld.local_items[self.player].value.add("Small Key (Hyrule Castle)") + self.multiworld.non_local_items[self.player].value.discard("Small Key (Hyrule Castle)") + if multiworld.bigkey_shuffle[player]: + self.multiworld.local_items[self.player].value.add("Big Key (Hyrule Castle)") + self.multiworld.non_local_items[self.player].value.discard("Big Key (Hyrule Castle)") # system for sharing ER layouts self.er_seed = str(multiworld.random.randint(0, 2 ** 64)) diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index c897e72dcd..21a8c684f9 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -5,7 +5,7 @@ import os import shutil import threading import zipfile -from typing import Optional, TYPE_CHECKING, Any, List, Callable, Tuple +from typing import Optional, TYPE_CHECKING, Any, List, Callable, Tuple, Union import jinja2 @@ -63,7 +63,7 @@ recipe_time_ranges = { class FactorioModFile(worlds.Files.APContainer): game = "Factorio" compression_method = zipfile.ZIP_DEFLATED # Factorio can't load LZMA archives - writing_tasks: List[Callable[[], Tuple[str, str]]] + writing_tasks: List[Callable[[], Tuple[str, Union[str, bytes]]]] def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) @@ -164,9 +164,7 @@ def generate_mod(world: "Factorio", output_directory: str): template_data["free_sample_blacklist"].update({item: 1 for item in multiworld.free_sample_blacklist[player].value}) template_data["free_sample_blacklist"].update({item: 0 for item in multiworld.free_sample_whitelist[player].value}) - mod_dir = os.path.join(output_directory, versioned_mod_name) - - zf_path = os.path.join(mod_dir + ".zip") + zf_path = os.path.join(output_directory, versioned_mod_name + ".zip") mod = FactorioModFile(zf_path, player=player, player_name=multiworld.player_name[player]) if world.zip_path: @@ -177,7 +175,13 @@ def generate_mod(world: "Factorio", output_directory: str): mod.writing_tasks.append(lambda arcpath=versioned_mod_name+"/"+path_part, content=zf.read(file): (arcpath, content)) else: - shutil.copytree(os.path.join(os.path.dirname(__file__), "data", "mod"), mod_dir, dirs_exist_ok=True) + basepath = os.path.join(os.path.dirname(__file__), "data", "mod") + for dirpath, dirnames, filenames in os.walk(basepath): + base_arc_path = versioned_mod_name+"/"+os.path.relpath(dirpath, basepath) + for filename in filenames: + mod.writing_tasks.append(lambda arcpath=base_arc_path+"/"+filename, + file_path=os.path.join(dirpath, filename): + (arcpath, open(file_path, "rb").read())) mod.writing_tasks.append(lambda: (versioned_mod_name + "/data.lua", data_template.render(**template_data))) @@ -197,5 +201,3 @@ def generate_mod(world: "Factorio", output_directory: str): # write the mod file mod.write() - # clean up - shutil.rmtree(mod_dir) diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index 18eee67e03..b72d57ad9b 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -2,7 +2,7 @@ from __future__ import annotations import typing import datetime -from Options import Choice, OptionDict, OptionSet, ItemDict, Option, DefaultOnToggle, Range, DeathLink, Toggle, \ +from Options import Choice, OptionDict, OptionSet, Option, DefaultOnToggle, Range, DeathLink, Toggle, \ StartInventoryPool from schema import Schema, Optional, And, Or @@ -207,10 +207,9 @@ class RecipeIngredientsOffset(Range): range_end = 5 -class FactorioStartItems(ItemDict): +class FactorioStartItems(OptionDict): """Mapping of Factorio internal item-name to amount granted on start.""" display_name = "Starting Items" - verify_item_name = False default = {"burner-mining-drill": 19, "stone-furnace": 19} diff --git a/worlds/ffmq/Options.py b/worlds/ffmq/Options.py index eaf3097494..4b9f4a4a88 100644 --- a/worlds/ffmq/Options.py +++ b/worlds/ffmq/Options.py @@ -267,11 +267,11 @@ class CompanionLevelingType(Choice): class CompanionSpellbookType(Choice): """Update companions' spellbook. Standard: Original game spellbooks. - Standard Extended: Add some extra spells. Tristam gains Exit and Quake and Reuben gets Blizzard. + Extended: Add some extra spells. Tristam gains Exit and Quake and Reuben gets Blizzard. Random Balanced: Randomize the spellbooks with an appropriate mix of spells. Random Chaos: Randomize the spellbooks in total free-for-all.""" option_standard = 0 - option_standard_extended = 1 + option_extended = 1 option_random_balanced = 2 option_random_chaos = 3 default = 0 diff --git a/worlds/ffmq/Regions.py b/worlds/ffmq/Regions.py index aac8289a36..61f70864c0 100644 --- a/worlds/ffmq/Regions.py +++ b/worlds/ffmq/Regions.py @@ -67,10 +67,10 @@ def create_regions(self): self.multiworld.regions.append(create_region(self.multiworld, self.player, room["name"], room["id"], [FFMQLocation(self.player, object["name"], location_table[object["name"]] if object["name"] in location_table else None, object["type"], object["access"], - self.create_item(yaml_item(object["on_trigger"][0])) if object["type"] == "Trigger" else None) for - object in room["game_objects"] if "Hero Chest" not in object["name"] and object["type"] not in - ("BattlefieldGp", "BattlefieldXp") and (object["type"] != "Box" or - self.multiworld.brown_boxes[self.player] == "include")], room["links"])) + self.create_item(yaml_item(object["on_trigger"][0])) if object["type"] == "Trigger" else None) for object in + room["game_objects"] if "Hero Chest" not in object["name"] and object["type"] not in ("BattlefieldGp", + "BattlefieldXp") and (object["type"] != "Box" or self.multiworld.brown_boxes[self.player] == "include") and + not (object["name"] == "Kaeli Companion" and not object["on_trigger"])], room["links"])) dark_king_room = self.multiworld.get_region("Doom Castle Dark King Room", self.player) dark_king = FFMQLocation(self.player, "Dark King", None, "Trigger", []) diff --git a/worlds/ffmq/data/settings.yaml b/worlds/ffmq/data/settings.yaml index ff03ed26e6..826a8c744d 100644 --- a/worlds/ffmq/data/settings.yaml +++ b/worlds/ffmq/data/settings.yaml @@ -85,7 +85,7 @@ Final Fantasy Mystic Quest: Normal: 0 OneAndHalf: 0 Double: 0 - DoubleHalf: 0 + DoubleAndHalf: 0 Triple: 0 Quadruple: 0 companion_leveling_type: @@ -98,7 +98,7 @@ Final Fantasy Mystic Quest: BenPlus10: 0 companion_spellbook_type: Standard: 0 - StandardExtended: 0 + Extended: 0 RandomBalanced: 0 RandomChaos: 0 starting_companion: diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index f7e7e22e69..8b07b34eb0 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -419,17 +419,16 @@ class HKWorld(World): def set_rules(self): world = self.multiworld player = self.player - if world.logic[player] != 'nologic': - goal = world.Goal[player] - if goal == Goal.option_hollowknight: - world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) - elif goal == Goal.option_siblings: - world.completion_condition[player] = lambda state: state._hk_siblings_ending(player) - elif goal == Goal.option_radiance: - world.completion_condition[player] = lambda state: state._hk_can_beat_radiance(player) - else: - # Any goal - world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) or state._hk_can_beat_radiance(player) + goal = world.Goal[player] + if goal == Goal.option_hollowknight: + world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) + elif goal == Goal.option_siblings: + world.completion_condition[player] = lambda state: state._hk_siblings_ending(player) + elif goal == Goal.option_radiance: + world.completion_condition[player] = lambda state: state._hk_can_beat_radiance(player) + else: + # Any goal + world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) or state._hk_can_beat_radiance(player) set_rules(self) diff --git a/worlds/kh2/Client.py b/worlds/kh2/Client.py index be85dc6907..a5be06c7fb 100644 --- a/worlds/kh2/Client.py +++ b/worlds/kh2/Client.py @@ -29,6 +29,7 @@ class KH2Context(CommonContext): self.kh2_local_items = None self.growthlevel = None self.kh2connected = False + self.kh2_finished_game = False self.serverconneced = False self.item_name_to_data = {name: data for name, data, in item_dictionary_table.items()} self.location_name_to_data = {name: data for name, data, in all_locations.items()} @@ -833,9 +834,9 @@ async def kh2_watcher(ctx: KH2Context): await asyncio.create_task(ctx.verifyItems()) await asyncio.create_task(ctx.verifyLevel()) message = [{"cmd": 'LocationChecks', "locations": ctx.sending}] - if finishedGame(ctx, message): + if finishedGame(ctx, message) and not ctx.kh2_finished_game: await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) - ctx.finished_game = True + ctx.kh2_finished_game = True await ctx.send_msgs(message) elif not ctx.kh2connected and ctx.serverconneced: logger.info("Game Connection lost. waiting 15 seconds until trying to reconnect.") diff --git a/worlds/kh2/Regions.py b/worlds/kh2/Regions.py index aceab97f37..6dd8313107 100644 --- a/worlds/kh2/Regions.py +++ b/worlds/kh2/Regions.py @@ -1020,10 +1020,9 @@ def create_regions(self): multiworld.regions += [create_region(multiworld, player, active_locations, region, locations) for region, locations in KH2REGIONS.items()] # fill the event locations with events - multiworld.worlds[player].item_name_to_id.update({event_name: None for event_name in Events_Table}) for location, item in event_location_to_item.items(): multiworld.get_location(location, player).place_locked_item( - multiworld.worlds[player].create_item(item)) + multiworld.worlds[player].create_event_item(item)) def connect_regions(self): diff --git a/worlds/kh2/Rules.py b/worlds/kh2/Rules.py index 18375231a5..41207c6cb3 100644 --- a/worlds/kh2/Rules.py +++ b/worlds/kh2/Rules.py @@ -224,7 +224,7 @@ class KH2WorldRules(KH2Rules): RegionName.Pl2: lambda state: self.pl_unlocked(state, 2), RegionName.Ag: lambda state: self.ag_unlocked(state, 1), - RegionName.Ag2: lambda state: self.ag_unlocked(state, 2), + RegionName.Ag2: lambda state: self.ag_unlocked(state, 2) and self.kh2_has_all([ItemName.FireElement,ItemName.BlizzardElement,ItemName.ThunderElement],state), RegionName.Bc: lambda state: self.bc_unlocked(state, 1), RegionName.Bc2: lambda state: self.bc_unlocked(state, 2), diff --git a/worlds/kh2/__init__.py b/worlds/kh2/__init__.py index 69f844f45a..2bddbd5ec3 100644 --- a/worlds/kh2/__init__.py +++ b/worlds/kh2/__init__.py @@ -119,11 +119,15 @@ class KH2World(World): item_classification = ItemClassification.useful else: item_classification = ItemClassification.filler - created_item = KH2Item(name, item_classification, self.item_name_to_id[name], self.player) return created_item + def create_event_item(self, name: str) -> Item: + item_classification = ItemClassification.progression + created_item = KH2Item(name, item_classification, None, self.player) + return created_item + def create_items(self) -> None: """ Fills ItemPool and manages schmovement, random growth, visit locking and random starting visit locking. @@ -461,7 +465,7 @@ class KH2World(World): if location in self.random_super_boss_list: self.random_super_boss_list.remove(location) - if not self.options.SummonLevelLocationToggle: + if not self.options.SummonLevelLocationToggle and LocationName.Summonlvl7 in self.random_super_boss_list: self.random_super_boss_list.remove(LocationName.Summonlvl7) # Testing if the player has the right amount of Bounties for Completion. diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index a8dac86221..f22d344c8f 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -104,9 +104,11 @@ class LingoWorld(World): classification = item.classification if hasattr(self, "options") and self.options.shuffle_paintings and len(item.painting_ids) > 0\ and len(item.door_ids) == 0 and all(painting_id not in self.player_logic.painting_mapping - for painting_id in item.painting_ids): + for painting_id in item.painting_ids)\ + and "pilgrim_painting2" not in item.painting_ids: # If this is a "door" that just moves one or more paintings, and painting shuffle is on and those paintings - # go nowhere, then this item should not be progression. + # go nowhere, then this item should not be progression. The Pilgrim Room painting is special and needs to be + # excluded from this. classification = ItemClassification.filler return LingoItem(name, classification, item.code, self.player) diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index 8a4f831f94..ea5886fea0 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -373,6 +373,7 @@ ANOTHER TRY: id: Entry Room/Panel_advance tag: topwhite + non_counting: True # This is a counting panel in-game, but it can never count towards the LEVEL 2 panel hunt. LEVEL 2: # We will set up special rules for this in code. id: EndPanel/Panel_level_2 @@ -1033,6 +1034,8 @@ Hallway Room (3): True Hallway Room (4): True Hedge Maze: True # through the door to the sectioned-off part of the hedge maze + Cellar: + door: Lookout Entrance panels: MASSACRED: id: Palindrome Room/Panel_massacred_sacred @@ -1168,11 +1171,21 @@ - KEEP - BAILEY - TOWER + Lookout Entrance: + id: Cross Room Doors/Door_missing + location_name: Outside The Agreeable - Lookout Panels + panels: + - NORTH + - WINTER + - DIAMONDS + - FIRE paintings: - id: panda_painting orientation: south - id: eyes_yellow_painting orientation: east + - id: pencil_painting7 + orientation: north progression: Progressive Hallway Room: - Hallway Door @@ -2043,7 +2056,7 @@ door: Sixth Floor Cellar: room: Room Room - door: Shortcut to Fifth Floor + door: Cellar Exit Welcome Back Area: door: Welcome Back Art Gallery: @@ -2302,9 +2315,6 @@ id: Master Room/Panel_mastery_mastery3 tag: midwhite hunt: True - required_door: - room: Orange Tower Seventh Floor - door: Mastery THE LIBRARY: id: EndPanel/Panel_library check: True @@ -2675,6 +2685,10 @@ Outside The Undeterred: True Outside The Agreeable: True Outside The Wanderer: True + The Observant: True + Art Gallery: True + The Scientific: True + Cellar: True Orange Tower Fifth Floor: room: Orange Tower Fifth Floor door: Welcome Back @@ -2991,8 +3005,7 @@ PATS: id: Rhyme Room/Panel_wrath_path colors: purple - tag: midpurp and rhyme - copy_to_sign: sign15 + tag: forbid KNIGHT: id: Rhyme Room/Panel_knight_write colors: purple @@ -3158,6 +3171,8 @@ door: Painting Shortcut painting: True Room Room: True # trapdoor + Outside The Agreeable: + painting: True panels: UNOPEN: id: Truncate Room/Panel_unopened_open @@ -6299,17 +6314,22 @@ SKELETON: id: Double Room/Panel_bones_syn tag: syn rhyme + colors: purple subtag: bot link: rhyme BONES REPENTANCE: id: Double Room/Panel_sentence_rhyme - colors: purple + colors: + - purple + - blue tag: whole rhyme subtag: top link: rhyme SENTENCE WORD: id: Double Room/Panel_sentence_whole - colors: blue + colors: + - purple + - blue tag: whole rhyme subtag: bot link: rhyme SENTENCE @@ -6321,6 +6341,7 @@ link: rhyme DREAM FANTASY: id: Double Room/Panel_dream_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme DREAM @@ -6332,6 +6353,7 @@ link: rhyme MYSTERY SECRET: id: Double Room/Panel_mystery_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme MYSTERY @@ -6386,25 +6408,33 @@ door: Nines FERN: id: Double Room/Panel_return_rhyme - colors: purple + colors: + - purple + - black tag: ant rhyme subtag: top link: rhyme RETURN STAY: id: Double Room/Panel_return_ant - colors: black + colors: + - purple + - black tag: ant rhyme subtag: bot link: rhyme RETURN FRIEND: id: Double Room/Panel_descend_rhyme - colors: purple + colors: + - purple + - black tag: ant rhyme subtag: top link: rhyme DESCEND RISE: id: Double Room/Panel_descend_ant - colors: black + colors: + - purple + - black tag: ant rhyme subtag: bot link: rhyme DESCEND @@ -6416,6 +6446,7 @@ link: rhyme JUMP BOUNCE: id: Double Room/Panel_jump_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme JUMP @@ -6427,6 +6458,7 @@ link: rhyme FALL PLUNGE: id: Double Room/Panel_fall_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme FALL @@ -6456,13 +6488,17 @@ panels: BIRD: id: Double Room/Panel_word_rhyme - colors: purple + colors: + - purple + - blue tag: whole rhyme subtag: top link: rhyme WORD LETTER: id: Double Room/Panel_word_whole - colors: blue + colors: + - purple + - blue tag: whole rhyme subtag: bot link: rhyme WORD @@ -6474,6 +6510,7 @@ link: rhyme HIDDEN CONCEALED: id: Double Room/Panel_hidden_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme HIDDEN @@ -6485,6 +6522,7 @@ link: rhyme SILENT MUTE: id: Double Room/Panel_silent_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme SILENT @@ -6531,6 +6569,7 @@ link: rhyme BLOCKED OBSTRUCTED: id: Double Room/Panel_blocked_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme BLOCKED @@ -6542,6 +6581,7 @@ link: rhyme RISE SWELL: id: Double Room/Panel_rise_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme RISE @@ -6553,6 +6593,7 @@ link: rhyme ASCEND CLIMB: id: Double Room/Panel_ascend_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme ASCEND @@ -6564,6 +6605,7 @@ link: rhyme DOUBLE DUPLICATE: id: Double Room/Panel_double_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme DOUBLE @@ -6642,6 +6684,7 @@ link: rhyme CHILD KID: id: Double Room/Panel_child_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme CHILD @@ -6653,6 +6696,7 @@ link: rhyme CRYSTAL QUARTZ: id: Double Room/Panel_crystal_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme CRYSTAL @@ -6664,6 +6708,7 @@ link: rhyme CREATIVE INNOVATIVE (Bottom): id: Double Room/Panel_creative_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme CREATIVE @@ -6882,7 +6927,7 @@ event: True panels: - WALL (1) - Shortcut to Fifth Floor: + Cellar Exit: id: - Tower Room Area Doors/Door_panel_basement - Tower Room Area Doors/Door_panel_basement2 @@ -6895,7 +6940,10 @@ door: Excavation Orange Tower Fifth Floor: room: Room Room - door: Shortcut to Fifth Floor + door: Cellar Exit + Outside The Agreeable: + room: Outside The Agreeable + door: Lookout Entrance Outside The Wise: entrances: Orange Tower Sixth Floor: @@ -7319,49 +7367,65 @@ link: change GRAVITY PART: id: Chemistry Room/Panel_physics_2 - colors: blue + colors: + - blue + - red tag: blue mid red bot subtag: mid link: xur PARTICLE MATTER: id: Chemistry Room/Panel_physics_1 - colors: red + colors: + - blue + - red tag: blue mid red bot subtag: bot link: xur PARTICLE ELECTRIC: id: Chemistry Room/Panel_physics_6 - colors: purple + colors: + - purple + - red tag: purple mid red bot subtag: mid link: xpr ELECTRON ATOM (1): id: Chemistry Room/Panel_physics_3 - colors: red + colors: + - purple + - red tag: purple mid red bot subtag: bot link: xpr ELECTRON NEUTRAL: id: Chemistry Room/Panel_physics_7 - colors: purple + colors: + - purple + - red tag: purple mid red bot subtag: mid link: xpr NEUTRON ATOM (2): id: Chemistry Room/Panel_physics_4 - colors: red + colors: + - purple + - red tag: purple mid red bot subtag: bot link: xpr NEUTRON PROPEL: id: Chemistry Room/Panel_physics_8 - colors: purple + colors: + - purple + - red tag: purple mid red bot subtag: mid link: xpr PROTON ATOM (3): id: Chemistry Room/Panel_physics_5 - colors: red + colors: + - purple + - red tag: purple mid red bot subtag: bot link: xpr PROTON diff --git a/worlds/lingo/data/ids.yaml b/worlds/lingo/data/ids.yaml index 1a1ceca24a..3239f21854 100644 --- a/worlds/lingo/data/ids.yaml +++ b/worlds/lingo/data/ids.yaml @@ -1064,6 +1064,9 @@ doors: Hallway Door: item: 444459 location: 445214 + Lookout Entrance: + item: 444579 + location: 445271 Dread Hallway: Tenacious Entrance: item: 444462 @@ -1402,7 +1405,7 @@ doors: item: 444570 location: 445266 Room Room: - Shortcut to Fifth Floor: + Cellar Exit: item: 444571 location: 445076 Outside The Wise: diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index a0b33d1dbe..b046f1cfe3 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -190,6 +190,25 @@ class LingoPlayerLogic: if item.should_include(world): self.real_items.append(name) + # Calculate the requirements for the fake pilgrimage. + fake_pilgrimage = [ + ["Second Room", "Exit Door"], ["Crossroads", "Tower Entrance"], + ["Orange Tower Fourth Floor", "Hot Crusts Door"], ["Outside The Initiated", "Shortcut to Hub Room"], + ["Orange Tower First Floor", "Shortcut to Hub Room"], ["Directional Gallery", "Shortcut to The Undeterred"], + ["Orange Tower First Floor", "Salt Pepper Door"], ["Hub Room", "Crossroads Entrance"], + ["Champion's Rest", "Shortcut to The Steady"], ["The Bearer", "Shortcut to The Bold"], + ["Art Gallery", "Exit"], ["The Tenacious", "Shortcut to Hub Room"], + ["Outside The Agreeable", "Tenacious Entrance"] + ] + pilgrimage_reqs = AccessRequirements() + for door in fake_pilgrimage: + door_object = DOORS_BY_ROOM[door[0]][door[1]] + if door_object.event or world.options.shuffle_doors == ShuffleDoors.option_none: + pilgrimage_reqs.merge(self.calculate_door_requirements(door[0], door[1], world)) + else: + pilgrimage_reqs.doors.add(RoomAndDoor(door[0], door[1])) + self.door_reqs.setdefault("Pilgrim Antechamber", {})["Pilgrimage"] = pilgrimage_reqs + # Create the paintings mapping, if painting shuffle is on. if painting_shuffle: # Shuffle paintings until we get something workable. @@ -369,11 +388,9 @@ class LingoPlayerLogic: door_object = DOORS_BY_ROOM[room][door] for req_panel in door_object.panels: - if req_panel.room is not None and req_panel.room != room: - access_reqs.rooms.add(req_panel.room) - - sub_access_reqs = self.calculate_panel_requirements(room if req_panel.room is None else req_panel.room, - req_panel.panel, world) + panel_room = room if req_panel.room is None else req_panel.room + access_reqs.rooms.add(panel_room) + sub_access_reqs = self.calculate_panel_requirements(panel_room, req_panel.panel, world) access_reqs.merge(sub_access_reqs) self.door_reqs[room][door] = access_reqs @@ -397,8 +414,8 @@ class LingoPlayerLogic: unhindered_panels_by_color: dict[Optional[str], int] = {} for panel_name, panel_data in room_data.items(): - # We won't count non-counting panels. - if panel_data.non_counting: + # We won't count non-counting panels. THE MASTER has special access rules and is handled separately. + if panel_data.non_counting or panel_name == "THE MASTER": continue # We won't coalesce any panels that have requirements beyond colors. To simplify things for now, we will diff --git a/worlds/lingo/regions.py b/worlds/lingo/regions.py index c24144a160..bdc42f42f5 100644 --- a/worlds/lingo/regions.py +++ b/worlds/lingo/regions.py @@ -4,7 +4,7 @@ from BaseClasses import Entrance, ItemClassification, Region from .items import LingoItem from .locations import LingoLocation from .player_logic import LingoPlayerLogic -from .rules import lingo_can_use_entrance, lingo_can_use_pilgrimage, make_location_lambda +from .rules import lingo_can_use_entrance, make_location_lambda from .static_logic import ALL_ROOMS, PAINTINGS, Room, RoomAndDoor if TYPE_CHECKING: @@ -25,15 +25,6 @@ def create_region(room: Room, world: "LingoWorld", player_logic: LingoPlayerLogi return new_region -def handle_pilgrim_room(regions: Dict[str, Region], world: "LingoWorld", player_logic: LingoPlayerLogic) -> None: - target_region = regions["Pilgrim Antechamber"] - source_region = regions["Outside The Agreeable"] - source_region.connect( - target_region, - "Pilgrimage", - lambda state: lingo_can_use_pilgrimage(state, world, player_logic)) - - def connect_entrance(regions: Dict[str, Region], source_region: Region, target_region: Region, description: str, door: Optional[RoomAndDoor], world: "LingoWorld", player_logic: LingoPlayerLogic): connection = Entrance(world.player, description, source_region) @@ -91,7 +82,9 @@ def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None: connect_entrance(regions, regions[entrance.room], regions[room.name], entrance_name, entrance.door, world, player_logic) - handle_pilgrim_room(regions, world, player_logic) + # Add the fake pilgrimage. + connect_entrance(regions, regions["Outside The Agreeable"], regions["Pilgrim Antechamber"], "Pilgrimage", + RoomAndDoor("Pilgrim Antechamber", "Pilgrimage"), world, player_logic) if early_color_hallways: regions["Starting Room"].connect(regions["Outside The Undeterred"], "Early Color Hallways") diff --git a/worlds/lingo/rules.py b/worlds/lingo/rules.py index ee9dcc4192..481fab18b5 100644 --- a/worlds/lingo/rules.py +++ b/worlds/lingo/rules.py @@ -17,23 +17,6 @@ def lingo_can_use_entrance(state: CollectionState, room: str, door: RoomAndDoor, return _lingo_can_open_door(state, effective_room, door.door, world, player_logic) -def lingo_can_use_pilgrimage(state: CollectionState, world: "LingoWorld", player_logic: LingoPlayerLogic): - fake_pilgrimage = [ - ["Second Room", "Exit Door"], ["Crossroads", "Tower Entrance"], - ["Orange Tower Fourth Floor", "Hot Crusts Door"], ["Outside The Initiated", "Shortcut to Hub Room"], - ["Orange Tower First Floor", "Shortcut to Hub Room"], ["Directional Gallery", "Shortcut to The Undeterred"], - ["Orange Tower First Floor", "Salt Pepper Door"], ["Hub Room", "Crossroads Entrance"], - ["Champion's Rest", "Shortcut to The Steady"], ["The Bearer", "Shortcut to The Bold"], - ["Art Gallery", "Exit"], ["The Tenacious", "Shortcut to Hub Room"], - ["Outside The Agreeable", "Tenacious Entrance"] - ] - for entrance in fake_pilgrimage: - if not _lingo_can_open_door(state, entrance[0], entrance[1], world, player_logic): - return False - - return True - - def lingo_can_use_location(state: CollectionState, location: PlayerLocation, world: "LingoWorld", player_logic: LingoPlayerLogic): return _lingo_can_satisfy_requirements(state, location.access, world, player_logic) @@ -56,6 +39,12 @@ def lingo_can_use_level_2_location(state: CollectionState, world: "LingoWorld", counted_panels += panel_count if counted_panels >= world.options.level_2_requirement.value - 1: return True + # THE MASTER has to be handled separately, because it has special access rules. + if state.can_reach("Orange Tower Seventh Floor", "Region", world.player)\ + and lingo_can_use_mastery_location(state, world, player_logic): + counted_panels += 1 + if counted_panels >= world.options.level_2_requirement.value - 1: + return True return False diff --git a/worlds/lingo/utils/validate_config.rb b/worlds/lingo/utils/validate_config.rb index bed5188e31..3ac49dc220 100644 --- a/worlds/lingo/utils/validate_config.rb +++ b/worlds/lingo/utils/validate_config.rb @@ -40,7 +40,7 @@ mentioned_panels = Set[] door_groups = {} directives = Set["entrances", "panels", "doors", "paintings", "progression"] -panel_directives = Set["id", "required_room", "required_door", "required_panel", "colors", "check", "exclude_reduce", "tag", "link", "subtag", "achievement", "copy_to_sign", "non_counting"] +panel_directives = Set["id", "required_room", "required_door", "required_panel", "colors", "check", "exclude_reduce", "tag", "link", "subtag", "achievement", "copy_to_sign", "non_counting", "hunt"] door_directives = Set["id", "painting_id", "panels", "item_name", "location_name", "skip_location", "skip_item", "group", "include_reduce", "junk_item", "event"] painting_directives = Set["id", "enter_only", "exit_only", "orientation", "required_door", "required", "required_when_no_doors", "move", "req_blocked", "req_blocked_when_no_doors"] diff --git a/worlds/pokemon_rb/basepatch_blue.bsdiff4 b/worlds/pokemon_rb/basepatch_blue.bsdiff4 index bee5a8d2f4..5ccf4e9bba 100644 Binary files a/worlds/pokemon_rb/basepatch_blue.bsdiff4 and b/worlds/pokemon_rb/basepatch_blue.bsdiff4 differ diff --git a/worlds/pokemon_rb/basepatch_red.bsdiff4 b/worlds/pokemon_rb/basepatch_red.bsdiff4 index f2db54a84f..26d2eb0c28 100644 Binary files a/worlds/pokemon_rb/basepatch_red.bsdiff4 and b/worlds/pokemon_rb/basepatch_red.bsdiff4 differ diff --git a/worlds/pokemon_rb/client.py b/worlds/pokemon_rb/client.py index fb29045cf4..7424cc8ddf 100644 --- a/worlds/pokemon_rb/client.py +++ b/worlds/pokemon_rb/client.py @@ -6,7 +6,7 @@ from NetUtils import ClientStatus from worlds._bizhawk.client import BizHawkClient from worlds._bizhawk import read, write, guarded_write -from worlds.pokemon_rb.locations import location_data +from .locations import location_data logger = logging.getLogger("Client") diff --git a/worlds/pokemon_rb/rom_addresses.py b/worlds/pokemon_rb/rom_addresses.py index cd57e317bd..ffb89a4dfc 100644 --- a/worlds/pokemon_rb/rom_addresses.py +++ b/worlds/pokemon_rb/rom_addresses.py @@ -168,12 +168,12 @@ rom_addresses = { "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_0_ITEM": 0x1a61c, "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_1_ITEM": 0x1a62a, "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_2_ITEM": 0x1a638, - "Event_SKC6F": 0x1a666, - "Warps_SilphCo6F": 0x1a741, - "Missable_Silph_Co_6F_Item_1": 0x1a791, - "Missable_Silph_Co_6F_Item_2": 0x1a798, - "Path_Pallet_Oak": 0x1a91e, - "Path_Pallet_Player": 0x1a92b, + "Event_SKC6F": 0x1a659, + "Warps_SilphCo6F": 0x1a737, + "Missable_Silph_Co_6F_Item_1": 0x1a787, + "Missable_Silph_Co_6F_Item_2": 0x1a78e, + "Path_Pallet_Oak": 0x1a914, + "Path_Pallet_Player": 0x1a921, "Warps_CinnabarIsland": 0x1c026, "Warps_Route1": 0x1c0e9, "Option_Extra_Key_Items_B": 0x1ca46, diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index 8735ce81fd..6574a176dc 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -103,12 +103,10 @@ class RiskOfRainWorld(World): if self.options.dlc_sotv: environment_offset_table = shift_by_offset(environment_sotv_table, environment_offset) environments_pool = {**environments_pool, **environment_offset_table} - environments_to_precollect = 5 if self.options.begin_with_loop else 1 - # percollect environments for each stage (or just stage 1) - for i in range(environments_to_precollect): - unlock = self.random.choices(list(environment_available_orderedstages_table[i].keys()), k=1) - self.multiworld.push_precollected(self.create_item(unlock[0])) - environments_pool.pop(unlock[0]) + # percollect starting environment for stage 1 + unlock = self.random.choices(list(environment_available_orderedstages_table[0].keys()), k=1) + self.multiworld.push_precollected(self.create_item(unlock[0])) + environments_pool.pop(unlock[0]) # Generate item pool itempool: List[str] = ["Beads of Fealty", "Radar Scanner"] diff --git a/worlds/ror2/options.py b/worlds/ror2/options.py index 7daf8a8446..abb8e91da2 100644 --- a/worlds/ror2/options.py +++ b/worlds/ror2/options.py @@ -142,14 +142,6 @@ class FinalStageDeath(Toggle): display_name = "Final Stage Death is Win" -class BeginWithLoop(Toggle): - """ - Enable to precollect a full loop of environments. - Only has an effect with Explore Mode. - """ - display_name = "Begin With Loop" - - class DLC_SOTV(Toggle): """ Enable if you are using SOTV DLC. @@ -385,7 +377,6 @@ class ROR2Options(PerGameCommonOptions): total_revivals: TotalRevivals start_with_revive: StartWithRevive final_stage_death: FinalStageDeath - begin_with_loop: BeginWithLoop dlc_sotv: DLC_SOTV death_link: DeathLink item_pickup_step: ItemPickupStep diff --git a/worlds/sa2b/Rules.py b/worlds/sa2b/Rules.py index 6b7ad69cd1..afd0dcb182 100644 --- a/worlds/sa2b/Rules.py +++ b/worlds/sa2b/Rules.py @@ -638,7 +638,7 @@ def set_mission_upgrade_rules_standard(multiworld: MultiWorld, world: World, pla add_rule(multiworld.get_location(LocationName.radical_highway_omo_2, player), lambda state: state.has(ItemName.shadow_air_shoes, player)) add_rule(multiworld.get_location(LocationName.weapons_bed_omo_2, player), - lambda state: state.has(ItemName.eggman_jet_engine, player)) + lambda state: state.has(ItemName.eggman_large_cannon, player)) add_rule(multiworld.get_location(LocationName.mission_street_omo_3, player), lambda state: state.has(ItemName.tails_booster, player)) diff --git a/worlds/sm/variaRandomizer/rando/GraphBuilder.py b/worlds/sm/variaRandomizer/rando/GraphBuilder.py index 7bee33ec82..88b539e7f0 100644 --- a/worlds/sm/variaRandomizer/rando/GraphBuilder.py +++ b/worlds/sm/variaRandomizer/rando/GraphBuilder.py @@ -170,7 +170,8 @@ class GraphBuilder(object): ap = "Landing Site" # dummy value it'll be overwritten at first collection while len(itemLocs) > 0 and not (sm.canPassG4() and graph.canAccess(sm, ap, "Landing Site", maxDiff)): il = itemLocs.pop(0) - if il.Location.restricted or il.Item.Type == "ArchipelagoItem": + # can happen with item links replacement items that its not in the container's itemPool + if il.Location.restricted or il.Item.Type == "ArchipelagoItem" or il.Item not in container.itemPool: continue self.log.debug("collecting " + getItemLocStr(il)) container.collect(il) diff --git a/worlds/sm64ex/Regions.py b/worlds/sm64ex/Regions.py index d0e767e7ec..d426804c30 100644 --- a/worlds/sm64ex/Regions.py +++ b/worlds/sm64ex/Regions.py @@ -1,4 +1,7 @@ import typing + +from enum import Enum + from BaseClasses import MultiWorld, Region, Entrance, Location from .Locations import SM64Location, location_table, locBoB_table, locWhomp_table, locJRB_table, locCCM_table, \ locBBH_table, \ @@ -7,36 +10,63 @@ from .Locations import SM64Location, location_table, locBoB_table, locWhomp_tabl locPSS_table, locSA_table, locBitDW_table, locTotWC_table, locCotMC_table, \ locVCutM_table, locBitFS_table, locWMotR_table, locBitS_table, locSS_table -# sm64paintings is dict of entrances, format LEVEL | AREA -sm64_level_to_paintings = { - 91: "Bob-omb Battlefield", - 241: "Whomp's Fortress", - 121: "Jolly Roger Bay", - 51: "Cool, Cool Mountain", - 41: "Big Boo's Haunt", - 71: "Hazy Maze Cave", - 221: "Lethal Lava Land", - 81: "Shifting Sand Land", - 231: "Dire, Dire Docks", - 101: "Snowman's Land", - 111: "Wet-Dry World", - 361: "Tall, Tall Mountain", - 132: "Tiny-Huge Island (Tiny)", - 131: "Tiny-Huge Island (Huge)", - 141: "Tick Tock Clock", - 151: "Rainbow Ride" +class SM64Levels(int, Enum): + BOB_OMB_BATTLEFIELD = 91 + WHOMPS_FORTRESS = 241 + JOLLY_ROGER_BAY = 121 + COOL_COOL_MOUNTAIN = 51 + BIG_BOOS_HAUNT = 41 + HAZY_MAZE_CAVE = 71 + LETHAL_LAVA_LAND = 221 + SHIFTING_SAND_LAND = 81 + DIRE_DIRE_DOCKS = 231 + SNOWMANS_LAND = 101 + WET_DRY_WORLD = 111 + TALL_TALL_MOUNTAIN = 361 + TINY_HUGE_ISLAND_TINY = 132 + TINY_HUGE_ISLAND_HUGE = 131 + TICK_TOCK_CLOCK = 141 + RAINBOW_RIDE = 151 + THE_PRINCESS_SECRET_SLIDE = 271 + THE_SECRET_AQUARIUM = 201 + BOWSER_IN_THE_DARK_WORLD = 171 + TOWER_OF_THE_WING_CAP = 291 + CAVERN_OF_THE_METAL_CAP = 281 + VANISH_CAP_UNDER_THE_MOAT = 181 + BOWSER_IN_THE_FIRE_SEA = 191 + WING_MARIO_OVER_THE_RAINBOW = 311 + +# sm64paintings is a dict of entrances, format LEVEL | AREA +sm64_level_to_paintings: typing.Dict[SM64Levels, str] = { + SM64Levels.BOB_OMB_BATTLEFIELD: "Bob-omb Battlefield", + SM64Levels.WHOMPS_FORTRESS: "Whomp's Fortress", + SM64Levels.JOLLY_ROGER_BAY: "Jolly Roger Bay", + SM64Levels.COOL_COOL_MOUNTAIN: "Cool, Cool Mountain", + SM64Levels.BIG_BOOS_HAUNT: "Big Boo's Haunt", + SM64Levels.HAZY_MAZE_CAVE: "Hazy Maze Cave", + SM64Levels.LETHAL_LAVA_LAND: "Lethal Lava Land", + SM64Levels.SHIFTING_SAND_LAND: "Shifting Sand Land", + SM64Levels.DIRE_DIRE_DOCKS: "Dire, Dire Docks", + SM64Levels.SNOWMANS_LAND: "Snowman's Land", + SM64Levels.WET_DRY_WORLD: "Wet-Dry World", + SM64Levels.TALL_TALL_MOUNTAIN: "Tall, Tall Mountain", + SM64Levels.TINY_HUGE_ISLAND_TINY: "Tiny-Huge Island (Tiny)", + SM64Levels.TINY_HUGE_ISLAND_HUGE: "Tiny-Huge Island (Huge)", + SM64Levels.TICK_TOCK_CLOCK: "Tick Tock Clock", + SM64Levels.RAINBOW_RIDE: "Rainbow Ride" } sm64_paintings_to_level = { painting: level for (level,painting) in sm64_level_to_paintings.items() } -# sm64secrets is list of secret areas, same format -sm64_level_to_secrets = { - 271: "The Princess's Secret Slide", - 201: "The Secret Aquarium", - 171: "Bowser in the Dark World", - 291: "Tower of the Wing Cap", - 281: "Cavern of the Metal Cap", - 181: "Vanish Cap under the Moat", - 191: "Bowser in the Fire Sea", - 311: "Wing Mario over the Rainbow" + +# sm64secrets is a dict of secret areas, same format as sm64paintings +sm64_level_to_secrets: typing.Dict[SM64Levels, str] = { + SM64Levels.THE_PRINCESS_SECRET_SLIDE: "The Princess's Secret Slide", + SM64Levels.THE_SECRET_AQUARIUM: "The Secret Aquarium", + SM64Levels.BOWSER_IN_THE_DARK_WORLD: "Bowser in the Dark World", + SM64Levels.TOWER_OF_THE_WING_CAP: "Tower of the Wing Cap", + SM64Levels.CAVERN_OF_THE_METAL_CAP: "Cavern of the Metal Cap", + SM64Levels.VANISH_CAP_UNDER_THE_MOAT: "Vanish Cap under the Moat", + SM64Levels.BOWSER_IN_THE_FIRE_SEA: "Bowser in the Fire Sea", + SM64Levels.WING_MARIO_OVER_THE_RAINBOW: "Wing Mario over the Rainbow" } sm64_secrets_to_level = { secret: level for (level,secret) in sm64_level_to_secrets.items() } diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py index d21ac30004..c428f85543 100644 --- a/worlds/sm64ex/Rules.py +++ b/worlds/sm64ex/Rules.py @@ -1,5 +1,5 @@ from ..generic.Rules import add_rule -from .Regions import connect_regions, sm64_level_to_paintings, sm64_paintings_to_level, sm64_level_to_secrets, sm64_entrances_to_level, sm64_level_to_entrances +from .Regions import connect_regions, SM64Levels, sm64_level_to_paintings, sm64_paintings_to_level, sm64_level_to_secrets, sm64_secrets_to_level, sm64_entrances_to_level, sm64_level_to_entrances def shuffle_dict_keys(world, obj: dict) -> dict: keys = list(obj.keys()) @@ -7,12 +7,16 @@ def shuffle_dict_keys(world, obj: dict) -> dict: world.random.shuffle(keys) return dict(zip(keys,values)) -def fix_reg(entrance_ids, entrance, destination, swapdict, world): - if entrance_ids[entrance] == destination: # Unlucky :C - rand = world.random.choice(swapdict.keys()) - entrance_ids[entrance], entrance_ids[swapdict[rand]] = rand, entrance_ids[entrance] - swapdict[rand] = entrance_ids[entrance] - swapdict.pop(entrance) +def fix_reg(entrance_map: dict, entrance: SM64Levels, invalid_regions: set, + swapdict: dict, world): + if entrance_map[entrance] in invalid_regions: # Unlucky :C + replacement_regions = [(rand_region, rand_entrance) for rand_region, rand_entrance in swapdict.items() + if rand_region not in invalid_regions] + rand_region, rand_entrance = world.random.choice(replacement_regions) + old_dest = entrance_map[entrance] + entrance_map[entrance], entrance_map[rand_entrance] = rand_region, old_dest + swapdict[rand_region] = entrance + swapdict.pop(entrance_map[entrance]) # Entrance now fixed to rand_region def set_rules(world, player: int, area_connections: dict): randomized_level_to_paintings = sm64_level_to_paintings.copy() @@ -24,22 +28,20 @@ def set_rules(world, player: int, area_connections: dict): randomized_entrances = { **randomized_level_to_paintings, **randomized_level_to_secrets } if world.AreaRandomizer[player].value == 3: # Randomize Courses and Secrets in one pool randomized_entrances = shuffle_dict_keys(world,randomized_entrances) + swapdict = { entrance: level for (level,entrance) in randomized_entrances.items() } # Guarantee first entrance is a course - swapdict = { entrance: level for (level,entrance) in randomized_entrances } - if randomized_entrances[91] not in sm64_paintings_to_level.keys(): # Unlucky :C (91 -> BoB Entrance) - rand = world.random.choice(sm64_paintings_to_level.values()) - randomized_entrances[91], randomized_entrances[swapdict[rand]] = rand, randomized_entrances[91] - swapdict[rand] = randomized_entrances[91] - swapdict.pop("Bob-omb Battlefield") - # Guarantee COTMC is not mapped to HMC, cuz thats impossible - fix_reg(randomized_entrances, "Cavern of the Metal Cap", "Hazy Maze Cave", swapdict, world) + fix_reg(randomized_entrances, SM64Levels.BOB_OMB_BATTLEFIELD, sm64_secrets_to_level.keys(), swapdict, world) # Guarantee BITFS is not mapped to DDD - fix_reg(randomized_entrances, "Bowser in the Fire Sea", "Dire, Dire Docks", swapdict, world) - if randomized_entrances[191] == "Hazy Maze Cave": # If BITFS is mapped to HMC... - fix_reg(randomized_entrances, "Cavern of the Metal Cap", "Dire, Dire Docks", swapdict, world) # ... then dont allow COTMC to be mapped to DDD + fix_reg(randomized_entrances, SM64Levels.BOWSER_IN_THE_FIRE_SEA, {"Dire, Dire Docks"}, swapdict, world) + # Guarantee COTMC is not mapped to HMC, cuz thats impossible. If BitFS -> HMC, also no COTMC -> DDD. + if randomized_entrances[SM64Levels.BOWSER_IN_THE_FIRE_SEA] == "Hazy Maze Cave": + fix_reg(randomized_entrances, SM64Levels.CAVERN_OF_THE_METAL_CAP, {"Hazy Maze Cave", "Dire, Dire Docks"}, swapdict, world) + else: + fix_reg(randomized_entrances, SM64Levels.CAVERN_OF_THE_METAL_CAP, {"Hazy Maze Cave"}, swapdict, world) # Destination Format: LVL | AREA with LVL = LEVEL_x, AREA = Area as used in sm64 code - area_connections.update({entrance_lvl: sm64_entrances_to_level[destination] for (entrance_lvl,destination) in randomized_entrances.items()}) + # Cast to int to not rely on availability of SM64Levels enum. Will cause crash in MultiServer otherwise + area_connections.update({int(entrance_lvl): int(sm64_entrances_to_level[destination]) for (entrance_lvl,destination) in randomized_entrances.items()}) randomized_entrances_s = {sm64_level_to_entrances[entrance_lvl]: destination for (entrance_lvl,destination) in randomized_entrances.items()} connect_regions(world, player, "Menu", randomized_entrances_s["Bob-omb Battlefield"]) diff --git a/worlds/smz3/TotalSMZ3/Patch.py b/worlds/smz3/TotalSMZ3/Patch.py index 049b200c46..c137442d9b 100644 --- a/worlds/smz3/TotalSMZ3/Patch.py +++ b/worlds/smz3/TotalSMZ3/Patch.py @@ -319,7 +319,7 @@ class Patch: def WriteZ3Locations(self, locations: List[Location]): for location in locations: if (location.Type == LocationType.HeraStandingKey): - self.patches.append((Snes(0x9E3BB), [0xE4] if location.APLocation.item.game == "SMZ3" and location.APLocation.item.item.Type == ItemType.KeyTH else [0xEB])) + self.patches.append((Snes(0x9E3BB), [0xEB])) elif (location.Type in [LocationType.Pedestal, LocationType.Ether, LocationType.Bombos]): text = Texts.ItemTextbox(location.APLocation.item.item if location.APLocation.item.game == "SMZ3" else Item(ItemType.Something)) if (location.Type == LocationType.Pedestal): diff --git a/worlds/tloz/ItemPool.py b/worlds/tloz/ItemPool.py index 1d33336172..7773accd8d 100644 --- a/worlds/tloz/ItemPool.py +++ b/worlds/tloz/ItemPool.py @@ -117,6 +117,9 @@ def get_pool_core(world): else: possible_level_locations = [location for location in standard_level_locations if location not in level_locations[8]] + for location in placed_items.keys(): + if location in possible_level_locations: + possible_level_locations.remove(location) for level in range(1, 9): if world.multiworld.TriforceLocations[world.player] == TriforceLocations.option_vanilla: placed_items[f"Level {level} Triforce"] = fragment diff --git a/worlds/witness/WitnessLogicVanilla.txt b/worlds/witness/WitnessLogicVanilla.txt index 8591a30d1f..779ead6bde 100644 --- a/worlds/witness/WitnessLogicVanilla.txt +++ b/worlds/witness/WitnessLogicVanilla.txt @@ -430,7 +430,7 @@ Door - 0x04F8F (Tower Shortcut) - 0x0361B 158705 - 0x03317 (Laser Panel Pressure Plates) - 0x033EA & 0x01BE9 & 0x01CD3 & 0x01D3F - Dots & Shapers & Black/White Squares & Rotated Shapers Laser - 0x014BB (Laser) - 0x0360E | 0x03317 159240 - 0x033BE (Pressure Plates 1 EP) - 0x033EA - True -159241 - 0x033BF (Pressure Plates 2 EP) - 0x01BE9 - True +159241 - 0x033BF (Pressure Plates 2 EP) - 0x01BE9 & 0x01BEA - True 159242 - 0x033DD (Pressure Plates 3 EP) - 0x01CD3 & 0x01CD5 - True 159243 - 0x033E5 (Pressure Plates 4 Left Exit EP) - 0x01D3F - True 159244 - 0x018B6 (Pressure Plates 4 Right Exit EP) - 0x01D3F - True diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index 1e54ec352c..d238aa4adf 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -161,7 +161,7 @@ joke_hints = [ ] -def get_always_hint_items(world: "WitnessWorld"): +def get_always_hint_items(world: "WitnessWorld") -> List[str]: always = [ "Boat", "Caves Shortcuts", @@ -187,17 +187,17 @@ def get_always_hint_items(world: "WitnessWorld"): return always -def get_always_hint_locations(_: "WitnessWorld"): - return { +def get_always_hint_locations(_: "WitnessWorld") -> List[str]: + return [ "Challenge Vault Box", "Mountain Bottom Floor Discard", "Theater Eclipse EP", "Shipwreck Couch EP", "Mountainside Cloud Cycle EP", - } + ] -def get_priority_hint_items(world: "WitnessWorld"): +def get_priority_hint_items(world: "WitnessWorld") -> List[str]: priority = { "Caves Mountain Shortcut (Door)", "Caves Swamp Shortcut (Door)", @@ -246,11 +246,11 @@ def get_priority_hint_items(world: "WitnessWorld"): lasers.append("Desert Laser") priority.update(world.random.sample(lasers, 6)) - return priority + return sorted(priority) -def get_priority_hint_locations(_: "WitnessWorld"): - return { +def get_priority_hint_locations(_: "WitnessWorld") -> List[str]: + return [ "Swamp Purple Underwater", "Shipwreck Vault Box", "Town RGB Room Left", @@ -264,7 +264,7 @@ def get_priority_hint_locations(_: "WitnessWorld"): "Tunnels Theater Flowers EP", "Boat Shipwreck Green EP", "Quarry Stoneworks Control Room Left", - } + ] def make_hint_from_item(world: "WitnessWorld", item_name: str, own_itempool: List[Item]): @@ -365,8 +365,8 @@ def make_hints(world: "WitnessWorld", hint_amount: int, own_itempool: List[Item] remaining_hints = hint_amount - len(hints) priority_hint_amount = int(max(0.0, min(len(priority_hint_pairs) / 2, remaining_hints / 2))) - prog_items_in_this_world = sorted(list(prog_items_in_this_world)) - locations_in_this_world = sorted(list(loc_in_this_world)) + prog_items_in_this_world = sorted(prog_items_in_this_world) + locations_in_this_world = sorted(loc_in_this_world) world.random.shuffle(prog_items_in_this_world) world.random.shuffle(locations_in_this_world) diff --git a/worlds/witness/items.py b/worlds/witness/items.py index 15c693b25d..a8c889de93 100644 --- a/worlds/witness/items.py +++ b/worlds/witness/items.py @@ -115,6 +115,7 @@ class WitnessPlayerItems: # Adjust item classifications based on game settings. eps_shuffled = self._world.options.shuffle_EPs come_to_you = self._world.options.elevators_come_to_you + difficulty = self._world.options.puzzle_randomization for item_name, item_data in self.item_data.items(): if not eps_shuffled and item_name in {"Monastery Garden Entry (Door)", "Monastery Shortcuts", @@ -130,10 +131,12 @@ class WitnessPlayerItems: "Monastery Laser Shortcut (Door)", "Orchard Second Gate (Door)", "Jungle Bamboo Laser Shortcut (Door)", - "Keep Pressure Plates 2 Exit (Door)", "Caves Elevator Controls (Panel)"}: # Downgrade doors that don't gate progress. item_data.classification = ItemClassification.useful + elif item_name == "Keep Pressure Plates 2 Exit (Door)" and not (difficulty == "none" and eps_shuffled): + # PP2EP requires the door in vanilla puzzles, otherwise it's unnecessary + item_data.classification = ItemClassification.useful # Build the mandatory item list. self._mandatory_items: Dict[str, int] = {}