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] = {}