From 0de09cd794e300e14fa17ceea0e59d7f8949f875 Mon Sep 17 00:00:00 2001
From: Mysteryem
Date: Sat, 21 Feb 2026 14:16:57 +0000
Subject: [PATCH 01/84] Core: Better scaling explicit indirect conditions
(#4582)
* Core: Better scaling explicit indirect conditions
When the number of connections to retry was large and `queue` was large
`new_entrance not in queue` would get slow.
For the average supported world, the difference this change makes is
negligible.
For a game like Blasphemous, with a lot of explicit indirect conditions,
generation of 10 template Blasphemous yamls with
`--skip_output --seed 1` and progression balancing disabled went from
19.0s to 17.9s (5.9% reduction in generation duration).
* Create a new variable for the new set created from the intersection
---
BaseClasses.py | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/BaseClasses.py b/BaseClasses.py
index d1b9b5f6d3f9..ccb8e0677f12 100644
--- a/BaseClasses.py
+++ b/BaseClasses.py
@@ -788,9 +788,11 @@ def _update_reachable_regions_explicit_indirect_conditions(self, player: int, qu
self.multiworld.worlds[player].reached_region(self, new_region)
# Retry connections if the new region can unblock them
- for new_entrance in self.multiworld.indirect_connections.get(new_region, set()):
- if new_entrance in blocked_connections and new_entrance not in queue:
- queue.append(new_entrance)
+ entrances = self.multiworld.indirect_connections.get(new_region)
+ if entrances is not None:
+ relevant_entrances = entrances.intersection(blocked_connections)
+ relevant_entrances.difference_update(queue)
+ queue.extend(relevant_entrances)
def _update_reachable_regions_auto_indirect_conditions(self, player: int, queue: deque[Entrance]):
reachable_regions = self.reachable_regions[player]
From d83da1b8180cf0992042d3457efb5aacaacfc74c Mon Sep 17 00:00:00 2001
From: Fabian Dill
Date: Sun, 22 Feb 2026 21:22:22 +0100
Subject: [PATCH 02/84] WebHost: memory leak fixes (#5966)
---
WebHostLib/customserver.py | 46 +++++++++++++++++++++++++---------
test/webhost/test_host_room.py | 37 ++++++++++++++++++++++++++-
2 files changed, 70 insertions(+), 13 deletions(-)
diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py
index 7248bf3bacc6..e353cf2ab2d4 100644
--- a/WebHostLib/customserver.py
+++ b/WebHostLib/customserver.py
@@ -89,19 +89,24 @@ def _load_game_data(self):
setattr(self, key, value)
self.non_hintable_names = collections.defaultdict(frozenset, self.non_hintable_names)
- def listen_to_db_commands(self):
+ async def listen_to_db_commands(self):
cmdprocessor = DBCommandProcessor(self)
while not self.exit_event.is_set():
- with db_session:
- commands = select(command for command in Command if command.room.id == self.room_id)
- if commands:
- for command in commands:
- self.main_loop.call_soon_threadsafe(cmdprocessor, command.commandtext)
- command.delete()
- commit()
- del commands
- time.sleep(5)
+ await self.main_loop.run_in_executor(None, self._process_db_commands, cmdprocessor)
+ try:
+ await asyncio.wait_for(self.exit_event.wait(), 5)
+ except asyncio.TimeoutError:
+ pass
+
+ def _process_db_commands(self, cmdprocessor):
+ with db_session:
+ commands = select(command for command in Command if command.room.id == self.room_id)
+ if commands:
+ for command in commands:
+ self.main_loop.call_soon_threadsafe(cmdprocessor, command.commandtext)
+ command.delete()
+ commit()
@db_session
def load(self, room_id: int):
@@ -156,9 +161,9 @@ def init_save(self, enabled: bool = True):
with db_session:
savegame_data = Room.get(id=self.room_id).multisave
if savegame_data:
- self.set_save(restricted_loads(Room.get(id=self.room_id).multisave))
+ self.set_save(restricted_loads(savegame_data))
self._start_async_saving(atexit_save=False)
- threading.Thread(target=self.listen_to_db_commands, daemon=True).start()
+ asyncio.create_task(self.listen_to_db_commands())
@db_session
def _save(self, exit_save: bool = False) -> bool:
@@ -229,6 +234,17 @@ def set_up_logging(room_id) -> logging.Logger:
return logger
+def tear_down_logging(room_id):
+ """Close logging handling for a room."""
+ logger_name = f"RoomLogger {room_id}"
+ if logger_name in logging.Logger.manager.loggerDict:
+ logger = logging.getLogger(logger_name)
+ for handler in logger.handlers[:]:
+ logger.removeHandler(handler)
+ handler.close()
+ del logging.Logger.manager.loggerDict[logger_name]
+
+
def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str],
host: str, rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue):
@@ -343,12 +359,18 @@ async def start_room(room_id):
ctx.save_dirty = False # make sure the saving thread does not write to DB after final wakeup
ctx.exit_event.set() # make sure the saving thread stops at some point
# NOTE: async saving should probably be an async task and could be merged with shutdown_task
+
+ if ctx.server and hasattr(ctx.server, "ws_server"):
+ ctx.server.ws_server.close()
+ await ctx.server.ws_server.wait_closed()
+
with db_session:
# ensure the Room does not spin up again on its own, minute of safety buffer
room = Room.get(id=room_id)
room.last_activity = datetime.datetime.utcnow() - \
datetime.timedelta(minutes=1, seconds=room.timeout)
del room
+ tear_down_logging(room_id)
logging.info(f"Shutting down room {room_id} on {name}.")
finally:
await asyncio.sleep(5)
diff --git a/test/webhost/test_host_room.py b/test/webhost/test_host_room.py
index 4aa83e3b1c6c..0f43fea208b8 100644
--- a/test/webhost/test_host_room.py
+++ b/test/webhost/test_host_room.py
@@ -1,11 +1,22 @@
+import logging
import os
from uuid import UUID, uuid4, uuid5
from flask import url_for
+from WebHostLib.customserver import set_up_logging, tear_down_logging
from . import TestBase
+def _cleanup_logger(room_id: UUID) -> None:
+ from Utils import user_path
+ tear_down_logging(room_id)
+ try:
+ os.unlink(user_path("logs", f"{room_id}.txt"))
+ except OSError:
+ pass
+
+
class TestHostFakeRoom(TestBase):
room_id: UUID
log_filename: str
@@ -39,7 +50,7 @@ def tearDown(self) -> None:
try:
os.unlink(self.log_filename)
- except FileNotFoundError:
+ except OSError:
pass
def test_display_log_missing_full(self) -> None:
@@ -191,3 +202,27 @@ def test_host_room_other_post(self) -> None:
with db_session:
commands = select(command for command in Command if command.room.id == self.room_id) # type: ignore
self.assertNotIn("/help", (command.commandtext for command in commands))
+
+ def test_logger_teardown(self) -> None:
+ """Verify that room loggers are removed from the global logging manager."""
+ from WebHostLib.customserver import tear_down_logging
+ room_id = uuid4()
+ self.addCleanup(_cleanup_logger, room_id)
+ set_up_logging(room_id)
+ self.assertIn(f"RoomLogger {room_id}", logging.Logger.manager.loggerDict)
+ tear_down_logging(room_id)
+ self.assertNotIn(f"RoomLogger {room_id}", logging.Logger.manager.loggerDict)
+
+ def test_handler_teardown(self) -> None:
+ """Verify that handlers for room loggers are closed by tear_down_logging."""
+ from WebHostLib.customserver import tear_down_logging
+ room_id = uuid4()
+ self.addCleanup(_cleanup_logger, room_id)
+ logger = set_up_logging(room_id)
+ handlers = logger.handlers[:]
+ self.assertGreater(len(handlers), 0)
+
+ tear_down_logging(room_id)
+ for handler in handlers:
+ if isinstance(handler, logging.FileHandler):
+ self.assertTrue(handler.stream is None or handler.stream.closed)
From fefd790de6731576f2533bdf37763345e6356bb0 Mon Sep 17 00:00:00 2001
From: Silvris <58583688+Silvris@users.noreply.github.com>
Date: Tue, 24 Feb 2026 11:43:42 -0600
Subject: [PATCH 03/84] ALTTP: remove `world: MultiWorld` and typing (#5974)
---
worlds/alttp/EntranceShuffle.py | 1038 +++++++++++++------------
worlds/alttp/InvertedRegions.py | 493 ++++++------
worlds/alttp/ItemPool.py | 80 +-
worlds/alttp/Items.py | 14 +-
worlds/alttp/Options.py | 10 +-
worlds/alttp/OverworldGlitchRules.py | 71 +-
worlds/alttp/Regions.py | 508 ++++++------
worlds/alttp/Rom.py | 284 +++----
worlds/alttp/Rules.py | 958 +++++++++++------------
worlds/alttp/Shops.py | 12 +-
worlds/alttp/UnderworldGlitchRules.py | 79 +-
11 files changed, 1786 insertions(+), 1761 deletions(-)
diff --git a/worlds/alttp/EntranceShuffle.py b/worlds/alttp/EntranceShuffle.py
index c062a17ea695..550e4878aabc 100644
--- a/worlds/alttp/EntranceShuffle.py
+++ b/worlds/alttp/EntranceShuffle.py
@@ -1,74 +1,75 @@
# ToDo: With shuffle_ganon option, prevent gtower from linking to an exit only location through a 2 entrance cave.
from collections import defaultdict
+from BaseClasses import MultiWorld
from .OverworldGlitchRules import overworld_glitch_connections
from .UnderworldGlitchRules import underworld_glitch_connections
from .Regions import mark_light_world_regions
from .InvertedRegions import mark_dark_world_regions
-def link_entrances(world, player):
- connect_two_way(world, 'Links House', 'Links House Exit', player) # unshuffled. For now
- connect_exit(world, 'Chris Houlihan Room Exit', 'Links House', player) # should always match link's house, except for plandos
+def link_entrances(multiworld: MultiWorld, player: int):
+ connect_two_way(multiworld, 'Links House', 'Links House Exit', player) # unshuffled. For now
+ connect_exit(multiworld, 'Chris Houlihan Room Exit', 'Links House', player) # should always match link's house, except for plandos
Dungeon_Exits = Dungeon_Exits_Base.copy()
Cave_Exits = Cave_Exits_Base.copy()
Old_Man_House = Old_Man_House_Base.copy()
Cave_Three_Exits = Cave_Three_Exits_Base.copy()
- unbias_some_entrances(world, Dungeon_Exits, Cave_Exits, Old_Man_House, Cave_Three_Exits)
+ unbias_some_entrances(multiworld, Dungeon_Exits, Cave_Exits, Old_Man_House, Cave_Three_Exits)
# setup mandatory connections
for exitname, regionname in mandatory_connections:
- connect_simple(world, exitname, regionname, player)
+ connect_simple(multiworld, exitname, regionname, player)
# if we do not shuffle, set default connections
- if world.worlds[player].options.entrance_shuffle == 'vanilla':
+ if multiworld.worlds[player].options.entrance_shuffle == 'vanilla':
for exitname, regionname in default_connections:
- connect_simple(world, exitname, regionname, player)
+ connect_simple(multiworld, exitname, regionname, player)
for exitname, regionname in default_dungeon_connections:
- connect_simple(world, exitname, regionname, player)
- elif world.worlds[player].options.entrance_shuffle == 'dungeons_simple':
+ connect_simple(multiworld, exitname, regionname, player)
+ elif multiworld.worlds[player].options.entrance_shuffle == 'dungeons_simple':
for exitname, regionname in default_connections:
- connect_simple(world, exitname, regionname, player)
+ connect_simple(multiworld, exitname, regionname, player)
- simple_shuffle_dungeons(world, player)
- elif world.worlds[player].options.entrance_shuffle == 'dungeons_full':
+ simple_shuffle_dungeons(multiworld, player)
+ elif multiworld.worlds[player].options.entrance_shuffle == 'dungeons_full':
for exitname, regionname in default_connections:
- connect_simple(world, exitname, regionname, player)
+ connect_simple(multiworld, exitname, regionname, player)
- skull_woods_shuffle(world, player)
+ skull_woods_shuffle(multiworld, player)
dungeon_exits = list(Dungeon_Exits)
lw_entrances = list(LW_Dungeon_Entrances)
dw_entrances = list(DW_Dungeon_Entrances)
- if world.worlds[player].options.mode == 'standard':
+ if multiworld.worlds[player].options.mode == 'standard':
# must connect front of hyrule castle to do escape
- connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
+ connect_two_way(multiworld, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
else:
dungeon_exits.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'))
lw_entrances.append('Hyrule Castle Entrance (South)')
- if not world.shuffle_ganon:
- connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player)
+ if not multiworld.shuffle_ganon:
+ connect_two_way(multiworld, 'Ganons Tower', 'Ganons Tower Exit', player)
else:
dw_entrances.append('Ganons Tower')
dungeon_exits.append('Ganons Tower Exit')
- if world.worlds[player].options.mode == 'standard':
+ if multiworld.worlds[player].options.mode == 'standard':
# rest of hyrule castle must be in light world, so it has to be the one connected to east exit of desert
hyrule_castle_exits = [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')]
- connect_mandatory_exits(world, lw_entrances, hyrule_castle_exits, list(LW_Dungeon_Entrances_Must_Exit), player)
- connect_caves(world, lw_entrances, [], hyrule_castle_exits, player)
+ connect_mandatory_exits(multiworld, lw_entrances, hyrule_castle_exits, list(LW_Dungeon_Entrances_Must_Exit), player)
+ connect_caves(multiworld, lw_entrances, [], hyrule_castle_exits, player)
else:
- connect_mandatory_exits(world, lw_entrances, dungeon_exits, list(LW_Dungeon_Entrances_Must_Exit), player)
- connect_mandatory_exits(world, dw_entrances, dungeon_exits, list(DW_Dungeon_Entrances_Must_Exit), player)
- connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player)
- elif world.worlds[player].options.entrance_shuffle == 'dungeons_crossed':
- crossed_shuffle_dungeons(world, player)
- elif world.worlds[player].options.entrance_shuffle == 'simple':
- simple_shuffle_dungeons(world, player)
+ connect_mandatory_exits(multiworld, lw_entrances, dungeon_exits, list(LW_Dungeon_Entrances_Must_Exit), player)
+ connect_mandatory_exits(multiworld, dw_entrances, dungeon_exits, list(DW_Dungeon_Entrances_Must_Exit), player)
+ connect_caves(multiworld, lw_entrances, dw_entrances, dungeon_exits, player)
+ elif multiworld.worlds[player].options.entrance_shuffle == 'dungeons_crossed':
+ crossed_shuffle_dungeons(multiworld, player)
+ elif multiworld.worlds[player].options.entrance_shuffle == 'simple':
+ simple_shuffle_dungeons(multiworld, player)
old_man_entrances = list(Old_Man_Entrances)
caves = list(Cave_Exits)
@@ -82,64 +83,64 @@ def link_entrances(world, player):
# we shuffle all 2 entrance caves as pairs as a start
# start with the ones that need to be directed
two_door_caves = list(Two_Door_Caves_Directional)
- world.random.shuffle(two_door_caves)
- world.random.shuffle(caves)
+ multiworld.random.shuffle(two_door_caves)
+ multiworld.random.shuffle(caves)
while two_door_caves:
entrance1, entrance2 = two_door_caves.pop()
exit1, exit2 = caves.pop()
- connect_two_way(world, entrance1, exit1, player)
- connect_two_way(world, entrance2, exit2, player)
+ connect_two_way(multiworld, entrance1, exit1, player)
+ connect_two_way(multiworld, entrance2, exit2, player)
# now the remaining pairs
two_door_caves = list(Two_Door_Caves)
- world.random.shuffle(two_door_caves)
+ multiworld.random.shuffle(two_door_caves)
while two_door_caves:
entrance1, entrance2 = two_door_caves.pop()
exit1, exit2 = caves.pop()
- connect_two_way(world, entrance1, exit1, player)
- connect_two_way(world, entrance2, exit2, player)
+ connect_two_way(multiworld, entrance1, exit1, player)
+ connect_two_way(multiworld, entrance2, exit2, player)
# at this point only Light World death mountain entrances remain
# place old man, has limited options
remaining_entrances = ['Old Man Cave (West)', 'Old Man House (Bottom)', 'Death Mountain Return Cave (West)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'Paradox Cave (Top)',
'Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave (Top)', 'Spiral Cave', 'Spiral Cave (Bottom)']
- world.random.shuffle(old_man_entrances)
+ multiworld.random.shuffle(old_man_entrances)
old_man_exit = old_man_entrances.pop()
remaining_entrances.extend(old_man_entrances)
- world.random.shuffle(remaining_entrances)
+ multiworld.random.shuffle(remaining_entrances)
old_man_entrance = remaining_entrances.pop()
- connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player)
- connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player)
+ connect_two_way(multiworld, old_man_entrance, 'Old Man Cave Exit (West)', player)
+ connect_two_way(multiworld, old_man_exit, 'Old Man Cave Exit (East)', player)
# add old man house to ensure it is always somewhere on light death mountain
caves.extend(list(Old_Man_House))
caves.extend(list(three_exit_caves))
# connect rest
- connect_caves(world, remaining_entrances, [], caves, player)
+ connect_caves(multiworld, remaining_entrances, [], caves, player)
# scramble holes
- scramble_holes(world, player)
+ scramble_holes(multiworld, player)
# place blacksmith, has limited options
- world.random.shuffle(blacksmith_doors)
+ multiworld.random.shuffle(blacksmith_doors)
blacksmith_hut = blacksmith_doors.pop()
- connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player)
+ connect_entrance(multiworld, blacksmith_hut, 'Blacksmiths Hut', player)
bomb_shop_doors.extend(blacksmith_doors)
# place bomb shop, has limited options
- world.random.shuffle(bomb_shop_doors)
+ multiworld.random.shuffle(bomb_shop_doors)
bomb_shop = bomb_shop_doors.pop()
- connect_entrance(world, bomb_shop, 'Big Bomb Shop', player)
+ connect_entrance(multiworld, bomb_shop, 'Big Bomb Shop', player)
single_doors.extend(bomb_shop_doors)
# tavern back door cannot be shuffled yet
- connect_doors(world, ['Tavern North'], ['Tavern'], player)
+ connect_doors(multiworld, ['Tavern North'], ['Tavern'], player)
# place remaining doors
- connect_doors(world, single_doors, door_targets, player)
- elif world.worlds[player].options.entrance_shuffle == 'restricted':
- simple_shuffle_dungeons(world, player)
+ connect_doors(multiworld, single_doors, door_targets, player)
+ elif multiworld.worlds[player].options.entrance_shuffle == 'restricted':
+ simple_shuffle_dungeons(multiworld, player)
lw_entrances = list(LW_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances)
dw_entrances = list(DW_Entrances + DW_Single_Cave_Doors)
@@ -152,26 +153,26 @@ def link_entrances(world, player):
door_targets = list(Single_Cave_Targets)
# tavern back door cannot be shuffled yet
- connect_doors(world, ['Tavern North'], ['Tavern'], player)
+ connect_doors(multiworld, ['Tavern North'], ['Tavern'], player)
# in restricted, the only mandatory exits are in dark world
- connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player)
+ connect_mandatory_exits(multiworld, dw_entrances, caves, dw_must_exits, player)
# place old man, has limited options
# exit has to come from specific set of doors, the entrance is free to move about
old_man_entrances = [door for door in old_man_entrances if door in lw_entrances]
- world.random.shuffle(old_man_entrances)
+ multiworld.random.shuffle(old_man_entrances)
old_man_exit = old_man_entrances.pop()
- connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player)
+ connect_two_way(multiworld, old_man_exit, 'Old Man Cave Exit (East)', player)
lw_entrances.remove(old_man_exit)
# place blacksmith, has limited options
all_entrances = lw_entrances + dw_entrances
# cannot place it anywhere already taken (or that are otherwise not eligable for placement)
blacksmith_doors = [door for door in blacksmith_doors if door in all_entrances]
- world.random.shuffle(blacksmith_doors)
+ multiworld.random.shuffle(blacksmith_doors)
blacksmith_hut = blacksmith_doors.pop()
- connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player)
+ connect_entrance(multiworld, blacksmith_hut, 'Blacksmiths Hut', player)
if blacksmith_hut in lw_entrances:
lw_entrances.remove(blacksmith_hut)
if blacksmith_hut in dw_entrances:
@@ -182,36 +183,36 @@ def link_entrances(world, player):
all_entrances = lw_entrances + dw_entrances
# cannot place it anywhere already taken (or that are otherwise not eligable for placement)
bomb_shop_doors = [door for door in bomb_shop_doors if door in all_entrances]
- world.random.shuffle(bomb_shop_doors)
+ multiworld.random.shuffle(bomb_shop_doors)
bomb_shop = bomb_shop_doors.pop()
- connect_entrance(world, bomb_shop, 'Big Bomb Shop', player)
+ connect_entrance(multiworld, bomb_shop, 'Big Bomb Shop', player)
if bomb_shop in lw_entrances:
lw_entrances.remove(bomb_shop)
if bomb_shop in dw_entrances:
dw_entrances.remove(bomb_shop)
# place the old man cave's entrance somewhere in the light world
- world.random.shuffle(lw_entrances)
+ multiworld.random.shuffle(lw_entrances)
old_man_entrance = lw_entrances.pop()
- connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player)
+ connect_two_way(multiworld, old_man_entrance, 'Old Man Cave Exit (West)', player)
# place Old Man House in Light World
- connect_caves(world, lw_entrances, [], list(Old_Man_House), player) #for multiple seeds
+ connect_caves(multiworld, lw_entrances, [], list(Old_Man_House), player) #for multiple seeds
# now scramble the rest
- connect_caves(world, lw_entrances, dw_entrances, caves, player)
+ connect_caves(multiworld, lw_entrances, dw_entrances, caves, player)
# scramble holes
- scramble_holes(world, player)
+ scramble_holes(multiworld, player)
doors = lw_entrances + dw_entrances
# place remaining doors
- connect_doors(world, doors, door_targets, player)
+ connect_doors(multiworld, doors, door_targets, player)
- elif world.worlds[player].options.entrance_shuffle == 'full':
- skull_woods_shuffle(world, player)
+ elif multiworld.worlds[player].options.entrance_shuffle == 'full':
+ skull_woods_shuffle(multiworld, player)
lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances)
dw_entrances = list(DW_Entrances + DW_Dungeon_Entrances + DW_Single_Cave_Doors)
@@ -225,18 +226,18 @@ def link_entrances(world, player):
old_man_house = list(Old_Man_House)
# tavern back door cannot be shuffled yet
- connect_doors(world, ['Tavern North'], ['Tavern'], player)
+ connect_doors(multiworld, ['Tavern North'], ['Tavern'], player)
- if world.worlds[player].options.mode == 'standard':
+ if multiworld.worlds[player].options.mode == 'standard':
# must connect front of hyrule castle to do escape
- connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
+ connect_two_way(multiworld, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
else:
- caves.append(tuple(world.random.sample(
+ caves.append(tuple(multiworld.random.sample(
['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'], 3)))
lw_entrances.append('Hyrule Castle Entrance (South)')
- if not world.shuffle_ganon:
- connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player)
+ if not multiworld.shuffle_ganon:
+ connect_two_way(multiworld, 'Ganons Tower', 'Ganons Tower Exit', player)
else:
dw_entrances.append('Ganons Tower')
caves.append('Ganons Tower Exit')
@@ -244,45 +245,45 @@ def link_entrances(world, player):
# we randomize which world requirements we fulfill first so we get better dungeon distribution
#we also places the Old Man House at this time to make sure he can be connected to the desert one way
- if world.random.randint(0, 1) == 0:
+ if multiworld.random.randint(0, 1) == 0:
caves += old_man_house
- connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player)
+ connect_mandatory_exits(multiworld, lw_entrances, caves, lw_must_exits, player)
try:
caves.remove(old_man_house[0])
except ValueError:
pass
else: # if the cave wasn't placed we get here
- connect_caves(world, lw_entrances, [], old_man_house, player)
- connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player)
+ connect_caves(multiworld, lw_entrances, [], old_man_house, player)
+ connect_mandatory_exits(multiworld, dw_entrances, caves, dw_must_exits, player)
else:
- connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player)
+ connect_mandatory_exits(multiworld, dw_entrances, caves, dw_must_exits, player)
caves += old_man_house
- connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player)
+ connect_mandatory_exits(multiworld, lw_entrances, caves, lw_must_exits, player)
try:
caves.remove(old_man_house[0])
except ValueError:
pass
else: #if the cave wasn't placed we get here
- connect_caves(world, lw_entrances, [], old_man_house, player)
- if world.worlds[player].options.mode == 'standard':
+ connect_caves(multiworld, lw_entrances, [], old_man_house, player)
+ if multiworld.worlds[player].options.mode == 'standard':
# rest of hyrule castle must be in light world
- connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player)
+ connect_caves(multiworld, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player)
# place old man, has limited options
# exit has to come from specific set of doors, the entrance is free to move about
old_man_entrances = [door for door in old_man_entrances if door in lw_entrances]
- world.random.shuffle(old_man_entrances)
+ multiworld.random.shuffle(old_man_entrances)
old_man_exit = old_man_entrances.pop()
- connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player)
+ connect_two_way(multiworld, old_man_exit, 'Old Man Cave Exit (East)', player)
lw_entrances.remove(old_man_exit)
# place blacksmith, has limited options
all_entrances = lw_entrances + dw_entrances
# cannot place it anywhere already taken (or that are otherwise not eligable for placement)
blacksmith_doors = [door for door in blacksmith_doors if door in all_entrances]
- world.random.shuffle(blacksmith_doors)
+ multiworld.random.shuffle(blacksmith_doors)
blacksmith_hut = blacksmith_doors.pop()
- connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player)
+ connect_entrance(multiworld, blacksmith_hut, 'Blacksmiths Hut', player)
if blacksmith_hut in lw_entrances:
lw_entrances.remove(blacksmith_hut)
if blacksmith_hut in dw_entrances:
@@ -293,9 +294,9 @@ def link_entrances(world, player):
all_entrances = lw_entrances + dw_entrances
# cannot place it anywhere already taken (or that are otherwise not eligable for placement)
bomb_shop_doors = [door for door in bomb_shop_doors if door in all_entrances]
- world.random.shuffle(bomb_shop_doors)
+ multiworld.random.shuffle(bomb_shop_doors)
bomb_shop = bomb_shop_doors.pop()
- connect_entrance(world, bomb_shop, 'Big Bomb Shop', player)
+ connect_entrance(multiworld, bomb_shop, 'Big Bomb Shop', player)
if bomb_shop in lw_entrances:
lw_entrances.remove(bomb_shop)
if bomb_shop in dw_entrances:
@@ -303,21 +304,21 @@ def link_entrances(world, player):
# place the old man cave's entrance somewhere in the light world
old_man_entrance = lw_entrances.pop()
- connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player)
+ connect_two_way(multiworld, old_man_entrance, 'Old Man Cave Exit (West)', player)
# now scramble the rest
- connect_caves(world, lw_entrances, dw_entrances, caves, player)
+ connect_caves(multiworld, lw_entrances, dw_entrances, caves, player)
# scramble holes
- scramble_holes(world, player)
+ scramble_holes(multiworld, player)
doors = lw_entrances + dw_entrances
# place remaining doors
- connect_doors(world, doors, door_targets, player)
- elif world.worlds[player].options.entrance_shuffle == 'crossed':
- skull_woods_shuffle(world, player)
+ connect_doors(multiworld, doors, door_targets, player)
+ elif multiworld.worlds[player].options.entrance_shuffle == 'crossed':
+ skull_woods_shuffle(multiworld, player)
entrances = list(LW_Entrances + LW_Dungeon_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances + DW_Entrances + DW_Dungeon_Entrances + DW_Single_Cave_Doors)
must_exits = list(DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit + LW_Dungeon_Entrances_Must_Exit)
@@ -329,43 +330,43 @@ def link_entrances(world, player):
door_targets = list(Single_Cave_Targets)
# tavern back door cannot be shuffled yet
- connect_doors(world, ['Tavern North'], ['Tavern'], player)
+ connect_doors(multiworld, ['Tavern North'], ['Tavern'], player)
- if world.worlds[player].options.mode == 'standard':
+ if multiworld.worlds[player].options.mode == 'standard':
# must connect front of hyrule castle to do escape
- connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
+ connect_two_way(multiworld, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
else:
- caves.append(tuple(world.random.sample(
+ caves.append(tuple(multiworld.random.sample(
['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'], 3)))
entrances.append('Hyrule Castle Entrance (South)')
- if not world.shuffle_ganon:
- connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player)
+ if not multiworld.shuffle_ganon:
+ connect_two_way(multiworld, 'Ganons Tower', 'Ganons Tower Exit', player)
else:
entrances.append('Ganons Tower')
caves.append('Ganons Tower Exit')
#place must-exit caves
- connect_mandatory_exits(world, entrances, caves, must_exits, player)
+ connect_mandatory_exits(multiworld, entrances, caves, must_exits, player)
- if world.worlds[player].options.mode == 'standard':
+ if multiworld.worlds[player].options.mode == 'standard':
# rest of hyrule castle must be dealt with
- connect_caves(world, entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player)
+ connect_caves(multiworld, entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player)
# place old man, has limited options
# exit has to come from specific set of doors, the entrance is free to move about
old_man_entrances = [door for door in old_man_entrances if door in entrances]
- world.random.shuffle(old_man_entrances)
+ multiworld.random.shuffle(old_man_entrances)
old_man_exit = old_man_entrances.pop()
- connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player)
+ connect_two_way(multiworld, old_man_exit, 'Old Man Cave Exit (East)', player)
entrances.remove(old_man_exit)
# place blacksmith, has limited options
# cannot place it anywhere already taken (or that are otherwise not eligable for placement)
blacksmith_doors = [door for door in blacksmith_doors if door in entrances]
- world.random.shuffle(blacksmith_doors)
+ multiworld.random.shuffle(blacksmith_doors)
blacksmith_hut = blacksmith_doors.pop()
- connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player)
+ connect_entrance(multiworld, blacksmith_hut, 'Blacksmiths Hut', player)
entrances.remove(blacksmith_hut)
bomb_shop_doors.extend(blacksmith_doors)
@@ -373,28 +374,28 @@ def link_entrances(world, player):
# cannot place it anywhere already taken (or that are otherwise not eligable for placement)
bomb_shop_doors = [door for door in bomb_shop_doors if door in entrances]
- world.random.shuffle(bomb_shop_doors)
+ multiworld.random.shuffle(bomb_shop_doors)
bomb_shop = bomb_shop_doors.pop()
- connect_entrance(world, bomb_shop, 'Big Bomb Shop', player)
+ connect_entrance(multiworld, bomb_shop, 'Big Bomb Shop', player)
entrances.remove(bomb_shop)
# place the old man cave's entrance somewhere
- world.random.shuffle(entrances)
+ multiworld.random.shuffle(entrances)
old_man_entrance = entrances.pop()
- connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player)
+ connect_two_way(multiworld, old_man_entrance, 'Old Man Cave Exit (West)', player)
# now scramble the rest
- connect_caves(world, entrances, [], caves, player)
+ connect_caves(multiworld, entrances, [], caves, player)
# scramble holes
- scramble_holes(world, player)
+ scramble_holes(multiworld, player)
# place remaining doors
- connect_doors(world, entrances, door_targets, player)
+ connect_doors(multiworld, entrances, door_targets, player)
- elif world.worlds[player].options.entrance_shuffle == 'insanity':
+ elif multiworld.worlds[player].options.entrance_shuffle == 'insanity':
# beware ye who enter here
entrances = LW_Entrances + LW_Dungeon_Entrances + DW_Entrances + DW_Dungeon_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave']
@@ -412,7 +413,7 @@ def link_entrances(world, player):
blacksmith_doors = list(Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors)
door_targets = list(Single_Cave_Targets)
- world.random.shuffle(doors)
+ multiworld.random.shuffle(doors)
old_man_entrances = list(Old_Man_Entrances) + ['Tower of Hera']
@@ -429,13 +430,13 @@ def link_entrances(world, player):
'Skull Woods First Section (Left)', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Top)']
# tavern back door cannot be shuffled yet
- connect_doors(world, ['Tavern North'], ['Tavern'], player)
+ connect_doors(multiworld, ['Tavern North'], ['Tavern'], player)
- if world.worlds[player].options.mode == 'standard':
+ if multiworld.worlds[player].options.mode == 'standard':
# cannot move uncle cave
- connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player)
- connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player)
- connect_entrance(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player)
+ connect_entrance(multiworld, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player)
+ connect_exit(multiworld, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player)
+ connect_entrance(multiworld, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player)
else:
hole_entrances.append('Hyrule Castle Secret Entrance Drop')
hole_targets.append('Hyrule Castle Secret Entrance')
@@ -443,10 +444,10 @@ def link_entrances(world, player):
entrances.append('Hyrule Castle Secret Entrance Stairs')
caves.append('Hyrule Castle Secret Entrance Exit')
- if not world.shuffle_ganon:
- connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player)
- connect_two_way(world, 'Pyramid Entrance', 'Pyramid Exit', player)
- connect_entrance(world, 'Pyramid Hole', 'Pyramid', player)
+ if not multiworld.shuffle_ganon:
+ connect_two_way(multiworld, 'Ganons Tower', 'Ganons Tower Exit', player)
+ connect_two_way(multiworld, 'Pyramid Entrance', 'Pyramid Exit', player)
+ connect_entrance(multiworld, 'Pyramid Hole', 'Pyramid', player)
else:
entrances.append('Ganons Tower')
caves.extend(['Ganons Tower Exit', 'Pyramid Exit'])
@@ -455,19 +456,19 @@ def link_entrances(world, player):
entrances_must_exits.append('Pyramid Entrance')
doors.extend(['Ganons Tower', 'Pyramid Entrance'])
- world.random.shuffle(hole_entrances)
- world.random.shuffle(hole_targets)
- world.random.shuffle(entrances)
+ multiworld.random.shuffle(hole_entrances)
+ multiworld.random.shuffle(hole_targets)
+ multiworld.random.shuffle(entrances)
# fill up holes
for hole in hole_entrances:
- connect_entrance(world, hole, hole_targets.pop(), player)
+ connect_entrance(multiworld, hole, hole_targets.pop(), player)
# hyrule castle handling
- if world.worlds[player].options.mode == 'standard':
+ if multiworld.worlds[player].options.mode == 'standard':
# must connect front of hyrule castle to do escape
- connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
- connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player)
+ connect_entrance(multiworld, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
+ connect_exit(multiworld, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player)
caves.append(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'))
else:
doors.append('Hyrule Castle Entrance (South)')
@@ -476,7 +477,7 @@ def link_entrances(world, player):
# now let's deal with mandatory reachable stuff
def extract_reachable_exit(cavelist):
- world.random.shuffle(cavelist)
+ multiworld.random.shuffle(cavelist)
candidate = None
for cave in cavelist:
if isinstance(cave, tuple) and len(cave) > 1:
@@ -496,8 +497,8 @@ def connect_reachable_exit(entrance, caves, doors):
exit = cave[-1]
cave = cave[:-1]
- connect_exit(world, exit, entrance, player)
- connect_entrance(world, doors.pop(), exit, player)
+ connect_exit(multiworld, exit, entrance, player)
+ connect_entrance(multiworld, doors.pop(), exit, player)
# rest of cave now is forced to be in this world
caves.append(cave)
@@ -508,26 +509,26 @@ def connect_reachable_exit(entrance, caves, doors):
# place old man, has limited options
# exit has to come from specific set of doors, the entrance is free to move about
old_man_entrances = [entrance for entrance in old_man_entrances if entrance in entrances]
- world.random.shuffle(old_man_entrances)
+ multiworld.random.shuffle(old_man_entrances)
old_man_exit = old_man_entrances.pop()
entrances.remove(old_man_exit)
- connect_exit(world, 'Old Man Cave Exit (East)', old_man_exit, player)
- connect_entrance(world, doors.pop(), 'Old Man Cave Exit (East)', player)
+ connect_exit(multiworld, 'Old Man Cave Exit (East)', old_man_exit, player)
+ connect_entrance(multiworld, doors.pop(), 'Old Man Cave Exit (East)', player)
caves.append('Old Man Cave Exit (West)')
# place blacksmith, has limited options
blacksmith_doors = [door for door in blacksmith_doors if door in doors]
- world.random.shuffle(blacksmith_doors)
+ multiworld.random.shuffle(blacksmith_doors)
blacksmith_hut = blacksmith_doors.pop()
- connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player)
+ connect_entrance(multiworld, blacksmith_hut, 'Blacksmiths Hut', player)
doors.remove(blacksmith_hut)
# place dam and pyramid fairy, have limited options
bomb_shop_doors = [door for door in bomb_shop_doors if door in doors]
- world.random.shuffle(bomb_shop_doors)
+ multiworld.random.shuffle(bomb_shop_doors)
bomb_shop = bomb_shop_doors.pop()
- connect_entrance(world, bomb_shop, 'Big Bomb Shop', player)
+ connect_entrance(multiworld, bomb_shop, 'Big Bomb Shop', player)
doors.remove(bomb_shop)
# handle remaining caves
@@ -536,40 +537,40 @@ def connect_reachable_exit(entrance, caves, doors):
cave = (cave,)
for exit in cave:
- connect_exit(world, exit, entrances.pop(), player)
- connect_entrance(world, doors.pop(), exit, player)
+ connect_exit(multiworld, exit, entrances.pop(), player)
+ connect_entrance(multiworld, doors.pop(), exit, player)
# place remaining doors
- connect_doors(world, doors, door_targets, player)
+ connect_doors(multiworld, doors, door_targets, player)
else:
raise NotImplementedError(
- f'{world.worlds[player].options.entrance_shuffle} Shuffling not supported yet. Player {world.get_player_name(player)}')
+ f'{multiworld.worlds[player].options.entrance_shuffle} Shuffling not supported yet. Player {multiworld.get_player_name(player)}')
- if world.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
- overworld_glitch_connections(world, player)
+ if multiworld.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
+ overworld_glitch_connections(multiworld, player)
# mandatory hybrid major glitches connections
- if world.worlds[player].options.glitches_required in ['hybrid_major_glitches', 'no_logic']:
- underworld_glitch_connections(world, player)
+ if multiworld.worlds[player].options.glitches_required in ['hybrid_major_glitches', 'no_logic']:
+ underworld_glitch_connections(multiworld, player)
# check for swamp palace fix
- if world.get_entrance('Dam', player).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)':
- world.worlds[player].swamp_patch_required = True
+ if multiworld.get_entrance('Dam', player).connected_region.name != 'Dam' or multiworld.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)':
+ multiworld.worlds[player].swamp_patch_required = True
# check for potion shop location
- if world.get_entrance('Potion Shop', player).connected_region.name != 'Potion Shop':
- world.worlds[player].powder_patch_required = True
+ if multiworld.get_entrance('Potion Shop', player).connected_region.name != 'Potion Shop':
+ multiworld.worlds[player].powder_patch_required = True
# check for ganon location
- if world.get_entrance('Pyramid Hole', player).connected_region.name != 'Pyramid':
- world.worlds[player].ganon_at_pyramid = False
+ if multiworld.get_entrance('Pyramid Hole', player).connected_region.name != 'Pyramid':
+ multiworld.worlds[player].ganon_at_pyramid = False
# check for Ganon's Tower location
- if world.get_entrance('Ganons Tower', player).connected_region.name != 'Ganons Tower (Entrance)':
- world.worlds[player].ganonstower_vanilla = False
+ if multiworld.get_entrance('Ganons Tower', player).connected_region.name != 'Ganons Tower (Entrance)':
+ multiworld.worlds[player].ganonstower_vanilla = False
-def link_inverted_entrances(world, player):
+def link_inverted_entrances(multiworld: MultiWorld, player: int):
# Link's house shuffled freely, Houlihan set in mandatory_connections
Dungeon_Exits = Inverted_Dungeon_Exits_Base.copy()
@@ -577,28 +578,28 @@ def link_inverted_entrances(world, player):
Old_Man_House = Old_Man_House_Base.copy()
Cave_Three_Exits = Cave_Three_Exits_Base.copy()
- unbias_some_entrances(world, Dungeon_Exits, Cave_Exits, Old_Man_House, Cave_Three_Exits)
+ unbias_some_entrances(multiworld, Dungeon_Exits, Cave_Exits, Old_Man_House, Cave_Three_Exits)
# setup mandatory connections
for exitname, regionname in inverted_mandatory_connections:
- connect_simple(world, exitname, regionname, player)
+ connect_simple(multiworld, exitname, regionname, player)
# if we do not shuffle, set default connections
- if world.worlds[player].options.entrance_shuffle == 'vanilla':
+ if multiworld.worlds[player].options.entrance_shuffle == 'vanilla':
for exitname, regionname in inverted_default_connections:
- connect_simple(world, exitname, regionname, player)
+ connect_simple(multiworld, exitname, regionname, player)
for exitname, regionname in inverted_default_dungeon_connections:
- connect_simple(world, exitname, regionname, player)
- elif world.worlds[player].options.entrance_shuffle == 'dungeons_simple':
+ connect_simple(multiworld, exitname, regionname, player)
+ elif multiworld.worlds[player].options.entrance_shuffle == 'dungeons_simple':
for exitname, regionname in inverted_default_connections:
- connect_simple(world, exitname, regionname, player)
+ connect_simple(multiworld, exitname, regionname, player)
- simple_shuffle_dungeons(world, player)
- elif world.worlds[player].options.entrance_shuffle == 'dungeons_full':
+ simple_shuffle_dungeons(multiworld, player)
+ elif multiworld.worlds[player].options.entrance_shuffle == 'dungeons_full':
for exitname, regionname in inverted_default_connections:
- connect_simple(world, exitname, regionname, player)
+ connect_simple(multiworld, exitname, regionname, player)
- skull_woods_shuffle(world, player)
+ skull_woods_shuffle(multiworld, player)
dungeon_exits = list(Dungeon_Exits)
lw_entrances = list(Inverted_LW_Dungeon_Entrances)
@@ -606,7 +607,7 @@ def link_inverted_entrances(world, player):
dw_entrances = list(Inverted_DW_Dungeon_Entrances)
# randomize which desert ledge door is a must-exit
- if world.random.randint(0, 1):
+ if multiworld.random.randint(0, 1):
lw_dungeon_entrances_must_exit.append('Desert Palace Entrance (North)')
lw_entrances.append('Desert Palace Entrance (West)')
else:
@@ -616,8 +617,8 @@ def link_inverted_entrances(world, player):
dungeon_exits.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'))
lw_entrances.append('Hyrule Castle Entrance (South)')
- if not world.shuffle_ganon:
- connect_two_way(world, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player)
+ if not multiworld.shuffle_ganon:
+ connect_two_way(multiworld, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player)
hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)']
else:
lw_entrances.append('Inverted Ganons Tower')
@@ -627,14 +628,14 @@ def link_inverted_entrances(world, player):
# shuffle aga door first. If it's on HC ledge, remaining HC ledge door must be must-exit
all_entrances_aga = lw_entrances + dw_entrances
aga_doors = [i for i in all_entrances_aga]
- world.random.shuffle(aga_doors)
+ multiworld.random.shuffle(aga_doors)
aga_door = aga_doors.pop()
if aga_door in hc_ledge_entrances:
lw_entrances.remove(aga_door)
hc_ledge_entrances.remove(aga_door)
- world.random.shuffle(hc_ledge_entrances)
+ multiworld.random.shuffle(hc_ledge_entrances)
hc_ledge_must_exit = hc_ledge_entrances.pop()
lw_entrances.remove(hc_ledge_must_exit)
lw_dungeon_entrances_must_exit.append(hc_ledge_must_exit)
@@ -643,16 +644,16 @@ def link_inverted_entrances(world, player):
else:
dw_entrances.remove(aga_door)
- connect_two_way(world, aga_door, 'Inverted Agahnims Tower Exit', player)
+ connect_two_way(multiworld, aga_door, 'Inverted Agahnims Tower Exit', player)
dungeon_exits.remove('Inverted Agahnims Tower Exit')
- connect_mandatory_exits(world, lw_entrances, dungeon_exits, lw_dungeon_entrances_must_exit, player)
+ connect_mandatory_exits(multiworld, lw_entrances, dungeon_exits, lw_dungeon_entrances_must_exit, player)
- connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player)
- elif world.worlds[player].options.entrance_shuffle == 'dungeons_crossed':
- inverted_crossed_shuffle_dungeons(world, player)
- elif world.worlds[player].options.entrance_shuffle == 'simple':
- simple_shuffle_dungeons(world, player)
+ connect_caves(multiworld, lw_entrances, dw_entrances, dungeon_exits, player)
+ elif multiworld.worlds[player].options.entrance_shuffle == 'dungeons_crossed':
+ inverted_crossed_shuffle_dungeons(multiworld, player)
+ elif multiworld.worlds[player].options.entrance_shuffle == 'simple':
+ simple_shuffle_dungeons(multiworld, player)
old_man_entrances = list(Inverted_Old_Man_Entrances)
caves = list(Cave_Exits)
@@ -666,28 +667,28 @@ def link_inverted_entrances(world, player):
# we shuffle all 2 entrance caves as pairs as a start
# start with the ones that need to be directed
two_door_caves = list(Inverted_Two_Door_Caves_Directional)
- world.random.shuffle(two_door_caves)
- world.random.shuffle(caves)
+ multiworld.random.shuffle(two_door_caves)
+ multiworld.random.shuffle(caves)
while two_door_caves:
entrance1, entrance2 = two_door_caves.pop()
exit1, exit2 = caves.pop()
- connect_two_way(world, entrance1, exit1, player)
- connect_two_way(world, entrance2, exit2, player)
+ connect_two_way(multiworld, entrance1, exit1, player)
+ connect_two_way(multiworld, entrance2, exit2, player)
# now the remaining pairs
two_door_caves = list(Inverted_Two_Door_Caves)
- world.random.shuffle(two_door_caves)
+ multiworld.random.shuffle(two_door_caves)
while two_door_caves:
entrance1, entrance2 = two_door_caves.pop()
exit1, exit2 = caves.pop()
- connect_two_way(world, entrance1, exit1, player)
- connect_two_way(world, entrance2, exit2, player)
+ connect_two_way(multiworld, entrance1, exit1, player)
+ connect_two_way(multiworld, entrance2, exit2, player)
# place links house
links_house_doors = [i for i in bomb_shop_doors + blacksmith_doors if
i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors]
- links_house = world.random.choice(list(links_house_doors))
- connect_two_way(world, links_house, 'Inverted Links House Exit', player)
+ links_house = multiworld.random.choice(list(links_house_doors))
+ connect_two_way(multiworld, links_house, 'Inverted Links House Exit', player)
if links_house in bomb_shop_doors:
bomb_shop_doors.remove(links_house)
if links_house in blacksmith_doors:
@@ -697,11 +698,11 @@ def link_inverted_entrances(world, player):
# place dark sanc
sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in bomb_shop_doors]
- sanc_door = world.random.choice(sanc_doors)
+ sanc_door = multiworld.random.choice(sanc_doors)
bomb_shop_doors.remove(sanc_door)
- connect_entrance(world, sanc_door, 'Inverted Dark Sanctuary', player)
- world.get_entrance('Inverted Dark Sanctuary Exit', player).connect(world.get_entrance(sanc_door, player).parent_region)
+ connect_entrance(multiworld, sanc_door, 'Inverted Dark Sanctuary', player)
+ multiworld.get_entrance('Inverted Dark Sanctuary Exit', player).connect(multiworld.get_entrance(sanc_door, player).parent_region)
lw_dm_entrances = ['Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'Paradox Cave (Top)', 'Old Man House (Bottom)',
'Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave (Top)', 'Spiral Cave (Bottom)', 'Old Man Cave (East)',
@@ -710,10 +711,10 @@ def link_inverted_entrances(world, player):
# place old man, bumper cave bottom to DDM entrances not in east bottom
- world.random.shuffle(old_man_entrances)
+ multiworld.random.shuffle(old_man_entrances)
old_man_exit = old_man_entrances.pop()
- connect_two_way(world, 'Bumper Cave (Bottom)', 'Old Man Cave Exit (West)', player)
- connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player)
+ connect_two_way(multiworld, 'Bumper Cave (Bottom)', 'Old Man Cave Exit (West)', player)
+ connect_two_way(multiworld, old_man_exit, 'Old Man Cave Exit (East)', player)
if old_man_exit == 'Spike Cave':
bomb_shop_doors.remove('Spike Cave')
bomb_shop_doors.extend(old_man_entrances)
@@ -723,33 +724,33 @@ def link_inverted_entrances(world, player):
caves.extend(list(three_exit_caves))
# connect rest
- connect_caves(world, lw_dm_entrances, [], caves, player)
+ connect_caves(multiworld, lw_dm_entrances, [], caves, player)
# scramble holes
- scramble_inverted_holes(world, player)
+ scramble_inverted_holes(multiworld, player)
# place blacksmith, has limited options
blacksmith_doors = [door for door in blacksmith_doors[:]]
- world.random.shuffle(blacksmith_doors)
+ multiworld.random.shuffle(blacksmith_doors)
blacksmith_hut = blacksmith_doors.pop()
- connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player)
+ connect_entrance(multiworld, blacksmith_hut, 'Blacksmiths Hut', player)
bomb_shop_doors.extend(blacksmith_doors)
# place bomb shop, has limited options
bomb_shop_doors = [door for door in bomb_shop_doors[:]]
- world.random.shuffle(bomb_shop_doors)
+ multiworld.random.shuffle(bomb_shop_doors)
bomb_shop = bomb_shop_doors.pop()
- connect_entrance(world, bomb_shop, 'Inverted Big Bomb Shop', player)
+ connect_entrance(multiworld, bomb_shop, 'Inverted Big Bomb Shop', player)
single_doors.extend(bomb_shop_doors)
# tavern back door cannot be shuffled yet
- connect_doors(world, ['Tavern North'], ['Tavern'], player)
+ connect_doors(multiworld, ['Tavern North'], ['Tavern'], player)
# place remaining doors
- connect_doors(world, single_doors, door_targets, player)
+ connect_doors(multiworld, single_doors, door_targets, player)
- elif world.worlds[player].options.entrance_shuffle == 'restricted':
- simple_shuffle_dungeons(world, player)
+ elif multiworld.worlds[player].options.entrance_shuffle == 'restricted':
+ simple_shuffle_dungeons(multiworld, player)
lw_entrances = list(Inverted_LW_Entrances + Inverted_LW_Single_Cave_Doors)
dw_entrances = list(Inverted_DW_Entrances + Inverted_DW_Single_Cave_Doors + Inverted_Old_Man_Entrances)
@@ -764,8 +765,8 @@ def link_inverted_entrances(world, player):
# place links house
links_house_doors = [i for i in lw_entrances + dw_entrances + lw_must_exits if
i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors]
- links_house = world.random.choice(list(links_house_doors))
- connect_two_way(world, links_house, 'Inverted Links House Exit', player)
+ links_house = multiworld.random.choice(list(links_house_doors))
+ connect_two_way(multiworld, links_house, 'Inverted Links House Exit', player)
if links_house in lw_entrances:
lw_entrances.remove(links_house)
elif links_house in dw_entrances:
@@ -775,32 +776,32 @@ def link_inverted_entrances(world, player):
# place dark sanc
sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in dw_entrances]
- sanc_door = world.random.choice(sanc_doors)
+ sanc_door = multiworld.random.choice(sanc_doors)
dw_entrances.remove(sanc_door)
- connect_entrance(world, sanc_door, 'Inverted Dark Sanctuary', player)
- world.get_entrance('Inverted Dark Sanctuary Exit', player).connect(world.get_entrance(sanc_door, player).parent_region)
+ connect_entrance(multiworld, sanc_door, 'Inverted Dark Sanctuary', player)
+ multiworld.get_entrance('Inverted Dark Sanctuary Exit', player).connect(multiworld.get_entrance(sanc_door, player).parent_region)
# tavern back door cannot be shuffled yet
- connect_doors(world, ['Tavern North'], ['Tavern'], player)
+ connect_doors(multiworld, ['Tavern North'], ['Tavern'], player)
# place must exits
- connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player)
+ connect_mandatory_exits(multiworld, lw_entrances, caves, lw_must_exits, player)
# place old man, has limited options
# exit has to come from specific set of doors, the entrance is free to move about
old_man_entrances = [door for door in old_man_entrances if door in dw_entrances]
- world.random.shuffle(old_man_entrances)
+ multiworld.random.shuffle(old_man_entrances)
old_man_exit = old_man_entrances.pop()
- connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player)
+ connect_two_way(multiworld, old_man_exit, 'Old Man Cave Exit (East)', player)
dw_entrances.remove(old_man_exit)
# place blacksmith, has limited options
all_entrances = lw_entrances + dw_entrances
# cannot place it anywhere already taken (or that are otherwise not eligible for placement)
blacksmith_doors = [door for door in blacksmith_doors if door in all_entrances]
- world.random.shuffle(blacksmith_doors)
+ multiworld.random.shuffle(blacksmith_doors)
blacksmith_hut = blacksmith_doors.pop()
- connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player)
+ connect_entrance(multiworld, blacksmith_hut, 'Blacksmiths Hut', player)
if blacksmith_hut in lw_entrances:
lw_entrances.remove(blacksmith_hut)
if blacksmith_hut in dw_entrances:
@@ -811,30 +812,30 @@ def link_inverted_entrances(world, player):
all_entrances = lw_entrances + dw_entrances
# cannot place it anywhere already taken (or that are otherwise not eligible for placement)
bomb_shop_doors = [door for door in bomb_shop_doors if door in all_entrances]
- world.random.shuffle(bomb_shop_doors)
+ multiworld.random.shuffle(bomb_shop_doors)
bomb_shop = bomb_shop_doors.pop()
- connect_entrance(world, bomb_shop, 'Inverted Big Bomb Shop', player)
+ connect_entrance(multiworld, bomb_shop, 'Inverted Big Bomb Shop', player)
if bomb_shop in lw_entrances:
lw_entrances.remove(bomb_shop)
if bomb_shop in dw_entrances:
dw_entrances.remove(bomb_shop)
# place the old man cave's entrance somewhere in the dark world
- world.random.shuffle(dw_entrances)
+ multiworld.random.shuffle(dw_entrances)
old_man_entrance = dw_entrances.pop()
- connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player)
+ connect_two_way(multiworld, old_man_entrance, 'Old Man Cave Exit (West)', player)
# now scramble the rest
- connect_caves(world, lw_entrances, dw_entrances, caves, player)
+ connect_caves(multiworld, lw_entrances, dw_entrances, caves, player)
# scramble holes
- scramble_inverted_holes(world, player)
+ scramble_inverted_holes(multiworld, player)
doors = lw_entrances + dw_entrances
# place remaining doors
- connect_doors(world, doors, door_targets, player)
- elif world.worlds[player].options.entrance_shuffle == 'full':
- skull_woods_shuffle(world, player)
+ connect_doors(multiworld, doors, door_targets, player)
+ elif multiworld.worlds[player].options.entrance_shuffle == 'full':
+ skull_woods_shuffle(multiworld, player)
lw_entrances = list(Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_LW_Single_Cave_Doors)
dw_entrances = list(Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_DW_Single_Cave_Doors + Inverted_Old_Man_Entrances)
@@ -847,7 +848,7 @@ def link_inverted_entrances(world, player):
old_man_house = list(Old_Man_House)
# randomize which desert ledge door is a must-exit
- if world.random.randint(0, 1) == 0:
+ if multiworld.random.randint(0, 1) == 0:
lw_must_exits.append('Desert Palace Entrance (North)')
lw_entrances.append('Desert Palace Entrance (West)')
else:
@@ -855,12 +856,12 @@ def link_inverted_entrances(world, player):
lw_entrances.append('Desert Palace Entrance (North)')
# tavern back door cannot be shuffled yet
- connect_doors(world, ['Tavern North'], ['Tavern'], player)
+ connect_doors(multiworld, ['Tavern North'], ['Tavern'], player)
lw_entrances.append('Hyrule Castle Entrance (South)')
- if not world.shuffle_ganon:
- connect_two_way(world, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player)
+ if not multiworld.shuffle_ganon:
+ connect_two_way(multiworld, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player)
hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)']
else:
lw_entrances.append('Inverted Ganons Tower')
@@ -870,14 +871,14 @@ def link_inverted_entrances(world, player):
# shuffle aga door first. if it's on hc ledge, then one other hc ledge door has to be must_exit
all_entrances_aga = lw_entrances + dw_entrances
aga_doors = [i for i in all_entrances_aga]
- world.random.shuffle(aga_doors)
+ multiworld.random.shuffle(aga_doors)
aga_door = aga_doors.pop()
if aga_door in hc_ledge_entrances:
lw_entrances.remove(aga_door)
hc_ledge_entrances.remove(aga_door)
- world.random.shuffle(hc_ledge_entrances)
+ multiworld.random.shuffle(hc_ledge_entrances)
hc_ledge_must_exit = hc_ledge_entrances.pop()
lw_entrances.remove(hc_ledge_must_exit)
lw_must_exits.append(hc_ledge_must_exit)
@@ -886,14 +887,14 @@ def link_inverted_entrances(world, player):
else:
dw_entrances.remove(aga_door)
- connect_two_way(world, aga_door, 'Inverted Agahnims Tower Exit', player)
+ connect_two_way(multiworld, aga_door, 'Inverted Agahnims Tower Exit', player)
caves.remove('Inverted Agahnims Tower Exit')
# place links house
links_house_doors = [i for i in lw_entrances + dw_entrances + lw_must_exits if
i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors]
- links_house = world.random.choice(list(links_house_doors))
- connect_two_way(world, links_house, 'Inverted Links House Exit', player)
+ links_house = multiworld.random.choice(list(links_house_doors))
+ connect_two_way(multiworld, links_house, 'Inverted Links House Exit', player)
if links_house in lw_entrances:
lw_entrances.remove(links_house)
if links_house in dw_entrances:
@@ -903,35 +904,35 @@ def link_inverted_entrances(world, player):
# place dark sanc
sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in dw_entrances]
- sanc_door = world.random.choice(sanc_doors)
+ sanc_door = multiworld.random.choice(sanc_doors)
dw_entrances.remove(sanc_door)
- connect_entrance(world, sanc_door, 'Inverted Dark Sanctuary', player)
- world.get_entrance('Inverted Dark Sanctuary Exit', player).connect(world.get_entrance(sanc_door, player).parent_region)
+ connect_entrance(multiworld, sanc_door, 'Inverted Dark Sanctuary', player)
+ multiworld.get_entrance('Inverted Dark Sanctuary Exit', player).connect(multiworld.get_entrance(sanc_door, player).parent_region)
# place old man house
# no dw must exits in inverted, but we randomize whether cave is in light or dark world
- if world.random.randint(0, 1) == 0:
+ if multiworld.random.randint(0, 1) == 0:
caves += old_man_house
- connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player)
+ connect_mandatory_exits(multiworld, lw_entrances, caves, lw_must_exits, player)
try:
caves.remove(old_man_house[0])
except ValueError:
pass
else: # if the cave wasn't placed we get here
- connect_caves(world, lw_entrances, [], old_man_house, player)
+ connect_caves(multiworld, lw_entrances, [], old_man_house, player)
else:
- connect_caves(world, dw_entrances, [], old_man_house, player)
- connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player)
+ connect_caves(multiworld, dw_entrances, [], old_man_house, player)
+ connect_mandatory_exits(multiworld, lw_entrances, caves, lw_must_exits, player)
# put all HC exits in LW in inverted full shuffle
- connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)', 'Hyrule Castle Exit (South)')], player)
+ connect_caves(multiworld, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)', 'Hyrule Castle Exit (South)')], player)
# place old man, has limited options
# exit has to come from specific set of doors, the entrance is free to move about
old_man_entrances = [door for door in old_man_entrances if door in dw_entrances + lw_entrances]
- world.random.shuffle(old_man_entrances)
+ multiworld.random.shuffle(old_man_entrances)
old_man_exit = old_man_entrances.pop()
- connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player)
+ connect_two_way(multiworld, old_man_exit, 'Old Man Cave Exit (East)', player)
if old_man_exit in dw_entrances:
dw_entrances.remove(old_man_exit)
old_man_world = 'dark'
@@ -943,9 +944,9 @@ def link_inverted_entrances(world, player):
all_entrances = lw_entrances + dw_entrances
# cannot place it anywhere already taken (or that are otherwise not eligible for placement)
blacksmith_doors = [door for door in blacksmith_doors if door in all_entrances]
- world.random.shuffle(blacksmith_doors)
+ multiworld.random.shuffle(blacksmith_doors)
blacksmith_hut = blacksmith_doors.pop()
- connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player)
+ connect_entrance(multiworld, blacksmith_hut, 'Blacksmiths Hut', player)
if blacksmith_hut in lw_entrances:
lw_entrances.remove(blacksmith_hut)
if blacksmith_hut in dw_entrances:
@@ -956,36 +957,36 @@ def link_inverted_entrances(world, player):
all_entrances = lw_entrances + dw_entrances
# cannot place it anywhere already taken (or that are otherwise not eligible for placement)
bomb_shop_doors = [door for door in bomb_shop_doors if door in all_entrances]
- world.random.shuffle(bomb_shop_doors)
+ multiworld.random.shuffle(bomb_shop_doors)
bomb_shop = bomb_shop_doors.pop()
- connect_entrance(world, bomb_shop, 'Inverted Big Bomb Shop', player)
+ connect_entrance(multiworld, bomb_shop, 'Inverted Big Bomb Shop', player)
if bomb_shop in lw_entrances:
lw_entrances.remove(bomb_shop)
if bomb_shop in dw_entrances:
dw_entrances.remove(bomb_shop)
- # place the old man cave's entrance somewhere in the same world he'll exit from
+ # place the old man cave's entrance somewhere in the same multiworld he'll exit from
if old_man_world == 'light':
- world.random.shuffle(lw_entrances)
+ multiworld.random.shuffle(lw_entrances)
old_man_entrance = lw_entrances.pop()
- connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player)
+ connect_two_way(multiworld, old_man_entrance, 'Old Man Cave Exit (West)', player)
elif old_man_world == 'dark':
- world.random.shuffle(dw_entrances)
+ multiworld.random.shuffle(dw_entrances)
old_man_entrance = dw_entrances.pop()
- connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player)
+ connect_two_way(multiworld, old_man_entrance, 'Old Man Cave Exit (West)', player)
# now scramble the rest
- connect_caves(world, lw_entrances, dw_entrances, caves, player)
+ connect_caves(multiworld, lw_entrances, dw_entrances, caves, player)
# scramble holes
- scramble_inverted_holes(world, player)
+ scramble_inverted_holes(multiworld, player)
doors = lw_entrances + dw_entrances
# place remaining doors
- connect_doors(world, doors, door_targets, player)
- elif world.worlds[player].options.entrance_shuffle == 'crossed':
- skull_woods_shuffle(world, player)
+ connect_doors(multiworld, doors, door_targets, player)
+ elif multiworld.worlds[player].options.entrance_shuffle == 'crossed':
+ skull_woods_shuffle(multiworld, player)
entrances = list(Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_LW_Single_Cave_Doors + Inverted_Old_Man_Entrances + Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_DW_Single_Cave_Doors)
must_exits = list(Inverted_LW_Entrances_Must_Exit + Inverted_LW_Dungeon_Entrances_Must_Exit)
@@ -997,19 +998,19 @@ def link_inverted_entrances(world, player):
door_targets = list(Inverted_Single_Cave_Targets)
# randomize which desert ledge door is a must-exit
- if world.random.randint(0, 1) == 0:
+ if multiworld.random.randint(0, 1) == 0:
must_exits.append('Desert Palace Entrance (North)')
entrances.append('Desert Palace Entrance (West)')
else:
must_exits.append('Desert Palace Entrance (West)')
entrances.append('Desert Palace Entrance (North)')
- caves.append(tuple(world.random.sample(
+ caves.append(tuple(multiworld.random.sample(
['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'], 3)))
entrances.append('Hyrule Castle Entrance (South)')
- if not world.shuffle_ganon:
- connect_two_way(world, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player)
+ if not multiworld.shuffle_ganon:
+ connect_two_way(multiworld, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player)
hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)']
else:
entrances.append('Inverted Ganons Tower')
@@ -1017,26 +1018,26 @@ def link_inverted_entrances(world, player):
hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)', 'Inverted Ganons Tower']
# shuffle aga door. if it's on hc ledge, then one other hc ledge door has to be must_exit
- aga_door = world.random.choice(list(entrances))
+ aga_door = multiworld.random.choice(list(entrances))
if aga_door in hc_ledge_entrances:
hc_ledge_entrances.remove(aga_door)
- world.random.shuffle(hc_ledge_entrances)
+ multiworld.random.shuffle(hc_ledge_entrances)
hc_ledge_must_exit = hc_ledge_entrances.pop()
entrances.remove(hc_ledge_must_exit)
must_exits.append(hc_ledge_must_exit)
entrances.remove(aga_door)
- connect_two_way(world, aga_door, 'Inverted Agahnims Tower Exit', player)
+ connect_two_way(multiworld, aga_door, 'Inverted Agahnims Tower Exit', player)
caves.remove('Inverted Agahnims Tower Exit')
# place links house
links_house_doors = [i for i in entrances + must_exits if
i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors]
- links_house = world.random.choice(list(links_house_doors))
- connect_two_way(world, links_house, 'Inverted Links House Exit', player)
+ links_house = multiworld.random.choice(list(links_house_doors))
+ connect_two_way(multiworld, links_house, 'Inverted Links House Exit', player)
if links_house in entrances:
entrances.remove(links_house)
elif links_house in must_exits:
@@ -1044,58 +1045,58 @@ def link_inverted_entrances(world, player):
# place dark sanc
sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in entrances]
- sanc_door = world.random.choice(sanc_doors)
+ sanc_door = multiworld.random.choice(sanc_doors)
entrances.remove(sanc_door)
- connect_entrance(world, sanc_door, 'Inverted Dark Sanctuary', player)
- world.get_entrance('Inverted Dark Sanctuary Exit', player).connect(world.get_entrance(sanc_door, player).parent_region)
+ connect_entrance(multiworld, sanc_door, 'Inverted Dark Sanctuary', player)
+ multiworld.get_entrance('Inverted Dark Sanctuary Exit', player).connect(multiworld.get_entrance(sanc_door, player).parent_region)
# tavern back door cannot be shuffled yet
- connect_doors(world, ['Tavern North'], ['Tavern'], player)
+ connect_doors(multiworld, ['Tavern North'], ['Tavern'], player)
#place must-exit caves
- connect_mandatory_exits(world, entrances, caves, must_exits, player)
+ connect_mandatory_exits(multiworld, entrances, caves, must_exits, player)
# place old man, has limited options
# exit has to come from specific set of doors, the entrance is free to move about
old_man_entrances = [door for door in old_man_entrances if door in entrances]
- world.random.shuffle(old_man_entrances)
+ multiworld.random.shuffle(old_man_entrances)
old_man_exit = old_man_entrances.pop()
- connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player)
+ connect_two_way(multiworld, old_man_exit, 'Old Man Cave Exit (East)', player)
entrances.remove(old_man_exit)
# place blacksmith, has limited options
# cannot place it anywhere already taken (or that are otherwise not eligible for placement)
blacksmith_doors = [door for door in blacksmith_doors if door in entrances]
- world.random.shuffle(blacksmith_doors)
+ multiworld.random.shuffle(blacksmith_doors)
blacksmith_hut = blacksmith_doors.pop()
- connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player)
+ connect_entrance(multiworld, blacksmith_hut, 'Blacksmiths Hut', player)
entrances.remove(blacksmith_hut)
# place bomb shop, has limited options
# cannot place it anywhere already taken (or that are otherwise not eligible for placement)
bomb_shop_doors = [door for door in bomb_shop_doors if door in entrances]
- world.random.shuffle(bomb_shop_doors)
+ multiworld.random.shuffle(bomb_shop_doors)
bomb_shop = bomb_shop_doors.pop()
- connect_entrance(world, bomb_shop, 'Inverted Big Bomb Shop', player)
+ connect_entrance(multiworld, bomb_shop, 'Inverted Big Bomb Shop', player)
entrances.remove(bomb_shop)
# place the old man cave's entrance somewhere
- world.random.shuffle(entrances)
+ multiworld.random.shuffle(entrances)
old_man_entrance = entrances.pop()
- connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player)
+ connect_two_way(multiworld, old_man_entrance, 'Old Man Cave Exit (West)', player)
# now scramble the rest
- connect_caves(world, entrances, [], caves, player)
+ connect_caves(multiworld, entrances, [], caves, player)
# scramble holes
- scramble_inverted_holes(world, player)
+ scramble_inverted_holes(multiworld, player)
# place remaining doors
- connect_doors(world, entrances, door_targets, player)
- elif world.worlds[player].options.entrance_shuffle == 'insanity':
+ connect_doors(multiworld, entrances, door_targets, player)
+ elif multiworld.worlds[player].options.entrance_shuffle == 'insanity':
# beware ye who enter here
entrances = Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_Old_Man_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave', 'Hyrule Castle Entrance (South)']
@@ -1106,7 +1107,7 @@ def link_inverted_entrances(world, player):
Inverted_LW_Single_Cave_Doors + Inverted_DW_Single_Cave_Doors + ['Desert Palace Entrance (West)', 'Desert Palace Entrance (North)']
# randomize which desert ledge door is a must-exit
- if world.random.randint(0, 1) == 0:
+ if multiworld.random.randint(0, 1) == 0:
entrances_must_exits.append('Desert Palace Entrance (North)')
entrances.append('Desert Palace Entrance (West)')
else:
@@ -1121,7 +1122,7 @@ def link_inverted_entrances(world, player):
blacksmith_doors = list(Blacksmith_Single_Cave_Doors + Inverted_Blacksmith_Multi_Cave_Doors)
door_targets = list(Inverted_Single_Cave_Targets)
- world.random.shuffle(doors)
+ multiworld.random.shuffle(doors)
old_man_entrances = list(Inverted_Old_Man_Entrances + Old_Man_Entrances) + ['Tower of Hera', 'Inverted Agahnims Tower']
@@ -1137,17 +1138,17 @@ def link_inverted_entrances(world, player):
'Skull Woods First Section (Left)', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Top)']
# tavern back door cannot be shuffled yet
- connect_doors(world, ['Tavern North'], ['Tavern'], player)
+ connect_doors(multiworld, ['Tavern North'], ['Tavern'], player)
hole_entrances.append('Hyrule Castle Secret Entrance Drop')
hole_targets.append('Hyrule Castle Secret Entrance')
entrances.append('Hyrule Castle Secret Entrance Stairs')
caves.append('Hyrule Castle Secret Entrance Exit')
- if not world.shuffle_ganon:
- connect_two_way(world, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player)
- connect_two_way(world, 'Inverted Pyramid Entrance', 'Pyramid Exit', player)
- connect_entrance(world, 'Inverted Pyramid Hole', 'Pyramid', player)
+ if not multiworld.shuffle_ganon:
+ connect_two_way(multiworld, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player)
+ connect_two_way(multiworld, 'Inverted Pyramid Entrance', 'Pyramid Exit', player)
+ connect_entrance(multiworld, 'Inverted Pyramid Hole', 'Pyramid', player)
else:
entrances.append('Inverted Ganons Tower')
caves.extend(['Inverted Ganons Tower Exit', 'Pyramid Exit'])
@@ -1155,13 +1156,13 @@ def link_inverted_entrances(world, player):
hole_targets.append('Pyramid')
doors.extend(['Inverted Ganons Tower', 'Inverted Pyramid Entrance'])
- world.random.shuffle(hole_entrances)
- world.random.shuffle(hole_targets)
- world.random.shuffle(entrances)
+ multiworld.random.shuffle(hole_entrances)
+ multiworld.random.shuffle(hole_targets)
+ multiworld.random.shuffle(entrances)
# fill up holes
for hole in hole_entrances:
- connect_entrance(world, hole, hole_targets.pop(), player)
+ connect_entrance(multiworld, hole, hole_targets.pop(), player)
doors.append('Hyrule Castle Entrance (South)')
caves.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'))
@@ -1169,8 +1170,8 @@ def link_inverted_entrances(world, player):
# place links house and dark sanc
links_house_doors = [i for i in entrances + entrances_must_exits if
i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors]
- links_house = world.random.choice(list(links_house_doors))
- connect_two_way(world, links_house, 'Inverted Links House Exit', player)
+ links_house = multiworld.random.choice(list(links_house_doors))
+ connect_two_way(multiworld, links_house, 'Inverted Links House Exit', player)
if links_house in entrances:
entrances.remove(links_house)
elif links_house in entrances_must_exits:
@@ -1178,15 +1179,15 @@ def link_inverted_entrances(world, player):
doors.remove(links_house)
sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in entrances]
- sanc_door = world.random.choice(sanc_doors)
+ sanc_door = multiworld.random.choice(sanc_doors)
entrances.remove(sanc_door)
doors.remove(sanc_door)
- connect_entrance(world, sanc_door, 'Inverted Dark Sanctuary', player)
- world.get_entrance('Inverted Dark Sanctuary Exit', player).connect(world.get_entrance(sanc_door, player).parent_region)
+ connect_entrance(multiworld, sanc_door, 'Inverted Dark Sanctuary', player)
+ multiworld.get_entrance('Inverted Dark Sanctuary Exit', player).connect(multiworld.get_entrance(sanc_door, player).parent_region)
# now let's deal with mandatory reachable stuff
def extract_reachable_exit(cavelist):
- world.random.shuffle(cavelist)
+ multiworld.random.shuffle(cavelist)
candidate = None
for cave in cavelist:
if isinstance(cave, tuple) and len(cave) > 1:
@@ -1206,9 +1207,9 @@ def connect_reachable_exit(entrance, caves, doors):
exit = cave[-1]
cave = cave[:-1]
- connect_exit(world, exit, entrance, player)
- connect_entrance(world, doors.pop(), exit, player)
- # rest of cave now is forced to be in this world
+ connect_exit(multiworld, exit, entrance, player)
+ connect_entrance(multiworld, doors.pop(), exit, player)
+ # rest of cave now is forced to be in this multiworld
caves.append(cave)
# connect mandatory exits
@@ -1218,26 +1219,26 @@ def connect_reachable_exit(entrance, caves, doors):
# place old man, has limited options
# exit has to come from specific set of doors, the entrance is free to move about
old_man_entrances = [entrance for entrance in old_man_entrances if entrance in entrances]
- world.random.shuffle(old_man_entrances)
+ multiworld.random.shuffle(old_man_entrances)
old_man_exit = old_man_entrances.pop()
entrances.remove(old_man_exit)
- connect_exit(world, 'Old Man Cave Exit (East)', old_man_exit, player)
- connect_entrance(world, doors.pop(), 'Old Man Cave Exit (East)', player)
+ connect_exit(multiworld, 'Old Man Cave Exit (East)', old_man_exit, player)
+ connect_entrance(multiworld, doors.pop(), 'Old Man Cave Exit (East)', player)
caves.append('Old Man Cave Exit (West)')
# place blacksmith, has limited options
blacksmith_doors = [door for door in blacksmith_doors if door in doors]
- world.random.shuffle(blacksmith_doors)
+ multiworld.random.shuffle(blacksmith_doors)
blacksmith_hut = blacksmith_doors.pop()
- connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player)
+ connect_entrance(multiworld, blacksmith_hut, 'Blacksmiths Hut', player)
doors.remove(blacksmith_hut)
# place dam and pyramid fairy, have limited options
bomb_shop_doors = [door for door in bomb_shop_doors if door in doors]
- world.random.shuffle(bomb_shop_doors)
+ multiworld.random.shuffle(bomb_shop_doors)
bomb_shop = bomb_shop_doors.pop()
- connect_entrance(world, bomb_shop, 'Inverted Big Bomb Shop', player)
+ connect_entrance(multiworld, bomb_shop, 'Inverted Big Bomb Shop', player)
doors.remove(bomb_shop)
# handle remaining caves
@@ -1246,49 +1247,49 @@ def connect_reachable_exit(entrance, caves, doors):
cave = (cave,)
for exit in cave:
- connect_exit(world, exit, entrances.pop(), player)
- connect_entrance(world, doors.pop(), exit, player)
+ connect_exit(multiworld, exit, entrances.pop(), player)
+ connect_entrance(multiworld, doors.pop(), exit, player)
# place remaining doors
- connect_doors(world, doors, door_targets, player)
+ connect_doors(multiworld, doors, door_targets, player)
else:
raise NotImplementedError('Shuffling not supported yet')
- if world.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
- overworld_glitch_connections(world, player)
+ if multiworld.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
+ overworld_glitch_connections(multiworld, player)
# mandatory hybrid major glitches connections
- if world.worlds[player].options.glitches_required in ['hybrid_major_glitches', 'no_logic']:
- underworld_glitch_connections(world, player)
+ if multiworld.worlds[player].options.glitches_required in ['hybrid_major_glitches', 'no_logic']:
+ underworld_glitch_connections(multiworld, player)
# patch swamp drain
- if world.get_entrance('Dam', player).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)':
- world.worlds[player].swamp_patch_required = True
+ if multiworld.get_entrance('Dam', player).connected_region.name != 'Dam' or multiworld.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)':
+ multiworld.worlds[player].swamp_patch_required = True
# check for potion shop location
- if world.get_entrance('Potion Shop', player).connected_region.name != 'Potion Shop':
- world.worlds[player].powder_patch_required = True
+ if multiworld.get_entrance('Potion Shop', player).connected_region.name != 'Potion Shop':
+ multiworld.worlds[player].powder_patch_required = True
# check for ganon location
- if world.get_entrance('Inverted Pyramid Hole', player).connected_region.name != 'Pyramid':
- world.worlds[player].ganon_at_pyramid = False
+ if multiworld.get_entrance('Inverted Pyramid Hole', player).connected_region.name != 'Pyramid':
+ multiworld.worlds[player].ganon_at_pyramid = False
# check for Ganon's Tower location
- if world.get_entrance('Inverted Ganons Tower', player).connected_region.name != 'Ganons Tower (Entrance)':
- world.worlds[player].ganonstower_vanilla = False
+ if multiworld.get_entrance('Inverted Ganons Tower', player).connected_region.name != 'Ganons Tower (Entrance)':
+ multiworld.worlds[player].ganonstower_vanilla = False
-def connect_simple(world, exitname, regionname, player):
- world.get_entrance(exitname, player).connect(world.get_region(regionname, player))
+def connect_simple(multiworld: MultiWorld, exitname: str, regionname: str, player: int):
+ multiworld.get_entrance(exitname, player).connect(multiworld.get_region(regionname, player))
-def connect_entrance(world, entrancename: str, exitname: str, player: int):
- entrance = world.get_entrance(entrancename, player)
+def connect_entrance(multiworld: MultiWorld, entrancename: str, exitname: str, player: int):
+ entrance = multiworld.get_entrance(entrancename, player)
# check if we got an entrance or a region to connect to
try:
- region = world.get_region(exitname, player)
+ region = multiworld.get_region(exitname, player)
exit = None
except KeyError:
- exit = world.get_entrance(exitname, player)
+ exit = multiworld.get_entrance(exitname, player)
region = exit.parent_region
# if this was already connected somewhere, remove the backreference
@@ -1299,24 +1300,24 @@ def connect_entrance(world, entrancename: str, exitname: str, player: int):
addresses = door_addresses[entrance.name][0]
entrance.connect(region, addresses, target)
- world.spoiler.set_entrance(entrance.name, exit.name if exit is not None else region.name, 'entrance', player)
+ multiworld.spoiler.set_entrance(entrance.name, exit.name if exit is not None else region.name, 'entrance', player)
-def connect_exit(world, exitname, entrancename, player):
- entrance = world.get_entrance(entrancename, player)
- exit = world.get_entrance(exitname, player)
+def connect_exit(multiworld: MultiWorld, exitname: str, entrancename: str, player: int):
+ entrance = multiworld.get_entrance(entrancename, player)
+ exit = multiworld.get_entrance(exitname, player)
# if this was already connected somewhere, remove the backreference
if exit.connected_region is not None:
exit.connected_region.entrances.remove(exit)
exit.connect(entrance.parent_region, door_addresses[entrance.name][1], exit_ids[exit.name][1])
- world.spoiler.set_entrance(entrance.name, exit.name, 'exit', player)
+ multiworld.spoiler.set_entrance(entrance.name, exit.name, 'exit', player)
-def connect_two_way(world, entrancename, exitname, player):
- entrance = world.get_entrance(entrancename, player)
- exit = world.get_entrance(exitname, player)
+def connect_two_way(multiworld: MultiWorld, entrancename: str, exitname: str, player: int):
+ entrance = multiworld.get_entrance(entrancename, player)
+ exit = multiworld.get_entrance(exitname, player)
# if these were already connected somewhere, remove the backreference
if entrance.connected_region is not None:
@@ -1326,10 +1327,10 @@ def connect_two_way(world, entrancename, exitname, player):
entrance.connect(exit.parent_region, door_addresses[entrance.name][0], exit_ids[exit.name][0])
exit.connect(entrance.parent_region, door_addresses[entrance.name][1], exit_ids[exit.name][1])
- world.spoiler.set_entrance(entrance.name, exit.name, 'both', player)
+ multiworld.spoiler.set_entrance(entrance.name, exit.name, 'both', player)
-def scramble_holes(world, player):
+def scramble_holes(multiworld: MultiWorld, player: int):
hole_entrances = [('Kakariko Well Cave', 'Kakariko Well Drop'),
('Bat Cave Cave', 'Bat Cave Drop'),
('North Fairy Cave', 'North Fairy Cave Drop'),
@@ -1343,39 +1344,39 @@ def scramble_holes(world, player):
('Lost Woods Hideout Exit', 'Lost Woods Hideout (top)'),
('Lumberjack Tree Exit', 'Lumberjack Tree (top)')]
- if not world.shuffle_ganon:
- connect_two_way(world, 'Pyramid Entrance', 'Pyramid Exit', player)
- connect_entrance(world, 'Pyramid Hole', 'Pyramid', player)
+ if not multiworld.shuffle_ganon:
+ connect_two_way(multiworld, 'Pyramid Entrance', 'Pyramid Exit', player)
+ connect_entrance(multiworld, 'Pyramid Hole', 'Pyramid', player)
else:
hole_targets.append(('Pyramid Exit', 'Pyramid'))
- if world.worlds[player].options.mode == 'standard':
+ if multiworld.worlds[player].options.mode == 'standard':
# cannot move uncle cave
- connect_two_way(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player)
- connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player)
+ connect_two_way(multiworld, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player)
+ connect_entrance(multiworld, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player)
else:
hole_entrances.append(('Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Drop'))
hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance'))
# do not shuffle sanctuary into pyramid hole unless shuffle is crossed
- if world.worlds[player].options.entrance_shuffle == 'crossed':
+ if multiworld.worlds[player].options.entrance_shuffle == 'crossed':
hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
- if world.shuffle_ganon:
- world.random.shuffle(hole_targets)
+ if multiworld.shuffle_ganon:
+ multiworld.random.shuffle(hole_targets)
exit, target = hole_targets.pop()
- connect_two_way(world, 'Pyramid Entrance', exit, player)
- connect_entrance(world, 'Pyramid Hole', target, player)
- if world.worlds[player].options.entrance_shuffle != 'crossed':
+ connect_two_way(multiworld, 'Pyramid Entrance', exit, player)
+ connect_entrance(multiworld, 'Pyramid Hole', target, player)
+ if multiworld.worlds[player].options.entrance_shuffle != 'crossed':
hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
- world.random.shuffle(hole_targets)
+ multiworld.random.shuffle(hole_targets)
for entrance, drop in hole_entrances:
exit, target = hole_targets.pop()
- connect_two_way(world, entrance, exit, player)
- connect_entrance(world, drop, target, player)
+ connect_two_way(multiworld, entrance, exit, player)
+ connect_entrance(multiworld, drop, target, player)
-def scramble_inverted_holes(world, player):
+def scramble_inverted_holes(multiworld: MultiWorld, player: int):
hole_entrances = [('Kakariko Well Cave', 'Kakariko Well Drop'),
('Bat Cave Cave', 'Bat Cave Drop'),
('North Fairy Cave', 'North Fairy Cave Drop'),
@@ -1389,9 +1390,9 @@ def scramble_inverted_holes(world, player):
('Lost Woods Hideout Exit', 'Lost Woods Hideout (top)'),
('Lumberjack Tree Exit', 'Lumberjack Tree (top)')]
- if not world.shuffle_ganon:
- connect_two_way(world, 'Inverted Pyramid Entrance', 'Pyramid Exit', player)
- connect_entrance(world, 'Inverted Pyramid Hole', 'Pyramid', player)
+ if not multiworld.shuffle_ganon:
+ connect_two_way(multiworld, 'Inverted Pyramid Entrance', 'Pyramid Exit', player)
+ connect_entrance(multiworld, 'Inverted Pyramid Hole', 'Pyramid', player)
else:
hole_targets.append(('Pyramid Exit', 'Pyramid'))
@@ -1400,58 +1401,59 @@ def scramble_inverted_holes(world, player):
hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance'))
# do not shuffle sanctuary into pyramid hole unless shuffle is crossed
- if world.worlds[player].options.entrance_shuffle == 'crossed':
+ if multiworld.worlds[player].options.entrance_shuffle == 'crossed':
hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
- if world.shuffle_ganon:
- world.random.shuffle(hole_targets)
+ if multiworld.shuffle_ganon:
+ multiworld.random.shuffle(hole_targets)
exit, target = hole_targets.pop()
- connect_two_way(world, 'Inverted Pyramid Entrance', exit, player)
- connect_entrance(world, 'Inverted Pyramid Hole', target, player)
- if world.worlds[player].options.entrance_shuffle != 'crossed':
+ connect_two_way(multiworld, 'Inverted Pyramid Entrance', exit, player)
+ connect_entrance(multiworld, 'Inverted Pyramid Hole', target, player)
+ if multiworld.worlds[player].options.entrance_shuffle != 'crossed':
hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
- world.random.shuffle(hole_targets)
+ multiworld.random.shuffle(hole_targets)
for entrance, drop in hole_entrances:
exit, target = hole_targets.pop()
- connect_two_way(world, entrance, exit, player)
- connect_entrance(world, drop, target, player)
+ connect_two_way(multiworld, entrance, exit, player)
+ connect_entrance(multiworld, drop, target, player)
-def connect_random(world, exitlist, targetlist, player, two_way=False):
+def connect_random(multiworld: MultiWorld, exitlist: list[str], targetlist: list[str],
+ player: int, two_way: bool = False):
targetlist = list(targetlist)
- world.random.shuffle(targetlist)
+ multiworld.random.shuffle(targetlist)
for exit, target in zip(exitlist, targetlist):
if two_way:
- connect_two_way(world, exit, target, player)
+ connect_two_way(multiworld, exit, target, player)
else:
- connect_entrance(world, exit, target, player)
+ connect_entrance(multiworld, exit, target, player)
-def connect_mandatory_exits(world, entrances, caves, must_be_exits, player):
+def connect_mandatory_exits(multiworld: MultiWorld, entrances: list[str], caves: list[tuple[str, str]], must_be_exits: list[str], player):
# Keeps track of entrances that cannot be used to access each exit / cave
- if world.worlds[player].options.mode == 'inverted':
+ if multiworld.worlds[player].options.mode == 'inverted':
invalid_connections = Inverted_Must_Exit_Invalid_Connections.copy()
else:
invalid_connections = Must_Exit_Invalid_Connections.copy()
invalid_cave_connections = defaultdict(set)
- if world.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
+ if multiworld.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
from . import OverworldGlitchRules
- for entrance in OverworldGlitchRules.get_non_mandatory_exits(world.worlds[player].options.mode == 'inverted'):
+ for entrance in OverworldGlitchRules.get_non_mandatory_exits(multiworld.worlds[player].options.mode == 'inverted'):
invalid_connections[entrance] = set()
if entrance in must_be_exits:
must_be_exits.remove(entrance)
entrances.append(entrance)
"""This works inplace"""
- world.random.shuffle(entrances)
- world.random.shuffle(caves)
+ multiworld.random.shuffle(entrances)
+ multiworld.random.shuffle(caves)
# Handle inverted Aga Tower - if it depends on connections, then so does Hyrule Castle Ledge
- if world.worlds[player].options.mode == 'inverted':
+ if multiworld.worlds[player].options.mode == 'inverted':
for entrance in invalid_connections:
- if world.get_entrance(entrance, player).connected_region == world.get_region('Inverted Agahnims Tower',
+ if multiworld.get_entrance(entrance, player).connected_region == multiworld.get_region('Inverted Agahnims Tower',
player):
for exit in invalid_connections[entrance]:
invalid_connections[exit] = invalid_connections[exit].union(
@@ -1473,11 +1475,11 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player):
raise KeyError('No more caves left. Should not happen!')
# all caves are sorted so that the last exit is always reachable
- connect_two_way(world, exit, cave[-1], player)
+ connect_two_way(multiworld, exit, cave[-1], player)
if len(cave) == 2:
entrance = next(e for e in entrances[::-1] if e not in invalid_connections[exit] and e not in invalid_cave_connections[tuple(cave)])
entrances.remove(entrance)
- connect_two_way(world, entrance, cave[0], player)
+ connect_two_way(multiworld, entrance, cave[0], player)
if cave in used_caves:
required_entrances -= 2
used_caves.remove(cave)
@@ -1490,7 +1492,7 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player):
entrance = next(e for e in entrances[::-1] if e not in invalid_connections[exit])
cave_entrances.append(entrance)
entrances.remove(entrance)
- connect_two_way(world, entrance, cave_exit, player)
+ connect_two_way(multiworld, entrance, cave_exit, player)
if entrance not in invalid_connections:
invalid_connections[exit] = set()
if all(entrance in invalid_connections for entrance in cave_entrances):
@@ -1504,7 +1506,7 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player):
else:
required_entrances += len(cave)-1
caves.append(cave[0:-1])
- world.random.shuffle(caves)
+ multiworld.random.shuffle(caves)
used_caves.append(cave[0:-1])
invalid_cave_connections[tuple(cave[0:-1])] = invalid_cave_connections[tuple(cave)].union(invalid_connections[exit])
caves.remove(cave)
@@ -1514,19 +1516,20 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player):
entrance = next(e for e in entrances[::-1] if e not in invalid_cave_connections[tuple(cave)])
invalid_cave_connections[tuple(cave)] = set()
entrances.remove(entrance)
- connect_two_way(world, entrance, cave_exit, player)
+ connect_two_way(multiworld, entrance, cave_exit, player)
caves.remove(cave)
-def connect_caves(world, lw_entrances, dw_entrances, caves, player):
+def connect_caves(multiworld: MultiWorld, lw_entrances: list[str], dw_entrances: list[str],
+ caves: list[tuple[str, str]], player: int):
"""This works inplace"""
- world.random.shuffle(lw_entrances)
- world.random.shuffle(dw_entrances)
- world.random.shuffle(caves)
+ multiworld.random.shuffle(lw_entrances)
+ multiworld.random.shuffle(dw_entrances)
+ multiworld.random.shuffle(caves)
# connect highest exit count caves first, prevent issue where we have 2 or 3 exits accross worlds left to fill
caves.sort(key=lambda cave: 1 if isinstance(cave, str) else len(cave), reverse=True)
for cave in caves:
- target = lw_entrances if world.random.randint(0, 1) else dw_entrances
+ target = lw_entrances if multiworld.random.randint(0, 1) else dw_entrances
if isinstance(cave, str):
cave = (cave,)
@@ -1536,37 +1539,37 @@ def connect_caves(world, lw_entrances, dw_entrances, caves, player):
target = lw_entrances if target is dw_entrances else dw_entrances
for exit in cave:
- connect_two_way(world, target.pop(), exit, player)
+ connect_two_way(multiworld, target.pop(), exit, player)
caves.clear() # emulating old behaviour of popping caves from the list in-place
-def connect_doors(world, doors, targets, player):
+def connect_doors(multiworld: MultiWorld, doors: list[str], targets: list[str], player: int):
"""This works inplace"""
- world.random.shuffle(doors)
- world.random.shuffle(targets)
+ multiworld.random.shuffle(doors)
+ multiworld.random.shuffle(targets)
placing = min(len(doors), len(targets))
for door, target in zip(doors, targets):
- connect_entrance(world, door, target, player)
+ connect_entrance(multiworld, door, target, player)
doors[:] = doors[placing:]
targets[:] = targets[placing:]
-def skull_woods_shuffle(world, player):
- connect_random(world, ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'],
+def skull_woods_shuffle(multiworld: MultiWorld, player: int):
+ connect_random(multiworld, ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'],
['Skull Woods First Section (Left)', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Top)', 'Skull Woods Second Section (Drop)'], player)
- connect_random(world, ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)'],
+ connect_random(multiworld, ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)'],
['Skull Woods First Section Exit', 'Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)'], player, True)
-def simple_shuffle_dungeons(world, player):
- skull_woods_shuffle(world, player)
+def simple_shuffle_dungeons(multiworld: MultiWorld, player: int):
+ skull_woods_shuffle(multiworld, player)
dungeon_entrances = ['Eastern Palace', 'Tower of Hera', 'Thieves Town', 'Skull Woods Final Section', 'Palace of Darkness', 'Ice Palace', 'Misery Mire', 'Swamp Palace']
dungeon_exits = ['Eastern Palace Exit', 'Tower of Hera Exit', 'Thieves Town Exit', 'Skull Woods Final Section Exit', 'Palace of Darkness Exit', 'Ice Palace Exit', 'Misery Mire Exit', 'Swamp Palace Exit']
- if world.worlds[player].options.mode != 'inverted':
- if not world.shuffle_ganon:
- connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player)
+ if multiworld.worlds[player].options.mode != 'inverted':
+ if not multiworld.shuffle_ganon:
+ connect_two_way(multiworld, 'Ganons Tower', 'Ganons Tower Exit', player)
else:
dungeon_entrances.append('Ganons Tower')
dungeon_exits.append('Ganons Tower Exit')
@@ -1575,17 +1578,17 @@ def simple_shuffle_dungeons(world, player):
dungeon_exits.append('Inverted Agahnims Tower Exit')
# shuffle up single entrance dungeons
- connect_random(world, dungeon_entrances, dungeon_exits, player, True)
+ connect_random(multiworld, dungeon_entrances, dungeon_exits, player, True)
# mix up 4 door dungeons
multi_dungeons = ['Desert', 'Turtle Rock']
- if world.worlds[player].options.mode == 'open' or (world.worlds[player].options.mode == 'inverted' and world.shuffle_ganon):
+ if multiworld.worlds[player].options.mode == 'open' or (multiworld.worlds[player].options.mode == 'inverted' and multiworld.shuffle_ganon):
multi_dungeons.append('Hyrule Castle')
- world.random.shuffle(multi_dungeons)
+ multiworld.random.shuffle(multi_dungeons)
dp_target = multi_dungeons[0]
tr_target = multi_dungeons[1]
- if world.worlds[player].options.mode not in ['open', 'inverted'] or (world.worlds[player].options.mode == 'inverted' and world.shuffle_ganon is False):
+ if multiworld.worlds[player].options.mode not in ['open', 'inverted'] or (multiworld.worlds[player].options.mode == 'inverted' and multiworld.shuffle_ganon is False):
# place hyrule castle as intended
hc_target = 'Hyrule Castle'
else:
@@ -1593,152 +1596,152 @@ def simple_shuffle_dungeons(world, player):
# ToDo improve this?
- if world.worlds[player].options.mode != 'inverted':
+ if multiworld.worlds[player].options.mode != 'inverted':
if hc_target == 'Hyrule Castle':
- connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
- connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Exit (East)', player)
- connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Hyrule Castle Exit (West)', player)
- connect_two_way(world, 'Agahnims Tower', 'Agahnims Tower Exit', player)
+ connect_two_way(multiworld, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
+ connect_two_way(multiworld, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Exit (East)', player)
+ connect_two_way(multiworld, 'Hyrule Castle Entrance (West)', 'Hyrule Castle Exit (West)', player)
+ connect_two_way(multiworld, 'Agahnims Tower', 'Agahnims Tower Exit', player)
elif hc_target == 'Desert':
- connect_two_way(world, 'Desert Palace Entrance (South)', 'Hyrule Castle Exit (South)', player)
- connect_two_way(world, 'Desert Palace Entrance (East)', 'Hyrule Castle Exit (East)', player)
- connect_two_way(world, 'Desert Palace Entrance (West)', 'Hyrule Castle Exit (West)', player)
- connect_two_way(world, 'Desert Palace Entrance (North)', 'Agahnims Tower Exit', player)
+ connect_two_way(multiworld, 'Desert Palace Entrance (South)', 'Hyrule Castle Exit (South)', player)
+ connect_two_way(multiworld, 'Desert Palace Entrance (East)', 'Hyrule Castle Exit (East)', player)
+ connect_two_way(multiworld, 'Desert Palace Entrance (West)', 'Hyrule Castle Exit (West)', player)
+ connect_two_way(multiworld, 'Desert Palace Entrance (North)', 'Agahnims Tower Exit', player)
elif hc_target == 'Turtle Rock':
- connect_two_way(world, 'Turtle Rock', 'Hyrule Castle Exit (South)', player)
- connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Hyrule Castle Exit (East)', player)
- connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Hyrule Castle Exit (West)', player)
- connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Agahnims Tower Exit', player)
+ connect_two_way(multiworld, 'Turtle Rock', 'Hyrule Castle Exit (South)', player)
+ connect_two_way(multiworld, 'Turtle Rock Isolated Ledge Entrance', 'Hyrule Castle Exit (East)', player)
+ connect_two_way(multiworld, 'Dark Death Mountain Ledge (West)', 'Hyrule Castle Exit (West)', player)
+ connect_two_way(multiworld, 'Dark Death Mountain Ledge (East)', 'Agahnims Tower Exit', player)
if dp_target == 'Hyrule Castle':
- connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Desert Palace Exit (South)', player)
- connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Desert Palace Exit (East)', player)
- connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Desert Palace Exit (West)', player)
- connect_two_way(world, 'Agahnims Tower', 'Desert Palace Exit (North)', player)
+ connect_two_way(multiworld, 'Hyrule Castle Entrance (South)', 'Desert Palace Exit (South)', player)
+ connect_two_way(multiworld, 'Hyrule Castle Entrance (East)', 'Desert Palace Exit (East)', player)
+ connect_two_way(multiworld, 'Hyrule Castle Entrance (West)', 'Desert Palace Exit (West)', player)
+ connect_two_way(multiworld, 'Agahnims Tower', 'Desert Palace Exit (North)', player)
elif dp_target == 'Desert':
- connect_two_way(world, 'Desert Palace Entrance (South)', 'Desert Palace Exit (South)', player)
- connect_two_way(world, 'Desert Palace Entrance (East)', 'Desert Palace Exit (East)', player)
- connect_two_way(world, 'Desert Palace Entrance (West)', 'Desert Palace Exit (West)', player)
- connect_two_way(world, 'Desert Palace Entrance (North)', 'Desert Palace Exit (North)', player)
+ connect_two_way(multiworld, 'Desert Palace Entrance (South)', 'Desert Palace Exit (South)', player)
+ connect_two_way(multiworld, 'Desert Palace Entrance (East)', 'Desert Palace Exit (East)', player)
+ connect_two_way(multiworld, 'Desert Palace Entrance (West)', 'Desert Palace Exit (West)', player)
+ connect_two_way(multiworld, 'Desert Palace Entrance (North)', 'Desert Palace Exit (North)', player)
elif dp_target == 'Turtle Rock':
- connect_two_way(world, 'Turtle Rock', 'Desert Palace Exit (South)', player)
- connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Desert Palace Exit (East)', player)
- connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Desert Palace Exit (West)', player)
- connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Desert Palace Exit (North)', player)
+ connect_two_way(multiworld, 'Turtle Rock', 'Desert Palace Exit (South)', player)
+ connect_two_way(multiworld, 'Turtle Rock Isolated Ledge Entrance', 'Desert Palace Exit (East)', player)
+ connect_two_way(multiworld, 'Dark Death Mountain Ledge (West)', 'Desert Palace Exit (West)', player)
+ connect_two_way(multiworld, 'Dark Death Mountain Ledge (East)', 'Desert Palace Exit (North)', player)
if tr_target == 'Hyrule Castle':
- connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Turtle Rock Exit (Front)', player)
- connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Turtle Rock Ledge Exit (East)', player)
- connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Turtle Rock Ledge Exit (West)', player)
- connect_two_way(world, 'Agahnims Tower', 'Turtle Rock Isolated Ledge Exit', player)
+ connect_two_way(multiworld, 'Hyrule Castle Entrance (South)', 'Turtle Rock Exit (Front)', player)
+ connect_two_way(multiworld, 'Hyrule Castle Entrance (East)', 'Turtle Rock Ledge Exit (East)', player)
+ connect_two_way(multiworld, 'Hyrule Castle Entrance (West)', 'Turtle Rock Ledge Exit (West)', player)
+ connect_two_way(multiworld, 'Agahnims Tower', 'Turtle Rock Isolated Ledge Exit', player)
elif tr_target == 'Desert':
- connect_two_way(world, 'Desert Palace Entrance (South)', 'Turtle Rock Exit (Front)', player)
- connect_two_way(world, 'Desert Palace Entrance (North)', 'Turtle Rock Ledge Exit (East)', player)
- connect_two_way(world, 'Desert Palace Entrance (West)', 'Turtle Rock Ledge Exit (West)', player)
- connect_two_way(world, 'Desert Palace Entrance (East)', 'Turtle Rock Isolated Ledge Exit', player)
+ connect_two_way(multiworld, 'Desert Palace Entrance (South)', 'Turtle Rock Exit (Front)', player)
+ connect_two_way(multiworld, 'Desert Palace Entrance (North)', 'Turtle Rock Ledge Exit (East)', player)
+ connect_two_way(multiworld, 'Desert Palace Entrance (West)', 'Turtle Rock Ledge Exit (West)', player)
+ connect_two_way(multiworld, 'Desert Palace Entrance (East)', 'Turtle Rock Isolated Ledge Exit', player)
elif tr_target == 'Turtle Rock':
- connect_two_way(world, 'Turtle Rock', 'Turtle Rock Exit (Front)', player)
- connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Turtle Rock Isolated Ledge Exit', player)
- connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Turtle Rock Ledge Exit (West)', player)
- connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Turtle Rock Ledge Exit (East)', player)
+ connect_two_way(multiworld, 'Turtle Rock', 'Turtle Rock Exit (Front)', player)
+ connect_two_way(multiworld, 'Turtle Rock Isolated Ledge Entrance', 'Turtle Rock Isolated Ledge Exit', player)
+ connect_two_way(multiworld, 'Dark Death Mountain Ledge (West)', 'Turtle Rock Ledge Exit (West)', player)
+ connect_two_way(multiworld, 'Dark Death Mountain Ledge (East)', 'Turtle Rock Ledge Exit (East)', player)
else:
if hc_target == 'Hyrule Castle':
- connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
- connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Exit (East)', player)
- connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Hyrule Castle Exit (West)', player)
- connect_two_way(world, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player)
+ connect_two_way(multiworld, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
+ connect_two_way(multiworld, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Exit (East)', player)
+ connect_two_way(multiworld, 'Hyrule Castle Entrance (West)', 'Hyrule Castle Exit (West)', player)
+ connect_two_way(multiworld, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player)
elif hc_target == 'Desert':
- connect_two_way(world, 'Desert Palace Entrance (South)', 'Hyrule Castle Exit (South)', player)
- connect_two_way(world, 'Desert Palace Entrance (East)', 'Hyrule Castle Exit (East)', player)
- connect_two_way(world, 'Desert Palace Entrance (West)', 'Hyrule Castle Exit (West)', player)
- connect_two_way(world, 'Desert Palace Entrance (North)', 'Inverted Ganons Tower Exit', player)
+ connect_two_way(multiworld, 'Desert Palace Entrance (South)', 'Hyrule Castle Exit (South)', player)
+ connect_two_way(multiworld, 'Desert Palace Entrance (East)', 'Hyrule Castle Exit (East)', player)
+ connect_two_way(multiworld, 'Desert Palace Entrance (West)', 'Hyrule Castle Exit (West)', player)
+ connect_two_way(multiworld, 'Desert Palace Entrance (North)', 'Inverted Ganons Tower Exit', player)
elif hc_target == 'Turtle Rock':
- connect_two_way(world, 'Turtle Rock', 'Hyrule Castle Exit (South)', player)
- connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Inverted Ganons Tower Exit', player)
- connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Hyrule Castle Exit (West)', player)
- connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Hyrule Castle Exit (East)', player)
+ connect_two_way(multiworld, 'Turtle Rock', 'Hyrule Castle Exit (South)', player)
+ connect_two_way(multiworld, 'Turtle Rock Isolated Ledge Entrance', 'Inverted Ganons Tower Exit', player)
+ connect_two_way(multiworld, 'Dark Death Mountain Ledge (West)', 'Hyrule Castle Exit (West)', player)
+ connect_two_way(multiworld, 'Dark Death Mountain Ledge (East)', 'Hyrule Castle Exit (East)', player)
if dp_target == 'Hyrule Castle':
- connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Desert Palace Exit (South)', player)
- connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Desert Palace Exit (East)', player)
- connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Desert Palace Exit (West)', player)
- connect_two_way(world, 'Inverted Ganons Tower', 'Desert Palace Exit (North)', player)
+ connect_two_way(multiworld, 'Hyrule Castle Entrance (South)', 'Desert Palace Exit (South)', player)
+ connect_two_way(multiworld, 'Hyrule Castle Entrance (East)', 'Desert Palace Exit (East)', player)
+ connect_two_way(multiworld, 'Hyrule Castle Entrance (West)', 'Desert Palace Exit (West)', player)
+ connect_two_way(multiworld, 'Inverted Ganons Tower', 'Desert Palace Exit (North)', player)
elif dp_target == 'Desert':
- connect_two_way(world, 'Desert Palace Entrance (South)', 'Desert Palace Exit (South)', player)
- connect_two_way(world, 'Desert Palace Entrance (East)', 'Desert Palace Exit (East)', player)
- connect_two_way(world, 'Desert Palace Entrance (West)', 'Desert Palace Exit (West)', player)
- connect_two_way(world, 'Desert Palace Entrance (North)', 'Desert Palace Exit (North)', player)
+ connect_two_way(multiworld, 'Desert Palace Entrance (South)', 'Desert Palace Exit (South)', player)
+ connect_two_way(multiworld, 'Desert Palace Entrance (East)', 'Desert Palace Exit (East)', player)
+ connect_two_way(multiworld, 'Desert Palace Entrance (West)', 'Desert Palace Exit (West)', player)
+ connect_two_way(multiworld, 'Desert Palace Entrance (North)', 'Desert Palace Exit (North)', player)
elif dp_target == 'Turtle Rock':
- connect_two_way(world, 'Turtle Rock', 'Desert Palace Exit (South)', player)
- connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Desert Palace Exit (East)', player)
- connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Desert Palace Exit (West)', player)
- connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Desert Palace Exit (North)', player)
+ connect_two_way(multiworld, 'Turtle Rock', 'Desert Palace Exit (South)', player)
+ connect_two_way(multiworld, 'Turtle Rock Isolated Ledge Entrance', 'Desert Palace Exit (East)', player)
+ connect_two_way(multiworld, 'Dark Death Mountain Ledge (West)', 'Desert Palace Exit (West)', player)
+ connect_two_way(multiworld, 'Dark Death Mountain Ledge (East)', 'Desert Palace Exit (North)', player)
if tr_target == 'Hyrule Castle':
- connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Turtle Rock Exit (Front)', player)
- connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Turtle Rock Ledge Exit (East)', player)
- connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Turtle Rock Ledge Exit (West)', player)
- connect_two_way(world, 'Inverted Ganons Tower', 'Turtle Rock Isolated Ledge Exit', player)
+ connect_two_way(multiworld, 'Hyrule Castle Entrance (South)', 'Turtle Rock Exit (Front)', player)
+ connect_two_way(multiworld, 'Hyrule Castle Entrance (East)', 'Turtle Rock Ledge Exit (East)', player)
+ connect_two_way(multiworld, 'Hyrule Castle Entrance (West)', 'Turtle Rock Ledge Exit (West)', player)
+ connect_two_way(multiworld, 'Inverted Ganons Tower', 'Turtle Rock Isolated Ledge Exit', player)
elif tr_target == 'Desert':
- connect_two_way(world, 'Desert Palace Entrance (South)', 'Turtle Rock Exit (Front)', player)
- connect_two_way(world, 'Desert Palace Entrance (North)', 'Turtle Rock Ledge Exit (East)', player)
- connect_two_way(world, 'Desert Palace Entrance (West)', 'Turtle Rock Ledge Exit (West)', player)
- connect_two_way(world, 'Desert Palace Entrance (East)', 'Turtle Rock Isolated Ledge Exit', player)
+ connect_two_way(multiworld, 'Desert Palace Entrance (South)', 'Turtle Rock Exit (Front)', player)
+ connect_two_way(multiworld, 'Desert Palace Entrance (North)', 'Turtle Rock Ledge Exit (East)', player)
+ connect_two_way(multiworld, 'Desert Palace Entrance (West)', 'Turtle Rock Ledge Exit (West)', player)
+ connect_two_way(multiworld, 'Desert Palace Entrance (East)', 'Turtle Rock Isolated Ledge Exit', player)
elif tr_target == 'Turtle Rock':
- connect_two_way(world, 'Turtle Rock', 'Turtle Rock Exit (Front)', player)
- connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Turtle Rock Isolated Ledge Exit', player)
- connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Turtle Rock Ledge Exit (West)', player)
- connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Turtle Rock Ledge Exit (East)', player)
+ connect_two_way(multiworld, 'Turtle Rock', 'Turtle Rock Exit (Front)', player)
+ connect_two_way(multiworld, 'Turtle Rock Isolated Ledge Entrance', 'Turtle Rock Isolated Ledge Exit', player)
+ connect_two_way(multiworld, 'Dark Death Mountain Ledge (West)', 'Turtle Rock Ledge Exit (West)', player)
+ connect_two_way(multiworld, 'Dark Death Mountain Ledge (East)', 'Turtle Rock Ledge Exit (East)', player)
-def crossed_shuffle_dungeons(world, player: int):
+def crossed_shuffle_dungeons(multiworld: MultiWorld, player: int):
lw_entrances = LW_Dungeon_Entrances.copy()
dw_entrances = DW_Dungeon_Entrances.copy()
for exitname, regionname in default_connections:
- connect_simple(world, exitname, regionname, player)
+ connect_simple(multiworld, exitname, regionname, player)
- skull_woods_shuffle(world, player)
+ skull_woods_shuffle(multiworld, player)
dungeon_exits = Dungeon_Exits_Base.copy()
dungeon_entrances = lw_entrances+dw_entrances
- if not world.shuffle_ganon:
- connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player)
+ if not multiworld.shuffle_ganon:
+ connect_two_way(multiworld, 'Ganons Tower', 'Ganons Tower Exit', player)
else:
dungeon_entrances.append('Ganons Tower')
dungeon_exits.append('Ganons Tower Exit')
- if world.worlds[player].options.mode == 'standard':
+ if multiworld.worlds[player].options.mode == 'standard':
# must connect front of hyrule castle to do escape
- connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
+ connect_two_way(multiworld, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
else:
dungeon_exits.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'))
dungeon_entrances.append('Hyrule Castle Entrance (South)')
- connect_mandatory_exits(world, dungeon_entrances, dungeon_exits,
+ connect_mandatory_exits(multiworld, dungeon_entrances, dungeon_exits,
LW_Dungeon_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit, player)
- if world.worlds[player].options.mode == 'standard':
- connect_caves(world, dungeon_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player)
+ if multiworld.worlds[player].options.mode == 'standard':
+ connect_caves(multiworld, dungeon_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player)
- connect_caves(world, dungeon_entrances, [], dungeon_exits, player)
+ connect_caves(multiworld, dungeon_entrances, [], dungeon_exits, player)
assert not dungeon_exits , "make sure all exits are accounted for"
-def inverted_crossed_shuffle_dungeons(world, player: int):
+def inverted_crossed_shuffle_dungeons(multiworld: MultiWorld, player: int):
lw_entrances = Inverted_LW_Dungeon_Entrances.copy()
dw_entrances = Inverted_DW_Dungeon_Entrances.copy()
lw_dungeon_entrances_must_exit = list(Inverted_LW_Dungeon_Entrances_Must_Exit)
for exitname, regionname in inverted_default_connections:
- connect_simple(world, exitname, regionname, player)
+ connect_simple(multiworld, exitname, regionname, player)
- skull_woods_shuffle(world, player)
+ skull_woods_shuffle(multiworld, player)
dungeon_exits = Inverted_Dungeon_Exits_Base.copy()
dungeon_entrances = lw_entrances+dw_entrances
# randomize which desert ledge door is a must-exit
- if world.random.randint(0, 1):
+ if multiworld.random.randint(0, 1):
lw_dungeon_entrances_must_exit.append('Desert Palace Entrance (North)')
dungeon_entrances.append('Desert Palace Entrance (West)')
else:
@@ -1748,8 +1751,8 @@ def inverted_crossed_shuffle_dungeons(world, player: int):
dungeon_exits.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'))
dungeon_entrances.append('Hyrule Castle Entrance (South)')
- if not world.shuffle_ganon:
- connect_two_way(world, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player)
+ if not multiworld.shuffle_ganon:
+ connect_two_way(multiworld, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player)
hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)']
else:
dungeon_entrances.append('Inverted Ganons Tower')
@@ -1757,29 +1760,34 @@ def inverted_crossed_shuffle_dungeons(world, player: int):
hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)', 'Inverted Ganons Tower']
# shuffle aga door first. If it's on HC ledge, remaining HC ledge door must be must-exit
- world.random.shuffle(dungeon_entrances)
+ multiworld.random.shuffle(dungeon_entrances)
aga_door = dungeon_entrances.pop()
if aga_door in hc_ledge_entrances:
hc_ledge_entrances.remove(aga_door)
- world.random.shuffle(hc_ledge_entrances)
+ multiworld.random.shuffle(hc_ledge_entrances)
hc_ledge_must_exit = hc_ledge_entrances.pop()
dungeon_entrances.remove(hc_ledge_must_exit)
lw_dungeon_entrances_must_exit.append(hc_ledge_must_exit)
- connect_two_way(world, aga_door, 'Inverted Agahnims Tower Exit', player)
+ connect_two_way(multiworld, aga_door, 'Inverted Agahnims Tower Exit', player)
dungeon_exits.remove('Inverted Agahnims Tower Exit')
- connect_mandatory_exits(world, dungeon_entrances, dungeon_exits, lw_dungeon_entrances_must_exit, player)
+ connect_mandatory_exits(multiworld, dungeon_entrances, dungeon_exits, lw_dungeon_entrances_must_exit, player)
- connect_caves(world, dungeon_entrances, [], dungeon_exits, player)
+ connect_caves(multiworld, dungeon_entrances, [], dungeon_exits, player)
assert not dungeon_exits, "make sure all exits are accounted for"
-def unbias_some_entrances(world, Dungeon_Exits, Cave_Exits, Old_Man_House, Cave_Three_Exits):
+
+def unbias_some_entrances(multiworld: MultiWorld,
+ Dungeon_Exits: list[list[str] | str],
+ Cave_Exits: list[str],
+ Old_Man_House: list[str],
+ Cave_Three_Exits: list[tuple[str, str, str]]):
def shuffle_lists_in_list(ls):
for i, item in enumerate(ls):
if isinstance(item, list):
- ls[i] = world.random.sample(item, len(item))
+ ls[i] = multiworld.random.sample(item, len(item))
def tuplize_lists_in_list(ls):
for i, item in enumerate(ls):
@@ -1793,7 +1801,7 @@ def tuplize_lists_in_list(ls):
# paradox fixup
if Cave_Three_Exits[1][0] == "Paradox Cave Exit (Bottom)":
- i = world.random.randint(1, 2)
+ i = multiworld.random.randint(1, 2)
Cave_Three_Exits[1][0] = Cave_Three_Exits[1][i]
Cave_Three_Exits[1][i] = "Paradox Cave Exit (Bottom)"
@@ -1822,18 +1830,18 @@ def tuplize_lists_in_list(ls):
}
-def plando_connect(world, player: int):
- if world.worlds[player].options.plando_connections:
- for connection in world.worlds[player].options.plando_connections:
+def plando_connect(multiworld: MultiWorld, player: int):
+ if multiworld.worlds[player].options.plando_connections:
+ for connection in multiworld.worlds[player].options.plando_connections:
func = lookup[connection.direction]
try:
- func(world, connection.entrance, connection.exit, player)
+ func(multiworld, connection.entrance, connection.exit, player)
except Exception as e:
raise Exception(f"Could not connect using {connection}") from e
- if world.worlds[player].options.mode != 'inverted':
- mark_light_world_regions(world, player)
+ if multiworld.worlds[player].options.mode != 'inverted':
+ mark_light_world_regions(multiworld, player)
else:
- mark_dark_world_regions(world, player)
+ mark_dark_world_regions(multiworld, player)
LW_Dungeon_Entrances = ['Desert Palace Entrance (South)',
diff --git a/worlds/alttp/InvertedRegions.py b/worlds/alttp/InvertedRegions.py
index 63a2d499e2d4..47e12d313f66 100644
--- a/worlds/alttp/InvertedRegions.py
+++ b/worlds/alttp/InvertedRegions.py
@@ -1,15 +1,16 @@
import collections
+from BaseClasses import MultiWorld
from .Regions import create_lw_region, create_dw_region, create_cave_region, create_dungeon_region
from .SubClasses import LTTPRegionType
-def create_inverted_regions(world, player):
+def create_inverted_regions(multiworld: MultiWorld, player: int):
- world.regions += [
- create_dw_region(world, player, 'Menu', None,
+ multiworld.regions += [
+ create_dw_region(multiworld, player, 'Menu', None,
['Links House S&Q', 'Dark Sanctuary S&Q', 'Old Man S&Q', 'Castle Ledge S&Q']),
- create_lw_region(world, player, 'Light World',
+ create_lw_region(multiworld, player, 'Light World',
['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure', 'Purple Chest',
'Bombos Tablet'],
["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Kings Grave Outer Rocks', 'Dam',
@@ -35,184 +36,184 @@ def create_inverted_regions(world, player):
'Hyrule Castle Entrance (South)', 'Secret Passage Outer Bushes',
'Bush Covered Lawn Outer Bushes',
'Potion Shop Outer Bushes', 'Graveyard Cave Outer Bushes', 'Bomb Hut Outer Bushes']),
- create_lw_region(world, player, 'Bush Covered Lawn', None,
+ create_lw_region(multiworld, player, 'Bush Covered Lawn', None,
['Bush Covered House', 'Bush Covered Lawn Inner Bushes', 'Bush Covered Lawn Mirror Spot']),
- create_lw_region(world, player, 'Bomb Hut Area', None,
+ create_lw_region(multiworld, player, 'Bomb Hut Area', None,
['Light World Bomb Hut', 'Bomb Hut Inner Bushes', 'Bomb Hut Mirror Spot']),
- create_lw_region(world, player, 'Hyrule Castle Secret Entrance Area', None,
+ create_lw_region(multiworld, player, 'Hyrule Castle Secret Entrance Area', None,
['Hyrule Castle Secret Entrance Stairs', 'Secret Passage Inner Bushes']),
- create_lw_region(world, player, 'Death Mountain Entrance', None,
+ create_lw_region(multiworld, player, 'Death Mountain Entrance', None,
['Old Man Cave (West)', 'Death Mountain Entrance Drop', 'Bumper Cave Entrance Mirror Spot']),
- create_lw_region(world, player, 'Lake Hylia Central Island', None,
+ create_lw_region(multiworld, player, 'Lake Hylia Central Island', None,
['Capacity Upgrade', 'Lake Hylia Central Island Mirror Spot']),
- create_cave_region(world, player, 'Blinds Hideout', 'a bounty of five items', ["Blind\'s Hideout - Top",
+ create_cave_region(multiworld, player, 'Blinds Hideout', 'a bounty of five items', ["Blind\'s Hideout - Top",
"Blind\'s Hideout - Left",
"Blind\'s Hideout - Right",
"Blind\'s Hideout - Far Left",
"Blind\'s Hideout - Far Right"]),
- create_lw_region(world, player, 'Northeast Light World', None,
+ create_lw_region(multiworld, player, 'Northeast Light World', None,
['Zoras River', 'Waterfall of Wishing Cave', 'Potion Shop Outer Rock', 'Catfish Mirror Spot',
'Northeast Light World Warp']),
- create_lw_region(world, player, 'Waterfall of Wishing Cave', None,
+ create_lw_region(multiworld, player, 'Waterfall of Wishing Cave', None,
['Waterfall of Wishing', 'Northeast Light World Return']),
- create_lw_region(world, player, 'Potion Shop Area', None,
+ create_lw_region(multiworld, player, 'Potion Shop Area', None,
['Potion Shop', 'Potion Shop Inner Bushes', 'Potion Shop Inner Rock',
'Potion Shop Mirror Spot', 'Potion Shop River Drop']),
- create_lw_region(world, player, 'Graveyard Cave Area', None,
+ create_lw_region(multiworld, player, 'Graveyard Cave Area', None,
['Graveyard Cave', 'Graveyard Cave Inner Bushes', 'Graveyard Cave Mirror Spot']),
- create_lw_region(world, player, 'River', None, ['Light World Pier', 'Potion Shop Pier']),
- create_cave_region(world, player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit',
+ create_lw_region(multiworld, player, 'River', None, ['Light World Pier', 'Potion Shop Pier']),
+ create_cave_region(multiworld, player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit',
['Link\'s Uncle', 'Secret Passage'], ['Hyrule Castle Secret Entrance Exit']),
- create_lw_region(world, player, 'Zoras River', ['King Zora', 'Zora\'s Ledge']),
- create_cave_region(world, player, 'Waterfall of Wishing', 'a cave with two chests',
+ create_lw_region(multiworld, player, 'Zoras River', ['King Zora', 'Zora\'s Ledge']),
+ create_cave_region(multiworld, player, 'Waterfall of Wishing', 'a cave with two chests',
['Waterfall Fairy - Left', 'Waterfall Fairy - Right']),
- create_lw_region(world, player, 'Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']),
- create_cave_region(world, player, 'Kings Grave', 'a cave with a chest', ['King\'s Tomb']),
- create_cave_region(world, player, 'North Fairy Cave', 'a drop\'s exit', None, ['North Fairy Cave Exit']),
- create_cave_region(world, player, 'Dam', 'the dam', ['Floodgate', 'Floodgate Chest']),
- create_cave_region(world, player, 'Inverted Links House', 'your house', ['Link\'s House'],
+ create_lw_region(multiworld, player, 'Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']),
+ create_cave_region(multiworld, player, 'Kings Grave', 'a cave with a chest', ['King\'s Tomb']),
+ create_cave_region(multiworld, player, 'North Fairy Cave', 'a drop\'s exit', None, ['North Fairy Cave Exit']),
+ create_cave_region(multiworld, player, 'Dam', 'the dam', ['Floodgate', 'Floodgate Chest']),
+ create_cave_region(multiworld, player, 'Inverted Links House', 'your house', ['Link\'s House'],
['Inverted Links House Exit']),
- create_cave_region(world, player, 'Chris Houlihan Room', 'I AM ERROR', None, ['Chris Houlihan Room Exit']),
- create_cave_region(world, player, 'Tavern', 'the tavern', ['Kakariko Tavern']),
- create_cave_region(world, player, 'Elder House', 'a connector', None,
+ create_cave_region(multiworld, player, 'Chris Houlihan Room', 'I AM ERROR', None, ['Chris Houlihan Room Exit']),
+ create_cave_region(multiworld, player, 'Tavern', 'the tavern', ['Kakariko Tavern']),
+ create_cave_region(multiworld, player, 'Elder House', 'a connector', None,
['Elder House Exit (East)', 'Elder House Exit (West)']),
- create_cave_region(world, player, 'Snitch Lady (East)', 'a boring house'),
- create_cave_region(world, player, 'Snitch Lady (West)', 'a boring house'),
- create_cave_region(world, player, 'Bush Covered House', 'the grass man'),
- create_cave_region(world, player, 'Tavern (Front)', 'the tavern'),
- create_cave_region(world, player, 'Light World Bomb Hut', 'a restock room'),
- create_cave_region(world, player, 'Kakariko Shop', 'a common shop'),
- create_cave_region(world, player, 'Fortune Teller (Light)', 'a fortune teller'),
- create_cave_region(world, player, 'Lake Hylia Fortune Teller', 'a fortune teller'),
- create_cave_region(world, player, 'Lumberjack House', 'a boring house'),
- create_cave_region(world, player, 'Bonk Fairy (Light)', 'a fairy fountain'),
- create_cave_region(world, player, 'Bonk Fairy (Dark)', 'a fairy fountain'),
- create_cave_region(world, player, 'Lake Hylia Healer Fairy', 'a fairy fountain'),
- create_cave_region(world, player, 'Swamp Healer Fairy', 'a fairy fountain'),
- create_cave_region(world, player, 'Desert Healer Fairy', 'a fairy fountain'),
- create_cave_region(world, player, 'Dark Lake Hylia Healer Fairy', 'a fairy fountain'),
- create_cave_region(world, player, 'Dark Lake Hylia Ledge Healer Fairy', 'a fairy fountain'),
- create_cave_region(world, player, 'Dark Desert Healer Fairy', 'a fairy fountain'),
- create_cave_region(world, player, 'Dark Death Mountain Healer Fairy', 'a fairy fountain'),
- create_cave_region(world, player, 'Chicken House', 'a house with a chest', ['Chicken House']),
- create_cave_region(world, player, 'Aginahs Cave', 'a cave with a chest', ['Aginah\'s Cave']),
- create_cave_region(world, player, 'Sahasrahlas Hut', 'Sahasrahla',
+ create_cave_region(multiworld, player, 'Snitch Lady (East)', 'a boring house'),
+ create_cave_region(multiworld, player, 'Snitch Lady (West)', 'a boring house'),
+ create_cave_region(multiworld, player, 'Bush Covered House', 'the grass man'),
+ create_cave_region(multiworld, player, 'Tavern (Front)', 'the tavern'),
+ create_cave_region(multiworld, player, 'Light World Bomb Hut', 'a restock room'),
+ create_cave_region(multiworld, player, 'Kakariko Shop', 'a common shop'),
+ create_cave_region(multiworld, player, 'Fortune Teller (Light)', 'a fortune teller'),
+ create_cave_region(multiworld, player, 'Lake Hylia Fortune Teller', 'a fortune teller'),
+ create_cave_region(multiworld, player, 'Lumberjack House', 'a boring house'),
+ create_cave_region(multiworld, player, 'Bonk Fairy (Light)', 'a fairy fountain'),
+ create_cave_region(multiworld, player, 'Bonk Fairy (Dark)', 'a fairy fountain'),
+ create_cave_region(multiworld, player, 'Lake Hylia Healer Fairy', 'a fairy fountain'),
+ create_cave_region(multiworld, player, 'Swamp Healer Fairy', 'a fairy fountain'),
+ create_cave_region(multiworld, player, 'Desert Healer Fairy', 'a fairy fountain'),
+ create_cave_region(multiworld, player, 'Dark Lake Hylia Healer Fairy', 'a fairy fountain'),
+ create_cave_region(multiworld, player, 'Dark Lake Hylia Ledge Healer Fairy', 'a fairy fountain'),
+ create_cave_region(multiworld, player, 'Dark Desert Healer Fairy', 'a fairy fountain'),
+ create_cave_region(multiworld, player, 'Dark Death Mountain Healer Fairy', 'a fairy fountain'),
+ create_cave_region(multiworld, player, 'Chicken House', 'a house with a chest', ['Chicken House']),
+ create_cave_region(multiworld, player, 'Aginahs Cave', 'a cave with a chest', ['Aginah\'s Cave']),
+ create_cave_region(multiworld, player, 'Sahasrahlas Hut', 'Sahasrahla',
['Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right',
'Sahasrahla']),
- create_cave_region(world, player, 'Kakariko Well (top)', 'a drop\'s exit',
+ create_cave_region(multiworld, player, 'Kakariko Well (top)', 'a drop\'s exit',
['Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle',
'Kakariko Well - Right', 'Kakariko Well - Bottom'], ['Kakariko Well (top to bottom)']),
- create_cave_region(world, player, 'Kakariko Well (bottom)', 'a drop\'s exit', None, ['Kakariko Well Exit']),
- create_cave_region(world, player, 'Blacksmiths Hut', 'the smith', ['Blacksmith', 'Missing Smith']),
- create_lw_region(world, player, 'Bat Cave Drop Ledge', None, ['Bat Cave Drop']),
- create_cave_region(world, player, 'Bat Cave (right)', 'a drop\'s exit', ['Magic Bat'], ['Bat Cave Door']),
- create_cave_region(world, player, 'Bat Cave (left)', 'a drop\'s exit', None, ['Bat Cave Exit']),
- create_cave_region(world, player, 'Sick Kids House', 'the sick kid', ['Sick Kid']),
- create_lw_region(world, player, 'Hobo Bridge', ['Hobo']),
- create_cave_region(world, player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'],
+ create_cave_region(multiworld, player, 'Kakariko Well (bottom)', 'a drop\'s exit', None, ['Kakariko Well Exit']),
+ create_cave_region(multiworld, player, 'Blacksmiths Hut', 'the smith', ['Blacksmith', 'Missing Smith']),
+ create_lw_region(multiworld, player, 'Bat Cave Drop Ledge', None, ['Bat Cave Drop']),
+ create_cave_region(multiworld, player, 'Bat Cave (right)', 'a drop\'s exit', ['Magic Bat'], ['Bat Cave Door']),
+ create_cave_region(multiworld, player, 'Bat Cave (left)', 'a drop\'s exit', None, ['Bat Cave Exit']),
+ create_cave_region(multiworld, player, 'Sick Kids House', 'the sick kid', ['Sick Kid']),
+ create_lw_region(multiworld, player, 'Hobo Bridge', ['Hobo']),
+ create_cave_region(multiworld, player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'],
['Lost Woods Hideout (top to bottom)']),
- create_cave_region(world, player, 'Lost Woods Hideout (bottom)', 'a drop\'s exit', None,
+ create_cave_region(multiworld, player, 'Lost Woods Hideout (bottom)', 'a drop\'s exit', None,
['Lost Woods Hideout Exit']),
- create_cave_region(world, player, 'Lumberjack Tree (top)', 'a drop\'s exit', ['Lumberjack Tree'],
+ create_cave_region(multiworld, player, 'Lumberjack Tree (top)', 'a drop\'s exit', ['Lumberjack Tree'],
['Lumberjack Tree (top to bottom)']),
- create_cave_region(world, player, 'Lumberjack Tree (bottom)', 'a drop\'s exit', None, ['Lumberjack Tree Exit']),
- create_cave_region(world, player, 'Cave 45', 'a cave with an item', ['Cave 45']),
- create_cave_region(world, player, 'Graveyard Cave', 'a cave with an item', ['Graveyard Cave']),
- create_cave_region(world, player, 'Checkerboard Cave', 'a cave with an item', ['Checkerboard Cave']),
- create_cave_region(world, player, 'Long Fairy Cave', 'a fairy fountain'),
- create_cave_region(world, player, 'Mini Moldorm Cave', 'a bounty of five items',
+ create_cave_region(multiworld, player, 'Lumberjack Tree (bottom)', 'a drop\'s exit', None, ['Lumberjack Tree Exit']),
+ create_cave_region(multiworld, player, 'Cave 45', 'a cave with an item', ['Cave 45']),
+ create_cave_region(multiworld, player, 'Graveyard Cave', 'a cave with an item', ['Graveyard Cave']),
+ create_cave_region(multiworld, player, 'Checkerboard Cave', 'a cave with an item', ['Checkerboard Cave']),
+ create_cave_region(multiworld, player, 'Long Fairy Cave', 'a fairy fountain'),
+ create_cave_region(multiworld, player, 'Mini Moldorm Cave', 'a bounty of five items',
['Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right',
'Mini Moldorm Cave - Far Right', 'Mini Moldorm Cave - Generous Guy']),
- create_cave_region(world, player, 'Ice Rod Cave', 'a cave with a chest', ['Ice Rod Cave']),
- create_cave_region(world, player, 'Good Bee Cave', 'a cold bee'),
- create_cave_region(world, player, '20 Rupee Cave', 'a cave with some cash'),
- create_cave_region(world, player, 'Cave Shop (Lake Hylia)', 'a common shop'),
- create_cave_region(world, player, 'Cave Shop (Dark Death Mountain)', 'a common shop'),
- create_cave_region(world, player, 'Bonk Rock Cave', 'a cave with a chest', ['Bonk Rock Cave']),
- create_cave_region(world, player, 'Library', 'the library', ['Library']),
- create_cave_region(world, player, 'Kakariko Gamble Game', 'a game of chance'),
- create_cave_region(world, player, 'Potion Shop', 'the potion shop', ['Potion Shop']),
- create_lw_region(world, player, 'Lake Hylia Island', ['Lake Hylia Island']),
- create_cave_region(world, player, 'Capacity Upgrade', 'the queen of fairies', ['Capacity Upgrade Shop']),
- create_cave_region(world, player, 'Two Brothers House', 'a connector', None,
+ create_cave_region(multiworld, player, 'Ice Rod Cave', 'a cave with a chest', ['Ice Rod Cave']),
+ create_cave_region(multiworld, player, 'Good Bee Cave', 'a cold bee'),
+ create_cave_region(multiworld, player, '20 Rupee Cave', 'a cave with some cash'),
+ create_cave_region(multiworld, player, 'Cave Shop (Lake Hylia)', 'a common shop'),
+ create_cave_region(multiworld, player, 'Cave Shop (Dark Death Mountain)', 'a common shop'),
+ create_cave_region(multiworld, player, 'Bonk Rock Cave', 'a cave with a chest', ['Bonk Rock Cave']),
+ create_cave_region(multiworld, player, 'Library', 'the library', ['Library']),
+ create_cave_region(multiworld, player, 'Kakariko Gamble Game', 'a game of chance'),
+ create_cave_region(multiworld, player, 'Potion Shop', 'the potion shop', ['Potion Shop']),
+ create_lw_region(multiworld, player, 'Lake Hylia Island', ['Lake Hylia Island']),
+ create_cave_region(multiworld, player, 'Capacity Upgrade', 'the queen of fairies', ['Capacity Upgrade Shop']),
+ create_cave_region(multiworld, player, 'Two Brothers House', 'a connector', None,
['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)']),
- create_lw_region(world, player, 'Maze Race Ledge', ['Maze Race'],
+ create_lw_region(multiworld, player, 'Maze Race Ledge', ['Maze Race'],
['Two Brothers House (West)', 'Maze Race Mirror Spot']),
- create_cave_region(world, player, '50 Rupee Cave', 'a cave with some cash'),
- create_lw_region(world, player, 'Desert Ledge', ['Desert Ledge'],
+ create_cave_region(multiworld, player, '50 Rupee Cave', 'a cave with some cash'),
+ create_lw_region(multiworld, player, 'Desert Ledge', ['Desert Ledge'],
['Desert Palace Entrance (North) Rocks', 'Desert Palace Entrance (West)',
'Desert Ledge Drop']),
- create_lw_region(world, player, 'Desert Palace Stairs', None,
+ create_lw_region(multiworld, player, 'Desert Palace Stairs', None,
['Desert Palace Entrance (South)', 'Desert Palace Stairs Mirror Spot']),
- create_lw_region(world, player, 'Desert Palace Lone Stairs', None,
+ create_lw_region(multiworld, player, 'Desert Palace Lone Stairs', None,
['Desert Palace Stairs Drop', 'Desert Palace Entrance (East)']),
- create_lw_region(world, player, 'Desert Palace Entrance (North) Spot', None,
+ create_lw_region(multiworld, player, 'Desert Palace Entrance (North) Spot', None,
['Desert Palace Entrance (North)', 'Desert Ledge Return Rocks',
'Desert Palace North Mirror Spot']),
- create_dungeon_region(world, player, 'Desert Palace Main (Outer)', 'Desert Palace', ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'],
+ create_dungeon_region(multiworld, player, 'Desert Palace Main (Outer)', 'Desert Palace', ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'],
['Desert Palace Pots (Outer)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)', 'Desert Palace East Wing']),
- create_dungeon_region(world, player, 'Desert Palace Main (Inner)', 'Desert Palace', None, ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']),
- create_dungeon_region(world, player, 'Desert Palace East', 'Desert Palace', ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']),
- create_dungeon_region(world, player, 'Desert Palace North', 'Desert Palace',
+ create_dungeon_region(multiworld, player, 'Desert Palace Main (Inner)', 'Desert Palace', None, ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']),
+ create_dungeon_region(multiworld, player, 'Desert Palace East', 'Desert Palace', ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']),
+ create_dungeon_region(multiworld, player, 'Desert Palace North', 'Desert Palace',
['Desert Palace - Desert Tiles 1 Pot Key', 'Desert Palace - Beamos Hall Pot Key',
'Desert Palace - Desert Tiles 2 Pot Key',
'Desert Palace - Boss', 'Desert Palace - Prize'], ['Desert Palace Exit (North)']),
- create_dungeon_region(world, player, 'Eastern Palace', 'Eastern Palace',
+ create_dungeon_region(multiworld, player, 'Eastern Palace', 'Eastern Palace',
['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest',
'Eastern Palace - Cannonball Chest',
'Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop',
'Eastern Palace - Big Key Chest',
'Eastern Palace - Map Chest', 'Eastern Palace - Boss', 'Eastern Palace - Prize'],
['Eastern Palace Exit']),
- create_lw_region(world, player, 'Master Sword Meadow', ['Master Sword Pedestal']),
- create_cave_region(world, player, 'Lost Woods Gamble', 'a game of chance'),
- create_lw_region(world, player, 'Hyrule Castle Ledge', None, ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Inverted Ganons Tower', 'Hyrule Castle Ledge Courtyard Drop', 'Inverted Pyramid Hole']),
- create_dungeon_region(world, player, 'Hyrule Castle', 'Hyrule Castle',
+ create_lw_region(multiworld, player, 'Master Sword Meadow', ['Master Sword Pedestal']),
+ create_cave_region(multiworld, player, 'Lost Woods Gamble', 'a game of chance'),
+ create_lw_region(multiworld, player, 'Hyrule Castle Ledge', None, ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Inverted Ganons Tower', 'Hyrule Castle Ledge Courtyard Drop', 'Inverted Pyramid Hole']),
+ create_dungeon_region(multiworld, player, 'Hyrule Castle', 'Hyrule Castle',
['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest',
'Hyrule Castle - Zelda\'s Chest',
'Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop',
'Hyrule Castle - Big Key Drop'],
['Hyrule Castle Exit (East)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (South)',
'Throne Room']),
- create_dungeon_region(world, player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks
- create_dungeon_region(world, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross', 'Sewers - Key Rat Key Drop'], ['Sewers Door']),
- create_dungeon_region(world, player, 'Sewers', 'a drop\'s exit', None, ['Sanctuary Push Door', 'Sewers Back Door', 'Sewers Secret Room']),
- create_dungeon_region(world, player, 'Sewers Secret Room', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle',
+ create_dungeon_region(multiworld, player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks
+ create_dungeon_region(multiworld, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross', 'Sewers - Key Rat Key Drop'], ['Sewers Door']),
+ create_dungeon_region(multiworld, player, 'Sewers', 'a drop\'s exit', None, ['Sanctuary Push Door', 'Sewers Back Door', 'Sewers Secret Room']),
+ create_dungeon_region(multiworld, player, 'Sewers Secret Room', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle',
'Sewers - Secret Room - Right']),
- create_dungeon_region(world, player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']),
- create_dungeon_region(world, player, 'Inverted Agahnims Tower', 'Castle Tower', ['Castle Tower - Room 03', 'Castle Tower - Dark Maze', 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'], ['Agahnim 1', 'Inverted Agahnims Tower Exit']),
- create_dungeon_region(world, player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None),
- create_cave_region(world, player, 'Old Man Cave', 'a connector', ['Old Man'],
+ create_dungeon_region(multiworld, player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']),
+ create_dungeon_region(multiworld, player, 'Inverted Agahnims Tower', 'Castle Tower', ['Castle Tower - Room 03', 'Castle Tower - Dark Maze', 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'], ['Agahnim 1', 'Inverted Agahnims Tower Exit']),
+ create_dungeon_region(multiworld, player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None),
+ create_cave_region(multiworld, player, 'Old Man Cave', 'a connector', ['Old Man'],
['Old Man Cave Exit (East)', 'Old Man Cave Exit (West)']),
- create_cave_region(world, player, 'Old Man House', 'a connector', None,
+ create_cave_region(multiworld, player, 'Old Man House', 'a connector', None,
['Old Man House Exit (Bottom)', 'Old Man House Front to Back']),
- create_cave_region(world, player, 'Old Man House Back', 'a connector', None,
+ create_cave_region(multiworld, player, 'Old Man House Back', 'a connector', None,
['Old Man House Exit (Top)', 'Old Man House Back to Front']),
- create_lw_region(world, player, 'Death Mountain', None,
+ create_lw_region(multiworld, player, 'Death Mountain', None,
['Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)',
'Death Mountain Return Cave (East)', 'Spectacle Rock Cave',
'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)', 'Broken Bridge (West)',
'Death Mountain Mirror Spot']),
- create_cave_region(world, player, 'Death Mountain Return Cave', 'a connector', None,
+ create_cave_region(multiworld, player, 'Death Mountain Return Cave', 'a connector', None,
['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)']),
- create_lw_region(world, player, 'Death Mountain Return Ledge', None,
+ create_lw_region(multiworld, player, 'Death Mountain Return Ledge', None,
['Death Mountain Return Ledge Drop', 'Death Mountain Return Cave (West)',
'Bumper Cave Ledge Mirror Spot']),
- create_cave_region(world, player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'],
+ create_cave_region(multiworld, player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'],
['Spectacle Rock Cave Drop', 'Spectacle Rock Cave Exit (Top)']),
- create_cave_region(world, player, 'Spectacle Rock Cave (Bottom)', 'a connector', None,
+ create_cave_region(multiworld, player, 'Spectacle Rock Cave (Bottom)', 'a connector', None,
['Spectacle Rock Cave Exit']),
- create_cave_region(world, player, 'Spectacle Rock Cave (Peak)', 'a connector', None,
+ create_cave_region(multiworld, player, 'Spectacle Rock Cave (Peak)', 'a connector', None,
['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']),
- create_lw_region(world, player, 'East Death Mountain (Bottom)', None,
+ create_lw_region(multiworld, player, 'East Death Mountain (Bottom)', None,
['Broken Bridge (East)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)',
'East Death Mountain Mirror Spot (Bottom)', 'Hookshot Fairy',
'Fairy Ascension Rocks', 'Spiral Cave (Bottom)']),
- create_cave_region(world, player, 'Hookshot Fairy', 'fairies deep in a cave'),
- create_cave_region(world, player, 'Paradox Cave Front', 'a connector', None,
+ create_cave_region(multiworld, player, 'Hookshot Fairy', 'fairies deep in a cave'),
+ create_cave_region(multiworld, player, 'Paradox Cave Front', 'a connector', None,
['Paradox Cave Push Block Reverse', 'Paradox Cave Exit (Bottom)',
'Light World Death Mountain Shop']),
- create_cave_region(world, player, 'Paradox Cave Chest Area', 'a connector', ['Paradox Cave Lower - Far Left',
+ create_cave_region(multiworld, player, 'Paradox Cave Chest Area', 'a connector', ['Paradox Cave Lower - Far Left',
'Paradox Cave Lower - Left',
'Paradox Cave Lower - Right',
'Paradox Cave Lower - Far Right',
@@ -220,273 +221,273 @@ def create_inverted_regions(world, player):
'Paradox Cave Upper - Left',
'Paradox Cave Upper - Right'],
['Paradox Cave Push Block', 'Paradox Cave Bomb Jump']),
- create_cave_region(world, player, 'Paradox Cave', 'a connector', None,
+ create_cave_region(multiworld, player, 'Paradox Cave', 'a connector', None,
['Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Top)', 'Paradox Cave Drop']),
- create_cave_region(world, player, 'Light World Death Mountain Shop', 'a common shop'),
- create_lw_region(world, player, 'East Death Mountain (Top)', ['Floating Island'],
+ create_cave_region(multiworld, player, 'Light World Death Mountain Shop', 'a common shop'),
+ create_lw_region(multiworld, player, 'East Death Mountain (Top)', ['Floating Island'],
['Paradox Cave (Top)', 'Death Mountain (Top)', 'Spiral Cave Ledge Access',
'East Death Mountain Drop', 'East Death Mountain Mirror Spot (Top)',
'Fairy Ascension Ledge Access', 'Mimic Cave Ledge Access',
'Floating Island Mirror Spot']),
- create_lw_region(world, player, 'Spiral Cave Ledge', None,
+ create_lw_region(multiworld, player, 'Spiral Cave Ledge', None,
['Spiral Cave', 'Spiral Cave Ledge Drop', 'Dark Death Mountain Ledge Mirror Spot (West)']),
- create_lw_region(world, player, 'Mimic Cave Ledge', None,
+ create_lw_region(multiworld, player, 'Mimic Cave Ledge', None,
['Mimic Cave', 'Mimic Cave Ledge Drop', 'Dark Death Mountain Ledge Mirror Spot (East)']),
- create_cave_region(world, player, 'Spiral Cave (Top)', 'a connector', ['Spiral Cave'],
+ create_cave_region(multiworld, player, 'Spiral Cave (Top)', 'a connector', ['Spiral Cave'],
['Spiral Cave (top to bottom)', 'Spiral Cave Exit (Top)']),
- create_cave_region(world, player, 'Spiral Cave (Bottom)', 'a connector', None, ['Spiral Cave Exit']),
- create_lw_region(world, player, 'Fairy Ascension Plateau', None,
+ create_cave_region(multiworld, player, 'Spiral Cave (Bottom)', 'a connector', None, ['Spiral Cave Exit']),
+ create_lw_region(multiworld, player, 'Fairy Ascension Plateau', None,
['Fairy Ascension Drop', 'Fairy Ascension Cave (Bottom)']),
- create_cave_region(world, player, 'Fairy Ascension Cave (Bottom)', 'a connector', None,
+ create_cave_region(multiworld, player, 'Fairy Ascension Cave (Bottom)', 'a connector', None,
['Fairy Ascension Cave Climb', 'Fairy Ascension Cave Exit (Bottom)']),
- create_cave_region(world, player, 'Fairy Ascension Cave (Drop)', 'a connector', None,
+ create_cave_region(multiworld, player, 'Fairy Ascension Cave (Drop)', 'a connector', None,
['Fairy Ascension Cave Pots']),
- create_cave_region(world, player, 'Fairy Ascension Cave (Top)', 'a connector', None,
+ create_cave_region(multiworld, player, 'Fairy Ascension Cave (Top)', 'a connector', None,
['Fairy Ascension Cave Exit (Top)', 'Fairy Ascension Cave Drop']),
- create_lw_region(world, player, 'Fairy Ascension Ledge', None,
+ create_lw_region(multiworld, player, 'Fairy Ascension Ledge', None,
['Fairy Ascension Ledge Drop', 'Fairy Ascension Cave (Top)', 'Laser Bridge Mirror Spot']),
- create_lw_region(world, player, 'Death Mountain (Top)', ['Ether Tablet', 'Spectacle Rock'],
+ create_lw_region(multiworld, player, 'Death Mountain (Top)', ['Ether Tablet', 'Spectacle Rock'],
['East Death Mountain (Top)', 'Tower of Hera', 'Death Mountain Drop',
'Death Mountain (Top) Mirror Spot']),
- create_dw_region(world, player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'],
+ create_dw_region(multiworld, player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'],
['Bumper Cave Ledge Drop', 'Bumper Cave (Top)']),
- create_dungeon_region(world, player, 'Tower of Hera (Bottom)', 'Tower of Hera', ['Tower of Hera - Basement Cage', 'Tower of Hera - Map Chest'], ['Tower of Hera Small Key Door', 'Tower of Hera Big Key Door', 'Tower of Hera Exit']),
- create_dungeon_region(world, player, 'Tower of Hera (Basement)', 'Tower of Hera', ['Tower of Hera - Big Key Chest']),
- create_dungeon_region(world, player, 'Tower of Hera (Top)', 'Tower of Hera', ['Tower of Hera - Compass Chest', 'Tower of Hera - Big Chest', 'Tower of Hera - Boss', 'Tower of Hera - Prize']),
+ create_dungeon_region(multiworld, player, 'Tower of Hera (Bottom)', 'Tower of Hera', ['Tower of Hera - Basement Cage', 'Tower of Hera - Map Chest'], ['Tower of Hera Small Key Door', 'Tower of Hera Big Key Door', 'Tower of Hera Exit']),
+ create_dungeon_region(multiworld, player, 'Tower of Hera (Basement)', 'Tower of Hera', ['Tower of Hera - Big Key Chest']),
+ create_dungeon_region(multiworld, player, 'Tower of Hera (Top)', 'Tower of Hera', ['Tower of Hera - Compass Chest', 'Tower of Hera - Big Chest', 'Tower of Hera - Boss', 'Tower of Hera - Prize']),
- create_dw_region(world, player, 'East Dark World', ['Pyramid'],
+ create_dw_region(multiworld, player, 'East Dark World', ['Pyramid'],
['Pyramid Fairy', 'South Dark World Bridge', 'Palace of Darkness',
'Dark Lake Hylia Drop (East)',
'Dark Lake Hylia Fairy', 'Palace of Darkness Hint', 'East Dark World Hint',
'Northeast Dark World Broken Bridge Pass', 'East Dark World Teleporter', 'EDW Flute']),
- create_dw_region(world, player, 'Catfish', ['Catfish'], ['Catfish Exit Rock']),
- create_dw_region(world, player, 'Northeast Dark World', None,
+ create_dw_region(multiworld, player, 'Catfish', ['Catfish'], ['Catfish Exit Rock']),
+ create_dw_region(multiworld, player, 'Northeast Dark World', None,
['West Dark World Gap', 'Dark World Potion Shop', 'East Dark World Broken Bridge Pass',
'NEDW Flute', 'Dark Lake Hylia Teleporter', 'Catfish Entrance Rock']),
- create_cave_region(world, player, 'Palace of Darkness Hint', 'a storyteller'),
- create_cave_region(world, player, 'East Dark World Hint', 'a storyteller'),
- create_dw_region(world, player, 'South Dark World', ['Stumpy', 'Digging Game'],
+ create_cave_region(multiworld, player, 'Palace of Darkness Hint', 'a storyteller'),
+ create_cave_region(multiworld, player, 'East Dark World Hint', 'a storyteller'),
+ create_dw_region(multiworld, player, 'South Dark World', ['Stumpy', 'Digging Game'],
['Dark Lake Hylia Drop (South)', 'Hype Cave', 'Swamp Palace', 'Village of Outcasts Heavy Rock',
'East Dark World Bridge', 'Inverted Links House', 'Archery Game', 'Bonk Fairy (Dark)',
'Dark Lake Hylia Shop', 'South Dark World Teleporter', 'Post Aga Teleporter', 'SDW Flute']),
- create_cave_region(world, player, 'Inverted Big Bomb Shop', 'the bomb shop'),
- create_cave_region(world, player, 'Archery Game', 'a game of skill'),
- create_dw_region(world, player, 'Dark Lake Hylia', None,
+ create_cave_region(multiworld, player, 'Inverted Big Bomb Shop', 'the bomb shop'),
+ create_cave_region(multiworld, player, 'Archery Game', 'a game of skill'),
+ create_dw_region(multiworld, player, 'Dark Lake Hylia', None,
['East Dark World Pier', 'Dark Lake Hylia Ledge Pier', 'Ice Palace Missing Wall']),
- create_dw_region(world, player, 'Dark Lake Hylia Central Island', None,
+ create_dw_region(multiworld, player, 'Dark Lake Hylia Central Island', None,
['Dark Lake Hylia Shallows', 'Ice Palace', 'Dark Lake Hylia Central Island Teleporter']),
- create_dw_region(world, player, 'Dark Lake Hylia Ledge', None,
+ create_dw_region(multiworld, player, 'Dark Lake Hylia Ledge', None,
['Dark Lake Hylia Ledge Drop', 'Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Ledge Hint',
'Dark Lake Hylia Ledge Spike Cave', 'DLHL Flute']),
- create_cave_region(world, player, 'Dark Lake Hylia Ledge Hint', 'a storyteller'),
- create_cave_region(world, player, 'Dark Lake Hylia Ledge Spike Cave', 'a spiky hint'),
- create_cave_region(world, player, 'Hype Cave', 'a bounty of five items',
+ create_cave_region(multiworld, player, 'Dark Lake Hylia Ledge Hint', 'a storyteller'),
+ create_cave_region(multiworld, player, 'Dark Lake Hylia Ledge Spike Cave', 'a spiky hint'),
+ create_cave_region(multiworld, player, 'Hype Cave', 'a bounty of five items',
['Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left',
'Hype Cave - Bottom', 'Hype Cave - Generous Guy']),
- create_dw_region(world, player, 'West Dark World', ['Frog', 'Flute Activation Spot'],
+ create_dw_region(multiworld, player, 'West Dark World', ['Frog', 'Flute Activation Spot'],
['Village of Outcasts Drop', 'East Dark World River Pier', 'Brewery', 'C-Shaped House',
'Chest Game', 'Thieves Town', 'Bumper Cave Entrance Rock',
'Skull Woods Forest', 'Village of Outcasts Pegs', 'Village of Outcasts Eastern Rocks',
'Red Shield Shop', 'Inverted Dark Sanctuary', 'Fortune Teller (Dark)',
'Dark World Lumberjack Shop',
'West Dark World Teleporter', 'WDW Flute']),
- create_dw_region(world, player, 'Dark Grassy Lawn', None,
+ create_dw_region(multiworld, player, 'Dark Grassy Lawn', None,
['Grassy Lawn Pegs', 'Village of Outcasts Shop', 'Dark Grassy Lawn Flute']),
- create_dw_region(world, player, 'Hammer Peg Area', ['Dark Blacksmith Ruins'],
+ create_dw_region(multiworld, player, 'Hammer Peg Area', ['Dark Blacksmith Ruins'],
['Dark World Hammer Peg Cave', 'Peg Area Rocks', 'Hammer Peg Area Flute']),
- create_dw_region(world, player, 'Bumper Cave Entrance', None,
+ create_dw_region(multiworld, player, 'Bumper Cave Entrance', None,
['Bumper Cave (Bottom)', 'Bumper Cave Entrance Drop']),
- create_cave_region(world, player, 'Fortune Teller (Dark)', 'a fortune teller'),
- create_cave_region(world, player, 'Village of Outcasts Shop', 'a common shop'),
- create_cave_region(world, player, 'Dark Lake Hylia Shop', 'a common shop'),
- create_cave_region(world, player, 'Dark World Lumberjack Shop', 'a common shop'),
- create_cave_region(world, player, 'Dark World Potion Shop', 'a common shop'),
- create_cave_region(world, player, 'Dark World Hammer Peg Cave', 'a cave with an item', ['Peg Cave']),
- create_cave_region(world, player, 'Pyramid Fairy', 'a cave with two chests',
+ create_cave_region(multiworld, player, 'Fortune Teller (Dark)', 'a fortune teller'),
+ create_cave_region(multiworld, player, 'Village of Outcasts Shop', 'a common shop'),
+ create_cave_region(multiworld, player, 'Dark Lake Hylia Shop', 'a common shop'),
+ create_cave_region(multiworld, player, 'Dark World Lumberjack Shop', 'a common shop'),
+ create_cave_region(multiworld, player, 'Dark World Potion Shop', 'a common shop'),
+ create_cave_region(multiworld, player, 'Dark World Hammer Peg Cave', 'a cave with an item', ['Peg Cave']),
+ create_cave_region(multiworld, player, 'Pyramid Fairy', 'a cave with two chests',
['Pyramid Fairy - Left', 'Pyramid Fairy - Right']),
- create_cave_region(world, player, 'Brewery', 'a house with a chest', ['Brewery']),
- create_cave_region(world, player, 'C-Shaped House', 'a house with a chest', ['C-Shaped House']),
- create_cave_region(world, player, 'Chest Game', 'a game of 16 chests', ['Chest Game']),
- create_cave_region(world, player, 'Red Shield Shop', 'the rare shop'),
- create_cave_region(world, player, 'Inverted Dark Sanctuary', 'a storyteller', None,
+ create_cave_region(multiworld, player, 'Brewery', 'a house with a chest', ['Brewery']),
+ create_cave_region(multiworld, player, 'C-Shaped House', 'a house with a chest', ['C-Shaped House']),
+ create_cave_region(multiworld, player, 'Chest Game', 'a game of 16 chests', ['Chest Game']),
+ create_cave_region(multiworld, player, 'Red Shield Shop', 'the rare shop'),
+ create_cave_region(multiworld, player, 'Inverted Dark Sanctuary', 'a storyteller', None,
['Inverted Dark Sanctuary Exit']),
- create_cave_region(world, player, 'Bumper Cave', 'a connector', None,
+ create_cave_region(multiworld, player, 'Bumper Cave', 'a connector', None,
['Bumper Cave Exit (Bottom)', 'Bumper Cave Exit (Top)']),
- create_dw_region(world, player, 'Skull Woods Forest', None,
+ create_dw_region(multiworld, player, 'Skull Woods Forest', None,
['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)',
'Skull Woods First Section Hole (North)',
'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)']),
- create_dw_region(world, player, 'Skull Woods Forest (West)', None,
+ create_dw_region(multiworld, player, 'Skull Woods Forest (West)', None,
['Skull Woods Second Section Hole', 'Skull Woods Second Section Door (West)',
'Skull Woods Final Section']),
- create_dw_region(world, player, 'Dark Desert', None,
+ create_dw_region(multiworld, player, 'Dark Desert', None,
['Misery Mire', 'Mire Shed', 'Dark Desert Hint', 'Dark Desert Fairy', 'DD Flute']),
- create_dw_region(world, player, 'Dark Desert Ledge', None, ['Dark Desert Drop', 'Dark Desert Teleporter']),
- create_cave_region(world, player, 'Mire Shed', 'a cave with two chests',
+ create_dw_region(multiworld, player, 'Dark Desert Ledge', None, ['Dark Desert Drop', 'Dark Desert Teleporter']),
+ create_cave_region(multiworld, player, 'Mire Shed', 'a cave with two chests',
['Mire Shed - Left', 'Mire Shed - Right']),
- create_cave_region(world, player, 'Dark Desert Hint', 'a storyteller'),
- create_dw_region(world, player, 'Dark Death Mountain', None,
+ create_cave_region(multiworld, player, 'Dark Desert Hint', 'a storyteller'),
+ create_dw_region(multiworld, player, 'Dark Death Mountain', None,
['Dark Death Mountain Drop (East)', 'Inverted Agahnims Tower', 'Superbunny Cave (Top)',
'Hookshot Cave', 'Turtle Rock',
'Spike Cave', 'Dark Death Mountain Fairy', 'Dark Death Mountain Teleporter (West)',
'Turtle Rock Tail Drop', 'DDM Flute']),
- create_dw_region(world, player, 'Dark Death Mountain Ledge', None,
+ create_dw_region(multiworld, player, 'Dark Death Mountain Ledge', None,
['Dark Death Mountain Ledge (East)', 'Dark Death Mountain Ledge (West)']),
- create_dw_region(world, player, 'Turtle Rock (Top)', None,
+ create_dw_region(multiworld, player, 'Turtle Rock (Top)', None,
['Dark Death Mountain Teleporter (East)', 'Turtle Rock Drop']),
- create_dw_region(world, player, 'Dark Death Mountain Isolated Ledge', None,
+ create_dw_region(multiworld, player, 'Dark Death Mountain Isolated Ledge', None,
['Turtle Rock Isolated Ledge Entrance']),
- create_dw_region(world, player, 'Dark Death Mountain (East Bottom)', None,
+ create_dw_region(multiworld, player, 'Dark Death Mountain (East Bottom)', None,
['Superbunny Cave (Bottom)', 'Cave Shop (Dark Death Mountain)',
'Dark Death Mountain Teleporter (East Bottom)', 'EDDM Flute']),
- create_cave_region(world, player, 'Superbunny Cave (Top)', 'a connector',
+ create_cave_region(multiworld, player, 'Superbunny Cave (Top)', 'a connector',
['Superbunny Cave - Top', 'Superbunny Cave - Bottom'], ['Superbunny Cave Exit (Top)']),
- create_cave_region(world, player, 'Superbunny Cave (Bottom)', 'a connector', None,
+ create_cave_region(multiworld, player, 'Superbunny Cave (Bottom)', 'a connector', None,
['Superbunny Cave Climb', 'Superbunny Cave Exit (Bottom)']),
- create_cave_region(world, player, 'Spike Cave', 'Spike Cave', ['Spike Cave']),
- create_cave_region(world, player, 'Hookshot Cave', 'a connector',
+ create_cave_region(multiworld, player, 'Spike Cave', 'Spike Cave', ['Spike Cave']),
+ create_cave_region(multiworld, player, 'Hookshot Cave', 'a connector',
['Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right',
'Hookshot Cave - Bottom Left'],
['Hookshot Cave Exit (South)', 'Hookshot Cave Bomb Wall (South)']),
- create_cave_region(world, player, 'Hookshot Cave (Upper)', 'a connector', None, ['Hookshot Cave Exit (North)',
+ create_cave_region(multiworld, player, 'Hookshot Cave (Upper)', 'a connector', None, ['Hookshot Cave Exit (North)',
'Hookshot Cave Bomb Wall (North)']),
- create_dw_region(world, player, 'Death Mountain Floating Island (Dark World)', None,
+ create_dw_region(multiworld, player, 'Death Mountain Floating Island (Dark World)', None,
['Floating Island Drop', 'Hookshot Cave Back Entrance']),
- create_cave_region(world, player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']),
+ create_cave_region(multiworld, player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']),
- create_dungeon_region(world, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None, ['Swamp Palace Moat', 'Swamp Palace Exit']),
- create_dungeon_region(world, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'], ['Swamp Palace Small Key Door']),
- create_dungeon_region(world, player, 'Swamp Palace (Starting Area)', 'Swamp Palace', ['Swamp Palace - Map Chest', 'Swamp Palace - Pot Row Pot Key',
+ create_dungeon_region(multiworld, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None, ['Swamp Palace Moat', 'Swamp Palace Exit']),
+ create_dungeon_region(multiworld, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'], ['Swamp Palace Small Key Door']),
+ create_dungeon_region(multiworld, player, 'Swamp Palace (Starting Area)', 'Swamp Palace', ['Swamp Palace - Map Chest', 'Swamp Palace - Pot Row Pot Key',
'Swamp Palace - Trench 1 Pot Key'], ['Swamp Palace (Center)']),
- create_dungeon_region(world, player, 'Swamp Palace (Center)', 'Swamp Palace', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', 'Swamp Palace - Hookshot Pot Key',
+ create_dungeon_region(multiworld, player, 'Swamp Palace (Center)', 'Swamp Palace', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', 'Swamp Palace - Hookshot Pot Key',
'Swamp Palace - Trench 2 Pot Key'], ['Swamp Palace (North)', 'Swamp Palace (West)']),
- create_dungeon_region(world, player, 'Swamp Palace (West)', 'Swamp Palace', ['Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest']),
- create_dungeon_region(world, player, 'Swamp Palace (North)', 'Swamp Palace', ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right',
+ create_dungeon_region(multiworld, player, 'Swamp Palace (West)', 'Swamp Palace', ['Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest']),
+ create_dungeon_region(multiworld, player, 'Swamp Palace (North)', 'Swamp Palace', ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right',
'Swamp Palace - Waterway Pot Key', 'Swamp Palace - Waterfall Room',
'Swamp Palace - Boss', 'Swamp Palace - Prize']),
- create_dungeon_region(world, player, 'Thieves Town (Entrance)', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest',
+ create_dungeon_region(multiworld, player, 'Thieves Town (Entrance)', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest',
'Thieves\' Town - Map Chest',
'Thieves\' Town - Compass Chest',
'Thieves\' Town - Ambush Chest'], ['Thieves Town Big Key Door', 'Thieves Town Exit']),
- create_dungeon_region(world, player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic',
+ create_dungeon_region(multiworld, player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic',
'Thieves\' Town - Big Chest',
'Thieves\' Town - Hallway Pot Key',
'Thieves\' Town - Spike Switch Pot Key',
'Thieves\' Town - Blind\'s Cell'],
['Blind Fight']),
- create_dungeon_region(world, player, 'Blind Fight', 'Thieves\' Town', ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']),
- create_dungeon_region(world, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'], ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump', 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']),
- create_dungeon_region(world, player, 'Skull Woods First Section (Right)', 'Skull Woods', ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']),
- create_dungeon_region(world, player, 'Skull Woods First Section (Left)', 'Skull Woods', ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'], ['Skull Woods First Section (Left) Door to Exit', 'Skull Woods First Section (Left) Door to Right']),
- create_dungeon_region(world, player, 'Skull Woods First Section (Top)', 'Skull Woods', ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']),
- create_dungeon_region(world, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None, ['Skull Woods Second Section (Drop)']),
- create_dungeon_region(world, player, 'Skull Woods Second Section', 'Skull Woods', ['Skull Woods - Big Key Chest', 'Skull Woods - West Lobby Pot Key'], ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']),
- create_dungeon_region(world, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']),
- create_dungeon_region(world, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Spike Corner Key Drop', 'Skull Woods - Boss', 'Skull Woods - Prize']),
- create_dungeon_region(world, player, 'Ice Palace (Entrance)', 'Ice Palace', ['Ice Palace - Jelly Key Drop', 'Ice Palace - Compass Chest'], ['Ice Palace (Second Section)', 'Ice Palace Exit']),
- create_dungeon_region(world, player, 'Ice Palace (Second Section)', 'Ice Palace', ['Ice Palace - Conveyor Key Drop'], ['Ice Palace (Main)']),
- create_dungeon_region(world, player, 'Ice Palace (Main)', 'Ice Palace', ['Ice Palace - Freezor Chest',
+ create_dungeon_region(multiworld, player, 'Blind Fight', 'Thieves\' Town', ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']),
+ create_dungeon_region(multiworld, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'], ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump', 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']),
+ create_dungeon_region(multiworld, player, 'Skull Woods First Section (Right)', 'Skull Woods', ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']),
+ create_dungeon_region(multiworld, player, 'Skull Woods First Section (Left)', 'Skull Woods', ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'], ['Skull Woods First Section (Left) Door to Exit', 'Skull Woods First Section (Left) Door to Right']),
+ create_dungeon_region(multiworld, player, 'Skull Woods First Section (Top)', 'Skull Woods', ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']),
+ create_dungeon_region(multiworld, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None, ['Skull Woods Second Section (Drop)']),
+ create_dungeon_region(multiworld, player, 'Skull Woods Second Section', 'Skull Woods', ['Skull Woods - Big Key Chest', 'Skull Woods - West Lobby Pot Key'], ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']),
+ create_dungeon_region(multiworld, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']),
+ create_dungeon_region(multiworld, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Spike Corner Key Drop', 'Skull Woods - Boss', 'Skull Woods - Prize']),
+ create_dungeon_region(multiworld, player, 'Ice Palace (Entrance)', 'Ice Palace', ['Ice Palace - Jelly Key Drop', 'Ice Palace - Compass Chest'], ['Ice Palace (Second Section)', 'Ice Palace Exit']),
+ create_dungeon_region(multiworld, player, 'Ice Palace (Second Section)', 'Ice Palace', ['Ice Palace - Conveyor Key Drop'], ['Ice Palace (Main)']),
+ create_dungeon_region(multiworld, player, 'Ice Palace (Main)', 'Ice Palace', ['Ice Palace - Freezor Chest',
'Ice Palace - Many Pots Pot Key',
'Ice Palace - Big Chest', 'Ice Palace - Iced T Room'], ['Ice Palace (East)', 'Ice Palace (Kholdstare)']),
- create_dungeon_region(world, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'], ['Ice Palace (East Top)']),
- create_dungeon_region(world, player, 'Ice Palace (East Top)', 'Ice Palace', ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest', 'Ice Palace - Hammer Block Key Drop']),
- create_dungeon_region(world, player, 'Ice Palace (Kholdstare)', 'Ice Palace', ['Ice Palace - Boss', 'Ice Palace - Prize']),
- create_dungeon_region(world, player, 'Misery Mire (Entrance)', 'Misery Mire', None, ['Misery Mire Entrance Gap', 'Misery Mire Exit']),
- create_dungeon_region(world, player, 'Misery Mire (Main)', 'Misery Mire', ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby',
+ create_dungeon_region(multiworld, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'], ['Ice Palace (East Top)']),
+ create_dungeon_region(multiworld, player, 'Ice Palace (East Top)', 'Ice Palace', ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest', 'Ice Palace - Hammer Block Key Drop']),
+ create_dungeon_region(multiworld, player, 'Ice Palace (Kholdstare)', 'Ice Palace', ['Ice Palace - Boss', 'Ice Palace - Prize']),
+ create_dungeon_region(multiworld, player, 'Misery Mire (Entrance)', 'Misery Mire', None, ['Misery Mire Entrance Gap', 'Misery Mire Exit']),
+ create_dungeon_region(multiworld, player, 'Misery Mire (Main)', 'Misery Mire', ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby',
'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest',
'Misery Mire - Spikes Pot Key', 'Misery Mire - Fishbone Pot Key',
'Misery Mire - Conveyor Crystal Key Drop'], ['Misery Mire (West)', 'Misery Mire Big Key Door']),
- create_dungeon_region(world, player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']),
- create_dungeon_region(world, player, 'Misery Mire (Final Area)', 'Misery Mire', None, ['Misery Mire (Vitreous)']),
- create_dungeon_region(world, player, 'Misery Mire (Vitreous)', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize']),
- create_dungeon_region(world, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']),
- create_dungeon_region(world, player, 'Turtle Rock (First Section)', 'Turtle Rock', ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left',
+ create_dungeon_region(multiworld, player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']),
+ create_dungeon_region(multiworld, player, 'Misery Mire (Final Area)', 'Misery Mire', None, ['Misery Mire (Vitreous)']),
+ create_dungeon_region(multiworld, player, 'Misery Mire (Vitreous)', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize']),
+ create_dungeon_region(multiworld, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']),
+ create_dungeon_region(multiworld, player, 'Turtle Rock (First Section)', 'Turtle Rock', ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left',
'Turtle Rock - Roller Room - Right'],
['Turtle Rock Entrance to Pokey Room', 'Turtle Rock Entrance Gap Reverse']),
- create_dungeon_region(world, player, 'Turtle Rock (Pokey Room)', 'Turtle Rock', ['Turtle Rock - Pokey 1 Key Drop'], ['Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Pokey Room) (South)']),
- create_dungeon_region(world, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'],
+ create_dungeon_region(multiworld, player, 'Turtle Rock (Pokey Room)', 'Turtle Rock', ['Turtle Rock - Pokey 1 Key Drop'], ['Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Pokey Room) (South)']),
+ create_dungeon_region(multiworld, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'],
['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']),
- create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock',
+ create_dungeon_region(multiworld, player, 'Turtle Rock (Second Section)', 'Turtle Rock',
['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'],
['Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door',
'Turtle Rock Second Section Bomb Wall']),
- create_dungeon_region(world, player, 'Turtle Rock (Second Section Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Second Section from Bomb Wall']),
- create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
- create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
- create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
- create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Isolated Ledge Exit', 'Turtle Rock Eye Bridge from Bomb Wall']),
- create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
+ create_dungeon_region(multiworld, player, 'Turtle Rock (Second Section Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Second Section from Bomb Wall']),
+ create_dungeon_region(multiworld, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
+ create_dungeon_region(multiworld, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
+ create_dungeon_region(multiworld, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
+ create_dungeon_region(multiworld, player, 'Turtle Rock (Eye Bridge Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Isolated Ledge Exit', 'Turtle Rock Eye Bridge from Bomb Wall']),
+ create_dungeon_region(multiworld, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'],
['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Eye Bridge Bomb Wall']),
- create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
- create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']),
- create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
+ create_dungeon_region(multiworld, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
+ create_dungeon_region(multiworld, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']),
+ create_dungeon_region(multiworld, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
['Palace of Darkness Big Key Chest Staircase', 'Palace of Darkness (North)', 'Palace of Darkness Big Key Door']),
- create_dungeon_region(world, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness', ['Palace of Darkness - Big Key Chest']),
- create_dungeon_region(world, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'], ['Palace of Darkness Hammer Peg Drop']),
- create_dungeon_region(world, player, 'Palace of Darkness (North)', 'Palace of Darkness', ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right'],
+ create_dungeon_region(multiworld, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness', ['Palace of Darkness - Big Key Chest']),
+ create_dungeon_region(multiworld, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'], ['Palace of Darkness Hammer Peg Drop']),
+ create_dungeon_region(multiworld, player, 'Palace of Darkness (North)', 'Palace of Darkness', ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right'],
['Palace of Darkness Spike Statue Room Door', 'Palace of Darkness Maze Door']),
- create_dungeon_region(world, player, 'Palace of Darkness (Maze)', 'Palace of Darkness', ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest']),
- create_dungeon_region(world, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness', ['Palace of Darkness - Harmless Hellway']),
- create_dungeon_region(world, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']),
- create_dungeon_region(world, player, 'Inverted Ganons Tower (Entrance)', 'Ganon\'s Tower',
+ create_dungeon_region(multiworld, player, 'Palace of Darkness (Maze)', 'Palace of Darkness', ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest']),
+ create_dungeon_region(multiworld, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness', ['Palace of Darkness - Harmless Hellway']),
+ create_dungeon_region(multiworld, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']),
+ create_dungeon_region(multiworld, player, 'Inverted Ganons Tower (Entrance)', 'Ganon\'s Tower',
['Ganons Tower - Bob\'s Torch', 'Ganons Tower - Hope Room - Left',
'Ganons Tower - Hope Room - Right', 'Ganons Tower - Conveyor Cross Pot Key'],
['Ganons Tower (Tile Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower Big Key Door',
'Inverted Ganons Tower Exit']),
- create_dungeon_region(world, player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'],
+ create_dungeon_region(multiworld, player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'],
['Ganons Tower (Tile Room) Key Door']),
- create_dungeon_region(world, player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower',
+ create_dungeon_region(multiworld, player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower',
['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right',
'Ganons Tower - Compass Room - Bottom Left',
'Ganons Tower - Compass Room - Bottom Right',
'Ganons Tower - Conveyor Star Pits Pot Key'],
['Ganons Tower (Bottom) (East)']),
- create_dungeon_region(world, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower',
+ create_dungeon_region(multiworld, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower',
['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right',
'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right',
'Ganons Tower - Double Switch Pot Key'],
['Ganons Tower (Map Room)', 'Ganons Tower (Double Switch Room)']),
- create_dungeon_region(world, player, 'Ganons Tower (Map Room)', 'Ganon\'s Tower', ['Ganons Tower - Map Chest']),
- create_dungeon_region(world, player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower',
+ create_dungeon_region(multiworld, player, 'Ganons Tower (Map Room)', 'Ganon\'s Tower', ['Ganons Tower - Map Chest']),
+ create_dungeon_region(multiworld, player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower',
['Ganons Tower - Firesnake Room'], ['Ganons Tower (Firesnake Room)']),
- create_dungeon_region(world, player, 'Ganons Tower (Teleport Room)', 'Ganon\'s Tower',
+ create_dungeon_region(multiworld, player, 'Ganons Tower (Teleport Room)', 'Ganon\'s Tower',
['Ganons Tower - Randomizer Room - Top Left',
'Ganons Tower - Randomizer Room - Top Right',
'Ganons Tower - Randomizer Room - Bottom Left',
'Ganons Tower - Randomizer Room - Bottom Right'],
['Ganons Tower (Bottom) (West)']),
- create_dungeon_region(world, player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower',
+ create_dungeon_region(multiworld, player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower',
['Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest',
'Ganons Tower - Big Key Room - Left',
'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest']),
- create_dungeon_region(world, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None, ['Ganons Tower Torch Rooms']),
- create_dungeon_region(world, player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower',
+ create_dungeon_region(multiworld, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None, ['Ganons Tower Torch Rooms']),
+ create_dungeon_region(multiworld, player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower',
['Ganons Tower - Mini Helmasaur Room - Left',
'Ganons Tower - Mini Helmasaur Room - Right',
'Ganons Tower - Pre-Moldorm Chest', 'Ganons Tower - Mini Helmasaur Key Drop'],
['Ganons Tower Moldorm Door']),
- create_dungeon_region(world, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None, ['Ganons Tower Moldorm Gap']),
+ create_dungeon_region(multiworld, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None, ['Ganons Tower Moldorm Gap']),
- create_dungeon_region(world, player, 'Agahnim 2', 'Ganon\'s Tower', ['Ganons Tower - Validation Chest', 'Agahnim 2'], None),
- create_cave_region(world, player, 'Pyramid', 'a drop\'s exit', ['Ganon'], ['Ganon Drop']),
- create_cave_region(world, player, 'Bottom of Pyramid', 'a drop\'s exit', None, ['Pyramid Exit']),
- create_dw_region(world, player, 'Pyramid Ledge', None, ['Pyramid Drop']), # houlihan room exits here in inverted
+ create_dungeon_region(multiworld, player, 'Agahnim 2', 'Ganon\'s Tower', ['Ganons Tower - Validation Chest', 'Agahnim 2'], None),
+ create_cave_region(multiworld, player, 'Pyramid', 'a drop\'s exit', ['Ganon'], ['Ganon Drop']),
+ create_cave_region(multiworld, player, 'Bottom of Pyramid', 'a drop\'s exit', None, ['Pyramid Exit']),
+ create_dw_region(multiworld, player, 'Pyramid Ledge', None, ['Pyramid Drop']), # houlihan room exits here in inverted
# to simplify flute connections
- create_cave_region(world, player, 'The Sky', 'A Dark Sky', None,
+ create_cave_region(multiworld, player, 'The Sky', 'A Dark Sky', None,
['DDM Landing', 'NEDW Landing', 'WDW Landing', 'SDW Landing', 'EDW Landing', 'DD Landing',
'DLHL Landing']),
- create_lw_region(world, player, 'Desert Northern Cliffs'),
- create_lw_region(world, player, 'Death Mountain Bunny Descent Area')
+ create_lw_region(multiworld, player, 'Desert Northern Cliffs'),
+ create_lw_region(multiworld, player, 'Death Mountain Bunny Descent Area')
]
-def mark_dark_world_regions(world, player):
+def mark_dark_world_regions(multiworld: MultiWorld, player: int):
# cross world caves may have some sections marked as both in_light_world, and in_dark_work.
# That is ok. the bunny logic will check for this case and incorporate special rules.
- queue = collections.deque(region for region in world.get_regions(player) if region.type == LTTPRegionType.DarkWorld)
+ queue = collections.deque(region for region in multiworld.get_regions(player) if region.type == LTTPRegionType.DarkWorld)
seen = set(queue)
while queue:
current = queue.popleft()
@@ -499,7 +500,7 @@ def mark_dark_world_regions(world, player):
seen.add(exit.connected_region)
queue.append(exit.connected_region)
- queue = collections.deque(region for region in world.get_regions(player) if region.type == LTTPRegionType.LightWorld)
+ queue = collections.deque(region for region in multiworld.get_regions(player) if region.type == LTTPRegionType.LightWorld)
seen = set(queue)
while queue:
current = queue.popleft()
diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py
index 6b0968f6e598..c7dc7a694848 100644
--- a/worlds/alttp/ItemPool.py
+++ b/worlds/alttp/ItemPool.py
@@ -1,8 +1,9 @@
from collections import namedtuple
import logging
-from BaseClasses import ItemClassification
+from BaseClasses import ItemClassification, MultiWorld
from Options import OptionError
+from typing import TYPE_CHECKING
from .SubClasses import ALttPLocation, LTTPRegion, LTTPRegionType
from .Shops import TakeAny, total_shop_slots, set_up_shops, shop_table_by_location, ShopType
@@ -14,6 +15,9 @@
from .StateHelpers import has_triforce_pieces, has_melee_weapon
from .Regions import key_drop_data
+if TYPE_CHECKING:
+ from . import ALTTPWorld
+
# This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space.
# Some basic items that various modes require are placed here, including pendants and crystals. Medallion requirements for the two relevant entrances are also decided.
@@ -222,7 +226,7 @@
)
-def generate_itempool(world):
+def generate_itempool(world: "ALTTPWorld"):
player: int = world.player
multiworld = world.multiworld
@@ -531,7 +535,7 @@ def cut_item(items, item_to_cut, minimum_items):
take_any_locations.sort()
-def set_up_take_anys(multiworld, world, player):
+def set_up_take_anys(multiworld: MultiWorld, world: "ALTTPWorld", player: int):
# these are references, do not modify these lists in-place
if world.options.mode == 'inverted':
take_any_locs = take_any_locations_inverted
@@ -585,15 +589,15 @@ def set_up_take_anys(multiworld, world, player):
location.place_locked_item(item_factory("Boss Heart Container", world))
-def get_pool_core(world, player: int):
- shuffle = world.worlds[player].options.entrance_shuffle.current_key
- difficulty = world.worlds[player].options.item_pool.current_key
- timer = world.worlds[player].options.timer.current_key
- goal = world.worlds[player].options.goal.current_key
- mode = world.worlds[player].options.mode.current_key
- swordless = world.worlds[player].options.swordless
- retro_bow = world.worlds[player].options.retro_bow
- logic = world.worlds[player].options.glitches_required
+def get_pool_core(multiworld: MultiWorld, player: int):
+ shuffle = multiworld.worlds[player].options.entrance_shuffle.current_key
+ difficulty = multiworld.worlds[player].options.item_pool.current_key
+ timer = multiworld.worlds[player].options.timer.current_key
+ goal = multiworld.worlds[player].options.goal.current_key
+ mode = multiworld.worlds[player].options.mode.current_key
+ swordless = multiworld.worlds[player].options.swordless
+ retro_bow = multiworld.worlds[player].options.retro_bow
+ logic = multiworld.worlds[player].options.glitches_required
pool = []
placed_items = {}
@@ -610,13 +614,13 @@ def place_item(loc, item):
placed_items[loc] = item
# provide boots to major glitch dependent seeds
- if logic.current_key in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and world.worlds[player].options.glitch_boots:
+ if logic.current_key in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and multiworld.worlds[player].options.glitch_boots:
precollected_items.append('Pegasus Boots')
pool.remove('Pegasus Boots')
pool.append('Rupees (20)')
- want_progressives = world.worlds[player].options.progressive.want_progressives
+ want_progressives = multiworld.worlds[player].options.progressive.want_progressives
- if want_progressives(world.random):
+ if want_progressives(multiworld.random):
pool.extend(diff.progressiveglove)
else:
pool.extend(diff.basicglove)
@@ -640,27 +644,27 @@ def place_item(loc, item):
thisbottle = None
for _ in range(diff.bottle_count):
if not diff.same_bottle or not thisbottle:
- thisbottle = world.random.choice(diff.bottles)
+ thisbottle = multiworld.random.choice(diff.bottles)
pool.append(thisbottle)
- if want_progressives(world.random):
+ if want_progressives(multiworld.random):
pool.extend(diff.progressiveshield)
else:
pool.extend(diff.basicshield)
- if want_progressives(world.random):
+ if want_progressives(multiworld.random):
pool.extend(diff.progressivearmor)
else:
pool.extend(diff.basicarmor)
- if want_progressives(world.random):
+ if want_progressives(multiworld.random):
pool.extend(diff.progressivemagic)
else:
pool.extend(diff.basicmagic)
- if want_progressives(world.random):
+ if want_progressives(multiworld.random):
pool.extend(diff.progressivebow)
- world.worlds[player].has_progressive_bows = True
+ multiworld.worlds[player].has_progressive_bows = True
elif (swordless or logic == 'no_glitches'):
swordless_bows = ['Bow', 'Silver Bow']
if difficulty == "easy":
@@ -672,7 +676,7 @@ def place_item(loc, item):
if swordless:
pool.extend(diff.swordless)
else:
- progressive_swords = want_progressives(world.random)
+ progressive_swords = want_progressives(multiworld.random)
pool.extend(diff.progressivesword if progressive_swords else diff.basicsword)
extraitems = total_items_to_place - len(pool) - len(placed_items)
@@ -688,29 +692,29 @@ def place_item(loc, item):
additional_pieces_to_place = 0
if 'triforce_hunt' in goal:
- if world.worlds[player].options.triforce_pieces_mode.value == TriforcePiecesMode.option_extra:
- treasure_hunt_total = (world.worlds[player].options.triforce_pieces_required.value
- + world.worlds[player].options.triforce_pieces_extra.value)
- elif world.worlds[player].options.triforce_pieces_mode.value == TriforcePiecesMode.option_percentage:
- percentage = float(world.worlds[player].options.triforce_pieces_percentage.value) / 100
- treasure_hunt_total = int(round(world.worlds[player].options.triforce_pieces_required.value * percentage, 0))
+ if multiworld.worlds[player].options.triforce_pieces_mode.value == TriforcePiecesMode.option_extra:
+ treasure_hunt_total = (multiworld.worlds[player].options.triforce_pieces_required.value
+ + multiworld.worlds[player].options.triforce_pieces_extra.value)
+ elif multiworld.worlds[player].options.triforce_pieces_mode.value == TriforcePiecesMode.option_percentage:
+ percentage = float(multiworld.worlds[player].options.triforce_pieces_percentage.value) / 100
+ treasure_hunt_total = int(round(multiworld.worlds[player].options.triforce_pieces_required.value * percentage, 0))
else: # available
- treasure_hunt_total = world.worlds[player].options.triforce_pieces_available.value
+ treasure_hunt_total = multiworld.worlds[player].options.triforce_pieces_available.value
- triforce_pieces = min(90, max(treasure_hunt_total, world.worlds[player].options.triforce_pieces_required.value))
+ triforce_pieces = min(90, max(treasure_hunt_total, multiworld.worlds[player].options.triforce_pieces_required.value))
pieces_in_core = min(extraitems, triforce_pieces)
additional_pieces_to_place = triforce_pieces - pieces_in_core
pool.extend(["Triforce Piece"] * pieces_in_core)
extraitems -= pieces_in_core
- treasure_hunt_required = world.worlds[player].options.triforce_pieces_required.value
+ treasure_hunt_required = multiworld.worlds[player].options.triforce_pieces_required.value
for extra in diff.extras:
if extraitems >= len(extra):
pool.extend(extra)
extraitems -= len(extra)
elif extraitems > 0:
- pool.extend(world.random.sample(extra, extraitems))
+ pool.extend(multiworld.random.sample(extra, extraitems))
break
else:
break
@@ -729,25 +733,25 @@ def place_item(loc, item):
else:
break
- if world.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal:
+ if multiworld.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal:
pool.extend(diff.universal_keys)
if mode == 'standard':
- if world.worlds[player].options.key_drop_shuffle:
+ if multiworld.worlds[player].options.key_drop_shuffle:
key_locations = ['Secret Passage', 'Hyrule Castle - Map Guard Key Drop']
- key_location = world.random.choice(key_locations)
+ key_location = multiworld.random.choice(key_locations)
key_locations.remove(key_location)
place_item(key_location, "Small Key (Universal)")
key_locations += ['Hyrule Castle - Boomerang Guard Key Drop', 'Hyrule Castle - Boomerang Chest',
'Hyrule Castle - Map Chest']
- key_location = world.random.choice(key_locations)
+ key_location = multiworld.random.choice(key_locations)
key_locations.remove(key_location)
place_item(key_location, "Small Key (Universal)")
key_locations += ['Hyrule Castle - Big Key Drop', 'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross']
- key_location = world.random.choice(key_locations)
+ key_location = multiworld.random.choice(key_locations)
key_locations.remove(key_location)
place_item(key_location, "Small Key (Universal)")
key_locations += ['Sewers - Key Rat Key Drop']
- key_location = world.random.choice(key_locations)
+ key_location = multiworld.random.choice(key_locations)
place_item(key_location, "Small Key (Universal)")
pool = pool[:-3]
diff --git a/worlds/alttp/Items.py b/worlds/alttp/Items.py
index cbe6e9964232..1c173c619e3f 100644
--- a/worlds/alttp/Items.py
+++ b/worlds/alttp/Items.py
@@ -1,24 +1,24 @@
import typing
-from BaseClasses import ItemClassification as IC
+from BaseClasses import MultiWorld, ItemClassification as IC
from worlds.AutoWorld import World
-def GetBeemizerItem(world, player: int, item):
+def GetBeemizerItem(multiworld: MultiWorld, player: int, item):
item_name = item if isinstance(item, str) else item.name
- if item_name not in trap_replaceable or player in world.groups:
+ if item_name not in trap_replaceable or player in multiworld.groups:
return item
# first roll - replaceable item should be replaced, within beemizer_total_chance
- if not world.worlds[player].options.beemizer_total_chance or world.random.random() > (world.worlds[player].options.beemizer_total_chance / 100):
+ if not multiworld.worlds[player].options.beemizer_total_chance or multiworld.random.random() > (multiworld.worlds[player].options.beemizer_total_chance / 100):
return item
# second roll - bee replacement should be trap, within beemizer_trap_chance
- if not world.worlds[player].options.beemizer_trap_chance or world.random.random() > (world.worlds[player].options.beemizer_trap_chance / 100):
- return "Bee" if isinstance(item, str) else world.create_item("Bee", player)
+ if not multiworld.worlds[player].options.beemizer_trap_chance or multiworld.random.random() > (multiworld.worlds[player].options.beemizer_trap_chance / 100):
+ return "Bee" if isinstance(item, str) else multiworld.create_item("Bee", player)
else:
- return "Bee Trap" if isinstance(item, str) else world.create_item("Bee Trap", player)
+ return "Bee Trap" if isinstance(item, str) else multiworld.create_item("Bee Trap", player)
def item_factory(items: typing.Union[str, typing.Iterable[str]], world: World):
diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py
index 519241d7f4a9..53bdbd6cba06 100644
--- a/worlds/alttp/Options.py
+++ b/worlds/alttp/Options.py
@@ -154,13 +154,13 @@ class OpenPyramid(Choice):
alias_true = option_open
alias_false = option_closed
- def to_bool(self, world: MultiWorld, player: int) -> bool:
+ def to_bool(self, multiworld: MultiWorld, player: int) -> bool:
if self.value == self.option_goal:
- return world.worlds[player].options.goal.current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'}
+ return multiworld.worlds[player].options.goal.current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'}
elif self.value == self.option_auto:
- return world.worlds[player].options.goal.current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'} \
- and (world.worlds[player].options.entrance_shuffle.current_key in {'vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed'} or not
- world.shuffle_ganon)
+ return multiworld.worlds[player].options.goal.current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'} \
+ and (multiworld.worlds[player].options.entrance_shuffle.current_key in {'vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed'} or not
+ multiworld.shuffle_ganon)
elif self.value == self.option_open:
return True
else:
diff --git a/worlds/alttp/OverworldGlitchRules.py b/worlds/alttp/OverworldGlitchRules.py
index aeff9cb88e9a..a243569cca57 100644
--- a/worlds/alttp/OverworldGlitchRules.py
+++ b/worlds/alttp/OverworldGlitchRules.py
@@ -2,6 +2,7 @@
Helper functions to deliver entrance/exit/region sets to OWG rules.
"""
+from BaseClasses import MultiWorld
from .StateHelpers import can_lift_heavy_rocks, can_boots_clip_lw, can_boots_clip_dw, can_get_glitched_speed_dw
@@ -200,7 +201,7 @@ def get_mirror_offset_spots_dw():
yield ('Dark Death Mountain Offset Mirror', 'Dark Death Mountain (West Bottom)', 'East Dark World')
-def get_mirror_offset_spots_lw(player):
+def get_mirror_offset_spots_lw(player: int):
"""
Mirror shenanigans placing a mirror portal with a broken camera
"""
@@ -218,54 +219,54 @@ def get_invalid_bunny_revival_dungeons():
yield 'Sanctuary'
-def overworld_glitch_connections(world, player):
+def overworld_glitch_connections(multiworld: MultiWorld, player: int):
# Boots-accessible locations.
- create_owg_connections(player, world, get_boots_clip_exits_lw(world.worlds[player].options.mode == 'inverted'))
- create_owg_connections(player, world, get_boots_clip_exits_dw(world.worlds[player].options.mode == 'inverted', player))
+ create_owg_connections(player, multiworld, get_boots_clip_exits_lw(multiworld.worlds[player].options.mode == 'inverted'))
+ create_owg_connections(player, multiworld, get_boots_clip_exits_dw(multiworld.worlds[player].options.mode == 'inverted', player))
# Glitched speed drops.
- create_owg_connections(player, world, get_glitched_speed_drops_dw(world.worlds[player].options.mode == 'inverted'))
+ create_owg_connections(player, multiworld, get_glitched_speed_drops_dw(multiworld.worlds[player].options.mode == 'inverted'))
# Mirror clip spots.
- if world.worlds[player].options.mode != 'inverted':
- create_owg_connections(player, world, get_mirror_clip_spots_dw())
- create_owg_connections(player, world, get_mirror_offset_spots_dw())
+ if multiworld.worlds[player].options.mode != 'inverted':
+ create_owg_connections(player, multiworld, get_mirror_clip_spots_dw())
+ create_owg_connections(player, multiworld, get_mirror_offset_spots_dw())
else:
- create_owg_connections(player, world, get_mirror_offset_spots_lw(player))
+ create_owg_connections(player, multiworld, get_mirror_offset_spots_lw(player))
-def overworld_glitches_rules(world, player):
+def overworld_glitches_rules(multiworld: MultiWorld, player: int):
# Boots-accessible locations.
- set_owg_connection_rules(player, world, get_boots_clip_exits_lw(world.worlds[player].options.mode == 'inverted'), lambda state: can_boots_clip_lw(state, player))
- set_owg_connection_rules(player, world, get_boots_clip_exits_dw(world.worlds[player].options.mode == 'inverted', player), lambda state: can_boots_clip_dw(state, player))
+ set_owg_connection_rules(player, multiworld, get_boots_clip_exits_lw(multiworld.worlds[player].options.mode == 'inverted'), lambda state: can_boots_clip_lw(state, player))
+ set_owg_connection_rules(player, multiworld, get_boots_clip_exits_dw(multiworld.worlds[player].options.mode == 'inverted', player), lambda state: can_boots_clip_dw(state, player))
# Glitched speed drops.
- set_owg_connection_rules(player, world, get_glitched_speed_drops_dw(world.worlds[player].options.mode == 'inverted'), lambda state: can_get_glitched_speed_dw(state, player))
+ set_owg_connection_rules(player, multiworld, get_glitched_speed_drops_dw(multiworld.worlds[player].options.mode == 'inverted'), lambda state: can_get_glitched_speed_dw(state, player))
# Dark Death Mountain Ledge Clip Spot also accessible with mirror.
- if world.worlds[player].options.mode != 'inverted':
- add_alternate_rule(world.get_entrance('Dark Death Mountain Ledge Clip Spot', player), lambda state: state.has('Magic Mirror', player))
+ if multiworld.worlds[player].options.mode != 'inverted':
+ add_alternate_rule(multiworld.get_entrance('Dark Death Mountain Ledge Clip Spot', player), lambda state: state.has('Magic Mirror', player))
# Mirror clip spots.
- if world.worlds[player].options.mode != 'inverted':
- set_owg_connection_rules(player, world, get_mirror_clip_spots_dw(), lambda state: state.has('Magic Mirror', player))
- set_owg_connection_rules(player, world, get_mirror_offset_spots_dw(), lambda state: state.has('Magic Mirror', player) and can_boots_clip_lw(state, player))
+ if multiworld.worlds[player].options.mode != 'inverted':
+ set_owg_connection_rules(player, multiworld, get_mirror_clip_spots_dw(), lambda state: state.has('Magic Mirror', player))
+ set_owg_connection_rules(player, multiworld, get_mirror_offset_spots_dw(), lambda state: state.has('Magic Mirror', player) and can_boots_clip_lw(state, player))
else:
- set_owg_connection_rules(player, world, get_mirror_offset_spots_lw(player), lambda state: state.has('Magic Mirror', player) and can_boots_clip_dw(state, player))
+ set_owg_connection_rules(player, multiworld, get_mirror_offset_spots_lw(player), lambda state: state.has('Magic Mirror', player) and can_boots_clip_dw(state, player))
# Regions that require the boots and some other stuff.
- if world.worlds[player].options.mode != 'inverted':
- world.get_entrance('Turtle Rock Teleporter', player).access_rule = lambda state: (can_boots_clip_lw(state, player) or can_lift_heavy_rocks(state, player)) and state.has('Hammer', player)
- add_alternate_rule(world.get_entrance('Waterfall of Wishing', player), lambda state: state.has('Moon Pearl', player) or state.has('Pegasus Boots', player))
+ if multiworld.worlds[player].options.mode != 'inverted':
+ multiworld.get_entrance('Turtle Rock Teleporter', player).access_rule = lambda state: (can_boots_clip_lw(state, player) or can_lift_heavy_rocks(state, player)) and state.has('Hammer', player)
+ add_alternate_rule(multiworld.get_entrance('Waterfall of Wishing', player), lambda state: state.has('Moon Pearl', player) or state.has('Pegasus Boots', player))
else:
- add_alternate_rule(world.get_entrance('Waterfall of Wishing Cave', player), lambda state: state.has('Moon Pearl', player))
+ add_alternate_rule(multiworld.get_entrance('Waterfall of Wishing Cave', player), lambda state: state.has('Moon Pearl', player))
- world.get_entrance('Dark Desert Teleporter', player).access_rule = lambda state: (state.has('Flute', player) or state.has('Pegasus Boots', player)) and can_lift_heavy_rocks(state, player)
- add_alternate_rule(world.get_entrance('Catfish Exit Rock', player), lambda state: can_boots_clip_dw(state, player))
- add_alternate_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: can_boots_clip_dw(state, player))
+ multiworld.get_entrance('Dark Desert Teleporter', player).access_rule = lambda state: (state.has('Flute', player) or state.has('Pegasus Boots', player)) and can_lift_heavy_rocks(state, player)
+ add_alternate_rule(multiworld.get_entrance('Catfish Exit Rock', player), lambda state: can_boots_clip_dw(state, player))
+ add_alternate_rule(multiworld.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: can_boots_clip_dw(state, player))
# Zora's Ledge via waterwalk setup.
- add_alternate_rule(world.get_location('Zora\'s Ledge', player), lambda state: state.has('Pegasus Boots', player))
+ add_alternate_rule(multiworld.get_location('Zora\'s Ledge', player), lambda state: state.has('Pegasus Boots', player))
def add_alternate_rule(entrance, rule):
@@ -273,22 +274,22 @@ def add_alternate_rule(entrance, rule):
entrance.access_rule = lambda state: old_rule(state) or rule(state)
-def create_no_logic_connections(player, world, connections):
+def create_no_logic_connections(player: int, multiworld: MultiWorld, connections):
for entrance, parent_region, target_region, *rule_override in connections:
- parent = world.get_region(parent_region, player)
- target = world.get_region(target_region, player)
+ parent = multiworld.get_region(parent_region, player)
+ target = multiworld.get_region(target_region, player)
parent.connect(target, entrance)
-def create_owg_connections(player, world, connections):
+def create_owg_connections(player: int, multiworld: MultiWorld, connections):
for entrance, parent_region, target_region, *rule_override in connections:
- parent = world.get_region(parent_region, player)
- target = world.get_region(target_region, player)
+ parent = multiworld.get_region(parent_region, player)
+ target = multiworld.get_region(target_region, player)
parent.connect(target, entrance)
-def set_owg_connection_rules(player, world, connections, default_rule):
+def set_owg_connection_rules(player: int, multiworld: MultiWorld, connections, default_rule):
for entrance, _, _, *rule_override in connections:
- connection = world.get_entrance(entrance, player)
+ connection = multiworld.get_entrance(entrance, player)
rule = rule_override[0] if len(rule_override) > 0 else default_rule
connection.access_rule = rule
diff --git a/worlds/alttp/Regions.py b/worlds/alttp/Regions.py
index c2af7956373e..d3789f91baf0 100644
--- a/worlds/alttp/Regions.py
+++ b/worlds/alttp/Regions.py
@@ -9,11 +9,11 @@ def is_main_entrance(entrance: LTTPEntrance) -> bool:
return entrance.parent_region.type in {LTTPRegionType.DarkWorld, LTTPRegionType.LightWorld} if entrance.parent_region.type else True
-def create_regions(world, player):
+def create_regions(multiworld: MultiWorld, player: int):
- world.regions += [
- create_lw_region(world, player, 'Menu', None, ['Links House S&Q', 'Sanctuary S&Q', 'Old Man S&Q']),
- create_lw_region(world, player, 'Light World', ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure',
+ multiworld.regions += [
+ create_lw_region(multiworld, player, 'Menu', None, ['Links House S&Q', 'Sanctuary S&Q', 'Old Man S&Q']),
+ create_lw_region(multiworld, player, 'Light World', ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure',
'Purple Chest', 'Flute Activation Spot'],
["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Zoras River', 'Kings Grave Outer Rocks', 'Dam',
'Links House', 'Tavern North', 'Chicken House', 'Aginahs Cave', 'Sahasrahlas Hut', 'Kakariko Well Drop', 'Kakariko Well Cave',
@@ -24,122 +24,122 @@ def create_regions(world, player):
'Elder House (East)', 'Elder House (West)', 'North Fairy Cave', 'North Fairy Cave Drop', 'Lost Woods Gamble', 'Snitch Lady (East)', 'Snitch Lady (West)', 'Tavern (Front)',
'Bush Covered House', 'Light World Bomb Hut', 'Kakariko Shop', 'Long Fairy Cave', 'Good Bee Cave', '20 Rupee Cave', 'Cave Shop (Lake Hylia)', 'Waterfall of Wishing', 'Hyrule Castle Main Gate',
'Bonk Fairy (Light)', '50 Rupee Cave', 'Fortune Teller (Light)', 'Lake Hylia Fairy', 'Light Hype Fairy', 'Desert Fairy', 'Lumberjack House', 'Lake Hylia Fortune Teller', 'Kakariko Gamble Game', 'Top of Pyramid']),
- create_lw_region(world, player, 'Death Mountain Entrance', None, ['Old Man Cave (West)', 'Death Mountain Entrance Drop']),
- create_lw_region(world, player, 'Lake Hylia Central Island', None, ['Capacity Upgrade', 'Lake Hylia Central Island Teleporter']),
- create_cave_region(world, player, 'Blinds Hideout', 'a bounty of five items', ["Blind\'s Hideout - Top",
+ create_lw_region(multiworld, player, 'Death Mountain Entrance', None, ['Old Man Cave (West)', 'Death Mountain Entrance Drop']),
+ create_lw_region(multiworld, player, 'Lake Hylia Central Island', None, ['Capacity Upgrade', 'Lake Hylia Central Island Teleporter']),
+ create_cave_region(multiworld, player, 'Blinds Hideout', 'a bounty of five items', ["Blind\'s Hideout - Top",
"Blind\'s Hideout - Left",
"Blind\'s Hideout - Right",
"Blind\'s Hideout - Far Left",
"Blind\'s Hideout - Far Right"]),
- create_cave_region(world, player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit', ['Link\'s Uncle', 'Secret Passage'], ['Hyrule Castle Secret Entrance Exit']),
- create_lw_region(world, player, 'Zoras River', ['King Zora', 'Zora\'s Ledge']),
- create_cave_region(world, player, 'Waterfall of Wishing', 'a cave with two chests', ['Waterfall Fairy - Left', 'Waterfall Fairy - Right']),
- create_lw_region(world, player, 'Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']),
- create_cave_region(world, player, 'Kings Grave', 'a cave with a chest', ['King\'s Tomb']),
- create_cave_region(world, player, 'North Fairy Cave', 'a drop\'s exit', None, ['North Fairy Cave Exit']),
- create_cave_region(world, player, 'Dam', 'the dam', ['Floodgate', 'Floodgate Chest']),
- create_cave_region(world, player, 'Links House', 'your house', ['Link\'s House'], ['Links House Exit']),
- create_cave_region(world, player, 'Chris Houlihan Room', 'I AM ERROR', None, ['Chris Houlihan Room Exit']),
- create_cave_region(world, player, 'Tavern', 'the tavern', ['Kakariko Tavern']),
- create_cave_region(world, player, 'Elder House', 'a connector', None, ['Elder House Exit (East)', 'Elder House Exit (West)']),
- create_cave_region(world, player, 'Snitch Lady (East)', 'a boring house'),
- create_cave_region(world, player, 'Snitch Lady (West)', 'a boring house'),
- create_cave_region(world, player, 'Bush Covered House', 'the grass man'),
- create_cave_region(world, player, 'Tavern (Front)', 'the tavern'),
- create_cave_region(world, player, 'Light World Bomb Hut', 'a restock room'),
- create_cave_region(world, player, 'Kakariko Shop', 'a common shop'),
- create_cave_region(world, player, 'Fortune Teller (Light)', 'a fortune teller'),
- create_cave_region(world, player, 'Lake Hylia Fortune Teller', 'a fortune teller'),
- create_cave_region(world, player, 'Lumberjack House', 'a boring house'),
- create_cave_region(world, player, 'Bonk Fairy (Light)', 'a fairy fountain'),
- create_cave_region(world, player, 'Bonk Fairy (Dark)', 'a fairy fountain'),
- create_cave_region(world, player, 'Lake Hylia Healer Fairy', 'a fairy fountain'),
- create_cave_region(world, player, 'Swamp Healer Fairy', 'a fairy fountain'),
- create_cave_region(world, player, 'Desert Healer Fairy', 'a fairy fountain'),
- create_cave_region(world, player, 'Dark Lake Hylia Healer Fairy', 'a fairy fountain'),
- create_cave_region(world, player, 'Dark Lake Hylia Ledge Healer Fairy', 'a fairy fountain'),
- create_cave_region(world, player, 'Dark Desert Healer Fairy', 'a fairy fountain'),
- create_cave_region(world, player, 'Dark Death Mountain Healer Fairy', 'a fairy fountain'),
- create_cave_region(world, player, 'Chicken House', 'a house with a chest', ['Chicken House']),
- create_cave_region(world, player, 'Aginahs Cave', 'a cave with a chest', ['Aginah\'s Cave']),
- create_cave_region(world, player, 'Sahasrahlas Hut', 'Sahasrahla', ['Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right', 'Sahasrahla']),
- create_cave_region(world, player, 'Kakariko Well (top)', 'a drop\'s exit', ['Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle',
+ create_cave_region(multiworld, player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit', ['Link\'s Uncle', 'Secret Passage'], ['Hyrule Castle Secret Entrance Exit']),
+ create_lw_region(multiworld, player, 'Zoras River', ['King Zora', 'Zora\'s Ledge']),
+ create_cave_region(multiworld, player, 'Waterfall of Wishing', 'a cave with two chests', ['Waterfall Fairy - Left', 'Waterfall Fairy - Right']),
+ create_lw_region(multiworld, player, 'Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']),
+ create_cave_region(multiworld, player, 'Kings Grave', 'a cave with a chest', ['King\'s Tomb']),
+ create_cave_region(multiworld, player, 'North Fairy Cave', 'a drop\'s exit', None, ['North Fairy Cave Exit']),
+ create_cave_region(multiworld, player, 'Dam', 'the dam', ['Floodgate', 'Floodgate Chest']),
+ create_cave_region(multiworld, player, 'Links House', 'your house', ['Link\'s House'], ['Links House Exit']),
+ create_cave_region(multiworld, player, 'Chris Houlihan Room', 'I AM ERROR', None, ['Chris Houlihan Room Exit']),
+ create_cave_region(multiworld, player, 'Tavern', 'the tavern', ['Kakariko Tavern']),
+ create_cave_region(multiworld, player, 'Elder House', 'a connector', None, ['Elder House Exit (East)', 'Elder House Exit (West)']),
+ create_cave_region(multiworld, player, 'Snitch Lady (East)', 'a boring house'),
+ create_cave_region(multiworld, player, 'Snitch Lady (West)', 'a boring house'),
+ create_cave_region(multiworld, player, 'Bush Covered House', 'the grass man'),
+ create_cave_region(multiworld, player, 'Tavern (Front)', 'the tavern'),
+ create_cave_region(multiworld, player, 'Light World Bomb Hut', 'a restock room'),
+ create_cave_region(multiworld, player, 'Kakariko Shop', 'a common shop'),
+ create_cave_region(multiworld, player, 'Fortune Teller (Light)', 'a fortune teller'),
+ create_cave_region(multiworld, player, 'Lake Hylia Fortune Teller', 'a fortune teller'),
+ create_cave_region(multiworld, player, 'Lumberjack House', 'a boring house'),
+ create_cave_region(multiworld, player, 'Bonk Fairy (Light)', 'a fairy fountain'),
+ create_cave_region(multiworld, player, 'Bonk Fairy (Dark)', 'a fairy fountain'),
+ create_cave_region(multiworld, player, 'Lake Hylia Healer Fairy', 'a fairy fountain'),
+ create_cave_region(multiworld, player, 'Swamp Healer Fairy', 'a fairy fountain'),
+ create_cave_region(multiworld, player, 'Desert Healer Fairy', 'a fairy fountain'),
+ create_cave_region(multiworld, player, 'Dark Lake Hylia Healer Fairy', 'a fairy fountain'),
+ create_cave_region(multiworld, player, 'Dark Lake Hylia Ledge Healer Fairy', 'a fairy fountain'),
+ create_cave_region(multiworld, player, 'Dark Desert Healer Fairy', 'a fairy fountain'),
+ create_cave_region(multiworld, player, 'Dark Death Mountain Healer Fairy', 'a fairy fountain'),
+ create_cave_region(multiworld, player, 'Chicken House', 'a house with a chest', ['Chicken House']),
+ create_cave_region(multiworld, player, 'Aginahs Cave', 'a cave with a chest', ['Aginah\'s Cave']),
+ create_cave_region(multiworld, player, 'Sahasrahlas Hut', 'Sahasrahla', ['Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right', 'Sahasrahla']),
+ create_cave_region(multiworld, player, 'Kakariko Well (top)', 'a drop\'s exit', ['Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle',
'Kakariko Well - Right', 'Kakariko Well - Bottom'], ['Kakariko Well (top to bottom)']),
- create_cave_region(world, player, 'Kakariko Well (bottom)', 'a drop\'s exit', None, ['Kakariko Well Exit']),
- create_cave_region(world, player, 'Blacksmiths Hut', 'the smith', ['Blacksmith', 'Missing Smith']),
- create_lw_region(world, player, 'Bat Cave Drop Ledge', None, ['Bat Cave Drop']),
- create_cave_region(world, player, 'Bat Cave (right)', 'a drop\'s exit', ['Magic Bat'], ['Bat Cave Door']),
- create_cave_region(world, player, 'Bat Cave (left)', 'a drop\'s exit', None, ['Bat Cave Exit']),
- create_cave_region(world, player, 'Sick Kids House', 'the sick kid', ['Sick Kid']),
- create_lw_region(world, player, 'Hobo Bridge', ['Hobo']),
- create_cave_region(world, player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'], ['Lost Woods Hideout (top to bottom)']),
- create_cave_region(world, player, 'Lost Woods Hideout (bottom)', 'a drop\'s exit', None, ['Lost Woods Hideout Exit']),
- create_cave_region(world, player, 'Lumberjack Tree (top)', 'a drop\'s exit', ['Lumberjack Tree'], ['Lumberjack Tree (top to bottom)']),
- create_cave_region(world, player, 'Lumberjack Tree (bottom)', 'a drop\'s exit', None, ['Lumberjack Tree Exit']),
- create_lw_region(world, player, 'Cave 45 Ledge', None, ['Cave 45']),
- create_cave_region(world, player, 'Cave 45', 'a cave with an item', ['Cave 45']),
- create_lw_region(world, player, 'Graveyard Ledge', None, ['Graveyard Cave']),
- create_cave_region(world, player, 'Graveyard Cave', 'a cave with an item', ['Graveyard Cave']),
- create_cave_region(world, player, 'Checkerboard Cave', 'a cave with an item', ['Checkerboard Cave']),
- create_cave_region(world, player, 'Long Fairy Cave', 'a fairy fountain'),
- create_cave_region(world, player, 'Mini Moldorm Cave', 'a bounty of five items', ['Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right',
+ create_cave_region(multiworld, player, 'Kakariko Well (bottom)', 'a drop\'s exit', None, ['Kakariko Well Exit']),
+ create_cave_region(multiworld, player, 'Blacksmiths Hut', 'the smith', ['Blacksmith', 'Missing Smith']),
+ create_lw_region(multiworld, player, 'Bat Cave Drop Ledge', None, ['Bat Cave Drop']),
+ create_cave_region(multiworld, player, 'Bat Cave (right)', 'a drop\'s exit', ['Magic Bat'], ['Bat Cave Door']),
+ create_cave_region(multiworld, player, 'Bat Cave (left)', 'a drop\'s exit', None, ['Bat Cave Exit']),
+ create_cave_region(multiworld, player, 'Sick Kids House', 'the sick kid', ['Sick Kid']),
+ create_lw_region(multiworld, player, 'Hobo Bridge', ['Hobo']),
+ create_cave_region(multiworld, player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'], ['Lost Woods Hideout (top to bottom)']),
+ create_cave_region(multiworld, player, 'Lost Woods Hideout (bottom)', 'a drop\'s exit', None, ['Lost Woods Hideout Exit']),
+ create_cave_region(multiworld, player, 'Lumberjack Tree (top)', 'a drop\'s exit', ['Lumberjack Tree'], ['Lumberjack Tree (top to bottom)']),
+ create_cave_region(multiworld, player, 'Lumberjack Tree (bottom)', 'a drop\'s exit', None, ['Lumberjack Tree Exit']),
+ create_lw_region(multiworld, player, 'Cave 45 Ledge', None, ['Cave 45']),
+ create_cave_region(multiworld, player, 'Cave 45', 'a cave with an item', ['Cave 45']),
+ create_lw_region(multiworld, player, 'Graveyard Ledge', None, ['Graveyard Cave']),
+ create_cave_region(multiworld, player, 'Graveyard Cave', 'a cave with an item', ['Graveyard Cave']),
+ create_cave_region(multiworld, player, 'Checkerboard Cave', 'a cave with an item', ['Checkerboard Cave']),
+ create_cave_region(multiworld, player, 'Long Fairy Cave', 'a fairy fountain'),
+ create_cave_region(multiworld, player, 'Mini Moldorm Cave', 'a bounty of five items', ['Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right',
'Mini Moldorm Cave - Far Right', 'Mini Moldorm Cave - Generous Guy']),
- create_cave_region(world, player, 'Ice Rod Cave', 'a cave with a chest', ['Ice Rod Cave']),
- create_cave_region(world, player, 'Good Bee Cave', 'a cold bee'),
- create_cave_region(world, player, '20 Rupee Cave', 'a cave with some cash'),
- create_cave_region(world, player, 'Cave Shop (Lake Hylia)', 'a common shop'),
- create_cave_region(world, player, 'Cave Shop (Dark Death Mountain)', 'a common shop'),
- create_cave_region(world, player, 'Bonk Rock Cave', 'a cave with a chest', ['Bonk Rock Cave']),
- create_cave_region(world, player, 'Library', 'the library', ['Library']),
- create_cave_region(world, player, 'Kakariko Gamble Game', 'a game of chance'),
- create_cave_region(world, player, 'Potion Shop', 'the potion shop', ['Potion Shop']),
- create_lw_region(world, player, 'Lake Hylia Island', ['Lake Hylia Island']),
- create_cave_region(world, player, 'Capacity Upgrade', 'the queen of fairies', ['Capacity Upgrade Shop']),
- create_cave_region(world, player, 'Two Brothers House', 'a connector', None, ['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)']),
- create_lw_region(world, player, 'Maze Race Ledge', ['Maze Race'], ['Two Brothers House (West)']),
- create_cave_region(world, player, '50 Rupee Cave', 'a cave with some cash'),
- create_lw_region(world, player, 'Desert Ledge', ['Desert Ledge'], ['Desert Palace Entrance (North) Rocks', 'Desert Palace Entrance (West)']),
- create_lw_region(world, player, 'Desert Ledge (Northeast)', None, ['Checkerboard Cave']),
- create_lw_region(world, player, 'Desert Palace Stairs', None, ['Desert Palace Entrance (South)']),
- create_lw_region(world, player, 'Desert Palace Lone Stairs', None, ['Desert Palace Stairs Drop', 'Desert Palace Entrance (East)']),
- create_lw_region(world, player, 'Desert Palace Entrance (North) Spot', None, ['Desert Palace Entrance (North)', 'Desert Ledge Return Rocks']),
- create_dungeon_region(world, player, 'Desert Palace Main (Outer)', 'Desert Palace', ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'],
+ create_cave_region(multiworld, player, 'Ice Rod Cave', 'a cave with a chest', ['Ice Rod Cave']),
+ create_cave_region(multiworld, player, 'Good Bee Cave', 'a cold bee'),
+ create_cave_region(multiworld, player, '20 Rupee Cave', 'a cave with some cash'),
+ create_cave_region(multiworld, player, 'Cave Shop (Lake Hylia)', 'a common shop'),
+ create_cave_region(multiworld, player, 'Cave Shop (Dark Death Mountain)', 'a common shop'),
+ create_cave_region(multiworld, player, 'Bonk Rock Cave', 'a cave with a chest', ['Bonk Rock Cave']),
+ create_cave_region(multiworld, player, 'Library', 'the library', ['Library']),
+ create_cave_region(multiworld, player, 'Kakariko Gamble Game', 'a game of chance'),
+ create_cave_region(multiworld, player, 'Potion Shop', 'the potion shop', ['Potion Shop']),
+ create_lw_region(multiworld, player, 'Lake Hylia Island', ['Lake Hylia Island']),
+ create_cave_region(multiworld, player, 'Capacity Upgrade', 'the queen of fairies', ['Capacity Upgrade Shop']),
+ create_cave_region(multiworld, player, 'Two Brothers House', 'a connector', None, ['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)']),
+ create_lw_region(multiworld, player, 'Maze Race Ledge', ['Maze Race'], ['Two Brothers House (West)']),
+ create_cave_region(multiworld, player, '50 Rupee Cave', 'a cave with some cash'),
+ create_lw_region(multiworld, player, 'Desert Ledge', ['Desert Ledge'], ['Desert Palace Entrance (North) Rocks', 'Desert Palace Entrance (West)']),
+ create_lw_region(multiworld, player, 'Desert Ledge (Northeast)', None, ['Checkerboard Cave']),
+ create_lw_region(multiworld, player, 'Desert Palace Stairs', None, ['Desert Palace Entrance (South)']),
+ create_lw_region(multiworld, player, 'Desert Palace Lone Stairs', None, ['Desert Palace Stairs Drop', 'Desert Palace Entrance (East)']),
+ create_lw_region(multiworld, player, 'Desert Palace Entrance (North) Spot', None, ['Desert Palace Entrance (North)', 'Desert Ledge Return Rocks']),
+ create_dungeon_region(multiworld, player, 'Desert Palace Main (Outer)', 'Desert Palace', ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'],
['Desert Palace Pots (Outer)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)', 'Desert Palace East Wing']),
- create_dungeon_region(world, player, 'Desert Palace Main (Inner)', 'Desert Palace', None, ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']),
- create_dungeon_region(world, player, 'Desert Palace East', 'Desert Palace', ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']),
- create_dungeon_region(world, player, 'Desert Palace North', 'Desert Palace', ['Desert Palace - Desert Tiles 1 Pot Key', 'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key',
+ create_dungeon_region(multiworld, player, 'Desert Palace Main (Inner)', 'Desert Palace', None, ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']),
+ create_dungeon_region(multiworld, player, 'Desert Palace East', 'Desert Palace', ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']),
+ create_dungeon_region(multiworld, player, 'Desert Palace North', 'Desert Palace', ['Desert Palace - Desert Tiles 1 Pot Key', 'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key',
'Desert Palace - Boss', 'Desert Palace - Prize'], ['Desert Palace Exit (North)']),
- create_dungeon_region(world, player, 'Eastern Palace', 'Eastern Palace', ['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest', 'Eastern Palace - Cannonball Chest',
+ create_dungeon_region(multiworld, player, 'Eastern Palace', 'Eastern Palace', ['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest', 'Eastern Palace - Cannonball Chest',
'Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop', 'Eastern Palace - Big Key Chest',
'Eastern Palace - Map Chest', 'Eastern Palace - Boss', 'Eastern Palace - Prize'], ['Eastern Palace Exit']),
- create_lw_region(world, player, 'Master Sword Meadow', ['Master Sword Pedestal']),
- create_cave_region(world, player, 'Lost Woods Gamble', 'a game of chance'),
- create_lw_region(world, player, 'Hyrule Castle Courtyard', None, ['Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Entrance (South)']),
- create_lw_region(world, player, 'Hyrule Castle Ledge', None, ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Agahnims Tower', 'Hyrule Castle Ledge Courtyard Drop']),
- create_dungeon_region(world, player, 'Hyrule Castle', 'Hyrule Castle', ['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', 'Hyrule Castle - Zelda\'s Chest',
+ create_lw_region(multiworld, player, 'Master Sword Meadow', ['Master Sword Pedestal']),
+ create_cave_region(multiworld, player, 'Lost Woods Gamble', 'a game of chance'),
+ create_lw_region(multiworld, player, 'Hyrule Castle Courtyard', None, ['Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Entrance (South)']),
+ create_lw_region(multiworld, player, 'Hyrule Castle Ledge', None, ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Agahnims Tower', 'Hyrule Castle Ledge Courtyard Drop']),
+ create_dungeon_region(multiworld, player, 'Hyrule Castle', 'Hyrule Castle', ['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', 'Hyrule Castle - Zelda\'s Chest',
'Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop', 'Hyrule Castle - Big Key Drop'],
['Hyrule Castle Exit (East)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (South)', 'Throne Room']),
- create_dungeon_region(world, player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks
- create_dungeon_region(world, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross', 'Sewers - Key Rat Key Drop'], ['Sewers Door']),
- create_dungeon_region(world, player, 'Sewers', 'a drop\'s exit', None, ['Sanctuary Push Door', 'Sewers Back Door', 'Sewers Secret Room']),
- create_dungeon_region(world, player, 'Sewers Secret Room', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle',
+ create_dungeon_region(multiworld, player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks
+ create_dungeon_region(multiworld, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross', 'Sewers - Key Rat Key Drop'], ['Sewers Door']),
+ create_dungeon_region(multiworld, player, 'Sewers', 'a drop\'s exit', None, ['Sanctuary Push Door', 'Sewers Back Door', 'Sewers Secret Room']),
+ create_dungeon_region(multiworld, player, 'Sewers Secret Room', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle',
'Sewers - Secret Room - Right']),
- create_dungeon_region(world, player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']),
- create_dungeon_region(world, player, 'Agahnims Tower', 'Castle Tower', ['Castle Tower - Room 03', 'Castle Tower - Dark Maze', 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'], ['Agahnim 1', 'Agahnims Tower Exit']),
- create_dungeon_region(world, player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None),
- create_cave_region(world, player, 'Old Man Cave', 'a connector', ['Old Man'], ['Old Man Cave Exit (East)', 'Old Man Cave Exit (West)']),
- create_cave_region(world, player, 'Old Man House', 'a connector', None, ['Old Man House Exit (Bottom)', 'Old Man House Front to Back']),
- create_cave_region(world, player, 'Old Man House Back', 'a connector', None, ['Old Man House Exit (Top)', 'Old Man House Back to Front']),
- create_lw_region(world, player, 'Death Mountain', None, ['Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)', 'Death Mountain Return Cave (East)', 'Spectacle Rock Cave', 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)', 'Broken Bridge (West)', 'Death Mountain Teleporter']),
- create_cave_region(world, player, 'Death Mountain Return Cave', 'a connector', None, ['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)']),
- create_lw_region(world, player, 'Death Mountain Return Ledge', None, ['Death Mountain Return Ledge Drop', 'Death Mountain Return Cave (West)']),
- create_cave_region(world, player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'], ['Spectacle Rock Cave Drop', 'Spectacle Rock Cave Exit (Top)']),
- create_cave_region(world, player, 'Spectacle Rock Cave (Bottom)', 'a connector', None, ['Spectacle Rock Cave Exit']),
- create_cave_region(world, player, 'Spectacle Rock Cave (Peak)', 'a connector', None, ['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']),
- create_lw_region(world, player, 'East Death Mountain (Bottom)', None, ['Broken Bridge (East)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'East Death Mountain Teleporter', 'Hookshot Fairy', 'Fairy Ascension Rocks', 'Spiral Cave (Bottom)']),
- create_cave_region(world, player, 'Hookshot Fairy', 'fairies deep in a cave'),
- create_cave_region(world, player, 'Paradox Cave Front', 'a connector', None, ['Paradox Cave Push Block Reverse', 'Paradox Cave Exit (Bottom)', 'Light World Death Mountain Shop']),
- create_cave_region(world, player, 'Paradox Cave Chest Area', 'a connector', ['Paradox Cave Lower - Far Left',
+ create_dungeon_region(multiworld, player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']),
+ create_dungeon_region(multiworld, player, 'Agahnims Tower', 'Castle Tower', ['Castle Tower - Room 03', 'Castle Tower - Dark Maze', 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'], ['Agahnim 1', 'Agahnims Tower Exit']),
+ create_dungeon_region(multiworld, player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None),
+ create_cave_region(multiworld, player, 'Old Man Cave', 'a connector', ['Old Man'], ['Old Man Cave Exit (East)', 'Old Man Cave Exit (West)']),
+ create_cave_region(multiworld, player, 'Old Man House', 'a connector', None, ['Old Man House Exit (Bottom)', 'Old Man House Front to Back']),
+ create_cave_region(multiworld, player, 'Old Man House Back', 'a connector', None, ['Old Man House Exit (Top)', 'Old Man House Back to Front']),
+ create_lw_region(multiworld, player, 'Death Mountain', None, ['Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)', 'Death Mountain Return Cave (East)', 'Spectacle Rock Cave', 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)', 'Broken Bridge (West)', 'Death Mountain Teleporter']),
+ create_cave_region(multiworld, player, 'Death Mountain Return Cave', 'a connector', None, ['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)']),
+ create_lw_region(multiworld, player, 'Death Mountain Return Ledge', None, ['Death Mountain Return Ledge Drop', 'Death Mountain Return Cave (West)']),
+ create_cave_region(multiworld, player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'], ['Spectacle Rock Cave Drop', 'Spectacle Rock Cave Exit (Top)']),
+ create_cave_region(multiworld, player, 'Spectacle Rock Cave (Bottom)', 'a connector', None, ['Spectacle Rock Cave Exit']),
+ create_cave_region(multiworld, player, 'Spectacle Rock Cave (Peak)', 'a connector', None, ['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']),
+ create_lw_region(multiworld, player, 'East Death Mountain (Bottom)', None, ['Broken Bridge (East)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'East Death Mountain Teleporter', 'Hookshot Fairy', 'Fairy Ascension Rocks', 'Spiral Cave (Bottom)']),
+ create_cave_region(multiworld, player, 'Hookshot Fairy', 'fairies deep in a cave'),
+ create_cave_region(multiworld, player, 'Paradox Cave Front', 'a connector', None, ['Paradox Cave Push Block Reverse', 'Paradox Cave Exit (Bottom)', 'Light World Death Mountain Shop']),
+ create_cave_region(multiworld, player, 'Paradox Cave Chest Area', 'a connector', ['Paradox Cave Lower - Far Left',
'Paradox Cave Lower - Left',
'Paradox Cave Lower - Right',
'Paradox Cave Lower - Far Right',
@@ -147,267 +147,267 @@ def create_regions(world, player):
'Paradox Cave Upper - Left',
'Paradox Cave Upper - Right'],
['Paradox Cave Push Block', 'Paradox Cave Bomb Jump']),
- create_cave_region(world, player, 'Paradox Cave', 'a connector', None,
+ create_cave_region(multiworld, player, 'Paradox Cave', 'a connector', None,
['Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Top)', 'Paradox Cave Drop']),
- create_cave_region(world, player, 'Light World Death Mountain Shop', 'a common shop'),
- create_lw_region(world, player, 'East Death Mountain (Top)', None,
+ create_cave_region(multiworld, player, 'Light World Death Mountain Shop', 'a common shop'),
+ create_lw_region(multiworld, player, 'East Death Mountain (Top)', None,
['Paradox Cave (Top)', 'Death Mountain (Top)', 'Spiral Cave Ledge Access',
'East Death Mountain Drop', 'Turtle Rock Teleporter', 'Fairy Ascension Ledge']),
- create_lw_region(world, player, 'Spiral Cave Ledge', None, ['Spiral Cave', 'Spiral Cave Ledge Drop']),
- create_cave_region(world, player, 'Spiral Cave (Top)', 'a connector', ['Spiral Cave'],
+ create_lw_region(multiworld, player, 'Spiral Cave Ledge', None, ['Spiral Cave', 'Spiral Cave Ledge Drop']),
+ create_cave_region(multiworld, player, 'Spiral Cave (Top)', 'a connector', ['Spiral Cave'],
['Spiral Cave (top to bottom)', 'Spiral Cave Exit (Top)']),
- create_cave_region(world, player, 'Spiral Cave (Bottom)', 'a connector', None, ['Spiral Cave Exit']),
- create_lw_region(world, player, 'Fairy Ascension Plateau', None,
+ create_cave_region(multiworld, player, 'Spiral Cave (Bottom)', 'a connector', None, ['Spiral Cave Exit']),
+ create_lw_region(multiworld, player, 'Fairy Ascension Plateau', None,
['Fairy Ascension Drop', 'Fairy Ascension Cave (Bottom)']),
- create_cave_region(world, player, 'Fairy Ascension Cave (Bottom)', 'a connector', None,
+ create_cave_region(multiworld, player, 'Fairy Ascension Cave (Bottom)', 'a connector', None,
['Fairy Ascension Cave Climb', 'Fairy Ascension Cave Exit (Bottom)']),
- create_cave_region(world, player, 'Fairy Ascension Cave (Drop)', 'a connector', None,
+ create_cave_region(multiworld, player, 'Fairy Ascension Cave (Drop)', 'a connector', None,
['Fairy Ascension Cave Pots']),
- create_cave_region(world, player, 'Fairy Ascension Cave (Top)', 'a connector', None,
+ create_cave_region(multiworld, player, 'Fairy Ascension Cave (Top)', 'a connector', None,
['Fairy Ascension Cave Exit (Top)', 'Fairy Ascension Cave Drop']),
- create_lw_region(world, player, 'Fairy Ascension Ledge', None,
+ create_lw_region(multiworld, player, 'Fairy Ascension Ledge', None,
['Fairy Ascension Ledge Drop', 'Fairy Ascension Cave (Top)']),
- create_lw_region(world, player, 'Death Mountain (Top)', ['Ether Tablet'],
+ create_lw_region(multiworld, player, 'Death Mountain (Top)', ['Ether Tablet'],
['East Death Mountain (Top)', 'Tower of Hera', 'Death Mountain Drop']),
- create_lw_region(world, player, 'Spectacle Rock', ['Spectacle Rock'], ['Spectacle Rock Drop']),
- create_dungeon_region(world, player, 'Tower of Hera (Bottom)', 'Tower of Hera',
+ create_lw_region(multiworld, player, 'Spectacle Rock', ['Spectacle Rock'], ['Spectacle Rock Drop']),
+ create_dungeon_region(multiworld, player, 'Tower of Hera (Bottom)', 'Tower of Hera',
['Tower of Hera - Basement Cage', 'Tower of Hera - Map Chest'],
['Tower of Hera Small Key Door', 'Tower of Hera Big Key Door', 'Tower of Hera Exit']),
- create_dungeon_region(world, player, 'Tower of Hera (Basement)', 'Tower of Hera',
+ create_dungeon_region(multiworld, player, 'Tower of Hera (Basement)', 'Tower of Hera',
['Tower of Hera - Big Key Chest']),
- create_dungeon_region(world, player, 'Tower of Hera (Top)', 'Tower of Hera',
+ create_dungeon_region(multiworld, player, 'Tower of Hera (Top)', 'Tower of Hera',
['Tower of Hera - Compass Chest', 'Tower of Hera - Big Chest', 'Tower of Hera - Boss',
'Tower of Hera - Prize']),
- create_dw_region(world, player, 'East Dark World', ['Pyramid'],
+ create_dw_region(multiworld, player, 'East Dark World', ['Pyramid'],
['Pyramid Fairy', 'South Dark World Bridge', 'Palace of Darkness',
'Dark Lake Hylia Drop (East)',
'Hyrule Castle Ledge Mirror Spot', 'Dark Lake Hylia Fairy', 'Palace of Darkness Hint',
'East Dark World Hint', 'Pyramid Hole', 'Northeast Dark World Broken Bridge Pass', ]),
- create_dw_region(world, player, 'Catfish', ['Catfish'], ['Catfish Exit Rock']),
- create_dw_region(world, player, 'Northeast Dark World', None,
+ create_dw_region(multiworld, player, 'Catfish', ['Catfish'], ['Catfish Exit Rock']),
+ create_dw_region(multiworld, player, 'Northeast Dark World', None,
['West Dark World Gap', 'Dark World Potion Shop', 'East Dark World Broken Bridge Pass',
'Catfish Entrance Rock', 'Dark Lake Hylia Teleporter']),
- create_cave_region(world, player, 'Palace of Darkness Hint', 'a storyteller'),
- create_cave_region(world, player, 'East Dark World Hint', 'a storyteller'),
- create_dw_region(world, player, 'South Dark World', ['Stumpy', 'Digging Game'],
+ create_cave_region(multiworld, player, 'Palace of Darkness Hint', 'a storyteller'),
+ create_cave_region(multiworld, player, 'East Dark World Hint', 'a storyteller'),
+ create_dw_region(multiworld, player, 'South Dark World', ['Stumpy', 'Digging Game'],
['Dark Lake Hylia Drop (South)', 'Hype Cave', 'Swamp Palace', 'Village of Outcasts Heavy Rock',
'Maze Race Mirror Spot',
'Cave 45 Mirror Spot', 'East Dark World Bridge', 'Big Bomb Shop', 'Archery Game',
'Bonk Fairy (Dark)', 'Dark Lake Hylia Shop',
'Bombos Tablet Mirror Spot']),
- create_lw_region(world, player, 'Bombos Tablet Ledge', ['Bombos Tablet']),
- create_cave_region(world, player, 'Big Bomb Shop', 'the bomb shop'),
- create_cave_region(world, player, 'Archery Game', 'a game of skill'),
- create_dw_region(world, player, 'Dark Lake Hylia', None,
+ create_lw_region(multiworld, player, 'Bombos Tablet Ledge', ['Bombos Tablet']),
+ create_cave_region(multiworld, player, 'Big Bomb Shop', 'the bomb shop'),
+ create_cave_region(multiworld, player, 'Archery Game', 'a game of skill'),
+ create_dw_region(multiworld, player, 'Dark Lake Hylia', None,
['Lake Hylia Island Mirror Spot', 'East Dark World Pier', 'Dark Lake Hylia Ledge']),
- create_dw_region(world, player, 'Dark Lake Hylia Central Island', None,
+ create_dw_region(multiworld, player, 'Dark Lake Hylia Central Island', None,
['Ice Palace', 'Lake Hylia Central Island Mirror Spot']),
- create_dw_region(world, player, 'Dark Lake Hylia Ledge', None,
+ create_dw_region(multiworld, player, 'Dark Lake Hylia Ledge', None,
['Dark Lake Hylia Ledge Drop', 'Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Ledge Hint',
'Dark Lake Hylia Ledge Spike Cave']),
- create_cave_region(world, player, 'Dark Lake Hylia Ledge Hint', 'a storyteller'),
- create_cave_region(world, player, 'Dark Lake Hylia Ledge Spike Cave', 'a spiky hint'),
- create_cave_region(world, player, 'Hype Cave', 'a bounty of five items',
+ create_cave_region(multiworld, player, 'Dark Lake Hylia Ledge Hint', 'a storyteller'),
+ create_cave_region(multiworld, player, 'Dark Lake Hylia Ledge Spike Cave', 'a spiky hint'),
+ create_cave_region(multiworld, player, 'Hype Cave', 'a bounty of five items',
['Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left',
'Hype Cave - Bottom', 'Hype Cave - Generous Guy']),
- create_dw_region(world, player, 'West Dark World', ['Frog'],
+ create_dw_region(multiworld, player, 'West Dark World', ['Frog'],
['Village of Outcasts Drop', 'East Dark World River Pier', 'Brewery', 'C-Shaped House',
'Chest Game', 'Thieves Town', 'Graveyard Ledge Mirror Spot', 'Kings Grave Mirror Spot',
'Bumper Cave Entrance Rock',
'Skull Woods Forest', 'Village of Outcasts Pegs', 'Village of Outcasts Eastern Rocks',
'Red Shield Shop', 'Dark Sanctuary Hint', 'Fortune Teller (Dark)',
'Dark World Lumberjack Shop']),
- create_dw_region(world, player, 'Dark Grassy Lawn', None, ['Grassy Lawn Pegs', 'Village of Outcasts Shop']),
- create_dw_region(world, player, 'Hammer Peg Area', ['Dark Blacksmith Ruins'],
+ create_dw_region(multiworld, player, 'Dark Grassy Lawn', None, ['Grassy Lawn Pegs', 'Village of Outcasts Shop']),
+ create_dw_region(multiworld, player, 'Hammer Peg Area', ['Dark Blacksmith Ruins'],
['Bat Cave Drop Ledge Mirror Spot', 'Dark World Hammer Peg Cave', 'Peg Area Rocks']),
- create_dw_region(world, player, 'Bumper Cave Entrance', None,
+ create_dw_region(multiworld, player, 'Bumper Cave Entrance', None,
['Bumper Cave (Bottom)', 'Bumper Cave Entrance Mirror Spot', 'Bumper Cave Entrance Drop']),
- create_cave_region(world, player, 'Fortune Teller (Dark)', 'a fortune teller'),
- create_cave_region(world, player, 'Village of Outcasts Shop', 'a common shop'),
- create_cave_region(world, player, 'Dark Lake Hylia Shop', 'a common shop'),
- create_cave_region(world, player, 'Dark World Lumberjack Shop', 'a common shop'),
- create_cave_region(world, player, 'Dark World Potion Shop', 'a common shop'),
- create_cave_region(world, player, 'Dark World Hammer Peg Cave', 'a cave with an item', ['Peg Cave']),
- create_cave_region(world, player, 'Pyramid Fairy', 'a cave with two chests',
+ create_cave_region(multiworld, player, 'Fortune Teller (Dark)', 'a fortune teller'),
+ create_cave_region(multiworld, player, 'Village of Outcasts Shop', 'a common shop'),
+ create_cave_region(multiworld, player, 'Dark Lake Hylia Shop', 'a common shop'),
+ create_cave_region(multiworld, player, 'Dark World Lumberjack Shop', 'a common shop'),
+ create_cave_region(multiworld, player, 'Dark World Potion Shop', 'a common shop'),
+ create_cave_region(multiworld, player, 'Dark World Hammer Peg Cave', 'a cave with an item', ['Peg Cave']),
+ create_cave_region(multiworld, player, 'Pyramid Fairy', 'a cave with two chests',
['Pyramid Fairy - Left', 'Pyramid Fairy - Right']),
- create_cave_region(world, player, 'Brewery', 'a house with a chest', ['Brewery']),
- create_cave_region(world, player, 'C-Shaped House', 'a house with a chest', ['C-Shaped House']),
- create_cave_region(world, player, 'Chest Game', 'a game of 16 chests', ['Chest Game']),
- create_cave_region(world, player, 'Red Shield Shop', 'the rare shop'),
- create_cave_region(world, player, 'Dark Sanctuary Hint', 'a storyteller'),
- create_cave_region(world, player, 'Bumper Cave', 'a connector', None,
+ create_cave_region(multiworld, player, 'Brewery', 'a house with a chest', ['Brewery']),
+ create_cave_region(multiworld, player, 'C-Shaped House', 'a house with a chest', ['C-Shaped House']),
+ create_cave_region(multiworld, player, 'Chest Game', 'a game of 16 chests', ['Chest Game']),
+ create_cave_region(multiworld, player, 'Red Shield Shop', 'the rare shop'),
+ create_cave_region(multiworld, player, 'Dark Sanctuary Hint', 'a storyteller'),
+ create_cave_region(multiworld, player, 'Bumper Cave', 'a connector', None,
['Bumper Cave Exit (Bottom)', 'Bumper Cave Exit (Top)']),
- create_dw_region(world, player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'],
+ create_dw_region(multiworld, player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'],
['Bumper Cave Ledge Drop', 'Bumper Cave (Top)', 'Bumper Cave Ledge Mirror Spot']),
- create_dw_region(world, player, 'Skull Woods Forest', None,
+ create_dw_region(multiworld, player, 'Skull Woods Forest', None,
['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)',
'Skull Woods First Section Hole (North)',
'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)']),
- create_dw_region(world, player, 'Skull Woods Forest (West)', None,
+ create_dw_region(multiworld, player, 'Skull Woods Forest (West)', None,
['Skull Woods Second Section Hole', 'Skull Woods Second Section Door (West)',
'Skull Woods Final Section']),
- create_dw_region(world, player, 'Dark Desert', None,
+ create_dw_region(multiworld, player, 'Dark Desert', None,
['Misery Mire', 'Mire Shed', 'Desert Ledge (Northeast) Mirror Spot',
'Desert Ledge Mirror Spot', 'Desert Palace Stairs Mirror Spot',
'Desert Palace Entrance (North) Mirror Spot', 'Dark Desert Hint', 'Dark Desert Fairy']),
- create_cave_region(world, player, 'Mire Shed', 'a cave with two chests',
+ create_cave_region(multiworld, player, 'Mire Shed', 'a cave with two chests',
['Mire Shed - Left', 'Mire Shed - Right']),
- create_cave_region(world, player, 'Dark Desert Hint', 'a storyteller'),
- create_dw_region(world, player, 'Dark Death Mountain (West Bottom)', None,
+ create_cave_region(multiworld, player, 'Dark Desert Hint', 'a storyteller'),
+ create_dw_region(multiworld, player, 'Dark Death Mountain (West Bottom)', None,
['Spike Cave', 'Spectacle Rock Mirror Spot', 'Dark Death Mountain Fairy']),
- create_dw_region(world, player, 'Dark Death Mountain (Top)', None,
+ create_dw_region(multiworld, player, 'Dark Death Mountain (Top)', None,
['Dark Death Mountain Drop (East)', 'Dark Death Mountain Drop (West)', 'Ganons Tower',
'Superbunny Cave (Top)',
'Hookshot Cave', 'East Death Mountain (Top) Mirror Spot', 'Turtle Rock']),
- create_dw_region(world, player, 'Dark Death Mountain Ledge', None,
+ create_dw_region(multiworld, player, 'Dark Death Mountain Ledge', None,
['Dark Death Mountain Ledge (East)', 'Dark Death Mountain Ledge (West)',
'Mimic Cave Mirror Spot', 'Spiral Cave Mirror Spot']),
- create_dw_region(world, player, 'Dark Death Mountain Isolated Ledge', None,
+ create_dw_region(multiworld, player, 'Dark Death Mountain Isolated Ledge', None,
['Isolated Ledge Mirror Spot', 'Turtle Rock Isolated Ledge Entrance']),
- create_dw_region(world, player, 'Dark Death Mountain (East Bottom)', None,
+ create_dw_region(multiworld, player, 'Dark Death Mountain (East Bottom)', None,
['Superbunny Cave (Bottom)', 'Cave Shop (Dark Death Mountain)',
'Fairy Ascension Mirror Spot']),
- create_cave_region(world, player, 'Superbunny Cave (Top)', 'a connector',
+ create_cave_region(multiworld, player, 'Superbunny Cave (Top)', 'a connector',
['Superbunny Cave - Top', 'Superbunny Cave - Bottom'], ['Superbunny Cave Exit (Top)']),
- create_cave_region(world, player, 'Superbunny Cave (Bottom)', 'a connector', None,
+ create_cave_region(multiworld, player, 'Superbunny Cave (Bottom)', 'a connector', None,
['Superbunny Cave Climb', 'Superbunny Cave Exit (Bottom)']),
- create_cave_region(world, player, 'Spike Cave', 'Spike Cave', ['Spike Cave']),
- create_cave_region(world, player, 'Hookshot Cave', 'a connector',
+ create_cave_region(multiworld, player, 'Spike Cave', 'Spike Cave', ['Spike Cave']),
+ create_cave_region(multiworld, player, 'Hookshot Cave', 'a connector',
['Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right',
'Hookshot Cave - Bottom Left'],
['Hookshot Cave Exit (South)', 'Hookshot Cave Bomb Wall (South)']),
- create_cave_region(world, player, 'Hookshot Cave (Upper)', 'a connector', None, ['Hookshot Cave Exit (North)',
+ create_cave_region(multiworld, player, 'Hookshot Cave (Upper)', 'a connector', None, ['Hookshot Cave Exit (North)',
'Hookshot Cave Bomb Wall (North)']),
- create_dw_region(world, player, 'Death Mountain Floating Island (Dark World)', None,
+ create_dw_region(multiworld, player, 'Death Mountain Floating Island (Dark World)', None,
['Floating Island Drop', 'Hookshot Cave Back Entrance', 'Floating Island Mirror Spot']),
- create_lw_region(world, player, 'Death Mountain Floating Island (Light World)', ['Floating Island']),
- create_dw_region(world, player, 'Turtle Rock (Top)', None, ['Turtle Rock Drop']),
- create_lw_region(world, player, 'Mimic Cave Ledge', None, ['Mimic Cave']),
- create_cave_region(world, player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']),
+ create_lw_region(multiworld, player, 'Death Mountain Floating Island (Light World)', ['Floating Island']),
+ create_dw_region(multiworld, player, 'Turtle Rock (Top)', None, ['Turtle Rock Drop']),
+ create_lw_region(multiworld, player, 'Mimic Cave Ledge', None, ['Mimic Cave']),
+ create_cave_region(multiworld, player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']),
- create_dungeon_region(world, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None, ['Swamp Palace Moat', 'Swamp Palace Exit']),
- create_dungeon_region(world, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'], ['Swamp Palace Small Key Door']),
- create_dungeon_region(world, player, 'Swamp Palace (Starting Area)', 'Swamp Palace', ['Swamp Palace - Map Chest', 'Swamp Palace - Pot Row Pot Key',
+ create_dungeon_region(multiworld, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None, ['Swamp Palace Moat', 'Swamp Palace Exit']),
+ create_dungeon_region(multiworld, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'], ['Swamp Palace Small Key Door']),
+ create_dungeon_region(multiworld, player, 'Swamp Palace (Starting Area)', 'Swamp Palace', ['Swamp Palace - Map Chest', 'Swamp Palace - Pot Row Pot Key',
'Swamp Palace - Trench 1 Pot Key'], ['Swamp Palace (Center)']),
- create_dungeon_region(world, player, 'Swamp Palace (Center)', 'Swamp Palace', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', 'Swamp Palace - Hookshot Pot Key',
+ create_dungeon_region(multiworld, player, 'Swamp Palace (Center)', 'Swamp Palace', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', 'Swamp Palace - Hookshot Pot Key',
'Swamp Palace - Trench 2 Pot Key'], ['Swamp Palace (North)', 'Swamp Palace (West)']),
- create_dungeon_region(world, player, 'Swamp Palace (West)', 'Swamp Palace', ['Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest']),
- create_dungeon_region(world, player, 'Swamp Palace (North)', 'Swamp Palace', ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right',
+ create_dungeon_region(multiworld, player, 'Swamp Palace (West)', 'Swamp Palace', ['Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest']),
+ create_dungeon_region(multiworld, player, 'Swamp Palace (North)', 'Swamp Palace', ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right',
'Swamp Palace - Waterway Pot Key', 'Swamp Palace - Waterfall Room',
'Swamp Palace - Boss', 'Swamp Palace - Prize']),
- create_dungeon_region(world, player, 'Thieves Town (Entrance)', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest',
+ create_dungeon_region(multiworld, player, 'Thieves Town (Entrance)', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest',
'Thieves\' Town - Map Chest',
'Thieves\' Town - Compass Chest',
'Thieves\' Town - Ambush Chest'], ['Thieves Town Big Key Door', 'Thieves Town Exit']),
- create_dungeon_region(world, player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic',
+ create_dungeon_region(multiworld, player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic',
'Thieves\' Town - Big Chest',
'Thieves\' Town - Hallway Pot Key',
'Thieves\' Town - Spike Switch Pot Key',
'Thieves\' Town - Blind\'s Cell'], ['Blind Fight']),
- create_dungeon_region(world, player, 'Blind Fight', 'Thieves\' Town', ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']),
- create_dungeon_region(world, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'], ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump', 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']),
- create_dungeon_region(world, player, 'Skull Woods First Section (Right)', 'Skull Woods', ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']),
- create_dungeon_region(world, player, 'Skull Woods First Section (Left)', 'Skull Woods', ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'], ['Skull Woods First Section (Left) Door to Exit', 'Skull Woods First Section (Left) Door to Right']),
- create_dungeon_region(world, player, 'Skull Woods First Section (Top)', 'Skull Woods', ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']),
- create_dungeon_region(world, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None, ['Skull Woods Second Section (Drop)']),
- create_dungeon_region(world, player, 'Skull Woods Second Section', 'Skull Woods', ['Skull Woods - Big Key Chest', 'Skull Woods - West Lobby Pot Key'], ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']),
- create_dungeon_region(world, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']),
- create_dungeon_region(world, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Spike Corner Key Drop', 'Skull Woods - Boss', 'Skull Woods - Prize']),
- create_dungeon_region(world, player, 'Ice Palace (Entrance)', 'Ice Palace', ['Ice Palace - Jelly Key Drop', 'Ice Palace - Compass Chest'], ['Ice Palace (Second Section)', 'Ice Palace Exit']),
- create_dungeon_region(world, player, 'Ice Palace (Second Section)', 'Ice Palace', ['Ice Palace - Conveyor Key Drop'], ['Ice Palace (Main)']),
- create_dungeon_region(world, player, 'Ice Palace (Main)', 'Ice Palace', ['Ice Palace - Freezor Chest',
+ create_dungeon_region(multiworld, player, 'Blind Fight', 'Thieves\' Town', ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']),
+ create_dungeon_region(multiworld, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'], ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump', 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']),
+ create_dungeon_region(multiworld, player, 'Skull Woods First Section (Right)', 'Skull Woods', ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']),
+ create_dungeon_region(multiworld, player, 'Skull Woods First Section (Left)', 'Skull Woods', ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'], ['Skull Woods First Section (Left) Door to Exit', 'Skull Woods First Section (Left) Door to Right']),
+ create_dungeon_region(multiworld, player, 'Skull Woods First Section (Top)', 'Skull Woods', ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']),
+ create_dungeon_region(multiworld, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None, ['Skull Woods Second Section (Drop)']),
+ create_dungeon_region(multiworld, player, 'Skull Woods Second Section', 'Skull Woods', ['Skull Woods - Big Key Chest', 'Skull Woods - West Lobby Pot Key'], ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']),
+ create_dungeon_region(multiworld, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']),
+ create_dungeon_region(multiworld, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Spike Corner Key Drop', 'Skull Woods - Boss', 'Skull Woods - Prize']),
+ create_dungeon_region(multiworld, player, 'Ice Palace (Entrance)', 'Ice Palace', ['Ice Palace - Jelly Key Drop', 'Ice Palace - Compass Chest'], ['Ice Palace (Second Section)', 'Ice Palace Exit']),
+ create_dungeon_region(multiworld, player, 'Ice Palace (Second Section)', 'Ice Palace', ['Ice Palace - Conveyor Key Drop'], ['Ice Palace (Main)']),
+ create_dungeon_region(multiworld, player, 'Ice Palace (Main)', 'Ice Palace', ['Ice Palace - Freezor Chest',
'Ice Palace - Many Pots Pot Key',
'Ice Palace - Big Chest', 'Ice Palace - Iced T Room'], ['Ice Palace (East)', 'Ice Palace (Kholdstare)']),
- create_dungeon_region(world, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'], ['Ice Palace (East Top)']),
- create_dungeon_region(world, player, 'Ice Palace (East Top)', 'Ice Palace', ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest', 'Ice Palace - Hammer Block Key Drop']),
- create_dungeon_region(world, player, 'Ice Palace (Kholdstare)', 'Ice Palace', ['Ice Palace - Boss', 'Ice Palace - Prize']),
- create_dungeon_region(world, player, 'Misery Mire (Entrance)', 'Misery Mire', None, ['Misery Mire Entrance Gap', 'Misery Mire Exit']),
- create_dungeon_region(world, player, 'Misery Mire (Main)', 'Misery Mire', ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby',
+ create_dungeon_region(multiworld, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'], ['Ice Palace (East Top)']),
+ create_dungeon_region(multiworld, player, 'Ice Palace (East Top)', 'Ice Palace', ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest', 'Ice Palace - Hammer Block Key Drop']),
+ create_dungeon_region(multiworld, player, 'Ice Palace (Kholdstare)', 'Ice Palace', ['Ice Palace - Boss', 'Ice Palace - Prize']),
+ create_dungeon_region(multiworld, player, 'Misery Mire (Entrance)', 'Misery Mire', None, ['Misery Mire Entrance Gap', 'Misery Mire Exit']),
+ create_dungeon_region(multiworld, player, 'Misery Mire (Main)', 'Misery Mire', ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby',
'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest',
'Misery Mire - Spikes Pot Key', 'Misery Mire - Fishbone Pot Key',
'Misery Mire - Conveyor Crystal Key Drop'], ['Misery Mire (West)', 'Misery Mire Big Key Door']),
- create_dungeon_region(world, player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']),
- create_dungeon_region(world, player, 'Misery Mire (Final Area)', 'Misery Mire', None, ['Misery Mire (Vitreous)']),
- create_dungeon_region(world, player, 'Misery Mire (Vitreous)', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize']),
- create_dungeon_region(world, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']),
- create_dungeon_region(world, player, 'Turtle Rock (First Section)', 'Turtle Rock', ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left',
+ create_dungeon_region(multiworld, player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']),
+ create_dungeon_region(multiworld, player, 'Misery Mire (Final Area)', 'Misery Mire', None, ['Misery Mire (Vitreous)']),
+ create_dungeon_region(multiworld, player, 'Misery Mire (Vitreous)', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize']),
+ create_dungeon_region(multiworld, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']),
+ create_dungeon_region(multiworld, player, 'Turtle Rock (First Section)', 'Turtle Rock', ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left',
'Turtle Rock - Roller Room - Right'],
['Turtle Rock Entrance to Pokey Room', 'Turtle Rock Entrance Gap Reverse']),
- create_dungeon_region(world, player, 'Turtle Rock (Pokey Room)', 'Turtle Rock', ['Turtle Rock - Pokey 1 Key Drop'], ['Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Pokey Room) (South)']),
- create_dungeon_region(world, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'], ['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']),
- create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock', ['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'], ['Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door', 'Turtle Rock Second Section Bomb Wall']),
- create_dungeon_region(world, player, 'Turtle Rock (Second Section Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Second Section from Bomb Wall']),
- create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
- create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
- create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
- create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Isolated Ledge Exit', 'Turtle Rock Eye Bridge from Bomb Wall']),
- create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
+ create_dungeon_region(multiworld, player, 'Turtle Rock (Pokey Room)', 'Turtle Rock', ['Turtle Rock - Pokey 1 Key Drop'], ['Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Pokey Room) (South)']),
+ create_dungeon_region(multiworld, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'], ['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']),
+ create_dungeon_region(multiworld, player, 'Turtle Rock (Second Section)', 'Turtle Rock', ['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'], ['Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door', 'Turtle Rock Second Section Bomb Wall']),
+ create_dungeon_region(multiworld, player, 'Turtle Rock (Second Section Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Second Section from Bomb Wall']),
+ create_dungeon_region(multiworld, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
+ create_dungeon_region(multiworld, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
+ create_dungeon_region(multiworld, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
+ create_dungeon_region(multiworld, player, 'Turtle Rock (Eye Bridge Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Isolated Ledge Exit', 'Turtle Rock Eye Bridge from Bomb Wall']),
+ create_dungeon_region(multiworld, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'],
['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Eye Bridge Bomb Wall']),
- create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
- create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']),
- create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
+ create_dungeon_region(multiworld, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
+ create_dungeon_region(multiworld, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']),
+ create_dungeon_region(multiworld, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
['Palace of Darkness Big Key Chest Staircase', 'Palace of Darkness (North)', 'Palace of Darkness Big Key Door']),
- create_dungeon_region(world, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness', ['Palace of Darkness - Big Key Chest']),
- create_dungeon_region(world, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'], ['Palace of Darkness Hammer Peg Drop']),
- create_dungeon_region(world, player, 'Palace of Darkness (North)', 'Palace of Darkness', ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right'],
+ create_dungeon_region(multiworld, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness', ['Palace of Darkness - Big Key Chest']),
+ create_dungeon_region(multiworld, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'], ['Palace of Darkness Hammer Peg Drop']),
+ create_dungeon_region(multiworld, player, 'Palace of Darkness (North)', 'Palace of Darkness', ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right'],
['Palace of Darkness Spike Statue Room Door', 'Palace of Darkness Maze Door']),
- create_dungeon_region(world, player, 'Palace of Darkness (Maze)', 'Palace of Darkness', ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest']),
- create_dungeon_region(world, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness', ['Palace of Darkness - Harmless Hellway']),
- create_dungeon_region(world, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']),
- create_dungeon_region(world, player, 'Ganons Tower (Entrance)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Torch', 'Ganons Tower - Hope Room - Left',
+ create_dungeon_region(multiworld, player, 'Palace of Darkness (Maze)', 'Palace of Darkness', ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest']),
+ create_dungeon_region(multiworld, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness', ['Palace of Darkness - Harmless Hellway']),
+ create_dungeon_region(multiworld, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']),
+ create_dungeon_region(multiworld, player, 'Ganons Tower (Entrance)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Torch', 'Ganons Tower - Hope Room - Left',
'Ganons Tower - Hope Room - Right', 'Ganons Tower - Conveyor Cross Pot Key'],
['Ganons Tower (Tile Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower Big Key Door', 'Ganons Tower Exit']),
- create_dungeon_region(world, player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'], ['Ganons Tower (Tile Room) Key Door']),
- create_dungeon_region(world, player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower', ['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right',
+ create_dungeon_region(multiworld, player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'], ['Ganons Tower (Tile Room) Key Door']),
+ create_dungeon_region(multiworld, player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower', ['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right',
'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Bottom Right',
'Ganons Tower - Conveyor Star Pits Pot Key'],
['Ganons Tower (Bottom) (East)']),
- create_dungeon_region(world, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower', ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right',
+ create_dungeon_region(multiworld, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower', ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right',
'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right',
'Ganons Tower - Double Switch Pot Key'],
['Ganons Tower (Map Room)', 'Ganons Tower (Double Switch Room)']),
- create_dungeon_region(world, player, 'Ganons Tower (Map Room)', 'Ganon\'s Tower', ['Ganons Tower - Map Chest']),
- create_dungeon_region(world, player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower', ['Ganons Tower - Firesnake Room'], ['Ganons Tower (Firesnake Room)']),
- create_dungeon_region(world, player, 'Ganons Tower (Teleport Room)', 'Ganon\'s Tower', ['Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right',
+ create_dungeon_region(multiworld, player, 'Ganons Tower (Map Room)', 'Ganon\'s Tower', ['Ganons Tower - Map Chest']),
+ create_dungeon_region(multiworld, player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower', ['Ganons Tower - Firesnake Room'], ['Ganons Tower (Firesnake Room)']),
+ create_dungeon_region(multiworld, player, 'Ganons Tower (Teleport Room)', 'Ganon\'s Tower', ['Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right',
'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right'],
['Ganons Tower (Bottom) (West)']),
- create_dungeon_region(world, player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest', 'Ganons Tower - Big Key Room - Left',
+ create_dungeon_region(multiworld, player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest', 'Ganons Tower - Big Key Room - Left',
'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest']),
- create_dungeon_region(world, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None, ['Ganons Tower Torch Rooms']),
- create_dungeon_region(world, player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower', ['Ganons Tower - Mini Helmasaur Room - Left', 'Ganons Tower - Mini Helmasaur Room - Right',
+ create_dungeon_region(multiworld, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None, ['Ganons Tower Torch Rooms']),
+ create_dungeon_region(multiworld, player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower', ['Ganons Tower - Mini Helmasaur Room - Left', 'Ganons Tower - Mini Helmasaur Room - Right',
'Ganons Tower - Pre-Moldorm Chest', 'Ganons Tower - Mini Helmasaur Key Drop'], ['Ganons Tower Moldorm Door']),
- create_dungeon_region(world, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None, ['Ganons Tower Moldorm Gap']),
- create_dungeon_region(world, player, 'Agahnim 2', 'Ganon\'s Tower', ['Ganons Tower - Validation Chest', 'Agahnim 2'], None),
- create_cave_region(world, player, 'Pyramid', 'a drop\'s exit', ['Ganon'], ['Ganon Drop']),
- create_cave_region(world, player, 'Bottom of Pyramid', 'a drop\'s exit', None, ['Pyramid Exit']),
- create_dw_region(world, player, 'Pyramid Ledge', None, ['Pyramid Entrance', 'Pyramid Drop']),
- create_lw_region(world, player, 'Desert Northern Cliffs'),
- create_dw_region(world, player, 'Dark Death Mountain Bunny Descent Area')
+ create_dungeon_region(multiworld, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None, ['Ganons Tower Moldorm Gap']),
+ create_dungeon_region(multiworld, player, 'Agahnim 2', 'Ganon\'s Tower', ['Ganons Tower - Validation Chest', 'Agahnim 2'], None),
+ create_cave_region(multiworld, player, 'Pyramid', 'a drop\'s exit', ['Ganon'], ['Ganon Drop']),
+ create_cave_region(multiworld, player, 'Bottom of Pyramid', 'a drop\'s exit', None, ['Pyramid Exit']),
+ create_dw_region(multiworld, player, 'Pyramid Ledge', None, ['Pyramid Entrance', 'Pyramid Drop']),
+ create_lw_region(multiworld, player, 'Desert Northern Cliffs'),
+ create_dw_region(multiworld, player, 'Dark Death Mountain Bunny Descent Area')
]
-def create_lw_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
- return _create_region(world, player, name, LTTPRegionType.LightWorld, 'Light World', locations, exits)
+def create_lw_region(multiworld: MultiWorld, player: int, name: str, locations=None, exits=None):
+ return _create_region(multiworld, player, name, LTTPRegionType.LightWorld, 'Light World', locations, exits)
-def create_dw_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
- return _create_region(world, player, name, LTTPRegionType.DarkWorld, 'Dark World', locations, exits)
+def create_dw_region(multiworld: MultiWorld, player: int, name: str, locations=None, exits=None):
+ return _create_region(multiworld, player, name, LTTPRegionType.DarkWorld, 'Dark World', locations, exits)
-def create_cave_region(world: MultiWorld, player: int, name: str, hint: str, locations=None, exits=None):
- return _create_region(world, player, name, LTTPRegionType.Cave, hint, locations, exits)
+def create_cave_region(multiworld: MultiWorld, player: int, name: str, hint: str, locations=None, exits=None):
+ return _create_region(multiworld, player, name, LTTPRegionType.Cave, hint, locations, exits)
-def create_dungeon_region(world: MultiWorld, player: int, name: str, hint: str, locations=None, exits=None):
- return _create_region(world, player, name, LTTPRegionType.Dungeon, hint, locations, exits)
+def create_dungeon_region(multiworld: MultiWorld, player: int, name: str, hint: str, locations=None, exits=None):
+ return _create_region(multiworld, player, name, LTTPRegionType.Dungeon, hint, locations, exits)
-def _create_region(world: MultiWorld, player: int, name: str, type: LTTPRegionType, hint: str, locations=None,
+def _create_region(multiworld: MultiWorld, player: int, name: str, type: LTTPRegionType, hint: str, locations=None,
exits=None):
from .SubClasses import ALttPLocation
- ret = LTTPRegion(name, type, hint, player, world)
+ ret = LTTPRegion(name, type, hint, player, multiworld)
if exits:
for exit in exits:
ret.create_exit(exit)
@@ -422,10 +422,10 @@ def _create_region(world: MultiWorld, player: int, name: str, type: LTTPRegionTy
return ret
-def mark_light_world_regions(world, player: int):
+def mark_light_world_regions(multiworld: MultiWorld, player: int):
# cross world caves may have some sections marked as both in_light_world, and in_dark_work.
# That is ok. the bunny logic will check for this case and incorporate special rules.
- queue = collections.deque(region for region in world.get_regions(player) if region.type == LTTPRegionType.LightWorld)
+ queue = collections.deque(region for region in multiworld.get_regions(player) if region.type == LTTPRegionType.LightWorld)
seen = set(queue)
while queue:
current = queue.popleft()
@@ -438,7 +438,7 @@ def mark_light_world_regions(world, player: int):
seen.add(exit.connected_region)
queue.append(exit.connected_region)
- queue = collections.deque(region for region in world.get_regions(player) if region.type == LTTPRegionType.DarkWorld)
+ queue = collections.deque(region for region in multiworld.get_regions(player) if region.type == LTTPRegionType.DarkWorld)
seen = set(queue)
while queue:
current = queue.popleft()
diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py
index 2f62b3768238..45b3ad39d9a6 100644
--- a/worlds/alttp/Rom.py
+++ b/worlds/alttp/Rom.py
@@ -19,7 +19,7 @@
import threading
import concurrent.futures
import bsdiff4
-from typing import Collection, Optional, List, SupportsIndex
+from typing import Collection, Optional, List, SupportsIndex, TYPE_CHECKING
from BaseClasses import CollectionState, Region, Location, MultiWorld
from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml, read_snes_rom
@@ -39,6 +39,9 @@
from .EntranceShuffle import door_addresses
from .Options import small_key_shuffle
+if TYPE_CHECKING:
+ from . import ALTTPWorld
+
try:
from maseya import z3pr
from maseya.z3pr.palette_randomizer import build_offset_collections
@@ -792,13 +795,13 @@ def get_nonnative_item_sprite(code: int) -> int:
# https://discord.com/channels/731205301247803413/827141303330406408/852102450822905886
-def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
- local_random = world.worlds[player].random
- local_world = world.worlds[player]
+def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool):
+ local_random = multiworld.worlds[player].random
+ local_world = multiworld.worlds[player]
# patch items
- for location in world.get_locations(player):
+ for location in multiworld.get_locations(player):
if location.address is None or location.shop_slot is not None:
continue
@@ -852,7 +855,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
rom.write_byte(0x155C9, local_random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle
# patch entrance/exits/holes
- for region in world.get_regions(player):
+ for region in multiworld.get_regions(player):
for exit in region.exits:
if exit.target is not None:
if isinstance(exit.addresses, tuple):
@@ -885,7 +888,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
rom.write_int16(0x15DB5 + 2 * offset, 0x0640)
elif room_id == 0x00d6 and local_world.fix_trock_exit:
rom.write_int16(0x15DB5 + 2 * offset, 0x0134)
- elif room_id == 0x000c and world.shuffle_ganon: # fix ganons tower exit point
+ elif room_id == 0x000c and multiworld.shuffle_ganon: # fix ganons tower exit point
rom.write_int16(0x15DB5 + 2 * offset, 0x00A4)
else:
rom.write_int16(0x15DB5 + 2 * offset, link_y)
@@ -905,9 +908,9 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
# patch door table
rom.write_byte(0xDBB73 + exit.addresses, exit.target)
if local_world.options.mode == 'inverted':
- patch_shuffled_dark_sanc(world, rom, player)
+ patch_shuffled_dark_sanc(multiworld, rom, player)
- write_custom_shops(rom, world, player)
+ write_custom_shops(rom, multiworld, player)
def credits_digit(num):
# top: $54 is 1, 55 2, etc , so 57=4, 5C=9
@@ -981,11 +984,11 @@ def credits_digit(num):
if local_world.options.mode in ['open', 'inverted']:
rom.write_byte(0x180032, 0x01) # open mode
if local_world.options.mode == 'inverted':
- set_inverted_mode(world, player, rom)
+ set_inverted_mode(multiworld, player, rom)
elif local_world.options.mode == 'standard':
rom.write_byte(0x180032, 0x00) # standard mode
- uncle_location = world.get_location('Link\'s Uncle', player)
+ uncle_location = multiworld.get_location('Link\'s Uncle', player)
if uncle_location.item is None or uncle_location.item.name not in ['Master Sword', 'Tempered Sword',
'Fighter Sword', 'Golden Sword',
'Progressive Sword']:
@@ -1280,7 +1283,7 @@ def chunk(l, n):
# set up goals for treasure hunt
rom.write_int16(0x180163, max(0, local_world.treasure_hunt_required -
- sum(1 for item in world.precollected_items[player] if item.name == "Triforce Piece")))
+ sum(1 for item in multiworld.precollected_items[player] if item.name == "Triforce Piece")))
rom.write_bytes(0x180165, [0x0E, 0x28]) # Triforce Piece Sprite
rom.write_byte(0x180194, 1) # Must turn in triforced pieces (instant win not enabled)
@@ -1309,7 +1312,7 @@ def chunk(l, n):
rom.write_bytes(0x50563, [0x3F, 0x14]) # disable below ganon chest
rom.write_byte(0x50599, 0x00) # disable below ganon chest
rom.write_bytes(0xE9A5, [0x7E, 0x00, 0x24]) # disable below ganon chest
- rom.write_byte(0x18008B, 0x01 if local_world.options.open_pyramid.to_bool(world, player) else 0x00) # pre-open Pyramid Hole
+ rom.write_byte(0x18008B, 0x01 if local_world.options.open_pyramid.to_bool(multiworld, player) else 0x00) # pre-open Pyramid Hole
rom.write_byte(0x18008C, 0x01 if local_world.options.crystals_needed_for_gt == 0 else 0x00) # GT pre-opened if crystal requirement is 0
rom.write_byte(0xF5D73, 0xF0) # bees are catchable
rom.write_byte(0xF5F10, 0xF0) # bees are catchable
@@ -1327,7 +1330,7 @@ def chunk(l, n):
starting_max_bombs = 0 if local_world.options.bombless_start else 10
starting_max_arrows = 30
- startingstate = CollectionState(world)
+ startingstate = CollectionState(multiworld)
if startingstate.has('Silver Bow', player):
equip[0x340] = 1
@@ -1375,7 +1378,7 @@ def chunk(l, n):
equip[0x37B] = 1
equip[0x36E] = 0x80
- for item in world.precollected_items[player]:
+ for item in multiworld.precollected_items[player]:
if item.name in {'Bow', 'Silver Bow', 'Silver Arrows', 'Progressive Bow', 'Progressive Bow (Alt)',
'Titans Mitts', 'Power Glove', 'Progressive Glove',
@@ -1590,7 +1593,7 @@ def chunk(l, n):
}
def get_reveal_bytes(itemName):
- locations = world.find_item_locations(itemName, player)
+ locations = multiworld.find_item_locations(itemName, player)
if len(locations) < 1:
return 0x0000
location = locations[0]
@@ -1667,7 +1670,7 @@ def get_reveal_bytes(itemName):
rom.write_byte(0x18004C, 0x01)
# set correct flag for hera basement item
- hera_basement = world.get_location('Tower of Hera - Basement Cage', player)
+ hera_basement = multiworld.get_location('Tower of Hera - Basement Cage', player)
if hera_basement.item is not None and hera_basement.item.name == 'Small Key (Tower of Hera)' and hera_basement.item.player == player:
rom.write_byte(0x4E3BB, 0xE4)
else:
@@ -1684,12 +1687,12 @@ def get_reveal_bytes(itemName):
rom.write_byte(0xFEE41, 0x2A) # bombable exit
if local_world.options.tile_shuffle:
- tile_set = TileSet.get_random_tile_set(world.worlds[player].random)
+ tile_set = TileSet.get_random_tile_set(multiworld.worlds[player].random)
rom.write_byte(0x4BA21, tile_set.get_speed())
rom.write_byte(0x4BA1D, tile_set.get_len())
rom.write_bytes(0x4BA2A, tile_set.get_bytes())
- write_strings(rom, world, player)
+ write_strings(rom, multiworld, player)
# remote items flag, does not currently work
rom.write_byte(0x18637C, 0)
@@ -1697,14 +1700,14 @@ def get_reveal_bytes(itemName):
# set rom name
# 21 bytes
from Utils import __version__
- rom.name = bytearray(f'AP{__version__.replace(".", "")[0:3]}_{player}_{world.seed:11}\0', 'utf8')[:21]
+ rom.name = bytearray(f'AP{__version__.replace(".", "")[0:3]}_{player}_{multiworld.seed:11}\0', 'utf8')[:21]
rom.name.extend([0] * (21 - len(rom.name)))
rom.write_bytes(0x7FC0, rom.name)
# set player names
- encoded_players = world.players + len(world.groups)
+ encoded_players = multiworld.players + len(multiworld.groups)
for p in range(1, min(encoded_players, ROM_PLAYER_LIMIT) + 1):
- rom.write_bytes(0x195FFC + ((p - 1) * 32), hud_format_text(world.player_name[p]))
+ rom.write_bytes(0x195FFC + ((p - 1) * 32), hud_format_text(multiworld.player_name[p]))
if encoded_players > ROM_PLAYER_LIMIT:
rom.write_bytes(0x195FFC + ((ROM_PLAYER_LIMIT - 1) * 32), hud_format_text("Archipelago"))
@@ -1723,9 +1726,9 @@ def get_reveal_bytes(itemName):
return rom
-def patch_race_rom(rom, world, player):
+def patch_race_rom(rom: LocalRom, multiworld: MultiWorld, player: int):
rom.write_bytes(0x180213, [0x01, 0x00]) # Tournament Seed
- rom.encrypt(world, player)
+ rom.encrypt(multiworld, player)
def get_price_data(price: int, price_type: int) -> List[int]:
@@ -1738,8 +1741,8 @@ def get_price_data(price: int, price_type: int) -> List[int]:
return int16_as_bytes(price)
-def write_custom_shops(rom, world, player):
- shops = sorted([shop for shop in world.worlds[player].shops if shop.custom], key=lambda shop: shop.sram_offset)
+def write_custom_shops(rom: LocalRom, multiworld: MultiWorld, player: int):
+ shops = sorted([shop for shop in multiworld.worlds[player].shops if shop.custom], key=lambda shop: shop.sram_offset)
shop_data = bytearray()
items_data = bytearray()
@@ -1758,9 +1761,9 @@ def write_custom_shops(rom, world, player):
slot = 0 if shop.type == ShopType.TakeAny else index
if item is None:
break
- if world.worlds[player].options.shop_item_slots or shop.type == ShopType.TakeAny:
- count_shop = (shop.region.name != 'Potion Shop' or world.worlds[player].options.include_witch_hut) and \
- (shop.region.name != 'Capacity Upgrade' or world.worlds[player].options.shuffle_capacity_upgrades)
+ if multiworld.worlds[player].options.shop_item_slots or shop.type == ShopType.TakeAny:
+ count_shop = (shop.region.name != 'Potion Shop' or multiworld.worlds[player].options.include_witch_hut) and \
+ (shop.region.name != 'Capacity Upgrade' or multiworld.worlds[player].options.shuffle_capacity_upgrades)
rom.write_byte(0x186560 + shop.sram_offset + slot, 1 if count_shop else 0)
if item['item'] == 'Single Arrow' and item['player'] == 0:
arrow_mask |= 1 << index
@@ -1773,11 +1776,11 @@ def write_custom_shops(rom, world, player):
price_data = get_price_data(item['price'], item["price_type"])
replacement_price_data = get_price_data(item['replacement_price'], item['replacement_price_type'])
slot = 0 if shop.type == ShopType.TakeAny else index
- if item['player'] and world.game[item['player']] != "A Link to the Past": # item not native to ALTTP
- item_code = get_nonnative_item_sprite(world.worlds[item['player']].item_name_to_id[item['item']])
+ if item['player'] and multiworld.game[item['player']] != "A Link to the Past": # item not native to ALTTP
+ item_code = get_nonnative_item_sprite(multiworld.worlds[item['player']].item_name_to_id[item['item']])
else:
item_code = item_table[item["item"]].item_code
- if item['item'] == 'Single Arrow' and item['player'] == 0 and world.worlds[player].options.retro_bow:
+ if item['item'] == 'Single Arrow' and item['player'] == 0 and multiworld.worlds[player].options.retro_bow:
rom.write_byte(0x186500 + shop.sram_offset + slot, arrow_mask)
item_data = [shop_id, item_code] + price_data + \
@@ -1790,12 +1793,12 @@ def write_custom_shops(rom, world, player):
items_data.extend([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
rom.write_bytes(0x184900, items_data)
- if world.worlds[player].options.retro_bow:
+ if multiworld.worlds[player].options.retro_bow:
retro_shop_slots.append(0xFF)
rom.write_bytes(0x186540, retro_shop_slots)
-def hud_format_text(text):
+def hud_format_text(text: str):
output = bytes()
for char in text.lower():
if 'a' <= char <= 'z':
@@ -1812,7 +1815,7 @@ def hud_format_text(text):
output += b'\x7f\x00'
return output[:32]
-def apply_oof_sfx(rom, oof: str):
+def apply_oof_sfx(rom: LocalRom, oof: str):
with open(oof, 'rb') as stream:
oof_bytes = bytearray(stream.read())
@@ -1862,9 +1865,10 @@ def apply_oof_sfx(rom, oof: str):
rom.write_bytes(0x13000D, [0x00, 0x00, 0x00, 0x08])
-def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, sprite: str, oof: str, palettes_options,
- world=None, player=1, allow_random_on_event=False, reduceflashing=False,
- triforcehud: str = None, deathlink: bool = False, allowcollect: bool = False):
+def apply_rom_settings(rom: LocalRom, beep: str, color: str, quickswap: bool, menuspeed: str, music: bool, sprite: str,
+ oof: str, palettes_options: dict[str, str], world: "ALTTPWorld | None" = None, player: int = 1,
+ allow_random_on_event: bool = False, reduceflashing: bool = False, triforcehud: str = None,
+ deathlink: bool = False, allowcollect: bool = False):
local_random = random if not world else world.worlds[player].random
disable_music: bool = not music
# enable instant item menu
@@ -1948,7 +1952,7 @@ def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, spri
rom.write_byte(0x180167, triforce_flag)
if z3pr:
- def buildAndRandomize(option_name, mode):
+ def buildAndRandomize(option_name: str, mode: str):
options = {
option_name: True
}
@@ -2012,7 +2016,7 @@ def next_color_generator():
rom.write_crc()
-def restore_maseya_colors(rom, offsets_array):
+def restore_maseya_colors(rom: LocalRom, offsets_array: list[list[int]]):
if not rom.orig_buffer:
return
for offsetC in offsets_array:
@@ -2020,7 +2024,7 @@ def restore_maseya_colors(rom, offsets_array):
rom.write_bytes(address, rom.orig_buffer[address:address + 2])
-def set_color(rom, address, color, shade):
+def set_color(rom: LocalRom, address: int, color: tuple[int, int, int], shade: int):
r = round(min(color[0], 0xFF) * pow(0.8, shade) * 0x1F / 0xFF)
g = round(min(color[1], 0xFF) * pow(0.8, shade) * 0x1F / 0xFF)
b = round(min(color[2], 0xFF) * pow(0.8, shade) * 0x1F / 0xFF)
@@ -2028,7 +2032,7 @@ def set_color(rom, address, color, shade):
rom.write_bytes(address, ((b << 10) | (g << 5) | (r << 0)).to_bytes(2, byteorder='little', signed=False))
-def default_ow_palettes(rom):
+def default_ow_palettes(rom: LocalRom):
if not rom.orig_buffer:
return
rom.write_bytes(0xDE604, rom.orig_buffer[0xDE604:0xDEBB4])
@@ -2037,7 +2041,7 @@ def default_ow_palettes(rom):
rom.write_bytes(address, rom.orig_buffer[address:address + 2])
-def randomize_ow_palettes(rom, local_random):
+def randomize_ow_palettes(rom: LocalRom, local_random: random.Random):
grass, grass2, grass3, dirt, dirt2, water, clouds, dwdirt, \
dwgrass, dwwater, dwdmdirt, dwdmgrass, dwdmclouds1, dwdmclouds2 = [[local_random.randint(60, 215) for _ in range(3)]
for _ in range(14)]
@@ -2113,7 +2117,7 @@ def randomize_ow_palettes(rom, local_random):
set_color(rom, address, color, shade)
-def blackout_ow_palettes(rom):
+def blackout_ow_palettes(rom: LocalRom):
rom.write_bytes(0xDE604, [0] * 0xC4)
for i in range(0xDE6C8, 0xDE86C, 70):
rom.write_bytes(i, [0] * 64)
@@ -2124,13 +2128,13 @@ def blackout_ow_palettes(rom):
rom.write_bytes(address, [0, 0])
-def default_uw_palettes(rom):
+def default_uw_palettes(rom: LocalRom):
if not rom.orig_buffer:
return
rom.write_bytes(0xDD734, rom.orig_buffer[0xDD734:0xDE544])
-def randomize_uw_palettes(rom, local_random):
+def randomize_uw_palettes(rom: LocalRom, local_random: random.Random):
for dungeon in range(20):
wall, pot, chest, floor1, floor2, floor3 = [[local_random.randint(60, 240) for _ in range(3)] for _ in range(6)]
@@ -2177,7 +2181,7 @@ def randomize_uw_palettes(rom, local_random):
set_color(rom, 0x0DD796 + (0xB4 * dungeon), floor3, 4)
-def blackout_uw_palettes(rom):
+def blackout_uw_palettes(rom: LocalRom):
for i in range(0xDD734, 0xDE544, 180):
rom.write_bytes(i, [0] * 38)
rom.write_bytes(i + 44, [0] * 76)
@@ -2188,25 +2192,25 @@ def get_hash_string(hash):
return ", ".join([hash_alphabet[code & 0x1F] for code in hash])
-def write_string_to_rom(rom, target, string):
+def write_string_to_rom(rom: LocalRom, target: str, string: str):
address, maxbytes = text_addresses[target]
rom.write_bytes(address, MultiByteTextMapper.convert(string, maxbytes))
-def write_strings(rom, world, player):
+def write_strings(rom: LocalRom, multiworld: MultiWorld, player: int):
from . import ALTTPWorld
- local_random = world.worlds[player].random
- w: ALTTPWorld = world.worlds[player]
+ local_random = multiworld.worlds[player].random
+ w: ALTTPWorld = multiworld.worlds[player]
tt = TextTable()
tt.removeUnwantedText()
# Let's keep this guy's text accurate to the shuffle setting.
- if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']:
+ if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']:
tt['kakariko_flophouse_man_no_flippers'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.'
tt['kakariko_flophouse_man'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.'
- if world.worlds[player].options.mode == 'inverted':
+ if multiworld.worlds[player].options.mode == 'inverted':
tt['sign_village_of_outcasts'] = 'attention\nferal ducks sighted\nhiding in statues\n\nflute players beware\n'
def hint_text(dest, ped_hint=False):
@@ -2218,45 +2222,45 @@ def hint_text(dest, ped_hint=False):
hint = dest.hint_text
if dest.player != player:
if ped_hint:
- hint += f" for {world.player_name[dest.player]}!"
+ hint += f" for {multiworld.player_name[dest.player]}!"
elif isinstance(dest, (Region, Location)):
- hint += f" in {world.player_name[dest.player]}'s world"
+ hint += f" in {multiworld.player_name[dest.player]}'s world"
else:
- hint += f" for {world.player_name[dest.player]}"
+ hint += f" for {multiworld.player_name[dest.player]}"
return hint
- if world.worlds[player].options.scams.gives_king_zora_hint:
+ if multiworld.worlds[player].options.scams.gives_king_zora_hint:
# Zora hint
- zora_location = world.get_location("King Zora", player)
+ zora_location = multiworld.get_location("King Zora", player)
tt['zora_tells_cost'] = f"You got 500 rupees to buy {hint_text(zora_location.item)}" \
f"\n âĨ Duh\n Oh carp\n{{CHOICE}}"
- if world.worlds[player].options.scams.gives_bottle_merchant_hint:
+ if multiworld.worlds[player].options.scams.gives_bottle_merchant_hint:
# Bottle Vendor hint
- vendor_location = world.get_location("Bottle Merchant", player)
+ vendor_location = multiworld.get_location("Bottle Merchant", player)
tt['bottle_vendor_choice'] = f"I gots {hint_text(vendor_location.item)}\nYous gots 100 rupees?" \
f"\n âĨ I want\n no way!\n{{CHOICE}}"
# First we write hints about entrances, some from the inconvenient list others from all reasonable entrances.
- if world.worlds[player].options.hints:
- if world.worlds[player].options.hints.value >= 2:
- if world.worlds[player].options.hints == "full":
+ if multiworld.worlds[player].options.hints:
+ if multiworld.worlds[player].options.hints.value >= 2:
+ if multiworld.worlds[player].options.hints == "full":
tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles have hints!'
else:
tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles can have hints!'
hint_locations = HintLocations.copy()
local_random.shuffle(hint_locations)
- all_entrances = list(world.get_entrances(player))
+ all_entrances = list(multiworld.get_entrances(player))
local_random.shuffle(all_entrances)
# First we take care of the one inconvenient dungeon in the appropriately simple shuffles.
entrances_to_hint = {}
entrances_to_hint.update(InconvenientDungeonEntrances)
- if world.shuffle_ganon:
- if world.worlds[player].options.mode == 'inverted':
+ if multiworld.shuffle_ganon:
+ if multiworld.worlds[player].options.mode == 'inverted':
entrances_to_hint.update({'Inverted Ganons Tower': 'The sealed castle door'})
else:
entrances_to_hint.update({'Ganons Tower': 'Ganon\'s Tower'})
- if world.worlds[player].options.entrance_shuffle in ['simple', 'restricted']:
+ if multiworld.worlds[player].options.entrance_shuffle in ['simple', 'restricted']:
for entrance in all_entrances:
if entrance.name in entrances_to_hint:
this_hint = entrances_to_hint[entrance.name] + ' leads to ' + hint_text(
@@ -2266,9 +2270,9 @@ def hint_text(dest, ped_hint=False):
break
# Now we write inconvenient locations for most shuffles and finish taking care of the less chaotic ones.
entrances_to_hint.update(InconvenientOtherEntrances)
- if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
+ if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
hint_count = 0
- elif world.worlds[player].options.entrance_shuffle in ['simple', 'restricted']:
+ elif multiworld.worlds[player].options.entrance_shuffle in ['simple', 'restricted']:
hint_count = 2
else:
hint_count = 4
@@ -2285,31 +2289,31 @@ def hint_text(dest, ped_hint=False):
# Next we handle hints for randomly selected other entrances,
# curating the selection intelligently based on shuffle.
- if world.worlds[player].options.entrance_shuffle not in ['simple', 'restricted']:
+ if multiworld.worlds[player].options.entrance_shuffle not in ['simple', 'restricted']:
entrances_to_hint.update(ConnectorEntrances)
entrances_to_hint.update(DungeonEntrances)
- if world.worlds[player].options.mode == 'inverted':
+ if multiworld.worlds[player].options.mode == 'inverted':
entrances_to_hint.update({'Inverted Agahnims Tower': 'The dark mountain tower'})
else:
entrances_to_hint.update({'Agahnims Tower': 'The sealed castle door'})
- elif world.worlds[player].options.entrance_shuffle == 'restricted':
+ elif multiworld.worlds[player].options.entrance_shuffle == 'restricted':
entrances_to_hint.update(ConnectorEntrances)
entrances_to_hint.update(OtherEntrances)
- if world.worlds[player].options.mode == 'inverted':
+ if multiworld.worlds[player].options.mode == 'inverted':
entrances_to_hint.update({'Inverted Dark Sanctuary': 'The dark sanctuary cave'})
entrances_to_hint.update({'Inverted Big Bomb Shop': 'The old hero\'s dark home'})
entrances_to_hint.update({'Inverted Links House': 'The old hero\'s light home'})
else:
entrances_to_hint.update({'Dark Sanctuary Hint': 'The dark sanctuary cave'})
entrances_to_hint.update({'Big Bomb Shop': 'The old bomb shop'})
- if world.worlds[player].options.entrance_shuffle != 'insanity':
+ if multiworld.worlds[player].options.entrance_shuffle != 'insanity':
entrances_to_hint.update(InsanityEntrances)
- if world.shuffle_ganon:
- if world.worlds[player].options.mode == 'inverted':
+ if multiworld.shuffle_ganon:
+ if multiworld.worlds[player].options.mode == 'inverted':
entrances_to_hint.update({'Inverted Pyramid Entrance': 'The extra castle passage'})
else:
entrances_to_hint.update({'Pyramid Ledge': 'The pyramid ledge'})
- hint_count = 4 if world.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
+ hint_count = 4 if multiworld.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
'dungeons_crossed'] else 0
for entrance in all_entrances:
if entrance.name in entrances_to_hint:
@@ -2324,77 +2328,77 @@ def hint_text(dest, ped_hint=False):
# Next we write a few hints for specific inconvenient locations. We don't make many because in entrance this is highly unpredictable.
locations_to_hint = InconvenientLocations.copy()
- if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
+ if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
locations_to_hint.extend(InconvenientVanillaLocations)
local_random.shuffle(locations_to_hint)
- hint_count = 3 if world.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
+ hint_count = 3 if multiworld.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
'dungeons_crossed'] else 5
for location in locations_to_hint[:hint_count]:
if location == 'Swamp Left':
if local_random.randint(0, 1):
- first_item = hint_text(world.get_location('Swamp Palace - West Chest', player).item)
- second_item = hint_text(world.get_location('Swamp Palace - Big Key Chest', player).item)
+ first_item = hint_text(multiworld.get_location('Swamp Palace - West Chest', player).item)
+ second_item = hint_text(multiworld.get_location('Swamp Palace - Big Key Chest', player).item)
else:
- second_item = hint_text(world.get_location('Swamp Palace - West Chest', player).item)
- first_item = hint_text(world.get_location('Swamp Palace - Big Key Chest', player).item)
+ second_item = hint_text(multiworld.get_location('Swamp Palace - West Chest', player).item)
+ first_item = hint_text(multiworld.get_location('Swamp Palace - Big Key Chest', player).item)
this_hint = ('The westmost chests in Swamp Palace contain ' + first_item + ' and ' + second_item + '.')
tt[hint_locations.pop(0)] = this_hint
elif location == 'Mire Left':
if local_random.randint(0, 1):
- first_item = hint_text(world.get_location('Misery Mire - Compass Chest', player).item)
- second_item = hint_text(world.get_location('Misery Mire - Big Key Chest', player).item)
+ first_item = hint_text(multiworld.get_location('Misery Mire - Compass Chest', player).item)
+ second_item = hint_text(multiworld.get_location('Misery Mire - Big Key Chest', player).item)
else:
- second_item = hint_text(world.get_location('Misery Mire - Compass Chest', player).item)
- first_item = hint_text(world.get_location('Misery Mire - Big Key Chest', player).item)
+ second_item = hint_text(multiworld.get_location('Misery Mire - Compass Chest', player).item)
+ first_item = hint_text(multiworld.get_location('Misery Mire - Big Key Chest', player).item)
this_hint = ('The westmost chests in Misery Mire contain ' + first_item + ' and ' + second_item + '.')
tt[hint_locations.pop(0)] = this_hint
elif location == 'Tower of Hera - Big Key Chest':
this_hint = 'Waiting in the Tower of Hera basement leads to ' + hint_text(
- world.get_location(location, player).item) + '.'
+ multiworld.get_location(location, player).item) + '.'
tt[hint_locations.pop(0)] = this_hint
elif location == 'Ganons Tower - Big Chest':
this_hint = 'The big chest in Ganon\'s Tower contains ' + hint_text(
- world.get_location(location, player).item) + '.'
+ multiworld.get_location(location, player).item) + '.'
tt[hint_locations.pop(0)] = this_hint
elif location == 'Thieves\' Town - Big Chest':
this_hint = 'The big chest in Thieves\' Town contains ' + hint_text(
- world.get_location(location, player).item) + '.'
+ multiworld.get_location(location, player).item) + '.'
tt[hint_locations.pop(0)] = this_hint
elif location == 'Ice Palace - Big Chest':
this_hint = 'The big chest in Ice Palace contains ' + hint_text(
- world.get_location(location, player).item) + '.'
+ multiworld.get_location(location, player).item) + '.'
tt[hint_locations.pop(0)] = this_hint
elif location == 'Eastern Palace - Big Key Chest':
this_hint = 'The antifairy guarded chest in Eastern Palace contains ' + hint_text(
- world.get_location(location, player).item) + '.'
+ multiworld.get_location(location, player).item) + '.'
tt[hint_locations.pop(0)] = this_hint
elif location == 'Sahasrahla':
this_hint = 'Sahasrahla seeks a green pendant for ' + hint_text(
- world.get_location(location, player).item) + '.'
+ multiworld.get_location(location, player).item) + '.'
tt[hint_locations.pop(0)] = this_hint
elif location == 'Graveyard Cave':
this_hint = 'The cave north of the graveyard contains ' + hint_text(
- world.get_location(location, player).item) + '.'
+ multiworld.get_location(location, player).item) + '.'
tt[hint_locations.pop(0)] = this_hint
else:
- this_hint = location + ' contains ' + hint_text(world.get_location(location, player).item) + '.'
+ this_hint = location + ' contains ' + hint_text(multiworld.get_location(location, player).item) + '.'
tt[hint_locations.pop(0)] = this_hint
# Lastly we write hints to show where certain interesting items are.
items_to_hint = RelevantItems.copy()
- if world.worlds[player].options.small_key_shuffle.hints_useful:
+ if multiworld.worlds[player].options.small_key_shuffle.hints_useful:
items_to_hint |= item_name_groups["Small Keys"]
- if world.worlds[player].options.big_key_shuffle.hints_useful:
+ if multiworld.worlds[player].options.big_key_shuffle.hints_useful:
items_to_hint |= item_name_groups["Big Keys"]
- if world.worlds[player].options.hints == "full":
+ if multiworld.worlds[player].options.hints == "full":
hint_count = len(hint_locations) # fill all remaining hint locations with Item hints.
else:
- hint_count = 5 if world.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
+ hint_count = 5 if multiworld.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
'dungeons_crossed'] else 8
hint_count = min(hint_count, len(items_to_hint), len(hint_locations))
if hint_count:
- locations = world.find_items_in_locations(items_to_hint, player, True)
+ locations = multiworld.find_items_in_locations(items_to_hint, player, True)
local_random.shuffle(locations)
# make locked locations less likely to appear as hint,
# chances are the lock means the player already knows.
@@ -2414,15 +2418,15 @@ def hint_text(dest, ped_hint=False):
# We still need the older hints of course. Those are done here.
- silverarrows = world.find_item_locations('Silver Bow', player, True)
+ silverarrows = multiworld.find_item_locations('Silver Bow', player, True)
local_random.shuffle(silverarrows)
silverarrow_hint = (
' %s?' % hint_text(silverarrows[0]).replace('Ganon\'s', 'my')) if silverarrows else '?\nI think not!'
tt['ganon_phase_3_no_silvers'] = 'Did you find the silver arrows%s' % silverarrow_hint
tt['ganon_phase_3_no_silvers_alt'] = 'Did you find the silver arrows%s' % silverarrow_hint
- if world.worlds[player].has_progressive_bows and (w.difficulty_requirements.progressive_bow_limit >= 2 or (
- world.worlds[player].options.swordless or world.worlds[player].options.glitches_required == 'no_glitches')):
- prog_bow_locs = world.find_item_locations('Progressive Bow', player, True)
+ if multiworld.worlds[player].has_progressive_bows and (w.difficulty_requirements.progressive_bow_limit >= 2 or (
+ multiworld.worlds[player].options.swordless or multiworld.worlds[player].options.glitches_required == 'no_glitches')):
+ prog_bow_locs = multiworld.find_item_locations('Progressive Bow', player, True)
local_random.shuffle(prog_bow_locs)
found_bow = False
found_bow_alt = False
@@ -2437,34 +2441,34 @@ def hint_text(dest, ped_hint=False):
silverarrow_hint = (' %s?' % hint_text(bow_loc).replace('Ganon\'s', 'my'))
tt[target] = 'Did you find the silver arrows%s' % silverarrow_hint
- crystal5 = world.find_item('Crystal 5', player)
- crystal6 = world.find_item('Crystal 6', player)
+ crystal5 = multiworld.find_item('Crystal 5', player)
+ crystal6 = multiworld.find_item('Crystal 6', player)
tt['bomb_shop'] = 'Big Bomb?\nMy supply is blocked until you clear %s and %s.' % (
crystal5.hint_text, crystal6.hint_text)
- greenpendant = world.find_item('Green Pendant', player)
+ greenpendant = multiworld.find_item('Green Pendant', player)
tt['sahasrahla_bring_courage'] = 'I lost my family heirloom in %s' % greenpendant.hint_text
- if world.worlds[player].options.crystals_needed_for_gt == 1:
+ if multiworld.worlds[player].options.crystals_needed_for_gt == 1:
tt['sign_ganons_tower'] = 'You need a crystal to enter.'
else:
- tt['sign_ganons_tower'] = f'You need {world.worlds[player].options.crystals_needed_for_gt} crystals to enter.'
+ tt['sign_ganons_tower'] = f'You need {multiworld.worlds[player].options.crystals_needed_for_gt} crystals to enter.'
- if world.worlds[player].options.goal == 'bosses':
+ if multiworld.worlds[player].options.goal == 'bosses':
tt['sign_ganon'] = 'You need to kill all bosses, Ganon last.'
- elif world.worlds[player].options.goal == 'ganon_pedestal':
+ elif multiworld.worlds[player].options.goal == 'ganon_pedestal':
tt['sign_ganon'] = 'You need to pull the pedestal to defeat Ganon.'
- elif world.worlds[player].options.goal == "ganon":
- if world.worlds[player].options.crystals_needed_for_ganon == 1:
+ elif multiworld.worlds[player].options.goal == "ganon":
+ if multiworld.worlds[player].options.crystals_needed_for_ganon == 1:
tt['sign_ganon'] = 'You need a crystal to beat Ganon and have beaten Agahnim atop Ganons Tower.'
else:
- tt['sign_ganon'] = f'You need {world.worlds[player].options.crystals_needed_for_ganon} crystals to beat Ganon and ' \
+ tt['sign_ganon'] = f'You need {multiworld.worlds[player].options.crystals_needed_for_ganon} crystals to beat Ganon and ' \
f'have beaten Agahnim atop Ganons Tower'
else:
- if world.worlds[player].options.crystals_needed_for_ganon == 1:
+ if multiworld.worlds[player].options.crystals_needed_for_ganon == 1:
tt['sign_ganon'] = 'You need a crystal to beat Ganon.'
else:
- tt['sign_ganon'] = f'You need {world.worlds[player].options.crystals_needed_for_ganon} crystals to beat Ganon.'
+ tt['sign_ganon'] = f'You need {multiworld.worlds[player].options.crystals_needed_for_ganon} crystals to beat Ganon.'
tt['uncle_leaving_text'] = Uncle_texts[local_random.randint(0, len(Uncle_texts) - 1)]
tt['end_triforce'] = "{NOBORDER}\n" + Triforce_texts[local_random.randint(0, len(Triforce_texts) - 1)]
@@ -2475,12 +2479,12 @@ def hint_text(dest, ped_hint=False):
tt['blind_by_the_light'] = Blind_texts[local_random.randint(0, len(Blind_texts) - 1)]
triforce_pieces_required = max(0, w.treasure_hunt_required -
- sum(1 for item in world.precollected_items[player] if item.name == "Triforce Piece"))
+ sum(1 for item in multiworld.precollected_items[player] if item.name == "Triforce Piece"))
- if world.worlds[player].options.goal in ['triforce_hunt', 'local_triforce_hunt']:
+ if multiworld.worlds[player].options.goal in ['triforce_hunt', 'local_triforce_hunt']:
tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Get the Triforce Pieces.'
tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.'
- if world.worlds[player].options.goal == 'triforce_hunt' and world.players > 1:
+ if multiworld.worlds[player].options.goal == 'triforce_hunt' and multiworld.players > 1:
tt['sign_ganon'] = 'Go find the Triforce pieces with your friends... Ganon is invincible!'
else:
tt['sign_ganon'] = 'Go find the Triforce pieces... Ganon is invincible!'
@@ -2494,7 +2498,7 @@ def hint_text(dest, ped_hint=False):
"invisibility.\n\n\n\nâĻ âĻ âĻ\n\nWait! you can see me? I knew I should have\n" \
"hidden in a hollow tree. If you bring\n%d Triforce piece out of %d, I can reassemble it." % \
(triforce_pieces_required, w.treasure_hunt_total)
- elif world.worlds[player].options.goal in ['pedestal']:
+ elif multiworld.worlds[player].options.goal in ['pedestal']:
tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Your goal is at the pedestal.'
tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.'
tt['sign_ganon'] = 'You need to get to the pedestal... Ganon is invincible!'
@@ -2503,44 +2507,44 @@ def hint_text(dest, ped_hint=False):
tt['ganon_fall_in_alt'] = 'You cannot defeat me until you finish your goal!'
tt['ganon_phase_3_alt'] = 'Got wax in\nyour ears?\nI can not die!'
if triforce_pieces_required > 1:
- if world.worlds[player].options.goal == 'ganon_triforce_hunt' and world.players > 1:
+ if multiworld.worlds[player].options.goal == 'ganon_triforce_hunt' and multiworld.players > 1:
tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d with your friends to defeat Ganon.' % \
(triforce_pieces_required, w.treasure_hunt_total)
- elif world.worlds[player].options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
+ elif multiworld.worlds[player].options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d to defeat Ganon.' % \
(triforce_pieces_required, w.treasure_hunt_total)
else:
- if world.worlds[player].options.goal == 'ganon_triforce_hunt' and world.players > 1:
+ if multiworld.worlds[player].options.goal == 'ganon_triforce_hunt' and multiworld.players > 1:
tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d with your friends to defeat Ganon.' % \
(triforce_pieces_required, w.treasure_hunt_total)
- elif world.worlds[player].options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
+ elif multiworld.worlds[player].options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d to defeat Ganon.' % \
(triforce_pieces_required, w.treasure_hunt_total)
tt['kakariko_tavern_fisherman'] = TavernMan_texts[local_random.randint(0, len(TavernMan_texts) - 1)]
- pedestalitem = world.get_location('Master Sword Pedestal', player).item
+ pedestalitem = multiworld.get_location('Master Sword Pedestal', player).item
pedestal_text = 'Some Hot Air' if pedestalitem is None else hint_text(pedestalitem,
True) if pedestalitem.pedestal_hint_text is not None else 'Unknown Item'
tt['mastersword_pedestal_translated'] = pedestal_text
pedestal_credit_text = 'and the Hot Air' if pedestalitem is None else \
w.pedestal_credit_texts.get(pedestalitem.code, 'and the Unknown Item')
- etheritem = world.get_location('Ether Tablet', player).item
+ etheritem = multiworld.get_location('Ether Tablet', player).item
ether_text = 'Some Hot Air' if etheritem is None else hint_text(etheritem,
True) if etheritem.pedestal_hint_text is not None else 'Unknown Item'
tt['tablet_ether_book'] = ether_text
- bombositem = world.get_location('Bombos Tablet', player).item
+ bombositem = multiworld.get_location('Bombos Tablet', player).item
bombos_text = 'Some Hot Air' if bombositem is None else hint_text(bombositem,
True) if bombositem.pedestal_hint_text is not None else 'Unknown Item'
tt['tablet_bombos_book'] = bombos_text
# inverted spawn menu changes
- if world.worlds[player].options.mode == 'inverted':
+ if multiworld.worlds[player].options.mode == 'inverted':
tt['menu_start_2'] = "{MENU}\n{SPEED0}\nâĨ@'s house\n Dark Chapel\n{CHOICE3}"
tt['menu_start_3'] = "{MENU}\n{SPEED0}\nâĨ@'s house\n Dark Chapel\n Mountain Cave\n{CHOICE2}"
- for at, text, _ in world.worlds[player].options.plando_texts:
+ for at, text, _ in multiworld.worlds[player].options.plando_texts:
if at not in tt:
raise Exception(f"No text target \"{at}\" found.")
@@ -2551,22 +2555,22 @@ def hint_text(dest, ped_hint=False):
credits = Credits()
- sickkiditem = world.get_location('Sick Kid', player).item
+ sickkiditem = multiworld.get_location('Sick Kid', player).item
sickkiditem_text = local_random.choice(SickKid_texts) \
if sickkiditem is None or sickkiditem.code not in w.sickkid_credit_texts \
else w.sickkid_credit_texts[sickkiditem.code]
- zoraitem = world.get_location('King Zora', player).item
+ zoraitem = multiworld.get_location('King Zora', player).item
zoraitem_text = local_random.choice(Zora_texts) \
if zoraitem is None or zoraitem.code not in w.zora_credit_texts \
else w.zora_credit_texts[zoraitem.code]
- magicshopitem = world.get_location('Potion Shop', player).item
+ magicshopitem = multiworld.get_location('Potion Shop', player).item
magicshopitem_text = local_random.choice(MagicShop_texts) \
if magicshopitem is None or magicshopitem.code not in w.magicshop_credit_texts \
else w.magicshop_credit_texts[magicshopitem.code]
- fluteboyitem = world.get_location('Flute Spot', player).item
+ fluteboyitem = multiworld.get_location('Flute Spot', player).item
fluteboyitem_text = local_random.choice(FluteBoy_texts) \
if fluteboyitem is None or fluteboyitem.code not in w.fluteboy_credit_texts \
else w.fluteboy_credit_texts[fluteboyitem.code]
@@ -2595,7 +2599,7 @@ def hint_text(dest, ped_hint=False):
rom.write_bytes(0x76CC0, [byte for p in pointers for byte in [p & 0xFF, p >> 8 & 0xFF]])
-def set_inverted_mode(world, player, rom):
+def set_inverted_mode(multiworld: MultiWorld, player: int, rom: LocalRom):
rom.write_byte(snes_to_pc(0x0283E0), 0xF0) # residual portals
rom.write_byte(snes_to_pc(0x02B34D), 0xF0)
rom.write_byte(snes_to_pc(0x06DB78), 0x8B)
@@ -2613,12 +2617,12 @@ def set_inverted_mode(world, player, rom):
rom.write_byte(snes_to_pc(0x08D40C), 0xD0) # morph proof
# the following bytes should only be written in vanilla
# or they'll overwrite the randomizer's shuffles
- if world.worlds[player].options.entrance_shuffle == 'vanilla':
+ if multiworld.worlds[player].options.entrance_shuffle == 'vanilla':
rom.write_byte(0xDBB73 + 0x23, 0x37) # switch AT and GT
rom.write_byte(0xDBB73 + 0x36, 0x24)
rom.write_int16(0x15AEE + 2 * 0x38, 0x00E0)
rom.write_int16(0x15AEE + 2 * 0x25, 0x000C)
- if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
+ if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
rom.write_byte(0x15B8C, 0x6C)
rom.write_byte(0xDBB73 + 0x00, 0x53) # switch bomb shop and links house
rom.write_byte(0xDBB73 + 0x52, 0x01)
@@ -2676,7 +2680,7 @@ def set_inverted_mode(world, player, rom):
rom.write_int16(snes_to_pc(0x02D9A6), 0x005A)
rom.write_byte(snes_to_pc(0x02D9B3), 0x12)
# keep the old man spawn point at old man house unless shuffle is vanilla
- if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']:
+ if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']:
rom.write_bytes(snes_to_pc(0x308350), [0x00, 0x00, 0x01])
rom.write_int16(snes_to_pc(0x02D8DE), 0x00F1)
rom.write_bytes(snes_to_pc(0x02D910), [0x1F, 0x1E, 0x1F, 0x1F, 0x03, 0x02, 0x03, 0x03])
@@ -2739,7 +2743,7 @@ def set_inverted_mode(world, player, rom):
rom.write_int16s(snes_to_pc(0x1bb836), [0x001B, 0x001B, 0x001B])
rom.write_int16(snes_to_pc(0x308300), 0x0140) # new pyramid hole entrance
rom.write_int16(snes_to_pc(0x308320), 0x001B)
- if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
+ if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
rom.write_byte(snes_to_pc(0x308340), 0x7B)
rom.write_int16(snes_to_pc(0x1af504), 0x148B)
rom.write_int16(snes_to_pc(0x1af50c), 0x149B)
@@ -2776,10 +2780,10 @@ def set_inverted_mode(world, player, rom):
rom.write_bytes(snes_to_pc(0x1BC85A), [0x50, 0x0F, 0x82])
rom.write_int16(0xDB96F + 2 * 0x35, 0x001B) # move pyramid exit door
rom.write_int16(0xDBA71 + 2 * 0x35, 0x06A4)
- if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
+ if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
rom.write_byte(0xDBB73 + 0x35, 0x36)
rom.write_byte(snes_to_pc(0x09D436), 0xF3) # remove castle gate warp
- if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
+ if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
rom.write_int16(0x15AEE + 2 * 0x37, 0x0010) # pyramid exit to new hc area
rom.write_byte(0x15B8C + 0x37, 0x1B)
rom.write_int16(0x15BDB + 2 * 0x37, 0x0418)
diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py
index 18e2965d8c5a..bce75a157be7 100644
--- a/worlds/alttp/Rules.py
+++ b/worlds/alttp/Rules.py
@@ -1,9 +1,9 @@
import collections
import logging
-from typing import Iterator, Set
+from typing import Callable, Iterator, Set, TYPE_CHECKING
from Options import ItemsAccessibility
-from BaseClasses import MultiWorld
+from BaseClasses import CollectionState, Item, Location, MultiWorld, Region
from worlds.generic.Rules import (add_item_rule, add_rule, forbid_item,
item_name_in_location_names, location_item_name, set_rule, allow_self_locking_items)
@@ -21,112 +21,116 @@
has_misery_mire_medallion, has_sword, has_turtle_rock_medallion,
has_triforce_pieces, can_use_bombs, can_bomb_or_bonk,
can_activate_crystal_switch, can_kill_standard_start)
+from .SubClasses import ALttPLocation
from .UnderworldGlitchRules import underworld_glitches_rules
+if TYPE_CHECKING:
+ from . import ALTTPWorld
-def set_rules(world):
+
+def set_rules(world: "ALTTPWorld"):
player = world.player
- world = world.multiworld
- if world.worlds[player].options.glitches_required == 'no_logic':
- if player == next(player_id for player_id in world.get_game_players("A Link to the Past")
- if world.worlds[player_id].options.glitches_required == 'no_logic'): # only warn one time
+ multiworld = world.multiworld
+ if world.options.glitches_required == 'no_logic':
+ if player == next(player_id for player_id in multiworld.get_game_players("A Link to the Past")
+ if multiworld.worlds[player_id].options.glitches_required == 'no_logic'): # only warn one time
logging.info(
'WARNING! Seeds generated under this logic often require major glitches and may be impossible!')
- if world.players == 1:
- for exit in world.get_region('Menu', player).exits:
+ if multiworld.players == 1:
+ for exit in multiworld.get_region('Menu', player).exits:
exit.hide_path = True
return
else:
# Set access rules according to max glitches for multiworld progression.
# Set accessibility to none, and shuffle assuming the no logic players can always win
- world.worlds[player].options.accessibility.value = ItemsAccessibility.option_minimal
- world.worlds[player].options.progression_balancing.value = 0
+ world.options.accessibility.value = ItemsAccessibility.option_minimal
+ world.options.progression_balancing.value = 0
else:
- world.completion_condition[player] = lambda state: state.has('Triforce', player)
+ multiworld.completion_condition[player] = lambda state: state.has('Triforce', player)
- dungeon_boss_rules(world, player)
- global_rules(world, player)
+ dungeon_boss_rules(multiworld, player)
+ global_rules(multiworld, player)
- if world.worlds[player].options.mode != 'inverted':
- default_rules(world, player)
+ if world.options.mode != 'inverted':
+ default_rules(multiworld, player)
- if world.worlds[player].options.mode == 'open':
- open_rules(world, player)
- elif world.worlds[player].options.mode == 'standard':
- standard_rules(world, player)
- elif world.worlds[player].options.mode == 'inverted':
- open_rules(world, player)
- inverted_rules(world, player)
+ if world.options.mode == 'open':
+ open_rules(multiworld, player)
+ elif world.options.mode == 'standard':
+ standard_rules(multiworld, player)
+ elif world.options.mode == 'inverted':
+ open_rules(multiworld, player)
+ inverted_rules(multiworld, player)
else:
- raise NotImplementedError(f'World state {world.worlds[player].options.mode} is not implemented yet')
+ raise NotImplementedError(f'World state {world.options.mode} is not implemented yet')
- if world.worlds[player].options.glitches_required == 'no_glitches':
- no_glitches_rules(world, player)
- forbid_bomb_jump_requirements(world, player)
- elif world.worlds[player].options.glitches_required == 'overworld_glitches':
+ if world.options.glitches_required == 'no_glitches':
+ no_glitches_rules(multiworld, player)
+ forbid_bomb_jump_requirements(multiworld, player)
+ elif world.options.glitches_required == 'overworld_glitches':
# Initially setting no_glitches_rules to set the baseline rules for some
# entrances. The overworld_glitches_rules set is primarily additive.
- no_glitches_rules(world, player)
- fake_flipper_rules(world, player)
- overworld_glitches_rules(world, player)
- forbid_bomb_jump_requirements(world, player)
- elif world.worlds[player].options.glitches_required in ['hybrid_major_glitches', 'no_logic']:
- no_glitches_rules(world, player)
- fake_flipper_rules(world, player)
- overworld_glitches_rules(world, player)
- underworld_glitches_rules(world, player)
- bomb_jump_requirements(world, player)
- elif world.worlds[player].options.glitches_required == 'minor_glitches':
- no_glitches_rules(world, player)
- fake_flipper_rules(world, player)
- forbid_bomb_jump_requirements(world, player)
+ no_glitches_rules(multiworld, player)
+ fake_flipper_rules(multiworld, player)
+ overworld_glitches_rules(multiworld, player)
+ forbid_bomb_jump_requirements(multiworld, player)
+ elif world.options.glitches_required in ['hybrid_major_glitches', 'no_logic']:
+ no_glitches_rules(multiworld, player)
+ fake_flipper_rules(multiworld, player)
+ overworld_glitches_rules(multiworld, player)
+ underworld_glitches_rules(multiworld, player)
+ bomb_jump_requirements(multiworld, player)
+ elif world.options.glitches_required == 'minor_glitches':
+ no_glitches_rules(multiworld, player)
+ fake_flipper_rules(multiworld, player)
+ forbid_bomb_jump_requirements(multiworld, player)
else:
- raise NotImplementedError(f'Not implemented yet: Logic - {world.worlds[player].options.glitches_required}')
+ raise NotImplementedError(f'Not implemented yet: Logic - {world.options.glitches_required}')
- if world.worlds[player].options.goal == 'bosses':
+ if world.options.goal == 'bosses':
# require all bosses to beat ganon
- add_rule(world.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player) and state.has('Beat Agahnim 1', player) and state.has('Beat Agahnim 2', player) and has_crystals(state, 7, player))
- elif world.worlds[player].options.goal == 'ganon':
+ add_rule(multiworld.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player) and state.has('Beat Agahnim 1', player) and state.has('Beat Agahnim 2', player) and has_crystals(state, 7, player))
+ elif world.options.goal == 'ganon':
# require aga2 to beat ganon
- add_rule(world.get_location('Ganon', player), lambda state: state.has('Beat Agahnim 2', player))
+ add_rule(multiworld.get_location('Ganon', player), lambda state: state.has('Beat Agahnim 2', player))
- if world.worlds[player].options.mode != 'inverted':
- set_big_bomb_rules(world, player)
- if world.worlds[player].options.glitches_required.current_key in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and world.worlds[player].options.entrance_shuffle.current_key not in {'insanity', 'insanity_legacy', 'madness'}:
- path_to_courtyard = mirrorless_path_to_castle_courtyard(world, player)
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.multiworld.get_entrance('Dark Death Mountain Offset Mirror', player).can_reach(state) and all(rule(state) for rule in path_to_courtyard), 'or')
+ if world.options.mode != 'inverted':
+ set_big_bomb_rules(multiworld, player)
+ if world.options.glitches_required.current_key in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and world.options.entrance_shuffle.current_key not in {'insanity', 'insanity_legacy', 'madness'}:
+ path_to_courtyard = mirrorless_path_to_castle_courtyard(multiworld, player)
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.multiworld.get_entrance('Dark Death Mountain Offset Mirror', player).can_reach(state) and all(rule(state) for rule in path_to_courtyard), 'or')
else:
- set_inverted_big_bomb_rules(world, player)
+ set_inverted_big_bomb_rules(multiworld, player)
# if swamp and dam have not been moved we require mirror for swamp palace
# however there is mirrorless swamp in hybrid MG, so we don't necessarily want this. HMG handles this requirement itself.
- if not world.worlds[player].swamp_patch_required and world.worlds[player].options.glitches_required not in ['hybrid_major_glitches', 'no_logic']:
- add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player))
+ if not multiworld.worlds[player].swamp_patch_required and world.options.glitches_required not in ['hybrid_major_glitches', 'no_logic']:
+ add_rule(multiworld.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player))
# GT Entrance may be required for Turtle Rock for OWG and < 7 required
- ganons_tower = world.get_entrance('Inverted Ganons Tower' if world.worlds[player].options.mode == 'inverted' else 'Ganons Tower', player)
- if (world.worlds[player].options.crystals_needed_for_gt == 7
- and not (world.worlds[player].options.glitches_required
+ ganons_tower = multiworld.get_entrance('Inverted Ganons Tower' if world.options.mode == 'inverted' else 'Ganons Tower', player)
+ if (world.options.crystals_needed_for_gt == 7
+ and not (world.options.glitches_required
in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']
- and world.worlds[player].options.mode != 'inverted')):
+ and world.options.mode != 'inverted')):
set_rule(ganons_tower, lambda state: False)
- set_trock_key_rules(world, player)
+ set_trock_key_rules(multiworld, player)
- set_rule(ganons_tower, lambda state: has_crystals(state, state.multiworld.worlds[player].options.crystals_needed_for_gt, player))
- if world.worlds[player].options.mode != 'inverted' and world.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
- add_rule(world.get_entrance('Ganons Tower', player), lambda state: state.multiworld.get_entrance('Ganons Tower Ascent', player).can_reach(state), 'or')
+ set_rule(ganons_tower, lambda state: has_crystals(state, world.options.crystals_needed_for_gt.value, player))
+ if world.options.mode != 'inverted' and world.options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
+ add_rule(multiworld.get_entrance('Ganons Tower', player), lambda state: state.multiworld.get_entrance('Ganons Tower Ascent', player).can_reach(state), 'or')
- set_bunny_rules(world, player, world.worlds[player].options.mode == 'inverted')
+ set_bunny_rules(multiworld, player, world.options.mode == 'inverted')
-def mirrorless_path_to_castle_courtyard(world, player):
+def mirrorless_path_to_castle_courtyard(multiworld: MultiWorld, player: int):
# If Agahnim is defeated then the courtyard needs to be accessible without using the mirror for the mirror offset glitch.
# Only considering the secret passage for now (in non-insanity shuffle). Basically, if it's Ganon you need the master sword.
- start = world.get_entrance('Hyrule Castle Secret Entrance Drop', player)
- target = world.get_region('Hyrule Castle Courtyard', player)
+ start = multiworld.get_entrance('Hyrule Castle Secret Entrance Drop', player)
+ target = multiworld.get_region('Hyrule Castle Courtyard', player)
seen = {start.parent_region, start.connected_region}
queue = collections.deque([(start.connected_region, [])])
while queue:
@@ -139,37 +143,37 @@ def mirrorless_path_to_castle_courtyard(world, player):
else:
queue.append((entrance.connected_region, new_path))
- raise Exception(f"Could not find mirrorless path to castle courtyard for Player {player} ({world.get_player_name(player)})")
+ raise Exception(f"Could not find mirrorless path to castle courtyard for Player {player} ({multiworld.get_player_name(player)})")
-def set_defeat_dungeon_boss_rule(location):
+def set_defeat_dungeon_boss_rule(location: ALttPLocation):
# Lambda required to defer evaluation of dungeon.boss since it will change later if boss shuffle is used
add_rule(location, lambda state: location.parent_region.dungeon.boss.can_defeat(state))
-def set_always_allow(spot, rule):
+def set_always_allow(spot: Location, rule: Callable[[CollectionState, Item], bool]):
spot.always_allow = rule
-def add_lamp_requirement(world: MultiWorld, spot, player: int, has_accessible_torch: bool = False):
- if world.worlds[player].options.dark_room_logic == "lamp":
+def add_lamp_requirement(multiworld: MultiWorld, spot, player: int, has_accessible_torch: bool = False):
+ if multiworld.worlds[player].options.dark_room_logic == "lamp":
add_rule(spot, lambda state: state.has('Lamp', player))
- elif world.worlds[player].options.dark_room_logic == "torches": # implicitly lamp as well
+ elif multiworld.worlds[player].options.dark_room_logic == "torches": # implicitly lamp as well
if has_accessible_torch:
add_rule(spot, lambda state: state.has('Lamp', player) or state.has('Fire Rod', player))
else:
add_rule(spot, lambda state: state.has('Lamp', player))
- elif world.worlds[player].options.dark_room_logic == "none":
+ elif multiworld.worlds[player].options.dark_room_logic == "none":
pass
else:
- raise ValueError(f"Unknown Dark Room Logic: {world.worlds[player].options.dark_room_logic}")
+ raise ValueError(f"Unknown Dark Room Logic: {multiworld.worlds[player].options.dark_room_logic}")
non_crossover_items = (item_name_groups["Small Keys"] | item_name_groups["Big Keys"] | progression_items) - {
"Small Key (Universal)"}
-def dungeon_boss_rules(world, player):
+def dungeon_boss_rules(multiworld: MultiWorld, player: int):
boss_locations = {
'Agahnim 1',
'Tower of Hera - Boss',
@@ -190,7 +194,7 @@ def dungeon_boss_rules(world, player):
'Palace of Darkness - Prize',
}
for location in boss_locations:
- set_defeat_dungeon_boss_rule(world.get_location(location, player))
+ set_defeat_dungeon_boss_rule(multiworld.get_location(location, player))
def global_rules(multiworld: MultiWorld, player: int):
@@ -616,330 +620,332 @@ def global_rules(multiworld: MultiWorld, player: int):
set_rule(multiworld.get_location('Flute Activation Spot', player), lambda state: state.has('Flute', player))
-def default_rules(world, player):
+def default_rules(multiworld: MultiWorld, player: int):
"""Default world rules when world state is not inverted."""
# overworld requirements
- set_rule(world.get_entrance('Light World Bomb Hut', player), lambda state: can_use_bombs(state, player))
- set_rule(world.get_entrance('Light Hype Fairy', player), lambda state: can_use_bombs(state, player))
- set_rule(world.get_entrance('Mini Moldorm Cave', player), lambda state: can_use_bombs(state, player))
- set_rule(world.get_entrance('Ice Rod Cave', player), lambda state: can_use_bombs(state, player))
+ set_rule(multiworld.get_entrance('Light World Bomb Hut', player), lambda state: can_use_bombs(state, player))
+ set_rule(multiworld.get_entrance('Light Hype Fairy', player), lambda state: can_use_bombs(state, player))
+ set_rule(multiworld.get_entrance('Mini Moldorm Cave', player), lambda state: can_use_bombs(state, player))
+ set_rule(multiworld.get_entrance('Ice Rod Cave', player), lambda state: can_use_bombs(state, player))
- set_rule(world.get_entrance('Kings Grave', player), lambda state: state.has('Pegasus Boots', player))
- set_rule(world.get_entrance('Kings Grave Outer Rocks', player), lambda state: can_lift_heavy_rocks(state, player))
- set_rule(world.get_entrance('Kings Grave Inner Rocks', player), lambda state: can_lift_heavy_rocks(state, player))
- set_rule(world.get_entrance('Kings Grave Mirror Spot', player), lambda state: state.has('Moon Pearl', player) and state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Kings Grave', player), lambda state: state.has('Pegasus Boots', player))
+ set_rule(multiworld.get_entrance('Kings Grave Outer Rocks', player), lambda state: can_lift_heavy_rocks(state, player))
+ set_rule(multiworld.get_entrance('Kings Grave Inner Rocks', player), lambda state: can_lift_heavy_rocks(state, player))
+ set_rule(multiworld.get_entrance('Kings Grave Mirror Spot', player), lambda state: state.has('Moon Pearl', player) and state.has('Magic Mirror', player))
# Caution: If king's grave is releaxed at all to account for reaching it via a two way cave's exit in insanity mode, then the bomb shop logic will need to be updated (that would involve create a small ledge-like Region for it)
- set_rule(world.get_entrance('Bonk Fairy (Light)', player), lambda state: state.has('Pegasus Boots', player))
- set_rule(world.get_entrance('Lumberjack Tree Tree', player), lambda state: state.has('Pegasus Boots', player) and state.has('Beat Agahnim 1', player))
- set_rule(world.get_entrance('Bonk Rock Cave', player), lambda state: state.has('Pegasus Boots', player))
- set_rule(world.get_entrance('Desert Palace Stairs', player), lambda state: state.has('Book of Mudora', player))
- set_rule(world.get_entrance('Sanctuary Grave', player), lambda state: can_lift_rocks(state, player))
- set_rule(world.get_entrance('20 Rupee Cave', player), lambda state: can_lift_rocks(state, player))
- set_rule(world.get_entrance('50 Rupee Cave', player), lambda state: can_lift_rocks(state, player))
- set_rule(world.get_entrance('Death Mountain Entrance Rock', player), lambda state: can_lift_rocks(state, player))
- set_rule(world.get_entrance('Bumper Cave Entrance Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Flute Spot 1', player), lambda state: state.has('Activated Flute', player))
- set_rule(world.get_entrance('Lake Hylia Central Island Teleporter', player), lambda state: can_lift_heavy_rocks(state, player))
- set_rule(world.get_entrance('Dark Desert Teleporter', player), lambda state: state.has('Activated Flute', player) and can_lift_heavy_rocks(state, player))
- set_rule(world.get_entrance('East Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer
- set_rule(world.get_entrance('South Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer
- set_rule(world.get_entrance('Kakariko Teleporter', player), lambda state: ((state.has('Hammer', player) and can_lift_rocks(state, player)) or can_lift_heavy_rocks(state, player)) and state.has('Moon Pearl', player)) # bunny cannot lift bushes
- set_rule(world.get_location('Flute Spot', player), lambda state: state.has('Shovel', player))
- set_rule(world.get_entrance('Bat Cave Drop Ledge', player), lambda state: state.has('Hammer', player))
-
- set_rule(world.get_location('Zora\'s Ledge', player), lambda state: state.has('Flippers', player))
- set_rule(world.get_entrance('Waterfall of Wishing', player), lambda state: state.has('Flippers', player))
- set_rule(world.get_location('Frog', player), lambda state: can_lift_heavy_rocks(state, player)) # will get automatic moon pearl requirement
- set_rule(world.get_location('Potion Shop', player), lambda state: state.has('Mushroom', player))
- set_rule(world.get_entrance('Desert Palace Entrance (North) Rocks', player), lambda state: can_lift_rocks(state, player))
- set_rule(world.get_entrance('Desert Ledge Return Rocks', player), lambda state: can_lift_rocks(state, player)) # should we decide to place something that is not a dungeon end up there at some point
- set_rule(world.get_entrance('Checkerboard Cave', player), lambda state: can_lift_rocks(state, player))
- set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or has_beam_sword(state, player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle
- set_rule(world.get_entrance('Top of Pyramid', player), lambda state: state.has('Beat Agahnim 1', player))
- set_rule(world.get_entrance('Old Man Cave Exit (West)', player), lambda state: False) # drop cannot be climbed up
- set_rule(world.get_entrance('Broken Bridge (West)', player), lambda state: state.has('Hookshot', player))
- set_rule(world.get_entrance('Broken Bridge (East)', player), lambda state: state.has('Hookshot', player))
- set_rule(world.get_entrance('East Death Mountain Teleporter', player), lambda state: can_lift_heavy_rocks(state, player))
- set_rule(world.get_entrance('Fairy Ascension Rocks', player), lambda state: can_lift_heavy_rocks(state, player))
- set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has('Mirror', player)) # can erase block
- set_rule(world.get_entrance('Death Mountain (Top)', player), lambda state: state.has('Hammer', player))
- set_rule(world.get_entrance('Turtle Rock Teleporter', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Hammer', player))
- set_rule(world.get_entrance('East Death Mountain (Top)', player), lambda state: state.has('Hammer', player))
-
- set_rule(world.get_entrance('Catfish Exit Rock', player), lambda state: can_lift_rocks(state, player))
- set_rule(world.get_entrance('Catfish Entrance Rock', player), lambda state: can_lift_rocks(state, player))
- set_rule(world.get_entrance('Northeast Dark World Broken Bridge Pass', player), lambda state: state.has('Moon Pearl', player) and (can_lift_rocks(state, player) or state.has('Hammer', player) or state.has('Flippers', player)))
- set_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: state.has('Moon Pearl', player) and (can_lift_rocks(state, player) or state.has('Hammer', player)))
- set_rule(world.get_entrance('South Dark World Bridge', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Bonk Fairy (Dark)', player), lambda state: state.has('Moon Pearl', player) and state.has('Pegasus Boots', player))
- set_rule(world.get_entrance('West Dark World Gap', player), lambda state: state.has('Moon Pearl', player) and state.has('Hookshot', player))
- set_rule(world.get_entrance('Palace of Darkness', player), lambda state: state.has('Moon Pearl', player)) # kiki needs pearl
- set_rule(world.get_entrance('Hyrule Castle Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Hyrule Castle Main Gate', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: (state.has('Moon Pearl', player) and state.has('Flippers', player) or state.has('Magic Mirror', player))) # Overworld Bunny Revival
- set_rule(world.get_location('Bombos Tablet', player), lambda state: can_retrieve_tablet(state, player))
- set_rule(world.get_entrance('Dark Lake Hylia Drop (South)', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # ToDo any fake flipper set up?
- set_rule(world.get_entrance('Dark Lake Hylia Ledge Fairy', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player))
- set_rule(world.get_entrance('Dark Lake Hylia Ledge Spike Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Village of Outcasts Heavy Rock', player), lambda state: state.has('Moon Pearl', player) and can_lift_heavy_rocks(state, player))
- set_rule(world.get_entrance('Hype Cave', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player))
- set_rule(world.get_entrance('Brewery', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player))
- set_rule(world.get_entrance('Thieves Town', player), lambda state: state.has('Moon Pearl', player)) # bunny cannot pull
- set_rule(world.get_entrance('Skull Woods First Section Hole (North)', player), lambda state: state.has('Moon Pearl', player)) # bunny cannot lift bush
- set_rule(world.get_entrance('Skull Woods Second Section Hole', player), lambda state: state.has('Moon Pearl', player)) # bunny cannot lift bush
- set_rule(world.get_entrance('Maze Race Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Cave 45 Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Bombos Tablet Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('East Dark World Bridge', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player))
- set_rule(world.get_entrance('Lake Hylia Island Mirror Spot', player), lambda state: state.has('Moon Pearl', player) and state.has('Magic Mirror', player) and state.has('Flippers', player))
- set_rule(world.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('East Dark World River Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player))
- set_rule(world.get_entrance('Graveyard Ledge Mirror Spot', player), lambda state: state.has('Moon Pearl', player) and state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Bumper Cave Entrance Rock', player), lambda state: state.has('Moon Pearl', player) and can_lift_rocks(state, player))
- set_rule(world.get_entrance('Bumper Cave Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Bat Cave Drop Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Dark World Hammer Peg Cave', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player))
- set_rule(world.get_entrance('Village of Outcasts Eastern Rocks', player), lambda state: state.has('Moon Pearl', player) and can_lift_heavy_rocks(state, player))
- set_rule(world.get_entrance('Peg Area Rocks', player), lambda state: state.has('Moon Pearl', player) and can_lift_heavy_rocks(state, player))
- set_rule(world.get_entrance('Village of Outcasts Pegs', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player))
- set_rule(world.get_entrance('Grassy Lawn Pegs', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player))
- set_rule(world.get_entrance('Bumper Cave Exit (Top)', player), lambda state: state.has('Cape', player))
- set_rule(world.get_entrance('Bumper Cave Exit (Bottom)', player), lambda state: state.has('Cape', player) or state.has('Hookshot', player))
-
- set_rule(world.get_entrance('Skull Woods Final Section', player), lambda state: state.has('Fire Rod', player) and state.has('Moon Pearl', player)) # bunny cannot use fire rod
- set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has('Moon Pearl', player) and has_sword(state, player) and has_misery_mire_medallion(state, player)) # sword required to cast magic (!)
- set_rule(world.get_entrance('Desert Ledge (Northeast) Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
-
- set_rule(world.get_entrance('Desert Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Desert Palace Stairs Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Desert Palace Entrance (North) Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Spectacle Rock Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Hookshot Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
-
- set_rule(world.get_entrance('East Death Mountain (Top) Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Mimic Cave Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Spiral Cave Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Fairy Ascension Mirror Spot', player), lambda state: state.has('Magic Mirror', player) and state.has('Moon Pearl', player)) # need to lift flowers
- set_rule(world.get_entrance('Isolated Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Superbunny Cave Exit (Bottom)', player), lambda state: False) # Cannot get to bottom exit from top. Just exists for shuffling
- set_rule(world.get_entrance('Floating Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and has_sword(state, player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!)
-
- set_rule(world.get_entrance('Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.worlds[player].options.open_pyramid.to_bool(world, player))
-
- if world.worlds[player].options.swordless:
- swordless_rules(world, player)
-
-
-def inverted_rules(world, player):
+ set_rule(multiworld.get_entrance('Bonk Fairy (Light)', player), lambda state: state.has('Pegasus Boots', player))
+ set_rule(multiworld.get_entrance('Lumberjack Tree Tree', player), lambda state: state.has('Pegasus Boots', player) and state.has('Beat Agahnim 1', player))
+ set_rule(multiworld.get_entrance('Bonk Rock Cave', player), lambda state: state.has('Pegasus Boots', player))
+ set_rule(multiworld.get_entrance('Desert Palace Stairs', player), lambda state: state.has('Book of Mudora', player))
+ set_rule(multiworld.get_entrance('Sanctuary Grave', player), lambda state: can_lift_rocks(state, player))
+ set_rule(multiworld.get_entrance('20 Rupee Cave', player), lambda state: can_lift_rocks(state, player))
+ set_rule(multiworld.get_entrance('50 Rupee Cave', player), lambda state: can_lift_rocks(state, player))
+ set_rule(multiworld.get_entrance('Death Mountain Entrance Rock', player), lambda state: can_lift_rocks(state, player))
+ set_rule(multiworld.get_entrance('Bumper Cave Entrance Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Flute Spot 1', player), lambda state: state.has('Activated Flute', player))
+ set_rule(multiworld.get_entrance('Lake Hylia Central Island Teleporter', player), lambda state: can_lift_heavy_rocks(state, player))
+ set_rule(multiworld.get_entrance('Dark Desert Teleporter', player), lambda state: state.has('Activated Flute', player) and can_lift_heavy_rocks(state, player))
+ set_rule(multiworld.get_entrance('East Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer
+ set_rule(multiworld.get_entrance('South Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer
+ set_rule(multiworld.get_entrance('Kakariko Teleporter', player), lambda state: ((state.has('Hammer', player) and can_lift_rocks(state, player)) or can_lift_heavy_rocks(state, player)) and state.has('Moon Pearl', player)) # bunny cannot lift bushes
+ set_rule(multiworld.get_location('Flute Spot', player), lambda state: state.has('Shovel', player))
+ set_rule(multiworld.get_entrance('Bat Cave Drop Ledge', player), lambda state: state.has('Hammer', player))
+
+ set_rule(multiworld.get_location('Zora\'s Ledge', player), lambda state: state.has('Flippers', player))
+ set_rule(multiworld.get_entrance('Waterfall of Wishing', player), lambda state: state.has('Flippers', player))
+ set_rule(multiworld.get_location('Frog', player), lambda state: can_lift_heavy_rocks(state, player)) # will get automatic moon pearl requirement
+ set_rule(multiworld.get_location('Potion Shop', player), lambda state: state.has('Mushroom', player))
+ set_rule(multiworld.get_entrance('Desert Palace Entrance (North) Rocks', player), lambda state: can_lift_rocks(state, player))
+ set_rule(multiworld.get_entrance('Desert Ledge Return Rocks', player), lambda state: can_lift_rocks(state, player)) # should we decide to place something that is not a dungeon end up there at some point
+ set_rule(multiworld.get_entrance('Checkerboard Cave', player), lambda state: can_lift_rocks(state, player))
+ set_rule(multiworld.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or has_beam_sword(state, player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle
+ set_rule(multiworld.get_entrance('Top of Pyramid', player), lambda state: state.has('Beat Agahnim 1', player))
+ set_rule(multiworld.get_entrance('Old Man Cave Exit (West)', player), lambda state: False) # drop cannot be climbed up
+ set_rule(multiworld.get_entrance('Broken Bridge (West)', player), lambda state: state.has('Hookshot', player))
+ set_rule(multiworld.get_entrance('Broken Bridge (East)', player), lambda state: state.has('Hookshot', player))
+ set_rule(multiworld.get_entrance('East Death Mountain Teleporter', player), lambda state: can_lift_heavy_rocks(state, player))
+ set_rule(multiworld.get_entrance('Fairy Ascension Rocks', player), lambda state: can_lift_heavy_rocks(state, player))
+ set_rule(multiworld.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has('Mirror', player)) # can erase block
+ set_rule(multiworld.get_entrance('Death Mountain (Top)', player), lambda state: state.has('Hammer', player))
+ set_rule(multiworld.get_entrance('Turtle Rock Teleporter', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Hammer', player))
+ set_rule(multiworld.get_entrance('East Death Mountain (Top)', player), lambda state: state.has('Hammer', player))
+
+ set_rule(multiworld.get_entrance('Catfish Exit Rock', player), lambda state: can_lift_rocks(state, player))
+ set_rule(multiworld.get_entrance('Catfish Entrance Rock', player), lambda state: can_lift_rocks(state, player))
+ set_rule(multiworld.get_entrance('Northeast Dark World Broken Bridge Pass', player), lambda state: state.has('Moon Pearl', player) and (can_lift_rocks(state, player) or state.has('Hammer', player) or state.has('Flippers', player)))
+ set_rule(multiworld.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: state.has('Moon Pearl', player) and (can_lift_rocks(state, player) or state.has('Hammer', player)))
+ set_rule(multiworld.get_entrance('South Dark World Bridge', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Bonk Fairy (Dark)', player), lambda state: state.has('Moon Pearl', player) and state.has('Pegasus Boots', player))
+ set_rule(multiworld.get_entrance('West Dark World Gap', player), lambda state: state.has('Moon Pearl', player) and state.has('Hookshot', player))
+ set_rule(multiworld.get_entrance('Palace of Darkness', player), lambda state: state.has('Moon Pearl', player)) # kiki needs pearl
+ set_rule(multiworld.get_entrance('Hyrule Castle Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Hyrule Castle Main Gate', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: (state.has('Moon Pearl', player) and state.has('Flippers', player) or state.has('Magic Mirror', player))) # Overworld Bunny Revival
+ set_rule(multiworld.get_location('Bombos Tablet', player), lambda state: can_retrieve_tablet(state, player))
+ set_rule(multiworld.get_entrance('Dark Lake Hylia Drop (South)', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # ToDo any fake flipper set up?
+ set_rule(multiworld.get_entrance('Dark Lake Hylia Ledge Fairy', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player))
+ set_rule(multiworld.get_entrance('Dark Lake Hylia Ledge Spike Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Village of Outcasts Heavy Rock', player), lambda state: state.has('Moon Pearl', player) and can_lift_heavy_rocks(state, player))
+ set_rule(multiworld.get_entrance('Hype Cave', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player))
+ set_rule(multiworld.get_entrance('Brewery', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player))
+ set_rule(multiworld.get_entrance('Thieves Town', player), lambda state: state.has('Moon Pearl', player)) # bunny cannot pull
+ set_rule(multiworld.get_entrance('Skull Woods First Section Hole (North)', player), lambda state: state.has('Moon Pearl', player)) # bunny cannot lift bush
+ set_rule(multiworld.get_entrance('Skull Woods Second Section Hole', player), lambda state: state.has('Moon Pearl', player)) # bunny cannot lift bush
+ set_rule(multiworld.get_entrance('Maze Race Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Cave 45 Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Bombos Tablet Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('East Dark World Bridge', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player))
+ set_rule(multiworld.get_entrance('Lake Hylia Island Mirror Spot', player), lambda state: state.has('Moon Pearl', player) and state.has('Magic Mirror', player) and state.has('Flippers', player))
+ set_rule(multiworld.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('East Dark World River Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player))
+ set_rule(multiworld.get_entrance('Graveyard Ledge Mirror Spot', player), lambda state: state.has('Moon Pearl', player) and state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Bumper Cave Entrance Rock', player), lambda state: state.has('Moon Pearl', player) and can_lift_rocks(state, player))
+ set_rule(multiworld.get_entrance('Bumper Cave Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Bat Cave Drop Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Dark World Hammer Peg Cave', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player))
+ set_rule(multiworld.get_entrance('Village of Outcasts Eastern Rocks', player), lambda state: state.has('Moon Pearl', player) and can_lift_heavy_rocks(state, player))
+ set_rule(multiworld.get_entrance('Peg Area Rocks', player), lambda state: state.has('Moon Pearl', player) and can_lift_heavy_rocks(state, player))
+ set_rule(multiworld.get_entrance('Village of Outcasts Pegs', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player))
+ set_rule(multiworld.get_entrance('Grassy Lawn Pegs', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player))
+ set_rule(multiworld.get_entrance('Bumper Cave Exit (Top)', player), lambda state: state.has('Cape', player))
+ set_rule(multiworld.get_entrance('Bumper Cave Exit (Bottom)', player), lambda state: state.has('Cape', player) or state.has('Hookshot', player))
+
+ set_rule(multiworld.get_entrance('Skull Woods Final Section', player), lambda state: state.has('Fire Rod', player) and state.has('Moon Pearl', player)) # bunny cannot use fire rod
+ set_rule(multiworld.get_entrance('Misery Mire', player), lambda state: state.has('Moon Pearl', player) and has_sword(state, player) and has_misery_mire_medallion(state, player)) # sword required to cast magic (!)
+ set_rule(multiworld.get_entrance('Desert Ledge (Northeast) Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+
+ set_rule(multiworld.get_entrance('Desert Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Desert Palace Stairs Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Desert Palace Entrance (North) Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Spectacle Rock Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Hookshot Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
+
+ set_rule(multiworld.get_entrance('East Death Mountain (Top) Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Mimic Cave Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Spiral Cave Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Fairy Ascension Mirror Spot', player), lambda state: state.has('Magic Mirror', player) and state.has('Moon Pearl', player)) # need to lift flowers
+ set_rule(multiworld.get_entrance('Isolated Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Superbunny Cave Exit (Bottom)', player), lambda state: False) # Cannot get to bottom exit from top. Just exists for shuffling
+ set_rule(multiworld.get_entrance('Floating Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and has_sword(state, player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!)
+
+ set_rule(multiworld.get_entrance('Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or multiworld.worlds[player].options.open_pyramid.to_bool(multiworld, player))
+
+ if multiworld.worlds[player].options.swordless:
+ swordless_rules(multiworld, player)
+
+
+def inverted_rules(multiworld: MultiWorld, player: int):
# s&q regions.
- set_rule(world.get_entrance('Castle Ledge S&Q', player), lambda state: state.has('Magic Mirror', player) and state.has('Beat Agahnim 1', player))
+ set_rule(multiworld.get_entrance('Castle Ledge S&Q', player), lambda state: state.has('Magic Mirror', player) and state.has('Beat Agahnim 1', player))
# overworld requirements
- set_rule(world.get_location('Maze Race', player), lambda state: state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Mini Moldorm Cave', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player))
- set_rule(world.get_entrance('Ice Rod Cave', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player))
- set_rule(world.get_entrance('Light Hype Fairy', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player))
- set_rule(world.get_entrance('Potion Shop Pier', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Light World Pier', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Kings Grave', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Kings Grave Outer Rocks', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Kings Grave Inner Rocks', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Potion Shop Inner Bushes', player), lambda state: state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Potion Shop Outer Bushes', player), lambda state: state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Potion Shop Outer Rock', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Potion Shop Inner Rock', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Graveyard Cave Inner Bushes', player), lambda state: state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Graveyard Cave Outer Bushes', player), lambda state: state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Secret Passage Inner Bushes', player), lambda state: state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Secret Passage Outer Bushes', player), lambda state: state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Bonk Fairy (Light)', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Bat Cave Drop Ledge', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Lumberjack Tree Tree', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player) and state.has('Beat Agahnim 1', player))
- set_rule(world.get_entrance('Bonk Rock Cave', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Desert Palace Stairs', player), lambda state: state.has('Book of Mudora', player)) # bunny can use book
- set_rule(world.get_entrance('Sanctuary Grave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
- set_rule(world.get_entrance('20 Rupee Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
- set_rule(world.get_entrance('50 Rupee Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Death Mountain Entrance Rock', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Bumper Cave Entrance Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Dark Lake Hylia Central Island Teleporter', player), lambda state: can_lift_heavy_rocks(state, player))
- set_rule(world.get_entrance('Dark Desert Teleporter', player), lambda state: state.has('Activated Flute', player) and can_lift_heavy_rocks(state, player))
- set_rule(world.get_entrance('East Dark World Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer
- set_rule(world.get_entrance('South Dark World Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer
- set_rule(world.get_entrance('West Dark World Teleporter', player), lambda state: ((state.has('Hammer', player) and can_lift_rocks(state, player)) or can_lift_heavy_rocks(state, player)) and state.has('Moon Pearl', player))
- set_rule(world.get_location('Flute Spot', player), lambda state: state.has('Shovel', player) and state.has('Moon Pearl', player))
-
- set_rule(world.get_location('Zora\'s Ledge', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Waterfall of Wishing Cave', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Northeast Light World Return', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player))
- set_rule(world.get_location('Frog', player), lambda state: can_lift_heavy_rocks(state, player) and (state.has('Moon Pearl', player) or state.has('Beat Agahnim 1', player)) or (state.can_reach('Light World', 'Region', player) and state.has('Magic Mirror', player))) # Need LW access using Mirror or Portal
- set_rule(world.get_location('Missing Smith', player), lambda state: state.has('Get Frog', player) and state.can_reach('Blacksmiths Hut', 'Region', player)) # Can't S&Q with smith
- set_rule(world.get_location('Blacksmith', player), lambda state: state.has('Return Smith', player))
- set_rule(world.get_location('Magic Bat', player), lambda state: state.has('Magic Powder', player) and state.has('Moon Pearl', player))
- set_rule(world.get_location('Sick Kid', player), lambda state: state.has_group("Bottles", player))
- set_rule(world.get_location('Mushroom', player), lambda state: state.has('Moon Pearl', player)) # need pearl to pick up bushes
- set_rule(world.get_entrance('Bush Covered Lawn Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Bush Covered Lawn Inner Bushes', player), lambda state: state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Bush Covered Lawn Outer Bushes', player), lambda state: state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Bomb Hut Inner Bushes', player), lambda state: state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Bomb Hut Outer Bushes', player), lambda state: state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Light World Bomb Hut', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player))
- set_rule(world.get_entrance('North Fairy Cave Drop', player), lambda state: state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Lost Woods Hideout Drop', player), lambda state: state.has('Moon Pearl', player))
- set_rule(world.get_location('Potion Shop', player), lambda state: state.has('Mushroom', player) and (state.can_reach('Potion Shop Area', 'Region', player))) # new inverted region, need pearl for bushes or access to potion shop door/waterfall fairy
- set_rule(world.get_entrance('Desert Palace Entrance (North) Rocks', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Desert Ledge Return Rocks', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # should we decide to place something that is not a dungeon end up there at some point
- set_rule(world.get_entrance('Checkerboard Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Hyrule Castle Secret Entrance Drop', player), lambda state: state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Old Man Cave Exit (West)', player), lambda state: False) # drop cannot be climbed up
- set_rule(world.get_entrance('Broken Bridge (West)', player), lambda state: state.has('Hookshot', player) and state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Broken Bridge (East)', player), lambda state: state.has('Hookshot', player) and state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Dark Death Mountain Teleporter (East Bottom)', player), lambda state: can_lift_heavy_rocks(state, player))
- set_rule(world.get_entrance('Fairy Ascension Rocks', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has('Mirror', player)) # can erase block
- set_rule(world.get_entrance('Death Mountain (Top)', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Dark Death Mountain Teleporter (East)', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Hammer', player) and state.has('Moon Pearl', player)) # bunny cannot use hammer
- set_rule(world.get_entrance('East Death Mountain (Top)', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player)) # bunny can not use hammer
-
- set_rule(world.get_entrance('Catfish Entrance Rock', player), lambda state: can_lift_rocks(state, player))
- set_rule(world.get_entrance('Northeast Dark World Broken Bridge Pass', player), lambda state: ((can_lift_rocks(state, player) or state.has('Hammer', player)) or state.has('Flippers', player)))
- set_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: (can_lift_rocks(state, player) or state.has('Hammer', player)))
- set_rule(world.get_entrance('South Dark World Bridge', player), lambda state: state.has('Hammer', player))
- set_rule(world.get_entrance('Bonk Fairy (Dark)', player), lambda state: state.has('Pegasus Boots', player))
- set_rule(world.get_entrance('West Dark World Gap', player), lambda state: state.has('Hookshot', player))
- set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Flippers', player))
- set_rule(world.get_location('Bombos Tablet', player), lambda state: can_retrieve_tablet(state, player))
- set_rule(world.get_entrance('Dark Lake Hylia Drop (South)', player), lambda state: state.has('Flippers', player)) # ToDo any fake flipper set up?
- set_rule(world.get_entrance('Dark Lake Hylia Ledge Pier', player), lambda state: state.has('Flippers', player))
- set_rule(world.get_entrance('Dark Lake Hylia Ledge Spike Cave', player), lambda state: can_lift_rocks(state, player))
- set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Flippers', player)) # Fake Flippers
- set_rule(world.get_entrance('Dark Lake Hylia Shallows', player), lambda state: state.has('Flippers', player))
- set_rule(world.get_entrance('Village of Outcasts Heavy Rock', player), lambda state: can_lift_heavy_rocks(state, player))
- set_rule(world.get_entrance('East Dark World Bridge', player), lambda state: state.has('Hammer', player))
- set_rule(world.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('East Dark World River Pier', player), lambda state: state.has('Flippers', player))
- set_rule(world.get_entrance('Bumper Cave Entrance Rock', player), lambda state: can_lift_rocks(state, player))
- set_rule(world.get_entrance('Bumper Cave Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Hammer Peg Area Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Dark World Hammer Peg Cave', player), lambda state: state.has('Hammer', player))
- set_rule(world.get_entrance('Village of Outcasts Eastern Rocks', player), lambda state: can_lift_heavy_rocks(state, player))
- set_rule(world.get_entrance('Peg Area Rocks', player), lambda state: can_lift_heavy_rocks(state, player))
- set_rule(world.get_entrance('Village of Outcasts Pegs', player), lambda state: state.has('Hammer', player))
- set_rule(world.get_entrance('Grassy Lawn Pegs', player), lambda state: state.has('Hammer', player))
- set_rule(world.get_entrance('Bumper Cave Exit (Top)', player), lambda state: state.has('Cape', player))
- set_rule(world.get_entrance('Bumper Cave Exit (Bottom)', player), lambda state: state.has('Cape', player) or state.has('Hookshot', player))
-
- set_rule(world.get_entrance('Hype Cave', player), lambda state: can_use_bombs(state, player))
- set_rule(world.get_entrance('Brewery', player), lambda state: can_use_bombs(state, player))
- set_rule(world.get_entrance('Dark Lake Hylia Ledge Fairy', player), lambda state: can_use_bombs(state, player))
-
-
- set_rule(world.get_entrance('Skull Woods Final Section', player), lambda state: state.has('Fire Rod', player))
- set_rule(world.get_entrance('Misery Mire', player), lambda state: has_sword(state, player) and has_misery_mire_medallion(state, player)) # sword required to cast magic (!)
-
- set_rule(world.get_entrance('Hookshot Cave', player), lambda state: can_lift_rocks(state, player))
-
- set_rule(world.get_entrance('East Death Mountain Mirror Spot (Top)', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Death Mountain (Top) Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
-
- set_rule(world.get_entrance('East Death Mountain Mirror Spot (Bottom)', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Dark Death Mountain Ledge Mirror Spot (East)', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Dark Death Mountain Ledge Mirror Spot (West)', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Laser Bridge Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Floating Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Turtle Rock', player), lambda state: has_sword(state, player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!)
+ set_rule(multiworld.get_location('Maze Race', player), lambda state: state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Mini Moldorm Cave', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player))
+ set_rule(multiworld.get_entrance('Ice Rod Cave', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player))
+ set_rule(multiworld.get_entrance('Light Hype Fairy', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player))
+ set_rule(multiworld.get_entrance('Potion Shop Pier', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Light World Pier', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Kings Grave', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Kings Grave Outer Rocks', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Kings Grave Inner Rocks', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Potion Shop Inner Bushes', player), lambda state: state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Potion Shop Outer Bushes', player), lambda state: state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Potion Shop Outer Rock', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Potion Shop Inner Rock', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Graveyard Cave Inner Bushes', player), lambda state: state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Graveyard Cave Outer Bushes', player), lambda state: state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Secret Passage Inner Bushes', player), lambda state: state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Secret Passage Outer Bushes', player), lambda state: state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Bonk Fairy (Light)', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Bat Cave Drop Ledge', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Lumberjack Tree Tree', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player) and state.has('Beat Agahnim 1', player))
+ set_rule(multiworld.get_entrance('Bonk Rock Cave', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Desert Palace Stairs', player), lambda state: state.has('Book of Mudora', player)) # bunny can use book
+ set_rule(multiworld.get_entrance('Sanctuary Grave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('20 Rupee Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('50 Rupee Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Death Mountain Entrance Rock', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Bumper Cave Entrance Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Dark Lake Hylia Central Island Teleporter', player), lambda state: can_lift_heavy_rocks(state, player))
+ set_rule(multiworld.get_entrance('Dark Desert Teleporter', player), lambda state: state.has('Activated Flute', player) and can_lift_heavy_rocks(state, player))
+ set_rule(multiworld.get_entrance('East Dark World Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer
+ set_rule(multiworld.get_entrance('South Dark World Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer
+ set_rule(multiworld.get_entrance('West Dark World Teleporter', player), lambda state: ((state.has('Hammer', player) and can_lift_rocks(state, player)) or can_lift_heavy_rocks(state, player)) and state.has('Moon Pearl', player))
+ set_rule(multiworld.get_location('Flute Spot', player), lambda state: state.has('Shovel', player) and state.has('Moon Pearl', player))
+
+ set_rule(multiworld.get_location('Zora\'s Ledge', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Waterfall of Wishing Cave', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Northeast Light World Return', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player))
+ set_rule(multiworld.get_location('Frog', player), lambda state: can_lift_heavy_rocks(state, player) and (state.has('Moon Pearl', player) or state.has('Beat Agahnim 1', player)) or (state.can_reach('Light World', 'Region', player) and state.has('Magic Mirror', player))) # Need LW access using Mirror or Portal
+ set_rule(multiworld.get_location('Missing Smith', player), lambda state: state.has('Get Frog', player) and state.can_reach('Blacksmiths Hut', 'Region', player)) # Can't S&Q with smith
+ set_rule(multiworld.get_location('Blacksmith', player), lambda state: state.has('Return Smith', player))
+ set_rule(multiworld.get_location('Magic Bat', player), lambda state: state.has('Magic Powder', player) and state.has('Moon Pearl', player))
+ set_rule(multiworld.get_location('Sick Kid', player), lambda state: state.has_group("Bottles", player))
+ set_rule(multiworld.get_location('Mushroom', player), lambda state: state.has('Moon Pearl', player)) # need pearl to pick up bushes
+ set_rule(multiworld.get_entrance('Bush Covered Lawn Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Bush Covered Lawn Inner Bushes', player), lambda state: state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Bush Covered Lawn Outer Bushes', player), lambda state: state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Bomb Hut Inner Bushes', player), lambda state: state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Bomb Hut Outer Bushes', player), lambda state: state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Light World Bomb Hut', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player))
+ set_rule(multiworld.get_entrance('North Fairy Cave Drop', player), lambda state: state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Lost Woods Hideout Drop', player), lambda state: state.has('Moon Pearl', player))
+ set_rule(multiworld.get_location('Potion Shop', player), lambda state: state.has('Mushroom', player) and (state.can_reach('Potion Shop Area', 'Region', player))) # new inverted region, need pearl for bushes or access to potion shop door/waterfall fairy
+ set_rule(multiworld.get_entrance('Desert Palace Entrance (North) Rocks', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Desert Ledge Return Rocks', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # should we decide to place something that is not a dungeon end up there at some point
+ set_rule(multiworld.get_entrance('Checkerboard Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Hyrule Castle Secret Entrance Drop', player), lambda state: state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Old Man Cave Exit (West)', player), lambda state: False) # drop cannot be climbed up
+ set_rule(multiworld.get_entrance('Broken Bridge (West)', player), lambda state: state.has('Hookshot', player) and state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Broken Bridge (East)', player), lambda state: state.has('Hookshot', player) and state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Dark Death Mountain Teleporter (East Bottom)', player), lambda state: can_lift_heavy_rocks(state, player))
+ set_rule(multiworld.get_entrance('Fairy Ascension Rocks', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has('Mirror', player)) # can erase block
+ set_rule(multiworld.get_entrance('Death Mountain (Top)', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Dark Death Mountain Teleporter (East)', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Hammer', player) and state.has('Moon Pearl', player)) # bunny cannot use hammer
+ set_rule(multiworld.get_entrance('East Death Mountain (Top)', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player)) # bunny can not use hammer
+
+ set_rule(multiworld.get_entrance('Catfish Entrance Rock', player), lambda state: can_lift_rocks(state, player))
+ set_rule(multiworld.get_entrance('Northeast Dark World Broken Bridge Pass', player), lambda state: ((can_lift_rocks(state, player) or state.has('Hammer', player)) or state.has('Flippers', player)))
+ set_rule(multiworld.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: (can_lift_rocks(state, player) or state.has('Hammer', player)))
+ set_rule(multiworld.get_entrance('South Dark World Bridge', player), lambda state: state.has('Hammer', player))
+ set_rule(multiworld.get_entrance('Bonk Fairy (Dark)', player), lambda state: state.has('Pegasus Boots', player))
+ set_rule(multiworld.get_entrance('West Dark World Gap', player), lambda state: state.has('Hookshot', player))
+ set_rule(multiworld.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Flippers', player))
+ set_rule(multiworld.get_location('Bombos Tablet', player), lambda state: can_retrieve_tablet(state, player))
+ set_rule(multiworld.get_entrance('Dark Lake Hylia Drop (South)', player), lambda state: state.has('Flippers', player)) # ToDo any fake flipper set up?
+ set_rule(multiworld.get_entrance('Dark Lake Hylia Ledge Pier', player), lambda state: state.has('Flippers', player))
+ set_rule(multiworld.get_entrance('Dark Lake Hylia Ledge Spike Cave', player), lambda state: can_lift_rocks(state, player))
+ set_rule(multiworld.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Flippers', player)) # Fake Flippers
+ set_rule(multiworld.get_entrance('Dark Lake Hylia Shallows', player), lambda state: state.has('Flippers', player))
+ set_rule(multiworld.get_entrance('Village of Outcasts Heavy Rock', player), lambda state: can_lift_heavy_rocks(state, player))
+ set_rule(multiworld.get_entrance('East Dark World Bridge', player), lambda state: state.has('Hammer', player))
+ set_rule(multiworld.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('East Dark World River Pier', player), lambda state: state.has('Flippers', player))
+ set_rule(multiworld.get_entrance('Bumper Cave Entrance Rock', player), lambda state: can_lift_rocks(state, player))
+ set_rule(multiworld.get_entrance('Bumper Cave Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Hammer Peg Area Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Dark World Hammer Peg Cave', player), lambda state: state.has('Hammer', player))
+ set_rule(multiworld.get_entrance('Village of Outcasts Eastern Rocks', player), lambda state: can_lift_heavy_rocks(state, player))
+ set_rule(multiworld.get_entrance('Peg Area Rocks', player), lambda state: can_lift_heavy_rocks(state, player))
+ set_rule(multiworld.get_entrance('Village of Outcasts Pegs', player), lambda state: state.has('Hammer', player))
+ set_rule(multiworld.get_entrance('Grassy Lawn Pegs', player), lambda state: state.has('Hammer', player))
+ set_rule(multiworld.get_entrance('Bumper Cave Exit (Top)', player), lambda state: state.has('Cape', player))
+ set_rule(multiworld.get_entrance('Bumper Cave Exit (Bottom)', player), lambda state: state.has('Cape', player) or state.has('Hookshot', player))
+
+ set_rule(multiworld.get_entrance('Hype Cave', player), lambda state: can_use_bombs(state, player))
+ set_rule(multiworld.get_entrance('Brewery', player), lambda state: can_use_bombs(state, player))
+ set_rule(multiworld.get_entrance('Dark Lake Hylia Ledge Fairy', player), lambda state: can_use_bombs(state, player))
+
+
+ set_rule(multiworld.get_entrance('Skull Woods Final Section', player), lambda state: state.has('Fire Rod', player))
+ set_rule(multiworld.get_entrance('Misery Mire', player), lambda state: has_sword(state, player) and has_misery_mire_medallion(state, player)) # sword required to cast magic (!)
+
+ set_rule(multiworld.get_entrance('Hookshot Cave', player), lambda state: can_lift_rocks(state, player))
+
+ set_rule(multiworld.get_entrance('East Death Mountain Mirror Spot (Top)', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Death Mountain (Top) Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+
+ set_rule(multiworld.get_entrance('East Death Mountain Mirror Spot (Bottom)', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Dark Death Mountain Ledge Mirror Spot (East)', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Dark Death Mountain Ledge Mirror Spot (West)', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Laser Bridge Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Floating Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Turtle Rock', player), lambda state: has_sword(state, player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!)
# new inverted spots
- set_rule(world.get_entrance('Post Aga Teleporter', player), lambda state: state.has('Beat Agahnim 1', player))
- set_rule(world.get_entrance('Mire Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Desert Palace Stairs Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Death Mountain Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('East Dark World Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('West Dark World Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('South Dark World Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Catfish Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Potion Shop Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Shopping Mall Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Maze Race Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Desert Palace North Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Death Mountain (Top) Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Graveyard Cave Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Bomb Hut Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
- set_rule(world.get_entrance('Skull Woods Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Post Aga Teleporter', player), lambda state: state.has('Beat Agahnim 1', player))
+ set_rule(multiworld.get_entrance('Mire Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Desert Palace Stairs Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Death Mountain Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('East Dark World Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('West Dark World Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('South Dark World Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Catfish Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Potion Shop Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Shopping Mall Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Maze Race Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Desert Palace North Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Death Mountain (Top) Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Graveyard Cave Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Bomb Hut Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
+ set_rule(multiworld.get_entrance('Skull Woods Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
# inverted flute spots
- set_rule(world.get_entrance('DDM Flute', player), lambda state: state.has('Activated Flute', player))
- set_rule(world.get_entrance('NEDW Flute', player), lambda state: state.has('Activated Flute', player))
- set_rule(world.get_entrance('WDW Flute', player), lambda state: state.has('Activated Flute', player))
- set_rule(world.get_entrance('SDW Flute', player), lambda state: state.has('Activated Flute', player))
- set_rule(world.get_entrance('EDW Flute', player), lambda state: state.has('Activated Flute', player))
- set_rule(world.get_entrance('DLHL Flute', player), lambda state: state.has('Activated Flute', player))
- set_rule(world.get_entrance('DD Flute', player), lambda state: state.has('Activated Flute', player))
- set_rule(world.get_entrance('EDDM Flute', player), lambda state: state.has('Activated Flute', player))
- set_rule(world.get_entrance('Dark Grassy Lawn Flute', player), lambda state: state.has('Activated Flute', player))
- set_rule(world.get_entrance('Hammer Peg Area Flute', player), lambda state: state.has('Activated Flute', player))
+ set_rule(multiworld.get_entrance('DDM Flute', player), lambda state: state.has('Activated Flute', player))
+ set_rule(multiworld.get_entrance('NEDW Flute', player), lambda state: state.has('Activated Flute', player))
+ set_rule(multiworld.get_entrance('WDW Flute', player), lambda state: state.has('Activated Flute', player))
+ set_rule(multiworld.get_entrance('SDW Flute', player), lambda state: state.has('Activated Flute', player))
+ set_rule(multiworld.get_entrance('EDW Flute', player), lambda state: state.has('Activated Flute', player))
+ set_rule(multiworld.get_entrance('DLHL Flute', player), lambda state: state.has('Activated Flute', player))
+ set_rule(multiworld.get_entrance('DD Flute', player), lambda state: state.has('Activated Flute', player))
+ set_rule(multiworld.get_entrance('EDDM Flute', player), lambda state: state.has('Activated Flute', player))
+ set_rule(multiworld.get_entrance('Dark Grassy Lawn Flute', player), lambda state: state.has('Activated Flute', player))
+ set_rule(multiworld.get_entrance('Hammer Peg Area Flute', player), lambda state: state.has('Activated Flute', player))
+
+ set_rule(multiworld.get_entrance('Inverted Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or multiworld.worlds[player].options.open_pyramid)
- set_rule(world.get_entrance('Inverted Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.worlds[player].options.open_pyramid)
+ if multiworld.worlds[player].options.swordless:
+ swordless_rules(multiworld, player)
- if world.worlds[player].options.swordless:
- swordless_rules(world, player)
-def no_glitches_rules(world, player):
+def no_glitches_rules(multiworld: MultiWorld, player: int):
""""""
- if world.worlds[player].options.mode == 'inverted':
- set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Moon Pearl', player) and (state.has('Flippers', player) or can_lift_rocks(state, player)))
- set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to
- set_rule(world.get_entrance('Lake Hylia Island Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to
- set_rule(world.get_entrance('Lake Hylia Warp', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to
- set_rule(world.get_entrance('Northeast Light World Warp', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to
- set_rule(world.get_entrance('Hobo Bridge', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player))
- set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Flippers', player))
- set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Flippers', player))
- set_rule(world.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: state.has('Flippers', player))
- set_rule(world.get_entrance('East Dark World Pier', player), lambda state: state.has('Flippers', player))
+ if multiworld.worlds[player].options.mode == 'inverted':
+ set_rule(multiworld.get_entrance('Zoras River', player), lambda state: state.has('Moon Pearl', player) and (state.has('Flippers', player) or can_lift_rocks(state, player)))
+ set_rule(multiworld.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to
+ set_rule(multiworld.get_entrance('Lake Hylia Island Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to
+ set_rule(multiworld.get_entrance('Lake Hylia Warp', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to
+ set_rule(multiworld.get_entrance('Northeast Light World Warp', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to
+ set_rule(multiworld.get_entrance('Hobo Bridge', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player))
+ set_rule(multiworld.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Flippers', player))
+ set_rule(multiworld.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Flippers', player))
+ set_rule(multiworld.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: state.has('Flippers', player))
+ set_rule(multiworld.get_entrance('East Dark World Pier', player), lambda state: state.has('Flippers', player))
else:
- set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Flippers', player) or can_lift_rocks(state, player))
- set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Flippers', player)) # can be fake flippered to
- set_rule(world.get_entrance('Hobo Bridge', player), lambda state: state.has('Flippers', player))
- set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player))
- set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player))
- set_rule(world.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player))
-
- add_rule(world.get_entrance('Ganons Tower (Double Switch Room)', player), lambda state: state.has('Hookshot', player))
- set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: False) # no glitches does not require block override
- add_conditional_lamps(world, player)
-
-def fake_flipper_rules(world, player):
- if world.worlds[player].options.mode == 'inverted':
- set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Lake Hylia Island Pier', player), lambda state: state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Lake Hylia Warp', player), lambda state: state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Northeast Light World Warp', player), lambda state: state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Hobo Bridge', player), lambda state: state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Flippers', player))
- set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: True)
- set_rule(world.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: True)
- set_rule(world.get_entrance('East Dark World Pier', player), lambda state: True)
+ set_rule(multiworld.get_entrance('Zoras River', player), lambda state: state.has('Flippers', player) or can_lift_rocks(state, player))
+ set_rule(multiworld.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Flippers', player)) # can be fake flippered to
+ set_rule(multiworld.get_entrance('Hobo Bridge', player), lambda state: state.has('Flippers', player))
+ set_rule(multiworld.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player))
+ set_rule(multiworld.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player))
+ set_rule(multiworld.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player))
+
+ add_rule(multiworld.get_entrance('Ganons Tower (Double Switch Room)', player), lambda state: state.has('Hookshot', player))
+ set_rule(multiworld.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: False) # no glitches does not require block override
+ add_conditional_lamps(multiworld, player)
+
+
+def fake_flipper_rules(multiworld: MultiWorld, player: int):
+ if multiworld.worlds[player].options.mode == 'inverted':
+ set_rule(multiworld.get_entrance('Zoras River', player), lambda state: state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Lake Hylia Island Pier', player), lambda state: state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Lake Hylia Warp', player), lambda state: state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Northeast Light World Warp', player), lambda state: state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Hobo Bridge', player), lambda state: state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Flippers', player))
+ set_rule(multiworld.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: True)
+ set_rule(multiworld.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: True)
+ set_rule(multiworld.get_entrance('East Dark World Pier', player), lambda state: True)
#qirn jump
- set_rule(world.get_entrance('East Dark World River Pier', player), lambda state: True)
+ set_rule(multiworld.get_entrance('East Dark World River Pier', player), lambda state: True)
else:
- set_rule(world.get_entrance('Zoras River', player), lambda state: True)
- set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: True)
- set_rule(world.get_entrance('Hobo Bridge', player), lambda state: True)
- set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player))
- set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Moon Pearl', player))
- set_rule(world.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Zoras River', player), lambda state: True)
+ set_rule(multiworld.get_entrance('Lake Hylia Central Island Pier', player), lambda state: True)
+ set_rule(multiworld.get_entrance('Hobo Bridge', player), lambda state: True)
+ set_rule(multiworld.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player))
+ set_rule(multiworld.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: state.has('Moon Pearl', player))
#qirn jump
- set_rule(world.get_entrance('East Dark World River Pier', player), lambda state: state.has('Moon Pearl', player))
+ set_rule(multiworld.get_entrance('East Dark World River Pier', player), lambda state: state.has('Moon Pearl', player))
-def bomb_jump_requirements(multiworld, player):
+def bomb_jump_requirements(multiworld: MultiWorld, player: int):
DMs_room_chests = ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right', 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right']
for location in DMs_room_chests:
add_rule(multiworld.get_location(location, player), lambda state: can_use_bombs(state, player), combine="or")
@@ -947,7 +953,7 @@ def bomb_jump_requirements(multiworld, player):
set_rule(multiworld.get_entrance('Skull Woods First Section Bomb Jump', player), lambda state: can_use_bombs(state, player))
-def forbid_bomb_jump_requirements(multiworld, player):
+def forbid_bomb_jump_requirements(multiworld: MultiWorld, player: int):
DMs_room_chests = ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right', 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right']
for location in DMs_room_chests:
add_rule(multiworld.get_location(location, player), lambda state: state.has('Hookshot', player))
@@ -972,14 +978,15 @@ def forbid_bomb_jump_requirements(multiworld, player):
'Turtle Rock',
'Dark Death Mountain Ledge (West)']
-def check_is_dark_world(region):
+
+def check_is_dark_world(region: Region):
for entrance in region.entrances:
if entrance.name in DW_Entrances:
return True
return False
-def add_conditional_lamps(multiworld, player):
+def add_conditional_lamps(multiworld: MultiWorld, player: int):
# Light cones in standard depend on which world we actually are in, not which one the location would normally be
# We add Lamp requirements only to those locations which lie in the dark world (or everything if open
local_world = multiworld.worlds[player]
@@ -1030,7 +1037,7 @@ def add_conditional_lamp(spot, region, spottype='Location', accessible_torch=Fal
add_lamp_requirement(multiworld, local_world.get_entrance("Throne Room"), player)
-def open_rules(world, player):
+def open_rules(multiworld: MultiWorld, player: int):
def basement_key_rule(state):
if location_item_name(state, 'Sewers - Key Rat Key Drop', player) == ("Small Key (Hyrule Castle)", player):
@@ -1038,96 +1045,95 @@ def basement_key_rule(state):
else:
return state._lttp_has_key("Small Key (Hyrule Castle)", player, 3)
- set_rule(world.get_location('Hyrule Castle - Boomerang Guard Key Drop', player),
+ set_rule(multiworld.get_location('Hyrule Castle - Boomerang Guard Key Drop', player),
lambda state: basement_key_rule(state) and can_kill_most_things(state, player, 2))
- set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player), lambda state: basement_key_rule(state) and can_kill_most_things(state, player, 1))
+ set_rule(multiworld.get_location('Hyrule Castle - Boomerang Chest', player), lambda state: basement_key_rule(state) and can_kill_most_things(state, player, 1))
- set_rule(world.get_location('Sewers - Key Rat Key Drop', player),
+ set_rule(multiworld.get_location('Sewers - Key Rat Key Drop', player),
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 3) and can_kill_most_things(state, player, 1))
- set_rule(world.get_location('Hyrule Castle - Big Key Drop', player),
+ set_rule(multiworld.get_location('Hyrule Castle - Big Key Drop', player),
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4) and can_kill_most_things(state, player, 1))
- set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
+ set_rule(multiworld.get_location('Hyrule Castle - Zelda\'s Chest', player),
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4)
and state.has('Big Key (Hyrule Castle)', player)
- and (world.worlds[player].options.enemy_health in ("easy", "default")
+ and (multiworld.worlds[player].options.enemy_health in ("easy", "default")
or can_kill_most_things(state, player, 1)))
-def swordless_rules(world, player):
- set_rule(world.get_entrance('Agahnim 1', player), lambda state: (state.has('Hammer', player) or state.has('Fire Rod', player) or can_shoot_arrows(state, player) or state.has('Cane of Somaria', player)) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2))
- set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player)) # no curtain
+def swordless_rules(multiworld: MultiWorld, player: int):
+ set_rule(multiworld.get_entrance('Agahnim 1', player), lambda state: (state.has('Hammer', player) or state.has('Fire Rod', player) or can_shoot_arrows(state, player) or state.has('Cane of Somaria', player)) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2))
+ set_rule(multiworld.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player)) # no curtain
- set_rule(world.get_location('Ice Palace - Jelly Key Drop', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player))
- set_rule(world.get_location('Ice Palace - Compass Chest', player), lambda state: (state.has('Fire Rod', player) or state.has('Bombos', player)) and state._lttp_has_key('Small Key (Ice Palace)', player))
- set_rule(world.get_entrance('Ice Palace (Second Section)', player), lambda state: (state.has('Fire Rod', player) or state.has('Bombos', player)) and state._lttp_has_key('Small Key (Ice Palace)', player))
+ set_rule(multiworld.get_location('Ice Palace - Jelly Key Drop', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player))
+ set_rule(multiworld.get_location('Ice Palace - Compass Chest', player), lambda state: (state.has('Fire Rod', player) or state.has('Bombos', player)) and state._lttp_has_key('Small Key (Ice Palace)', player))
+ set_rule(multiworld.get_entrance('Ice Palace (Second Section)', player), lambda state: (state.has('Fire Rod', player) or state.has('Bombos', player)) and state._lttp_has_key('Small Key (Ice Palace)', player))
- set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has('Hammer', player)) # need to damage ganon to get tiles to drop
+ set_rule(multiworld.get_entrance('Ganon Drop', player), lambda state: state.has('Hammer', player)) # need to damage ganon to get tiles to drop
- if world.worlds[player].options.mode != 'inverted':
- set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has('Hammer', player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle
- set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!)
- set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has('Moon Pearl', player) and has_misery_mire_medallion(state, player)) # sword not required to use medallion for opening in swordless (!)
+ if multiworld.worlds[player].options.mode != 'inverted':
+ set_rule(multiworld.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has('Hammer', player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle
+ set_rule(multiworld.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!)
+ set_rule(multiworld.get_entrance('Misery Mire', player), lambda state: state.has('Moon Pearl', player) and has_misery_mire_medallion(state, player)) # sword not required to use medallion for opening in swordless (!)
else:
# only need ddm access for aga tower in inverted
- set_rule(world.get_entrance('Turtle Rock', player), lambda state: has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!)
- set_rule(world.get_entrance('Misery Mire', player), lambda state: has_misery_mire_medallion(state, player)) # sword not required to use medallion for opening in swordless (!)
+ set_rule(multiworld.get_entrance('Turtle Rock', player), lambda state: has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!)
+ set_rule(multiworld.get_entrance('Misery Mire', player), lambda state: has_misery_mire_medallion(state, player)) # sword not required to use medallion for opening in swordless (!)
-def add_connection(parent_name, target_name, entrance_name, world, player):
- parent = world.get_region(parent_name, player)
- target = world.get_region(target_name, player)
+def add_connection(parent_name: str, target_name: str, entrance_name: str, multiworld: MultiWorld, player: int):
+ parent = multiworld.get_region(parent_name, player)
+ target = multiworld.get_region(target_name, player)
parent.connect(target, entrance_name)
+def standard_rules(multiworld: MultiWorld, player: int):
+ add_connection('Menu', 'Hyrule Castle Secret Entrance', 'Uncle S&Q', multiworld, player)
+ multiworld.get_entrance('Uncle S&Q', player).hide_path = True
+ set_rule(multiworld.get_entrance('Throne Room', player), lambda state: state.can_reach('Hyrule Castle - Zelda\'s Chest', 'Location', player))
+ set_rule(multiworld.get_entrance('Hyrule Castle Exit (East)', player), lambda state: state.can_reach('Sanctuary', 'Region', player))
+ set_rule(multiworld.get_entrance('Hyrule Castle Exit (West)', player), lambda state: state.can_reach('Sanctuary', 'Region', player))
+ set_rule(multiworld.get_entrance('Links House S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player))
+ set_rule(multiworld.get_entrance('Sanctuary S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player))
-def standard_rules(world, player):
- add_connection('Menu', 'Hyrule Castle Secret Entrance', 'Uncle S&Q', world, player)
- world.get_entrance('Uncle S&Q', player).hide_path = True
- set_rule(world.get_entrance('Throne Room', player), lambda state: state.can_reach('Hyrule Castle - Zelda\'s Chest', 'Location', player))
- set_rule(world.get_entrance('Hyrule Castle Exit (East)', player), lambda state: state.can_reach('Sanctuary', 'Region', player))
- set_rule(world.get_entrance('Hyrule Castle Exit (West)', player), lambda state: state.can_reach('Sanctuary', 'Region', player))
- set_rule(world.get_entrance('Links House S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player))
- set_rule(world.get_entrance('Sanctuary S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player))
-
- if world.worlds[player].options.small_key_shuffle != small_key_shuffle.option_universal:
- set_rule(world.get_location('Hyrule Castle - Boomerang Guard Key Drop', player),
+ if multiworld.worlds[player].options.small_key_shuffle != small_key_shuffle.option_universal:
+ set_rule(multiworld.get_location('Hyrule Castle - Boomerang Guard Key Drop', player),
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 1)
and can_kill_standard_start(state, player, 2))
- set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player),
+ set_rule(multiworld.get_location('Hyrule Castle - Boomerang Chest', player),
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 1)
and can_kill_standard_start(state, player, 1))
- set_rule(world.get_location('Hyrule Castle - Map Guard Key Drop', player),
+ set_rule(multiworld.get_location('Hyrule Castle - Map Guard Key Drop', player),
lambda state: can_kill_standard_start(state, player, 1))
- set_rule(world.get_location('Hyrule Castle - Big Key Drop', player),
+ set_rule(multiworld.get_location('Hyrule Castle - Big Key Drop', player),
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 2))
- set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
+ set_rule(multiworld.get_location('Hyrule Castle - Zelda\'s Chest', player),
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 2)
and state.has('Big Key (Hyrule Castle)', player)
- and (world.worlds[player].options.enemy_health in ("easy", "default")
+ and (multiworld.worlds[player].options.enemy_health in ("easy", "default")
or can_kill_standard_start(state, player, 1)))
- set_rule(world.get_location('Sewers - Key Rat Key Drop', player),
+ set_rule(multiworld.get_location('Sewers - Key Rat Key Drop', player),
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 3)
and can_kill_standard_start(state, player, 1))
else:
- set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
+ set_rule(multiworld.get_location('Hyrule Castle - Zelda\'s Chest', player),
lambda state: state.has('Big Key (Hyrule Castle)', player))
-def toss_junk_item(world, player):
+def toss_junk_item(multiworld: MultiWorld, player: int):
items = ['Rupees (20)', 'Bombs (3)', 'Arrows (10)', 'Rupees (5)', 'Rupee (1)', 'Bombs (10)',
'Single Arrow', 'Rupees (50)', 'Rupees (100)', 'Single Bomb', 'Bee', 'Bee Trap',
'Rupees (300)', 'Nothing']
for item in items:
- big20 = next((i for i in world.itempool if i.name == item and i.player == player), None)
+ big20 = next((i for i in multiworld.itempool if i.name == item and i.player == player), None)
if big20:
- world.itempool.remove(big20)
+ multiworld.itempool.remove(big20)
return
raise Exception("Unable to find a junk item to toss to make room for a TR small key")
-def set_trock_key_rules(multiworld, player):
+def set_trock_key_rules(multiworld: MultiWorld, player: int):
# First set all relevant locked doors to impassible.
for entrance in ['Turtle Rock Dark Room Staircase', 'Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock Entrance to Pokey Room', 'Turtle Rock (Pokey Room) (South)', 'Turtle Rock (Pokey Room) (North)', 'Turtle Rock Big Key Door']:
set_rule(multiworld.get_entrance(entrance, player), lambda state: False)
@@ -1228,9 +1234,9 @@ def tr_big_key_chest_keys_needed(state):
and state.can_reach(state.multiworld.get_region('Turtle Rock (Second Section)', player)))
-def set_big_bomb_rules(world, player):
+def set_big_bomb_rules(multiworld: MultiWorld, player: int):
# this is a mess
- bombshop_entrance = world.get_region('Big Bomb Shop', player).entrances[0]
+ bombshop_entrance = multiworld.get_region('Big Bomb Shop', player).entrances[0]
Normal_LW_entrances = ['Blinds Hideout',
'Bonk Fairy (Light)',
'Lake Hylia Fairy',
@@ -1345,7 +1351,7 @@ def set_big_bomb_rules(world, player):
'Desert Palace Entrance (South)',
'Checkerboard Cave']
- set_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.can_reach('East Dark World', 'Region', player) and state.can_reach('Big Bomb Shop', 'Region', player) and state.has('Crystal 5', player) and state.has('Crystal 6', player))
+ set_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.can_reach('East Dark World', 'Region', player) and state.can_reach('Big Bomb Shop', 'Region', player) and state.has('Crystal 5', player) and state.has('Crystal 6', player))
#crossing peg bridge starting from the southern dark world
def cross_peg_bridge(state):
@@ -1372,96 +1378,96 @@ def basic_routes(state):
#1. basic routes
#2. Can reach Eastern dark world some other way, mirror, get bomb, return to mirror spot, walk to pyramid: Needs mirror
# -> M or BR
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: basic_routes(state) or state.has('Magic Mirror', player))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: basic_routes(state) or state.has('Magic Mirror', player))
elif bombshop_entrance.name in LW_walkable_entrances:
#1. Mirror then basic routes
# -> M and BR
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and basic_routes(state))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and basic_routes(state))
elif bombshop_entrance.name in Northern_DW_entrances:
#1. Mirror and basic routes
#2. Go to south DW and then cross peg bridge: Need Mitts and hammer and moon pearl
# -> (Mitts and CPB) or (M and BR)
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (can_lift_heavy_rocks(state, player) and cross_peg_bridge(state)) or (state.has('Magic Mirror', player) and basic_routes(state)))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (can_lift_heavy_rocks(state, player) and cross_peg_bridge(state)) or (state.has('Magic Mirror', player) and basic_routes(state)))
elif bombshop_entrance.name == 'Bumper Cave (Bottom)':
#1. Mirror and Lift rock and basic_routes
#2. Mirror and Flute and basic routes (can make difference if accessed via insanity or w/ mirror from connector, and then via hyrule castle gate, because no gloves are needed in that case)
#3. Go to south DW and then cross peg bridge: Need Mitts and hammer and moon pearl
# -> (Mitts and CPB) or (((G or Flute) and M) and BR))
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (can_lift_heavy_rocks(state, player) and cross_peg_bridge(state)) or (((can_lift_rocks(state, player) or state.has('Flute', player)) and state.has('Magic Mirror', player)) and basic_routes(state)))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (can_lift_heavy_rocks(state, player) and cross_peg_bridge(state)) or (((can_lift_rocks(state, player) or state.has('Flute', player)) and state.has('Magic Mirror', player)) and basic_routes(state)))
elif bombshop_entrance.name in Southern_DW_entrances:
#1. Mirror and enter via gate: Need mirror and Aga1
#2. cross peg bridge: Need hammer and moon pearl
# -> CPB or (M and A)
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: cross_peg_bridge(state) or (state.has('Magic Mirror', player) and state.has('Beat Agahnim 1', player)))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: cross_peg_bridge(state) or (state.has('Magic Mirror', player) and state.has('Beat Agahnim 1', player)))
elif bombshop_entrance.name in Isolated_DW_entrances:
# 1. mirror then flute then basic routes
# -> M and Flute and BR
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and state.has('Activated Flute', player) and basic_routes(state))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and state.has('Activated Flute', player) and basic_routes(state))
elif bombshop_entrance.name in Isolated_LW_entrances:
# 1. flute then basic routes
# Prexisting mirror spot is not permitted, because mirror might have been needed to reach these isolated locations.
# -> Flute and BR
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and basic_routes(state))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and basic_routes(state))
elif bombshop_entrance.name in West_LW_DM_entrances:
# 1. flute then basic routes or mirror
# Prexisting mirror spot is permitted, because flute can be used to reach west DM directly.
# -> Flute and (M or BR)
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and (state.has('Magic Mirror', player) or basic_routes(state)))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and (state.has('Magic Mirror', player) or basic_routes(state)))
elif bombshop_entrance.name in East_LW_DM_entrances:
# 1. flute then basic routes or mirror and hookshot
# Prexisting mirror spot is permitted, because flute can be used to reach west DM directly and then east DM via Hookshot
# -> Flute and ((M and Hookshot) or BR)
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and ((state.has('Magic Mirror', player) and state.has('Hookshot', player)) or basic_routes(state)))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and ((state.has('Magic Mirror', player) and state.has('Hookshot', player)) or basic_routes(state)))
elif bombshop_entrance.name == 'Fairy Ascension Cave (Bottom)':
# Same as East_LW_DM_entrances except navigation without BR requires Mitts
# -> Flute and ((M and Hookshot and Mitts) or BR)
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and ((state.has('Magic Mirror', player) and state.has('Hookshot', player) and can_lift_heavy_rocks(state, player)) or basic_routes(state)))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and ((state.has('Magic Mirror', player) and state.has('Hookshot', player) and can_lift_heavy_rocks(state, player)) or basic_routes(state)))
elif bombshop_entrance.name in Castle_ledge_entrances:
# 1. mirror on pyramid to castle ledge, grab bomb, return through mirror spot: Needs mirror
# 2. flute then basic routes
# -> M or (Flute and BR)
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) or (state.has('Activated Flute', player) and basic_routes(state)))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) or (state.has('Activated Flute', player) and basic_routes(state)))
elif bombshop_entrance.name in Desert_mirrorable_ledge_entrances:
# Cases when you have mire access: Mirror to reach locations, return via mirror spot, move to center of desert, mirror anagin and:
# 1. Have mire access, Mirror to reach locations, return via mirror spot, move to center of desert, mirror again and then basic routes
# 2. flute then basic routes
# -> (Mire access and M) or Flute) and BR
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: ((state.can_reach('Dark Desert', 'Region', player) and state.has('Magic Mirror', player)) or state.has('Activated Flute', player)) and basic_routes(state))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: ((state.can_reach('Dark Desert', 'Region', player) and state.has('Magic Mirror', player)) or state.has('Activated Flute', player)) and basic_routes(state))
elif bombshop_entrance.name == 'Old Man Cave (West)':
# 1. Lift rock then basic_routes
# 2. flute then basic_routes
# -> (Flute or G) and BR
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or can_lift_rocks(state, player)) and basic_routes(state))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or can_lift_rocks(state, player)) and basic_routes(state))
elif bombshop_entrance.name == 'Graveyard Cave':
# 1. flute then basic routes
# 2. (has west dark world access) use existing mirror spot (required Pearl), mirror again off ledge
# -> (Flute or (M and P and West Dark World access) and BR
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or (state.can_reach('West Dark World', 'Region', player) and state.has('Moon Pearl', player) and state.has('Magic Mirror', player))) and basic_routes(state))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or (state.can_reach('West Dark World', 'Region', player) and state.has('Moon Pearl', player) and state.has('Magic Mirror', player))) and basic_routes(state))
elif bombshop_entrance.name in Mirror_from_SDW_entrances:
# 1. flute then basic routes
# 2. (has South dark world access) use existing mirror spot, mirror again off ledge
# -> (Flute or (M and South Dark World access) and BR
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or (state.can_reach('South Dark World', 'Region', player) and state.has('Magic Mirror', player))) and basic_routes(state))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or (state.can_reach('South Dark World', 'Region', player) and state.has('Magic Mirror', player))) and basic_routes(state))
elif bombshop_entrance.name == 'Dark World Potion Shop':
# 1. walk down by lifting rock: needs gloves and pearl`
# 2. walk down by hammering peg: needs hammer and pearl
# 3. mirror and basic routes
# -> (P and (H or Gloves)) or (M and BR)
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Moon Pearl', player) and (state.has('Hammer', player) or can_lift_rocks(state, player))) or (state.has('Magic Mirror', player) and basic_routes(state)))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Moon Pearl', player) and (state.has('Hammer', player) or can_lift_rocks(state, player))) or (state.has('Magic Mirror', player) and basic_routes(state)))
elif bombshop_entrance.name == 'Kings Grave':
# same as the Normal_LW_entrances case except that the pre-existing mirror is only possible if you have mitts
# (because otherwise mirror was used to reach the grave, so would cancel a pre-existing mirror spot)
# to account for insanity, must consider a way to escape without a cave for basic_routes
# -> (M and Mitts) or ((Mitts or Flute or (M and P and West Dark World access)) and BR)
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (can_lift_heavy_rocks(state, player) and state.has('Magic Mirror', player)) or ((can_lift_heavy_rocks(state, player) or state.has('Activated Flute', player) or (state.can_reach('West Dark World', 'Region', player) and state.has('Moon Pearl', player) and state.has('Magic Mirror', player))) and basic_routes(state)))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (can_lift_heavy_rocks(state, player) and state.has('Magic Mirror', player)) or ((can_lift_heavy_rocks(state, player) or state.has('Activated Flute', player) or (state.can_reach('West Dark World', 'Region', player) and state.has('Moon Pearl', player) and state.has('Magic Mirror', player))) and basic_routes(state)))
elif bombshop_entrance.name == 'Waterfall of Wishing':
# same as the Normal_LW_entrances case except in insanity it's possible you could be here without Flippers which
# means you need an escape route of either Flippers or Flute
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Flippers', player) or state.has('Activated Flute', player)) and (basic_routes(state) or state.has('Magic Mirror', player)))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Flippers', player) or state.has('Activated Flute', player)) and (basic_routes(state) or state.has('Magic Mirror', player)))
-def set_inverted_big_bomb_rules(world, player):
- bombshop_entrance = world.get_region('Inverted Big Bomb Shop', player).entrances[0]
+def set_inverted_big_bomb_rules(multiworld: MultiWorld, player: int):
+ bombshop_entrance = multiworld.get_region('Inverted Big Bomb Shop', player).entrances[0]
Normal_LW_entrances = ['Blinds Hideout',
'Bonk Fairy (Light)',
'Lake Hylia Fairy',
@@ -1579,7 +1585,7 @@ def set_inverted_big_bomb_rules(world, player):
'Spectacle Rock Cave',
'Spectacle Rock Cave (Bottom)']
- set_rule(world.get_entrance('Pyramid Fairy', player),
+ set_rule(multiworld.get_entrance('Pyramid Fairy', player),
lambda state: state.can_reach('East Dark World', 'Region', player) and state.can_reach('Inverted Big Bomb Shop', 'Region', player) and state.has('Crystal 5', player) and state.has('Crystal 6', player))
# Key for below abbreviations:
@@ -1593,64 +1599,64 @@ def set_inverted_big_bomb_rules(world, player):
pass
elif bombshop_entrance.name in Normal_LW_entrances:
# Just walk to the castle and mirror.
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player))
elif bombshop_entrance.name in Isolated_LW_entrances:
# For these entrances, you cannot walk to the castle/pyramid and thus must use Mirror and then Flute.
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and state.has('Magic Mirror', player))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and state.has('Magic Mirror', player))
elif bombshop_entrance.name in Northern_DW_entrances:
# You can just fly with the Flute, you can take a long walk with Mitts and Hammer,
# or you can leave a Mirror portal nearby and then walk to the castle to Mirror again.
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player)))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player)))
elif bombshop_entrance.name in Southern_DW_entrances:
# This is the same as north DW without the Mitts rock present.
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Hammer', player) or state.has('Activated Flute', player) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player)))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Hammer', player) or state.has('Activated Flute', player) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player)))
elif bombshop_entrance.name in Isolated_DW_entrances:
# There's just no way to escape these places with the bomb and no Flute.
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player))
elif bombshop_entrance.name in LW_walkable_entrances:
# You can fly with the flute, or leave a mirror portal and walk through the light world
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player)))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player)))
elif bombshop_entrance.name in LW_bush_entrances:
# These entrances are behind bushes in LW so you need either Pearl or the tools to solve NDW bomb shop locations.
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and (state.has('Activated Flute', player) or state.has('Moon Pearl', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player))))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and (state.has('Activated Flute', player) or state.has('Moon Pearl', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player))))
elif bombshop_entrance.name == 'Village of Outcasts Shop':
# This is mostly the same as NDW but the Mirror path requires the Pearl, or using the Hammer
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player) and (state.has('Moon Pearl', player) or state.has('Hammer', player))))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player) and (state.has('Moon Pearl', player) or state.has('Hammer', player))))
elif bombshop_entrance.name == 'Bumper Cave (Bottom)':
# This is mostly the same as NDW but the Mirror path requires being able to lift a rock.
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and can_lift_rocks(state, player) and state.can_reach('Light World', 'Region', player)))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and can_lift_rocks(state, player) and state.can_reach('Light World', 'Region', player)))
elif bombshop_entrance.name == 'Old Man Cave (West)':
# The three paths back are Mirror and DW walk, Mirror and Flute, or LW walk and then Mirror.
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and ((can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (can_lift_rocks(state, player) and state.has('Moon Pearl', player)) or state.has('Activated Flute', player)))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and ((can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (can_lift_rocks(state, player) and state.has('Moon Pearl', player)) or state.has('Activated Flute', player)))
elif bombshop_entrance.name == 'Dark World Potion Shop':
# You either need to Flute to 5 or cross the rock/hammer choice pass to the south.
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or state.has('Hammer', player) or can_lift_rocks(state, player))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or state.has('Hammer', player) or can_lift_rocks(state, player))
elif bombshop_entrance.name == 'Kings Grave':
# Either lift the rock and walk to the castle to Mirror or Mirror immediately and Flute.
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or can_lift_heavy_rocks(state, player)) and state.has('Magic Mirror', player))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or can_lift_heavy_rocks(state, player)) and state.has('Magic Mirror', player))
elif bombshop_entrance.name == 'Waterfall of Wishing':
# You absolutely must be able to swim to return it from here.
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player) and state.has('Magic Mirror', player))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player) and state.has('Magic Mirror', player))
elif bombshop_entrance.name == 'Ice Palace':
# You can swim to the dock or use the Flute to get off the island.
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Flippers', player) or state.has('Activated Flute', player))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Flippers', player) or state.has('Activated Flute', player))
elif bombshop_entrance.name == 'Capacity Upgrade':
# You must Mirror but then can use either Ice Palace return path.
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Flippers', player) or state.has('Activated Flute', player)) and state.has('Magic Mirror', player))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Flippers', player) or state.has('Activated Flute', player)) and state.has('Magic Mirror', player))
elif bombshop_entrance.name == 'Two Brothers House (West)':
# First you must Mirror. Then you can either Flute, cross the peg bridge, or use the Agah 1 portal to Mirror again.
- add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or state.has('Hammer', player) or state.has('Beat Agahnim 1', player)) and state.has('Magic Mirror', player))
+ add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or state.has('Hammer', player) or state.has('Beat Agahnim 1', player)) and state.has('Magic Mirror', player))
elif bombshop_entrance.name in LW_inaccessible_entrances:
# You can't get to the pyramid from these entrances without bomb duping.
raise Exception('No valid path to open Pyramid Fairy. (Could not route from %s)' % bombshop_entrance.name)
elif bombshop_entrance.name == 'Pyramid Fairy':
# Self locking. The shuffles don't put the bomb shop here, but doesn't lock anything important.
- set_rule(world.get_entrance('Pyramid Fairy', player), lambda state: False)
+ set_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: False)
else:
raise Exception('No logic found for routing from %s to the pyramid.' % bombshop_entrance.name)
-def set_bunny_rules(world: MultiWorld, player: int, inverted: bool):
+def set_bunny_rules(multiworld: MultiWorld, player: int, inverted: bool):
# regions for the exits of multi-entrance caves/drops that bunny cannot pass
# Note spiral cave and two brothers house are passable in superbunny state for glitch logic with extra requirements.
@@ -1690,7 +1696,7 @@ def is_link(region):
def get_rule_to_add(region, location = None, connecting_entrance = None):
# In OWG, a location can potentially be superbunny-mirror accessible or
# bunny revival accessible.
- if world.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
+ if multiworld.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
if region.name == 'Swamp Palace (Entrance)': # Need to 0hp revive - not in logic
return lambda state: state.has('Moon Pearl', player)
if region.name == 'Tower of Hera (Bottom)': # Need to hit the crystal switch
@@ -1730,7 +1736,7 @@ def get_rule_to_add(region, location = None, connecting_entrance = None):
seen.add(new_region)
if not is_link(new_region):
# For glitch rulesets, establish superbunny and revival rules.
- if world.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] and entrance.name not in OverworldGlitchRules.get_invalid_bunny_revival_dungeons():
+ if multiworld.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] and entrance.name not in OverworldGlitchRules.get_invalid_bunny_revival_dungeons():
if region.name in OverworldGlitchRules.get_sword_required_superbunny_mirror_regions():
possible_options.append(lambda state: path_to_access_rule(new_path, entrance) and state.has('Magic Mirror', player) and has_sword(state, player))
elif (region.name in OverworldGlitchRules.get_boots_required_superbunny_mirror_regions()
@@ -1753,29 +1759,29 @@ def get_rule_to_add(region, location = None, connecting_entrance = None):
return options_to_access_rule(possible_options)
# Add requirements for bunny-impassible caves if link is a bunny in them
- for region in (world.get_region(name, player) for name in bunny_impassable_caves):
+ for region in (multiworld.get_region(name, player) for name in bunny_impassable_caves):
if not is_bunny(region):
continue
rule = get_rule_to_add(region)
for region_exit in region.exits:
add_rule(region_exit, rule)
- paradox_shop = world.get_region('Light World Death Mountain Shop', player)
+ paradox_shop = multiworld.get_region('Light World Death Mountain Shop', player)
if is_bunny(paradox_shop):
add_rule(paradox_shop.entrances[0], get_rule_to_add(paradox_shop))
# Add requirements for all locations that are actually in the dark world, except those available to the bunny, including dungeon revival
- for entrance in world.get_entrances(player):
+ for entrance in multiworld.get_entrances(player):
if is_bunny(entrance.connected_region):
- if world.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] :
+ if multiworld.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] :
if entrance.connected_region.type == LTTPRegionType.Dungeon:
if entrance.parent_region.type != LTTPRegionType.Dungeon and entrance.connected_region.name in OverworldGlitchRules.get_invalid_bunny_revival_dungeons():
add_rule(entrance, get_rule_to_add(entrance.connected_region, None, entrance))
continue
if entrance.connected_region.name == 'Turtle Rock (Entrance)':
- add_rule(world.get_entrance('Turtle Rock Entrance Gap', player), get_rule_to_add(entrance.connected_region, None, entrance))
+ add_rule(multiworld.get_entrance('Turtle Rock Entrance Gap', player), get_rule_to_add(entrance.connected_region, None, entrance))
for location in entrance.connected_region.locations:
- if world.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] and entrance.name in OverworldGlitchRules.get_invalid_mirror_bunny_entrances():
+ if multiworld.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] and entrance.name in OverworldGlitchRules.get_invalid_mirror_bunny_entrances():
continue
if location.name in bunny_accessible_locations:
continue
diff --git a/worlds/alttp/Shops.py b/worlds/alttp/Shops.py
index 89e43a1a041a..182f25ddc288 100644
--- a/worlds/alttp/Shops.py
+++ b/worlds/alttp/Shops.py
@@ -7,7 +7,7 @@
from worlds.generic.Rules import add_rule
-from BaseClasses import CollectionState
+from BaseClasses import CollectionState, Item, MultiWorld
from .SubClasses import ALttPLocation
from .Items import item_name_groups
@@ -159,7 +159,7 @@ class UpgradeShop(Shop):
ShopType.TakeAny: TakeAny}
-def push_shop_inventories(multiworld):
+def push_shop_inventories(multiworld: MultiWorld):
all_shops = []
for world in multiworld.get_game_worlds(ALttPLocation.game):
all_shops.extend(world.shops)
@@ -183,7 +183,7 @@ def push_shop_inventories(multiworld):
world.pushed_shop_inventories.set()
-def create_shops(multiworld, player: int):
+def create_shops(multiworld: MultiWorld, player: int):
from .Options import RandomizeShopInventories
player_shop_table = shop_table.copy()
if multiworld.worlds[player].options.include_witch_hut:
@@ -306,7 +306,7 @@ class ShopData(NamedTuple):
}
-def set_up_shops(multiworld, player: int):
+def set_up_shops(multiworld: MultiWorld, player: int):
from .Options import small_key_shuffle
# TODO: move hard+ mode changes for shields here, utilizing the new shops
@@ -408,7 +408,7 @@ def set_up_shops(multiworld, player: int):
}
-def get_price_modifier(item) -> float:
+def get_price_modifier(item: Item) -> float:
if item.game == "A Link to the Past":
if any(x in item.name for x in
['Compass', 'Map', 'Single Bomb', 'Single Arrow', 'Piece of Heart']):
@@ -428,7 +428,7 @@ def get_price_modifier(item) -> float:
return 0.25
-def get_price(multiworld, item, player: int, price_type=None):
+def get_price(multiworld: MultiWorld, item: Item, player: int, price_type=None):
"""Converts a raw Rupee price into a special price type"""
from .Options import small_key_shuffle
if price_type:
diff --git a/worlds/alttp/UnderworldGlitchRules.py b/worlds/alttp/UnderworldGlitchRules.py
index 25511f320d4d..1b606a23af3b 100644
--- a/worlds/alttp/UnderworldGlitchRules.py
+++ b/worlds/alttp/UnderworldGlitchRules.py
@@ -1,3 +1,4 @@
+from BaseClasses import MultiWorld, CollectionState
from worlds.generic.Rules import set_rule, add_rule
from .StateHelpers import can_bomb_clip, has_sword, has_beam_sword, has_fire_source, can_melt_things, has_misery_mire_medallion
from .SubClasses import LTTPEntrance
@@ -5,27 +6,27 @@
# We actually need the logic to properly "mark" these regions as Light or Dark world.
# Therefore we need to make these connections during the normal link_entrances stage, rather than during set_rules.
-def underworld_glitch_connections(world, player):
- specrock = world.get_region('Spectacle Rock Cave (Bottom)', player)
- mire = world.get_region('Misery Mire (West)', player)
+def underworld_glitch_connections(multiworld: MultiWorld, player: int):
+ specrock = multiworld.get_region('Spectacle Rock Cave (Bottom)', player)
+ mire = multiworld.get_region('Misery Mire (West)', player)
kikiskip = specrock.create_exit('Kiki Skip')
mire_to_hera = mire.create_exit('Mire to Hera Clip')
mire_to_swamp = mire.create_exit('Hera to Swamp Clip')
- if world.worlds[player].fix_fake_world:
- kikiskip.connect(world.get_entrance('Palace of Darkness Exit', player).connected_region)
- mire_to_hera.connect(world.get_entrance('Tower of Hera Exit', player).connected_region)
- mire_to_swamp.connect(world.get_entrance('Swamp Palace Exit', player).connected_region)
+ if multiworld.worlds[player].fix_fake_world:
+ kikiskip.connect(multiworld.get_entrance('Palace of Darkness Exit', player).connected_region)
+ mire_to_hera.connect(multiworld.get_entrance('Tower of Hera Exit', player).connected_region)
+ mire_to_swamp.connect(multiworld.get_entrance('Swamp Palace Exit', player).connected_region)
else:
- kikiskip.connect(world.get_region('Palace of Darkness (Entrance)', player))
- mire_to_hera.connect(world.get_region('Tower of Hera (Bottom)', player))
- mire_to_swamp.connect(world.get_region('Swamp Palace (Entrance)', player))
+ kikiskip.connect(multiworld.get_region('Palace of Darkness (Entrance)', player))
+ mire_to_hera.connect(multiworld.get_region('Tower of Hera (Bottom)', player))
+ mire_to_swamp.connect(multiworld.get_region('Swamp Palace (Entrance)', player))
# For some entrances, we need to fake having pearl, because we're in fake DW/LW.
# This creates a copy of the input state that has Moon Pearl.
-def fake_pearl_state(state, player):
+def fake_pearl_state(state: CollectionState, player: int):
if state.has('Moon Pearl', player):
return state
fake_state = state.copy()
@@ -35,11 +36,11 @@ def fake_pearl_state(state, player):
# Sets the rules on where we can actually go using this clip.
# Behavior differs based on what type of ER shuffle we're playing.
-def dungeon_reentry_rules(world, player, clip: LTTPEntrance, dungeon_region: str, dungeon_exit: str):
- fix_dungeon_exits = world.worlds[player].fix_palaceofdarkness_exit
- fix_fake_worlds = world.worlds[player].fix_fake_world
+def dungeon_reentry_rules(multiworld: MultiWorld, player: int, clip: LTTPEntrance, dungeon_region: str, dungeon_exit: str):
+ fix_dungeon_exits = multiworld.worlds[player].fix_palaceofdarkness_exit
+ fix_fake_worlds = multiworld.worlds[player].fix_fake_world
- dungeon_entrance = [r for r in world.get_region(dungeon_region, player).entrances if r.name != clip.name][0]
+ dungeon_entrance = [r for r in multiworld.get_region(dungeon_region, player).entrances if r.name != clip.name][0]
if not fix_dungeon_exits: # vanilla, simple, restricted, dungeons_simple; should never have fake worlds fix
# Dungeons are only shuffled among themselves. We need to check SW, MM, and AT because they can't be reentered trivially.
if dungeon_entrance.name == 'Skull Woods Final Section':
@@ -49,64 +50,64 @@ def dungeon_reentry_rules(world, player, clip: LTTPEntrance, dungeon_region: str
elif dungeon_entrance.name == 'Agahnims Tower':
add_rule(clip, lambda state: state.has('Cape', player) or has_beam_sword(state, player) or state.has('Beat Agahnim 1', player)) # kill/bypass barrier
# Then we set a restriction on exiting the dungeon, so you can't leave unless you got in normally.
- add_rule(world.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state))
+ add_rule(multiworld.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state))
elif not fix_fake_worlds: # full, dungeons_full; fixed dungeon exits, but no fake worlds fix
# Entry requires the entrance's requirements plus a fake pearl, but you don't gain logical access to the surrounding region.
add_rule(clip, lambda state: dungeon_entrance.access_rule(fake_pearl_state(state, player)))
# exiting restriction
- add_rule(world.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state))
+ add_rule(multiworld.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state))
# Otherwise, the shuffle type is crossed, dungeons_crossed, or insanity; all of these do not need additional rules on where we can go,
# since the clip links directly to the exterior region.
-def underworld_glitches_rules(world, player):
+def underworld_glitches_rules(multiworld: MultiWorld, player: int):
# Ice Palace Entrance Clip
# This is the easiest one since it's a simple internal clip.
# Need to also add melting to freezor chest since it's otherwise assumed.
# Also can pick up the first jelly key from behind.
- add_rule(world.get_entrance('Ice Palace (Main)', player), lambda state: can_bomb_clip(state, world.get_region('Ice Palace (Entrance)', player), player), combine='or')
- add_rule(world.get_location('Ice Palace - Freezor Chest', player), lambda state: can_melt_things(state, player))
- add_rule(world.get_location('Ice Palace - Jelly Key Drop', player), lambda state: can_bomb_clip(state, world.get_region('Ice Palace (Entrance)', player), player), combine='or')
+ add_rule(multiworld.get_entrance('Ice Palace (Main)', player), lambda state: can_bomb_clip(state, multiworld.get_region('Ice Palace (Entrance)', player), player), combine='or')
+ add_rule(multiworld.get_location('Ice Palace - Freezor Chest', player), lambda state: can_melt_things(state, player))
+ add_rule(multiworld.get_location('Ice Palace - Jelly Key Drop', player), lambda state: can_bomb_clip(state, multiworld.get_region('Ice Palace (Entrance)', player), player), combine='or')
# Kiki Skip
- kikiskip = world.get_entrance('Kiki Skip', player)
+ kikiskip = multiworld.get_entrance('Kiki Skip', player)
set_rule(kikiskip, lambda state: can_bomb_clip(state, kikiskip.parent_region, player))
- dungeon_reentry_rules(world, player, kikiskip, 'Palace of Darkness (Entrance)', 'Palace of Darkness Exit')
+ dungeon_reentry_rules(multiworld, player, kikiskip, 'Palace of Darkness (Entrance)', 'Palace of Darkness Exit')
# Mire -> Hera -> Swamp
# Using mire keys on other dungeon doors
- mire = world.get_region('Misery Mire (West)', player)
+ mire = multiworld.get_region('Misery Mire (West)', player)
mire_clip = lambda state: state.can_reach('Misery Mire (West)', 'Region', player) and can_bomb_clip(state, mire, player) and has_fire_source(state, player)
- hera_clip = lambda state: state.can_reach('Tower of Hera (Top)', 'Region', player) and can_bomb_clip(state, world.get_region('Tower of Hera (Top)', player), player)
- add_rule(world.get_entrance('Tower of Hera Big Key Door', player), lambda state: mire_clip(state) and state.has('Big Key (Misery Mire)', player), combine='or')
- add_rule(world.get_entrance('Swamp Palace Small Key Door', player), lambda state: mire_clip(state), combine='or')
- add_rule(world.get_entrance('Swamp Palace (Center)', player), lambda state: mire_clip(state) or hera_clip(state), combine='or')
+ hera_clip = lambda state: state.can_reach('Tower of Hera (Top)', 'Region', player) and can_bomb_clip(state, multiworld.get_region('Tower of Hera (Top)', player), player)
+ add_rule(multiworld.get_entrance('Tower of Hera Big Key Door', player), lambda state: mire_clip(state) and state.has('Big Key (Misery Mire)', player), combine='or')
+ add_rule(multiworld.get_entrance('Swamp Palace Small Key Door', player), lambda state: mire_clip(state), combine='or')
+ add_rule(multiworld.get_entrance('Swamp Palace (Center)', player), lambda state: mire_clip(state) or hera_clip(state), combine='or')
# Build the rule for SP moat.
# We need to be able to s+q to old man, then go to either Mire or Hera at either Hera or GT.
# First we require a certain type of entrance shuffle, then build the rule from its pieces.
- if not world.worlds[player].swamp_patch_required:
- if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
+ if not multiworld.worlds[player].swamp_patch_required:
+ if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
rule_map = {
'Misery Mire (Entrance)': (lambda state: True),
'Tower of Hera (Bottom)': (lambda state: state.can_reach('Tower of Hera Big Key Door', 'Entrance', player))
}
- inverted = world.worlds[player].options.mode == 'inverted'
+ inverted = multiworld.worlds[player].options.mode == 'inverted'
hera_rule = lambda state: (state.has('Moon Pearl', player) or not inverted) and \
- rule_map.get(world.get_entrance('Tower of Hera', player).connected_region.name, lambda state: False)(state)
+ rule_map.get(multiworld.get_entrance('Tower of Hera', player).connected_region.name, lambda state: False)(state)
gt_rule = lambda state: (state.has('Moon Pearl', player) or inverted) and \
- rule_map.get(world.get_entrance(('Ganons Tower' if not inverted else 'Inverted Ganons Tower'), player).connected_region.name, lambda state: False)(state)
+ rule_map.get(multiworld.get_entrance(('Ganons Tower' if not inverted else 'Inverted Ganons Tower'), player).connected_region.name, lambda state: False)(state)
mirrorless_moat_rule = lambda state: state.can_reach('Old Man S&Q', 'Entrance', player) and mire_clip(state) and (hera_rule(state) or gt_rule(state))
- add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player) or mirrorless_moat_rule(state))
+ add_rule(multiworld.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player) or mirrorless_moat_rule(state))
else:
- add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player))
+ add_rule(multiworld.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player))
# Using the entrances for various ER types. Hera -> Swamp never matters because you can only logically traverse with the mire keys
- mire_to_hera = world.get_entrance('Mire to Hera Clip', player)
- mire_to_swamp = world.get_entrance('Hera to Swamp Clip', player)
+ mire_to_hera = multiworld.get_entrance('Mire to Hera Clip', player)
+ mire_to_swamp = multiworld.get_entrance('Hera to Swamp Clip', player)
set_rule(mire_to_hera, mire_clip)
set_rule(mire_to_swamp, lambda state: mire_clip(state) and state.has('Flippers', player))
- dungeon_reentry_rules(world, player, mire_to_hera, 'Tower of Hera (Bottom)', 'Tower of Hera Exit')
- dungeon_reentry_rules(world, player, mire_to_swamp, 'Swamp Palace (Entrance)', 'Swamp Palace Exit')
+ dungeon_reentry_rules(multiworld, player, mire_to_hera, 'Tower of Hera (Bottom)', 'Tower of Hera Exit')
+ dungeon_reentry_rules(multiworld, player, mire_to_swamp, 'Swamp Palace (Entrance)', 'Swamp Palace Exit')
From 699ca8adf6bcf8dbb64ae34607bf0204ecce9446 Mon Sep 17 00:00:00 2001
From: DrAwesome4333 <22409322+DrAwesome4333@users.noreply.github.com>
Date: Tue, 24 Feb 2026 18:47:54 -0700
Subject: [PATCH 04/84] WebHost: Add CORS headers to API Endpoints (#5777)
---
WebHostLib/api/__init__.py | 10 ++++++++++
WebHostLib/requirements.txt | 1 +
2 files changed, 11 insertions(+)
diff --git a/WebHostLib/api/__init__.py b/WebHostLib/api/__init__.py
index 54eb5c1de151..63914a06baef 100644
--- a/WebHostLib/api/__init__.py
+++ b/WebHostLib/api/__init__.py
@@ -2,10 +2,20 @@
from typing import List, Tuple
from flask import Blueprint
+from flask_cors import CORS
from ..models import Seed, Slot
api_endpoints = Blueprint('api', __name__, url_prefix="/api")
+cors = CORS(api_endpoints, resources={
+ r"/api/datapackage/*": {"origins": "*"},
+ r"/api/datapackage": {"origins": "*"},
+ r"/api/datapackage_checksum/*": {"origins": "*"},
+ r"/api/room_status/*": {"origins": "*"},
+ r"/api/tracker/*": {"origins": "*"},
+ r"/api/static_tracker/*": {"origins": "*"},
+ r"/api/slot_data_tracker/*": {"origins": "*"}
+ })
def get_players(seed: Seed) -> List[Tuple[str, str]]:
diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt
index c4267dc2846b..c9a923680add 100644
--- a/WebHostLib/requirements.txt
+++ b/WebHostLib/requirements.txt
@@ -6,6 +6,7 @@ waitress>=3.0.2
Flask-Caching>=2.3.0
Flask-Compress==1.18 # pkg_resources can't resolve the "backports.zstd" dependency of >1.18, breaking ModuleUpdate.py
Flask-Limiter>=3.12
+Flask-Cors>=6.0.2
bokeh>=3.6.3
markupsafe>=3.0.2
setproctitle>=1.3.5
From b30b2ecb07552521d61046844a62cdf54a9b0d53 Mon Sep 17 00:00:00 2001
From: Duck <31627079+duckboycool@users.noreply.github.com>
Date: Wed, 25 Feb 2026 12:52:34 -0700
Subject: [PATCH 05/84] Return new state man (Vi's note: I have chosen not to
change this title) (#5978)
---
docs/world api.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/docs/world api.md b/docs/world api.md
index 48e863fb2616..2df7b127447e 100644
--- a/docs/world api.md
+++ b/docs/world api.md
@@ -770,6 +770,7 @@ class MyGameState(LogicMixin):
new_state.mygame_defeatable_enemies = {
player: enemies.copy() for player, enemies in self.mygame_defeatable_enemies.items()
}
+ return new_state
```
After doing this, you can now access `state.mygame_defeatable_enemies[player]` from your access rules.
From eeb022fa0c69fa787cf6f771221d39232d646430 Mon Sep 17 00:00:00 2001
From: Aaron Wagener
Date: Wed, 25 Feb 2026 19:24:50 -0600
Subject: [PATCH 06/84] The Messenger: minor maintenance (#5965)
---
worlds/messenger/archipelago.json | 4 ++++
worlds/messenger/client_setup.py | 6 +++++-
worlds/messenger/rules.py | 4 ++--
3 files changed, 11 insertions(+), 3 deletions(-)
create mode 100644 worlds/messenger/archipelago.json
diff --git a/worlds/messenger/archipelago.json b/worlds/messenger/archipelago.json
new file mode 100644
index 000000000000..86aefc42a84e
--- /dev/null
+++ b/worlds/messenger/archipelago.json
@@ -0,0 +1,4 @@
+{
+ "game": "The Messenger",
+ "authors": ["alwaysintreble"]
+}
\ No newline at end of file
diff --git a/worlds/messenger/client_setup.py b/worlds/messenger/client_setup.py
index 3ef1df75cc13..02fd299a6c2f 100644
--- a/worlds/messenger/client_setup.py
+++ b/worlds/messenger/client_setup.py
@@ -28,6 +28,8 @@ def create_yes_no_popup(title: str, text: str, callback: Callable[[str], None])
def launch_game(*args) -> None:
"""Check the game installation, then launch it"""
+ prompt: ButtonsPrompt | None = None
+
def courier_installed() -> bool:
"""Check if Courier is installed"""
assembly_path = os.path.join(game_folder, "TheMessenger_Data", "Managed", "Assembly-CSharp.dll")
@@ -190,7 +192,7 @@ def after_mod_update_popup(answer: str) -> None:
def launch(answer: str | None = None) -> None:
"""Launch the game."""
- nonlocal args
+ nonlocal args, prompt
if prompt:
prompt.dismiss()
@@ -256,3 +258,5 @@ def launch(answer: str | None = None) -> None:
prompt = create_yes_no_popup("Launch Game",
"Mod installed and up to date. Would you like to launch the game now?",
launch)
+ else:
+ launch()
diff --git a/worlds/messenger/rules.py b/worlds/messenger/rules.py
index 2d5ee1b8a976..7f17232cfbf8 100644
--- a/worlds/messenger/rules.py
+++ b/worlds/messenger/rules.py
@@ -1,7 +1,7 @@
from typing import TYPE_CHECKING
-from BaseClasses import CollectionState
-from worlds.generic.Rules import CollectionRule, add_rule, allow_self_locking_items
+from BaseClasses import CollectionState, CollectionRule
+from worlds.generic.Rules import add_rule, allow_self_locking_items
from .constants import NOTES, PHOBEKINS
from .options import MessengerAccessibility
From 2db5435474f3d722b60f8354a6b9c98fb0705840 Mon Sep 17 00:00:00 2001
From: black-sliver <59490463+black-sliver@users.noreply.github.com>
Date: Thu, 26 Feb 2026 09:34:23 +0000
Subject: [PATCH 07/84] CI: upgrade InnoSetup to 6.7.0 (#5979)
---
.github/workflows/build.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 9ebe42307dd3..772a6c0be359 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -51,7 +51,7 @@ jobs:
run: |
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip
Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force
- choco install innosetup --version=6.2.2 --allow-downgrade
+ choco install innosetup --version=6.7.0 --allow-downgrade
- name: Build
run: |
python -m pip install --upgrade pip
From fcccbfca65d4c86180748f036f1e46ae31da6b93 Mon Sep 17 00:00:00 2001
From: black-sliver <59490463+black-sliver@users.noreply.github.com>
Date: Thu, 26 Feb 2026 18:31:39 +0000
Subject: [PATCH 08/84] MultiServer: don't keep multidata alive for race_mode
(#5980)
---
MultiServer.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/MultiServer.py b/MultiServer.py
index 52c80c55402a..d317e7b8fa5c 100644
--- a/MultiServer.py
+++ b/MultiServer.py
@@ -496,7 +496,8 @@ def _load(self, decoded_obj: MultiData, game_data_packages: typing.Dict[str, typ
self.read_data = {}
# there might be a better place to put this.
- self.read_data["race_mode"] = lambda: decoded_obj.get("race_mode", 0)
+ race_mode = decoded_obj.get("race_mode", 0)
+ self.read_data["race_mode"] = lambda: race_mode
mdata_ver = decoded_obj["minimum_versions"]["server"]
if mdata_ver > version_tuple:
raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver}, "
From ff5402c410b570592cff0b8a6d1bc70e1f841f24 Mon Sep 17 00:00:00 2001
From: Chris W
Date: Sat, 28 Feb 2026 23:56:28 +0100
Subject: [PATCH 09/84] Fix(undertale): prevent massive bounce msg spam for
position updates (#5990)
* fix(undertale): prevent massive bounce msg spam for position updates
* make sure player is removed on leaving / timing out
* do not check for tags: online, as bounce evaluation is or'd
---
UndertaleClient.py | 100 +++++++++++++++++++++++++++++++++++++--------
1 file changed, 82 insertions(+), 18 deletions(-)
diff --git a/UndertaleClient.py b/UndertaleClient.py
index 1c522fac924d..9dc1136b7751 100644
--- a/UndertaleClient.py
+++ b/UndertaleClient.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import os
import sys
+import time
import asyncio
import typing
import bsdiff4
@@ -15,6 +16,9 @@
gui_enabled, ClientCommandProcessor, logger, get_base_parser
from Utils import async_start
+# Heartbeat for position sharing via bounces, in seconds
+UNDERTALE_STATUS_INTERVAL = 30.0
+UNDERTALE_ONLINE_TIMEOUT = 60.0
class UndertaleCommandProcessor(ClientCommandProcessor):
def __init__(self, ctx):
@@ -109,6 +113,11 @@ def __init__(self, server_address, password):
self.completed_routes = {"pacifist": 0, "genocide": 0, "neutral": 0}
# self.save_game_folder: files go in this path to pass data between us and the actual game
self.save_game_folder = os.path.expandvars(r"%localappdata%/UNDERTALE")
+ self.last_sent_position: typing.Optional[tuple] = None
+ self.last_room: typing.Optional[str] = None
+ self.last_status_write: float = 0.0
+ self.other_undertale_status: dict[int, dict] = {}
+
def patch_game(self):
with open(Utils.user_path("Undertale", "data.win"), "rb") as f:
@@ -219,6 +228,9 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
await ctx.send_msgs([{"cmd": "SetNotify", "keys": [str(ctx.slot)+" RoutesDone neutral",
str(ctx.slot)+" RoutesDone pacifist",
str(ctx.slot)+" RoutesDone genocide"]}])
+ if any(info.game == "Undertale" and slot != ctx.slot
+ for slot, info in ctx.slot_info.items()):
+ ctx.set_notify("undertale_room_status")
if args["slot_data"]["only_flakes"]:
with open(os.path.join(ctx.save_game_folder, "GenoNoChest.flag"), "w") as f:
f.close()
@@ -263,6 +275,12 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
if str(ctx.slot)+" RoutesDone pacifist" in args["keys"]:
if args["keys"][str(ctx.slot) + " RoutesDone pacifist"] is not None:
ctx.completed_routes["pacifist"] = args["keys"][str(ctx.slot)+" RoutesDone pacifist"]
+ if "undertale_room_status" in args["keys"] and args["keys"]["undertale_room_status"]:
+ status = args["keys"]["undertale_room_status"]
+ ctx.other_undertale_status = {
+ int(key): val for key, val in status.items()
+ if int(key) != ctx.slot
+ }
elif cmd == "SetReply":
if args["value"] is not None:
if str(ctx.slot)+" RoutesDone pacifist" == args["key"]:
@@ -271,6 +289,11 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
ctx.completed_routes["genocide"] = args["value"]
elif str(ctx.slot)+" RoutesDone neutral" == args["key"]:
ctx.completed_routes["neutral"] = args["value"]
+ if args.get("key") == "undertale_room_status" and args.get("value"):
+ ctx.other_undertale_status = {
+ int(key): val for key, val in args["value"].items()
+ if int(key) != ctx.slot
+ }
elif cmd == "ReceivedItems":
start_index = args["index"]
@@ -368,9 +391,8 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
f.close()
elif cmd == "Bounced":
- tags = args.get("tags", [])
- if "Online" in tags:
- data = args.get("data", {})
+ data = args.get("data", {})
+ if "x" in data and "room" in data:
if data["player"] != ctx.slot and data["player"] is not None:
filename = f"FRISK" + str(data["player"]) + ".playerspot"
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
@@ -381,21 +403,63 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
async def multi_watcher(ctx: UndertaleContext):
while not ctx.exit_event.is_set():
- path = ctx.save_game_folder
- for root, dirs, files in os.walk(path):
- for file in files:
- if "spots.mine" in file and "Online" in ctx.tags:
- with open(os.path.join(root, file), "r") as mine:
- this_x = mine.readline()
- this_y = mine.readline()
- this_room = mine.readline()
- this_sprite = mine.readline()
- this_frame = mine.readline()
- mine.close()
- message = [{"cmd": "Bounce", "tags": ["Online"],
- "data": {"player": ctx.slot, "x": this_x, "y": this_y, "room": this_room,
- "spr": this_sprite, "frm": this_frame}}]
- await ctx.send_msgs(message)
+ if "Online" in ctx.tags and any(
+ info.game == "Undertale" and slot != ctx.slot
+ for slot, info in ctx.slot_info.items()):
+ now = time.time()
+ path = ctx.save_game_folder
+ for root, dirs, files in os.walk(path):
+ for file in files:
+ if "spots.mine" in file:
+ with open(os.path.join(root, file), "r") as mine:
+ this_x = mine.readline()
+ this_y = mine.readline()
+ this_room = mine.readline()
+ this_sprite = mine.readline()
+ this_frame = mine.readline()
+
+ if this_room != ctx.last_room or \
+ now - ctx.last_status_write >= UNDERTALE_STATUS_INTERVAL:
+ ctx.last_room = this_room
+ ctx.last_status_write = now
+ await ctx.send_msgs([{
+ "cmd": "Set",
+ "key": "undertale_room_status",
+ "default": {},
+ "want_reply": False,
+ "operations": [{"operation": "update",
+ "value": {str(ctx.slot): {"room": this_room,
+ "time": now}}}]
+ }])
+
+ # If player was visible but timed out (heartbeat) or left the room, remove them.
+ for slot, entry in ctx.other_undertale_status.items():
+ if entry.get("room") != this_room or \
+ now - entry.get("time", now) > UNDERTALE_ONLINE_TIMEOUT:
+ playerspot = os.path.join(ctx.save_game_folder,
+ f"FRISK{slot}.playerspot")
+ if os.path.exists(playerspot):
+ os.remove(playerspot)
+
+ current_position = (this_x, this_y, this_room, this_sprite, this_frame)
+ if current_position == ctx.last_sent_position:
+ continue
+
+ # Empty status dict = no data yet â send to bootstrap.
+ online_in_room = any(
+ entry.get("room") == this_room and
+ now - entry.get("time", now) <= UNDERTALE_ONLINE_TIMEOUT
+ for entry in ctx.other_undertale_status.values()
+ )
+ if ctx.other_undertale_status and not online_in_room:
+ continue
+
+ message = [{"cmd": "Bounce", "games": ["Undertale"],
+ "data": {"player": ctx.slot, "x": this_x, "y": this_y,
+ "room": this_room, "spr": this_sprite,
+ "frm": this_frame}}]
+ await ctx.send_msgs(message)
+ ctx.last_sent_position = current_position
await asyncio.sleep(0.1)
From 61d5120f66b87a683c8099d4a7b9cf4a0b62a583 Mon Sep 17 00:00:00 2001
From: Doug Hoskisson
Date: Sat, 28 Feb 2026 15:14:33 -0800
Subject: [PATCH 10/84] Core: use typing_extensions `deprecated` (#5989)
---
Utils.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/Utils.py b/Utils.py
index bf46d0832d27..c18298559ac7 100644
--- a/Utils.py
+++ b/Utils.py
@@ -23,6 +23,7 @@
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union, TypeGuard
from yaml import load, load_all, dump
from pathspec import PathSpec, GitIgnoreSpec
+from typing_extensions import deprecated
try:
from yaml import CLoader as UnsafeLoader, CSafeLoader as SafeLoader, CDumper as Dumper
@@ -315,6 +316,7 @@ def get_public_ipv6() -> str:
return ip
+@deprecated("Utils.get_options() is deprecated. Use the settings API instead.")
def get_options() -> Settings:
deprecate("Utils.get_options() is deprecated. Use the settings API instead.")
return get_settings()
@@ -1003,6 +1005,7 @@ def async_start(co: Coroutine[None, None, typing.Any], name: Optional[str] = Non
def deprecate(message: str, add_stacklevels: int = 0):
+ """also use typing_extensions.deprecated wherever you use this"""
if __debug__:
raise Exception(message)
warnings.warn(message, stacklevel=2 + add_stacklevels)
@@ -1067,6 +1070,7 @@ def _noop() -> None:
multiprocessing.freeze_support = multiprocessing.spawn.freeze_support = _freeze_support if is_frozen() else _noop
+@deprecated("Use multiprocessing.freeze_support() instead")
def freeze_support() -> None:
"""This now only calls multiprocessing.freeze_support since we are patching freeze_support on module load."""
import multiprocessing
From e49ba2ff6fc849952889e344927a35e8db1fd7b0 Mon Sep 17 00:00:00 2001
From: Duck <31627079+duckboycool@users.noreply.github.com>
Date: Sat, 28 Feb 2026 17:30:26 -0700
Subject: [PATCH 11/84] Undertale: Use check_locations helper to avoid
redundant sends (#5993)
---
UndertaleClient.py | 16 ++++++----------
1 file changed, 6 insertions(+), 10 deletions(-)
diff --git a/UndertaleClient.py b/UndertaleClient.py
index 9dc1136b7751..b0efce206ae5 100644
--- a/UndertaleClient.py
+++ b/UndertaleClient.py
@@ -300,11 +300,8 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
if start_index == 0:
ctx.items_received = []
elif start_index != len(ctx.items_received):
- sync_msg = [{"cmd": "Sync"}]
- if ctx.locations_checked:
- sync_msg.append({"cmd": "LocationChecks",
- "locations": list(ctx.locations_checked)})
- await ctx.send_msgs(sync_msg)
+ await ctx.check_locations(ctx.locations_checked)
+ await ctx.send_msgs([{"cmd": "Sync"}])
if start_index == len(ctx.items_received):
counter = -1
placedWeapon = 0
@@ -473,10 +470,9 @@ async def game_watcher(ctx: UndertaleContext):
for file in files:
if ".item" in file:
os.remove(os.path.join(root, file))
- sync_msg = [{"cmd": "Sync"}]
- if ctx.locations_checked:
- sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
- await ctx.send_msgs(sync_msg)
+ await ctx.check_locations(ctx.locations_checked)
+ await ctx.send_msgs([{"cmd": "Sync"}])
+
ctx.syncing = False
if ctx.got_deathlink:
ctx.got_deathlink = False
@@ -511,7 +507,7 @@ async def game_watcher(ctx: UndertaleContext):
for l in lines:
sending = sending+[(int(l.rstrip('\n')))+12000]
finally:
- await ctx.send_msgs([{"cmd": "LocationChecks", "locations": sending}])
+ await ctx.check_locations(sending)
if "victory" in file and str(ctx.route) in file:
victory = True
if ".playerspot" in file and "Online" not in ctx.tags:
From 922c7fe86aa580984584b8541e33b1a9a3962aca Mon Sep 17 00:00:00 2001
From: Fabian Dill
Date: Sun, 1 Mar 2026 17:51:56 +0100
Subject: [PATCH 12/84] Core: allow async def functions as commands (#5859)
---
MultiServer.py | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/MultiServer.py b/MultiServer.py
index d317e7b8fa5c..ed50c98db617 100644
--- a/MultiServer.py
+++ b/MultiServer.py
@@ -1302,6 +1302,13 @@ def __new__(cls, name, bases, attrs):
commands.update(base.commands)
commands.update({command_name[5:]: method for command_name, method in attrs.items() if
command_name.startswith("_cmd_")})
+ for command_name, method in commands.items():
+ # wrap async def functions so they run on default asyncio loop
+ if inspect.iscoroutinefunction(method):
+ def _wrapper(self, *args, _method=method, **kwargs):
+ return async_start(_method(self, *args, **kwargs))
+ functools.update_wrapper(_wrapper, method)
+ commands[command_name] = _wrapper
return super(CommandMeta, cls).__new__(cls, name, bases, attrs)
From a3e8f69909b3e77344033be88b2f87b7b54b939a Mon Sep 17 00:00:00 2001
From: Fabian Dill
Date: Sun, 1 Mar 2026 17:53:41 +0100
Subject: [PATCH 13/84] Core: introduce finalize_multiworld and pre_output
stages (#5700)
Co-authored-by: Ishigh1
Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
---
Main.py | 3 +++
test/bases.py | 1 +
test/general/test_ids.py | 1 +
test/general/test_implemented.py | 3 +++
test/general/test_items.py | 1 +
test/multiworld/test_multiworlds.py | 2 ++
worlds/AutoWorld.py | 17 +++++++++++++++++
7 files changed, 28 insertions(+)
diff --git a/Main.py b/Main.py
index 47a28813fce4..924def653b27 100644
--- a/Main.py
+++ b/Main.py
@@ -207,6 +207,9 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None)
else:
logger.info("Progression balancing skipped.")
+ AutoWorld.call_all(multiworld, "finalize_multiworld")
+ AutoWorld.call_all(multiworld, "pre_output")
+
# we're about to output using multithreading, so we're removing the global random state to prevent accidental use
multiworld.random.passthrough = False
diff --git a/test/bases.py b/test/bases.py
index dd93ca6452dd..19b19bea67ec 100644
--- a/test/bases.py
+++ b/test/bases.py
@@ -248,6 +248,7 @@ def fulfills_accessibility() -> bool:
with self.subTest("Game", game=self.game, seed=self.multiworld.seed):
distribute_items_restrictive(self.multiworld)
call_all(self.multiworld, "post_fill")
+ call_all(self.multiworld, "finalize_multiworld")
self.assertTrue(fulfills_accessibility(), "Collected all locations, but can't beat the game.")
placed_items = [loc.item for loc in self.multiworld.get_locations() if loc.item and loc.item.code]
self.assertLessEqual(len(self.multiworld.itempool), len(placed_items),
diff --git a/test/general/test_ids.py b/test/general/test_ids.py
index ad8aad11d15c..08b4d0aa494d 100644
--- a/test/general/test_ids.py
+++ b/test/general/test_ids.py
@@ -88,6 +88,7 @@ def test_postgen_datapackage(self):
multiworld = setup_solo_multiworld(world_type)
distribute_items_restrictive(multiworld)
call_all(multiworld, "post_fill")
+ call_all(multiworld, "finalize_multiworld")
datapackage = world_type.get_data_package_data()
for item_group, item_names in datapackage["item_name_groups"].items():
self.assertIsInstance(item_group, str,
diff --git a/test/general/test_implemented.py b/test/general/test_implemented.py
index de432e369099..add6e5321e7f 100644
--- a/test/general/test_implemented.py
+++ b/test/general/test_implemented.py
@@ -46,6 +46,8 @@ def test_slot_data(self):
with self.subTest(game=game_name, seed=multiworld.seed):
distribute_items_restrictive(multiworld)
call_all(multiworld, "post_fill")
+ call_all(multiworld, "finalize_multiworld")
+ call_all(multiworld, "pre_output")
for key, data in multiworld.worlds[1].fill_slot_data().items():
self.assertIsInstance(key, str, "keys in slot data must be a string")
convert_to_base_types(data) # only put base data types into slot data
@@ -93,6 +95,7 @@ def test_explicit_indirect_conditions_spheres(self):
with self.subTest(game=game_name, seed=multiworld.seed):
distribute_items_restrictive(multiworld)
call_all(multiworld, "post_fill")
+ call_all(multiworld, "finalize_multiworld")
# Note: `multiworld.get_spheres()` iterates a set of locations, so the order that locations are checked
# is nondeterministic and may vary between runs with the same seed.
diff --git a/test/general/test_items.py b/test/general/test_items.py
index 694e0db406ca..9c300cf94ed6 100644
--- a/test/general/test_items.py
+++ b/test/general/test_items.py
@@ -123,6 +123,7 @@ def setup_link_multiworld(world: Type[World], link_replace: bool) -> None:
call_all(multiworld, "pre_fill")
distribute_items_restrictive(multiworld)
call_all(multiworld, "post_fill")
+ call_all(multiworld, "finalize_multiworld")
self.assertTrue(multiworld.can_beat_game(CollectionState(multiworld)), f"seed = {multiworld.seed}")
for game_name, world_type in AutoWorldRegister.world_types.items():
diff --git a/test/multiworld/test_multiworlds.py b/test/multiworld/test_multiworlds.py
index 203af8b63a8b..d22013b4e078 100644
--- a/test/multiworld/test_multiworlds.py
+++ b/test/multiworld/test_multiworlds.py
@@ -61,6 +61,7 @@ def test_fills(self) -> None:
with self.subTest("filling multiworld", seed=self.multiworld.seed):
distribute_items_restrictive(self.multiworld)
call_all(self.multiworld, "post_fill")
+ call_all(self.multiworld, "finalize_multiworld")
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
@@ -78,4 +79,5 @@ def test_two_player_single_game_fills(self) -> None:
with self.subTest("filling multiworld", games=world_type.game, seed=self.multiworld.seed):
distribute_items_restrictive(self.multiworld)
call_all(self.multiworld, "post_fill")
+ call_all(self.multiworld, "finalize_multiworld")
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py
index 327e386c05f1..327746f1ce52 100644
--- a/worlds/AutoWorld.py
+++ b/worlds/AutoWorld.py
@@ -430,6 +430,23 @@ def post_fill(self) -> None:
This happens before progression balancing, so the items may not be in their final locations yet.
"""
+ def finalize_multiworld(self) -> None:
+ """
+ Optional Method that is called after fill and progression balancing.
+ This is the last stage of generation where worlds may change logically relevant data,
+ such as item placements and connections. To not break assumptions,
+ only ever increase accessibility, never decrease it.
+ """
+ pass
+
+ def pre_output(self):
+ """
+ Optional method that is called before output generation.
+ Items and connections are not meant to be moved anymore,
+ anything that would affect logical spheres is forbidden at this point.
+ """
+ pass
+
def generate_output(self, output_directory: str) -> None:
"""
This method gets called from a threadpool, do not use multiworld.random here.
From f26313367e55ebf7555dfc97aad40682dfc71d13 Mon Sep 17 00:00:00 2001
From: black-sliver <59490463+black-sliver@users.noreply.github.com>
Date: Tue, 3 Mar 2026 23:02:12 +0000
Subject: [PATCH 14/84] MultiServer: graceful shutdown for ctrl+c and sigterm
(#5996)
---
MultiServer.py | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/MultiServer.py b/MultiServer.py
index ed50c98db617..0ba5adaf4025 100644
--- a/MultiServer.py
+++ b/MultiServer.py
@@ -21,6 +21,7 @@
import typing
import weakref
import zlib
+from signal import SIGINT, SIGTERM
import ModuleUpdate
@@ -2571,6 +2572,8 @@ async def console(ctx: Context):
input_text = await queue.get()
queue.task_done()
ctx.commandprocessor(input_text)
+ except asyncio.exceptions.CancelledError:
+ ctx.logger.info("ConsoleTask cancelled")
except:
import traceback
traceback.print_exc()
@@ -2737,6 +2740,15 @@ async def main(args: argparse.Namespace):
console_task = asyncio.create_task(console(ctx))
if ctx.auto_shutdown:
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, [console_task]))
+
+ def stop():
+ for remove_signal in [SIGINT, SIGTERM]:
+ asyncio.get_event_loop().remove_signal_handler(remove_signal)
+ ctx.commandprocessor._cmd_exit()
+
+ for signal in [SIGINT, SIGTERM]:
+ asyncio.get_event_loop().add_signal_handler(signal, stop)
+
await ctx.exit_event.wait()
console_task.cancel()
if ctx.shutdown_task:
From b372b02273436874dd7c5ce145387f96339eb5ed Mon Sep 17 00:00:00 2001
From: Silvris <58583688+Silvris@users.noreply.github.com>
Date: Wed, 4 Mar 2026 12:47:30 -0600
Subject: [PATCH 15/84] OptionCreator: 0.6.6 reported issues (#5949)
---
OptionsCreator.py | 58 ++++++++++++++++++++++++++++-------------------
1 file changed, 35 insertions(+), 23 deletions(-)
diff --git a/OptionsCreator.py b/OptionsCreator.py
index 4e56b680b879..94ca8ba7acc1 100644
--- a/OptionsCreator.py
+++ b/OptionsCreator.py
@@ -29,7 +29,7 @@
import re
from urllib.parse import urlparse
from worlds.AutoWorld import AutoWorldRegister, World
-from Options import (Option, Toggle, TextChoice, Choice, FreeText, NamedRange, Range, OptionSet, OptionList, Removed,
+from Options import (Option, Toggle, TextChoice, Choice, FreeText, NamedRange, Range, OptionSet, OptionList,
OptionCounter, Visibility)
@@ -318,26 +318,28 @@ def export_options(self, button: Widget) -> None:
else:
self.show_result_snack("Name cannot be longer than 16 characters.")
- def create_range(self, option: typing.Type[Range], name: str):
+ def create_range(self, option: typing.Type[Range], name: str, bind=True):
def update_text(range_box: VisualRange):
self.options[name] = int(range_box.slider.value)
range_box.tag.text = str(int(range_box.slider.value))
return
box = VisualRange(option=option, name=name)
- box.slider.bind(on_touch_move=lambda _, _1: update_text(box))
+ if bind:
+ box.slider.bind(value=lambda _, _1: update_text(box))
self.options[name] = option.default
return box
def create_named_range(self, option: typing.Type[NamedRange], name: str):
def set_to_custom(range_box: VisualNamedRange):
- if (not self.options[name] == range_box.range.slider.value) \
- and (not self.options[name] in option.special_range_names or
- range_box.range.slider.value != option.special_range_names[self.options[name]]):
- # we should validate the touch here,
- # but this is much cheaper
+ range_box.range.tag.text = str(int(range_box.range.slider.value))
+ if range_box.range.slider.value in option.special_range_names.values():
+ value = next(key for key, val in option.special_range_names.items()
+ if val == range_box.range.slider.value)
+ self.options[name] = value
+ set_button_text(box.choice, value.title())
+ else:
self.options[name] = int(range_box.range.slider.value)
- range_box.range.tag.text = str(int(range_box.range.slider.value))
set_button_text(range_box.choice, "Custom")
def set_button_text(button: MDButton, text: str):
@@ -346,7 +348,7 @@ def set_button_text(button: MDButton, text: str):
def set_value(text: str, range_box: VisualNamedRange):
range_box.range.slider.value = min(max(option.special_range_names[text.lower()], option.range_start),
option.range_end)
- range_box.range.tag.text = str(int(range_box.range.slider.value))
+ range_box.range.tag.text = str(option.special_range_names[text.lower()])
set_button_text(range_box.choice, text)
self.options[name] = text.lower()
range_box.range.slider.dropdown.dismiss()
@@ -355,13 +357,18 @@ def open_dropdown(button):
# for some reason this fixes an issue causing some to not open
box.range.slider.dropdown.open()
- box = VisualNamedRange(option=option, name=name, range_widget=self.create_range(option, name))
- if option.default in option.special_range_names:
+ box = VisualNamedRange(option=option, name=name, range_widget=self.create_range(option, name, bind=False))
+ default: int | str = option.default
+ if default in option.special_range_names:
# value can get mismatched in this case
- box.range.slider.value = min(max(option.special_range_names[option.default], option.range_start),
+ box.range.slider.value = min(max(option.special_range_names[default], option.range_start),
option.range_end)
box.range.tag.text = str(int(box.range.slider.value))
- box.range.slider.bind(on_touch_move=lambda _, _2: set_to_custom(box))
+ elif default in option.special_range_names.values():
+ # better visual
+ default = next(key for key, val in option.special_range_names.items() if val == option.default)
+ set_button_text(box.choice, default.title())
+ box.range.slider.bind(value=lambda _, _2: set_to_custom(box))
items = [
{
"text": choice.title(),
@@ -371,7 +378,7 @@ def open_dropdown(button):
]
box.range.slider.dropdown = MDDropdownMenu(caller=box.choice, items=items)
box.choice.bind(on_release=open_dropdown)
- self.options[name] = option.default
+ self.options[name] = default
return box
def create_free_text(self, option: typing.Type[FreeText] | typing.Type[TextChoice], name: str):
@@ -447,8 +454,12 @@ def create_popup(self, option: typing.Type[OptionList] | typing.Type[OptionSet]
valid_keys = sorted(option.valid_keys)
if option.verify_item_name:
valid_keys += list(world.item_name_to_id.keys())
+ if option.convert_name_groups:
+ valid_keys += list(world.item_name_groups.keys())
if option.verify_location_name:
valid_keys += list(world.location_name_to_id.keys())
+ if option.convert_name_groups:
+ valid_keys += list(world.location_name_groups.keys())
if not issubclass(option, OptionCounter):
def apply_changes(button):
@@ -470,14 +481,6 @@ def apply_changes(button):
dialog.scrollbox.layout.spacing = dp(5)
dialog.scrollbox.layout.padding = [0, dp(5), 0, 0]
- if name not in self.options:
- # convert from non-mutable to mutable
- # We use list syntax even for sets, set behavior is enforced through GUI
- if issubclass(option, OptionCounter):
- self.options[name] = deepcopy(option.default)
- else:
- self.options[name] = sorted(option.default)
-
if issubclass(option, OptionCounter):
for value in sorted(self.options[name]):
dialog.add_set_item(value, self.options[name].get(value, None))
@@ -491,6 +494,15 @@ def apply_changes(button):
def create_option_set_list_counter(self, option: typing.Type[OptionList] | typing.Type[OptionSet] |
typing.Type[OptionCounter], name: str, world: typing.Type[World]):
main_button = MDButton(MDButtonText(text="Edit"), on_release=lambda x: self.create_popup(option, name, world))
+
+ if name not in self.options:
+ # convert from non-mutable to mutable
+ # We use list syntax even for sets, set behavior is enforced through GUI
+ if issubclass(option, OptionCounter):
+ self.options[name] = deepcopy(option.default)
+ else:
+ self.options[name] = sorted(option.default)
+
return main_button
def create_option(self, option: typing.Type[Option], name: str, world: typing.Type[World]) -> Widget:
From 3ecd856e29a2ac58faf5335f765eee9445ddb93f Mon Sep 17 00:00:00 2001
From: Silvris <58583688+Silvris@users.noreply.github.com>
Date: Thu, 5 Mar 2026 18:41:48 -0600
Subject: [PATCH 16/84] MultiServer: fix Windows compatibility (#6010)
---
MultiServer.py | 21 ++++++++++++++++-----
1 file changed, 16 insertions(+), 5 deletions(-)
diff --git a/MultiServer.py b/MultiServer.py
index 0ba5adaf4025..ed14b6506ff5 100644
--- a/MultiServer.py
+++ b/MultiServer.py
@@ -21,7 +21,7 @@
import typing
import weakref
import zlib
-from signal import SIGINT, SIGTERM
+from signal import SIGINT, SIGTERM, signal
import ModuleUpdate
@@ -2742,12 +2742,23 @@ async def main(args: argparse.Namespace):
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, [console_task]))
def stop():
- for remove_signal in [SIGINT, SIGTERM]:
- asyncio.get_event_loop().remove_signal_handler(remove_signal)
+ try:
+ for remove_signal in [SIGINT, SIGTERM]:
+ asyncio.get_event_loop().remove_signal_handler(remove_signal)
+ except NotImplementedError:
+ pass
ctx.commandprocessor._cmd_exit()
- for signal in [SIGINT, SIGTERM]:
- asyncio.get_event_loop().add_signal_handler(signal, stop)
+ def shutdown(signum, frame):
+ stop()
+
+ try:
+ for sig in [SIGINT, SIGTERM]:
+ asyncio.get_event_loop().add_signal_handler(sig, stop)
+ except NotImplementedError:
+ # add_signal_handler is only implemented for UNIX platforms
+ for sig in [SIGINT, SIGTERM]:
+ signal(sig, shutdown)
await ctx.exit_event.wait()
console_task.cancel()
From b53f9d377302c26ae0a53bea6860f419dbd75dec Mon Sep 17 00:00:00 2001
From: qwint
Date: Sat, 7 Mar 2026 17:51:42 -0600
Subject: [PATCH 17/84] Docs: Better document state.locations_checked (#6018)
---
BaseClasses.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/BaseClasses.py b/BaseClasses.py
index ccb8e0677f12..69b900212c50 100644
--- a/BaseClasses.py
+++ b/BaseClasses.py
@@ -727,6 +727,7 @@ class CollectionState():
advancements: Set[Location]
path: Dict[Union[Region, Entrance], PathValue]
locations_checked: Set[Location]
+ """Internal cache for Advancement Locations already checked by this CollectionState. Not for use in logic."""
stale: Dict[int, bool]
allow_partial_entrances: bool
additional_init_functions: List[Callable[[CollectionState, MultiWorld], None]] = []
From 366fd3712a2af9c42a02d0917417a828480d1187 Mon Sep 17 00:00:00 2001
From: Suyooo
Date: Sun, 8 Mar 2026 21:28:44 +0100
Subject: [PATCH 18/84] MM2: Fix /request command help (#5805)
---
worlds/mm2/client.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/worlds/mm2/client.py b/worlds/mm2/client.py
index 96c477757dcd..725023c82066 100644
--- a/worlds/mm2/client.py
+++ b/worlds/mm2/client.py
@@ -140,8 +140,8 @@ def cmd_pool(self: "BizHawkClientCommandProcessor") -> None:
def cmd_request(self: "BizHawkClientCommandProcessor", amount: str, target: str) -> None:
- from worlds._bizhawk.context import BizHawkClientContext
"""Request a refill from EnergyLink."""
+ from worlds._bizhawk.context import BizHawkClientContext
if self.ctx.game != "Mega Man 2":
logger.warning("This command can only be used when playing Mega Man 2.")
return
From 9f298598107894f3ab495d0722f1262066dc787c Mon Sep 17 00:00:00 2001
From: jamesbrq
Date: Sun, 8 Mar 2026 16:30:18 -0400
Subject: [PATCH 19/84] MLSS: Fix client auto-connect bug + Client cleanup
(#5895)
---
worlds/mlss/Client.py | 17 +++++------------
1 file changed, 5 insertions(+), 12 deletions(-)
diff --git a/worlds/mlss/Client.py b/worlds/mlss/Client.py
index 7944442b2a36..0334cc5abba9 100644
--- a/worlds/mlss/Client.py
+++ b/worlds/mlss/Client.py
@@ -1,11 +1,11 @@
-from typing import TYPE_CHECKING, Optional, Set, List, Dict
+import asyncio
import struct
+from typing import TYPE_CHECKING, Optional, Set, List, Dict
from NetUtils import ClientStatus
from .Locations import roomCount, nonBlock, beanstones, roomException, shop, badge, pants, eReward
from .Items import items_by_id
-import asyncio
import worlds._bizhawk as bizhawk
from worlds._bizhawk.client import BizHawkClient
@@ -41,8 +41,6 @@ def __init__(self) -> None:
self.local_events = []
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
- from CommonClient import logger
-
try:
# Check ROM name/patch version
rom_name_bytes = await bizhawk.read(ctx.bizhawk_ctx, [(0xA0, 14, "ROM")])
@@ -72,20 +70,15 @@ async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
async def set_auth(self, ctx: "BizHawkClientContext") -> None:
ctx.auth = self.player_name
- def on_package(self, ctx, cmd, args) -> None:
- if cmd == "RoomInfo":
- ctx.seed_name = args["seed_name"]
-
async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
from CommonClient import logger
-
try:
- if ctx.seed_name is None:
+ if ctx.server_seed_name is None:
return
if not self.seed_verify:
- seed = await bizhawk.read(ctx.bizhawk_ctx, [(0xDF00A0, len(ctx.seed_name), "ROM")])
+ seed = await bizhawk.read(ctx.bizhawk_ctx, [(0xDF00A0, len(ctx.server_seed_name), "ROM")])
seed = seed[0].decode("UTF-8")
- if seed not in ctx.seed_name:
+ if seed not in ctx.server_seed_name:
logger.info(
"ERROR: The ROM you loaded is for a different game of AP. "
"Please make sure the host has sent you the correct patch file, "
From 9efcba5323e938c58224db85840b1a0120fd0d99 Mon Sep 17 00:00:00 2001
From: Rosalie <61372066+Rosalie-A@users.noreply.github.com>
Date: Sun, 8 Mar 2026 16:31:04 -0400
Subject: [PATCH 20/84] FF1: Added manifest (#5911)
---
worlds/ff1/archipelago.json | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 worlds/ff1/archipelago.json
diff --git a/worlds/ff1/archipelago.json b/worlds/ff1/archipelago.json
new file mode 100644
index 000000000000..e3cf746a3b32
--- /dev/null
+++ b/worlds/ff1/archipelago.json
@@ -0,0 +1,5 @@
+{
+ "game": "Final Fantasy",
+ "world_version": "1.0.0",
+ "authors": ["Rosalie"]
+}
\ No newline at end of file
From fc2cb3c961cda23f62484e37d1f742556b3592d6 Mon Sep 17 00:00:00 2001
From: StripesOO7 <54711792+StripesOO7@users.noreply.github.com>
Date: Sun, 8 Mar 2026 21:31:48 +0100
Subject: [PATCH 21/84] OoT: change setup-guides to have 2.10 be the minimum
version recommended (#5799)
---
worlds/oot/docs/setup_de.md | 7 +------
worlds/oot/docs/setup_en.md | 7 +------
worlds/oot/docs/setup_fr.md | 6 +-----
3 files changed, 3 insertions(+), 17 deletions(-)
diff --git a/worlds/oot/docs/setup_de.md b/worlds/oot/docs/setup_de.md
index f257ddc015f4..d456f6907939 100644
--- a/worlds/oot/docs/setup_de.md
+++ b/worlds/oot/docs/setup_de.md
@@ -7,7 +7,7 @@ Da wir BizHawk benutzen, gilt diese Anleitung nur fÃŧr Windows und Linux.
## BenÃļtigte Software
- BizHawk: [BizHawk VerÃļffentlichungen von TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
- - Version 2.3.1 und später werden unterstÃŧtzt. Version 2.10 ist empfohlen.
+ - Version 2.10 und neuer werden unterstÃŧtzt. Version 2.10 ist empfohlen.
- Detailierte Installtionsanweisungen fÃŧr BizHawk kÃļnnen Ãŧber den obrigen Link gefunden werden.
- Windows-Benutzer mÃŧssen die Prerequisiten installiert haben. Diese kÃļnnen ebenfalls Ãŧber
den obrigen Link gefunden werden.
@@ -19,11 +19,6 @@ Da wir BizHawk benutzen, gilt diese Anleitung nur fÃŧr Windows und Linux.
Sobald Bizhawk einmal installiert wurde, Ãļffne **EmuHawk** und ändere die folgenen Einsteluungen:
-- (⤠2.8) Gehe zu `Config > Customize`. Wechlse zu dem `Advanced`-Reiter, wechsle dann den `Lua Core` von "NLua+KopiLua" zu
- `"Lua+LuaInterface"`. Starte danach EmuHawk neu. Dies ist zwingend notwendig, damit die Lua-Scripts, mit denen man sich mit dem Client verbindet, ordnungsgemäà funktionieren.
- **ANMERKUNG: Selbst wenn "Lua+LuaInterface" bereits ausgewählt ist, wechsle zwischen den beiden Optionen umher und**
- **wähle es erneut aus. Neue Installationen oder Versionen von EmuHawk neigen dazu "Lua+LuaInterface" als die**
- **Standard-Option anzuzeigen, aber laden dennoch "NLua+KopiLua", bis dieser Schritt getan ist.**
- Unter `Config > Customize > Advanced`, gehe sicher dass der Haken bei `AutoSaveRAM` ausgeählt ist, und klicke dann
den 5s-Knopf. Dies verringert die Wahrscheinlichkeit den Speicherfrotschritt zu verlieren, sollte der Emulator mal
abstÃŧrzen.
diff --git a/worlds/oot/docs/setup_en.md b/worlds/oot/docs/setup_en.md
index 31b7137bd8b1..a09752f0d840 100644
--- a/worlds/oot/docs/setup_en.md
+++ b/worlds/oot/docs/setup_en.md
@@ -7,7 +7,7 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst
## Required Software
- BizHawk: [BizHawk Releases from TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
- - Version 2.3.1 and later are supported. Version 2.10 is recommended for stability.
+ - Version 2.10 and later are supported. Version 2.10 is recommended for stability.
- Detailed installation instructions for BizHawk can be found at the above link.
- Windows users must run the prereq installer first, which can also be found at the above link.
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases).
@@ -17,11 +17,6 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst
Once BizHawk has been installed, open EmuHawk and change the following settings:
-- (⤠2.8) Go to Config > Customize. Switch to the Advanced tab, then switch the Lua Core from "NLua+KopiLua" to
- "Lua+LuaInterface". Then restart EmuHawk. This is required for the Lua script to function correctly.
- **NOTE: Even if "Lua+LuaInterface" is already selected, toggle between the two options and reselect it. Fresh installs**
- **of newer versions of EmuHawk have a tendency to show "Lua+LuaInterface" as the default selected option but still load**
- **"NLua+KopiLua" until this step is done.**
- Under Config > Customize > Advanced, make sure the box for AutoSaveRAM is checked, and click the 5s button.
This reduces the possibility of losing save data in emulator crashes.
- Under Config > Customize, check the "Run in background" and "Accept background input" boxes. This will allow you to
diff --git a/worlds/oot/docs/setup_fr.md b/worlds/oot/docs/setup_fr.md
index eb2e97384afa..8df50e05bd36 100644
--- a/worlds/oot/docs/setup_fr.md
+++ b/worlds/oot/docs/setup_fr.md
@@ -7,7 +7,7 @@ Comme nous utilisons BizHawk, ce guide s'applique uniquement aux systèmes Windo
## Logiciel requis
- BizHawk : [Sorties BizHawk de TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
- - Les versions 2.3.1 et ultÊrieures sont prises en charge. La version 2.10 est recommandÊe pour des raisons de stabilitÊ.
+ - Les versions 2.10 et ultÊrieures sont prises en charge. La version 2.10 est recommandÊe pour des raisons de stabilitÊ.
- Des instructions d'installation dÊtaillÊes pour BizHawk peuvent ÃĒtre trouvÊes sur le lien ci-dessus.
- Les utilisateurs Windows doivent d'abord exÊcuter le programme d'installation des prÊrequis, qui peut Êgalement ÃĒtre trouvÊ sur le lien ci-dessus.
- Le client Archipelago intÊgrÊ, qui peut ÃĒtre installÊ [ici](https://github.com/ArchipelagoMW/Archipelago/releases)
@@ -18,10 +18,6 @@ Comme nous utilisons BizHawk, ce guide s'applique uniquement aux systèmes Windo
Une fois BizHawk installÊ, ouvrez EmuHawk et modifiez les paramètres suivants :
-- (⤠2,8) Allez dans Config > Personnaliser. Passez à l'onglet AvancÊ, puis faites passer le Lua Core de "NLua+KopiLua" Ã
- "Lua+LuaInterface". Puis redÊmarrez EmuHawk. Ceci est nÊcessaire pour que le script Lua fonctionne correctement.
- **REMARQUE : MÃĒme si ÂĢ Lua+LuaInterface Âģ est dÊjà sÊlectionnÊ, basculez entre les deux options et resÊlectionnez-la. Nouvelles installations**
- **des versions plus rÊcentes d'EmuHawk ont tendance à afficher "Lua+LuaInterface" comme option sÊlectionnÊe par dÊfaut mais ce pendant refait l'Êpate juste au dessus par prÊcautions**
- Sous Config > Personnaliser > AvancÊ, assurez-vous que la case AutoSaveRAM est cochÊe et cliquez sur le bouton 5s.
Cela rÊduit la possibilitÊ de perdre des donnÊes de sauvegarde en cas de crash de l'Êmulateur.
- Sous Config > Personnaliser, cochez les cases ÂĢ ExÊcuter en arrière-plan Âģ et ÂĢ Accepter la saisie en arrière-plan Âģ. Cela vous permettra continuez à jouer en arrière-plan, mÃĒme si une autre fenÃĒtre est sÊlectionnÊe.
From a8ac828241066d4079e20123d043d417764bdce0 Mon Sep 17 00:00:00 2001
From: Bryce Wilson
Date: Sun, 8 Mar 2026 13:32:40 -0700
Subject: [PATCH 22/84] Pokemon Emerald: Fix rare fuzzer errors (#5914)
---
worlds/pokemon_emerald/opponents.py | 2 +-
worlds/pokemon_emerald/pokemon.py | 8 ++++----
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/worlds/pokemon_emerald/opponents.py b/worlds/pokemon_emerald/opponents.py
index 966d19205447..00eceff9a71e 100644
--- a/worlds/pokemon_emerald/opponents.py
+++ b/worlds/pokemon_emerald/opponents.py
@@ -63,7 +63,7 @@ def randomize_opponent_parties(world: "PokemonEmeraldWorld") -> None:
if len(merged_blacklist) < NUM_REAL_SPECIES:
break
else:
- raise RuntimeError("This should never happen")
+ merged_blacklist: Set[int] = set()
candidates = [
species
diff --git a/worlds/pokemon_emerald/pokemon.py b/worlds/pokemon_emerald/pokemon.py
index b39f8c2abf6e..8f799ce61154 100644
--- a/worlds/pokemon_emerald/pokemon.py
+++ b/worlds/pokemon_emerald/pokemon.py
@@ -245,7 +245,7 @@ def _rename_wild_events(world: "PokemonEmeraldWorld", map_data: MapData, new_slo
for r, sc in _encounter_subcategory_ranges[encounter_type].items()
if i in r
)
- subcategory_species = []
+ subcategory_species: list[int] = []
for k in subcategory_range:
if new_slots[k] not in subcategory_species:
subcategory_species.append(new_slots[k])
@@ -278,7 +278,7 @@ def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None:
RandomizeWildPokemon.option_match_base_stats_and_type,
}
- already_placed = set()
+ already_placed: set[int] = set()
num_placeable_species = NUM_REAL_SPECIES - len(world.blacklisted_wilds)
priority_species = [data.constants["SPECIES_WAILORD"], data.constants["SPECIES_RELICANTH"]]
@@ -349,7 +349,7 @@ def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None:
if len(merged_blacklist) < NUM_REAL_SPECIES:
break
else:
- raise RuntimeError("This should never happen")
+ merged_blacklist = set()
candidates = [
species
@@ -365,7 +365,7 @@ def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None:
species_old_to_new_map[species_id] = new_species_id
if world.options.dexsanity and encounter_type != EncounterType.ROCK_SMASH \
- and map_name not in OUT_OF_LOGIC_MAPS:
+ and map_name not in OUT_OF_LOGIC_MAPS and new_species_id not in world.blacklisted_wilds:
already_placed.add(new_species_id)
# Actually create the new list of slots and encounter table
From b38548f89ba7bf73e970ed9da85290e200ab6ffa Mon Sep 17 00:00:00 2001
From: GodlFire <46984098+GodlFire@users.noreply.github.com>
Date: Sun, 8 Mar 2026 14:33:16 -0600
Subject: [PATCH 23/84] Shivers: Adds Manifest File (#5918)
---
worlds/shivers/archipelago.json | 6 ++++++
1 file changed, 6 insertions(+)
create mode 100644 worlds/shivers/archipelago.json
diff --git a/worlds/shivers/archipelago.json b/worlds/shivers/archipelago.json
new file mode 100644
index 000000000000..d9838d3841c1
--- /dev/null
+++ b/worlds/shivers/archipelago.json
@@ -0,0 +1,6 @@
+{
+ "game": "Shivers",
+ "minimum_ap_version": "0.6.0",
+ "world_version": "2.7.5",
+ "authors": ["GodlFire", "Cynbel_Terreus"]
+}
\ No newline at end of file
From 53956b7d4d21356bd6bd6fbea9addca3231fd448 Mon Sep 17 00:00:00 2001
From: josephwhite <22449090+josephwhite@users.noreply.github.com>
Date: Sun, 8 Mar 2026 16:34:19 -0400
Subject: [PATCH 24/84] OOT: UTC deprecation warning fix (#5983)
---
worlds/oot/Patches.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/worlds/oot/Patches.py b/worlds/oot/Patches.py
index db7be3d4ddc5..98bb44efc80b 100644
--- a/worlds/oot/Patches.py
+++ b/worlds/oot/Patches.py
@@ -272,7 +272,7 @@ def truncstr(txt, size):
world_str = ""
rom.write_bytes(rom.sym('WORLD_STRING_TXT'), makebytes(world_str, 12))
- time_str = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M") + " UTC"
+ time_str = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d %H:%M") + " UTC"
rom.write_bytes(rom.sym('TIME_STRING_TXT'), makebytes(time_str, 25))
rom.write_byte(rom.sym('CFG_SHOW_SETTING_INFO'), 0x01)
From 99601ccebc39dafedc1f6009f90ad543ddf46d4a Mon Sep 17 00:00:00 2001
From: LeonarthCG <33758848+LeonarthCG@users.noreply.github.com>
Date: Sun, 8 Mar 2026 21:34:51 +0100
Subject: [PATCH 25/84] Saving Princess: add manifest (#6008)
---
worlds/saving_princess/archipelago.json | 6 ++++++
1 file changed, 6 insertions(+)
create mode 100644 worlds/saving_princess/archipelago.json
diff --git a/worlds/saving_princess/archipelago.json b/worlds/saving_princess/archipelago.json
new file mode 100644
index 000000000000..d4f65dabff0f
--- /dev/null
+++ b/worlds/saving_princess/archipelago.json
@@ -0,0 +1,6 @@
+{
+ "game": "Saving Princess",
+ "authors": [ "LeonarthCG" ],
+ "minimum_ap_version": "0.6.6",
+ "world_version": "1.0.0"
+}
From 4bb6cac7c4533aa19d336823a1bd6833592ff311 Mon Sep 17 00:00:00 2001
From: Star Rauchenberger
Date: Sun, 8 Mar 2026 16:35:12 -0400
Subject: [PATCH 26/84] Lingo: Add archipelago.json (#6017)
---
worlds/lingo/archipelago.json | 6 ++++++
1 file changed, 6 insertions(+)
create mode 100644 worlds/lingo/archipelago.json
diff --git a/worlds/lingo/archipelago.json b/worlds/lingo/archipelago.json
new file mode 100644
index 000000000000..d065ab5c0b32
--- /dev/null
+++ b/worlds/lingo/archipelago.json
@@ -0,0 +1,6 @@
+{
+ "game": "Lingo",
+ "authors": ["hatkirby"],
+ "minimum_ap_version": "0.6.3",
+ "world_version": "5.0.0"
+}
From 5b99118dda7025d78aa457c4098d58aef69e6378 Mon Sep 17 00:00:00 2001
From: Silvris <58583688+Silvris@users.noreply.github.com>
Date: Sun, 8 Mar 2026 15:42:06 -0500
Subject: [PATCH 27/84] Mega Man 3: Implement new game (#5237)
---
README.md | 1 +
docs/CODEOWNERS | 3 +
inno_setup.iss | 5 +
worlds/mm3/.apignore | 1 +
worlds/mm3/__init__.py | 275 +++++++++
worlds/mm3/archipelago.json | 6 +
worlds/mm3/client.py | 783 ++++++++++++++++++++++++++
worlds/mm3/color.py | 331 +++++++++++
worlds/mm3/data/mm3_basepatch.bsdiff4 | Bin 0 -> 1235 bytes
worlds/mm3/docs/en_Mega Man 3.md | 131 +++++
worlds/mm3/docs/setup_en.md | 53 ++
worlds/mm3/items.py | 80 +++
worlds/mm3/locations.py | 312 ++++++++++
worlds/mm3/names.py | 221 ++++++++
worlds/mm3/options.py | 164 ++++++
worlds/mm3/rom.py | 374 ++++++++++++
worlds/mm3/rules.py | 388 +++++++++++++
worlds/mm3/src/__init__.py | 0
worlds/mm3/src/mm3_basepatch.asm | 781 +++++++++++++++++++++++++
worlds/mm3/src/patch_mm3base.py | 8 +
worlds/mm3/test/__init__.py | 0
worlds/mm3/test/bases.py | 5 +
worlds/mm3/test/test_weakness.py | 105 ++++
worlds/mm3/text.py | 63 +++
24 files changed, 4090 insertions(+)
create mode 100644 worlds/mm3/.apignore
create mode 100644 worlds/mm3/__init__.py
create mode 100644 worlds/mm3/archipelago.json
create mode 100644 worlds/mm3/client.py
create mode 100644 worlds/mm3/color.py
create mode 100644 worlds/mm3/data/mm3_basepatch.bsdiff4
create mode 100644 worlds/mm3/docs/en_Mega Man 3.md
create mode 100644 worlds/mm3/docs/setup_en.md
create mode 100644 worlds/mm3/items.py
create mode 100644 worlds/mm3/locations.py
create mode 100644 worlds/mm3/names.py
create mode 100644 worlds/mm3/options.py
create mode 100644 worlds/mm3/rom.py
create mode 100644 worlds/mm3/rules.py
create mode 100644 worlds/mm3/src/__init__.py
create mode 100644 worlds/mm3/src/mm3_basepatch.asm
create mode 100644 worlds/mm3/src/patch_mm3base.py
create mode 100644 worlds/mm3/test/__init__.py
create mode 100644 worlds/mm3/test/bases.py
create mode 100644 worlds/mm3/test/test_weakness.py
create mode 100644 worlds/mm3/text.py
diff --git a/README.md b/README.md
index efa18bc1ef07..7a0c663db067 100644
--- a/README.md
+++ b/README.md
@@ -85,6 +85,7 @@ Currently, the following games are supported:
* APQuest
* Satisfactory
* EarthBound
+* Mega Man 3
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS
index 0e368386c54f..46afd3045692 100644
--- a/docs/CODEOWNERS
+++ b/docs/CODEOWNERS
@@ -134,6 +134,9 @@
# Mega Man 2
/worlds/mm2/ @Silvris
+# Mega Man 3
+/worlds/mm3/ @Silvris
+
# MegaMan Battle Network 3
/worlds/mmbn3/ @digiholic
diff --git a/inno_setup.iss b/inno_setup.iss
index c396224c5631..999070ad07a0 100644
--- a/inno_setup.iss
+++ b/inno_setup.iss
@@ -213,6 +213,11 @@ Root: HKCR; Subkey: "{#MyAppName}ebpatch"; ValueData: "Archi
Root: HKCR; Subkey: "{#MyAppName}ebpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}ebpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: ".apmm3"; ValueData: "{#MyAppName}mm3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}mm3patch"; ValueData: "Archipelago Mega Man 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}mm3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
+Root: HKCR; Subkey: "{#MyAppName}mm3patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
+
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: "";
diff --git a/worlds/mm3/.apignore b/worlds/mm3/.apignore
new file mode 100644
index 000000000000..4ae3da2695a9
--- /dev/null
+++ b/worlds/mm3/.apignore
@@ -0,0 +1 @@
+/src/*
diff --git a/worlds/mm3/__init__.py b/worlds/mm3/__init__.py
new file mode 100644
index 000000000000..5b349bc9c364
--- /dev/null
+++ b/worlds/mm3/__init__.py
@@ -0,0 +1,275 @@
+import hashlib
+import logging
+from copy import deepcopy
+from typing import Any, Sequence, ClassVar
+
+from BaseClasses import Tutorial, ItemClassification, MultiWorld, Item, Location
+from worlds.AutoWorld import World, WebWorld
+from .names import (gamma, gemini_man_stage, needle_man_stage, hard_man_stage, magnet_man_stage, top_man_stage,
+ snake_man_stage, spark_man_stage, shadow_man_stage, rush_marine, rush_jet, rush_coil)
+from .items import (item_table, item_names, MM3Item, filler_item_weights, robot_master_weapon_table,
+ stage_access_table, rush_item_table, lookup_item_to_id)
+from .locations import (MM3Location, mm3_regions, MM3Region, lookup_location_to_id,
+ location_groups)
+from .rom import patch_rom, MM3ProcedurePatch, MM3LCHASH, MM3VCHASH, PROTEUSHASH, MM3NESHASH
+from .options import MM3Options, Consumables
+from .client import MegaMan3Client
+from .rules import set_rules, weapon_damage, robot_masters, weapons_to_name, minimum_weakness_requirement
+import os
+import threading
+import base64
+import settings
+logger = logging.getLogger("Mega Man 3")
+
+
+class MM3Settings(settings.Group):
+ class RomFile(settings.UserFilePath):
+ """File name of the MM3 EN rom"""
+ description = "Mega Man 3 ROM File"
+ copy_to: str | None = "Mega Man 3 (USA).nes"
+ md5s = [MM3NESHASH, MM3LCHASH, PROTEUSHASH, MM3VCHASH]
+
+ def browse(self: settings.T,
+ filetypes: Sequence[tuple[str, Sequence[str]]] | None = None,
+ **kwargs: Any) -> settings.T | None:
+ if not filetypes:
+ file_types = [("NES", [".nes"]), ("Program", [".exe"])] # LC1 is only a windows executable, no linux
+ return super().browse(file_types, **kwargs)
+ else:
+ return super().browse(filetypes, **kwargs)
+
+ @classmethod
+ def validate(cls, path: str) -> None:
+ """Try to open and validate file against hashes"""
+ with open(path, "rb", buffering=0) as f:
+ try:
+ f.seek(0)
+ if f.read(4) == b"NES\x1A":
+ f.seek(16)
+ else:
+ f.seek(0)
+ cls._validate_stream_hashes(f)
+ base_rom_bytes = f.read()
+ basemd5 = hashlib.md5()
+ basemd5.update(base_rom_bytes)
+ if basemd5.hexdigest() == PROTEUSHASH:
+ # we need special behavior here
+ cls.copy_to = None
+ except ValueError:
+ raise ValueError(f"File hash does not match for {path}")
+
+ rom_file: RomFile = RomFile(RomFile.copy_to)
+
+
+class MM3WebWorld(WebWorld):
+ theme = "partyTime"
+ tutorials = [
+
+ Tutorial(
+ "Multiworld Setup Guide",
+ "A guide to setting up the Mega Man 3 randomizer connected to an Archipelago Multiworld.",
+ "English",
+ "setup_en.md",
+ "setup/en",
+ ["Silvris"]
+ )
+ ]
+
+
+class MM3World(World):
+ """
+ Following his second defeat by Mega Man, Dr. Wily has finally come to his senses. He and Dr. Light begin work on
+ Gamma, a giant peacekeeping robot. However, Gamma's power source, the Energy Elements, are being guarded by the
+ Robot Masters sent to retrieve them. It's up to Mega Man to retrieve the Energy Elements and defeat the mastermind
+ behind the Robot Masters' betrayal.
+ """
+
+ game = "Mega Man 3"
+ settings: ClassVar[MM3Settings]
+ options_dataclass = MM3Options
+ options: MM3Options
+ item_name_to_id = lookup_item_to_id
+ location_name_to_id = lookup_location_to_id
+ item_name_groups = item_names
+ location_name_groups = location_groups
+ web = MM3WebWorld()
+ rom_name: bytearray
+
+ def __init__(self, world: MultiWorld, player: int):
+ self.rom_name = bytearray()
+ self.rom_name_available_event = threading.Event()
+ super().__init__(world, player)
+ self.weapon_damage = deepcopy(weapon_damage)
+ self.wily_4_weapons: dict[int, list[int]] = {}
+
+ def create_regions(self) -> None:
+ menu = MM3Region("Menu", self.player, self.multiworld)
+ self.multiworld.regions.append(menu)
+ location: MM3Location
+ for name, region in mm3_regions.items():
+ stage = MM3Region(name, self.player, self.multiworld)
+ if not region.parent:
+ menu.connect(stage, f"To {name}",
+ lambda state, req=tuple(region.required_items): state.has_all(req, self.player))
+ else:
+ old_stage = self.get_region(region.parent)
+ old_stage.connect(stage, f"To {name}",
+ lambda state, req=tuple(region.required_items): state.has_all(req, self.player))
+ stage.add_locations({loc: data.location_id for loc, data in region.locations.items()
+ if (not data.energy or self.options.consumables.value in (Consumables.option_weapon_health, Consumables.option_all))
+ and (not data.oneup_tank or self.options.consumables.value in (Consumables.option_1up_etank, Consumables.option_all))})
+ for location in stage.get_locations():
+ if location.address is None and location.name != gamma:
+ location.place_locked_item(MM3Item(location.name, ItemClassification.progression,
+ None, self.player))
+ self.multiworld.regions.append(stage)
+ goal_location = self.get_location(gamma)
+ goal_location.place_locked_item(MM3Item("Victory", ItemClassification.progression, None, self.player))
+ self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
+
+ def create_item(self, name: str, force_non_progression: bool = False) -> MM3Item:
+ item = item_table[name]
+ classification = ItemClassification.filler
+ if item.progression and not force_non_progression:
+ classification = ItemClassification.progression_skip_balancing \
+ if item.skip_balancing else ItemClassification.progression
+ if item.useful:
+ classification |= ItemClassification.useful
+ return MM3Item(name, classification, item.code, self.player)
+
+ def get_filler_item_name(self) -> str:
+ return self.random.choices(list(filler_item_weights.keys()),
+ weights=list(filler_item_weights.values()))[0]
+
+ def create_items(self) -> None:
+ itempool = []
+ # grab first robot master
+ robot_master = self.item_id_to_name[0x0101 + self.options.starting_robot_master.value]
+ self.multiworld.push_precollected(self.create_item(robot_master))
+ itempool.extend([self.create_item(name) for name in stage_access_table.keys()
+ if name != robot_master])
+ itempool.extend([self.create_item(name) for name in robot_master_weapon_table.keys()])
+ itempool.extend([self.create_item(name) for name in rush_item_table.keys()])
+ total_checks = 31
+ if self.options.consumables in (Consumables.option_1up_etank,
+ Consumables.option_all):
+ total_checks += 33
+ if self.options.consumables in (Consumables.option_weapon_health,
+ Consumables.option_all):
+ total_checks += 106
+ remaining = total_checks - len(itempool)
+ itempool.extend([self.create_item(name)
+ for name in self.random.choices(list(filler_item_weights.keys()),
+ weights=list(filler_item_weights.values()),
+ k=remaining)])
+ self.multiworld.itempool += itempool
+
+ set_rules = set_rules
+
+ def generate_early(self) -> None:
+ if (self.options.starting_robot_master.current_key == "gemini_man"
+ and not any(item in self.options.start_inventory for item in rush_item_table.keys())) or \
+ (self.options.starting_robot_master.current_key == "hard_man"
+ and not any(item in self.options.start_inventory for item in [rush_coil, rush_jet])):
+ robot_master_pool = [0, 1, 4, 5, 6, 7, ]
+ if rush_marine in self.options.start_inventory:
+ robot_master_pool.append(2)
+ self.options.starting_robot_master.value = self.random.choice(robot_master_pool)
+ logger.warning(
+ f"Incompatible starting Robot Master, changing to "
+ f"{self.options.starting_robot_master.current_key.replace('_', ' ').title()}")
+
+ def fill_hook(self,
+ prog_item_pool: list["Item"],
+ useful_item_pool: list["Item"],
+ filler_item_pool: list["Item"],
+ fill_locations: list["Location"]) -> None:
+ # on a solo gen, fill can try to force Wily into sphere 2, but for most generations this is impossible
+ # MM3 is worse than MM2 here, some of the RBMs can also require Rush
+ if self.multiworld.players > 1:
+ return # Don't need to change anything on a multi gen, fill should be able to solve it with a 4 sphere 1
+ rbm_to_item = {
+ 0: needle_man_stage,
+ 1: magnet_man_stage,
+ 2: gemini_man_stage,
+ 3: hard_man_stage,
+ 4: top_man_stage,
+ 5: snake_man_stage,
+ 6: spark_man_stage,
+ 7: shadow_man_stage
+ }
+ affected_rbm = [2, 3] # Gemini and Hard will always have this happen
+ possible_rbm = [0, 7] # Needle and Shadow are always valid targets, due to Rush Marine/Jet receive
+ if self.options.consumables:
+ possible_rbm.extend([4, 5]) # every stage has at least one of each consumable
+ if self.options.consumables in (Consumables.option_weapon_health, Consumables.option_all):
+ possible_rbm.extend([1, 6])
+ else:
+ affected_rbm.extend([1, 6])
+ else:
+ affected_rbm.extend([1, 4, 5, 6]) # only two checks on non consumables
+ if self.options.starting_robot_master.value in affected_rbm:
+ rbm_names = list(map(lambda s: rbm_to_item[s], possible_rbm))
+ valid_second = [item for item in prog_item_pool
+ if item.name in rbm_names
+ and item.player == self.player]
+ placed_item = self.random.choice(valid_second)
+ rbm_defeated = (f"{robot_masters[self.options.starting_robot_master.value].replace(' Defeated', '')}"
+ f" - Defeated")
+ rbm_location = self.get_location(rbm_defeated)
+ rbm_location.place_locked_item(placed_item)
+ prog_item_pool.remove(placed_item)
+ fill_locations.remove(rbm_location)
+ target_rbm = (placed_item.code & 0xF) - 1
+ if self.options.strict_weakness or (self.options.random_weakness
+ and not (self.weapon_damage[0][target_rbm] > 0)):
+ # we need to find a weakness for this boss
+ weaknesses = [weapon for weapon in range(1, 9)
+ if self.weapon_damage[weapon][target_rbm] >= minimum_weakness_requirement[weapon]]
+ weapons = list(map(lambda s: weapons_to_name[s], weaknesses))
+ valid_weapons = [item for item in prog_item_pool
+ if item.name in weapons
+ and item.player == self.player]
+ placed_weapon = self.random.choice(valid_weapons)
+ weapon_name = next(name for name, idx in lookup_location_to_id.items()
+ if idx == 0x0101 + self.options.starting_robot_master.value)
+ weapon_location = self.get_location(weapon_name)
+ weapon_location.place_locked_item(placed_weapon)
+ prog_item_pool.remove(placed_weapon)
+ fill_locations.remove(weapon_location)
+
+ def generate_output(self, output_directory: str) -> None:
+ try:
+ patch = MM3ProcedurePatch(player=self.player, player_name=self.player_name)
+ patch_rom(self, patch)
+
+ self.rom_name = patch.name
+
+ patch.write(os.path.join(output_directory,
+ f"{self.multiworld.get_out_file_name_base(self.player)}{patch.patch_file_ending}"))
+ except Exception:
+ raise
+ finally:
+ self.rom_name_available_event.set() # make sure threading continues and errors are collected
+
+ def fill_slot_data(self) -> dict[str, Any]:
+ return {
+ "death_link": self.options.death_link.value,
+ "weapon_damage": self.weapon_damage,
+ "wily_4_weapons": self.wily_4_weapons
+ }
+
+ @staticmethod
+ def interpret_slot_data(slot_data: dict[str, Any]) -> dict[str, Any]:
+ local_weapon = {int(key): value for key, value in slot_data["weapon_damage"].items()}
+ local_wily = {int(key): value for key, value in slot_data["wily_4_weapons"].items()}
+ return {"weapon_damage": local_weapon, "wily_4_weapons": local_wily}
+
+ def modify_multidata(self, multidata: dict[str, Any]) -> None:
+ # wait for self.rom_name to be available.
+ self.rom_name_available_event.wait()
+ rom_name = getattr(self, "rom_name", None)
+ # we skip in case of error, so that the original error in the output thread is the one that gets raised
+ if rom_name:
+ new_name = base64.b64encode(bytes(self.rom_name)).decode()
+ multidata["connect_names"][new_name] = multidata["connect_names"][self.player_name]
diff --git a/worlds/mm3/archipelago.json b/worlds/mm3/archipelago.json
new file mode 100644
index 000000000000..ed5ecffc6cbe
--- /dev/null
+++ b/worlds/mm3/archipelago.json
@@ -0,0 +1,6 @@
+{
+ "game": "Mega Man 3",
+ "authors": ["Silvris"],
+ "world_version": "0.1.7",
+ "minimum_ap_version": "0.6.4"
+}
diff --git a/worlds/mm3/client.py b/worlds/mm3/client.py
new file mode 100644
index 000000000000..0e069043a7d9
--- /dev/null
+++ b/worlds/mm3/client.py
@@ -0,0 +1,783 @@
+import logging
+import time
+from enum import IntEnum
+from base64 import b64encode
+from typing import TYPE_CHECKING, Any
+from NetUtils import ClientStatus, color, NetworkItem
+from worlds._bizhawk.client import BizHawkClient
+
+if TYPE_CHECKING:
+ from worlds._bizhawk.context import BizHawkClientContext, BizHawkClientCommandProcessor
+
+nes_logger = logging.getLogger("NES")
+logger = logging.getLogger("Client")
+
+MM3_CURRENT_STAGE = 0x22
+MM3_MEGAMAN_STATE = 0x30
+MM3_PROG_STATE = 0x60
+MM3_ROBOT_MASTERS_DEFEATED = 0x61
+MM3_DOC_STATUS = 0x62
+MM3_HEALTH = 0xA2
+MM3_WEAPON_ENERGY = 0xA3
+MM3_WEAPONS = {
+ 1: 1,
+ 2: 3,
+ 3: 0,
+ 4: 2,
+ 5: 4,
+ 6: 5,
+ 7: 7,
+ 8: 9,
+ 0x11: 6,
+ 0x12: 8,
+ 0x13: 10,
+}
+
+MM3_DOC_REMAP = {
+ 0: 0,
+ 1: 1,
+ 2: 2,
+ 3: 3,
+ 4: 6,
+ 5: 7,
+ 6: 4,
+ 7: 5
+}
+MM3_LIVES = 0xAE
+MM3_E_TANKS = 0xAF
+MM3_ENERGY_BAR = 0xB2
+MM3_CONSUMABLES = 0x150
+MM3_ROBOT_MASTERS_UNLOCKED = 0x680
+MM3_DOC_ROBOT_UNLOCKED = 0x681
+MM3_ENERGYLINK = 0x682
+MM3_LAST_WILY = 0x683
+MM3_RBM_STROBE = 0x684
+MM3_SFX_QUEUE = 0x685
+MM3_DOC_ROBOT_DEFEATED = 0x686
+MM3_COMPLETED_STAGES = 0x687
+MM3_RECEIVED_ITEMS = 0x688
+MM3_RUSH_RECEIVED = 0x689
+
+MM3_CONSUMABLE_TABLE: dict[int, dict[int, tuple[int, int]]] = {
+ # Stage:
+ # Item: (byte offset, bit mask)
+ 0: {
+ 0x0200: (0, 5),
+ 0x0201: (3, 2),
+ },
+ 1: {
+ 0x0202: (2, 6),
+ 0x0203: (2, 5),
+ 0x0204: (2, 4),
+ 0x0205: (2, 3),
+ 0x0206: (3, 6),
+ 0x0207: (3, 5),
+ 0x0208: (3, 7),
+ 0x0209: (4, 0)
+ },
+ 2: {
+ 0x020A: (2, 7),
+ 0x020B: (3, 0),
+ 0x020C: (3, 1),
+ 0x020D: (3, 2),
+ 0x020E: (4, 2),
+ 0x020F: (4, 3),
+ 0x0210: (4, 7),
+ 0x0211: (5, 1),
+ 0x0212: (6, 1),
+ 0x0213: (7, 0)
+ },
+ 3: {
+ 0x0214: (0, 6),
+ 0x0215: (1, 5),
+ 0x0216: (2, 3),
+ 0x0217: (2, 7),
+ 0x0218: (2, 6),
+ 0x0219: (2, 5),
+ 0x021A: (4, 5),
+ },
+ 4: {
+ 0x021B: (1, 3),
+ 0x021C: (1, 5),
+ 0x021D: (1, 7),
+ 0x021E: (2, 0),
+ 0x021F: (1, 6),
+ 0x0220: (2, 4),
+ 0x0221: (2, 5),
+ 0x0222: (4, 5)
+ },
+ 5: {
+ 0x0223: (3, 0),
+ 0x0224: (3, 2),
+ 0x0225: (4, 5),
+ 0x0226: (4, 6),
+ 0x0227: (6, 4),
+ },
+ 6: {
+ 0x0228: (2, 0),
+ 0x0229: (2, 1),
+ 0x022A: (3, 1),
+ 0x022B: (3, 2),
+ 0x022C: (3, 3),
+ 0x022D: (3, 4),
+ },
+ 7: {
+ 0x022E: (3, 5),
+ 0x022F: (3, 4),
+ 0x0230: (3, 3),
+ 0x0231: (3, 2),
+ },
+ 8: {
+ 0x0232: (1, 4),
+ 0x0233: (2, 1),
+ 0x0234: (2, 2),
+ 0x0235: (2, 5),
+ 0x0236: (3, 5),
+ 0x0237: (4, 2),
+ 0x0238: (4, 4),
+ 0x0239: (5, 3),
+ 0x023A: (6, 0),
+ 0x023B: (6, 1),
+ 0x023C: (7, 5),
+
+ },
+ 9: {
+ 0x023D: (3, 2),
+ 0x023E: (3, 6),
+ 0x023F: (4, 5),
+ 0x0240: (5, 4),
+ },
+ 10: {
+ 0x0241: (0, 2),
+ 0x0242: (2, 4)
+ },
+ 11: {
+ 0x0243: (4, 1),
+ 0x0244: (6, 0),
+ 0x0245: (6, 1),
+ 0x0246: (6, 2),
+ 0x0247: (6, 3),
+ },
+ 12: {
+ 0x0248: (0, 0),
+ 0x0249: (0, 3),
+ 0x024A: (0, 5),
+ 0x024B: (1, 6),
+ 0x024C: (2, 7),
+ 0x024D: (2, 3),
+ 0x024E: (2, 1),
+ 0x024F: (2, 2),
+ 0x0250: (3, 5),
+ 0x0251: (3, 4),
+ 0x0252: (3, 6),
+ 0x0253: (3, 7)
+ },
+ 13: {
+ 0x0254: (0, 3),
+ 0x0255: (0, 6),
+ 0x0256: (1, 0),
+ 0x0257: (3, 0),
+ 0x0258: (3, 2),
+ 0x0259: (3, 3),
+ 0x025A: (3, 4),
+ 0x025B: (3, 5),
+ 0x025C: (3, 6),
+ 0x025D: (4, 0),
+ 0x025E: (3, 7),
+ 0x025F: (4, 1),
+ 0x0260: (4, 2),
+ },
+ 14: {
+ 0x0261: (0, 3),
+ 0x0262: (0, 2),
+ 0x0263: (0, 6),
+ 0x0264: (1, 2),
+ 0x0265: (1, 7),
+ 0x0266: (2, 0),
+ 0x0267: (2, 1),
+ 0x0268: (2, 2),
+ 0x0269: (2, 3),
+ 0x026A: (5, 2),
+ 0x026B: (5, 3),
+ },
+ 15: {
+ 0x026C: (0, 0),
+ 0x026D: (0, 1),
+ 0x026E: (0, 2),
+ 0x026F: (0, 3),
+ 0x0270: (0, 4),
+ 0x0271: (0, 6),
+ 0x0272: (1, 0),
+ 0x0273: (1, 2),
+ 0x0274: (1, 3),
+ 0x0275: (1, 1),
+ 0x0276: (0, 7),
+ 0x0277: (3, 2),
+ 0x0278: (2, 2),
+ 0x0279: (2, 3),
+ 0x027A: (2, 4),
+ 0x027B: (2, 5),
+ 0x027C: (3, 1),
+ 0x027D: (3, 0),
+ 0x027E: (2, 7),
+ 0x027F: (2, 6),
+ },
+ 16: {
+ 0x0280: (0, 0),
+ 0x0281: (0, 3),
+ 0x0282: (0, 1),
+ 0x0283: (0, 2),
+ },
+ 17: {
+ 0x0284: (0, 2),
+ 0x0285: (0, 6),
+ 0x0286: (0, 1),
+ 0x0287: (0, 5),
+ 0x0288: (0, 3),
+ 0x0289: (0, 0),
+ 0x028A: (0, 4)
+ }
+}
+
+
+def to_oneup_format(val: int) -> int:
+ return ((val // 10) * 0x10) + val % 10
+
+
+def from_oneup_format(val: int) -> int:
+ return ((val // 0x10) * 10) + val % 0x10
+
+
+class MM3EnergyLinkType(IntEnum):
+ Life = 0
+ NeedleCannon = 1
+ MagnetMissile = 2
+ GeminiLaser = 3
+ HardKnuckle = 4
+ TopSpin = 5
+ SearchSnake = 6
+ SparkShot = 7
+ ShadowBlade = 8
+ OneUP = 12
+ RushCoil = 0x11
+ RushMarine = 0x12
+ RushJet = 0x13
+
+
+request_to_name: dict[str, str] = {
+ "HP": "health",
+ "NE": "Needle Cannon energy",
+ "MA": "Magnet Missile energy",
+ "GE": "Gemini Laser energy",
+ "HA": "Hard Knuckle energy",
+ "TO": "Top Spin energy",
+ "SN": "Search Snake energy",
+ "SP": "Spark Shot energy",
+ "SH": "Shadow Blade energy",
+ "RC": "Rush Coil energy",
+ "RM": "Rush Marine energy",
+ "RJ": "Rush Jet energy",
+ "1U": "lives"
+}
+
+HP_EXCHANGE_RATE = 500000000
+WEAPON_EXCHANGE_RATE = 250000000
+ONEUP_EXCHANGE_RATE = 14000000000
+
+
+def cmd_pool(self: "BizHawkClientCommandProcessor") -> None:
+ """Check the current pool of EnergyLink, and requestable refills from it."""
+ if self.ctx.game != "Mega Man 3":
+ logger.warning("This command can only be used when playing Mega Man 3.")
+ return
+ if not self.ctx.server or not self.ctx.slot:
+ logger.warning("You must be connected to a server to use this command.")
+ return
+ energylink = self.ctx.stored_data.get(f"EnergyLink{self.ctx.team}", 0)
+ health_points = energylink // HP_EXCHANGE_RATE
+ weapon_points = energylink // WEAPON_EXCHANGE_RATE
+ lives = energylink // ONEUP_EXCHANGE_RATE
+ logger.info(f"Healing available: {health_points}\n"
+ f"Weapon refill available: {weapon_points}\n"
+ f"Lives available: {lives}")
+
+
+def cmd_request(self: "BizHawkClientCommandProcessor", amount: str, target: str) -> None:
+ """Request a refill from EnergyLink."""
+ from worlds._bizhawk.context import BizHawkClientContext
+ if self.ctx.game != "Mega Man 3":
+ logger.warning("This command can only be used when playing Mega Man 3.")
+ return
+ if not self.ctx.server or not self.ctx.slot:
+ logger.warning("You must be connected to a server to use this command.")
+ return
+ valid_targets: dict[str, MM3EnergyLinkType] = {
+ "HP": MM3EnergyLinkType.Life,
+ "NE": MM3EnergyLinkType.NeedleCannon,
+ "MA": MM3EnergyLinkType.MagnetMissile,
+ "GE": MM3EnergyLinkType.GeminiLaser,
+ "HA": MM3EnergyLinkType.HardKnuckle,
+ "TO": MM3EnergyLinkType.TopSpin,
+ "SN": MM3EnergyLinkType.SearchSnake,
+ "SP": MM3EnergyLinkType.SparkShot,
+ "SH": MM3EnergyLinkType.ShadowBlade,
+ "RC": MM3EnergyLinkType.RushCoil,
+ "RM": MM3EnergyLinkType.RushMarine,
+ "RJ": MM3EnergyLinkType.RushJet,
+ "1U": MM3EnergyLinkType.OneUP
+ }
+ if target.upper() not in valid_targets:
+ logger.warning(f"Unrecognized target {target.upper()}. Available targets: {', '.join(valid_targets.keys())}")
+ return
+ ctx = self.ctx
+ assert isinstance(ctx, BizHawkClientContext)
+ client = ctx.client_handler
+ assert isinstance(client, MegaMan3Client)
+ client.refill_queue.append((valid_targets[target.upper()], int(amount)))
+ logger.info(f"Restoring {amount} {request_to_name[target.upper()]}.")
+
+
+def cmd_autoheal(self: "BizHawkClientCommandProcessor") -> None:
+ """Enable auto heal from EnergyLink."""
+ if self.ctx.game != "Mega Man 3":
+ logger.warning("This command can only be used when playing Mega Man 3.")
+ return
+ if not self.ctx.server or not self.ctx.slot:
+ logger.warning("You must be connected to a server to use this command.")
+ return
+ else:
+ assert isinstance(self.ctx.client_handler, MegaMan3Client)
+ if self.ctx.client_handler.auto_heal:
+ self.ctx.client_handler.auto_heal = False
+ logger.info(f"Auto healing disabled.")
+ else:
+ self.ctx.client_handler.auto_heal = True
+ logger.info(f"Auto healing enabled.")
+
+
+def get_sfx_writes(sfx: int) -> tuple[int, bytes, str]:
+ return MM3_SFX_QUEUE, sfx.to_bytes(1, 'little'), "RAM"
+
+
+class MegaMan3Client(BizHawkClient):
+ game = "Mega Man 3"
+ system = "NES"
+ patch_suffix = ".apmm3"
+ item_queue: list[NetworkItem] = []
+ pending_death_link: bool = False
+ # default to true, as we don't want to send a deathlink until Mega Man's HP is initialized once
+ sending_death_link: bool = True
+ death_link: bool = False
+ energy_link: bool = False
+ rom: bytes | None = None
+ weapon_energy: int = 0
+ health_energy: int = 0
+ auto_heal: bool = False
+ refill_queue: list[tuple[MM3EnergyLinkType, int]] = []
+ last_wily: int | None = None # default to wily 1
+ doc_status: int | None = None # default to no doc progress
+
+ async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
+ from worlds._bizhawk import RequestFailedError, read, get_memory_size
+ from . import MM3World
+
+ try:
+
+ if (await get_memory_size(ctx.bizhawk_ctx, "PRG ROM")) < 0x3FFB0:
+ # not the entire size, but enough to check validation
+ if "pool" in ctx.command_processor.commands:
+ ctx.command_processor.commands.pop("pool")
+ if "request" in ctx.command_processor.commands:
+ ctx.command_processor.commands.pop("request")
+ if "autoheal" in ctx.command_processor.commands:
+ ctx.command_processor.commands.pop("autoheal")
+ return False
+
+ game_name, version = (await read(ctx.bizhawk_ctx, [(0x3F320, 21, "PRG ROM"),
+ (0x3F33C, 3, "PRG ROM")]))
+ if game_name[:3] != b"MM3" or version != bytes(MM3World.world_version):
+ if game_name[:3] == b"MM3":
+ # I think this is an easier check than the other?
+ older_version = f"{version[0]}.{version[1]}.{version[2]}"
+ logger.warning(f"This Mega Man 3 patch was generated for an different version of the apworld. "
+ f"Please use that version to connect instead.\n"
+ f"Patch version: ({older_version})\n"
+ f"Client version: ({'.'.join([str(i) for i in MM3World.world_version])})")
+ if "pool" in ctx.command_processor.commands:
+ ctx.command_processor.commands.pop("pool")
+ if "request" in ctx.command_processor.commands:
+ ctx.command_processor.commands.pop("request")
+ if "autoheal" in ctx.command_processor.commands:
+ ctx.command_processor.commands.pop("autoheal")
+ return False
+ except UnicodeDecodeError:
+ return False
+ except RequestFailedError:
+ return False # Should verify on the next pass
+
+ ctx.game = self.game
+ self.rom = game_name
+ ctx.items_handling = 0b111
+ ctx.want_slot_data = False
+ deathlink = (await read(ctx.bizhawk_ctx, [(0x3F336, 1, "PRG ROM")]))[0][0]
+ if deathlink & 0x01:
+ self.death_link = True
+ await ctx.update_death_link(self.death_link)
+ if deathlink & 0x02:
+ self.energy_link = True
+
+ if self.energy_link:
+ if "pool" not in ctx.command_processor.commands:
+ ctx.command_processor.commands["pool"] = cmd_pool
+ if "request" not in ctx.command_processor.commands:
+ ctx.command_processor.commands["request"] = cmd_request
+ if "autoheal" not in ctx.command_processor.commands:
+ ctx.command_processor.commands["autoheal"] = cmd_autoheal
+
+ return True
+
+ async def set_auth(self, ctx: "BizHawkClientContext") -> None:
+ if self.rom:
+ ctx.auth = b64encode(self.rom).decode()
+
+ def on_package(self, ctx: "BizHawkClientContext", cmd: str, args: dict[str, Any]) -> None:
+ if cmd == "Bounced":
+ if "tags" in args:
+ assert ctx.slot is not None
+ if "DeathLink" in args["tags"] and args["data"]["source"] != ctx.slot_info[ctx.slot].name:
+ self.on_deathlink(ctx)
+ elif cmd == "Retrieved":
+ if f"MM3_LAST_WILY_{ctx.team}_{ctx.slot}" in args["keys"]:
+ self.last_wily = args["keys"][f"MM3_LAST_WILY_{ctx.team}_{ctx.slot}"]
+ if f"MM3_DOC_STATUS_{ctx.team}_{ctx.slot}" in args["keys"]:
+ self.doc_status = args["keys"][f"MM3_DOC_STATUS_{ctx.team}_{ctx.slot}"]
+ elif cmd == "Connected":
+ if self.energy_link:
+ ctx.set_notify(f"EnergyLink{ctx.team}")
+ if ctx.ui:
+ ctx.ui.enable_energy_link()
+
+ async def send_deathlink(self, ctx: "BizHawkClientContext") -> None:
+ self.sending_death_link = True
+ ctx.last_death_link = time.time()
+ await ctx.send_death("Mega Man was defeated.")
+
+ def on_deathlink(self, ctx: "BizHawkClientContext") -> None:
+ ctx.last_death_link = time.time()
+ self.pending_death_link = True
+
+ async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
+ from worlds._bizhawk import read, write
+
+ if ctx.server is None:
+ return
+
+ if ctx.slot is None:
+ return
+
+ # get our relevant bytes
+ (prog_state, robot_masters_unlocked, robot_masters_defeated, doc_status, doc_robo_unlocked, doc_robo_defeated,
+ rush_acquired, received_items, completed_stages, consumable_checks,
+ e_tanks, lives, weapon_energy, health, state, bar_state, current_stage,
+ energy_link_packet, last_wily) = await read(ctx.bizhawk_ctx, [
+ (MM3_PROG_STATE, 1, "RAM"),
+ (MM3_ROBOT_MASTERS_UNLOCKED, 1, "RAM"),
+ (MM3_ROBOT_MASTERS_DEFEATED, 1, "RAM"),
+ (MM3_DOC_STATUS, 1, "RAM"),
+ (MM3_DOC_ROBOT_UNLOCKED, 1, "RAM"),
+ (MM3_DOC_ROBOT_DEFEATED, 1, "RAM"),
+ (MM3_RUSH_RECEIVED, 1, "RAM"),
+ (MM3_RECEIVED_ITEMS, 1, "RAM"),
+ (MM3_COMPLETED_STAGES, 0x1, "RAM"),
+ (MM3_CONSUMABLES, 16, "RAM"), # Could be more but 16 definitely catches all current
+ (MM3_E_TANKS, 1, "RAM"),
+ (MM3_LIVES, 1, "RAM"),
+ (MM3_WEAPON_ENERGY, 11, "RAM"),
+ (MM3_HEALTH, 1, "RAM"),
+ (MM3_MEGAMAN_STATE, 1, "RAM"),
+ (MM3_ENERGY_BAR, 2, "RAM"),
+ (MM3_CURRENT_STAGE, 1, "RAM"),
+ (MM3_ENERGYLINK, 1, "RAM"),
+ (MM3_LAST_WILY, 1, "RAM"),
+ ])
+
+ if bar_state[0] not in (0x00, 0x80):
+ return # Game is not initialized
+ # Bit of a trick here, bar state can only be 0x00 or 0x80 (display health bar, or don't)
+ # This means it can double as init guard and in-stage tracker
+
+ if not ctx.finished_game and completed_stages[0] & 0x20:
+ await ctx.send_msgs([{
+ "cmd": "StatusUpdate",
+ "status": ClientStatus.CLIENT_GOAL
+ }])
+ writes = []
+
+ # deathlink
+ # only handle deathlink in bar state 0x80 (in stage)
+ if bar_state[0] == 0x80:
+ if self.pending_death_link:
+ writes.append((MM3_MEGAMAN_STATE, bytes([0x0E]), "RAM"))
+ self.pending_death_link = False
+ self.sending_death_link = True
+ if "DeathLink" in ctx.tags and ctx.last_death_link + 1 < time.time():
+ if state[0] == 0x0E and not self.sending_death_link:
+ await self.send_deathlink(ctx)
+ elif state[0] != 0x0E:
+ self.sending_death_link = False
+
+ if self.last_wily != last_wily[0]:
+ if self.last_wily is None:
+ # revalidate last wily from data storage
+ await ctx.send_msgs([{"cmd": "Set", "key": f"MM3_LAST_WILY_{ctx.team}_{ctx.slot}", "operations": [
+ {"operation": "default", "value": 0xC}
+ ]}])
+ await ctx.send_msgs([{"cmd": "Get", "keys": [f"MM3_LAST_WILY_{ctx.team}_{ctx.slot}"]}])
+ elif last_wily[0] == 0:
+ writes.append((MM3_LAST_WILY, self.last_wily.to_bytes(1, "little"), "RAM"))
+ else:
+ # correct our setting
+ self.last_wily = last_wily[0]
+ await ctx.send_msgs([{"cmd": "Set", "key": f"MM3_LAST_WILY_{ctx.team}_{ctx.slot}", "operations": [
+ {"operation": "replace", "value": self.last_wily}
+ ]}])
+
+ if self.doc_status != doc_status[0]:
+ if self.doc_status is None:
+ # revalidate doc status from data storage
+ await ctx.send_msgs([{"cmd": "Set", "key": f"MM3_DOC_STATUS_{ctx.team}_{ctx.slot}", "operations": [
+ {"operation": "default", "value": 0}
+ ]}])
+ await ctx.send_msgs([{"cmd": "Get", "keys": [f"MM3_DOC_STATUS_{ctx.team}_{ctx.slot}"]}])
+ elif doc_status[0] == 0:
+ writes.append((MM3_DOC_STATUS, self.doc_status.to_bytes(1, "little"), "RAM"))
+ else:
+ # correct our setting
+ # shouldn't be possible to desync, but we'll account for it anyways
+ self.doc_status |= doc_status[0]
+ await ctx.send_msgs([{"cmd": "Set", "key": f"MM3_DOC_STATUS_{ctx.team}_{ctx.slot}", "operations": [
+ {"operation": "replace", "value": self.doc_status}
+ ]}])
+
+ weapon_energy = bytearray(weapon_energy)
+ # handle receiving items
+ recv_amount = received_items[0]
+ if recv_amount < len(ctx.items_received):
+ item = ctx.items_received[recv_amount]
+ logging.info('Received %s from %s (%s) (%d/%d in list)' % (
+ color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'),
+ color(ctx.player_names[item.player], 'yellow'),
+ ctx.location_names.lookup_in_slot(item.location, item.player), recv_amount, len(ctx.items_received)))
+
+ if item.item & 0x120 == 0:
+ # Robot Master Weapon, or Rush
+ new_weapons = item.item & 0xFF
+ weapon_energy[MM3_WEAPONS[new_weapons]] |= 0x9C
+ writes.append((MM3_WEAPON_ENERGY, weapon_energy, "RAM"))
+ writes.append(get_sfx_writes(0x32))
+ elif item.item & 0x20 == 0:
+ # Robot Master Stage Access
+ # Catch the Doc Robo here
+ if item.item & 0x10:
+ ptr = MM3_DOC_ROBOT_UNLOCKED
+ unlocked = doc_robo_unlocked
+ else:
+ ptr = MM3_ROBOT_MASTERS_UNLOCKED
+ unlocked = robot_masters_unlocked
+ new_stages = unlocked[0] | (1 << ((item.item & 0xF) - 1))
+ print(new_stages)
+ writes.append((ptr, new_stages.to_bytes(1, 'little'), "RAM"))
+ writes.append(get_sfx_writes(0x34))
+ writes.append((MM3_RBM_STROBE, b"\x01", "RAM"))
+ else:
+ # append to the queue, so we handle it later
+ self.item_queue.append(item)
+ recv_amount += 1
+ writes.append((MM3_RECEIVED_ITEMS, recv_amount.to_bytes(1, 'little'), "RAM"))
+
+ if energy_link_packet[0]:
+ pickup = energy_link_packet[0]
+ if pickup in (0x64, 0x65):
+ # Health pickups
+ if pickup == 0x65:
+ value = 2
+ else:
+ value = 10
+ exchange_rate = HP_EXCHANGE_RATE
+ elif pickup in (0x66, 0x67):
+ # Weapon Energy
+ if pickup == 0x67:
+ value = 2
+ else:
+ value = 10
+ exchange_rate = WEAPON_EXCHANGE_RATE
+ elif pickup == 0x69:
+ # 1-Up
+ value = 1
+ exchange_rate = ONEUP_EXCHANGE_RATE
+ else:
+ # if we managed to pickup something else, we should just fall through
+ value = 0
+ exchange_rate = 0
+ contribution = (value * exchange_rate) >> 1
+ if contribution:
+ await ctx.send_msgs([{
+ "cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations":
+ [{"operation": "add", "value": contribution},
+ {"operation": "max", "value": 0}]}])
+ logger.info(f"Deposited {contribution / HP_EXCHANGE_RATE} health into the pool.")
+ writes.append((MM3_ENERGYLINK, 0x00.to_bytes(1, "little"), "RAM"))
+
+ if self.weapon_energy:
+ # Weapon Energy
+ # We parse the whole thing to spread it as thin as possible
+ current_energy = self.weapon_energy
+ for i, weapon in zip(range(len(weapon_energy)), weapon_energy):
+ if weapon & 0x80 and (weapon & 0x7F) < 0x1C:
+ missing = 0x1C - (weapon & 0x7F)
+ if missing > self.weapon_energy:
+ missing = self.weapon_energy
+ self.weapon_energy -= missing
+ weapon_energy[i] = weapon + missing
+ if not self.weapon_energy:
+ writes.append((MM3_WEAPON_ENERGY, weapon_energy, "RAM"))
+ break
+ else:
+ if current_energy != self.weapon_energy:
+ writes.append((MM3_WEAPON_ENERGY, weapon_energy, "RAM"))
+
+ if self.health_energy or self.auto_heal:
+ # Health Energy
+ # We save this if the player has not taken any damage
+ current_health = health[0]
+ if 0 < (current_health & 0x7F) < 0x1C:
+ health_diff = 0x1C - (current_health & 0x7F)
+ if self.health_energy:
+ if health_diff > self.health_energy:
+ health_diff = self.health_energy
+ self.health_energy -= health_diff
+ else:
+ pool = ctx.stored_data.get(f"EnergyLink{ctx.team}", 0)
+ if health_diff * HP_EXCHANGE_RATE > pool:
+ health_diff = int(pool // HP_EXCHANGE_RATE)
+ await ctx.send_msgs([{
+ "cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations":
+ [{"operation": "add", "value": -health_diff * HP_EXCHANGE_RATE},
+ {"operation": "max", "value": 0}]}])
+ current_health += health_diff
+ writes.append((MM3_HEALTH, current_health.to_bytes(1, 'little'), "RAM"))
+
+ if self.refill_queue:
+ refill_type, refill_amount = self.refill_queue.pop()
+ if refill_type == MM3EnergyLinkType.Life:
+ exchange_rate = HP_EXCHANGE_RATE
+ elif refill_type == MM3EnergyLinkType.OneUP:
+ exchange_rate = ONEUP_EXCHANGE_RATE
+ else:
+ exchange_rate = WEAPON_EXCHANGE_RATE
+ pool = ctx.stored_data.get(f"EnergyLink{ctx.team}", 0)
+ request = exchange_rate * refill_amount
+ if request > pool:
+ logger.warning(
+ f"Not enough energy to fulfill the request. Maximum request: {pool // exchange_rate}")
+ else:
+ await ctx.send_msgs([{
+ "cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations":
+ [{"operation": "add", "value": -request},
+ {"operation": "max", "value": 0}]}])
+ if refill_type == MM3EnergyLinkType.Life:
+ refill_ptr = MM3_HEALTH
+ elif refill_type == MM3EnergyLinkType.OneUP:
+ refill_ptr = MM3_LIVES
+ else:
+ refill_ptr = MM3_WEAPON_ENERGY + MM3_WEAPONS[refill_type]
+ current_value = (await read(ctx.bizhawk_ctx, [(refill_ptr, 1, "RAM")]))[0][0]
+ if refill_type == MM3EnergyLinkType.OneUP:
+ current_value = from_oneup_format(current_value)
+ new_value = min(0x9C if refill_type != MM3EnergyLinkType.OneUP else 99, current_value + refill_amount)
+ if refill_type == MM3EnergyLinkType.OneUP:
+ new_value = to_oneup_format(new_value)
+ writes.append((refill_ptr, new_value.to_bytes(1, "little"), "RAM"))
+
+ if len(self.item_queue):
+ item = self.item_queue.pop(0)
+ idx = item.item & 0xF
+ if idx == 0:
+ # 1-Up
+ current_lives = from_oneup_format(lives[0])
+ if current_lives > 99:
+ self.item_queue.append(item)
+ else:
+ current_lives += 1
+ current_lives = to_oneup_format(current_lives)
+ writes.append((MM3_LIVES, current_lives.to_bytes(1, 'little'), "RAM"))
+ writes.append(get_sfx_writes(0x14))
+ elif idx == 1:
+ self.weapon_energy += 0xE
+ writes.append(get_sfx_writes(0x1C))
+ elif idx == 2:
+ self.health_energy += 0xE
+ writes.append(get_sfx_writes(0x1C))
+ elif idx == 3:
+ current_tanks = from_oneup_format(e_tanks[0])
+ if current_tanks > 99:
+ self.item_queue.append(item)
+ else:
+ current_tanks += 1
+ current_tanks = to_oneup_format(current_tanks)
+ writes.append((MM3_E_TANKS, current_tanks.to_bytes(1, 'little'), "RAM"))
+ writes.append(get_sfx_writes(0x14))
+
+ await write(ctx.bizhawk_ctx, writes)
+
+ new_checks = []
+ # check for locations
+ for i in range(8):
+ flag = 1 << i
+ if robot_masters_defeated[0] & flag:
+ rbm_id = 0x0001 + i
+ if rbm_id not in ctx.checked_locations:
+ new_checks.append(rbm_id)
+ wep_id = 0x0101 + i
+ if wep_id not in ctx.checked_locations:
+ new_checks.append(wep_id)
+ if doc_robo_defeated[0] & flag:
+ doc_id = 0x0010 + MM3_DOC_REMAP[i]
+ if doc_id not in ctx.checked_locations:
+ new_checks.append(doc_id)
+
+ for i in range(2):
+ flag = 1 << i
+ if rush_acquired[0] & flag:
+ itm_id = 0x0111 + i
+ if itm_id not in ctx.checked_locations:
+ new_checks.append(itm_id)
+
+ for i in (0, 1, 2, 4):
+ # Wily 4 does not have a boss check
+ boss_id = 0x0009 + i
+ if completed_stages[0] & (1 << i) != 0:
+ if boss_id not in ctx.checked_locations:
+ new_checks.append(boss_id)
+
+ if completed_stages[0] & 0x80 and 0x000F not in ctx.checked_locations:
+ new_checks.append(0x000F)
+
+ if bar_state[0] == 0x80: # currently in stage
+ if (prog_state[0] > 0x00 and current_stage[0] >= 8) or prog_state[0] == 0x00:
+ # need to block the specific state of Break Man prog=0x12 stage=0x5
+ # it doesn't clean the consumable table and he doesn't have any anyways
+ for consumable in MM3_CONSUMABLE_TABLE[current_stage[0]]:
+ consumable_info = MM3_CONSUMABLE_TABLE[current_stage[0]][consumable]
+ if consumable not in ctx.checked_locations:
+ is_checked = consumable_checks[consumable_info[0]] & (1 << consumable_info[1])
+ if is_checked:
+ new_checks.append(consumable)
+
+ for new_check_id in new_checks:
+ ctx.locations_checked.add(new_check_id)
+ location = ctx.location_names.lookup_in_game(new_check_id)
+ nes_logger.info(
+ f'New Check: {location} ({len(ctx.locations_checked)}/'
+ f'{len(ctx.missing_locations) + len(ctx.checked_locations)})')
+ await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}])
diff --git a/worlds/mm3/color.py b/worlds/mm3/color.py
new file mode 100644
index 000000000000..094402643216
--- /dev/null
+++ b/worlds/mm3/color.py
@@ -0,0 +1,331 @@
+import sys
+from typing import TYPE_CHECKING
+from . import names
+from zlib import crc32
+import struct
+import logging
+
+if TYPE_CHECKING:
+ from . import MM3World
+ from .rom import MM3ProcedurePatch
+
+HTML_TO_NES: dict[str, int] = {
+ 'SNOW': 0x20,
+ 'LINEN': 0x36,
+ 'SEASHELL': 0x36,
+ 'AZURE': 0x3C,
+ 'LAVENDER': 0x33,
+ 'WHITE': 0x30,
+ 'BLACK': 0x0F,
+ 'GREY': 0x00,
+ 'GRAY': 0x00,
+ 'ROYALBLUE': 0x12,
+ 'BLUE': 0x11,
+ 'SKYBLUE': 0x21,
+ 'LIGHTBLUE': 0x31,
+ 'TURQUOISE': 0x2B,
+ 'CYAN': 0x2C,
+ 'AQUAMARINE': 0x3B,
+ 'DARKGREEN': 0x0A,
+ 'GREEN': 0x1A,
+ 'YELLOW': 0x28,
+ 'GOLD': 0x28,
+ 'WHEAT': 0x37,
+ 'TAN': 0x37,
+ 'CHOCOLATE': 0x07,
+ 'BROWN': 0x07,
+ 'SALMON': 0x26,
+ 'ORANGE': 0x27,
+ 'CORAL': 0x36,
+ 'TOMATO': 0x16,
+ 'RED': 0x16,
+ 'PINK': 0x25,
+ 'MAROON': 0x06,
+ 'MAGENTA': 0x24,
+ 'FUSCHIA': 0x24,
+ 'VIOLET': 0x24,
+ 'PLUM': 0x33,
+ 'PURPLE': 0x14,
+ 'THISTLE': 0x34,
+ 'DARKBLUE': 0x01,
+ 'SILVER': 0x10,
+ 'NAVY': 0x02,
+ 'TEAL': 0x1C,
+ 'OLIVE': 0x18,
+ 'LIME': 0x2A,
+ 'AQUA': 0x2C,
+ # can add more as needed
+}
+
+MM3_COLORS: dict[str, tuple[int, int]] = {
+ names.gemini_laser: (0x30, 0x21),
+ names.needle_cannon: (0x30, 0x17),
+ names.hard_knuckle: (0x10, 0x01),
+ names.magnet_missile: (0x10, 0x16),
+ names.top_spin: (0x36, 0x00),
+ names.search_snake: (0x30, 0x19),
+ names.rush_coil: (0x30, 0x15),
+ names.spark_shock: (0x30, 0x26),
+ names.rush_marine: (0x30, 0x15),
+ names.shadow_blade: (0x34, 0x14),
+ names.rush_jet: (0x30, 0x15),
+ names.needle_man_stage: (0x3C, 0x11),
+ names.magnet_man_stage: (0x30, 0x15),
+ names.gemini_man_stage: (0x30, 0x21),
+ names.hard_man_stage: (0x10, 0xC),
+ names.top_man_stage: (0x30, 0x26),
+ names.snake_man_stage: (0x30, 0x29),
+ names.spark_man_stage: (0x30, 0x26),
+ names.shadow_man_stage: (0x30, 0x11),
+ names.doc_needle_stage: (0x27, 0x15),
+ names.doc_gemini_stage: (0x27, 0x15),
+ names.doc_spark_stage: (0x27, 0x15),
+ names.doc_shadow_stage: (0x27, 0x15),
+}
+
+MM3_KNOWN_COLORS: dict[str, tuple[int, int]] = {
+ **MM3_COLORS,
+ # Metroid series
+ "Varia Suit": (0x27, 0x16),
+ "Gravity Suit": (0x14, 0x16),
+ "Phazon Suit": (0x06, 0x1D),
+ # Street Fighter, technically
+ "Hadouken": (0x3C, 0x11),
+ "Shoryuken": (0x38, 0x16),
+ # X Series
+ "Z-Saber": (0x20, 0x16),
+ "Helmet Upgrade": (0x20, 0x01),
+ "Body Upgrade": (0x20, 0x01),
+ "Arms Upgrade": (0x20, 0x01),
+ "Plasma Shot Upgrade": (0x20, 0x01),
+ "Stock Charge Upgrade": (0x20, 0x01),
+ "Legs Upgrade": (0x20, 0x01),
+ # X1
+ "Homing Torpedo": (0x3D, 0x37),
+ "Chameleon Sting": (0x3B, 0x1A),
+ "Rolling Shield": (0x3A, 0x25),
+ "Fire Wave": (0x37, 0x26),
+ "Storm Tornado": (0x34, 0x14),
+ "Electric Spark": (0x3D, 0x28),
+ "Boomerang Cutter": (0x3B, 0x2D),
+ "Shotgun Ice": (0x28, 0x2C),
+ # X2
+ "Crystal Hunter": (0x33, 0x21),
+ "Bubble Splash": (0x35, 0x28),
+ "Spin Wheel": (0x34, 0x1B),
+ "Silk Shot": (0x3B, 0x27),
+ "Sonic Slicer": (0x27, 0x01),
+ "Strike Chain": (0x30, 0x23),
+ "Magnet Mine": (0x28, 0x2D),
+ "Speed Burner": (0x31, 0x16),
+ # X3
+ "Acid Burst": (0x28, 0x2A),
+ "Tornado Fang": (0x28, 0x2C),
+ "Triad Thunder": (0x2B, 0x23),
+ "Spinning Blade": (0x20, 0x16),
+ "Ray Splasher": (0x28, 0x17),
+ "Gravity Well": (0x38, 0x14),
+ "Parasitic Bomb": (0x31, 0x28),
+ "Frost Shield": (0x23, 0x2C),
+ # X4
+ "Lightning Web": (0x3D, 0x28),
+ "Aiming Laser": (0x2C, 0x14),
+ "Double Cyclone": (0x28, 0x1A),
+ "Rising Fire": (0x20, 0x16),
+ "Ground Hunter": (0x2C, 0x15),
+ "Soul Body": (0x37, 0x27),
+ "Twin Slasher": (0x28, 0x00),
+ "Frost Tower": (0x3D, 0x2C),
+}
+
+if "worlds.mm2" in sys.modules:
+ # is this the proper way to do this? who knows!
+ try:
+ mm2 = sys.modules["worlds.mm2"]
+ MM3_KNOWN_COLORS.update(mm2.color.MM2_COLORS)
+ for item in MM3_COLORS:
+ mm2.color.add_color_to_mm2(item, MM3_COLORS[item])
+ except AttributeError:
+ # pass through if an old MM2 is found
+ pass
+
+palette_pointers: dict[str, list[int]] = {
+ "Mega Buster": [0x7C8A8, 0x4650],
+ "Gemini Laser": [0x4654],
+ "Needle Cannon": [0x4658],
+ "Hard Knuckle": [0x465C],
+ "Magnet Missile": [0x4660],
+ "Top Spin": [0x4664],
+ "Search Snake": [0x4668],
+ "Rush Coil": [0x466C],
+ "Spark Shock": [0x4670],
+ "Rush Marine": [0x4674],
+ "Shadow Blade": [0x4678],
+ "Rush Jet": [0x467C],
+ "Needle Man": [0x216C],
+ "Magnet Man": [0x215C],
+ "Gemini Man": [0x217C],
+ "Hard Man": [0x2164],
+ "Top Man": [0x2194],
+ "Snake Man": [0x2174],
+ "Spark Man": [0x2184],
+ "Shadow Man": [0x218C],
+ "Doc Robot": [0x20B8]
+}
+
+
+def add_color_to_mm3(name: str, color: tuple[int, int]) -> None:
+ """
+ Add a color combo for Mega Man 3 to recognize as the color to display for a given item.
+ For information on available colors: https://www.nesdev.org/wiki/PPU_palettes#2C02
+ """
+ MM3_KNOWN_COLORS[name] = validate_colors(*color)
+
+
+def extrapolate_color(color: int) -> tuple[int, int]:
+ if color > 0x1F:
+ color_1 = color
+ color_2 = color_1 - 0x10
+ else:
+ color_2 = color
+ color_1 = color_2 + 0x10
+ return color_1, color_2
+
+
+def validate_colors(color_1: int, color_2: int, allow_match: bool = False) -> tuple[int, int]:
+ # Black should be reserved for outlines, a gray should suffice
+ if color_1 in [0x0D, 0x0E, 0x0F, 0x1E, 0x2E, 0x3E, 0x1F, 0x2F, 0x3F]:
+ color_1 = 0x10
+ if color_2 in [0x0D, 0x0E, 0x0F, 0x1E, 0x2E, 0x3E, 0x1F, 0x2F, 0x3F]:
+ color_2 = 0x10
+
+ # one final check, make sure we don't have two matching
+ if not allow_match and color_1 == color_2:
+ color_1 = 0x30 # color 1 to white works with about any paired color
+
+ return color_1, color_2
+
+
+def expand_colors(color_1: int, color_2: int) -> tuple[tuple[int, int, int], tuple[int, int, int]]:
+ if color_2 >= 0x30:
+ color_a = color_b = color_2
+ else:
+ color_a = color_2 + 0x10
+ color_b = color_2
+
+ if color_1 < 0x10:
+ color_c = color_1 + 0x10
+ color_d = color_1
+ color_e = color_1 + 0x20
+ elif color_1 >= 0x30:
+ color_c = color_1 - 0x10
+ color_d = color_1 - 0x20
+ color_e = color_1
+ else:
+ color_c = color_1
+ color_d = color_1 - 0x10
+ color_e = color_1 + 0x10
+
+ return (0x30, color_a, color_b), (color_d, color_e, color_c)
+
+
+def get_colors_for_item(name: str) -> tuple[tuple[int, int, int], tuple[int, int, int]]:
+ if name in MM3_KNOWN_COLORS:
+ return expand_colors(*MM3_KNOWN_COLORS[name])
+
+ check_colors = {color: color in name.upper().replace(" ", '') for color in HTML_TO_NES}
+ colors = [color for color in check_colors if check_colors[color]]
+ if colors:
+ # we have at least one color pattern matched
+ if len(colors) > 1:
+ # we have at least 2
+ color_1 = HTML_TO_NES[colors[0]]
+ color_2 = HTML_TO_NES[colors[1]]
+ else:
+ color_1, color_2 = extrapolate_color(HTML_TO_NES[colors[0]])
+ else:
+ # generate hash
+ crc_hash = crc32(name.encode('utf-8'))
+ hash_color = struct.pack("I", crc_hash)
+ color_1 = hash_color[0] % 0x3F
+ color_2 = hash_color[1] % 0x3F
+
+ if color_1 < color_2:
+ temp = color_1
+ color_1 = color_2
+ color_2 = temp
+
+ color_1, color_2 = validate_colors(color_1, color_2)
+
+ return expand_colors(color_1, color_2)
+
+
+def parse_color(colors: list[str]) -> tuple[int, int]:
+ color_a = colors[0]
+ if color_a.startswith("$"):
+ color_1 = int(color_a[1:], 16)
+ else:
+ # assume it's in our list of colors
+ color_1 = HTML_TO_NES[color_a.upper()]
+
+ if len(colors) == 1:
+ color_1, color_2 = extrapolate_color(color_1)
+ else:
+ color_b = colors[1]
+ if color_b.startswith("$"):
+ color_2 = int(color_b[1:], 16)
+ else:
+ color_2 = HTML_TO_NES[color_b.upper()]
+ return color_1, color_2
+
+
+def write_palette_shuffle(world: "MM3World", rom: "MM3ProcedurePatch") -> None:
+ palette_shuffle: int | str = world.options.palette_shuffle.value
+ palettes_to_write: dict[str, tuple[int, int]] = {}
+ if isinstance(palette_shuffle, str):
+ color_sets = palette_shuffle.split(";")
+ if len(color_sets) == 1:
+ palette_shuffle = world.options.palette_shuffle.option_none
+ # singularity is more correct, but this is faster
+ else:
+ palette_shuffle = world.options.palette_shuffle.options[color_sets.pop()]
+ for color_set in color_sets:
+ if "-" in color_set:
+ character, color = color_set.split("-")
+ if character.title() not in palette_pointers:
+ logging.warning(f"Player {world.player_name} "
+ f"attempted to set color for unrecognized option {character}")
+ colors = color.split("|")
+ real_colors = validate_colors(*parse_color(colors), allow_match=True)
+ palettes_to_write[character.title()] = real_colors
+ else:
+ # If color is provided with no character, assume singularity
+ colors = color_set.split("|")
+ real_colors = validate_colors(*parse_color(colors), allow_match=True)
+ for character in palette_pointers:
+ palettes_to_write[character] = real_colors
+ # Now we handle the real values
+ if palette_shuffle != 0:
+ if palette_shuffle > 1:
+ if palette_shuffle == 3:
+ # singularity
+ real_colors = validate_colors(world.random.randint(0, 0x3F), world.random.randint(0, 0x3F))
+ for character in palette_pointers:
+ if character not in palettes_to_write:
+ palettes_to_write[character] = real_colors
+ else:
+ for character in palette_pointers:
+ if character not in palettes_to_write:
+ real_colors = validate_colors(world.random.randint(0, 0x3F), world.random.randint(0, 0x3F))
+ palettes_to_write[character] = real_colors
+ else:
+ shuffled_colors = list(MM3_COLORS.values())[:-3] # only include one Doc Robot
+ shuffled_colors.append((0x2C, 0x11)) # Mega Buster
+ world.random.shuffle(shuffled_colors)
+ for character in palette_pointers:
+ if character not in palettes_to_write:
+ palettes_to_write[character] = shuffled_colors.pop()
+
+ for character in palettes_to_write:
+ for pointer in palette_pointers[character]:
+ rom.write_bytes(pointer + 2, bytes(palettes_to_write[character]))
diff --git a/worlds/mm3/data/mm3_basepatch.bsdiff4 b/worlds/mm3/data/mm3_basepatch.bsdiff4
new file mode 100644
index 0000000000000000000000000000000000000000..f80cb76d67cfabb7966f20053e5fc7eee454d066
GIT binary patch
literal 1235
zcmV;^1T6bPQ$$HdMl>+C000000001}0RR910000G015yA0000&T4*&fL0KkKS%F}>
zEC2*-|MmU2gh&Jchy+0(z<|IY5CGr;zyKfwKmjTN00AHXiiDLX>QB;{4Lwb$4H^Sa
zQIJU@ntB2TBO@WEnqe81zjLVq0#rTFg?qI9x#KPy?X%1?2tYM@B!nPJ-%davhF$Tl
zIiQx9IRsK^@InFXApw4Wk((hP9HukLu*#@mW7;7BK!gT3(#}EbP{I%l&Sp1fuJ`R`
z)1SrMkxmpOAXqMo076=5H8DY1CR154gXBH{004jg{`SR7$$$Vu_HSX?*R;Z^xg
zN~RefD;61p@_)9BLve%jTr__4F;J8nW=~*lAr(pG&BZ)27qK_00000
z8UO$p27?l5rbv5K0(rmI4pd;$A>;sOqPkexARL4PSb#YCDB=L*w^7+8+^OF9^b%)!c
zTe|0`3%i#3PAn3jp>W!T@T%g&6c3SJfAqDq(5
z5zZ~pgXl;67p-uDx`V;Oa>g~JbIfQ+CKtA&^X8nlQL07SqM
z0uvIzMFzk_w|EUnwGHN
zT(ZiIHw384c-|)-l=*9tHOx%iW1EfJxZuj8?X?gr;mM3+kH4=}*UJe<$7F6!Vl1Ip
zTlcpSgA`JbuD*Xa0y2LyLHW&qkSM08tldQMOf%dD7z?yF{#(OguYm^gokIx_Cz>AN
zc47huEyVFV%9i33j^bUngBQWyOJrwgoW8i4A5KL1;hXV
literal 0
HcmV?d00001
diff --git a/worlds/mm3/docs/en_Mega Man 3.md b/worlds/mm3/docs/en_Mega Man 3.md
new file mode 100644
index 000000000000..abb619858c48
--- /dev/null
+++ b/worlds/mm3/docs/en_Mega Man 3.md
@@ -0,0 +1,131 @@
+# Mega Man 3
+
+## Where is the options page?
+
+The [player options page for this game](../player-options) contains all the options you need to configure and export a
+config file.
+
+## What does randomization do to this game?
+
+Weapons received from Robot Masters, access to each individual stage (including Doc Robot stages), and Items from Dr. Light are randomized
+into the multiworld. Access to the Wily Stages is locked behind clearing the 4 Doc Robot stages and defeating Break Man. The game is complete upon
+viewing the ending sequence after defeating Gamma.
+
+## What Mega Man 3 items can appear in other players' worlds?
+- Robot Master weapons
+- Robot Master Access Codes (stage access)
+- Doc Robot Access Codes (stage access)
+- Rush Coil/Jet/Marine
+- 1-Ups
+- E-Tanks
+- Health Energy (L)
+- Weapon Energy (L)
+
+## What is considered a location check in Mega Man 3?
+- The defeat of a Robot Master, Doc Robot, or Wily Boss
+- Receiving a weapon or Rush item from Dr. Light
+- Optionally, 1-Ups and E-Tanks present within stages
+- Optionally, Weapon and Health Energy pickups present within stages
+
+## When the player receives an item, what happens?
+A sound effect will play based on the type of item received, and the effects of the item will be immediately applied,
+such as unlocking the use of a weapon mid-stage. If the effects of the item cannot be fully applied (such as receiving
+Health Energy while at full health), the remaining are withheld until they can be applied.
+
+## How do I access the Doc Robot stages?
+By pressing Select on the Robot Master screen, the screen will transition between Robot Masters and
+Doc Robots.
+
+## Useful Information
+* **NesHawk is the recommended core for this game!** Players using QuickNes (or QuickerNes) will experience graphical
+ glitches while in Gemini Man's stage and fighting Gamma.
+* Pressing A+B+Start+Select while in a stage will take you to the Game Over screen, allowing you to leave the stage.
+ Your E-Tanks will be preserved.
+* Your current progress through the Wily stages is saved to the multiworld, allowing you to return to the last stage you
+ reached should you need to leave and enter a Robot Master stage. If you need to return to an earlier Wily stage, holding
+ Select while entering Break Man's stage will take you to Wily 1.
+* When Random Weaknesses are enabled, Break Man's weakness will be changed from Mega Buster to one random weapon.
+
+
+## What is EnergyLink?
+EnergyLink is an energy storage supported by certain games that is shared across all worlds in a multiworld. In Mega Man
+ 3, when enabled, drops from enemies are not applied directly to Mega Man and are instead deposited into the EnergyLink.
+Half of the energy that would be gained is lost upon transfer to the EnergyLink.
+
+Energy from the EnergyLink storage can be converted into health, weapon energy, and lives at different conversion rates.
+You can find out how much of each type you can pull using `/pool` in the client. Additionally, you can have it
+automatically pull from the EnergyLink storage to keep Mega Man healed using the `/autoheal` command in the client.
+Finally, you can use the `/request` command to request a certain type of energy from the storage.
+
+## Plando Palettes
+The palette shuffle option supports specifying a specific palette for a given weapon/Robot Master. The format for doing
+so is `Character-Color1|Color2;Option`. Character is the individual that this should apply to, and can only be one of
+the following:
+- Mega Buster
+- Gemini Laser
+- Needle Cannon
+- Hard Knuckle
+- Magnet Missile
+- Top Spin
+- Search Snake
+- Spark Shot
+- Shadow Blade
+- Rush Coil
+- Rush Jet
+- Rush Marine
+- Needle Man
+- Magnet Man
+- Gemini Man
+- Hard Man
+- Top Man
+- Snake Man
+- Spark Man
+- Shadow Man
+- Doc Robot
+
+Colors attempt to map a list of HTML-defined colors to what the NES can render. A full list of applicable colors can be
+found [here](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/mm2/Color.py#L11). Alternatively, colors can
+be supplied directly using `$xx` format. A full list of NES colors can be found [here](https://www.nesdev.org/wiki/PPU_palettes#2C02).
+
+You can also pass only one color (such as `Mega Buster-Red`) and it will interpret a second color based off of the color
+given. Additionally, passing only colors (such as `Red|Blue`) and not any specific boss/weapon will apply that color to
+all weapons/bosses that did not have a prior color specified.
+
+The option is the method to be used to set the palettes of the remaining bosses/weapons, and will not overwrite any
+plando placements.
+
+## Plando Weaknesses
+Plando Weaknesses allows you to override the amount of damage a boss should take from a given weapon, ignoring prior
+weaknesses generated by strict/random weakness options. Formatting for this is as follows:
+```yaml
+plando_weakness:
+ Needle Man:
+ Top Spin: 0
+ Hard Knuckle: 4
+```
+This would cause Air Man to take 4 damage from Hard Knuckle, and 0 from Top Spin.
+
+Note: it is possible that plando weakness is not be respected should the plando create a situation in which the game
+becomes impossible to complete. In this situation, the damage would be boosted to the minimum required to defeat the
+Robot Master.
+
+
+## Unique Local Commands
+- `/pool` Only present with EnergyLink, prints the max amount of each type of request that could be fulfilled.
+- `/autoheal` Only present with EnergyLink, will automatically drain energy from the EnergyLink in order to
+restore Mega Man's health.
+- `/request ` Only present with EnergyLink, sends a request of a certain type of energy to be pulled from
+the EnergyLink. Types are as follows:
+ - `HP` Health
+ - `NE` Needle Cannon
+ - `MA` Magnet Missile
+ - `GE` Gemini Laser
+ - `HA` Hard Knuckle
+ - `TO` Top Spin
+ - `SN` Search Snake
+ - `SP` Spark Shot
+ - `SH` Shadow Blade
+ - `RC` Rush Coil
+ - `RM` Rush Marine
+ - `RJ` Rush Jet
+ - `1U` Lives
\ No newline at end of file
diff --git a/worlds/mm3/docs/setup_en.md b/worlds/mm3/docs/setup_en.md
new file mode 100644
index 000000000000..07cae74a8a08
--- /dev/null
+++ b/worlds/mm3/docs/setup_en.md
@@ -0,0 +1,53 @@
+# Mega Man 3 Setup Guide
+
+## Required Software
+
+- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
+- An English Mega Man 3 ROM. Alternatively, the [Mega Man Legacy Collection](https://store.steampowered.com/app/363440/Mega_Man_Legacy_Collection/) on Steam.
+- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) 2.7 or later. Bizhawk 2.10
+
+### Configuring Bizhawk
+
+Once you have installed BizHawk, open `EmuHawk.exe` and change the following settings:
+
+- If you're using BizHawk 2.7 or 2.8, go to `Config > Customize`. On the Advanced tab, switch the Lua Core from
+`NLua+KopiLua` to `Lua+LuaInterface`, then restart EmuHawk. (If you're using BizHawk 2.9, you can skip this step.)
+- Under `Config > Customize`, check the "Run in background" option to prevent disconnecting from the client while you're
+tabbed out of EmuHawk.
+- Open a `.nes` file in EmuHawk and go to `Config > ControllersâĻ` to configure your inputs. If you can't click
+`ControllersâĻ`, load any `.nes` ROM first.
+- Consider clearing keybinds in `Config > HotkeysâĻ` if you don't intend to use them. Select the keybind and press Esc to
+clear it.
+
+## Generating and Patching a Game
+
+1. Create your options file (YAML). You can make one on the
+[Mega Man 3 options page](../../../games/Mega%20Man%203/player-options).
+2. Follow the general Archipelago instructions for [generating a game](../../Archipelago/setup/en#generating-a-game).
+This will generate an output file for you. Your patch file will have the `.apmm3` file extension.
+3. Open `ArchipelagoLauncher.exe`
+4. Select "Open Patch" on the left side and select your patch file.
+5. If this is your first time patching, you will be prompted to locate your vanilla ROM. If you are using the Legacy
+Collection, provide `Proteus.exe` in place of your rom.
+6. A patched `.nes` file will be created in the same place as the patch file.
+7. On your first time opening a patch with BizHawk Client, you will also be asked to locate `EmuHawk.exe` in your
+BizHawk install.
+
+## Connecting to a Server
+
+By default, opening a patch file will do steps 1-5 below for you automatically. Even so, keep them in your memory just
+in case you have to close and reopen a window mid-game for some reason.
+
+1. Mega Man 3 uses Archipelago's BizHawk Client. If the client isn't still open from when you patched your game,
+you can re-open it from the launcher.
+2. Ensure EmuHawk is running the patched ROM.
+3. In EmuHawk, go to `Tools > Lua Console`. This window must stay open while playing.
+4. In the Lua Console window, go to `Script > Open ScriptâĻ`.
+5. Navigate to your Archipelago install folder and open `data/lua/connector_bizhawk_generic.lua`.
+6. The emulator and client will eventually connect to each other. The BizHawk Client window should indicate that it
+connected and recognized Mega Man 3.
+7. To connect the client to the server, enter your room's address and port (e.g. `archipelago.gg:38281`) into the
+top text field of the client and click Connect.
+
+You should now be able to receive and send items. You'll need to do these steps every time you want to reconnect. It is
+perfectly safe to make progress offline; everything will re-sync when you reconnect.
diff --git a/worlds/mm3/items.py b/worlds/mm3/items.py
new file mode 100644
index 000000000000..40e6114fffc1
--- /dev/null
+++ b/worlds/mm3/items.py
@@ -0,0 +1,80 @@
+from BaseClasses import Item
+from typing import NamedTuple
+from .names import (needle_cannon, magnet_missile, gemini_laser, hard_knuckle, top_spin, search_snake, spark_shock,
+ shadow_blade, rush_coil, rush_marine, rush_jet, needle_man_stage, magnet_man_stage,
+ gemini_man_stage, hard_man_stage, top_man_stage, snake_man_stage, spark_man_stage, shadow_man_stage,
+ doc_needle_stage, doc_gemini_stage, doc_spark_stage, doc_shadow_stage, e_tank, weapon_energy,
+ health_energy, one_up)
+
+
+class ItemData(NamedTuple):
+ code: int
+ progression: bool
+ useful: bool = False # primarily use this for incredibly useful items of their class, like Metal Blade
+ skip_balancing: bool = False
+
+
+class MM3Item(Item):
+ game = "Mega Man 3"
+
+
+robot_master_weapon_table = {
+ needle_cannon: ItemData(0x0001, True),
+ magnet_missile: ItemData(0x0002, True, True),
+ gemini_laser: ItemData(0x0003, True),
+ hard_knuckle: ItemData(0x0004, True),
+ top_spin: ItemData(0x0005, True, True),
+ search_snake: ItemData(0x0006, True),
+ spark_shock: ItemData(0x0007, True),
+ shadow_blade: ItemData(0x0008, True, True),
+}
+
+stage_access_table = {
+ needle_man_stage: ItemData(0x0101, True),
+ magnet_man_stage: ItemData(0x0102, True),
+ gemini_man_stage: ItemData(0x0103, True),
+ hard_man_stage: ItemData(0x0104, True),
+ top_man_stage: ItemData(0x0105, True),
+ snake_man_stage: ItemData(0x0106, True),
+ spark_man_stage: ItemData(0x0107, True),
+ shadow_man_stage: ItemData(0x0108, True),
+ doc_needle_stage: ItemData(0x0111, True, True),
+ doc_gemini_stage: ItemData(0x0113, True, True),
+ doc_spark_stage: ItemData(0x0117, True, True),
+ doc_shadow_stage: ItemData(0x0118, True, True),
+}
+
+rush_item_table = {
+ rush_coil: ItemData(0x0011, True, True),
+ rush_marine: ItemData(0x0012, True),
+ rush_jet: ItemData(0x0013, True, True),
+}
+
+filler_item_table = {
+ one_up: ItemData(0x0020, False),
+ weapon_energy: ItemData(0x0021, False),
+ health_energy: ItemData(0x0022, False),
+ e_tank: ItemData(0x0023, False, True),
+}
+
+filler_item_weights = {
+ one_up: 1,
+ weapon_energy: 4,
+ health_energy: 1,
+ e_tank: 2,
+}
+
+item_table = {
+ **robot_master_weapon_table,
+ **stage_access_table,
+ **rush_item_table,
+ **filler_item_table,
+}
+
+item_names = {
+ "Weapons": {name for name in robot_master_weapon_table.keys()},
+ "Stages": {name for name in stage_access_table.keys()},
+ "Rush": {name for name in rush_item_table.keys()}
+}
+
+lookup_item_to_id: dict[str, int] = {item_name: data.code for item_name, data in item_table.items() if data.code}
diff --git a/worlds/mm3/locations.py b/worlds/mm3/locations.py
new file mode 100644
index 000000000000..2504236bda83
--- /dev/null
+++ b/worlds/mm3/locations.py
@@ -0,0 +1,312 @@
+from BaseClasses import Location, Region
+from typing import NamedTuple
+from . import names
+
+
+class MM3Location(Location):
+ game = "Mega Man 3"
+
+
+class MM3Region(Region):
+ game = "Mega Man 3"
+
+
+class LocationData(NamedTuple):
+ location_id: int | None
+ energy: bool = False
+ oneup_tank: bool = False
+
+
+class RegionData(NamedTuple):
+ locations: dict[str, LocationData]
+ required_items: list[str]
+ parent: str = ""
+
+mm3_regions: dict[str, RegionData] = {
+ "Needle Man Stage": RegionData({
+ names.needle_man: LocationData(0x0001),
+ names.get_needle_cannon: LocationData(0x0101),
+ names.get_rush_jet: LocationData(0x0111),
+ names.needle_man_c1: LocationData(0x0200, energy=True),
+ names.needle_man_c2: LocationData(0x0201, oneup_tank=True),
+ }, [names.needle_man_stage]),
+
+ "Magnet Man Stage": RegionData({
+ names.magnet_man: LocationData(0x0002),
+ names.get_magnet_missile: LocationData(0x0102),
+ names.magnet_man_c1: LocationData(0x0202, energy=True),
+ names.magnet_man_c2: LocationData(0x0203, energy=True),
+ names.magnet_man_c3: LocationData(0x0204, energy=True),
+ names.magnet_man_c4: LocationData(0x0205, energy=True),
+ names.magnet_man_c5: LocationData(0x0206, energy=True),
+ names.magnet_man_c6: LocationData(0x0207, energy=True),
+ names.magnet_man_c7: LocationData(0x0208, energy=True),
+ names.magnet_man_c8: LocationData(0x0209, energy=True),
+ }, [names.magnet_man_stage]),
+
+ "Gemini Man Stage": RegionData({
+ names.gemini_man: LocationData(0x0003),
+ names.get_gemini_laser: LocationData(0x0103),
+ names.gemini_man_c1: LocationData(0x020A, oneup_tank=True),
+ names.gemini_man_c2: LocationData(0x020B, energy=True),
+ names.gemini_man_c3: LocationData(0x020C, oneup_tank=True),
+ names.gemini_man_c4: LocationData(0x020D, energy=True),
+ names.gemini_man_c5: LocationData(0x020E, energy=True),
+ names.gemini_man_c6: LocationData(0x020F, oneup_tank=True),
+ names.gemini_man_c7: LocationData(0x0210, oneup_tank=True),
+ names.gemini_man_c8: LocationData(0x0211, energy=True),
+ names.gemini_man_c9: LocationData(0x0212, energy=True),
+ names.gemini_man_c10: LocationData(0x0213, oneup_tank=True),
+ }, [names.gemini_man_stage]),
+
+ "Hard Man Stage": RegionData({
+ names.hard_man: LocationData(0x0004),
+ names.get_hard_knuckle: LocationData(0x0104),
+ names.hard_man_c1: LocationData(0x0214, energy=True),
+ names.hard_man_c2: LocationData(0x0215, energy=True),
+ names.hard_man_c3: LocationData(0x0216, oneup_tank=True),
+ names.hard_man_c4: LocationData(0x0217, energy=True),
+ names.hard_man_c5: LocationData(0x0218, energy=True),
+ names.hard_man_c6: LocationData(0x0219, energy=True),
+ names.hard_man_c7: LocationData(0x021A, energy=True),
+ }, [names.hard_man_stage]),
+
+ "Top Man Stage": RegionData({
+ names.top_man: LocationData(0x0005),
+ names.get_top_spin: LocationData(0x0105),
+ names.top_man_c1: LocationData(0x021B, energy=True),
+ names.top_man_c2: LocationData(0x021C, energy=True),
+ names.top_man_c3: LocationData(0x021D, energy=True),
+ names.top_man_c4: LocationData(0x021E, energy=True),
+ names.top_man_c5: LocationData(0x021F, energy=True),
+ names.top_man_c6: LocationData(0x0220, oneup_tank=True),
+ names.top_man_c7: LocationData(0x0221, energy=True),
+ names.top_man_c8: LocationData(0x0222, energy=True),
+ }, [names.top_man_stage]),
+
+ "Snake Man Stage": RegionData({
+ names.snake_man: LocationData(0x0006),
+ names.get_search_snake: LocationData(0x0106),
+ names.snake_man_c1: LocationData(0x0223, energy=True),
+ names.snake_man_c2: LocationData(0x0224, energy=True),
+ names.snake_man_c3: LocationData(0x0225, oneup_tank=True),
+ names.snake_man_c4: LocationData(0x0226, oneup_tank=True),
+ names.snake_man_c5: LocationData(0x0227, energy=True),
+ }, [names.snake_man_stage]),
+
+ "Spark Man Stage": RegionData({
+ names.spark_man: LocationData(0x0007),
+ names.get_spark_shock: LocationData(0x0107),
+ names.spark_man_c1: LocationData(0x0228, energy=True),
+ names.spark_man_c2: LocationData(0x0229, energy=True),
+ names.spark_man_c3: LocationData(0x022A, energy=True),
+ names.spark_man_c4: LocationData(0x022B, energy=True),
+ names.spark_man_c5: LocationData(0x022C, energy=True),
+ names.spark_man_c6: LocationData(0x022D, energy=True),
+ }, [names.spark_man_stage]),
+
+ "Shadow Man Stage": RegionData({
+ names.shadow_man: LocationData(0x0008),
+ names.get_shadow_blade: LocationData(0x0108),
+ names.get_rush_marine: LocationData(0x0112),
+ names.shadow_man_c1: LocationData(0x022E, energy=True),
+ names.shadow_man_c2: LocationData(0x022F, energy=True),
+ names.shadow_man_c3: LocationData(0x0230, energy=True),
+ names.shadow_man_c4: LocationData(0x0231, energy=True),
+ }, [names.shadow_man_stage]),
+
+ "Doc Robot (Needle) - Air": RegionData({
+ names.doc_air: LocationData(0x0010),
+ names.doc_needle_c1: LocationData(0x0232, energy=True),
+ names.doc_needle_c2: LocationData(0x0233, oneup_tank=True),
+ names.doc_needle_c3: LocationData(0x0234, oneup_tank=True),
+ }, [names.doc_needle_stage]),
+
+ "Doc Robot (Needle) - Crash": RegionData({
+ names.doc_crash: LocationData(0x0011),
+ names.doc_needle: LocationData(None),
+ names.doc_needle_c4: LocationData(0x0235, energy=True),
+ names.doc_needle_c5: LocationData(0x0236, energy=True),
+ names.doc_needle_c6: LocationData(0x0237, energy=True),
+ names.doc_needle_c7: LocationData(0x0238, energy=True),
+ names.doc_needle_c8: LocationData(0x0239, energy=True),
+ names.doc_needle_c9: LocationData(0x023A, energy=True),
+ names.doc_needle_c10: LocationData(0x023B, energy=True),
+ names.doc_needle_c11: LocationData(0x023C, energy=True),
+ }, [], parent="Doc Robot (Needle) - Air"),
+
+ "Doc Robot (Gemini) - Flash": RegionData({
+ names.doc_flash: LocationData(0x0012),
+ names.doc_gemini_c1: LocationData(0x023D, oneup_tank=True),
+ names.doc_gemini_c2: LocationData(0x023E, oneup_tank=True),
+ }, [names.doc_gemini_stage]),
+
+ "Doc Robot (Gemini) - Bubble": RegionData({
+ names.doc_bubble: LocationData(0x0013),
+ names.doc_gemini: LocationData(None),
+ names.doc_gemini_c3: LocationData(0x023F, energy=True),
+ names.doc_gemini_c4: LocationData(0x0240, energy=True),
+ }, [], parent="Doc Robot (Gemini) - Flash"),
+
+ "Doc Robot (Shadow) - Wood": RegionData({
+ names.doc_wood: LocationData(0x0014),
+ }, [names.doc_shadow_stage]),
+
+ "Doc Robot (Shadow) - Heat": RegionData({
+ names.doc_heat: LocationData(0x0015),
+ names.doc_shadow: LocationData(None),
+ names.doc_shadow_c1: LocationData(0x0243, energy=True),
+ names.doc_shadow_c2: LocationData(0x0244, energy=True),
+ names.doc_shadow_c3: LocationData(0x0245, energy=True),
+ names.doc_shadow_c4: LocationData(0x0246, energy=True),
+ names.doc_shadow_c5: LocationData(0x0247, energy=True),
+ }, [], parent="Doc Robot (Shadow) - Wood"),
+
+ "Doc Robot (Spark) - Metal": RegionData({
+ names.doc_metal: LocationData(0x0016),
+ names.doc_spark_c1: LocationData(0x0241, energy=True),
+ }, [names.doc_spark_stage]),
+
+ "Doc Robot (Spark) - Quick": RegionData({
+ names.doc_quick: LocationData(0x0017),
+ names.doc_spark: LocationData(None),
+ names.doc_spark_c2: LocationData(0x0242, energy=True),
+ }, [], parent="Doc Robot (Spark) - Metal"),
+
+ "Break Man": RegionData({
+ names.break_man: LocationData(0x000F),
+ names.break_stage: LocationData(None),
+ }, [names.doc_needle, names.doc_gemini, names.doc_spark, names.doc_shadow]),
+
+ "Wily Stage 1": RegionData({
+ names.wily_1_boss: LocationData(0x0009),
+ names.wily_stage_1: LocationData(None),
+ names.wily_1_c1: LocationData(0x0248, oneup_tank=True),
+ names.wily_1_c2: LocationData(0x0249, oneup_tank=True),
+ names.wily_1_c3: LocationData(0x024A, energy=True),
+ names.wily_1_c4: LocationData(0x024B, oneup_tank=True),
+ names.wily_1_c5: LocationData(0x024C, energy=True),
+ names.wily_1_c6: LocationData(0x024D, energy=True),
+ names.wily_1_c7: LocationData(0x024E, energy=True),
+ names.wily_1_c8: LocationData(0x024F, oneup_tank=True),
+ names.wily_1_c9: LocationData(0x0250, energy=True),
+ names.wily_1_c10: LocationData(0x0251, energy=True),
+ names.wily_1_c11: LocationData(0x0252, energy=True),
+ names.wily_1_c12: LocationData(0x0253, energy=True),
+ }, [names.break_stage], parent="Break Man"),
+
+ "Wily Stage 2": RegionData({
+ names.wily_2_boss: LocationData(0x000A),
+ names.wily_stage_2: LocationData(None),
+ names.wily_2_c1: LocationData(0x0254, energy=True),
+ names.wily_2_c2: LocationData(0x0255, energy=True),
+ names.wily_2_c3: LocationData(0x0256, oneup_tank=True),
+ names.wily_2_c4: LocationData(0x0257, energy=True),
+ names.wily_2_c5: LocationData(0x0258, energy=True),
+ names.wily_2_c6: LocationData(0x0259, energy=True),
+ names.wily_2_c7: LocationData(0x025A, energy=True),
+ names.wily_2_c8: LocationData(0x025B, energy=True),
+ names.wily_2_c9: LocationData(0x025C, oneup_tank=True),
+ names.wily_2_c10: LocationData(0x025D, energy=True),
+ names.wily_2_c11: LocationData(0x025E, oneup_tank=True),
+ names.wily_2_c12: LocationData(0x025F, energy=True),
+ names.wily_2_c13: LocationData(0x0260, energy=True),
+ }, [names.wily_stage_1], parent="Wily Stage 1"),
+
+ "Wily Stage 3": RegionData({
+ names.wily_3_boss: LocationData(0x000B),
+ names.wily_stage_3: LocationData(None),
+ names.wily_3_c1: LocationData(0x0261, energy=True),
+ names.wily_3_c2: LocationData(0x0262, energy=True),
+ names.wily_3_c3: LocationData(0x0263, oneup_tank=True),
+ names.wily_3_c4: LocationData(0x0264, oneup_tank=True),
+ names.wily_3_c5: LocationData(0x0265, energy=True),
+ names.wily_3_c6: LocationData(0x0266, energy=True),
+ names.wily_3_c7: LocationData(0x0267, energy=True),
+ names.wily_3_c8: LocationData(0x0268, energy=True),
+ names.wily_3_c9: LocationData(0x0269, energy=True),
+ names.wily_3_c10: LocationData(0x026A, oneup_tank=True),
+ names.wily_3_c11: LocationData(0x026B, oneup_tank=True)
+ }, [names.wily_stage_2], parent="Wily Stage 2"),
+
+ "Wily Stage 4": RegionData({
+ names.wily_stage_4: LocationData(None),
+ names.wily_4_c1: LocationData(0x026C, energy=True),
+ names.wily_4_c2: LocationData(0x026D, energy=True),
+ names.wily_4_c3: LocationData(0x026E, energy=True),
+ names.wily_4_c4: LocationData(0x026F, energy=True),
+ names.wily_4_c5: LocationData(0x0270, energy=True),
+ names.wily_4_c6: LocationData(0x0271, energy=True),
+ names.wily_4_c7: LocationData(0x0272, energy=True),
+ names.wily_4_c8: LocationData(0x0273, energy=True),
+ names.wily_4_c9: LocationData(0x0274, energy=True),
+ names.wily_4_c10: LocationData(0x0275, oneup_tank=True),
+ names.wily_4_c11: LocationData(0x0276, energy=True),
+ names.wily_4_c12: LocationData(0x0277, oneup_tank=True),
+ names.wily_4_c13: LocationData(0x0278, energy=True),
+ names.wily_4_c14: LocationData(0x0279, energy=True),
+ names.wily_4_c15: LocationData(0x027A, energy=True),
+ names.wily_4_c16: LocationData(0x027B, energy=True),
+ names.wily_4_c17: LocationData(0x027C, energy=True),
+ names.wily_4_c18: LocationData(0x027D, energy=True),
+ names.wily_4_c19: LocationData(0x027E, energy=True),
+ names.wily_4_c20: LocationData(0x027F, energy=True),
+ }, [names.wily_stage_3], parent="Wily Stage 3"),
+
+ "Wily Stage 5": RegionData({
+ names.wily_5_boss: LocationData(0x000D),
+ names.wily_stage_5: LocationData(None),
+ names.wily_5_c1: LocationData(0x0280, energy=True),
+ names.wily_5_c2: LocationData(0x0281, energy=True),
+ names.wily_5_c3: LocationData(0x0282, oneup_tank=True),
+ names.wily_5_c4: LocationData(0x0283, oneup_tank=True),
+ }, [names.wily_stage_4], parent="Wily Stage 4"),
+
+ "Wily Stage 6": RegionData({
+ names.gamma: LocationData(None),
+ names.wily_6_c1: LocationData(0x0284, oneup_tank=True),
+ names.wily_6_c2: LocationData(0x0285, oneup_tank=True),
+ names.wily_6_c3: LocationData(0x0286, energy=True),
+ names.wily_6_c4: LocationData(0x0287, energy=True),
+ names.wily_6_c5: LocationData(0x0288, oneup_tank=True),
+ names.wily_6_c6: LocationData(0x0289, oneup_tank=True),
+ names.wily_6_c7: LocationData(0x028A, energy=True),
+ }, [names.wily_stage_5], parent="Wily Stage 5"),
+}
+
+
+def get_boss_locations(region: str) -> list[str]:
+ return [location for location, data in mm3_regions[region].locations.items()
+ if not data.energy and not data.oneup_tank]
+
+
+def get_energy_locations(region: str) -> list[str]:
+ return [location for location, data in mm3_regions[region].locations.items() if data.energy]
+
+
+def get_oneup_locations(region: str) -> list[str]:
+ return [location for location, data in mm3_regions[region].locations.items() if data.oneup_tank]
+
+
+location_table: dict[str, int | None] = {
+ location: data.location_id for region in mm3_regions.values() for location, data in region.locations.items()
+}
+
+
+location_groups = {
+ "Get Equipped": {
+ names.get_needle_cannon,
+ names.get_magnet_missile,
+ names.get_gemini_laser,
+ names.get_hard_knuckle,
+ names.get_top_spin,
+ names.get_search_snake,
+ names.get_spark_shock,
+ names.get_shadow_blade,
+ names.get_rush_marine,
+ names.get_rush_jet,
+ },
+ **{name: {location for location, data in region.locations.items() if data.location_id} for name, region in mm3_regions.items()}
+}
+
+lookup_location_to_id: dict[str, int] = {location: idx for location, idx in location_table.items() if idx is not None}
diff --git a/worlds/mm3/names.py b/worlds/mm3/names.py
new file mode 100644
index 000000000000..dfad7526766c
--- /dev/null
+++ b/worlds/mm3/names.py
@@ -0,0 +1,221 @@
+# Robot Master Weapons
+gemini_laser = "Gemini Laser"
+needle_cannon = "Needle Cannon"
+hard_knuckle = "Hard Knuckle"
+magnet_missile = "Magnet Missile"
+top_spin = "Top Spin"
+search_snake = "Search Snake"
+spark_shock = "Spark Shock"
+shadow_blade = "Shadow Blade"
+
+# Rush
+rush_coil = "Rush Coil"
+rush_jet = "Rush Jet"
+rush_marine = "Rush Marine"
+
+# Access Codes
+needle_man_stage = "Needle Man Access Codes"
+magnet_man_stage = "Magnet Man Access Codes"
+gemini_man_stage = "Gemini Man Access Codes"
+hard_man_stage = "Hard Man Access Codes"
+top_man_stage = "Top Man Access Codes"
+snake_man_stage = "Snake Man Access Codes"
+spark_man_stage = "Spark Man Access Codes"
+shadow_man_stage = "Shadow Man Access Codes"
+doc_needle_stage = "Doc Robot (Needle) Access Codes"
+doc_gemini_stage = "Doc Robot (Gemini) Access Codes"
+doc_spark_stage = "Doc Robot (Spark) Access Codes"
+doc_shadow_stage = "Doc Robot (Shadow) Access Codes"
+
+# Misc. Items
+one_up = "1-Up"
+weapon_energy = "Weapon Energy (L)"
+health_energy = "Health Energy (L)"
+e_tank = "E-Tank"
+
+needle_man = "Needle Man - Defeated"
+magnet_man = "Magnet Man - Defeated"
+gemini_man = "Gemini Man - Defeated"
+hard_man = "Hard Man - Defeated"
+top_man = "Top Man - Defeated"
+snake_man = "Snake Man - Defeated"
+spark_man = "Spark Man - Defeated"
+shadow_man = "Shadow Man - Defeated"
+doc_air = "Doc Robot (Air) - Defeated"
+doc_crash = "Doc Robot (Crash) - Defeated"
+doc_flash = "Doc Robot (Flash) - Defeated"
+doc_bubble = "Doc Robot (Bubble) - Defeated"
+doc_wood = "Doc Robot (Wood) - Defeated"
+doc_heat = "Doc Robot (Heat) - Defeated"
+doc_metal = "Doc Robot (Metal) - Defeated"
+doc_quick = "Doc Robot (Quick) - Defeated"
+break_man = "Break Man - Defeated"
+wily_1_boss = "Kamegoro Maker - Defeated"
+wily_2_boss = "Yellow Devil MK-II - Defeated"
+wily_3_boss = "Holograph Mega Man - Defeated"
+wily_5_boss = "Wily Machine 3 - Defeated"
+gamma = "Gamma - Defeated"
+
+get_gemini_laser = "Gemini Laser - Received"
+get_needle_cannon = "Needle Cannon - Received"
+get_hard_knuckle = "Hard Knuckle - Received"
+get_magnet_missile = "Magnet Missile - Received"
+get_top_spin = "Top Spin - Received"
+get_search_snake = "Search Snake - Received"
+get_spark_shock = "Spark Shock - Received"
+get_shadow_blade = "Shadow Blade - Received"
+get_rush_jet = "Rush Jet - Received"
+get_rush_marine = "Rush Marine - Received"
+
+# Wily Stage Event Items
+doc_needle = "Doc Robot (Needle) - Completed"
+doc_gemini = "Doc Robot (Gemini) - Completed"
+doc_spark = "Doc Robot (Spark) - Completed"
+doc_shadow = "Doc Robot (Shadow) - Completed"
+break_stage = "Break Man"
+wily_stage_1 = "Wily Stage 1 - Completed"
+wily_stage_2 = "Wily Stage 2 - Completed"
+wily_stage_3 = "Wily Stage 3 - Completed"
+wily_stage_4 = "Wily Stage 4 - Completed"
+wily_stage_5 = "Wily Stage 5 - Completed"
+
+# Consumable Locations
+needle_man_c1 = "Needle Man Stage - Weapon Energy 1"
+needle_man_c2 = "Needle Man Stage - E-Tank"
+magnet_man_c1 = "Magnet Man Stage - Health Energy 1"
+magnet_man_c2 = "Magnet Man Stage - Health Energy 2"
+magnet_man_c3 = "Magnet Man Stage - Health Energy 3"
+magnet_man_c4 = "Magnet Man Stage - Health Energy 4"
+magnet_man_c5 = "Magnet Man Stage - Weapon Energy 1"
+magnet_man_c6 = "Magnet Man Stage - Weapon Energy 2"
+magnet_man_c7 = "Magnet Man Stage - Weapon Energy 3"
+magnet_man_c8 = "Magnet Man Stage - Health Energy 5"
+gemini_man_c1 = "Gemini Man Stage - 1-Up 1"
+gemini_man_c2 = "Gemini Man Stage - Health Energy 1"
+gemini_man_c3 = "Gemini Man Stage - Mystery Tank"
+gemini_man_c4 = "Gemini Man Stage - Weapon Energy 1"
+gemini_man_c5 = "Gemini Man Stage - Health Energy 2"
+gemini_man_c6 = "Gemini Man Stage - 1-Up 2"
+gemini_man_c7 = "Gemini Man Stage - E-Tank 1"
+gemini_man_c8 = "Gemini Man Stage - Weapon Energy 2"
+gemini_man_c9 = "Gemini Man Stage - Weapon Energy 3"
+gemini_man_c10 = "Gemini Man Stage - E-Tank 2"
+hard_man_c1 = "Hard Man Stage - Health Energy 1"
+hard_man_c2 = "Hard Man Stage - Health Energy 2"
+hard_man_c3 = "Hard Man Stage - E-Tank"
+hard_man_c4 = "Hard Man Stage - Health Energy 3"
+hard_man_c5 = "Hard Man Stage - Health Energy 4"
+hard_man_c6 = "Hard Man Stage - Health Energy 5"
+hard_man_c7 = "Hard Man Stage - Health Energy 6"
+top_man_c1 = "Top Man Stage - Health Energy 1"
+top_man_c2 = "Top Man Stage - Health Energy 2"
+top_man_c3 = "Top Man Stage - Health Energy 3"
+top_man_c4 = "Top Man Stage - Health Energy 4"
+top_man_c5 = "Top Man Stage - Weapon Energy 1"
+top_man_c6 = "Top Man Stage - 1-Up"
+top_man_c7 = "Top Man Stage - Health Energy 5"
+top_man_c8 = "Top Man Stage - Health Energy 6"
+snake_man_c1 = "Snake Man Stage - Health Energy 1"
+snake_man_c2 = "Snake Man Stage - Health Energy 2"
+snake_man_c3 = "Snake Man Stage - Mystery Tank 1"
+snake_man_c4 = "Snake Man Stage - Mystery Tank 2"
+snake_man_c5 = "Snake Man Stage - Health Energy 3"
+spark_man_c1 = "Spark Man Stage - Health Energy 1"
+spark_man_c2 = "Spark Man Stage - Weapon Energy 1"
+spark_man_c3 = "Spark Man Stage - Weapon Energy 2"
+spark_man_c4 = "Spark Man Stage - Weapon Energy 3"
+spark_man_c5 = "Spark Man Stage - Weapon Energy 4"
+spark_man_c6 = "Spark Man Stage - Weapon Energy 5"
+shadow_man_c1 = "Shadow Man Stage - Weapon Energy 1"
+shadow_man_c2 = "Shadow Man Stage - Weapon Energy 2"
+shadow_man_c3 = "Shadow Man Stage - Weapon Energy 3"
+shadow_man_c4 = "Shadow Man Stage - Weapon Energy 4"
+doc_needle_c1 = "Doc Robot (Needle) - Health Energy 1"
+doc_needle_c2 = "Doc Robot (Needle) - 1-Up 1"
+doc_needle_c3 = "Doc Robot (Needle) - E-Tank 1"
+doc_needle_c4 = "Doc Robot (Needle) - Weapon Energy 1"
+doc_needle_c5 = "Doc Robot (Needle) - Weapon Energy 2"
+doc_needle_c6 = "Doc Robot (Needle) - Weapon Energy 3"
+doc_needle_c7 = "Doc Robot (Needle) - Weapon Energy 4"
+doc_needle_c8 = "Doc Robot (Needle) - Weapon Energy 5"
+doc_needle_c9 = "Doc Robot (Needle) - Weapon Energy 6"
+doc_needle_c10 = "Doc Robot (Needle) - Weapon Energy 7"
+doc_needle_c11 = "Doc Robot (Needle) - Health Energy 2"
+doc_gemini_c1 = "Doc Robot (Gemini) - Mystery Tank 1"
+doc_gemini_c2 = "Doc Robot (Gemini) - Mystery Tank 2"
+doc_gemini_c3 = "Doc Robot (Gemini) - Weapon Energy 1"
+doc_gemini_c4 = "Doc Robot (Gemini) - Weapon Energy 2"
+doc_spark_c1 = "Doc Robot (Spark) - Health Energy 1"
+doc_spark_c2 = "Doc Robot (Spark) - Health Energy 2"
+doc_shadow_c1 = "Doc Robot (Shadow) - Health Energy 1"
+doc_shadow_c2 = "Doc Robot (Shadow) - Weapon Energy 1"
+doc_shadow_c3 = "Doc Robot (Shadow) - Weapon Energy 2"
+doc_shadow_c4 = "Doc Robot (Shadow) - Weapon Energy 3"
+doc_shadow_c5 = "Doc Robot (Shadow) - Weapon Energy 4"
+wily_1_c1 = "Wily Stage 1 - 1-Up 1"
+wily_1_c2 = "Wily Stage 1 - E-Tank 1"
+wily_1_c3 = "Wily Stage 1 - Weapon Energy 1"
+wily_1_c4 = "Wily Stage 1 - 1-Up 2" # Hard Knuckle
+wily_1_c5 = "Wily Stage 1 - Health Energy 1" # Hard Knuckle
+wily_1_c6 = "Wily Stage 1 - Weapon Energy 2" # Hard Knuckle & Rush Vertical
+wily_1_c7 = "Wily Stage 1 - Health Energy 2" # Hard Knuckle & Rush Vertical
+wily_1_c8 = "Wily Stage 1 - E-Tank 2" # Hard Knuckle & Rush Vertical
+wily_1_c9 = "Wily Stage 1 - Health Energy 3"
+wily_1_c10 = "Wily Stage 1 - Health Energy 4"
+wily_1_c11 = "Wily Stage 1 - Weapon Energy 3" # Rush Vertical
+wily_1_c12 = "Wily Stage 1 - Weapon Energy 4" # Rush Vertical
+wily_2_c1 = "Wily Stage 2 - Weapon Energy 1"
+wily_2_c2 = "Wily Stage 2 - Weapon Energy 2"
+wily_2_c3 = "Wily Stage 2 - 1-Up 1"
+wily_2_c4 = "Wily Stage 2 - Weapon Energy 3"
+wily_2_c5 = "Wily Stage 2 - Health Energy 1"
+wily_2_c6 = "Wily Stage 2 - Health Energy 2"
+wily_2_c7 = "Wily Stage 2 - Health Energy 3"
+wily_2_c8 = "Wily Stage 2 - Weapon Energy 4"
+wily_2_c9 = "Wily Stage 2 - E-Tank 1"
+wily_2_c10 = "Wily Stage 2 - Weapon Energy 5"
+wily_2_c11 = "Wily Stage 2 - E-Tank 2"
+wily_2_c12 = "Wily Stage 2 - Weapon Energy 6"
+wily_2_c13 = "Wily Stage 2 - Weapon Energy 7"
+wily_3_c1 = "Wily Stage 3 - Weapon Energy 1" # Hard Knuckle
+wily_3_c2 = "Wily Stage 3 - Weapon Energy 2" # Hard Knuckle
+wily_3_c3 = "Wily Stage 3 - E-Tank 1"
+wily_3_c4 = "Wily Stage 3 - 1-Up 1"
+wily_3_c5 = "Wily Stage 3 - Health Energy 1"
+wily_3_c6 = "Wily Stage 3 - Health Energy 2"
+wily_3_c7 = "Wily Stage 3 - Health Energy 3"
+wily_3_c8 = "Wily Stage 3 - Health Energy 4"
+wily_3_c9 = "Wily Stage 3 - Weapon Energy 3"
+wily_3_c10 = "Wily Stage 3 - Mystery Tank 1" # Hard Knuckle
+wily_3_c11 = "Wily Stage 3 - Mystery Tank 2" # Hard Knuckle
+wily_4_c1 = "Wily Stage 4 - Weapon Energy 1"
+wily_4_c2 = "Wily Stage 4 - Weapon Energy 2"
+wily_4_c3 = "Wily Stage 4 - Weapon Energy 3"
+wily_4_c4 = "Wily Stage 4 - Weapon Energy 4"
+wily_4_c5 = "Wily Stage 4 - Weapon Energy 5"
+wily_4_c6 = "Wily Stage 4 - Health Energy 1"
+wily_4_c7 = "Wily Stage 4 - Health Energy 2"
+wily_4_c8 = "Wily Stage 4 - Health Energy 3"
+wily_4_c9 = "Wily Stage 4 - Health Energy 4"
+wily_4_c10 = "Wily Stage 4 - Mystery Tank"
+wily_4_c11 = "Wily Stage 4 - Weapon Energy 6"
+wily_4_c12 = "Wily Stage 4 - 1-Up"
+wily_4_c13 = "Wily Stage 4 - Weapon Energy 7"
+wily_4_c14 = "Wily Stage 4 - Weapon Energy 8"
+wily_4_c15 = "Wily Stage 4 - Weapon Energy 9"
+wily_4_c16 = "Wily Stage 4 - Weapon Energy 10"
+wily_4_c17 = "Wily Stage 4 - Weapon Energy 11"
+wily_4_c18 = "Wily Stage 4 - Weapon Energy 12"
+wily_4_c19 = "Wily Stage 4 - Weapon Energy 13"
+wily_4_c20 = "Wily Stage 4 - Weapon Energy 14"
+wily_5_c1 = "Wily Stage 5 - Weapon Energy 1"
+wily_5_c2 = "Wily Stage 5 - Weapon Energy 2"
+wily_5_c3 = "Wily Stage 5 - Mystery Tank 1"
+wily_5_c4 = "Wily Stage 5 - Mystery Tank 2"
+wily_6_c1 = "Wily Stage 6 - Mystery Tank 1"
+wily_6_c2 = "Wily Stage 6 - Mystery Tank 2"
+wily_6_c3 = "Wily Stage 6 - Weapon Energy 1"
+wily_6_c4 = "Wily Stage 6 - Weapon Energy 2"
+wily_6_c5 = "Wily Stage 6 - 1-Up"
+wily_6_c6 = "Wily Stage 6 - E-Tank"
+wily_6_c7 = "Wily Stage 6 - Health Energy"
diff --git a/worlds/mm3/options.py b/worlds/mm3/options.py
new file mode 100644
index 000000000000..a1e9b24834f3
--- /dev/null
+++ b/worlds/mm3/options.py
@@ -0,0 +1,164 @@
+from dataclasses import dataclass
+
+from Options import Choice, Toggle, DeathLink, TextChoice, Range, OptionDict, PerGameCommonOptions
+from schema import Schema, And, Use, Optional
+from .rules import bosses, weapons_to_id
+
+
+class EnergyLink(Toggle):
+ """
+ Enables EnergyLink support.
+ When enabled, pickups dropped from enemies are sent to the EnergyLink pool, and healing/weapon energy/1-Ups can
+ be requested from the EnergyLink pool.
+ Some of the energy sent to the pool will be lost on transfer.
+ """
+ display_name = "EnergyLink"
+
+
+class StartingRobotMaster(Choice):
+ """
+ The initial stage unlocked at the start.
+ """
+ display_name = "Starting Robot Master"
+ option_needle_man = 0
+ option_magnet_man = 1
+ option_gemini_man = 2
+ option_hard_man = 3
+ option_top_man = 4
+ option_snake_man = 5
+ option_spark_man = 6
+ option_shadow_man = 7
+ default = "random"
+
+
+class Consumables(Choice):
+ """
+ When enabled, e-tanks/1-ups/health/weapon energy will be added to the pool of items and included as checks.
+ """
+ display_name = "Consumables"
+ option_none = 0
+ option_1up_etank = 1
+ option_weapon_health = 2
+ option_all = 3
+ default = 1
+ alias_true = 3
+ alias_false = 0
+
+ @classmethod
+ def get_option_name(cls, value: int) -> str:
+ if value == 1:
+ return "1-Ups/E-Tanks"
+ elif value == 2:
+ return "Weapon/Health Energy"
+ return super().get_option_name(value)
+
+
+class PaletteShuffle(TextChoice):
+ """
+ Change the color of Mega Man and the Robot Masters.
+ None: The palettes are unchanged.
+ Shuffled: Palette colors are shuffled amongst the robot masters.
+ Randomized: Random (usually good) palettes are generated for each robot master.
+ Singularity: one palette is generated and used for all robot masters.
+ Supports custom palettes using HTML named colors in the
+ following format: Mega Buster-Lavender|Violet;randomized
+ The first value is the character whose palette you'd like to define, then separated by - is a set of 2 colors for
+ that character. separate every color with a pipe, and separate every character as well as the remaining shuffle with
+ a semicolon.
+ """
+ display_name = "Palette Shuffle"
+ option_none = 0
+ option_shuffled = 1
+ option_randomized = 2
+ option_singularity = 3
+
+
+class EnemyWeaknesses(Toggle):
+ """
+ Randomizes the damage dealt to enemies by weapons. Certain enemies will always take damage from the buster.
+ """
+ display_name = "Random Enemy Weaknesses"
+
+
+class StrictWeaknesses(Toggle):
+ """
+ Only your starting Robot Master will take damage from the Mega Buster, the rest must be defeated with weapons.
+ Weapons that only do 1-3 damage to bosses no longer deal damage (aside from Wily/Gamma).
+ """
+ display_name = "Strict Boss Weaknesses"
+
+
+class RandomWeaknesses(Choice):
+ """
+ None: Bosses will have their regular weaknesses.
+ Shuffled: Weapon damage will be shuffled amongst the weapons, so Shadow Blade may do Top Spin damage.
+ Randomized: Weapon damage will be fully randomized.
+ """
+ display_name = "Random Boss Weaknesses"
+ option_none = 0
+ option_shuffled = 1
+ option_randomized = 2
+ alias_false = 0
+ alias_true = 2
+
+
+class Wily4Requirement(Range):
+ """
+ Change the amount of Robot Masters that are required to be defeated for
+ the door to the Wily Machine to open.
+ """
+ display_name = "Wily 4 Requirement"
+ default = 8
+ range_start = 1
+ range_end = 8
+
+
+class WeaknessPlando(OptionDict):
+ """
+ Specify specific damage numbers for boss damage. Can be used even without strict/random weaknesses.
+ plando_weakness:
+ Robot Master:
+ Weapon: Damage
+ """
+ display_name = "Plando Weaknesses"
+ schema = Schema({
+ Optional(And(str, Use(str.title), lambda s: s in bosses)): {
+ And(str, Use(str.title), lambda s: s in weapons_to_id): And(int, lambda i: i in range(0, 14))
+ }
+ })
+ default = {}
+
+
+class ReduceFlashing(Toggle):
+ """
+ Reduce flashing seen in gameplay, such as in stages and when defeating certain bosses.
+ """
+ display_name = "Reduce Flashing"
+
+
+class MusicShuffle(Choice):
+ """
+ Shuffle the music that plays in every stage
+ """
+ display_name = "Music Shuffle"
+ option_none = 0
+ option_shuffled = 1
+ option_randomized = 2
+ option_no_music = 3
+ default = 0
+
+
+@dataclass
+class MM3Options(PerGameCommonOptions):
+ death_link: DeathLink
+ energy_link: EnergyLink
+ starting_robot_master: StartingRobotMaster
+ consumables: Consumables
+ enemy_weakness: EnemyWeaknesses
+ strict_weakness: StrictWeaknesses
+ random_weakness: RandomWeaknesses
+ wily_4_requirement: Wily4Requirement
+ plando_weakness: WeaknessPlando
+ palette_shuffle: PaletteShuffle
+ reduce_flashing: ReduceFlashing
+ music_shuffle: MusicShuffle
diff --git a/worlds/mm3/rom.py b/worlds/mm3/rom.py
new file mode 100644
index 000000000000..8803f38cc545
--- /dev/null
+++ b/worlds/mm3/rom.py
@@ -0,0 +1,374 @@
+import pkgutil
+from typing import TYPE_CHECKING, Iterable
+import hashlib
+import Utils
+import os
+
+from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes
+from . import names
+from .rules import bosses
+
+from .text import MM3TextEntry
+from .color import get_colors_for_item, write_palette_shuffle
+from .options import Consumables
+
+if TYPE_CHECKING:
+ from . import MM3World
+
+MM3LCHASH = "5266687de215e790b2008284402f3917"
+PROTEUSHASH = "b69fff40212b80c94f19e786d1efbf61"
+MM3NESHASH = "4a53b6f58067d62c9a43404fe835dd5c"
+MM3VCHASH = "c50008f1ac86fae8d083232cdd3001a5"
+
+enemy_weakness_ptrs: dict[int, int] = {
+ 0: 0x14100,
+ 1: 0x14200,
+ 2: 0x14300,
+ 3: 0x14400,
+ 4: 0x14500,
+ 5: 0x14600,
+ 6: 0x14700,
+ 7: 0x14800,
+ 8: 0x14900,
+}
+
+enemy_addresses: dict[str, int] = {
+ "Dada": 0x12,
+ "Potton": 0x13,
+ "New Shotman": 0x15,
+ "Hammer Joe": 0x16,
+ "Peterchy": 0x17,
+ "Bubukan": 0x18,
+ "Vault Pole": 0x19, # Capcom..., why did you name an enemy Pole?
+ "Bomb Flier": 0x1A,
+ "Yambow": 0x1D,
+ "Metall 2": 0x1E,
+ "Cannon": 0x22,
+ "Jamacy": 0x25,
+ "Jamacy 2": 0x26, # dunno what this is, but I won't question
+ "Jamacy 3": 0x27,
+ "Jamacy 4": 0x28, # tf is this Capcom
+ "Mag Fly": 0x2A,
+ "Egg": 0x2D,
+ "Gyoraibo 2": 0x2E,
+ "Junk Golem": 0x2F,
+ "Pickelman Bull": 0x30,
+ "Nitron": 0x35,
+ "Pole": 0x37,
+ "Gyoraibo": 0x38,
+ "Hari Harry": 0x3A,
+ "Penpen Maker": 0x3B,
+ "Returning Monking": 0x3C,
+ "Have 'Su' Bee": 0x3E,
+ "Hive": 0x3F,
+ "Bolton-Nutton": 0x40,
+ "Walking Bomb": 0x44,
+ "Elec'n": 0x45,
+ "Mechakkero": 0x47,
+ "Chibee": 0x4B,
+ "Swimming Penpen": 0x4D,
+ "Top": 0x52,
+ "Penpen": 0x56,
+ "Komasaburo": 0x57,
+ "Parasyu": 0x59,
+ "Hologran (Static)": 0x5A,
+ "Hologran (Moving)": 0x5B,
+ "Bomber Pepe": 0x5C,
+ "Metall DX": 0x5D,
+ "Petit Snakey": 0x5E,
+ "Proto Man": 0x62,
+ "Break Man": 0x63,
+ "Metall": 0x7D,
+ "Giant Springer": 0x83,
+ "Springer Missile": 0x85,
+ "Giant Snakey": 0x99,
+ "Tama": 0x9A,
+ "Doc Robot (Flash)": 0xB0,
+ "Doc Robot (Wood)": 0xB1,
+ "Doc Robot (Crash)": 0xB2,
+ "Doc Robot (Metal)": 0xB3,
+ "Doc Robot (Bubble)": 0xC0,
+ "Doc Robot (Heat)": 0xC1,
+ "Doc Robot (Quick)": 0xC2,
+ "Doc Robot (Air)": 0xC3,
+ "Snake": 0xCA,
+ "Needle Man": 0xD0,
+ "Magnet Man": 0xD1,
+ "Top Man": 0xD2,
+ "Shadow Man": 0xD3,
+ "Top Man's Top": 0xD5,
+ "Shadow Man (Sliding)": 0xD8, # Capcom I swear
+ "Hard Man": 0xE0,
+ "Spark Man": 0xE2,
+ "Snake Man": 0xE4,
+ "Gemini Man": 0xE6,
+ "Gemini Man (Clone)": 0xE7, # Capcom why
+ "Yellow Devil MK-II": 0xF1,
+ "Wily Machine 3": 0xF3,
+ "Gamma": 0xF8,
+ "Kamegoro": 0x101,
+ "Kamegoro Shell": 0x102,
+ "Holograph Mega Man": 0x105,
+ "Giant Metall": 0x10C, # This is technically FC but we're +16 from the rom header
+}
+
+# addresses printed when assembling basepatch
+wily_4_ptr: int = 0x7F570
+consumables_ptr: int = 0x7FDEA
+energylink_ptr: int = 0x7FDF9
+
+
+class MM3ProcedurePatch(APProcedurePatch, APTokenMixin):
+ hash = [MM3LCHASH, MM3NESHASH, MM3VCHASH]
+ game = "Mega Man 3"
+ patch_file_ending = ".apmm3"
+ result_file_ending = ".nes"
+ name: bytearray
+ procedure = [
+ ("apply_bsdiff4", ["mm3_basepatch.bsdiff4"]),
+ ("apply_tokens", ["token_patch.bin"]),
+ ]
+
+ @classmethod
+ def get_source_data(cls) -> bytes:
+ return get_base_rom_bytes()
+
+ def write_byte(self, offset: int, value: int) -> None:
+ self.write_token(APTokenTypes.WRITE, offset, value.to_bytes(1, "little"))
+
+ def write_bytes(self, offset: int, value: Iterable[int]) -> None:
+ self.write_token(APTokenTypes.WRITE, offset, bytes(value))
+
+
+def patch_rom(world: "MM3World", patch: MM3ProcedurePatch) -> None:
+ patch.write_file("mm3_basepatch.bsdiff4", pkgutil.get_data(__name__, os.path.join("data", "mm3_basepatch.bsdiff4")))
+ # text writing
+
+ base_address = 0x3C000
+ color_address = 0x31BC7
+ for i, offset, location in zip([0, 8, 1, 2,
+ 3, 4, 5, 6,
+ 7, 9],
+ [0x10, 0x50, 0x91, 0xD2,
+ 0x113, 0x154, 0x195, 0x1D6,
+ 0x217, 0x257],
+ [
+ names.get_needle_cannon,
+ names.get_rush_jet,
+ names.get_magnet_missile,
+ names.get_gemini_laser,
+ names.get_hard_knuckle,
+ names.get_top_spin,
+ names.get_search_snake,
+ names.get_spark_shock,
+ names.get_shadow_blade,
+ names.get_rush_marine,
+ ]):
+ item = world.get_location(location).item
+ if item:
+ if len(item.name) <= 13:
+ # we want to just place it in the center
+ first_str = ""
+ second_str = item.name
+ third_str = ""
+ elif len(item.name) <= 26:
+ # spread across second and third
+ first_str = ""
+ second_str = item.name[:13]
+ third_str = item.name[13:]
+ else:
+ # all three
+ first_str = item.name[:13]
+ second_str = item.name[13:26]
+ third_str = item.name[26:]
+ if len(third_str) > 13:
+ third_str = third_str[:13]
+ player_str = world.multiworld.get_player_name(item.player)
+ if len(player_str) > 13:
+ player_str = player_str[:13]
+ y_coords = 0xA5
+ row = 0x21
+ if location in [names.get_rush_marine, names.get_rush_jet]:
+ y_coords = 0x45
+ row = 0x22
+ patch.write_bytes(base_address + offset, MM3TextEntry(first_str, y_coords, row).resolve())
+ patch.write_bytes(base_address + 16 + offset, MM3TextEntry(second_str, y_coords + 0x20, row).resolve())
+ patch.write_bytes(base_address + 32 + offset, MM3TextEntry(third_str, y_coords + 0x40, row).resolve())
+ if y_coords + 0x60 > 0xFF:
+ row += 1
+ y_coords = 0x01
+ patch.write_bytes(base_address + 48 + offset, MM3TextEntry(player_str, y_coords, row).resolve())
+ colors_high, colors_low = get_colors_for_item(item.name)
+ patch.write_bytes(color_address + (i * 8) + 1, colors_high)
+ patch.write_bytes(color_address + (i * 8) + 5, colors_low)
+ else:
+ patch.write_bytes(base_address + 48 + offset, MM3TextEntry(player_str, y_coords + 0x60, row).resolve())
+
+ write_palette_shuffle(world, patch)
+
+ enemy_weaknesses: dict[str, dict[int, int]] = {}
+
+ if world.options.strict_weakness or world.options.random_weakness or world.options.plando_weakness:
+ # we need to write boss weaknesses
+ for boss in bosses:
+ if boss == "Kamegoro Maker":
+ enemy_weaknesses["Kamegoro"] = {i: world.weapon_damage[i][bosses[boss]] for i in world.weapon_damage}
+ enemy_weaknesses["Kamegoro Shell"] = {i: world.weapon_damage[i][bosses[boss]]
+ for i in world.weapon_damage}
+ elif boss == "Gemini Man":
+ enemy_weaknesses[boss] = {i: world.weapon_damage[i][bosses[boss]] for i in world.weapon_damage}
+ enemy_weaknesses["Gemini Man (Clone)"] = {i: world.weapon_damage[i][bosses[boss]]
+ for i in world.weapon_damage}
+ elif boss == "Shadow Man":
+ enemy_weaknesses[boss] = {i: world.weapon_damage[i][bosses[boss]] for i in world.weapon_damage}
+ enemy_weaknesses["Shadow Man (Sliding)"] = {i: world.weapon_damage[i][bosses[boss]]
+ for i in world.weapon_damage}
+ else:
+ enemy_weaknesses[boss] = {i: world.weapon_damage[i][bosses[boss]] for i in world.weapon_damage}
+
+ if world.options.enemy_weakness:
+ for enemy in enemy_addresses:
+ if enemy in [*bosses.keys(), "Kamegoro", "Kamegoro Shell", "Gemini Man (Clone)", "Shadow Man (Sliding)"]:
+ continue
+ enemy_weaknesses[enemy] = {weapon: world.random.randint(-4, 4) for weapon in enemy_weakness_ptrs}
+ if enemy in ["Tama", "Giant Snakey", "Proto Man", "Giant Metall"] and enemy_weaknesses[enemy][0] <= 0:
+ enemy_weaknesses[enemy][0] = 1
+ elif enemy == "Jamacy 2":
+ # bruh
+ if not enemy_weaknesses[enemy][8] > 0:
+ enemy_weaknesses[enemy][8] = 1
+ if not enemy_weaknesses[enemy][3] > 0:
+ enemy_weaknesses[enemy][3] = 1
+
+ for enemy, damage in enemy_weaknesses.items():
+ for weapon in enemy_weakness_ptrs:
+ if damage[weapon] < 0:
+ damage[weapon] = 256 + damage[weapon]
+ patch.write_byte(enemy_weakness_ptrs[weapon] + enemy_addresses[enemy], damage[weapon])
+
+ if world.options.consumables != Consumables.option_all:
+ value_a = 0x64
+ value_b = 0x6A
+ if world.options.consumables in (Consumables.option_none, Consumables.option_1up_etank):
+ value_a = 0x68
+ if world.options.consumables in (Consumables.option_none, Consumables.option_weapon_health):
+ value_b = 0x67
+ patch.write_byte(consumables_ptr - 3, value_a)
+ patch.write_byte(consumables_ptr + 1, value_b)
+
+ patch.write_byte(wily_4_ptr + 1, world.options.wily_4_requirement.value)
+
+ patch.write_byte(energylink_ptr + 1, world.options.energy_link.value)
+
+ if world.options.reduce_flashing:
+ # Spark Man
+ patch.write_byte(0x12649, 8)
+ patch.write_byte(0x1264E, 8)
+ patch.write_byte(0x12653, 8)
+ # Shadow Man
+ patch.write_byte(0x12658, 0x10)
+ # Gemini Man
+ patch.write_byte(0x12637, 0x20)
+ patch.write_byte(0x1263D, 0x20)
+ patch.write_byte(0x12643, 0x20)
+ # Gamma
+ patch.write_byte(0x7DA4A, 0xF)
+
+ if world.options.music_shuffle:
+ if world.options.music_shuffle.current_key == "no_music":
+ pool = [0xF0] * 18
+ elif world.options.music_shuffle.current_key == "randomized":
+ pool = world.random.choices(range(1, 0xC), k=18)
+ else:
+ pool = [1, 2, 3, 4, 5, 6, 7, 8, 1, 3, 7, 8, 9, 9, 10, 10, 11, 11]
+ world.random.shuffle(pool)
+ patch.write_bytes(0x7CD1C, pool)
+
+ from Utils import __version__
+ patch.name = bytearray(f'MM3{__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0',
+ 'utf8')[:21]
+ patch.name.extend([0] * (21 - len(patch.name)))
+ patch.write_bytes(0x3F330, patch.name) # We changed this section, but this pointer is still valid!
+ deathlink_byte = world.options.death_link.value | (world.options.energy_link.value << 1)
+ patch.write_byte(0x3F346, deathlink_byte)
+
+ patch.write_bytes(0x3F34C, world.world_version)
+
+ version_map = {
+ "0": 0x00,
+ "1": 0x01,
+ "2": 0x02,
+ "3": 0x03,
+ "4": 0x04,
+ "5": 0x05,
+ "6": 0x06,
+ "7": 0x07,
+ "8": 0x08,
+ "9": 0x09,
+ ".": 0x26
+ }
+ patch.write_token(APTokenTypes.RLE, 0x653B, (11, 0x25))
+ patch.write_token(APTokenTypes.RLE, 0x6549, (25, 0x25))
+
+ # BY SILVRIS
+ patch.write_bytes(0x653B, [0x0B, 0x22, 0x25, 0x1C, 0x12, 0x15, 0x1F, 0x1B, 0x12, 0x1C])
+ # ARCHIPELAGO x.x.x
+ patch.write_bytes(0x654D,
+ [0x0A, 0x1B, 0x0C, 0x11, 0x12, 0x19, 0x0E, 0x15, 0x0A, 0x10, 0x18])
+ patch.write_bytes(0x6559, list(map(lambda c: version_map[c], __version__)))
+
+ patch.write_file("token_patch.bin", patch.get_token_binary())
+
+
+header = b"\x4E\x45\x53\x1A\x10\x10\x40\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+
+
+def read_headerless_nes_rom(rom: bytes) -> bytes:
+ if rom[:4] == b"NES\x1A":
+ return rom[16:]
+ else:
+ return rom
+
+
+def get_base_rom_bytes(file_name: str = "") -> bytes:
+ base_rom_bytes: bytes | None = getattr(get_base_rom_bytes, "base_rom_bytes", None)
+ if not base_rom_bytes:
+ file_name = get_base_rom_path(file_name)
+ base_rom_bytes = read_headerless_nes_rom(bytes(open(file_name, "rb").read()))
+
+ basemd5 = hashlib.md5()
+ basemd5.update(base_rom_bytes)
+ if basemd5.hexdigest() == PROTEUSHASH:
+ base_rom_bytes = extract_mm3(base_rom_bytes)
+ basemd5 = hashlib.md5()
+ basemd5.update(base_rom_bytes)
+ if basemd5.hexdigest() not in {MM3LCHASH, MM3NESHASH, MM3VCHASH}:
+ print(basemd5.hexdigest())
+ raise Exception("Supplied Base Rom does not match known MD5 for US, LC, or US VC release. "
+ "Get the correct game and version, then dump it")
+ headered_rom = bytearray(base_rom_bytes)
+ headered_rom[0:0] = header
+ setattr(get_base_rom_bytes, "base_rom_bytes", bytes(headered_rom))
+ return bytes(headered_rom)
+ return base_rom_bytes
+
+
+def get_base_rom_path(file_name: str = "") -> str:
+ from . import MM3World
+ if not file_name:
+ file_name = MM3World.settings.rom_file
+ if not os.path.exists(file_name):
+ file_name = Utils.user_path(file_name)
+ return file_name
+
+
+prg_offset = 0xCF1B0
+prg_size = 0x40000
+chr_offset = 0x10F1B0
+chr_size = 0x20000
+
+
+def extract_mm3(proteus: bytes) -> bytes:
+ mm3 = bytearray(proteus[prg_offset:prg_offset + prg_size])
+ mm3.extend(proteus[chr_offset:chr_offset + chr_size])
+ return bytes(mm3)
diff --git a/worlds/mm3/rules.py b/worlds/mm3/rules.py
new file mode 100644
index 000000000000..b43908f42c82
--- /dev/null
+++ b/worlds/mm3/rules.py
@@ -0,0 +1,388 @@
+from math import ceil
+from typing import TYPE_CHECKING
+from . import names
+from .locations import get_boss_locations, get_oneup_locations, get_energy_locations
+from worlds.generic.Rules import add_rule
+
+if TYPE_CHECKING:
+ from . import MM3World
+ from BaseClasses import CollectionState
+
+bosses: dict[str, int] = {
+ "Needle Man": 0,
+ "Magnet Man": 1,
+ "Gemini Man": 2,
+ "Hard Man": 3,
+ "Top Man": 4,
+ "Snake Man": 5,
+ "Spark Man": 6,
+ "Shadow Man": 7,
+ "Doc Robot (Metal)": 8,
+ "Doc Robot (Quick)": 9,
+ "Doc Robot (Air)": 10,
+ "Doc Robot (Crash)": 11,
+ "Doc Robot (Flash)": 12,
+ "Doc Robot (Bubble)": 13,
+ "Doc Robot (Wood)": 14,
+ "Doc Robot (Heat)": 15,
+ "Break Man": 16,
+ "Kamegoro Maker": 17,
+ "Yellow Devil MK-II": 18,
+ "Holograph Mega Man": 19,
+ "Wily Machine 3": 20,
+ "Gamma": 21
+}
+
+weapons_to_id: dict[str, int] = {
+ "Mega Buster": 0,
+ "Needle Cannon": 1,
+ "Magnet Missile": 2,
+ "Gemini Laser": 3,
+ "Hard Knuckle": 4,
+ "Top Spin": 5,
+ "Search Snake": 6,
+ "Spark Shot": 7,
+ "Shadow Blade": 8,
+}
+
+weapon_damage: dict[int, list[int]] = {
+ 0: [1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 3, 1, 1, 1, 0, ], # Mega Buster
+ 1: [4, 1, 1, 0, 2, 4, 2, 1, 0, 1, 1, 2, 4, 2, 4, 2, 0, 3, 1, 1, 1, 0, ], # Needle Cannon
+ 2: [1, 4, 2, 4, 1, 0, 0, 1, 4, 2, 4, 1, 1, 0, 0, 1, 0, 3, 1, 0, 1, 0, ], # Magnet Missile
+ 3: [7, 2, 4, 1, 0, 1, 1, 1, 1, 4, 2, 0, 4, 1, 1, 1, 0, 3, 1, 1, 1, 0, ], # Gemini Laser
+ 4: [0, 2, 2, 4, 7, 2, 2, 2, 4, 1, 2, 7, 0, 2, 2, 2, 0, 1, 5, 4, 7, 4, ], # Hard Knuckle
+ 5: [1, 1, 2, 0, 4, 2, 1, 7, 0, 1, 1, 4, 1, 1, 2, 7, 0, 1, 0, 7, 0, 2, ], # Top Spin
+ 6: [1, 1, 5, 0, 1, 4, 0, 1, 0, 4, 1, 1, 1, 0, 4, 1, 0, 1, 0, 7, 4, 2, ], # Search Snake
+ 7: [0, 7, 1, 0, 1, 1, 4, 1, 2, 1, 4, 1, 0, 4, 1, 1, 0, 0, 0, 0, 7, 0, ], # Spark Shot
+ 8: [2, 7, 2, 0, 1, 2, 4, 4, 2, 2, 0, 1, 2, 4, 2, 4, 0, 1, 3, 2, 2, 2, ], # Shadow Blade
+}
+
+weapons_to_name: dict[int, str] = {
+ 1: names.needle_cannon,
+ 2: names.magnet_missile,
+ 3: names.gemini_laser,
+ 4: names.hard_knuckle,
+ 5: names.top_spin,
+ 6: names.search_snake,
+ 7: names.spark_shock,
+ 8: names.shadow_blade
+}
+
+minimum_weakness_requirement: dict[int, int] = {
+ 0: 1, # Mega Buster is free
+ 1: 1, # 112 shots of Needle Cannon
+ 2: 2, # 14 shots of Magnet Missile
+ 3: 2, # 14 shots of Gemini Laser
+ 4: 2, # 14 uses of Hard Knuckle
+ 5: 4, # an unknown amount of Top Spin (4 means you should be able to be fine)
+ 6: 1, # 56 uses of Search Snake
+ 7: 2, # 14 functional uses of Spark Shot (fires in twos)
+ 8: 1, # 56 uses of Shadow Blade
+}
+
+robot_masters: dict[int, str] = {
+ 0: "Needle Man Defeated",
+ 1: "Magnet Man Defeated",
+ 2: "Gemini Man Defeated",
+ 3: "Hard Man Defeated",
+ 4: "Top Man Defeated",
+ 5: "Snake Man Defeated",
+ 6: "Spark Man Defeated",
+ 7: "Shadow Man Defeated"
+}
+
+weapon_costs = {
+ 0: 0,
+ 1: 0.25,
+ 2: 2,
+ 3: 2,
+ 4: 2,
+ 5: 7, # Not really, but we can really only rely on Top for one RBM
+ 6: 0.5,
+ 7: 2,
+ 8: 0.5,
+}
+
+
+def can_defeat_enough_rbms(state: "CollectionState", player: int,
+ required: int, boss_requirements: dict[int, list[int]]) -> bool:
+ can_defeat = 0
+ for boss, reqs in boss_requirements.items():
+ if boss in robot_masters:
+ if state.has_all(map(lambda x: weapons_to_name[x], reqs), player):
+ can_defeat += 1
+ if can_defeat >= required:
+ return True
+ return False
+
+
+def has_rush_vertical(state: "CollectionState", player: int) -> bool:
+ return state.has_any([names.rush_coil, names.rush_jet], player)
+
+
+def can_traverse_long_water(state: "CollectionState", player: int) -> bool:
+ return state.has_any([names.rush_marine, names.rush_jet], player)
+
+
+def has_any_rush(state: "CollectionState", player: int) -> bool:
+ return state.has_any([names.rush_coil, names.rush_jet, names.rush_marine], player)
+
+
+def has_rush_jet(state: "CollectionState", player: int) -> bool:
+ return state.has(names.rush_jet, player)
+
+
+def set_rules(world: "MM3World") -> None:
+ # most rules are set on region, so we only worry about rules required within stage access
+ # or rules variable on settings
+ if hasattr(world.multiworld, "re_gen_passthrough"):
+ slot_data = getattr(world.multiworld, "re_gen_passthrough")["Mega Man 3"]
+ world.weapon_damage = slot_data["weapon_damage"]
+ else:
+ if world.options.random_weakness == world.options.random_weakness.option_shuffled:
+ weapon_tables = [table.copy() for weapon, table in weapon_damage.items() if weapon != 0]
+ world.random.shuffle(weapon_tables)
+ for i in range(1, 9):
+ world.weapon_damage[i] = weapon_tables.pop()
+ elif world.options.random_weakness == world.options.random_weakness.option_randomized:
+ world.weapon_damage = {i: [] for i in range(9)}
+ for boss in range(22):
+ for weapon in world.weapon_damage:
+ world.weapon_damage[weapon].append(min(14, max(0, int(world.random.normalvariate(3, 3)))))
+ if not any([world.weapon_damage[weapon][boss] >= 4
+ for weapon in range(1, 9)]):
+ # failsafe, there should be at least one defined non-Buster weakness
+ weapon = world.random.randint(1, 7)
+ world.weapon_damage[weapon][boss] = world.random.randint(4, 14) # Force weakness
+ # handle Break Man
+ boss = 16
+ for weapon in world.weapon_damage:
+ world.weapon_damage[weapon][boss] = 0
+ weapon = world.random.choice(list(world.weapon_damage.keys()))
+ world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon]
+
+ if world.options.strict_weakness:
+ for weapon in weapon_damage:
+ for i in range(22):
+ if i == 16:
+ continue # Break is only weak to buster on non-random, and minimal damage on random
+ elif weapon == 0:
+ world.weapon_damage[weapon][i] = 0
+ elif i in (20, 21) and not world.options.random_weakness:
+ continue
+ # Gamma and Wily Machine need all weaknesses present, so allow
+ elif not world.options.random_weakness == world.options.random_weakness.option_randomized \
+ and i == 17:
+ if 3 > world.weapon_damage[weapon][i] > 0:
+ # Kamegoros take 3 max from weapons on non-random
+ world.weapon_damage[weapon][i] = 0
+ elif 4 > world.weapon_damage[weapon][i] > 0:
+ world.weapon_damage[weapon][i] = 0
+
+ for p_boss in world.options.plando_weakness:
+ for p_weapon in world.options.plando_weakness[p_boss]:
+ if not any(w for w in world.weapon_damage
+ if w != weapons_to_id[p_weapon]
+ and world.weapon_damage[w][bosses[p_boss]] > minimum_weakness_requirement[w]):
+ # we need to replace this weakness
+ weakness = world.random.choice([key for key in world.weapon_damage
+ if key != weapons_to_id[p_weapon]])
+ world.weapon_damage[weakness][bosses[p_boss]] = minimum_weakness_requirement[weakness]
+ world.weapon_damage[weapons_to_id[p_weapon]][bosses[p_boss]] \
+ = world.options.plando_weakness[p_boss][p_weapon]
+
+ # handle special cases
+ for boss in range(22):
+ for weapon in range(1, 9):
+ if (0 < world.weapon_damage[weapon][boss] < minimum_weakness_requirement[weapon] and
+ not any(world.weapon_damage[i][boss] >= minimum_weakness_requirement[weapon]
+ for i in range(1, 8) if i != weapon)):
+ world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon]
+
+ if world.weapon_damage[0][world.options.starting_robot_master.value] < 1:
+ world.weapon_damage[0][world.options.starting_robot_master.value] = 1
+
+ # weakness validation, it is better to confirm a completable seed than respect plando
+ boss_health = {boss: 0x1C for boss in range(8)}
+
+ weapon_energy = {key: float(0x1C) for key in weapon_costs}
+ weapon_boss = {boss: {weapon: world.weapon_damage[weapon][boss] for weapon in world.weapon_damage}
+ for boss in range(8)}
+ flexibility = {
+ boss: (
+ sum(damage_value > 0 for damage_value in
+ weapon_damages.values()) # Amount of weapons that hit this boss
+ * sum(weapon_damages.values()) # Overall damage that those weapons do
+ )
+ for boss, weapon_damages in weapon_boss.items()
+ }
+ boss_flexibility = sorted(flexibility, key=flexibility.get) # Fast way to sort dict by value
+ used_weapons: dict[int, set[int]] = {i: set() for i in range(8)}
+ for boss in boss_flexibility:
+ boss_damage = weapon_boss[boss]
+ weapon_weight = {weapon: (weapon_energy[weapon] / damage) if damage else 0 for weapon, damage in
+ boss_damage.items() if weapon_energy[weapon] > 0}
+ while boss_health[boss] > 0:
+ if boss_damage[0] > 0:
+ boss_health[boss] = 0 # if we can buster, we should buster
+ continue
+ highest, wp = max(zip(weapon_weight.values(), weapon_weight.keys()))
+ uses = weapon_energy[wp] // weapon_costs[wp]
+ if int(uses * boss_damage[wp]) >= boss_health[boss]:
+ used = ceil(boss_health[boss] / boss_damage[wp])
+ weapon_energy[wp] -= weapon_costs[wp] * used
+ boss_health[boss] = 0
+ used_weapons[boss].add(wp)
+ elif highest <= 0:
+ # we are out of weapons that can actually damage the boss
+ # so find the weapon that has the most uses, and apply that as an additional weakness
+ # it should be impossible to be out of energy
+ max_uses, wp = max((weapon_energy[weapon] // weapon_costs[weapon], weapon)
+ for weapon in weapon_weight
+ if weapon != 0)
+ world.weapon_damage[wp][boss] = minimum_weakness_requirement[wp]
+ used = min(int(weapon_energy[wp] // weapon_costs[wp]),
+ ceil(boss_health[boss] / minimum_weakness_requirement[wp]))
+ weapon_energy[wp] -= weapon_costs[wp] * used
+ boss_health[boss] -= int(used * minimum_weakness_requirement[wp])
+ weapon_weight.pop(wp)
+ used_weapons[boss].add(wp)
+ else:
+ # drain the weapon and continue
+ boss_health[boss] -= int(uses * boss_damage[wp])
+ weapon_energy[wp] -= weapon_costs[wp] * uses
+ weapon_weight.pop(wp)
+ used_weapons[boss].add(wp)
+
+ world.wily_4_weapons = {boss: sorted(weapons) for boss, weapons in used_weapons.items()}
+
+ for i, boss_locations in zip(range(22), [
+ get_boss_locations("Needle Man Stage"),
+ get_boss_locations("Magnet Man Stage"),
+ get_boss_locations("Gemini Man Stage"),
+ get_boss_locations("Hard Man Stage"),
+ get_boss_locations("Top Man Stage"),
+ get_boss_locations("Snake Man Stage"),
+ get_boss_locations("Spark Man Stage"),
+ get_boss_locations("Shadow Man Stage"),
+ get_boss_locations("Doc Robot (Spark) - Metal"),
+ get_boss_locations("Doc Robot (Spark) - Quick"),
+ get_boss_locations("Doc Robot (Needle) - Air"),
+ get_boss_locations("Doc Robot (Needle) - Crash"),
+ get_boss_locations("Doc Robot (Gemini) - Flash"),
+ get_boss_locations("Doc Robot (Gemini) - Bubble"),
+ get_boss_locations("Doc Robot (Shadow) - Wood"),
+ get_boss_locations("Doc Robot (Shadow) - Heat"),
+ get_boss_locations("Break Man"),
+ get_boss_locations("Wily Stage 1"),
+ get_boss_locations("Wily Stage 2"),
+ get_boss_locations("Wily Stage 3"),
+ get_boss_locations("Wily Stage 5"),
+ get_boss_locations("Wily Stage 6")
+ ]):
+ if world.weapon_damage[0][i] > 0:
+ continue # this can always be in logic
+ weapons = []
+ for weapon in range(1, 9):
+ if world.weapon_damage[weapon][i] > 0:
+ if world.weapon_damage[weapon][i] < minimum_weakness_requirement[weapon]:
+ continue
+ weapons.append(weapons_to_name[weapon])
+ if not weapons:
+ raise Exception(f"Attempted to have boss {i} with no weakness! Seed: {world.multiworld.seed}")
+ for location in boss_locations:
+ if i in (20, 21):
+ # multi-phase fights, get all potential weaknesses
+ # we should probably do this smarter, but this works for now
+ add_rule(world.get_location(location),
+ lambda state, weps=tuple(weapons): state.has_all(weps, world.player))
+ else:
+ add_rule(world.get_location(location),
+ lambda state, weps=tuple(weapons): state.has_any(weps, world.player))
+
+ # Need to defeat x amount of robot masters for Wily 4
+ add_rule(world.get_location(names.wily_stage_4),
+ lambda state: can_defeat_enough_rbms(state, world.player, world.options.wily_4_requirement.value,
+ world.wily_4_weapons))
+
+ # Handle Doc Robo stage connections
+ for entrance, location in (("To Doc Robot (Needle) - Crash", names.doc_air),
+ ("To Doc Robot (Gemini) - Bubble", names.doc_flash),
+ ("To Doc Robot (Shadow) - Heat", names.doc_wood),
+ ("To Doc Robot (Spark) - Quick", names.doc_metal)):
+ entrance_object = world.get_entrance(entrance)
+ add_rule(entrance_object, lambda state, loc=location: state.can_reach(loc, "Location", world.player))
+
+ # finally, real logic
+ for location in get_boss_locations("Hard Man Stage"):
+ add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player))
+
+ for location in get_boss_locations("Gemini Man Stage"):
+ add_rule(world.get_location(location), lambda state: has_any_rush(state, world.player))
+
+ add_rule(world.get_entrance("To Doc Robot (Spark) - Metal"),
+ lambda state: has_rush_vertical(state, world.player) and
+ state.has_any([names.shadow_blade, names.gemini_laser], world.player))
+ add_rule(world.get_entrance("To Doc Robot (Needle) - Air"),
+ lambda state: has_rush_vertical(state, world.player))
+ add_rule(world.get_entrance("To Doc Robot (Needle) - Crash"),
+ lambda state: has_rush_jet(state, world.player))
+ add_rule(world.get_entrance("To Doc Robot (Gemini) - Bubble"),
+ lambda state: has_rush_vertical(state, world.player) and can_traverse_long_water(state, world.player))
+
+ for location in get_boss_locations("Wily Stage 1"):
+ add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player))
+
+ for location in get_boss_locations("Wily Stage 2"):
+ add_rule(world.get_location(location), lambda state: has_rush_jet(state, world.player))
+
+ # Wily 3 technically needs vertical
+ # However, Wily 3 requires beating Wily 2, and Wily 2 explicitly needs Jet
+ # So we can skip the additional rule on Wily 3
+
+ if world.options.consumables in (world.options.consumables.option_1up_etank,
+ world.options.consumables.option_all):
+ add_rule(world.get_location(names.needle_man_c2), lambda state: has_rush_jet(state, world.player))
+ add_rule(world.get_location(names.gemini_man_c1), lambda state: has_rush_jet(state, world.player))
+ add_rule(world.get_location(names.gemini_man_c3),
+ lambda state: has_rush_vertical(state, world.player)
+ or state.has_any([names.gemini_laser, names.shadow_blade], world.player))
+ for location in (names.gemini_man_c6, names.gemini_man_c7, names.gemini_man_c10):
+ add_rule(world.get_location(location), lambda state: has_any_rush(state, world.player))
+ for location in get_oneup_locations("Hard Man Stage"):
+ add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player))
+ add_rule(world.get_location(names.top_man_c6), lambda state: has_rush_vertical(state, world.player))
+ add_rule(world.get_location(names.doc_needle_c2), lambda state: has_rush_jet(state, world.player))
+ add_rule(world.get_location(names.doc_needle_c3), lambda state: has_rush_jet(state, world.player))
+ add_rule(world.get_location(names.doc_gemini_c1), lambda state: has_rush_vertical(state, world.player))
+ add_rule(world.get_location(names.doc_gemini_c2), lambda state: has_rush_vertical(state, world.player))
+ add_rule(world.get_location(names.wily_1_c8), lambda state: has_rush_vertical(state, world.player))
+ for location in [names.wily_1_c4, names.wily_1_c8]:
+ add_rule(world.get_location(location), lambda state: state.has(names.hard_knuckle, world.player))
+ for location in get_oneup_locations("Wily Stage 2"):
+ if location == names.wily_2_c3:
+ continue
+ add_rule(world.get_location(location), lambda state: has_rush_jet(state, world.player))
+ if world.options.consumables in (world.options.consumables.option_weapon_health,
+ world.options.consumables.option_all):
+ add_rule(world.get_location(names.gemini_man_c2), lambda state: has_rush_vertical(state, world.player))
+ add_rule(world.get_location(names.gemini_man_c4), lambda state: has_rush_vertical(state, world.player))
+ add_rule(world.get_location(names.gemini_man_c5), lambda state: has_rush_vertical(state, world.player))
+ for location in (names.gemini_man_c8, names.gemini_man_c9):
+ add_rule(world.get_location(location), lambda state: has_any_rush(state, world.player))
+ for location in get_energy_locations("Hard Man Stage"):
+ if location == names.hard_man_c1:
+ continue
+ add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player))
+ for location in (names.spark_man_c1, names.spark_man_c2):
+ add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player))
+ for location in [names.top_man_c2, names.top_man_c3, names.top_man_c4, names.top_man_c7]:
+ add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player))
+ for location in [names.wily_1_c5, names.wily_1_c6, names.wily_1_c7]:
+ add_rule(world.get_location(location), lambda state: state.has(names.hard_knuckle, world.player))
+ for location in [names.wily_1_c6, names.wily_1_c7, names.wily_1_c11, names.wily_1_c12]:
+ add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player))
+ for location in get_energy_locations("Wily Stage 2"):
+ if location in (names.wily_2_c1, names.wily_2_c2, names.wily_2_c4):
+ continue
+ add_rule(world.get_location(location), lambda state: has_rush_jet(state, world.player))
diff --git a/worlds/mm3/src/__init__.py b/worlds/mm3/src/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/worlds/mm3/src/mm3_basepatch.asm b/worlds/mm3/src/mm3_basepatch.asm
new file mode 100644
index 000000000000..16e0567ff5a3
--- /dev/null
+++ b/worlds/mm3/src/mm3_basepatch.asm
@@ -0,0 +1,781 @@
+norom
+!headersize = 16
+
+!controller_flip = $14 ; only on first frame of input, used by crash man, etc
+!controller_mirror = $16
+!current_stage = $22
+!current_state = $60
+!completed_rbm_stages = $61
+!completed_doc_stages = $62
+!current_wily = $75
+!received_rbm_stages = $680
+!received_doc_stages = $681
+; !deathlink = $30, set to $0E
+!energylink_packet = $682
+!last_wily = $683
+!rbm_strobe = $684
+!sound_effect_strobe = $685
+!doc_robo_kills = $686
+!wily_stage_completion = $687
+;!received_items = $688
+!acquired_rush = $689
+
+!current_weapon = $A0
+!current_health = $A2
+!received_weapons = $A3
+
+'0' = $00
+'1' = $01
+'2' = $02
+'3' = $03
+'4' = $04
+'5' = $05
+'6' = $06
+'7' = $07
+'8' = $08
+'9' = $09
+'A' = $0A
+'B' = $0B
+'C' = $0C
+'D' = $0D
+'E' = $0E
+'F' = $0F
+'G' = $10
+'H' = $11
+'I' = $12
+'J' = $13
+'K' = $14
+'L' = $15
+'M' = $16
+'N' = $17
+'O' = $18
+'P' = $19
+'Q' = $1A
+'R' = $1B
+'S' = $1C
+'T' = $1D
+'U' = $1E
+'V' = $1F
+'W' = $20
+'X' = $21
+'Y' = $22
+'Z' = $23
+' ' = $25
+'.' = $26
+',' = $27
+'!' = $29
+'r' = $2A
+':' = $2B
+
+; !consumable_checks = $0F80 ; have to find in-stage solutions for this, there's literally not enough ram
+
+!CONTROLLER_SELECT = #$20
+!CONTROLLER_SELECT_START = #$30
+!CONTROLLER_ALL_BUTTON = #$F0
+
+!PpuControl_2000 = $2000
+!PpuMask_2001 = $2001
+!PpuAddr_2006 = $2006
+!PpuData_2007 = $2007
+
+;!LOAD_BANK = $C000
+
+macro org(address,bank)
+ if == $3E
+ org -$C000+($2000*)+!headersize ; org sets the position in the output file to write to (in norom, at least)
+ base ; base sets the position that all labels are relative to - this is necessary so labels will still start from $8000, instead of $0000 or somewhere
+ else
+ if == $3F
+ org -$E000+($2000*)+!headersize ; org sets the position in the output file to write to (in norom, at least)
+ base ; base sets the position that all labels are relative to - this is necessary so labels will still start from $8000, instead of $0000 or somewhere
+ else
+ if >= $A000
+ org -$A000+($2000*)+!headersize
+ base
+ else
+ org -$8000+($2000*)+!headersize
+ base
+ endif
+ endif
+ endif
+endmacro
+
+; capcom.....
+; i can't keep defending you like this
+
+;P
+%org($BEBA, $13)
+RemoveP:
+db $25
+;A
+%org($BD7D, $13)
+RemoveA:
+db $25
+;S
+%org($BE7D, $13)
+RemoveS1:
+db $25
+;S
+%org($BDD5, $13)
+RemoveS2:
+db $25
+
+;W
+%org($BDC7, $13)
+RemoveW:
+db $25
+;O
+%org($BEC7, $13)
+RemoveO:
+db $25
+;R
+%org($BDCF, $13)
+RemoveR:
+db $25
+;D
+%org($BECF, $13)
+RemoveD:
+db $25
+
+%org($A17C, $02)
+AdjustWeaponRefill:
+ ; compare vs unreceived instead. Since the stage ends anyways, this just means you aren't granted the weapon if you don't have it already
+ CMP #$1C
+ BCS WeaponRefillJump
+
+%org($A18B, $02)
+WeaponRefillJump:
+ ; just as a branch target
+
+%org($A3BF, $02)
+FixPseudoSnake:
+ JMP CheckFirstWep
+ NOP
+
+%org($A3CB, $02)
+FixPseudoRush:
+ JMP CheckRushWeapon
+ NOP
+
+%org($BF80, $02)
+CheckRushWeapon:
+ AND #$01
+ BNE .Rush
+ JMP $A3CF
+ .Rush:
+ LDA $A1
+ CLC
+ ADC $B4
+ TAY
+ LDA $00A2, Y
+ BNE .Skip
+ DEC $A1
+ .Skip:
+ JMP $A477
+
+; don't even try to go past this point
+
+%org($802F, $0B)
+HookBreakMan:
+ JSR SetBreakMan
+ NOP
+
+%org($90BC, $18)
+BlockPassword:
+ AND #$08 ; originally 0C, just block down inputs
+
+%org($9258, $18)
+HookStageSelect:
+ JSR ChangeStageMode
+ NOP
+
+%org($92F2, $18)
+AccessStageTarget:
+
+%org($9316, $18)
+AccessStage:
+ JSR RewireDocRobotAccess
+ NOP #2
+ BEQ AccessStageTarget
+
+%org($9468, $18)
+HookWeaponGet:
+ JSR WeaponReceived
+ NOP #4
+
+%org($9917, $18)
+GameOverStageSelect:
+ ; fix it returning to Wily 1
+ CMP #$16
+
+%org($9966, $18)
+SwapSelectTiles:
+ ; swaps when stage select face tiles should be shown
+ JMP InvertSelectTiles
+ NOP
+
+%org($9A54, $18)
+SwapSelectSprites:
+ JMP InvertSelectSprites
+ NOP
+
+%org($9AFF, $18)
+BreakManSelect:
+ JSR ApplyLastWily
+ NOP
+
+%org($BE22, $1D)
+ConsumableHook:
+ JMP CheckConsumable
+
+%org($BE32, $1D)
+EnergyLinkHook:
+ JSR EnergyLink
+
+%org($A000, $1E)
+db $21, $A5, $0C, "PLACEHOLDER 1"
+db $21, $C5, $0C, "PLACEHOLDER 2"
+db $21, $E5, $0C, "PLACEHOLDER 3"
+db $22, $05, $0C, "PLACEHOLDER P"
+db $22, $45, $0C, "PLACEHOLDER 1"
+db $22, $65, $0C, "PLACEHOLDER 2"
+db $22, $85, $0C, "PLACEHOLDER 3"
+db $22, $A5, $0C, "PLACEHOLDER P", $FF
+db $21, $A5, $0C, "PLACEHOLDER 1"
+db $21, $C5, $0C, "PLACEHOLDER 2"
+db $21, $E5, $0C, "PLACEHOLDER 3"
+db $22, $05, $0C, "PLACEHOLDER P", $FF
+db $21, $A5, $0C, "PLACEHOLDER 1"
+db $21, $C5, $0C, "PLACEHOLDER 2"
+db $21, $E5, $0C, "PLACEHOLDER 3"
+db $22, $05, $0C, "PLACEHOLDER P", $FF
+db $21, $A5, $0C, "PLACEHOLDER 1"
+db $21, $C5, $0C, "PLACEHOLDER 2"
+db $21, $E5, $0C, "PLACEHOLDER 3"
+db $22, $05, $0C, "PLACEHOLDER P", $FF
+db $21, $A5, $0C, "PLACEHOLDER 1"
+db $21, $C5, $0C, "PLACEHOLDER 2"
+db $21, $E5, $0C, "PLACEHOLDER 3"
+db $22, $05, $0C, "PLACEHOLDER P", $FF
+db $21, $A5, $0C, "PLACEHOLDER 1"
+db $21, $C5, $0C, "PLACEHOLDER 2"
+db $21, $E5, $0C, "PLACEHOLDER 3"
+db $22, $05, $0C, "PLACEHOLDER P", $FF
+db $21, $A5, $0C, "PLACEHOLDER 1"
+db $21, $C5, $0C, "PLACEHOLDER 2"
+db $21, $E5, $0C, "PLACEHOLDER 3"
+db $22, $05, $0C, "PLACEHOLDER P", $FF
+db $21, $A5, $0C, "PLACEHOLDER 1"
+db $21, $C5, $0C, "PLACEHOLDER 2"
+db $21, $E5, $0C, "PLACEHOLDER 3"
+db $22, $05, $0C, "PLACEHOLDER P"
+db $22, $45, $0C, "PLACEHOLDER 1"
+db $22, $65, $0C, "PLACEHOLDER 2"
+db $22, $85, $0C, "PLACEHOLDER 3"
+db $22, $A5, $0C, "PLACEHOLDER P", $FF
+
+ShowItemString:
+ STY $04
+ LDA ItemLower,X
+ STA $02
+ LDA ItemUpper,X
+ STA $03
+ LDY #$00
+ .LoadString:
+ LDA ($02),Y
+ ORA $10
+ STA $0780,Y
+ BMI .Return
+ INY
+ LDA ($02),Y
+ STA $0780,Y
+ INY
+ LDA ($02),Y
+ STA $0780,Y
+ STA $00
+ INY
+ .LoadCharacters:
+ LDA ($02),Y
+ STA $0780,Y
+ INY
+ DEC $00
+ BPL .LoadCharacters
+ BMI .LoadString
+ .Return:
+ STA $19
+ LDY $04
+ RTS
+
+ItemUpper:
+ db $A0, $A0, $A0, $A1, $A1, $A1, $A1, $A2, $A2
+
+ItemLower:
+ db $00, $81, $C2, $03, $44, $85, $C6, $07, $47
+
+%org($C8F7, $3E)
+RemoveRushCoil:
+ NOP #4
+
+%org($CA73, $3E)
+HookController:
+ JMP ControllerHook
+ NOP
+
+%org($DA18, $3E)
+NullWeaponGet:
+ NOP #5 ; TODO: see if I can reroute this write instead for nicer timings
+
+%org($DB99, $3E)
+HookMidDoc:
+ JSR SetMidDoc
+ NOP
+
+%org($DBB0, $3E)
+HoodEndDoc:
+ JSR SetEndDoc
+ NOP
+
+%org($DC57, $3E)
+RerouteStageComplete:
+ LDA $60
+ JSR SetStageComplete
+ NOP #2
+
+%org($DC6F, $3E)
+RerouteRushMarine:
+ JMP SetRushMarine
+ NOP
+
+%org($DC6A, $3E)
+RerouteRushJet:
+ JMP SetRushJet
+ NOP
+
+%org($DC78, $3E)
+RerouteWilyComplete:
+ JMP SetEndWily
+ NOP
+ EndWilyReturn:
+
+%org($DF81, $3E)
+NullBreak:
+ NOP #5 ; nop break man giving every weapon
+
+%org($E15F, $3F)
+Wily4:
+ JMP Wily4Comparison
+ NOP
+
+
+%org($F340, $3F)
+RewireDocRobotAccess:
+ LDA !current_state
+ BNE .DocRobo
+ LDA !received_rbm_stages
+ SEC
+ BCS .Return
+ .DocRobo:
+ LDA !received_doc_stages
+ .Return:
+ AND $9DED,Y
+ RTS
+
+ChangeStageMode:
+ ; also handles hot reload of stage select
+ ; kinda broken, sprites don't disappear and palettes go wonky with Break Man access
+ ; but like, it functions!
+ LDA !sound_effect_strobe
+ BEQ .Continue
+ JSR $F89A
+ LDA #$00
+ STA !sound_effect_strobe
+ .Continue:
+ LDA $14
+ AND #$20
+ BEQ .Next
+ LDA !current_state
+ BNE .Set
+ LDA !completed_doc_stages
+ CMP #$C5
+ BEQ .BreakMan
+ LDA #$09
+ SEC
+ BCS .Set
+ .EarlyReturn:
+ LDA $14
+ AND #$90
+ RTS
+ .BreakMan:
+ LDA #$12
+ .Set:
+ EOR !current_state
+ STA !current_state
+ LDA #$01
+ STA !rbm_strobe
+ .Next:
+ LDA !rbm_strobe
+ BEQ .EarlyReturn
+ LDA #$00
+ STA !rbm_strobe
+ ; Clear the sprite buffer
+ LDX #$98
+ .Loop:
+ LDA #$00
+ STA $01FF, X
+ DEX
+ STA $01FF, X
+ DEX
+ STA $01FF, X
+ DEX
+ LDA #$F8
+ STA $01FF, X
+ DEX
+ CPX #$00
+ BNE .Loop
+ ; Break Man Sprites
+ LDX #$24
+ .Loop2:
+ LDA #$00
+ STA $02DB, X
+ DEX
+ STA $02DB, X
+ DEX
+ STA $02DB, X
+ DEX
+ LDA #$F8
+ STA $02DB, X
+ DEX
+ CPX #$00
+ BNE .Loop2
+ ; Swap out the tilemap and write sprites
+ LDY #$10
+ LDA $11
+ BMI .B1
+ LDA $FD
+ EOR #$01
+ ASL A
+ ASL A
+ STA $10
+ LDA #$01
+ JSR $E8B4
+ LDA #$00
+ STA $70
+ STA $EE
+ .B3:
+ LDA $10
+ PHA
+ JSR $EF8C
+ PLA
+ STA $10
+ JSR $FF21
+ LDA $70
+ BNE .B3
+ JSR $995C
+ LDX #$03
+ JSR $939E
+ JSR $FF21
+ LDX #$04
+ JSR $939E
+ LDA $FD
+ EOR #$01
+ STA $FD
+ LDY #$00
+ LDA #$7E
+ STA $E9
+ JSR $FF3C
+ .B1:
+ LDX #$00
+ ; palettes
+ .B2:
+ LDA $9C33,Y
+ STA $0600,X
+ LDA $9C23,Y
+ STA $0610,X
+ INY
+ INX
+ CPX #$10
+ BNE .B2
+ LDA #$FF
+ STA $18
+ LDA #$01
+ STA $12
+ LDA #$03
+ STA $13
+ LDA $11
+ JSR $99FA
+ LDA $14
+ AND #$90
+ RTS
+
+InvertSelectTiles:
+ LDY !current_state
+ BNE .DocRobo
+ AND !received_rbm_stages
+ SEC
+ BCS .Compare
+ .DocRobo:
+ AND !received_doc_stages
+ .Compare:
+ BNE .False
+ JMP $996A
+ .False:
+ JMP $99BA
+
+InvertSelectSprites:
+ LDY !current_state
+ BNE .DocRobo
+ AND !received_rbm_stages
+ SEC
+ BCS .Compare
+ .DocRobo:
+ AND !received_doc_stages
+ .Compare:
+ BNE .False
+ JMP $9A58
+ .False:
+ JMP $9A6D
+
+SetStageComplete:
+ CMP #$00
+ BNE .DocRobo
+ LDA !completed_rbm_stages
+ ORA $DEC2, Y
+ STA !completed_rbm_stages
+ SEC
+ BCS .Return
+ .DocRobo:
+ LDA !completed_doc_stages
+ ORA $DEC2, Y
+ STA !completed_doc_stages
+ .Return:
+ RTS
+
+ControllerHook:
+ ; Jump in here too for sfx
+ LDA !sound_effect_strobe
+ BEQ .Next
+ JSR $F89A
+ LDA #$00
+ STA !sound_effect_strobe
+ .Next:
+ LDA !controller_mirror
+ CMP !CONTROLLER_ALL_BUTTON
+ BNE .Continue
+ JMP $CBB1
+ .Continue:
+ LDA !controller_flip
+ AND #$10 ; start
+ JMP $CA77
+
+SetRushMarine:
+ LDA #$01
+ SEC
+ BCS SetRushAcquire
+
+SetRushJet:
+ LDA #$02
+ SEC
+ BCS SetRushAcquire
+
+SetRushAcquire:
+ ORA !acquired_rush
+ STA !acquired_rush
+ RTS
+
+ApplyLastWily:
+ LDA !controller_mirror
+ AND !CONTROLLER_SELECT
+ BEQ .LastWily
+ .Default:
+ LDA #$00
+ SEC
+ BCS .Set
+ .LastWily:
+ LDA !last_wily
+ BEQ .Default
+ SEC
+ SBC #$0C
+ .Set:
+ STA $75 ; wily index
+ LDA #$03
+ STA !current_stage
+ RTS
+
+SetMidDoc:
+ LDA !current_stage
+ SEC
+ SBC #$08
+ ASL
+ TAY
+ LDA #$01
+ .Loop:
+ CPY #$00
+ BEQ .Return
+ DEY
+ ASL
+ SEC
+ BCS .Loop
+ .Return:
+ ORA !doc_robo_kills
+ STA !doc_robo_kills
+ LDA #$00
+ STA $30
+ RTS
+
+SetEndDoc:
+ LDA !current_stage
+ SEC
+ SBC #$08
+ ASL
+ TAY
+ INY
+ LDA #$01
+ .Loop:
+ CPY #$00
+ BEQ .Set
+ DEY
+ ASL
+ SEC
+ BCS .Loop
+ .Set:
+ ORA !doc_robo_kills
+ STA !doc_robo_kills
+ .Return:
+ LDA #$0D
+ STA $30
+ RTS
+
+SetEndWily:
+ LDA !current_wily
+ PHA
+ CLC
+ ADC #$0C
+ STA !last_wily
+ PLA
+ TAX
+ LDA #$01
+ .WLoop:
+ CPX #$00
+ BEQ .WContinue
+ DEX
+ ASL A
+ SEC
+ BCS .WLoop
+ .WContinue:
+ ORA !wily_stage_completion
+ STA !wily_stage_completion
+ INC !current_wily
+ LDA #$9C
+ JMP EndWilyReturn
+
+
+SetBreakMan:
+ LDA #$80
+ ORA !wily_stage_completion
+ STA !wily_stage_completion
+ LDA #$16
+ STA $22
+ RTS
+
+CheckFirstWep:
+ LDA $B4
+ BEQ .SetNone
+ TAY
+ .Loop:
+ LDA $00A2,Y
+ BMI .SetNew
+ INY
+ CPY #$0C
+ BEQ .SetSame
+ BCC .Loop
+ .SetSame:
+ LDA #$80
+ STA $A1
+ JMP $A3A1
+ .SetNew:
+ TYA
+ SEC
+ SBC $B4
+ BCS .Set
+ .SetNone:
+ LDA #$00
+ .Set:
+ STA $A1
+ JMP $A3DE
+
+Wily4Comparison:
+ TYA
+ PHA
+ TXA
+ PHA
+ LDY #$00
+ LDX #$08
+ LDA #$01
+ .Loop:
+ PHA
+ AND $6E
+ BEQ .Skip
+ INY
+ .Skip:
+ PLA
+ ASL
+ DEX
+ BNE .Loop
+ print "Wily 4 Requirement:", hex(realbase())
+ CPY #$08
+ BCC .Return
+ LDA #$FF
+ STA $6E
+ .Return:
+ PLA
+ TAX
+ PLA
+ TAY
+ LDA #$0C
+ STA $EC
+ RTS
+
+; out of space here :(
+
+%org($FDBA, $3F)
+WeaponReceived:
+ TAX
+ LDA $F5
+ PHA
+ LDA #$1E
+ STA $F5
+ JSR $FF6B
+ TXA
+ JSR ShowItemString
+ PLA
+ STA $F5
+ JSR $FF6B
+ RTS
+
+CheckConsumable:
+ STA $0150, Y
+ LDA $0320, X
+ CMP #$64
+ BMI .Return
+ print "Consumables (replace 67): ", hex(realbase())
+ CMP #$6A
+ BPL .Return
+ LDA #$00
+ STA $0300, X
+ JMP $BE49
+ .Return:
+ JMP $BE25
+
+EnergyLink:
+ print "Energylink: ", hex(realbase())
+ LDA #$01
+ BEQ .Return
+ TYA
+ STA !energylink_packet
+ LDA #$49
+ STA $00
+ .Return:
+ LDA $BDEC, Y
+ RTS
+
+; out of room here :(
diff --git a/worlds/mm3/src/patch_mm3base.py b/worlds/mm3/src/patch_mm3base.py
new file mode 100644
index 000000000000..c64c83c3c028
--- /dev/null
+++ b/worlds/mm3/src/patch_mm3base.py
@@ -0,0 +1,8 @@
+import os
+
+os.chdir(os.path.dirname(os.path.realpath(__file__)))
+
+mm3 = bytearray(open("Mega Man 3 (USA).nes", 'rb').read())
+mm3[0x3C010:0x3C010] = [0] * 0x40000
+mm3[0x4] = 0x20 # have to do it here, because we don't this in the basepatch itself
+open("mm3_basepatch.nes", 'wb').write(mm3)
diff --git a/worlds/mm3/test/__init__.py b/worlds/mm3/test/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/worlds/mm3/test/bases.py b/worlds/mm3/test/bases.py
new file mode 100644
index 000000000000..38ea47ab2fb3
--- /dev/null
+++ b/worlds/mm3/test/bases.py
@@ -0,0 +1,5 @@
+from test.bases import WorldTestBase
+
+
+class MM3TestBase(WorldTestBase):
+ game = "Mega Man 3"
diff --git a/worlds/mm3/test/test_weakness.py b/worlds/mm3/test/test_weakness.py
new file mode 100644
index 000000000000..400eab1f4bc0
--- /dev/null
+++ b/worlds/mm3/test/test_weakness.py
@@ -0,0 +1,105 @@
+from math import ceil
+
+from .bases import MM3TestBase
+from ..rules import minimum_weakness_requirement, bosses
+
+
+# Need to figure out how this test should work
+def validate_wily_4(base: MM3TestBase) -> None:
+ world = base.multiworld.worlds[base.player]
+ weapon_damage = world.weapon_damage
+ weapon_costs = {
+ 0: 0,
+ 1: 0.25,
+ 2: 2,
+ 3: 1,
+ 4: 2,
+ 5: 7, # Not really, but we can really only rely on Top for one RBM
+ 6: 0.5,
+ 7: 2,
+ 8: 0.5,
+ }
+ boss_health = {boss: 0x1C for boss in range(8)}
+ weapon_energy = {key: float(0x1C) for key in weapon_costs}
+ weapon_boss = {boss: {weapon: world.weapon_damage[weapon][boss] for weapon in world.weapon_damage}
+ for boss in range(8)}
+ flexibility = {
+ boss: (
+ sum(damage_value > 0 for damage_value in
+ weapon_damages.values()) # Amount of weapons that hit this boss
+ * sum(weapon_damages.values()) # Overall damage that those weapons do
+ )
+ for boss, weapon_damages in weapon_boss.items()
+ }
+ boss_flexibility = sorted(flexibility, key=flexibility.get) # Fast way to sort dict by value
+ used_weapons: dict[int, set[int]] = {i: set() for i in range(8)}
+ for boss in boss_flexibility:
+ boss_damage = weapon_boss[boss]
+ weapon_weight = {weapon: (weapon_energy[weapon] / damage) if damage else 0 for weapon, damage in
+ boss_damage.items() if weapon_energy[weapon] > 0}
+ while boss_health[boss] > 0:
+ if boss_damage[0] > 0:
+ boss_health[boss] = 0 # if we can buster, we should buster
+ continue
+ highest, wp = max(zip(weapon_weight.values(), weapon_weight.keys()))
+ uses = weapon_energy[wp] // weapon_costs[wp]
+ used_weapons[boss].add(wp)
+ if int(uses * boss_damage[wp]) > boss_health[boss]:
+ used = ceil(boss_health[boss] / boss_damage[wp])
+ weapon_energy[wp] -= weapon_costs[wp] * used
+ boss_health[boss] = 0
+ elif highest <= 0:
+ # we are out of weapons that can actually damage the boss
+ base.fail(f"Ran out of weapon energy to damage "
+ f"{next(name for name in bosses if bosses[name] == boss)}\n"
+ f"Seed: {base.multiworld.seed}\n"
+ f"Damage Table: {weapon_damage}")
+ else:
+ # drain the weapon and continue
+ boss_health[boss] -= int(uses * boss_damage[wp])
+ weapon_energy[wp] -= weapon_costs[wp] * uses
+ weapon_weight.pop(wp)
+
+
+class WeaknessTests(MM3TestBase):
+ def test_that_every_boss_has_a_weakness(self) -> None:
+ world = self.multiworld.worlds[self.player]
+ weapon_damage = world.weapon_damage
+ for boss in range(22):
+ if not any(weapon_damage[weapon][boss] >= minimum_weakness_requirement[weapon] for weapon in range(9)):
+ self.fail(f"Boss {boss} generated without weakness! Seed: {self.multiworld.seed}")
+
+ def test_wily_4(self) -> None:
+ validate_wily_4(self)
+
+
+class StrictWeaknessTests(WeaknessTests):
+ options = {
+ "strict_weakness": True,
+ }
+
+
+class RandomWeaknessTests(WeaknessTests):
+ options = {
+ "random_weakness": "randomized"
+ }
+
+
+class ShuffledWeaknessTests(WeaknessTests):
+ options = {
+ "random_weakness": "shuffled"
+ }
+
+
+class RandomStrictWeaknessTests(WeaknessTests):
+ options = {
+ "strict_weakness": True,
+ "random_weakness": "randomized",
+ }
+
+
+class ShuffledStrictWeaknessTests(WeaknessTests):
+ options = {
+ "strict_weakness": True,
+ "random_weakness": "shuffled"
+ }
diff --git a/worlds/mm3/text.py b/worlds/mm3/text.py
new file mode 100644
index 000000000000..337837244c40
--- /dev/null
+++ b/worlds/mm3/text.py
@@ -0,0 +1,63 @@
+from collections import defaultdict
+from typing import DefaultDict
+
+MM3_WEAPON_ENCODING: DefaultDict[str, int] = defaultdict(lambda: 0x25, {
+ '0': 0x00,
+ '1': 0x01,
+ '2': 0x02,
+ '3': 0x03,
+ '4': 0x04,
+ '5': 0x05,
+ '6': 0x06,
+ '7': 0x07,
+ '8': 0x08,
+ '9': 0x09,
+ 'A': 0x0A,
+ 'B': 0x0B,
+ 'C': 0x0C,
+ 'D': 0x0D,
+ 'E': 0x0E,
+ 'F': 0x0F,
+ 'G': 0x10,
+ 'H': 0x11,
+ 'I': 0x12,
+ 'J': 0x13,
+ 'K': 0x14,
+ 'L': 0x15,
+ 'M': 0x16,
+ 'N': 0x17,
+ 'O': 0x18,
+ 'P': 0x19,
+ 'Q': 0x1A,
+ 'R': 0x1B,
+ 'S': 0x1C,
+ 'T': 0x1D,
+ 'U': 0x1E,
+ 'V': 0x1F,
+ 'W': 0x20,
+ 'X': 0x21,
+ 'Y': 0x22,
+ 'Z': 0x23,
+ ' ': 0x25,
+ '.': 0x26,
+ ',': 0x27,
+ '\'': 0x28,
+ '!': 0x29,
+ ':': 0x2B
+})
+
+
+class MM3TextEntry:
+ def __init__(self, text: str = "", y_coords: int = 0xA5, row: int = 0x21):
+ self.target_area: int = row # don't change
+ self.coords: int = y_coords # 0xYX, Y can only be increments of 0x20
+ self.text: str = text
+
+ def resolve(self) -> bytes:
+ data = bytearray()
+ data.append(self.target_area)
+ data.append(self.coords)
+ data.append(12)
+ data.extend([MM3_WEAPON_ENCODING[x] for x in self.text.upper()])
+ data.extend([0x25] * (13 - len(self.text)))
+ return bytes(data)
From 371db533717a1740d7f60224b380a6be21e8498d Mon Sep 17 00:00:00 2001
From: Noa Aarts
Date: Sun, 8 Mar 2026 21:50:34 +0100
Subject: [PATCH 28/84] Stardew Valley: morel doesn't spawn in fall secret
woods (#6003)
---
worlds/stardew_valley/content/vanilla/pelican_town.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/worlds/stardew_valley/content/vanilla/pelican_town.py b/worlds/stardew_valley/content/vanilla/pelican_town.py
index 3c2fb1a2da23..72aa113543b9 100644
--- a/worlds/stardew_valley/content/vanilla/pelican_town.py
+++ b/worlds/stardew_valley/content/vanilla/pelican_town.py
@@ -144,7 +144,7 @@
),
Mushroom.morel: (
Tag(ItemTag.FORAGE),
- ForagingSource(seasons=(Season.spring, Season.fall), regions=(Region.secret_woods,)),
+ ForagingSource(seasons=(Season.spring,), regions=(Region.secret_woods,)),
),
Mushroom.red: (
Tag(ItemTag.FORAGE),
From 44e424362e1f7b731a5d33268fe6ca43b0e6b219 Mon Sep 17 00:00:00 2001
From: Remy Jette
Date: Mon, 9 Mar 2026 00:51:26 -0700
Subject: [PATCH 29/84] Docs: Don't serve non-static files in
example_nginx.conf (#5971)
* Docs: Don't serve non-static files in example_nginx.conf
`try_files` will serve the file as long as it exists, which means I could `GET /autolauncher.py` and be served the file.
* Use root instead of alias, add route for favicon
* Update deploy/example_nginx.conf
---
deploy/example_nginx.conf | 19 ++++++++++---------
1 file changed, 10 insertions(+), 9 deletions(-)
diff --git a/deploy/example_nginx.conf b/deploy/example_nginx.conf
index b0c0e8e5a043..a4f042739c0a 100644
--- a/deploy/example_nginx.conf
+++ b/deploy/example_nginx.conf
@@ -41,16 +41,8 @@ http {
# server_name example.com www.example.com;
keepalive_timeout 5;
-
- # path for static files
- root /app/WebHostLib;
-
+
location / {
- # checks for static file, if not found proxy to app
- try_files $uri @proxy_to_app;
- }
-
- location @proxy_to_app {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
@@ -60,5 +52,14 @@ http {
proxy_pass http://app_server;
}
+
+ location /static/ {
+ root /app/WebHostLib/;
+ autoindex off;
+ }
+
+ location = /favicon.ico {
+ alias /app/WebHostLib/static/static/favicon.ico;
+ access_log off;
}
}
From 123e1f5d95b844e0f2063526cdab18ca26cc10b5 Mon Sep 17 00:00:00 2001
From: Star Rauchenberger
Date: Mon, 9 Mar 2026 09:13:45 -0400
Subject: [PATCH 30/84] Lingo: Fix logic for Near Eight Painting (#6014)
---
worlds/lingo/data/LL1.yaml | 6 +++++-
worlds/lingo/data/generated.dat | Bin 149835 -> 149954 bytes
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml
index 4c41f3236f62..613eedc4b655 100644
--- a/worlds/lingo/data/LL1.yaml
+++ b/worlds/lingo/data/LL1.yaml
@@ -4470,6 +4470,10 @@
panel: SEVEN (1)
- room: Outside The Initiated
panel: SEVEN (2)
+ First Eight:
+ event: True
+ panels:
+ - EIGHT
Nines:
id:
- Count Up Room Area Doors/Door_nine_hider
@@ -4612,7 +4616,7 @@
enter_only: True
orientation: east
required_door:
- door: Eights
+ door: First Eight
progression:
Progressive Number Hunt:
panel_doors:
diff --git a/worlds/lingo/data/generated.dat b/worlds/lingo/data/generated.dat
index f5eb3e069927f30aa069594bd3a98e6caa17e001..dff56b10d846da2142a4bfa267a225f160060160 100644
GIT binary patch
delta 17553
zcmb_@d3;nwwm;QL-%e)*vJ*l&34ySNBrE}B=_H+`q0=2Z9U=%wfM~cvSR;z8DzX?i
zZb&IA5I~lvB8ZBJEILm`2nge-fCz%isH39eo6&KB-&yYM?g+m3&rd%e`qZ~hZKuvT
zb?Vg3u{G8=G;8S6(6ynDhGyDor{)cu+Hb&={sX7>pImTj?tp^a0aI%S6im&_&8@vP
zf6DICp%EP-N~X=3JJ05rc3a)Nru&zmmQ>A*N+O^A8&&9TD*O=^X
zm@&C_j;*Yrex9w5Ex3oq*tT^JXK9(QX+o1)4b9z+W-4nfN0VdB?rHV4bDQ*jzu2??
z{qaJHii7Smv<|5Wd-6YgOo+i+>*=?YQbfCIr%oqmJI^c@m0F*(i^W9k*|Uvenl|{O
z6=I(D`bUez5^cn}m14DaB_cP;kQXHC|4?e$N$iFj?=XDfwG`{!qyMQ^S7
z^Lv#+@ziGI7x7yEFKU%SET(JACdX?pe(|R8Y773p5vG>%Ws6X?179|aJ2c-{Qrx3`
z^VK5dL1fO7KP(r?+ODrdwZ^Zf3aS0@^)jUeYcjaz(SLlX?80)cc1n$thwc;6+9Th5
zDGq6OeS4ocy64KbqfCl90T{Yz_RDd5CVzjsNtEn4@y{tHWd*`nTJHbE?HT>A?}Rw4
z4f&}_v}*7Ev_+iO=KcJt_(IG1&*S1p?VbM|Hiac>5BzdKq-xGz4~SgN^4sHLnAY-J
zi&BOI!A?b?ZU_@DOCvl(K(w*TrFW#y`s
z6PmKMWmjX@=8FN!z1T;tw(O;pwX4J`@RzBzI%Vv=v<0D1P-ld^pkWj*spjD0}_&u|DLv)tSiugd}?ww%~
z%Y+!S_i~tcKqw24ucvk~v6GCpipafN!-Y$T^%81(?oEvp6NUJTTo@&O6{qE$(V|{l
zl7EjD4dPolAx69?ev=*Bi>E`wlH0YDpR^ZQd*PquBJ>6ty;GksMa-b^07w;<`$EIh7tw7DCZER`&JyDDP~lpMSwbAuLE;D^-L8ojz5>$x*2^
zftpk?2B6v7MY=qbO8Qi@8EB=8Xs^#n=5!Gq4SHM`k~s0KW*+MLKo=1MJhKKs+wZ#2
zdVcOAZZM!RY1CcrJQ#W8Y7r|RN~5u;?Q3bG12Eb*i9~tjpy()nN~1vlNS0|fVS`Fz
z8^_s17bxD?Rdj~MHFL?o-ZT=-B~_#Z3r4AS*u1Q1db{*=>Qpb?k}k6JK2xa!kNQTN
z4nloZUbI)1;wR}OMZIxY1{q|Z400LIWr*BxrOe}Uv7tb@0J{e1TS7`5F6<;GlUU(r
zsC+n+Y>%w{oVuF8iItMMRK|KQ)X$SZro*>JmzN>#lHfoHF
zsw22@W2oEdCR+sQb&@A4M7-G6oJH->aOH?BVS(wOZOTbmA`EDn<~>;=325EQ_GZxn
zvBcB-2}fGeW~8V}kIPeO%PyQ=Gv#)2QOF)a6JvHct{a&Knt*(yTR`3RcMGUt`0FA%
z%&=f>);gbbcL&GspUSP-WblE$JBPqjK9;+(NoDj2|07#u>uq=jJ&@~1Ii!bSoaj8}
z^&mlDn@F1H5ahC^%GY~PXNEfd(!)O{v>MV=qy^@4M^9o!lZXs+IZAwnH$%i~_EJ~{
z`k+rNtQYBIk6vUo;4-?&hi(;d&}~aEkq+JZAy(2$YF%oOGBk#kIg4pOq5BL=v3(pB
zz)&+{>S|C)%x;g@SB4rmueVqM{r=XQR4l!Z=)qmqs86-+vc&17UT*6ndb4f)kwvL?
zx_u7vaj+A!WsO7GD{-a9A-_oXjDg
z19Yamnj_30Q)a3vGz6C}J^g5;!>_=oclINvrf<-eek3mH>g#?+SFpDzcw2TpX*Slz
zM3mbrK`3Kw*|`LQlXb|)a;Y<-zs(hqF#57Qk*y!vL~<8I_NRtGqVpzXpn+tY2l+|a
zFP{!_;26p6Bg=n*eh`Lc#gj0c>BxfZh?j#0lC#9_W&J>7HmLXB(f&C*#;{kU)|a{Ug&`tErVoP1
zkz~;cV}mCL(cmD&PFZyLtKp$w?sujI+ZUb8mAZ|k%oY700CL`hV_`e&rn-P+hkGG6
z6wo1I&`MyN)5AhboMTAgsMa=^W)JhqleY{epG!30VR|bl4>q$ZVO5mXQE4yoI;z=V
z5DE)*RJjUqGSv3u5D^~KKtdVgB&Zw4KY7wKG%zgCK||%r)}e41tdp09iaGG}Xf5cL
z-Inc}Ka=zh!Uvn&cmp*j`uGiG3`GBjX`@Z$v^%K{m7TV1PkpVwEEf)=I04&5S)tNb
zH>Z}XsjI8QgxNB)VNPIBKrrCN&SdZ5Z5vnC-cei6tdEIK;N2Dv7rDSpU7Z|G>jyf$
zc?8+_?x&!S>=B|vTQe|X!txQM5$Pi-%+N-Vrvf!fkE&}x{SNUuC_?*e1bJ)ZpX<<`UJYuf%g66Ws@aq{h=CzlfB_zV#t
zm)L17{+P!~-WFpX`L>-#-R&X)gcj1-geCUKOVyEbR3WSgqyiJ>$bcto3)b{&BLgF@
zn$YH4SHP^x*-68}jYkv(3%!fT!09Y6FCwQGT`UIbx=bWHIwIN80XYQP!%wfUP_M^B
z?gC1Q^7M8H2%jz{Yu{HaZUn9f2W>}Rhp2)QIn;L?fjH`W2ZfV_mzGdNqBjQ7{|TbK
zr2(F2N~tZ*Jto3e=CHY(gNd1qv;Ub{R9{P#YMyE4tG(%AX)Ok_qmvvO`z2!`iXUWe|JHK
z0R~6)v{pL6b^7m_@)tKbGCEW`d*J2>8l?|h+I1+OO?CGGY0H6aAgej+{C(SEcxV(WXGhlqrJkaA0p{qNZRO8)B>+R^og)k
zcp+iHff(@gl;^yp=X9;T0_~v@hazJN~w<(X2|tf~Opx+*^#3|&-ack<<)r&3l`Zl^04whLwukcjT@=#G_Yc-8ct-h`1BNlT|We&Jq;zFotB8T0@>jEg*TH&g4ml_Rp|DPlu8f#n}ljKgOF#*Dw4@nF8^H@5A3|Ejd
zPT2DP#GBQfYBU!*N}cW?rS*qh<3tXe^Sj4UZ(v7!i~SS(zzZ08aTKBd?nMHryqTOn
zMGDIUv?XL58wXXKznP?lbd=fHl!$aIK_f=tw
zz?7p)zs$wNuz`0=}jX|s~ySo@}CuB6xPcN7_;_|>mQf`23k!}pi
z%k6yiqyEZ0ReW>Ap&{ogw?rS#=T+kBnoLaP7-!l%Je$XRjywy!en#}
zDPcklRr_kl9sZz#j{Fy--1G!mV<>E)Cs?)gTMO6>CKO91gE!nJyG^ERJ(dh5-XmvD
z7RkVJYBKouZSpz%;gZ49iY)PF4t0<%P+^}!SFtfu;F89x2CG;)g>U1SoZB~rG=u`(
z@21f1q2j@P$=C=e;@L!TwPP$?KRHo~T$T_gD-ssu{;AYFz{nG#y7qjl20Gm`3dBtI
z0R`i_jZEQ6i5&tV(sbwqV^L~*uGW76u$n#K7)V{It7^N8JP#xwLiU;F2mdc2{C^B6jJdZBDB4_i`2Z-$@1IwY-)}&F+y$_uKJs0a!+up1
zg24LUE=HhwjD&1MPmcQK|IOI~Nlg*M2l{wk1psra$z+daSX^RYl
zj8n0ZA;-fO4r9MNz_R>ix^Ylv$mAI!>iXg7kQo9WTF6(;njw_7T<};!&oQfKh|brq
zd3A^zgK8m??{
z@}`+|DnVx6CRfj-paJLFA&LW7+gJ<({0}hl?99LoNX){+W^d9gvBK1Lih9s5_spUE
zCb)BY9Qkn$jSE=~4D)8s6RC36T)L4##=d7vb{J=M3@b&g{SyVPaOkHPG(=^9zQ*&9
zN2WH=UPDc`bT^P&g6gg^dX5N}_cy?4PT_ElVaIJ9K&Vd-G_gH|g7CmaUMBwzcoJME
zm);?&;Rh;vO4Iy6yw!I;DHY+9=2JEq5|wj-_4E1SF#K%26T}5t+Xu8Z=Pv3CoPte0
zc^5Imk&`Y@+(m{7;_A|VuG-0p;%E$kb4b_9v<`FC-i(GqoJmPHYFN8~%m5q8#~09s
z65hIiIwuEzWdVr-?x>~Gb2n`?ODAnmoHuBt{yV2RFxTO?(w6pI1y~?TMoid(#(*1tt^*o*b_&;Vq&g
zztBOk37`LNe)t2fbW4ec;{>dchnD*DPLD1V1LRXn@%|+ZIs-CS?jkRoG#o_cEu%CX
zRM#+)a1DAoihUT$0s>65QkktTr+=&x_K)pVUO0KD`X+_4t8
z_)~ajAQiVl5FPSP59To}P<4o0&MviJc~ytD^JLUSWbWAOuqDaOf3jWCy;
z50Im#P7tt~tL=C}gK_ps7v5L6p$1%Kdh(Zr4FSouB-%Tl3BCqB+V
zZ+ZOFTP}W(VtTrhzWbnvw?Je}5w?No1N64|^ynH|_p)L-h2v4;qucDlhiD|Sf}IbM
z;h>>g)Sm4j9m5vY{4fo77@rn;JWP88`HuuS=3#PIz@H%-A2!-%;G+mWgW!sny$msr
zy||c#L!2>>kO^Ro9PtQAmbg5SSfoe}xj<|97EEjhnGGw#Lg7Y1(&Cb@(oS+GzG06B
zx`Rtck-t0mk^uA$C&aBF+fHtQOO^V2LCc%4koIfXW38cskBnj|yhsOSEAL)IHc39}
zxizBW^-rqz))+BonoQNm5x{8aa*RgGMH#h~emY(5*GMchhp#om`=-gXCiJX(`!+$?
z2m4PM!AI7Tu<=9xML7mTn4IBDbMpKkDVa!Jb1Y
zv^!l6&LILHCyJR*E^DSKV*57uYt0m$AWkAv&quUtC*hNpSlM2Zs$mr!!c?Uu=L8RF
zVnqpbi_KN|C_w&UI33?F>57B=5Qh9)vdsM>e1olvf=5vM>Us@4oJhWwr!lm$Og|!l
zifgh7nM;y}g({UY&o>lW=xz
zlgd++??j8k<5sa&
zcKT@K!e%gw;~U9o1%Rs?X<(W}&lWoHkdAD$z!zOU4=42zf$U%)DGWen`v)W08Cu@p-x7(lAgje7IGYHk^^;8vd`ViTp8{1|k0n3yLTS-r_9yD~clQTy+
z(OFyltA?6VerrZP`R3L@So`Z%Isg!d8-Ve=J!~6&P=hz6$gpCM$N$+F-JMQsqa`67
zuE5SXIl9pw$x^qI%|LCop6dn@-`ok|TnUl>tr4hd(1Mfm!Qij!&t7sK@tGgSG_sO>Kkv
zrgBi<-~F*|(3*&MFwM)pQ*>8y`vuYbPR%&a1?lGJ=z9x>d5KK(6qx4WnPzD+%|kQo
zA2!?UA64Hkn|x>|Y0=|5!Bn=%)}3@DT;3^i`H1=q%vyEk2OpFWQ?gl(d|vd<#4L__
z9WD$E@q-R>YN?_?zWqGq8;w_|tX=%wlviX%@&^R>Vi>SkF4)CMJulq<4(uXD#iN{2UuEhf-~u9bL6G(ivpqf$D$1P*E!d}KAa?CDczL49N?O&pGQgg_1SNNxgnQ#VpFqm(;eE21Hdh4O(D=!6N
z?$2K`Ldgu7_A>RzL9C;+)Ch|yAMrAULUi1oc$uV+G@Wu|_@i2iy2ro;!Dt5&f}Vk;
zs!HEjc8l215(3{;;cB_Zy+I?xCyM?aBEw*B%Z;oq1+x{ei1ybHk>|m82_P!`76udx
z-U>*Q4!=S#fpkS_zmL=vPw{ZYbJN(;pofho_xaCm*}9L;XWH50`-G+WRrYka(^%HC
z*&us=9(duaBoyu%6_V4)fs0{mRjUl`xw(91@N
zx5`O&h^@nwa(j6>Kbw)_74ow=YGJxu@*ue|P%8}W=xkbefTl(C_dXG7=5MEsD{x2b
z(5J_qeo%%Vq*ior!}qSqx(heT9V0AmN0raZ^Dxk4{opXl9p%RdNmvGe$xMUp2q}3Z
zZX_YkJmgnh`NSa_9u+XcQs(j5ykq^BIQj2Gqz$B<*@px4xWoQNa^+z<(nu@!9rmZ%
zz`2w`=HC%`5=@f%YbRdeWzOqj6wHz6M_#9(-LDezZ?99ZZEUK?>*gpK+z4I`U=bOX
zQg0P~;$-v?2Ln3b{n_wt^1vHp?0DWmq>Z2Ilc|x`wEqj$CI5x20xtrPm-ZBr<{(3q
zwE|Sv1`8nPh60F*06THoT{c)2J-8x>53WU{Z7xr#ABu^vk8#@FHvg9&z?T_S0-pqU
zZN8xUa)+oAP*(U5wy3UV8h@)45WuVd?KizhosHcXPE|&{Nwu_+f&d+5)mixz#p;*{
zm%YN_vQ>g;C=UtSWEb1zr*Dc8$_IG9O_bShiQdX1oroUymguTH!SsD^i6Z4`rjH}O
zi|L=<5*f;?OsBmqdMHisc{KFnep}d;4NR|nTVyGFnLhkB1O@mBHSm1&c4)j7wLVf792Gs4p3Jo2C}gph9~E)R06&ncRQTys`QuTMtl$@~Fl@(T
zFy&!gh#qkadO6PY%wr-i0zU*)C*n}qnr(Zsyz97VFW)~VY>K2B`Rg$dD1HY7J@si7
z-ITLTk8Ks*Bi<#NCwg&LXDw&9Sw7n;5+k~#8{PKRP7O|$e{Y3H;E@E%=;LCDf?x4K
zkM855OnHRqStlTGw(Gd)qdXY|{Di=E26~jp^i*u6<7ofLf|b`p(C?5?%c$IG23
zL9+Xp;rK~0&{6h|n-~%Jj!~ZH0m*7hRZ(ullk!Y)^
zsjresHHkA!^UoC2)cSBVn-RX&c
z{U*QvD{S&h*)+Gr_rPY}V!H4>F-SSd^pf|$9?mj-;5|@;&>qD9JJSDVeAwTB{*viM
ze-nkuD&~0;>BUS(y)SYi%6l7g%+wzLHbqW;UnDAbK!OT{v*dj-Sixi!&1pd+TvKLBZayQr%X6niv|N80-sU}L;48+jPK&IF%7$t4=GyFYYHOP0
z>@rh|+*M{ul;*3Zb$29VVFkk;a
z@?l*l*A2K{otf(f{;^IDKMVUu{cn}EXF+^Az2z+UNTRpP*0Un%Hl}eL6ZI8Y0^hRE
ztFx6%pE+}mKHcqsDZ*cB-g>=So@4~5?e%KS_iH1ckBbS6rQld{($65rv)xTG78c76
zx#1(xpQpD&p7}`R8FYtp@OOUHe1~+M6ZsKL4EmMHG**S*D){Urk+ZmhSV0*jq^KMTEEf?_#HF){N_jy+xin4`&c{v`xle
z05`AGLodL=pwqWs03Sp2)AFGUqA%~k)Bb_wcOy(TeIbN~G=B{1IN9A49<6`U6tp*8
zqs{QaCC16rCGAm$tS>Yv{vCv!%97vxEG&Km_A9dkzd~}WKknN3LQ}Y(8@4S=j<_ho
zdDc55>|JkzhHdL*(6D2fa&b{m(=RTHJ}w$!3l71y2d^OTrK>(+!dcJtzf=pbv!MA?
z8(!WPdCMh{I3>WsO3u&A0hDdftbh0sz)8pTze5fl-$Hh#=j@nWp1CAacwwYpx_;gp|{XUfWnr%E*ueny2IQQFN_lXUh@2w4GAQ;-TrsE=fRQ1HL=rNTkzt5r5;B~Tk%)99#7;=I
zT7*y*Kx%OulHHJWAk`hI5?SBhWE)XR+^Q4t&D4GrBQ8Yx5>n2H2ay~?DhSC|z4CZ}
z(=e}(8O9((e`2U+WGo_s2pLC6wmKf60)W&BNDTq1L7k{qPC{rXK-z^qS=LdTsIq3b
z+1QbovpjNFgN7(!MsvI>!ELRK$fs1c!?2zro_hY=Y|$RmubL1Y{u8Y62FxtWk=
zMjk_CJRy%W@&qCi2zio_Y;`?C69H<*Uy$Dyn0nsuA|ms!?j@ueka`)ZJCNFo
z)O@5~LF!JR8q|G2!eghpUyd1UvMqUyit0gRxEmPes)uxj*AZHXt=~Xu5w-pcRjO|y
zvY6Q4V&n)SO9**~kzg-Azv}_4@4dy
zm9@i{8Kv)tRAFwFP8Kc0SH`qZyZEw}2_
z*{V3AS&u(#4QUBkudEBnuouoOEG){)n>O&KfinxI=M5~HHluLR;QWEbgBtQ?<`oRu
zQ!&(LR-ChEH7;t}yV{mQ)y0AnvYp+~*NVI4pK7CAS
z#Woq*o{MqXZ=c?;Y{X)=HswmJw&mg`v19L`OZN(KK-+(Li8!hazVe9pqju)Xo#KM#
z{%pJWv-aa>8^wQXD?Z<){LUTi{yb5u`(lN#X_vov2nc9_`6SZaEPFExr)2V3r?qjiY@8$2h3h}b`*YEFF4q;uEwtGSB-Zeku
znM}XL?mhh1`%KEPIKX>q4S$Q=Z
zi>H+bu#XHG6>UnCPYLm)*d%LB;vMBV!nK8W6P&F5eAOl=28+eYVXVm1rYuQ9{&4ww
zus9*k?>iA98ieU;yv$R?1<_4Ln8im*b^`LsbzvfU->+sdTPRhC^p=P6Oi6NixUlV8
zYlU{o0)%^YfKzOuS}1J+?tQ=w?e`;+Ayd1EIN7C(xGYY}|LP*mLP0;t?Zv56s8lU+fOIRc?lxI
z-~rkiTHtRVrj$9oPLI#u--QGbX7K*rp^wx=qwfU8%V{-Lx&nh#x$BLBoS7uLzzmy{L^}LT
zsDw5Z*dWyHDRX*=Ek`7ac6mNYWB|vW7Lj0rrA8S+gq
zgYS7=q+5&g1{Dk_#NygNhG1fdOf&j9OC-RI4)ZbV*HaYfC7#hwI99V<)stp~T%GXKI*#II
z&i0F2RD{6W1p!1Yq81#(H&Lttxr#3BD
zV%_vHGg1|R-yE`2z*38%#xf?U6+$f91@t@
z`}U@@MfAaYg(WcSrM*eaw)Gan0E9C>1UgE9^PrcyleB%NPVU}mqbglxv{BF_eh!?&
zGB90FNu*=4Nw0N*8sd7g@l7u(kgQDy4mA&
zkfNiN+1j5JUDQC4uITC=aciif$~Bfhw@_qAd!7h|uCXfI;`Mo)Ua!;WB;D-wIY{HL
zL)J%`m3l07kn)9QcKLmth=MNkol4H9rJ^~k&KG9g7#d|XrbNGBr(*;VFl)>hed|D%E`Jx6dFUs
z%F;p+-PsmBw4AM!S}1zrbPGu=;3qTGNva<|Aj0aZu66ia&KgtEnL^Oe=74Anhd3u+WZjLj
zSt=daun;(~^LlhVG-O%5`oX!gNDS)G!O0?8x3prB9m=j0kZ7c#%QQK!SeOkua)_|#
zEHIM-OGrso9iO0LF&vl>ll|U0!<0%8CM+N9`3$!esed{$%y|Zr%
zG(E}ehewMfP+I>OF7k~jRE(h2!Rc5+Fp9)=+cOh}*g8UV>n#3;ul&{sR)^cbnSXvH
z*wJM%Ny;|g;9^@;i4t>4zB!U~@0F3FFU(}YesDS>SAf$|gM+uhv)_Ufv*sUen2A7o!k<3>d-Bb!%KJu^F<=c
z_MIhUW3E8aO0wP9q7IUWOTfc{%`(1$ZTfYl)5)f@%Kl9C$}y$n1(Ju?QcCKE4ZFA2
z2>n21gx
z5Ba9yeyA~_M*f%&PTqS?5(`wkpfG$6m#5U>r9(%~$ELgCNU^p5ft3gIP1
z3v8Y{1&@IyvSE~nbN}DFZC3ZgP)picLrcJ~$1l51_wwi{l4{+KMvSIV)^Lu-a8W-1
z8%NX8)DQXvE`i^Yj53~HMaB)JnTm6`i0T|`U#_BlVWFw`3)kX-z}nJkkq6%3qH1x3
zyg_s#-ENZTb>l>0cxjaz0xB2F3kGy@y_?RTG5Gs#vV;Dk)JYvA*OE^MXEhI$$(&je
z2P_#|IaxYX5{FL5*>KxBM;V>YqQT6GNs~{+!zpNZLS)L}J_=osHiH{R-2qd&C8TnEEtQ}uL%!i7
zb4qOWPIk+ft)xRq4y&V0$V0$Vm@>M?r6&upEDzREhs;^+;9!?ItKFUow$C*5
zh_S}d8J5brY6l%z
zXaI6Uu^eszchgznW{pCbkVPRdukTeytwR_=#qZn_X}iGSN&a
z3()=kWCM%&S@octeTi5cSNU@zw!ppYh)Or%dbpzUN_PQ!aVz4u5olFOg
z+SJvQ>5&XoO_@v+K^jh0otN_hShP_#(8v7~?}Y$wgcZz?*HNk;JgkOPh|5!3<>0uS
z8cd!-VG_}6ZsO1iIAJLqloF$vRyt}(7=bHQUY$az6;g)~w%dE#;V%k`cxvAm?Snc=*;eem<4V0`|)j;6!2GU-z?}^erja}ct%zDn?fbw_x^bWp|ZG82E+V!>#0
zPjE|p!{h%9aS(|F*5Ha77=uEFW)2--76yL)VBd?
zONh(E>0YVMM$Ak#S7)%2gc_T<
z9Mor}o|!|hztmmhgNzQV6k>0gN%DfUC8Qp*Y$yfbRHLtp$%lYdOPeL4JG)|$6GXgG
zUoNN260x1@(^QdQ)cdXCE3=Fxzc%IOH-`V5Wh^;5ruIgX4+`I>Hd3Uo`waIq(q@2y
zP7Y&7_#5?bwv3uBqV>H1;mg#0WXPDv$k`My=FS$GsEw2VF;s1+ckVfJMYmw4%0-d5)Qt%Nslaj&1rS%H$D%ajDLS^~QUpd%5()tJVr8_g{IQQiUmvNV2_JJ1(Ge>T$&Hr
z*{%(Ki;SibQb?qY$hfYVE4p0Ya_pJQNjOmcNx`fHJ9?dv;-FMK+|FE+iZ^QV=Usb?
zV$|F;45#C4RhXk&=8-YboAL`toqEYqAIWj
zr!J<-l*JSj1}3(6vEg?mnrq#<$LIgx4;PDWW_=GS3u(Pgc%b8nw~0gW11Zd8S#-Oo
z0~*qvH^C1^{ZUQorxprjLQ3k`AA=}7O*-!|%zvu9^A1X#f+Fl8pSgpUm3S`PLGvLa
z8hIxP0OHW)%~tF(rxh-{D0NRKV6&5yJzTBe94O`Rk2?cBXWm6U(t{GDl#uXt%+LZegLF-t%Z`(=6I*YVo6iHf%Em~G$2CE^Yf42`qt=q01C
zfF3S+GF+%}lCyxE7O;NotV0!Af~W`J%3{sEB2wOQ54hK-THx|xD
zi8Jv2a^k(@78$_cPW}CQF9lHc`^3|LAG=S~^Ur8G_kJ?3_uMbma0|@uK_?Z`b+3rF
zgczC%+VqiEAE0y{F!x2et87>%#sNRt28;-
ziA!aU@f}~wNw^wXMUEw;TK5X*R2HuSFQC<2=5&?niBmre-&XG!
z%WD8FamL(1iKB$V7C)yIuRcp?rofRaQ&zDPf(IK0z#Bj#tc;e+SCMxMjB)bPDiLR{
zuH&G_Kv0$V*`aH2nJHo15vz2uXJfR9ldD&AXsUM`D-W#>EdJ%yGzs$LYaMXiL90a?
zpmt4%SYxz{V-(DoZ(Tz>4oq?K#2S*_D{Ck$hMx?Xx0bqJw@zeO9A)|~G%%+}SgYu!
z8}3F?nt&loakF<%%qwZOK2w3qIvfmWd}~0>CqK;1Q57zGnD(E-RKHi+DW8$4+m=rGjAnJ5jTCEeIrmSR
zDpUJU+1XJV&5m?fn@0MN*7$_Rnzap<^>;r_iU?;nwGGWEUh&%H#B5Y9`<`e6ClN5r
z2!g??ba?pwbOYH>cuSFJHk@1Cd0e!C(!f}vKtY`j4_DML`*l3#N{ItfoWJwW1XYaNR)US
zr9RT7ojt^d87(B*sWl(?qU1w_jz
z4?@Q7K%Ud7mour>(cU5*rj&c&HkI50EL%b#^rt0<624i%k;0S`IHr2!Pi1c@T!K^N
zO*1+9cwqFOAE(iwdA!6=3sb6`KE8eD2QUEQt1)W0Wjw6|(l+y0m%GYvx&Z(?UdI_*
zs7{mJpI|9|f)*Th0TGUe_k*uc@uAlXyH4hn5lX_c#*t}AJ{yq$p+o3zsyI!&(J
z$-)FZ$TxR(>tuDXo_4c9+fOG}LWs1h8}
z&!6PT;92;Xq^D>i5Q>45i=iby1;etO{}e4I&~!P__7pja^uTHD({QcIm%IM5jC`8h
zT0fxsf`KyM;TvVTAjQ{G)EQso8+B&=Ha}42EB$+(5xw+riR2^#kmx=W!ebCQkgi6F
z%WplQm%cLCQLA=i-5$NYC^aAlFN5A8P
zZ!ump5b0lyoKPN$Q2#pQ1eO3Fpi!lNq->?YKUB87^gQKuaMR!soW~8%0O%Y=z5p>H
z-0HkQ_Il0!7vW%rMhxg^BST8`TfluUh(2s}m`ncf0(lo;1B&GE-ISO#ZjZL?
zCSMOOJu<@isla{c2`rL7?xyq-RHVwnJ#^mntG%+iDpKHhQ6k&-kYk5c;g&L2joa&B
z#fpeI*zF+oiL7A?+z8a__mog6YhNVU#5zmJcwHYu3B!6&zUb#eC+RYLFX=kc$M^sY
z)izu!;*%;&yaA)gc-w>m_2rDW-G?L`ZAS6>?q7zH>sF(b0#6e%Vi3L-BP79cs)o%c(EZfyd_7P@fAfjmXGgtLI-1^aiw9
zUVfP*5_xUr3YW)G&M!y+NR@qG>Eus`hg%>htv0fA=(3uPED{x*6%~dJ$&lZ^LLWzd
zuWsbg3%8bg>RfDUs0a#Xtp3mhJ>;$X*h2=h>CerQKR%w*^X!4c3n~008Wap_xlxb{
zUZqc`T=?c!NhOgxd-PQib^UbDx35xm39sX1zx^B_U@TF;pS~R;7z$#+UWO^|apNmE
z^-lJ4|9=0yJCxJpg4g`YjJ9t6Yk}>aZ|LuT(R?si00JI7%
z2dI0R=+g%%JwTTf9(W?^7Z+%Y+7+4L*%59WRzXaL=YrTv_?{aJ_{a%4L5@7Ir&Szq
z!dR$FoMZK~fMz9-NTV3K_8?6KXnc-9b{5{0ChGdxD0h?J9;6L50DOUM(B0rV&Eu}+
zLtuc3`hpwfZt}pJenkPhS?hC^lDwk0*kIDNmHInylrlN;kRdLP=%+CTmWd-3)Kp|4W|3D(fOg24le
zkrN*J(qmA(UW7CNeE^AY*jqGF`k11(jE_l|v)>|7CVV5~;3#IuV;%6H-{P6C6ghJ8
z+kT-V347sf5P$vR8!tks-hZ=_CU?J00yY(Nj6a^r`oupSPL*?R{~B
z5|c(eBTtACwn0SWbbHEtczG%xJ0ZF&_&^4FdHaN@RF*LveG*puVWw|HdMnf0Pl}Ps
zGfaPt_$y5J`v7+R2-8g;0Dh9`*FJ!q{)Fk^Q-FWPH1rcAC!c~(_!|RjvC;`?M3~6O
zr$j&74x(|Y*_z|SSebQNB-&o^gP8yty)ftzP2Kwv6Wpr`CfuNby~#Q
z*25JiZ&H?)-#tz{bjmCv{wTWIT>gqotdKYSQN%9AOH}3#gI9ZY7A9Sm@D`No!s&fr
z=h}lLxj4A39G(hxs%&oj4D#RfE}1*9;Y+_xEGk|KsNG4L!r;|k=Sobm!dL$AN0Fdl
zd<%5=8K5!R2fEi8Sc${gL|2^=!<1)ph+cgLj>`e2_n#4im7`4m^9=ZUe?0?ErB4?A
zNeof`#rPtmzhn9(q%Si4_dkgeWoa(;IqXBA7cqVRhoZl&v=8F=;Tc-w56SY}ha%oK
z2a-h$W@%Bk#>lQ8f$NGHBnIu;lz$~k??^
zzh!3g9NG7r=q_8%L4cTX4*1_VC%VXA&%%q27MCemUO6W!inkO61&tP{l3J|=c5m~%
z#SOPCXj7j6Sfx4>*A2L~PBwfZ;)VzKLYQyhwS3rrDAx_TwobnM3B1jqJ|2-5J`u$R
zoqQftF5&I+`->n?I^A#{RFh7(oDbkzJJ1KtizI{R!g*0>BmPbFopR5okm-&5Sy*LD
zj;X7ezZ3p#+XYZ(%qM@n4EH9_T@aB5(Eke9En98}_Q_X(WoSoa9U>Dy6_FiAGvjEn
za&4|D(Ld0xPeq~8Z|-N%uk~VJpdJ^EuiGpaeh!9g@<0bfCDE&Zaa^%r+_@$d
z{}l#2Bec{&D$Y)MVq$re78P0K!c0?qjE!y>?U>htdnsm-#g|1dUKL^w=2cl!Vu}ja
zm%sDYMEVyCb|qc@*JWc3HsRLv<%w>Ruq(L+4g2B8VJG_k`zBnzBDxveum_p)RH;ev
z4+J~WaBndrNeroPetb*=C`lhSYjoR5Y6;v@jmdbNh!==8uVu>fqvHDYB8|Dufk
z9Ymm#PfjpAV=$Uj;K+PvLcRQaKESTfVG7k#`~lodxSo4uSk`_nlKI0(q6Qc=2$NkN
z`&>jB07#U+0-I&*7h>q}#?y@vY6=F`3HT=y{xr^RoT2td;*G|Ijb~LmR#4m>gW#UV
z6^#{*lhn~r*thx%u_AeWn(21y`W#mf{rB=_)9L)E(BX9Ds+!?UN>lznd)rRP>sMk3m{Eu7eaE>NQA5asZmIUA=ORh3^1jJM8IBKLOU_}gdlgLOiBC&*|5R#*&A{2++^*|~fDLYaLNTnf_gj70G$w+0$
z9)+d>OEUpj(x&!A+zxn?n$1WqB58#5Vx$iu>4fxSBoC2rLh>0IfJg=*g^UbBB$JTA
zgyg8j2xS4J4v9gsCz3;v%0_CK{IbwwuegD@)e(q4PVGlBQi4c7LP{BNBGR9bazb*{
zN`&$N(iVcFG^30cO-Lx|i+$Z|$jA~J!HRz_AM
zGLeuqjI2Xs5+M&0lB2dEG#Q}Q@Df>7WXh>pk4OU`>IS5yAtjNTj?_k^ZUU-V-2@~|
z_Yp*90MevxAtXoLiqK4eaA}i9&BBI{BGyO^x9Mz;$sdYLMHP=@%^c*}fz({2oRF_kk=ljStw=qG
z)MBKbN9s19n$;J8L??KUJW*`2FMW}U>PyIQConXrFY65Z5V{LnzlzinYQ3K-)z=ZZ
zo7fL9auAWFguKbfVMJO8Il{L-B;+$fa?~$m
z-cXZWe2E9PF6V1xco-O()jxBSZ~5m3{`rxA{w_}rHFaP56PJF5pHb_n*MBkcD>6MKx)tkpf(~UklF-PvuXkoWHAJh&D1cI5i=r>5Mp5@43RB>WHhNZh9VGZ
TC#EipMDtJVzE#6a10((yk0o_E
From 0b6ba103c5d8aa0323d8df9d1a679cda9aa57eda Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Bolduc?=
<16137441+Jouramie@users.noreply.github.com>
Date: Mon, 9 Mar 2026 23:56:05 -0400
Subject: [PATCH 31/84] The Messenger: Universal Tracker support (#5344)
---
worlds/messenger/__init__.py | 27 ++++++++++++++++--
worlds/messenger/subclasses.py | 3 +-
worlds/messenger/transitions.py | 28 +++++++++++-------
worlds/messenger/universal_tracker.py | 41 +++++++++++++++++++++++++++
4 files changed, 83 insertions(+), 16 deletions(-)
create mode 100644 worlds/messenger/universal_tracker.py
diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py
index 88a0cec2caad..d1b672db7d8c 100644
--- a/worlds/messenger/__init__.py
+++ b/worlds/messenger/__init__.py
@@ -1,7 +1,8 @@
import logging
from typing import Any, ClassVar, TextIO
-from BaseClasses import CollectionState, Entrance, EntranceType, Item, ItemClassification, MultiWorld, Tutorial
+from BaseClasses import CollectionState, Entrance, EntranceType, Item, ItemClassification, MultiWorld, Tutorial, \
+ PlandoOptions
from Options import Accessibility
from Utils import output_path
from settings import FilePath, Group
@@ -18,6 +19,7 @@
from .shop import FIGURINES, PROG_SHOP_ITEMS, SHOP_ITEMS, USEFUL_SHOP_ITEMS, shuffle_shop_prices
from .subclasses import MessengerItem, MessengerRegion, MessengerShopLocation
from .transitions import disconnect_entrances, shuffle_transitions
+from .universal_tracker import reverse_portal_exits_into_portal_plando, reverse_transitions_into_plando_connections
components.append(
Component(
@@ -151,6 +153,10 @@ class MessengerWorld(World):
reachable_locs: bool = False
filler: dict[str, int]
+ @staticmethod
+ def interpret_slot_data(slot_data: dict[str, Any]) -> dict[str, Any]:
+ return slot_data
+
def generate_early(self) -> None:
if self.options.goal == Goal.option_power_seal_hunt:
self.total_seals = self.options.total_seals.value
@@ -188,6 +194,11 @@ def generate_early(self) -> None:
self.spoiler_portal_mapping = {}
self.transitions = []
+ if hasattr(self.multiworld, "re_gen_passthrough"):
+ slot_data = self.multiworld.re_gen_passthrough.get(self.game)
+ if slot_data:
+ self.starting_portals = slot_data["starting_portals"]
+
def create_regions(self) -> None:
# MessengerRegion adds itself to the multiworld
# create simple regions
@@ -279,6 +290,16 @@ def set_rules(self) -> None:
def connect_entrances(self) -> None:
if self.options.shuffle_transitions:
disconnect_entrances(self)
+ keep_entrance_logic = False
+
+ if hasattr(self.multiworld, "re_gen_passthrough"):
+ slot_data = self.multiworld.re_gen_passthrough.get(self.game)
+ if slot_data:
+ self.multiworld.plando_options |= PlandoOptions.connections
+ self.options.portal_plando.value = reverse_portal_exits_into_portal_plando(slot_data["portal_exits"])
+ self.options.plando_connections.value = reverse_transitions_into_plando_connections(slot_data["transitions"])
+ keep_entrance_logic = True
+
add_closed_portal_reqs(self)
# i need portal shuffle to happen after rules exist so i can validate it
attempts = 20
@@ -295,7 +316,7 @@ def connect_entrances(self) -> None:
raise RuntimeError("Unable to generate valid portal output.")
if self.options.shuffle_transitions:
- shuffle_transitions(self)
+ shuffle_transitions(self, keep_entrance_logic)
def write_spoiler_header(self, spoiler_handle: TextIO) -> None:
if self.options.available_portals < 6:
@@ -463,7 +484,7 @@ def stage_generate_output(cls, multiworld: MultiWorld, output_directory: str) ->
"loc_data": {loc.address: {loc.item.name: [loc.item.code, loc.item.flags]}
for loc in multiworld.get_filled_locations() if loc.address},
}
-
+
output = orjson.dumps(data, option=orjson.OPT_NON_STR_KEYS)
with open(out_path, "wb") as f:
f.write(output)
diff --git a/worlds/messenger/subclasses.py b/worlds/messenger/subclasses.py
index 2e438fdbfdc7..8beed4302719 100644
--- a/worlds/messenger/subclasses.py
+++ b/worlds/messenger/subclasses.py
@@ -1,8 +1,7 @@
from functools import cached_property
from typing import TYPE_CHECKING
-from BaseClasses import CollectionState, Entrance, EntranceType, Item, ItemClassification, Location, Region
-from entrance_rando import ERPlacementState
+from BaseClasses import CollectionState, Item, ItemClassification, Location, Region
from .regions import LOCATIONS, MEGA_SHARDS
from .shop import FIGURINES, SHOP_ITEMS
diff --git a/worlds/messenger/transitions.py b/worlds/messenger/transitions.py
index c0ae64c5489e..39ad591bf2d7 100644
--- a/worlds/messenger/transitions.py
+++ b/worlds/messenger/transitions.py
@@ -1,6 +1,6 @@
from typing import TYPE_CHECKING
-from BaseClasses import Entrance, Region
+from BaseClasses import Region, CollectionRule
from entrance_rando import EntranceType, randomize_entrances
from .connections import RANDOMIZED_CONNECTIONS, TRANSITIONS
from .options import ShuffleTransitions, TransitionPlando
@@ -26,7 +26,6 @@ def disconnect_entrance() -> None:
entrance.randomization_type = er_type
mock_entrance.randomization_type = er_type
-
for parent, child in RANDOMIZED_CONNECTIONS.items():
if child == "Corrupted Future":
entrance = world.get_entrance("Artificer's Portal")
@@ -36,8 +35,9 @@ def disconnect_entrance() -> None:
entrance = world.get_entrance(f"{parent} -> {child}")
disconnect_entrance()
-def connect_plando(world: "MessengerWorld", plando_connections: TransitionPlando) -> None:
- def remove_dangling_exit(region: Region) -> None:
+
+def connect_plando(world: "MessengerWorld", plando_connections: TransitionPlando, keep_logic: bool = False) -> None:
+ def remove_dangling_exit(region: Region) -> CollectionRule:
# find the disconnected exit and remove references to it
for _exit in region.exits:
if not _exit.connected_region:
@@ -45,6 +45,7 @@ def remove_dangling_exit(region: Region) -> None:
else:
raise ValueError(f"Unable to find randomized transition for {plando_connection}")
region.exits.remove(_exit)
+ return _exit.access_rule
def remove_dangling_entrance(region: Region) -> None:
# find the disconnected entrance and remove references to it
@@ -65,30 +66,35 @@ def remove_dangling_entrance(region: Region) -> None:
else:
dangling_exit = world.get_entrance("Artificer's Challenge")
reg1.exits.remove(dangling_exit)
+ access_rule = dangling_exit.access_rule
else:
reg1 = world.get_region(plando_connection.entrance)
- remove_dangling_exit(reg1)
-
+ access_rule = remove_dangling_exit(reg1)
+
reg2 = world.get_region(plando_connection.exit)
remove_dangling_entrance(reg2)
# connect the regions
- reg1.connect(reg2)
+ new_exit1 = reg1.connect(reg2)
+ if keep_logic:
+ new_exit1.access_rule = access_rule
# pretend the user set the plando direction as "both" regardless of what they actually put on coupled
if ((world.options.shuffle_transitions == ShuffleTransitions.option_coupled
or plando_connection.direction == "both")
and plando_connection.exit in RANDOMIZED_CONNECTIONS):
- remove_dangling_exit(reg2)
+ access_rule = remove_dangling_exit(reg2)
remove_dangling_entrance(reg1)
- reg2.connect(reg1)
+ new_exit2 = reg2.connect(reg1)
+ if keep_logic:
+ new_exit2.access_rule = access_rule
-def shuffle_transitions(world: "MessengerWorld") -> None:
+def shuffle_transitions(world: "MessengerWorld", keep_logic: bool = False) -> None:
coupled = world.options.shuffle_transitions == ShuffleTransitions.option_coupled
plando = world.options.plando_connections
if plando:
- connect_plando(world, plando)
+ connect_plando(world, plando, keep_logic)
result = randomize_entrances(world, coupled, {0: [0]})
diff --git a/worlds/messenger/universal_tracker.py b/worlds/messenger/universal_tracker.py
new file mode 100644
index 000000000000..9d752031bf62
--- /dev/null
+++ b/worlds/messenger/universal_tracker.py
@@ -0,0 +1,41 @@
+from Options import PlandoConnection
+from .connections import RANDOMIZED_CONNECTIONS
+from .portals import REGION_ORDER, SHOP_POINTS, CHECKPOINTS
+from .transitions import TRANSITIONS
+
+REVERSED_RANDOMIZED_CONNECTIONS = {v: k for k, v in RANDOMIZED_CONNECTIONS.items()}
+
+
+def find_spot(portal_key: int) -> str:
+ """finds the spot associated with the portal key"""
+ parent = REGION_ORDER[portal_key // 100]
+ if portal_key % 100 == 0:
+ return f"{parent} Portal"
+ if portal_key % 100 // 10 == 1:
+ return SHOP_POINTS[parent][portal_key % 10]
+ return CHECKPOINTS[parent][portal_key % 10]
+
+
+def reverse_portal_exits_into_portal_plando(portal_exits: list[int]) -> list[PlandoConnection]:
+ return [
+ PlandoConnection("Autumn Hills", find_spot(portal_exits[0]), "both"),
+ PlandoConnection("Riviere Turquoise", find_spot(portal_exits[1]), "both"),
+ PlandoConnection("Howling Grotto", find_spot(portal_exits[2]), "both"),
+ PlandoConnection("Sunken Shrine", find_spot(portal_exits[3]), "both"),
+ PlandoConnection("Searing Crags", find_spot(portal_exits[4]), "both"),
+ PlandoConnection("Glacial Peak", find_spot(portal_exits[5]), "both"),
+ ]
+
+
+def reverse_transitions_into_plando_connections(transitions: list[list[int]]) -> list[PlandoConnection]:
+ plando_connections = []
+
+ for connection in [
+ PlandoConnection(REVERSED_RANDOMIZED_CONNECTIONS[TRANSITIONS[transition[0]]], TRANSITIONS[transition[1]], "both")
+ for transition in transitions
+ ]:
+ if connection.exit in {con.entrance for con in plando_connections}:
+ continue
+ plando_connections.append(connection)
+
+ return plando_connections
From 07a1ec0a1d1e958a24e9aeeb32cd5c1e8c8e4e5e Mon Sep 17 00:00:00 2001
From: josephwhite <22449090+josephwhite@users.noreply.github.com>
Date: Tue, 10 Mar 2026 00:23:26 -0400
Subject: [PATCH 32/84] Test: Defaults for Options test (#5428)
---
test/general/test_options.py | 25 ++++++++++++++++++++++++-
1 file changed, 24 insertions(+), 1 deletion(-)
diff --git a/test/general/test_options.py b/test/general/test_options.py
index e610e36794e8..6b08c8e9b048 100644
--- a/test/general/test_options.py
+++ b/test/general/test_options.py
@@ -1,7 +1,7 @@
import unittest
from BaseClasses import PlandoOptions
-from Options import Choice, ItemLinks, OptionSet, PlandoConnections, PlandoItems, PlandoTexts
+from Options import Choice, TextChoice, ItemLinks, OptionSet, PlandoConnections, PlandoItems, PlandoTexts
from Utils import restricted_dumps
from worlds.AutoWorld import AutoWorldRegister
@@ -16,6 +16,29 @@ def test_options_have_doc_string(self):
with self.subTest(game=gamename, option=option_key):
self.assertTrue(option.__doc__)
+ def test_option_defaults(self):
+ """Test that defaults for submitted options are valid."""
+ for gamename, world_type in AutoWorldRegister.world_types.items():
+ if not world_type.hidden:
+ for option_key, option in world_type.options_dataclass.type_hints.items():
+ with self.subTest(game=gamename, option=option_key):
+ if issubclass(option, TextChoice):
+ self.assertTrue(option.default in option.name_lookup,
+ f"Default value {option.default} for TextChoice option {option.__name__} in"
+ f" {gamename} does not resolve to a listed value!"
+ )
+ # Standard "can default generate" test
+ err_raised = None
+ try:
+ option.from_any(option.default)
+ except Exception as ex:
+ err_raised = ex
+ self.assertIsNone(err_raised,
+ f"Default value {option.default} for option {option.__name__} in {gamename}"
+ f" is not valid! Exception: {err_raised}"
+ )
+
+
def test_options_are_not_set_by_world(self):
"""Test that options attribute is not already set"""
for gamename, world_type in AutoWorldRegister.world_types.items():
From 2c279cef09a53445aa4c25d4f5744ec6d45d2c24 Mon Sep 17 00:00:00 2001
From: Justus Lind
Date: Tue, 10 Mar 2026 15:11:34 +1000
Subject: [PATCH 33/84] Muse Dash: Adds 3 new music packs plus fixes being able
to roll songs without a legal way to obtain them (#5698)
---
worlds/musedash/MuseDashCollection.py | 2 ++
worlds/musedash/MuseDashData.py | 28 ++++++++++++++++++++++++++-
worlds/musedash/archipelago.json | 2 +-
3 files changed, 30 insertions(+), 2 deletions(-)
diff --git a/worlds/musedash/MuseDashCollection.py b/worlds/musedash/MuseDashCollection.py
index 2a9f56750e8a..4c23a5c64f32 100644
--- a/worlds/musedash/MuseDashCollection.py
+++ b/worlds/musedash/MuseDashCollection.py
@@ -28,6 +28,7 @@ class MuseDashCollections:
"Miku in Museland", # Paid DLC not included in Muse Plus
"Rin Len's Mirrorland", # Paid DLC not included in Muse Plus
"MSR Anthology_Vol.02", # Goes away January 26, 2026.
+ "MD-level Tactical Training Blu-ray", # Goes away December 27, 2025.
]
REMOVED_SONGS = [
@@ -38,6 +39,7 @@ class MuseDashCollections:
"Tsukuyomi Ni Naru Replaced",
"Heart Message feat. Aoi Tokimori Secret",
"Meow Rock feat. Chun Ge, Yuan Shen",
+ "Stra Stella Secret",
]
song_items = SONG_DATA
diff --git a/worlds/musedash/MuseDashData.py b/worlds/musedash/MuseDashData.py
index 4950c53e9667..86486a4929b0 100644
--- a/worlds/musedash/MuseDashData.py
+++ b/worlds/musedash/MuseDashData.py
@@ -625,7 +625,7 @@
"Synthesis.": SongData(2900749, "83-1", "Cosmic Radio 2024", True, 6, 8, 10),
"COSMiC FANFARE!!!!": SongData(2900750, "83-2", "Cosmic Radio 2024", False, 7, 9, 11),
"Sharp Bubbles": SongData(2900751, "83-3", "Cosmic Radio 2024", True, 7, 9, 11),
- "Replay": SongData(2900752, "83-4", "Cosmic Radio 2024", True, 5, 7, 9),
+ "Replay": SongData(2900752, "83-4", "Cosmic Radio 2024", False, 5, 7, 9),
"Cosmic Dusty Girl": SongData(2900753, "83-5", "Cosmic Radio 2024", True, 5, 7, 9),
"Meow Rock feat. Chun Ge, Yuan Shen": SongData(2900754, "84-0", "Muse DashãģLegend", True, None, None, None),
"Even if you make an old radio song with AI": SongData(2900755, "84-1", "Muse DashãģLegend", False, 3, 6, 8),
@@ -677,4 +677,30 @@
"City Lights": SongData(2900801, "90-3", "MEDIUM5 Echoes", True, 4, 6, 9),
"Polaris Wandering Night": SongData(2900802, "90-4", "MEDIUM5 Echoes", True, 5, 8, 10),
"Chasing the Moonlight": SongData(2900803, "90-5", "MEDIUM5 Echoes", True, 4, 6, 8),
+ "WILDCARD": SongData(2900804, "91-0", "48 Hours After Discharge", True, 3, 6, 9),
+ "It was all just a dream!": SongData(2900805, "91-1", "48 Hours After Discharge", True, 5, 7, 9),
+ "Science": SongData(2900806, "91-2", "48 Hours After Discharge", False, 4, 7, 9),
+ "Hit Maker": SongData(2900807, "91-3", "48 Hours After Discharge", False, 4, 6, 9),
+ "THX 4 playing": SongData(2900808, "91-4", "48 Hours After Discharge", True, 3, 5, 8),
+ "Theory of Existence": SongData(2900809, "91-5", "48 Hours After Discharge", True, 4, 6, 9),
+ "Kirakira Noel Story!!": SongData(2900810, "43-68", "MD Plus Project", False, 6, 8, 10),
+ "Fantasista LAST END": SongData(2900811, "92-0", "HARDCORE MOTTO TANO*C", True, 7, 9, 11),
+ "Colorful Universe": SongData(2900812, "92-1", "HARDCORE MOTTO TANO*C", True, 3, 6, 9),
+ "Future Flux": SongData(2900813, "92-2", "HARDCORE MOTTO TANO*C", True, 5, 8, 10),
+ "SOMEONE STOP ME!!!": SongData(2900814, "92-3", "HARDCORE MOTTO TANO*C", True, 6, 8, 10),
+ "Azathoth": SongData(2900815, "92-4", "HARDCORE MOTTO TANO*C", True, 6, 8, 10),
+ "Change the Game feat. Iori Matsunaga": SongData(2900816, "92-5", "HARDCORE MOTTO TANO*C", False, 6, 8, 10),
+ "Stra Stella Secret": SongData(2900817, "0-59", "Default Music", False, 6, 8, 10),
+ "Stra Stella": SongData(2900818, "0-60", "Default Music", False, 1, 4, None),
+ "Ultra-Digital Super Detox": SongData(2900819, "43-69", "MD Plus Project", False, 3, 6, 9),
+ "Otsukimi Koete Otsukiai": SongData(2900820, "43-70", "MD Plus Project", True, 6, 8, 10),
+ "Obenkyou Time": SongData(2900821, "43-71", "MD Plus Project", False, 6, 8, 11),
+ "Retry Now": SongData(2900822, "43-72", "MD Plus Project", False, 3, 6, 9),
+ "Master Bancho's Sushi Class ": SongData(2900823, "93-0", "Welcome to the Blue Hole!", False, None, None, None),
+ "CHAOTiC BATTLE": SongData(2900824, "94-0", "Cosmic Radio 2025", False, 7, 9, 11),
+ "FATAL GAME": SongData(2900825, "94-1", "Cosmic Radio 2025", False, 3, 6, 9),
+ "Aria": SongData(2900826, "94-2", "Cosmic Radio 2025", False, 4, 6, 9),
+ "+1 UNKNOWN -NUMBER": SongData(2900827, "94-3", "Cosmic Radio 2025", True, 4, 7, 10),
+ "To the Beyond, from the Nameless Seaside": SongData(2900828, "94-4", "Cosmic Radio 2025", False, 5, 8, 10),
+ "REK421": SongData(2900829, "94-5", "Cosmic Radio 2025", True, 7, 9, 11),
}
diff --git a/worlds/musedash/archipelago.json b/worlds/musedash/archipelago.json
index 9b22a9960523..dea7846b4f38 100644
--- a/worlds/musedash/archipelago.json
+++ b/worlds/musedash/archipelago.json
@@ -1,6 +1,6 @@
{
"game": "Muse Dash",
"authors": ["DeamonHunter"],
- "world_version": "1.5.26",
+ "world_version": "1.5.29",
"minimum_ap_version": "0.6.3"
}
\ No newline at end of file
From fd81553420297fbd74845bda76a9821d35d858ba Mon Sep 17 00:00:00 2001
From: Remy Jette
Date: Tue, 10 Mar 2026 03:38:02 -0700
Subject: [PATCH 34/84] Fix missing } in example_nginx.conf (#6027)
---
deploy/example_nginx.conf | 1 +
1 file changed, 1 insertion(+)
diff --git a/deploy/example_nginx.conf b/deploy/example_nginx.conf
index a4f042739c0a..d44d1ffbd1ef 100644
--- a/deploy/example_nginx.conf
+++ b/deploy/example_nginx.conf
@@ -61,5 +61,6 @@ http {
location = /favicon.ico {
alias /app/WebHostLib/static/static/favicon.ico;
access_log off;
+ }
}
}
From c255ea8fc680236347485f2d92aa8d578070e9b1 Mon Sep 17 00:00:00 2001
From: Goo-Dang
Date: Tue, 10 Mar 2026 13:52:59 -0400
Subject: [PATCH 35/84] Pokemon Emerald: Dexsanity Encounter Type Option
(#6016)
---------
Co-authored-by: Bryce Wilson
---
worlds/pokemon_emerald/__init__.py | 4 +++-
worlds/pokemon_emerald/locations.py | 2 +-
worlds/pokemon_emerald/options.py | 14 +++++++++++++-
worlds/pokemon_emerald/pokemon.py | 8 ++++++++
worlds/pokemon_emerald/rules.py | 2 +-
5 files changed, 26 insertions(+), 4 deletions(-)
diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py
index 4f2c2ef95cb4..10abed539f70 100644
--- a/worlds/pokemon_emerald/__init__.py
+++ b/worlds/pokemon_emerald/__init__.py
@@ -123,6 +123,7 @@ class PokemonEmeraldWorld(World):
blacklisted_wilds: Set[int]
blacklisted_starters: Set[int]
blacklisted_opponent_pokemon: Set[int]
+ allowed_dexsanity_species: set[int]
hm_requirements: Dict[str, Union[int, List[str]]]
auth: bytes
@@ -142,6 +143,7 @@ def __init__(self, multiworld, player):
self.blacklisted_wilds = set()
self.blacklisted_starters = set()
self.blacklisted_opponent_pokemon = set()
+ self.allowed_dexsanity_species = set()
self.modified_maps = copy.deepcopy(emerald_data.maps)
self.modified_species = copy.deepcopy(emerald_data.species)
self.modified_tmhm_moves = []
@@ -265,6 +267,7 @@ def create_regions(self) -> None:
from .regions import create_regions
all_regions = create_regions(self)
+ randomize_wild_encounters(self)
# Categories with progression items always included
categories = {
LocationCategory.BADGE,
@@ -494,7 +497,6 @@ def set_rules(self):
set_rules(self)
def connect_entrances(self):
- randomize_wild_encounters(self)
self.shuffle_badges_hms()
# For entrance randomization, disconnect entrances here, randomize map, then
# undo badge/HM placement and re-shuffle them in the new map.
diff --git a/worlds/pokemon_emerald/locations.py b/worlds/pokemon_emerald/locations.py
index 49ce147041ee..fd8d0ebc7d58 100644
--- a/worlds/pokemon_emerald/locations.py
+++ b/worlds/pokemon_emerald/locations.py
@@ -110,7 +110,7 @@ def create_locations_by_category(world: "PokemonEmeraldWorld", regions: Dict[str
national_dex_id = int(location_name[-3:]) # Location names are formatted POKEDEX_REWARD_###
# Don't create this pokedex location if player can't find it in the wild
- if NATIONAL_ID_TO_SPECIES_ID[national_dex_id] in world.blacklisted_wilds:
+ if NATIONAL_ID_TO_SPECIES_ID[national_dex_id] in world.blacklisted_wilds or NATIONAL_ID_TO_SPECIES_ID[national_dex_id] not in world.allowed_dexsanity_species:
continue
location_id += POKEDEX_OFFSET + national_dex_id
diff --git a/worlds/pokemon_emerald/options.py b/worlds/pokemon_emerald/options.py
index 29929bd67237..9529be877ebe 100644
--- a/worlds/pokemon_emerald/options.py
+++ b/worlds/pokemon_emerald/options.py
@@ -4,7 +4,7 @@
from dataclasses import dataclass
from Options import (Choice, DeathLink, DefaultOnToggle, OptionSet, NamedRange, Range, Toggle, FreeText,
- PerGameCommonOptions, OptionGroup, StartInventory)
+ PerGameCommonOptions, OptionGroup, StartInventory, OptionList)
from .data import data
@@ -129,6 +129,17 @@ class Dexsanity(Toggle):
display_name = "Dexsanity"
+class DexsanityEncounterTypes(OptionList):
+ """
+ Determines which Dexsanity encounter areas are in logic.
+
+ Logic will only consider access to Pokemon at these encounter types, but they may still be found elsewhere.
+ """
+ display_name = "Dexsanity Encounter Types"
+ valid_keys = {"Land", "Water", "Fishing"}
+ default = valid_keys.copy()
+
+
class Trainersanity(Toggle):
"""
Defeating a trainer gives you an item.
@@ -870,6 +881,7 @@ class PokemonEmeraldOptions(PerGameCommonOptions):
npc_gifts: RandomizeNpcGifts
berry_trees: RandomizeBerryTrees
dexsanity: Dexsanity
+ dexsanity_encounter_types: DexsanityEncounterTypes
trainersanity: Trainersanity
item_pool_type: ItemPoolType
diff --git a/worlds/pokemon_emerald/pokemon.py b/worlds/pokemon_emerald/pokemon.py
index 8f799ce61154..73af6c465840 100644
--- a/worlds/pokemon_emerald/pokemon.py
+++ b/worlds/pokemon_emerald/pokemon.py
@@ -264,6 +264,12 @@ def _rename_wild_events(world: "PokemonEmeraldWorld", map_data: MapData, new_slo
def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None:
+ encounter_table = {
+ "Land": EncounterType.LAND,
+ "Water": EncounterType.WATER,
+ "Fishing": EncounterType.FISHING,
+ }
+ enabled_encounters = {encounter_table[encounter_type] for encounter_type in world.options.dexsanity_encounter_types.value}
if world.options.wild_pokemon == RandomizeWildPokemon.option_vanilla:
return
@@ -370,6 +376,8 @@ def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None:
# Actually create the new list of slots and encounter table
new_slots: List[int] = []
+ if encounter_type in enabled_encounters:
+ world.allowed_dexsanity_species.update(table.slots)
for species_id in table.slots:
new_slots.append(species_old_to_new_map[species_id])
diff --git a/worlds/pokemon_emerald/rules.py b/worlds/pokemon_emerald/rules.py
index 828eb20f7218..eeadb8bea21a 100644
--- a/worlds/pokemon_emerald/rules.py
+++ b/worlds/pokemon_emerald/rules.py
@@ -1548,7 +1548,7 @@ def get_location(location: str):
for i in range(NUM_REAL_SPECIES):
species = data.species[NATIONAL_ID_TO_SPECIES_ID[i + 1]]
- if species.species_id in world.blacklisted_wilds:
+ if species.species_id in world.blacklisted_wilds or species.species_id not in world.allowed_dexsanity_species:
continue
set_rule(
From 1a8a71f59343ce1c285624ef2faaf744fa7d3c30 Mon Sep 17 00:00:00 2001
From: Matthew Wells <91291346+richarm4@users.noreply.github.com>
Date: Tue, 10 Mar 2026 10:54:24 -0700
Subject: [PATCH 36/84] Dark Souls 3: Update location descriptions for Red
Tearstone Ring and Hood of Prayer (#5602)
RTSR's description was incorrect and Hood of Prayer was missing its description
---
worlds/dark_souls_3/Locations.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/worlds/dark_souls_3/Locations.py b/worlds/dark_souls_3/Locations.py
index b4e45fb57791..569a0348d29f 100644
--- a/worlds/dark_souls_3/Locations.py
+++ b/worlds/dark_souls_3/Locations.py
@@ -2025,13 +2025,13 @@ def __init__(
DS3LocationData("LC: Rusted Coin - chapel", "Rusted Coin x2"),
DS3LocationData("LC: Braille Divine Tome of Lothric - wyvern room",
"Braille Divine Tome of Lothric", hidden=True), # Hidden fall
- DS3LocationData("LC: Red Tearstone Ring - chapel, drop onto roof", "Red Tearstone Ring"),
+ DS3LocationData("LC: Red Tearstone Ring - chapel, balcony before drop", "Red Tearstone Ring"),
DS3LocationData("LC: Twinkling Titanite - moat, left side", "Twinkling Titanite x2"),
DS3LocationData("LC: Large Soul of a Nameless Soldier - plaza left, by pillar",
"Large Soul of a Nameless Soldier"),
DS3LocationData("LC: Titanite Scale - altar", "Titanite Scale x3"),
DS3LocationData("LC: Titanite Scale - chapel, chest", "Titanite Scale"),
- DS3LocationData("LC: Hood of Prayer", "Hood of Prayer"),
+ DS3LocationData("LC: Hood of Prayer - ascent, chest at beginning", "Hood of Prayer"),
DS3LocationData("LC: Robe of Prayer - ascent, chest at beginning", "Robe of Prayer"),
DS3LocationData("LC: Skirt of Prayer - ascent, chest at beginning", "Skirt of Prayer"),
DS3LocationData("LC: Spirit Tree Crest Shield - basement, chest",
From c3659fb3ef43fa25a4b72fbb67564154a8407d31 Mon Sep 17 00:00:00 2001
From: Scipio Wright
Date: Tue, 10 Mar 2026 13:55:07 -0400
Subject: [PATCH 37/84] TUNIC: Refactor entrance hint generation (#5620)
* Refactor hint generation
* Remove debug print
* Early out per qwint's comment
---
worlds/tunic/__init__.py | 41 ++++++++++++++++++++++----------------
worlds/tunic/er_scripts.py | 8 ++++++++
2 files changed, 32 insertions(+), 17 deletions(-)
diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py
index 78c9dcdb67b9..2d7a63245300 100644
--- a/worlds/tunic/__init__.py
+++ b/worlds/tunic/__init__.py
@@ -672,25 +672,29 @@ def write_spoiler_header(self, spoiler_handle: TextIO):
# Remove parentheses for better readability
spoiler_handle.write(f'{ability[ability.find("(")+1:ability.find(")")]}: {self.ability_unlocks[ability]} Gold Questagons\n')
- def extend_hint_information(self, hint_data: dict[int, dict[int, str]]) -> None:
- if self.options.entrance_rando:
- hint_data.update({self.player: {}})
- # all state seems to have efficient paths
- all_state = self.multiworld.get_all_state(True)
- all_state.update_reachable_regions(self.player)
- paths = all_state.path
- portal_names = {portal.name for portal in portal_mapping}.union({f"Shop Portal {i + 1}" for i in range(500)})
- for location in self.multiworld.get_locations(self.player):
- # skipping event locations
- if not location.address:
+ @classmethod
+ def stage_extend_hint_information(cls, multiworld: MultiWorld, hint_data: dict[int, dict[int, str]]) -> None:
+ tunic_er_worlds: list[TunicWorld] = [world for world in multiworld.get_game_worlds("TUNIC")
+ if world.options.entrance_rando]
+ if not tunic_er_worlds:
+ return
+
+ hint_data.update({world.player: {} for world in tunic_er_worlds})
+ all_state = multiworld.get_all_state()
+ paths = all_state.path
+ portal_names = {portal.name for portal in portal_mapping}.union({f"Shop Portal {i + 1}" for i in range(500)})
+ for world in tunic_er_worlds:
+ all_state.update_reachable_regions(world.player)
+ for region in world.get_regions():
+ if region.name == "Menu":
continue
- path_to_loc = []
+ path_to_region = []
previous_name = "placeholder"
try:
- name, connection = paths[location.parent_region]
+ name, connection = paths[region]
except KeyError:
# logic bug, proceed with warning since it takes a long time to update AP
- warning(f"{location.name} is not logically accessible for {self.player_name}. "
+ warning(f"{region.name} is not logically accessible for {world.player_name}. "
"Creating entrance hint Inaccessible. Please report this to the TUNIC rando devs. "
"If you are using Plando Items (excluding early locations), then this is likely the cause.")
hint_text = "Inaccessible"
@@ -703,11 +707,14 @@ def extend_hint_information(self, hint_data: dict[int, dict[int, str]]) -> None:
# was getting some cases like Library Grave -> Library Grave -> other place
if name in portal_names and name != previous_name:
previous_name = name
- path_to_loc.append(name)
- hint_text = " -> ".join(reversed(path_to_loc))
+ path_to_region.append(name)
+ hint_text = " -> ".join(reversed(path_to_region))
if hint_text:
- hint_data[self.player][location.address] = hint_text
+ for location in region.get_locations():
+ if location.address is None:
+ continue
+ hint_data[world.player][location.address] = hint_text
def get_real_location(self, location: Location) -> tuple[str, int]:
# if it's not in a group, it's not in an item link
diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py
index a1b8b2fefdbc..60de72906dcb 100644
--- a/worlds/tunic/er_scripts.py
+++ b/worlds/tunic/er_scripts.py
@@ -29,6 +29,8 @@ def create_er_regions(world: "TunicWorld") -> dict[Portal, Portal]:
world.used_shop_numbers = set()
for region_name, region_data in world.er_regions.items():
+ if region_name == "Zig Skip Exit":
+ continue
if world.options.entrance_rando and region_name == "Zig Skip Exit":
# need to check if there's a seed group for this first
if world.options.entrance_rando.value not in EntranceRando.options.values():
@@ -773,11 +775,17 @@ def too_few_portals_for_direction_pairs(direction: int, offset: int) -> bool:
# loop through our list of paired portals and make two-way connections
def create_randomized_entrances(world: "TunicWorld", portal_pairs: dict[Portal, Portal], regions: dict[str, Region]) -> None:
for portal1, portal2 in portal_pairs.items():
+ # this portal is completely inaccessible, so let's not make this connection
+ if portal1.region == "Zig Skip Exit":
+ continue
# connect to the outlet region if there is one, if not connect to the actual region
regions[portal1.region].connect(
connecting_region=regions[get_portal_outlet_region(portal2, world)],
name=portal1.name)
if not world.options.decoupled or not world.options.entrance_rando:
+ # this portal is completely inaccessible, so let's not make this connection
+ if portal2.region == "Zig Skip Exit":
+ continue
regions[portal2.region].connect(
connecting_region=regions[get_portal_outlet_region(portal1, world)],
name=portal2.name)
From 4b37283d228e98586a58dbbf2a8423765af2969a Mon Sep 17 00:00:00 2001
From: josephwhite <22449090+josephwhite@users.noreply.github.com>
Date: Tue, 10 Mar 2026 13:57:48 -0400
Subject: [PATCH 38/84] WebHost: Update UTC datetime usage (timezone-naive)
(#4906)
---
Utils.py | 11 +++++++++++
WebHostLib/autolauncher.py | 8 ++++----
WebHostLib/customserver.py | 5 ++---
WebHostLib/landing.py | 7 ++++---
WebHostLib/misc.py | 12 +++++++-----
WebHostLib/models.py | 8 +++++---
WebHostLib/tracker.py | 7 ++++---
test/hosting/webhost.py | 8 +++++---
8 files changed, 42 insertions(+), 24 deletions(-)
diff --git a/Utils.py b/Utils.py
index c18298559ac7..627235f24925 100644
--- a/Utils.py
+++ b/Utils.py
@@ -18,6 +18,8 @@
import warnings
from argparse import Namespace
+from datetime import datetime, timezone
+
from settings import Settings, get_settings
from time import sleep
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union, TypeGuard
@@ -1291,6 +1293,15 @@ def is_iterable_except_str(obj: object) -> TypeGuard[typing.Iterable[typing.Any]
return isinstance(obj, typing.Iterable)
+def utcnow() -> datetime:
+ """
+ Implementation of Python's datetime.utcnow() function for use after deprecation.
+ Needed for timezone-naive UTC datetimes stored in databases with PonyORM (upstream).
+ https://ponyorm.org/ponyorm-list/2014-August/000113.html
+ """
+ return datetime.now(timezone.utc).replace(tzinfo=None)
+
+
class DaemonThreadPoolExecutor(concurrent.futures.ThreadPoolExecutor):
"""
ThreadPoolExecutor that uses daemonic threads that do not keep the program alive.
diff --git a/WebHostLib/autolauncher.py b/WebHostLib/autolauncher.py
index 96ffbe9e9540..b48c6a8cbbe1 100644
--- a/WebHostLib/autolauncher.py
+++ b/WebHostLib/autolauncher.py
@@ -4,14 +4,14 @@
import logging
import multiprocessing
import typing
-from datetime import timedelta, datetime
+from datetime import timedelta
from threading import Event, Thread
from typing import Any
from uuid import UUID
from pony.orm import db_session, select, commit, PrimaryKey
-from Utils import restricted_loads
+from Utils import restricted_loads, utcnow
from .locker import Locker, AlreadyRunningException
_stop_event = Event()
@@ -129,10 +129,10 @@ def keep_running():
with db_session:
rooms = select(
room for room in Room if
- room.last_activity >= datetime.utcnow() - timedelta(days=3))
+ room.last_activity >= utcnow() - timedelta(days=3))
for room in rooms:
# we have to filter twice, as the per-room timeout can't currently be PonyORM transpiled.
- if room.last_activity >= datetime.utcnow() - timedelta(seconds=room.timeout + 5):
+ if room.last_activity >= utcnow() - timedelta(seconds=room.timeout + 5):
hosters[room.id.int % len(hosters)].start_room(room.id)
except AlreadyRunningException:
diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py
index e353cf2ab2d4..4257c6aff3e4 100644
--- a/WebHostLib/customserver.py
+++ b/WebHostLib/customserver.py
@@ -172,7 +172,7 @@ def _save(self, exit_save: bool = False) -> bool:
room.multisave = pickle.dumps(self.get_save())
# saving only occurs on activity, so we can "abuse" this information to mark this as last_activity
if not exit_save: # we don't want to count a shutdown as activity, which would restart the server again
- room.last_activity = datetime.datetime.utcnow()
+ room.last_activity = Utils.utcnow()
return True
def get_save(self) -> dict:
@@ -367,8 +367,7 @@ async def start_room(room_id):
with db_session:
# ensure the Room does not spin up again on its own, minute of safety buffer
room = Room.get(id=room_id)
- room.last_activity = datetime.datetime.utcnow() - \
- datetime.timedelta(minutes=1, seconds=room.timeout)
+ room.last_activity = Utils.utcnow() - datetime.timedelta(minutes=1, seconds=room.timeout)
del room
tear_down_logging(room_id)
logging.info(f"Shutting down room {room_id} on {name}.")
diff --git a/WebHostLib/landing.py b/WebHostLib/landing.py
index 14e90cc28df4..f1b8de21bfbf 100644
--- a/WebHostLib/landing.py
+++ b/WebHostLib/landing.py
@@ -1,8 +1,9 @@
-from datetime import timedelta, datetime
+from datetime import timedelta
from flask import render_template
from pony.orm import count
+from Utils import utcnow
from WebHostLib import app, cache
from .models import Room, Seed
@@ -10,6 +11,6 @@
@app.route('/', methods=['GET', 'POST'])
@cache.cached(timeout=300) # cache has to appear under app route for caching to work
def landing():
- rooms = count(room for room in Room if room.creation_time >= datetime.utcnow() - timedelta(days=7))
- seeds = count(seed for seed in Seed if seed.creation_time >= datetime.utcnow() - timedelta(days=7))
+ rooms = count(room for room in Room if room.creation_time >= utcnow() - timedelta(days=7))
+ seeds = count(seed for seed in Seed if seed.creation_time >= utcnow() - timedelta(days=7))
return render_template("landing.html", rooms=rooms, seeds=seeds)
diff --git a/WebHostLib/misc.py b/WebHostLib/misc.py
index e30f1a6dd413..8d04fe984eb5 100644
--- a/WebHostLib/misc.py
+++ b/WebHostLib/misc.py
@@ -9,11 +9,12 @@
from pony.orm import count, commit, db_session
from werkzeug.utils import secure_filename
+
from worlds.AutoWorld import AutoWorldRegister, World
from . import app, cache
from .markdown import render_markdown
from .models import Seed, Room, Command, UUID, uuid4
-from Utils import title_sorted
+from Utils import title_sorted, utcnow
class WebWorldTheme(StrEnum):
DIRT = "dirt"
@@ -233,11 +234,12 @@ def host_room(room: UUID):
if room is None:
return abort(404)
- now = datetime.datetime.utcnow()
+ now = utcnow()
# indicate that the page should reload to get the assigned port
- should_refresh = ((not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3))
- or room.last_activity < now - datetime.timedelta(seconds=room.timeout))
-
+ should_refresh = (
+ (not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3))
+ or room.last_activity < now - datetime.timedelta(seconds=room.timeout)
+ )
if now - room.last_activity > datetime.timedelta(minutes=1):
# we only set last_activity if needed, otherwise parallel access on /room will cause an internal server error
# due to "pony.orm.core.OptimisticCheckError: Object Room was updated outside of current transaction"
diff --git a/WebHostLib/models.py b/WebHostLib/models.py
index 7fa54f26a004..9060bc0ca4c5 100644
--- a/WebHostLib/models.py
+++ b/WebHostLib/models.py
@@ -2,6 +2,8 @@
from uuid import UUID, uuid4
from pony.orm import Database, PrimaryKey, Required, Set, Optional, buffer, LongStr
+from Utils import utcnow
+
db = Database()
STATE_QUEUED = 0
@@ -20,8 +22,8 @@ class Slot(db.Entity):
class Room(db.Entity):
id = PrimaryKey(UUID, default=uuid4)
- last_activity = Required(datetime, default=lambda: datetime.utcnow(), index=True)
- creation_time = Required(datetime, default=lambda: datetime.utcnow(), index=True) # index used by landing page
+ last_activity: datetime = Required(datetime, default=lambda: utcnow(), index=True)
+ creation_time: datetime = Required(datetime, default=lambda: utcnow(), index=True) # index used by landing page
owner = Required(UUID, index=True)
commands = Set('Command')
seed = Required('Seed', index=True)
@@ -38,7 +40,7 @@ class Seed(db.Entity):
rooms = Set(Room)
multidata = Required(bytes, lazy=True)
owner = Required(UUID, index=True)
- creation_time = Required(datetime, default=lambda: datetime.utcnow(), index=True) # index used by landing page
+ creation_time: datetime = Required(datetime, default=lambda: utcnow(), index=True) # index used by landing page
slots = Set(Slot)
spoiler = Optional(LongStr, lazy=True)
meta = Required(LongStr, default=lambda: "{\"race\": false}") # additional meta information/tags
diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py
index d1471aa6580a..cb40c8293f2d 100644
--- a/WebHostLib/tracker.py
+++ b/WebHostLib/tracker.py
@@ -10,7 +10,7 @@
from MultiServer import Context, get_saving_second
from NetUtils import ClientStatus, Hint, NetworkItem, NetworkSlot, SlotType
-from Utils import restricted_loads, KeyedDefaultDict
+from Utils import restricted_loads, KeyedDefaultDict, utcnow
from . import app, cache
from .models import GameDataPackage, Room
@@ -273,9 +273,10 @@ def get_room_last_activity(self) -> Dict[TeamPlayer, datetime.timedelta]:
Does not include players who have no activity recorded.
"""
last_activity: Dict[TeamPlayer, datetime.timedelta] = {}
- now = datetime.datetime.utcnow()
+ now = utcnow()
for (team, player), timestamp in self._multisave.get("client_activity_timers", []):
- last_activity[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp)
+ from_timestamp = datetime.datetime.fromtimestamp(timestamp, datetime.timezone.utc).replace(tzinfo=None)
+ last_activity[team, player] = now - from_timestamp
return last_activity
diff --git a/test/hosting/webhost.py b/test/hosting/webhost.py
index a8e70a50c20c..286ef63a5583 100644
--- a/test/hosting/webhost.py
+++ b/test/hosting/webhost.py
@@ -6,6 +6,7 @@
from pathlib import Path
from typing import TYPE_CHECKING, Iterable, Optional, cast
+from Utils import utcnow
from WebHostLib import to_python
if TYPE_CHECKING:
@@ -133,7 +134,7 @@ def stop_room(app_client: "FlaskClient",
room_id: str,
timeout: Optional[float] = None,
simulate_idle: bool = True) -> None:
- from datetime import datetime, timedelta
+ from datetime import timedelta
from time import sleep
from pony.orm import db_session
@@ -151,10 +152,11 @@ def stop_room(app_client: "FlaskClient",
with db_session:
room: Room = Room.get(id=room_uuid)
+ now = utcnow()
if simulate_idle:
- new_last_activity = datetime.utcnow() - timedelta(seconds=room.timeout + 5)
+ new_last_activity = now - timedelta(seconds=room.timeout + 5)
else:
- new_last_activity = datetime.utcnow() - timedelta(days=3)
+ new_last_activity = now - timedelta(days=3)
room.last_activity = new_last_activity
address = f"localhost:{room.last_port}" if room.last_port > 0 else None
if address:
From 72ff9b1a7dace3063f95023a14f251362a9b9d29 Mon Sep 17 00:00:00 2001
From: LeonarthCG <33758848+LeonarthCG@users.noreply.github.com>
Date: Tue, 10 Mar 2026 19:12:48 +0100
Subject: [PATCH 39/84] Saving Princess: Security fixes for issues detected by
Bandit (#6013)
* Saving Princess: absolute paths on suprocess.run
* Saving Princess: more error handling for downloads
* Saving Princess: rework launch_command setting
Apparently subprocess.Popen requires a list for args instead of a string everywhere but in Windows, so the change was preventing the game from running on Linux. Additionally, the game is now launched using absolute paths.
* Saving Princess: prevent bandit warnings
* Saving Princess: remove unnecessary compare_digest
* Saving Princess: fix Linux paths by using which
* Saving Princess: rename launch command setting
Previously, launch_command held a string. Now it holds a list of strings. Additionally, the defaults have changed.
To prevent the wrong type from being used, the setting has been renamed, effectively abandoning the original launch_command setting.
* Saving Princess: fix Linux default command return type
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
---------
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
---
worlds/saving_princess/Client.py | 42 +++++++++++++++++++-----------
worlds/saving_princess/__init__.py | 16 +++++++++---
2 files changed, 40 insertions(+), 18 deletions(-)
diff --git a/worlds/saving_princess/Client.py b/worlds/saving_princess/Client.py
index 29a97bb667c0..195a6a57039e 100644
--- a/worlds/saving_princess/Client.py
+++ b/worlds/saving_princess/Client.py
@@ -1,4 +1,5 @@
import argparse
+import ssl
import zipfile
from io import BytesIO
@@ -8,12 +9,13 @@
import json
import logging
import os
+
+import certifi
import requests
-import secrets
import shutil
-import subprocess
+import subprocess # nosec
from tkinter import messagebox
-from typing import Any, Dict, Set
+from typing import Any, Dict, Set, List
import urllib
import urllib.parse
@@ -90,7 +92,7 @@ def get_timestamp(date: str) -> float:
def send_request(request_url: str) -> UrlResponse:
"""Fetches status code and json response from given url"""
- response = requests.get(request_url)
+ response = requests.get(request_url, timeout=10)
if response.status_code == 200: # success
try:
data = response.json()
@@ -129,13 +131,16 @@ def update(target_asset: str, url: str) -> bool:
if update_available and messagebox.askyesnocancel(f"New {target_asset}",
"Would you like to install the new version now?"):
# unzip and patch
- with urllib.request.urlopen(release_url) as download:
+ if not release_url.lower().startswith("https"):
+ raise ValueError(f'Unexpected scheme for url "{release_url}".')
+ context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=certifi.where())
+ with urllib.request.urlopen(release_url, context=context) as download: # nosec
with zipfile.ZipFile(BytesIO(download.read())) as zf:
zf.extractall()
patch_game()
set_date(target_asset, newest_date)
- except (ValueError, RuntimeError, urllib.error.HTTPError):
- update_error = f"Failed to apply update."
+ except (ValueError, RuntimeError, urllib.error.HTTPError, urllib.error.URLError) as e:
+ update_error = f"Failed to apply update:\n{e}"
messagebox.showerror("Failure", update_error)
raise RuntimeError(update_error)
return True
@@ -158,8 +163,8 @@ def is_install_valid() -> bool:
if not os.path.exists(file_name):
return False
with open(file_name, "rb") as clean:
- current_hash = hashlib.md5(clean.read()).hexdigest()
- if not secrets.compare_digest(current_hash, expected_hash):
+ current_hash = hashlib.md5(clean.read(), usedforsecurity=False).hexdigest()
+ if current_hash != expected_hash:
return False
return True
@@ -189,12 +194,16 @@ def install() -> None:
logging.info("Extracting files from cab archive.")
if Utils.is_windows:
- subprocess.run(["Extrac32", "/Y", "/E", "saving_princess.cab"])
+ windows_path = os.environ["WINDIR"]
+ extractor_path = f"{windows_path}/System32/Extrac32"
+ subprocess.run([extractor_path, "/Y", "/E", "saving_princess.cab"]) #nosec
else:
- if shutil.which("wine") is not None:
- subprocess.run(["wine", "Extrac32", "/Y", "/E", "saving_princess.cab"])
- elif shutil.which("7z") is not None:
- subprocess.run(["7z", "e", "saving_princess.cab"])
+ wine_path = shutil.which("wine")
+ p7zip_path = shutil.which("7z")
+ if wine_path is not None:
+ subprocess.run([wine_path, "Extrac32", "/Y", "/E", "saving_princess.cab"]) #nosec
+ elif p7zip_path is not None:
+ subprocess.run([p7zip_path, "e", "saving_princess.cab"]) #nosec
else:
error = "Could not find neither wine nor 7z.\n\nPlease install either the wine or the p7zip package."
messagebox.showerror("Missing package!", f"Error: {error}")
@@ -250,7 +259,10 @@ def launch(*args: str) -> Any:
if SavingPrincessWorld.settings.launch_game:
logging.info("Launching game.")
try:
- subprocess.Popen(f"{SavingPrincessWorld.settings.launch_command} {name} {password} {server}")
+ game: str = os.path.join(os.getcwd(), "Saving Princess v0_8.exe")
+ launch_command: List[str] = (SavingPrincessWorld.settings.launch_command_with_args
+ + [game, name, password, server])
+ subprocess.Popen(launch_command) # nosec
except FileNotFoundError:
error = ("Could not run the game!\n\n"
"Please check that launch_command in options.yaml or host.yaml is set up correctly.")
diff --git a/worlds/saving_princess/__init__.py b/worlds/saving_princess/__init__.py
index b4caf3828c5f..0c6208638abb 100644
--- a/worlds/saving_princess/__init__.py
+++ b/worlds/saving_princess/__init__.py
@@ -1,3 +1,4 @@
+import shutil
from typing import ClassVar, Dict, Any, Type, List, Union
import Utils
@@ -20,6 +21,15 @@ def launch_client(*args: str):
)
+def get_default_launch_command() -> List[str]:
+ """Returns platform-dependant default launch command for Saving Princess"""
+ if Utils.is_windows:
+ return []
+ else:
+ wine_path = shutil.which("wine")
+ return [wine_path] if wine_path is not None else ["/usr/bin/wine"]
+
+
class SavingPrincessSettings(Group):
class GamePath(UserFilePath):
"""Path to the game executable from which files are extracted"""
@@ -34,17 +44,17 @@ class InstallFolder(LocalFolderPath):
class LaunchGame(Bool):
"""Set this to false to never autostart the game"""
- class LaunchCommand(str):
+ class LaunchCommandWithArgs(List[str]):
"""
The console command that will be used to launch the game
The command will be executed with the installation folder as the current directory
+ Additional items in the list will be passed in as arguments
"""
exe_path: GamePath = GamePath("Saving Princess.exe")
install_folder: InstallFolder = InstallFolder("Saving Princess")
launch_game: Union[LaunchGame, bool] = True
- launch_command: LaunchCommand = LaunchCommand('"Saving Princess v0_8.exe"' if Utils.is_windows
- else 'wine "Saving Princess v0_8.exe"')
+ launch_command_with_args: LaunchCommandWithArgs = LaunchCommandWithArgs(get_default_launch_command())
class SavingPrincessWeb(WebWorld):
From 94136ac223b88a3b82d13c6205cb0603496f9f77 Mon Sep 17 00:00:00 2001
From: Duck <31627079+duckboycool@users.noreply.github.com>
Date: Tue, 10 Mar 2026 12:18:03 -0600
Subject: [PATCH 40/84] Docs: Add references to running from source (#6022)
---
docs/adding games.md | 3 ++-
docs/apworld specification.md | 4 ++--
2 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/docs/adding games.md b/docs/adding games.md
index 9149e9f6260b..a977109bde9f 100644
--- a/docs/adding games.md
+++ b/docs/adding games.md
@@ -87,7 +87,8 @@ The world is your game integration for the Archipelago generator, webhost, and m
information necessary for creating the items and locations to be randomized, the logic for item placement, the
datapackage information so other game clients can recognize your game data, and documentation. Your world must be
written as a Python package to be loaded by Archipelago. This is currently done by creating a fork of the Archipelago
-repository and creating a new world package in `/worlds/`.
+repository and creating a new world package in `/worlds/` (see [running from source](/docs/running%20from%20source.md)
+for setup).
The base World class can be found in [AutoWorld](/worlds/AutoWorld.py). Methods available for your world to call
during generation can be found in [BaseClasses](/BaseClasses.py) and [Fill](/Fill.py). Some examples and documentation
diff --git a/docs/apworld specification.md b/docs/apworld specification.md
index 591ce51cae1c..7e13d6ccd34d 100644
--- a/docs/apworld specification.md
+++ b/docs/apworld specification.md
@@ -46,8 +46,8 @@ which is the correct way to package your `.apworld` as a world developer. Do not
### "Build APWorlds" Launcher Component
-In the Archipelago Launcher, there is a "Build APWorlds" component that will package all world folders to `.apworld`,
-and add `archipelago.json` manifest files to them.
+In the Archipelago Launcher (on [source only](/docs/running%20from%20source.md)), there is a "Build APWorlds"
+component that will package all world folders to `.apworld`, and add `archipelago.json` manifest files to them.
These .apworld files will be output to `build/apworlds` (relative to the Archipelago root directory).
The `archipelago.json` file in each .apworld will automatically include the appropriate
`version` and `compatible_version`.
From d000c0f265c1f184df2cf44e193d98387dc34d05 Mon Sep 17 00:00:00 2001
From: Gryphonlady
Date: Tue, 10 Mar 2026 13:23:25 -0500
Subject: [PATCH 41/84] Docs: Update plando_en.md with item group example
(#6024)
* Update plando_en.md with item group example
Added example YAML block for item placement using an item group, including recommendation of use of `true` value with item groups to avoid unintended behaviors, with an example of the same. Adjustments more than welcome!
* Made clarifying revision to description of Generator handling of item groups
Clarified the behavior of the Generator regarding item creation when item groups are used in plando.
---
worlds/generic/docs/plando_en.md | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
diff --git a/worlds/generic/docs/plando_en.md b/worlds/generic/docs/plando_en.md
index 69f59c739eda..625601d96b38 100644
--- a/worlds/generic/docs/plando_en.md
+++ b/worlds/generic/docs/plando_en.md
@@ -216,6 +216,28 @@ dungeon major item chests. Because the from_pool value is `false`, a copy of the
while the originals remain in the item pool to be shuffled. The second block will place the Kokiri Sword in the Deku
Tree Slingshot Chest, again not from the pool.
+```yaml
+ plando_items:
+ # Example block - Hollow Knight
+ - items:
+ Claw : true
+ world:
+ - BobsWitness
+ - BobsRogueLegacy
+```
+This block will attempt to place all items in the Claw item group into any locations within the game slots named
+"BobsWitness" and "BobsRogueLegacy."
+
+**NOTE:** As item groups may contain items that are not currently present in the item pool, use of `true` with
+item groups, as shown here, is strongly recommended to avoid creation of unintended items.
+
+For example, the Claw item group for Hollow Knight includes Mantis_Claw, Left_Mantis_Claw, and Right_Mantis_Claw.
+Depending on a different yaml setting, the Generator will create either one Mantis_Claw item, or one each of the
+Left_Mantis_Claw and Right_Mantis_Claw items. By default, the Generator will create any missing item(s) in addition
+to using the intended item(s), resulting in placement of all three items from the item group: Mantis_Claw,
+Left_Mantis_Claw and Right_Mantis_Claw. Use of the true value, as shown in the example, restricts the Generator to
+using only the items from the item group that are already present in the item pool.
+
## Boss Plando
This is currently only supported by A Link to the Past and Kirby's Dream Land 3. Boss plando allows a player to place a
From f00d29e07203518bae5d87f463284830b36f8ff4 Mon Sep 17 00:00:00 2001
From: black-sliver <59490463+black-sliver@users.noreply.github.com>
Date: Tue, 10 Mar 2026 18:56:23 +0000
Subject: [PATCH 42/84] Tests: fix race in test hosting shutdown (#5987)
---
test/hosting/webhost.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/test/hosting/webhost.py b/test/hosting/webhost.py
index 286ef63a5583..025044555129 100644
--- a/test/hosting/webhost.py
+++ b/test/hosting/webhost.py
@@ -190,6 +190,7 @@ def stop_room(app_client: "FlaskClient",
if address:
room.timeout = original_timeout
room.last_activity = new_last_activity
+ room.commands.clear() # make sure there is no leftover /exit
print("timeout restored")
From 3235863f2e759d7eeeaabdfba4edd96de42cd12b Mon Sep 17 00:00:00 2001
From: black-sliver <59490463+black-sliver@users.noreply.github.com>
Date: Tue, 10 Mar 2026 18:57:04 +0000
Subject: [PATCH 43/84] WebHost: add stats show cli command (#5995)
Usage: flask -A "WebHost:get_app()" stats show
Currently only shows sum and top10 biggest games packages.
---
WebHostLib/__init__.py | 2 ++
WebHostLib/cli/__init__.py | 8 ++++++++
WebHostLib/cli/stats.py | 36 ++++++++++++++++++++++++++++++++++++
3 files changed, 46 insertions(+)
create mode 100644 WebHostLib/cli/__init__.py
create mode 100644 WebHostLib/cli/stats.py
diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py
index f856eea4c538..d10c17bff8ad 100644
--- a/WebHostLib/__init__.py
+++ b/WebHostLib/__init__.py
@@ -11,6 +11,7 @@
from werkzeug.routing import BaseConverter
from Utils import title_sorted, get_file_safe_name
+from .cli import CLI
UPLOAD_FOLDER = os.path.relpath('uploads')
LOGS_FOLDER = os.path.relpath('logs')
@@ -64,6 +65,7 @@
cache = Cache()
Compress(app)
+CLI(app)
def to_python(value: str) -> uuid.UUID:
diff --git a/WebHostLib/cli/__init__.py b/WebHostLib/cli/__init__.py
new file mode 100644
index 000000000000..a210e1475c1b
--- /dev/null
+++ b/WebHostLib/cli/__init__.py
@@ -0,0 +1,8 @@
+from flask import Flask
+
+
+class CLI:
+ def __init__(self, app: Flask) -> None:
+ from .stats import stats_cli
+
+ app.cli.add_command(stats_cli)
diff --git a/WebHostLib/cli/stats.py b/WebHostLib/cli/stats.py
new file mode 100644
index 000000000000..85edfb4348ec
--- /dev/null
+++ b/WebHostLib/cli/stats.py
@@ -0,0 +1,36 @@
+import click
+from flask.cli import AppGroup
+from pony.orm import raw_sql
+
+from Utils import format_SI_prefix
+
+stats_cli = AppGroup("stats")
+
+
+@stats_cli.command("show")
+def show() -> None:
+ from pony.orm import db_session, select
+
+ from WebHostLib.models import GameDataPackage
+
+ total_games_package_count: int = 0
+ total_games_package_size: int
+ top_10_package_sizes: list[tuple[int, str]] = []
+
+ with db_session:
+ data_length = raw_sql("LENGTH(data)")
+ data_length_desc = raw_sql("LENGTH(data) DESC")
+ data_length_sum = raw_sql("SUM(LENGTH(data))")
+ total_games_package_count = GameDataPackage.select().count()
+ total_games_package_size = select(data_length_sum for _ in GameDataPackage).first() # type: ignore
+ top_10_package_sizes = list(
+ select((data_length, dp.checksum) for dp in GameDataPackage) # type: ignore
+ .order_by(lambda _, _2: data_length_desc)
+ .limit(10)
+ )
+
+ click.echo(f"Total number of games packages: {total_games_package_count}")
+ click.echo(f"Total size of games packages: {format_SI_prefix(total_games_package_size, power=1024)}B")
+ click.echo(f"Top {len(top_10_package_sizes)} biggest games packages:")
+ for size, checksum in top_10_package_sizes:
+ click.echo(f" {checksum}: {size:>8d}")
From 47e581bc306e1ffd80ef0293cceb58a2372f6836 Mon Sep 17 00:00:00 2001
From: Fabian Dill
Date: Tue, 10 Mar 2026 20:04:27 +0100
Subject: [PATCH 44/84] LttP: add manifest (#6005)
---
worlds/AutoWorld.py | 2 +-
worlds/alttp/Rom.py | 3 +--
worlds/alttp/archipelago.json | 6 ++++++
3 files changed, 8 insertions(+), 3 deletions(-)
create mode 100644 worlds/alttp/archipelago.json
diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py
index 327746f1ce52..04f0b61ff8b9 100644
--- a/worlds/AutoWorld.py
+++ b/worlds/AutoWorld.py
@@ -363,7 +363,7 @@ def __init__(self, multiworld: "MultiWorld", player: int):
def __getattr__(self, item: str) -> Any:
if item == "settings":
- return self.__class__.settings
+ return getattr(self.__class__, item)
raise AttributeError
# overridable methods that get called by Main.py, sorted by execution order
diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py
index 45b3ad39d9a6..8acff214f24b 100644
--- a/worlds/alttp/Rom.py
+++ b/worlds/alttp/Rom.py
@@ -1699,8 +1699,7 @@ def get_reveal_bytes(itemName):
# set rom name
# 21 bytes
- from Utils import __version__
- rom.name = bytearray(f'AP{__version__.replace(".", "")[0:3]}_{player}_{multiworld.seed:11}\0', 'utf8')[:21]
+ rom.name = bytearray(f'AP{local_world.world_version.as_simple_string().replace(".", "")[0:3]}_{player}_{multiworld.seed:11}\0', 'utf8')[:21]
rom.name.extend([0] * (21 - len(rom.name)))
rom.write_bytes(0x7FC0, rom.name)
diff --git a/worlds/alttp/archipelago.json b/worlds/alttp/archipelago.json
new file mode 100644
index 000000000000..19bbc0321354
--- /dev/null
+++ b/worlds/alttp/archipelago.json
@@ -0,0 +1,6 @@
+{
+ "game": "A Link to the Past",
+ "minimum_ap_version": "0.6.6",
+ "world_version": "5.1.0",
+ "authors": ["Berserker"]
+}
From 56c2272bfd7921bd1f42577583295ffd053589e3 Mon Sep 17 00:00:00 2001
From: Rjosephson
Date: Tue, 10 Mar 2026 13:05:59 -0600
Subject: [PATCH 45/84] RoR2: Seekers of the Storm (SOTS) DLC Support (#5569)
---
worlds/ror2/__init__.py | 48 +++++++++++++---
worlds/ror2/archipelago.json | 6 ++
worlds/ror2/docs/en_Risk of Rain 2.md | 11 +++-
worlds/ror2/docs/setup_en.md | 8 +++
worlds/ror2/locations.py | 14 ++++-
worlds/ror2/options.py | 38 ++++++++++++-
worlds/ror2/regions.py | 75 ++++++++++++++++++++----
worlds/ror2/ror2environments.py | 61 +++++++++++++++++---
worlds/ror2/rules.py | 79 ++++++++++++++------------
worlds/ror2/test/test_any_goal.py | 18 ++++--
worlds/ror2/test/test_falseson_goal.py | 17 ++++++
11 files changed, 303 insertions(+), 72 deletions(-)
create mode 100644 worlds/ror2/archipelago.json
create mode 100644 worlds/ror2/test/test_falseson_goal.py
diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py
index 7873ae54bbba..f52ff789eb6e 100644
--- a/worlds/ror2/__init__.py
+++ b/worlds/ror2/__init__.py
@@ -4,7 +4,10 @@
from .locations import RiskOfRainLocation, item_pickups, get_locations
from .rules import set_rules
from .ror2environments import environment_vanilla_table, environment_vanilla_orderedstages_table, \
- environment_sotv_orderedstages_table, environment_sotv_table, collapse_dict_list_vertical, shift_by_offset
+ environment_sotv_orderedstages_table, environment_sotv_table, environment_sost_orderedstages_table, \
+ environment_sost_table, collapse_dict_list_vertical, shift_by_offset, environment_vanilla_variants_table, \
+ environment_vanilla_variant_orderedstages_table, environment_sots_variants_table, \
+ environment_sots_variants_orderedstages_table
from BaseClasses import Item, ItemClassification, Tutorial
from .options import ItemWeights, ROR2Options, ror2_option_groups
@@ -46,7 +49,7 @@ class RiskOfRainWorld(World):
}
location_name_to_id = item_pickups
- required_client_version = (0, 5, 0)
+ required_client_version = (0, 6, 4)
web = RiskOfWeb()
total_revivals: int
@@ -62,7 +65,9 @@ def generate_early(self) -> None:
scavengers=self.options.scavengers_per_stage.value,
scanners=self.options.scanner_per_stage.value,
altars=self.options.altars_per_stage.value,
- dlc_sotv=bool(self.options.dlc_sotv.value)
+ dlc_sotv=bool(self.options.dlc_sotv.value),
+ dlc_sots=bool(self.options.dlc_sots.value),
+ stage_variants=bool(self.options.stage_variants)
)
)
self.total_revivals = int(self.options.total_revivals.value / 100 *
@@ -71,6 +76,8 @@ def generate_early(self) -> None:
self.total_revivals -= 1
if self.options.victory == "voidling" and not self.options.dlc_sotv:
self.options.victory.value = self.options.victory.option_any
+ if self.options.victory == "falseson" and not self.options.dlc_sots:
+ self.options.victory.value = self.options.victory.option_any
def create_regions(self) -> None:
@@ -105,16 +112,39 @@ def create_items(self) -> None:
# figure out all available ordered stages for each tier
environment_available_orderedstages_table = environment_vanilla_orderedstages_table
+ environments_pool = shift_by_offset(environment_vanilla_table, environment_offset)
+ # Vanilla Variants
+ if self.options.stage_variants:
+ environment_available_orderedstages_table = \
+ collapse_dict_list_vertical(environment_available_orderedstages_table,
+ environment_vanilla_variant_orderedstages_table)
if self.options.dlc_sotv:
environment_available_orderedstages_table = \
collapse_dict_list_vertical(environment_available_orderedstages_table,
environment_sotv_orderedstages_table)
+ if self.options.dlc_sots:
+ environment_available_orderedstages_table = \
+ collapse_dict_list_vertical(environment_available_orderedstages_table,
+ environment_sost_orderedstages_table)
+ if self.options.dlc_sots and self.options.stage_variants:
+ environment_available_orderedstages_table = \
+ collapse_dict_list_vertical(environment_available_orderedstages_table,
+ environment_sots_variants_orderedstages_table)
- environments_pool = shift_by_offset(environment_vanilla_table, environment_offset)
-
+ if self.options.stage_variants:
+ environment_offset_table = shift_by_offset(environment_vanilla_variants_table, environment_offset)
+ environments_pool = {**environments_pool, **environment_offset_table}
if self.options.dlc_sotv:
environment_offset_table = shift_by_offset(environment_sotv_table, environment_offset)
environments_pool = {**environments_pool, **environment_offset_table}
+ if self.options.dlc_sots:
+ environment_offset_table = shift_by_offset(environment_sost_table, environment_offset)
+ environments_pool = {**environments_pool, **environment_offset_table}
+ # SOTS Variant Environments
+ if self.options.dlc_sots and self.options.stage_variants:
+ environment_offset_table = shift_by_offset(environment_sots_variants_table, environment_offset)
+ environments_pool = {**environments_pool, **environment_offset_table}
+
# 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]))
@@ -146,7 +176,9 @@ def create_items(self) -> None:
scavengers=self.options.scavengers_per_stage.value,
scanners=self.options.scanner_per_stage.value,
altars=self.options.altars_per_stage.value,
- dlc_sotv=bool(self.options.dlc_sotv.value)
+ dlc_sotv=bool(self.options.dlc_sotv.value),
+ dlc_sots=bool(self.options.dlc_sots.value),
+ stage_variants=bool(self.options.stage_variants)
)
)
# Create junk items
@@ -223,7 +255,7 @@ def fill_slot_data(self) -> Dict[str, Any]:
"chests_per_stage", "shrines_per_stage", "scavengers_per_stage",
"scanner_per_stage", "altars_per_stage", "total_revivals",
"start_with_revive", "final_stage_death", "death_link", "require_stages",
- "progressive_stages", casing="camel")
+ "progressive_stages", "stage_variants", "show_seer_portals", casing="camel")
return {
**options_dict,
"seed": "".join(self.random.choice(string.digits) for _ in range(16)),
@@ -254,7 +286,7 @@ def create_events(self) -> None:
event_loc.place_locked_item(RiskOfRainItem("Stage 5", ItemClassification.progression, None, self.player))
event_loc.show_in_spoiler = False
event_region.locations.append(event_loc)
- event_loc.access_rule = lambda state: state.has("Sky Meadow", self.player)
+ event_loc.access_rule = lambda state: state.has("Sky Meadow", self.player) or state.has("Helminth Hatchery", self.player)
victory_region = self.multiworld.get_region("Victory", self.player)
victory_event = RiskOfRainLocation(self.player, "Victory", None, victory_region)
diff --git a/worlds/ror2/archipelago.json b/worlds/ror2/archipelago.json
new file mode 100644
index 000000000000..78c54c1420c0
--- /dev/null
+++ b/worlds/ror2/archipelago.json
@@ -0,0 +1,6 @@
+{
+ "game": "Risk of Rain 2",
+ "minimum_ap_version": "0.6.4",
+ "world_version": "1.5.0",
+ "authors": ["Kindasneaki"]
+}
\ No newline at end of file
diff --git a/worlds/ror2/docs/en_Risk of Rain 2.md b/worlds/ror2/docs/en_Risk of Rain 2.md
index 651c89a33923..2acd133e26f7 100644
--- a/worlds/ror2/docs/en_Risk of Rain 2.md
+++ b/worlds/ror2/docs/en_Risk of Rain 2.md
@@ -88,12 +88,21 @@ Explore Mode items are:
* `Commencement`
* `All the Hidden Realms`
-Dlc_Sotv items
+DLC Survivors of the Void (SOTV) items
* `Siphoned Forest`
* `Aphelian Sanctuary`
* `Sulfur Pools`
* `Void Locus`
+DLC Seekers of the Storm (SOTS) items
+
+* `Shattered Abodes`, `Vicious Falls`, `Disturbed Impact`
+* `Reformed Altar`
+* `Treeborn Colony`, `Golden Dieback`
+* `Prime Meridian`
+* `Helminth Hatchery`
+
+
When an explore item is granted, it will unlock that environment and will now be accessible! The
game will still pick randomly which environment is next, but it will first check to see if they are available. If you have
multiple of the next environments unlocked, it will weight the game to have a ***higher chance*** to go to one you
diff --git a/worlds/ror2/docs/setup_en.md b/worlds/ror2/docs/setup_en.md
index 6acf2654a8b2..cef0885970e1 100644
--- a/worlds/ror2/docs/setup_en.md
+++ b/worlds/ror2/docs/setup_en.md
@@ -23,6 +23,13 @@ all necessary dependencies as well.
Click on the `Start modded` button in the top left in `r2modman` to start the game with the Archipelago mod installed.
+### Troubleshooting
+
+* The mod doesn't show up in game!
+ * `r2modman` looks for the game at its default directory. If you have the game installed somewhere else,
+ you can update `r2modman` by going to `Settings > Change Risk of Rain 2 folder`
+ and selecting the correct directory.
+
## Configuring your YAML File
### What is a YAML and why do I need one?
You can see the [basic multiworld setup guide](/tutorial/Archipelago/setup/en) here on the Archipelago website to learn
@@ -59,6 +66,7 @@ also optionally connect to the multiworld using the text client, which can be fo
### In-Game Commands
These commands are to be used in-game by using ``Ctrl + Alt + ` `` and then typing the following:
+ - `archipelago_reconnect` Reconnect to AP.
- `archipelago_connect [password]` example: "archipelago_connect archipelago.gg 38281 SlotName".
- `archipelago_deathlink true/false` Toggle deathlink.
- `archipelago_disconnect` Disconnect from AP.
diff --git a/worlds/ror2/locations.py b/worlds/ror2/locations.py
index 13077b3e149c..32972311520b 100644
--- a/worlds/ror2/locations.py
+++ b/worlds/ror2/locations.py
@@ -3,7 +3,8 @@
from .options import TotalLocations, ChestsPerEnvironment, ShrinesPerEnvironment, ScavengersPerEnvironment, \
ScannersPerEnvironment, AltarsPerEnvironment
from .ror2environments import compress_dict_list_horizontal, environment_vanilla_orderedstages_table, \
- environment_sotv_orderedstages_table
+ environment_sotv_orderedstages_table, environment_sost_orderedstages_table, \
+ environment_sots_variants_orderedstages_table, environment_vanilla_variant_orderedstages_table
class RiskOfRainLocation(Location):
@@ -57,13 +58,20 @@ def get_environment_locations(chests: int, shrines: int, scavengers: int, scanne
return locations
-def get_locations(chests: int, shrines: int, scavengers: int, scanners: int, altars: int, dlc_sotv: bool) \
+def get_locations(chests: int, shrines: int, scavengers: int, scanners: int, altars: int, dlc_sotv: bool,
+ dlc_sots: bool, stage_variants: bool) \
-> Dict[str, int]:
"""Get a dictionary of locations for the orderedstage environments with the locations from the parameters."""
locations = {}
orderedstages = compress_dict_list_horizontal(environment_vanilla_orderedstages_table)
+ if stage_variants:
+ orderedstages.update(compress_dict_list_horizontal(environment_vanilla_variant_orderedstages_table))
if dlc_sotv:
orderedstages.update(compress_dict_list_horizontal(environment_sotv_orderedstages_table))
+ if dlc_sots:
+ orderedstages.update(compress_dict_list_horizontal(environment_sost_orderedstages_table))
+ if dlc_sots and stage_variants:
+ orderedstages.update(compress_dict_list_horizontal(environment_sots_variants_orderedstages_table))
# for every environment, generate the respective locations
for environment_name, environment_index in orderedstages.items():
locations.update(get_environment_locations(
@@ -86,4 +94,6 @@ def get_locations(chests: int, shrines: int, scavengers: int, scanners: int, alt
scanners=ScannersPerEnvironment.range_end,
altars=AltarsPerEnvironment.range_end,
dlc_sotv=True,
+ dlc_sots=True,
+ stage_variants=True
))
diff --git a/worlds/ror2/options.py b/worlds/ror2/options.py
index 381c5942b07b..876a67b7fbc6 100644
--- a/worlds/ror2/options.py
+++ b/worlds/ror2/options.py
@@ -22,8 +22,9 @@ class Goal(Choice):
class Victory(Choice):
"""
Mithrix: Defeat Mithrix in Commencement
- Voidling: Defeat the Voidling in The Planetarium (DLC required! Will select any if not enabled.)
+ Voidling: Defeat the Voidling in The Planetarium (SOTV DLC required! Will select any if not enabled.)
Limbo: Defeat the Scavenger in Hidden Realm: A Moment, Whole
+ Falseson: Defeat False son and gift an item to the altar in Prime Meridian (SOTS DLC required! Will select any if not enabled.)
Any: Any victory in the game will count. See Final Stage Death for additional ways.
"""
display_name = "Victory Condition"
@@ -31,6 +32,7 @@ class Victory(Choice):
option_mithrix = 1
option_voidling = 2
option_limbo = 3
+ option_falseson = 4
default = 0
@@ -138,18 +140,26 @@ class FinalStageDeath(Toggle):
If not use the following to tell if final stage death will count:
Victory: mithrix - only dying in Commencement will count.
Victory: voidling - only dying in The Planetarium will count.
- Victory: limbo - Obliterating yourself will count."""
+ Victory: limbo - Obliterating yourself will count.
+ Victory: falseson - only dying in Prime Meridian will count."""
display_name = "Final Stage Death is Win"
class DLC_SOTV(Toggle):
"""
- Enable if you are using SOTV DLC.
+ Enable if you are using Survivors of the Void DLC.
Affects environment availability for Explore Mode.
Adds Void Items into the item pool
"""
display_name = "Enable DLC - SOTV"
+class DLC_SOTS(Toggle):
+ """
+ Enable if you are using Seekers of the Storm DLC.
+ Affects environment availability for Explore Mode.
+ """
+ display_name = "Enable DLC - SOTS"
+
class RequireStages(DefaultOnToggle):
"""Add Stage items to the pool to block access to the next set of environments."""
@@ -162,6 +172,23 @@ class ProgressiveStages(DefaultOnToggle):
display_name = "Progressive Stages"
+class StageVariants(Toggle):
+ """Enable if you want to include stage variants in the environment pool.
+ Stages included are:
+ - Distant Roost (2)
+ - Titanic Plains (2)
+ SOTS DLC Enabled:
+ - Vicious Falls
+ - Shattered Abodes
+ - Golden Dieback"""
+ display_name = "Include Stage Variants"
+
+
+class ShowSeerPortals(DefaultOnToggle):
+ """Shows Seer Portals at the teleporter to allow choosing the next environment."""
+ display_name = "Show Seer Portals"
+
+
class GreenScrap(Range):
"""Weight of Green Scraps in the item pool.
@@ -384,6 +411,8 @@ class ItemWeights(Choice):
AltarsPerEnvironment,
RequireStages,
ProgressiveStages,
+ StageVariants,
+ ShowSeerPortals,
]),
OptionGroup("Classic Mode Options", [
TotalLocations,
@@ -427,8 +456,11 @@ class ROR2Options(PerGameCommonOptions):
start_with_revive: StartWithRevive
final_stage_death: FinalStageDeath
dlc_sotv: DLC_SOTV
+ dlc_sots: DLC_SOTS
require_stages: RequireStages
progressive_stages: ProgressiveStages
+ stage_variants: StageVariants
+ show_seer_portals: ShowSeerPortals
death_link: DeathLink
item_pickup_step: ItemPickupStep
shrine_use_step: ShrineUseStep
diff --git a/worlds/ror2/regions.py b/worlds/ror2/regions.py
index def29b47286b..780f66bcac90 100644
--- a/worlds/ror2/regions.py
+++ b/worlds/ror2/regions.py
@@ -18,13 +18,10 @@ def create_explore_regions(ror2_world: "RiskOfRainWorld") -> None:
multiworld = ror2_world.multiworld
# Default Locations
non_dlc_regions: Dict[str, RoRRegionData] = {
- "Menu": RoRRegionData(None, ["Distant Roost", "Distant Roost (2)",
- "Titanic Plains", "Titanic Plains (2)",
+ "Menu": RoRRegionData(None, ["Distant Roost", "Titanic Plains",
"Verdant Falls"]),
"Distant Roost": RoRRegionData([], ["OrderedStage_1"]),
- "Distant Roost (2)": RoRRegionData([], ["OrderedStage_1"]),
"Titanic Plains": RoRRegionData([], ["OrderedStage_1"]),
- "Titanic Plains (2)": RoRRegionData([], ["OrderedStage_1"]),
"Verdant Falls": RoRRegionData([], ["OrderedStage_1"]),
"Abandoned Aqueduct": RoRRegionData([], ["OrderedStage_2"]),
"Wetland Aspect": RoRRegionData([], ["OrderedStage_2"]),
@@ -35,12 +32,30 @@ def create_explore_regions(ror2_world: "RiskOfRainWorld") -> None:
"Sundered Grove": RoRRegionData([], ["OrderedStage_4"]),
"Sky Meadow": RoRRegionData([], ["Hidden Realm: Bulwark's Ambry", "OrderedStage_5"]),
}
+ non_dlc_variant_regions: Dict[str, RoRRegionData] = {
+ "Distant Roost (2)": RoRRegionData([], ["OrderedStage_1"]),
+ "Titanic Plains (2)": RoRRegionData([], ["OrderedStage_1"]),
+ }
# SOTV Regions
- dlc_regions: Dict[str, RoRRegionData] = {
+ dlc_sotv_regions: Dict[str, RoRRegionData] = {
"Siphoned Forest": RoRRegionData([], ["OrderedStage_1"]),
"Aphelian Sanctuary": RoRRegionData([], ["OrderedStage_2"]),
"Sulfur Pools": RoRRegionData([], ["OrderedStage_3"])
}
+
+ dlc_sost_regions: Dict[str, RoRRegionData] = {
+ "Shattered Abodes": RoRRegionData([], ["OrderedStage_1"]),
+ "Reformed Altar": RoRRegionData([], ["OrderedStage_2", "Treeborn Colony"]),
+ "Treeborn Colony": RoRRegionData([], ["OrderedStage_3", "Prime Meridian"]),
+ "Helminth Hatchery": RoRRegionData([], ["Hidden Realm: Bulwark's Ambry", "OrderedStage_5"]),
+ }
+
+ dlc_sots_variant_regions: Dict[str, RoRRegionData] = {
+ "Viscous Falls": RoRRegionData([], ["OrderedStage_1"]),
+ "Disturbed Impact": RoRRegionData([], ["OrderedStage_1"]),
+ "Golden Dieback": RoRRegionData([], ["OrderedStage_3", "Prime Meridian"]),
+ }
+
other_regions: Dict[str, RoRRegionData] = {
"Commencement": RoRRegionData(None, ["Victory", "Petrichor V"]),
"OrderedStage_5": RoRRegionData(None, ["Hidden Realm: A Moment, Fractured",
@@ -61,10 +76,15 @@ def create_explore_regions(ror2_world: "RiskOfRainWorld") -> None:
"Hidden Realm: Bazaar Between Time": RoRRegionData(None, ["Void Fields"]),
"Hidden Realm: Gilded Coast": RoRRegionData(None, None)
}
- dlc_other_regions: Dict[str, RoRRegionData] = {
+ dlc_sotv_other_regions: Dict[str, RoRRegionData] = {
"The Planetarium": RoRRegionData(None, ["Victory", "Petrichor V"]),
"Void Locus": RoRRegionData(None, ["The Planetarium"])
}
+
+ dlc_sost_other_regions: Dict[str, RoRRegionData] = {
+ "Prime Meridian": RoRRegionData(None, ["Victory", "Petrichor V"]),
+ }
+
# Totals of each item
chests = int(ror2_options.chests_per_stage)
shrines = int(ror2_options.shrines_per_stage)
@@ -72,8 +92,14 @@ def create_explore_regions(ror2_world: "RiskOfRainWorld") -> None:
scanners = int(ror2_options.scanner_per_stage)
newt = int(ror2_options.altars_per_stage)
all_location_regions = {**non_dlc_regions}
+ if ror2_options.stage_variants:
+ all_location_regions.update(non_dlc_variant_regions)
if ror2_options.dlc_sotv:
- all_location_regions = {**non_dlc_regions, **dlc_regions}
+ all_location_regions.update(dlc_sotv_regions)
+ if ror2_options.dlc_sots:
+ all_location_regions.update(dlc_sost_regions)
+ if ror2_options.dlc_sots and ror2_options.stage_variants:
+ all_location_regions.update(dlc_sots_variant_regions)
# Locations
for key in all_location_regions:
@@ -99,25 +125,52 @@ def create_explore_regions(ror2_world: "RiskOfRainWorld") -> None:
all_location_regions[key].locations.append(f"{key}: Newt Altar {i + 1}")
regions_pool: Dict = {**all_location_regions, **other_regions}
- # DLC Locations
+ # Non DLC Variant Locations
+ if ror2_options.stage_variants:
+ non_dlc_regions["Menu"].region_exits.append("Distant Roost (2)")
+ non_dlc_regions["Menu"].region_exits.append("Titanic Plains (2)")
+ # SOTV DLC Locations
if ror2_options.dlc_sotv:
non_dlc_regions["Menu"].region_exits.append("Siphoned Forest")
other_regions["OrderedStage_1"].region_exits.append("Aphelian Sanctuary")
other_regions["OrderedStage_2"].region_exits.append("Sulfur Pools")
other_regions["Void Fields"].region_exits.append("Void Locus")
other_regions["Commencement"].region_exits.append("The Planetarium")
- regions_pool: Dict = {**all_location_regions, **other_regions, **dlc_other_regions}
+
+ # SOTS DLC Locations
+ if ror2_options.dlc_sots:
+ non_dlc_regions["Menu"].region_exits.append("Shattered Abodes")
+ other_regions["OrderedStage_1"].region_exits.append("Reformed Altar")
+ other_regions["OrderedStage_4"].region_exits.append("Helminth Hatchery")
+
+ # SOTS Variant Locations
+ if ror2_options.dlc_sots and ror2_options.stage_variants:
+ non_dlc_regions["Menu"].region_exits.append("Viscous Falls")
+ non_dlc_regions["Menu"].region_exits.append("Disturbed Impact")
+ dlc_sost_regions["Reformed Altar"].region_exits.append("Golden Dieback")
+
+ if ror2_options.dlc_sotv:
+ regions_pool.update(dlc_sotv_other_regions)
+ if ror2_options.dlc_sots:
+ regions_pool.update(dlc_sost_other_regions)
# Check to see if Victory needs to be removed from regions
if ror2_options.victory == "mithrix":
other_regions["Hidden Realm: A Moment, Whole"].region_exits.pop(0)
- dlc_other_regions["The Planetarium"].region_exits.pop(0)
+ dlc_sotv_other_regions["The Planetarium"].region_exits.pop(0)
+ dlc_sost_other_regions["Prime Meridian"].region_exits.pop(0)
elif ror2_options.victory == "voidling":
other_regions["Commencement"].region_exits.pop(0)
other_regions["Hidden Realm: A Moment, Whole"].region_exits.pop(0)
+ dlc_sost_other_regions["Prime Meridian"].region_exits.pop(0)
elif ror2_options.victory == "limbo":
other_regions["Commencement"].region_exits.pop(0)
- dlc_other_regions["The Planetarium"].region_exits.pop(0)
+ dlc_sotv_other_regions["The Planetarium"].region_exits.pop(0)
+ dlc_sost_other_regions["Prime Meridian"].region_exits.pop(0)
+ elif ror2_options.victory == "falseson":
+ other_regions["Commencement"].region_exits.pop(0)
+ other_regions["Hidden Realm: A Moment, Whole"].region_exits.pop(0)
+ dlc_sotv_other_regions["The Planetarium"].region_exits.pop(0)
# Create all the regions
for name, data in regions_pool.items():
diff --git a/worlds/ror2/ror2environments.py b/worlds/ror2/ror2environments.py
index 61707b336241..40e63a35b13b 100644
--- a/worlds/ror2/ror2environments.py
+++ b/worlds/ror2/ror2environments.py
@@ -4,11 +4,14 @@
environment_vanilla_orderedstage_1_table: Dict[str, int] = {
"Distant Roost": 7, # blackbeach
- "Distant Roost (2)": 8, # blackbeach2
"Titanic Plains": 15, # golemplains
- "Titanic Plains (2)": 16, # golemplains2
"Verdant Falls": 28, # lakes
}
+environment_vanilla_variant_orderedstage_1_table: Dict[str, int] = {
+ "Distant Roost (2)": 8, # blackbeach2
+ "Titanic Plains (2)": 16, # golemplains2
+}
+
environment_vanilla_orderedstage_2_table: Dict[str, int] = {
"Abandoned Aqueduct": 17, # goolake
"Wetland Aspect": 12, # foggyswamp
@@ -54,6 +57,34 @@
"The Planetarium": 45, # voidraid
}
+environment_sost_orderstage_1_table: Dict[str, int] = {
+ "Shattered Abodes": 54, # village
+
+}
+environment_sost_variant_orderstage_1_table: Dict[str, int] = {
+ "Viscous Falls": 34, # lakesnight
+ "Disturbed Impact": 55, # villagenight
+}
+
+environment_sost_orderstage_2_table: Dict[str, int] = {
+ "Reformed Altar": 36, # lemuriantemple
+}
+
+environment_sost_orderstage_3_table: Dict[str, int] = {
+ "Treeborn Colony": 21, # habitat
+}
+environment_sost_variant_orderstage_3_table: Dict[str, int] = {
+ "Golden Dieback": 22, # habitatfall
+}
+
+environment_sost_orderstage_5_table: Dict[str, int] = {
+ "Helminth Hatchery": 23, # helminthroost
+}
+
+environment_sost_special_table: Dict[str, int] = {
+ "Prime Meridian": 40, # meridian
+}
+
X = TypeVar("X")
Y = TypeVar("Y")
@@ -100,18 +131,32 @@ def collapse_dict_list_vertical(list_of_dict_1: List[Dict[X, Y]], *args: List[Di
environment_vanilla_table = \
{**compress_dict_list_horizontal(environment_vanilla_orderedstages_table),
**environment_vanilla_hidden_realm_table, **environment_vanilla_special_table}
+# Vanilla Variants
+environment_vanilla_variant_orderedstages_table = \
+ [environment_vanilla_variant_orderedstage_1_table]
+environment_vanilla_variants_table = \
+ {**compress_dict_list_horizontal(environment_vanilla_variant_orderedstages_table)}
+# SoTV
environment_sotv_orderedstages_table = \
[environment_sotv_orderedstage_1_table, environment_sotv_orderedstage_2_table,
environment_sotv_orderedstage_3_table]
environment_sotv_table = \
{**compress_dict_list_horizontal(environment_sotv_orderedstages_table), **environment_sotv_special_table}
-
-environment_non_orderedstages_table = \
- {**environment_vanilla_hidden_realm_table, **environment_vanilla_special_table, **environment_sotv_special_table}
-environment_orderedstages_table = \
- collapse_dict_list_vertical(environment_vanilla_orderedstages_table, environment_sotv_orderedstages_table)
-environment_all_table = {**environment_vanilla_table, **environment_sotv_table}
+# SoST
+environment_sost_orderedstages_table = \
+ [environment_sost_orderstage_1_table, environment_sost_orderstage_2_table,
+ environment_sost_orderstage_3_table, {}, environment_sost_orderstage_5_table] # There is no new stage 4 in SoST
+environment_sost_table = \
+ {**compress_dict_list_horizontal(environment_sost_orderedstages_table), **environment_sost_special_table}
+# SOTS Variants
+environment_sots_variants_orderedstages_table = \
+ [environment_sost_variant_orderstage_1_table, {}, environment_sost_variant_orderstage_3_table]
+environment_sots_variants_table = \
+ {**compress_dict_list_horizontal(environment_sots_variants_orderedstages_table)}
+
+environment_all_table = {**environment_vanilla_table, **environment_sotv_table, **environment_sost_table,
+ **environment_vanilla_variants_table, **environment_sots_variants_table}
def shift_by_offset(dictionary: Dict[str, int], offset: int) -> Dict[str, int]:
diff --git a/worlds/ror2/rules.py b/worlds/ror2/rules.py
index f0ab9f28313f..d8d92ca27099 100644
--- a/worlds/ror2/rules.py
+++ b/worlds/ror2/rules.py
@@ -1,7 +1,9 @@
from worlds.generic.Rules import set_rule, add_rule
from BaseClasses import MultiWorld
from .locations import get_locations
-from .ror2environments import environment_vanilla_orderedstages_table, environment_sotv_orderedstages_table
+from .ror2environments import environment_vanilla_orderedstages_table, environment_sotv_orderedstages_table, \
+ environment_sost_orderedstages_table, environment_vanilla_variant_orderedstages_table, \
+ environment_sots_variants_orderedstages_table
from typing import Set, TYPE_CHECKING
if TYPE_CHECKING:
@@ -43,6 +45,24 @@ def has_location_access_rule(multiworld: MultiWorld, environment: str, player: i
multiworld.get_location(location_name, player).access_rule = \
lambda state: state.has(environment, player)
+def explore_environment_location_rules(table, multiworld, player, chests, shrines, newts, scavengers, scanners):
+ for i in range(len(table)):
+ for environment_name, _ in table[i].items():
+ # Make sure to go through each location
+ if scavengers == 1:
+ has_location_access_rule(multiworld, environment_name, player, scavengers, "Scavenger")
+ if scanners == 1:
+ has_location_access_rule(multiworld, environment_name, player, scanners, "Radio Scanner")
+ for chest in range(1, chests + 1):
+ has_location_access_rule(multiworld, environment_name, player, chest, "Chest")
+ for shrine in range(1, shrines + 1):
+ has_location_access_rule(multiworld, environment_name, player, shrine, "Shrine")
+ if newts > 0:
+ for newt in range(1, newts + 1):
+ has_location_access_rule(multiworld, environment_name, player, newt, "Newt Altar")
+ if i > 0:
+ has_stage_access_rule(multiworld, f"Stage {i}", i, environment_name, player)
+
def set_rules(ror2_world: "RiskOfRainWorld") -> None:
player = ror2_world.player
@@ -60,7 +80,9 @@ def set_rules(ror2_world: "RiskOfRainWorld") -> None:
scavengers=ror2_options.scavengers_per_stage.value,
scanners=ror2_options.scanner_per_stage.value,
altars=ror2_options.altars_per_stage.value,
- dlc_sotv=bool(ror2_options.dlc_sotv.value)
+ dlc_sotv=bool(ror2_options.dlc_sotv.value),
+ dlc_sots=bool(ror2_options.dlc_sots.value),
+ stage_variants=bool(ror2_options.stage_variants)
)
)
@@ -101,40 +123,25 @@ def set_rules(ror2_world: "RiskOfRainWorld") -> None:
newts = ror2_options.altars_per_stage.value
scavengers = ror2_options.scavengers_per_stage.value
scanners = ror2_options.scanner_per_stage.value
- for i in range(len(environment_vanilla_orderedstages_table)):
- for environment_name, _ in environment_vanilla_orderedstages_table[i].items():
- # Make sure to go through each location
- if scavengers == 1:
- has_location_access_rule(multiworld, environment_name, player, scavengers, "Scavenger")
- if scanners == 1:
- has_location_access_rule(multiworld, environment_name, player, scanners, "Radio Scanner")
- for chest in range(1, chests + 1):
- has_location_access_rule(multiworld, environment_name, player, chest, "Chest")
- for shrine in range(1, shrines + 1):
- has_location_access_rule(multiworld, environment_name, player, shrine, "Shrine")
- if newts > 0:
- for newt in range(1, newts + 1):
- has_location_access_rule(multiworld, environment_name, player, newt, "Newt Altar")
- if i > 0:
- has_stage_access_rule(multiworld, f"Stage {i}", i, environment_name, player)
-
+ # Vanilla stages
+ explore_environment_location_rules(environment_vanilla_orderedstages_table, multiworld, player, chests, shrines, newts,
+ scavengers, scanners)
+ # Vanilla Variant stages
+ if ror2_options.stage_variants:
+ explore_environment_location_rules(environment_vanilla_variant_orderedstages_table, multiworld, player, chests, shrines, newts,
+ scavengers, scanners)
+ # SoTv stages
if ror2_options.dlc_sotv:
- for i in range(len(environment_sotv_orderedstages_table)):
- for environment_name, _ in environment_sotv_orderedstages_table[i].items():
- # Make sure to go through each location
- if scavengers == 1:
- has_location_access_rule(multiworld, environment_name, player, scavengers, "Scavenger")
- if scanners == 1:
- has_location_access_rule(multiworld, environment_name, player, scanners, "Radio Scanner")
- for chest in range(1, chests + 1):
- has_location_access_rule(multiworld, environment_name, player, chest, "Chest")
- for shrine in range(1, shrines + 1):
- has_location_access_rule(multiworld, environment_name, player, shrine, "Shrine")
- if newts > 0:
- for newt in range(1, newts + 1):
- has_location_access_rule(multiworld, environment_name, player, newt, "Newt Altar")
- if i > 0:
- has_stage_access_rule(multiworld, f"Stage {i}", i, environment_name, player)
+ explore_environment_location_rules(environment_sotv_orderedstages_table, multiworld, player, chests, shrines,
+ newts, scavengers, scanners)
+ # SoTS stages
+ if ror2_options.dlc_sots:
+ explore_environment_location_rules(environment_sost_orderedstages_table, multiworld, player, chests, shrines,
+ newts, scavengers, scanners)
+ if ror2_options.dlc_sots and ror2_options.stage_variants:
+ explore_environment_location_rules(environment_sots_variants_orderedstages_table, multiworld, player, chests, shrines,
+ newts, scavengers, scanners)
+
has_entrance_access_rule(multiworld, "Hidden Realm: A Moment, Fractured", "Hidden Realm: A Moment, Whole",
player)
has_stage_access_rule(multiworld, "Stage 1", 1, "Hidden Realm: Bazaar Between Time", player)
@@ -147,6 +154,8 @@ def set_rules(ror2_world: "RiskOfRainWorld") -> None:
has_entrance_access_rule(multiworld, "Stage 5", "Void Locus", player)
if ror2_options.victory == "voidling":
has_all_items(multiworld, {"Stage 5", "The Planetarium"}, "Commencement", player)
+ if ror2_options.dlc_sots:
+ has_entrance_access_rule(multiworld, "Stage 5", "Prime Meridian", player)
# Win Condition
multiworld.completion_condition[player] = lambda state: state.has("Victory", player)
diff --git a/worlds/ror2/test/test_any_goal.py b/worlds/ror2/test/test_any_goal.py
index 18d49944195d..7dbd549c9f5f 100644
--- a/worlds/ror2/test/test_any_goal.py
+++ b/worlds/ror2/test/test_any_goal.py
@@ -4,23 +4,33 @@
class DLCTest(RoR2TestBase):
options = {
"dlc_sotv": "true",
- "victory": "any"
+ "victory": "any",
+ "dlc_sots": "true",
}
def test_commencement_victory(self) -> None:
- self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Victory"])
+ self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Prime Meridian",
+ "Victory"])
self.assertBeatable(False)
self.collect_by_name("Commencement")
self.assertBeatable(True)
def test_planetarium_victory(self) -> None:
- self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Victory"])
+ self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Prime Meridian",
+ "Victory"])
self.assertBeatable(False)
self.collect_by_name("The Planetarium")
self.assertBeatable(True)
def test_moment_whole_victory(self) -> None:
- self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Victory"])
+ self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Prime Meridian",
+ "Victory"])
self.assertBeatable(False)
self.collect_by_name("Hidden Realm: A Moment, Whole")
self.assertBeatable(True)
+ def test_false_son_victory(self) -> None:
+ self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Prime Meridian",
+ "Victory"])
+ self.assertBeatable(False)
+ self.collect_by_name("Prime Meridian")
+ self.assertBeatable(True)
diff --git a/worlds/ror2/test/test_falseson_goal.py b/worlds/ror2/test/test_falseson_goal.py
new file mode 100644
index 000000000000..3cf815c94255
--- /dev/null
+++ b/worlds/ror2/test/test_falseson_goal.py
@@ -0,0 +1,17 @@
+from . import RoR2TestBase
+
+
+class FalseSonGoalTest(RoR2TestBase):
+ options = {
+ "dlc_sots": "true",
+ "victory": "falseson",
+ "stage_variants": "true"
+ }
+
+ def test_false_son(self) -> None:
+ self.collect_all_but(["Prime Meridian", "Victory"])
+ self.assertFalse(self.can_reach_region("Prime Meridian"))
+ self.assertBeatable(False)
+ self.collect_by_name("Prime Meridian")
+ self.assertTrue(self.can_reach_region("Prime Meridian"))
+ self.assertBeatable(True)
From a8e926a1a9dad779599c855da3ed74d528638ba2 Mon Sep 17 00:00:00 2001
From: Mysteryem
Date: Tue, 10 Mar 2026 19:08:20 +0000
Subject: [PATCH 46/84] Core: Make Generic ER only consider the current world
in isolation (#4680)
---
entrance_rando.py | 17 ++++++++++++++---
1 file changed, 14 insertions(+), 3 deletions(-)
diff --git a/entrance_rando.py b/entrance_rando.py
index a417767036ee..47664cb685bf 100644
--- a/entrance_rando.py
+++ b/entrance_rando.py
@@ -186,9 +186,20 @@ def __init__(self, world: World, entrance_lookup: EntranceLookup, coupled: bool)
self.pairings = []
self.world = world
self.coupled = coupled
- self.collection_state = world.multiworld.get_all_state(False, True)
self.entrance_lookup = entrance_lookup
+ # Construct an 'all state', similar to MultiWorld.get_all_state(), but only for the world which is having its
+ # entrances randomized.
+ single_player_all_state = CollectionState(world.multiworld, True)
+ player = world.player
+ for item in world.multiworld.itempool:
+ if item.player == player:
+ world.collect(single_player_all_state, item)
+ for item in world.get_pre_fill_items():
+ world.collect(single_player_all_state, item)
+ single_player_all_state.sweep_for_advancements(world.get_locations())
+ self.collection_state = single_player_all_state
+
@property
def placed_regions(self) -> set[Region]:
return self.collection_state.reachable_regions[self.world.player]
@@ -226,7 +237,7 @@ def test_speculative_connection(self, source_exit: Entrance, target_entrance: En
copied_state.blocked_connections[self.world.player].remove(source_exit)
copied_state.blocked_connections[self.world.player].update(target_entrance.connected_region.exits)
copied_state.update_reachable_regions(self.world.player)
- copied_state.sweep_for_advancements()
+ copied_state.sweep_for_advancements(self.world.get_locations())
# test that at there are newly reachable randomized exits that are ACTUALLY reachable
available_randomized_exits = copied_state.blocked_connections[self.world.player]
for _exit in available_randomized_exits:
@@ -402,7 +413,7 @@ def do_placement(source_exit: Entrance, target_entrance: Entrance) -> None:
placed_exits, paired_entrances = er_state.connect(source_exit, target_entrance)
# propagate new connections
er_state.collection_state.update_reachable_regions(world.player)
- er_state.collection_state.sweep_for_advancements()
+ er_state.collection_state.sweep_for_advancements(world.get_locations())
if on_connect:
change = on_connect(er_state, placed_exits, paired_entrances)
if change:
From 3c802d03a1423320ac454c4f05e23dd4e2a304fe Mon Sep 17 00:00:00 2001
From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
Date: Tue, 10 Mar 2026 15:11:23 -0400
Subject: [PATCH 47/84] DS3: Use remaining_fill instead of custom fill (#4397)
---------
Co-authored-by: Mysteryem
---
Fill.py | 1 +
worlds/dark_souls_3/__init__.py | 25 +++++++------------------
2 files changed, 8 insertions(+), 18 deletions(-)
diff --git a/Fill.py b/Fill.py
index 48ed7253d9d1..7bd575662708 100644
--- a/Fill.py
+++ b/Fill.py
@@ -280,6 +280,7 @@ def location_can_fill_item(location_to_fill: Location, item_to_fill: Item):
item_to_place = itempool.pop()
spot_to_fill: typing.Optional[Location] = None
+ # going through locations in the same order as the provided `locations` argument
for i, location in enumerate(locations):
if location_can_fill_item(location, item_to_place):
# popping by index is faster than removing by content,
diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py
index 8c9716c03d5a..4ee972be9cb2 100644
--- a/worlds/dark_souls_3/__init__.py
+++ b/worlds/dark_souls_3/__init__.py
@@ -6,6 +6,7 @@
from typing import cast, Any, Callable, Dict, Set, List, Optional, TextIO, Union
from BaseClasses import CollectionState, MultiWorld, Region, Location, LocationProgressType, Entrance, Tutorial, ItemClassification
+from Fill import remaining_fill
from worlds.AutoWorld import World, WebWorld
from worlds.generic.Rules import CollectionRule, ItemRule, add_rule, add_item_rule
@@ -1473,6 +1474,7 @@ def smooth_items(item_order: List[Union[DS3ItemData, DarkSouls3Item]]) -> None:
f"contain smoothed items, but only {len(converted_item_order)} items to smooth."
)
+ sorted_spheres = []
for sphere in locations_by_sphere:
locations = [loc for loc in sphere if loc.item.name in names]
@@ -1480,12 +1482,12 @@ def smooth_items(item_order: List[Union[DS3ItemData, DarkSouls3Item]]) -> None:
offworld = ds3_world._shuffle([loc for loc in locations if loc.game != "Dark Souls III"])
onworld = sorted((loc for loc in locations if loc.game == "Dark Souls III"),
key=lambda loc: loc.data.region_value)
-
# Give offworld regions the last (best) items within a given sphere
- for location in onworld + offworld:
- new_item = ds3_world._pop_item(location, converted_item_order)
- location.item = new_item
- new_item.location = location
+ sorted_spheres.extend(onworld)
+ sorted_spheres.extend(offworld)
+
+ converted_item_order.reverse()
+ remaining_fill(multiworld, sorted_spheres, converted_item_order, name="DS3 Smoothing", check_location_can_fill=True)
if ds3_world.options.smooth_upgrade_items:
base_names = {
@@ -1518,19 +1520,6 @@ def _shuffle(self, seq: Sequence) -> List:
self.random.shuffle(copy)
return copy
- def _pop_item(
- self,
- location: Location,
- items: List[DarkSouls3Item]
- ) -> DarkSouls3Item:
- """Returns the next item in items that can be assigned to location."""
- for i, item in enumerate(items):
- if location.can_fill(self.multiworld.state, item, False):
- return items.pop(i)
-
- # If we can't find a suitable item, give up and assign an unsuitable one.
- return items.pop(0)
-
def _get_our_locations(self) -> List[DarkSouls3Location]:
return cast(List[DarkSouls3Location], self.multiworld.get_locations(self.player))
From 03b638d027edc7a5844d05bd99242ba4c3b140e0 Mon Sep 17 00:00:00 2001
From: Ixrec
Date: Tue, 10 Mar 2026 19:49:47 +0000
Subject: [PATCH 48/84] Docs: Reword 'could be generated from json' to avoid
encouraging slow world loads (#5960)
---
docs/world api.md | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/docs/world api.md b/docs/world api.md
index 2df7b127447e..4e2354930445 100644
--- a/docs/world api.md
+++ b/docs/world api.md
@@ -491,9 +491,10 @@ class MyGameWorld(World):
base_id = 1234
# instead of dynamic numbering, IDs could be part of data
- # The following two dicts are required for the generation to know which
- # items exist. They could be generated from json or something else. They can
- # include events, but don't have to since events will be placed manually.
+ # The following two dicts are required for the generation to know which items exist.
+ # They can be generated with arbitrary code during world load, but keep in mind that
+ # anything expensive (e.g. parsing non-python data files) will delay world loading.
+ # They can include events, but don't have to since events will be placed manually.
item_name_to_id = {name: id for
id, name in enumerate(mygame_items, base_id)}
location_name_to_id = {name: id for
From 3016379b85efdb336a25f82c146933476380f62d Mon Sep 17 00:00:00 2001
From: Mysteryem
Date: Tue, 10 Mar 2026 22:06:44 +0000
Subject: [PATCH 49/84] KH2: Fix nondeterministic generation when
CasualBounties is enabled (#5967)
When CasualBounties was enabled, the location names in
`exclusion_table["HitlistCasual"]` would be iterated into
`self.random_super_boss_list` in `generate_early`, but
`exclusion_table["HitlistCasual"]` was a `set`, so its iteration order
would vary on each generation, even with same seed.
Random location names would be picked from `self.random_super_boss_list`
to place Bounty items at, so different locations could be picked on each
generation with the same seed.
`exclusion_table["Hitlist"]` is similar and was already a `list`,
avoiding the issue of nondeterministic iteration order, so
`exclusion_table["HitlistCasual"]` has been changed to a `list` to
match.
---
worlds/kh2/Locations.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/worlds/kh2/Locations.py b/worlds/kh2/Locations.py
index 3b5a6e7e69cb..4b423d255a74 100644
--- a/worlds/kh2/Locations.py
+++ b/worlds/kh2/Locations.py
@@ -1281,7 +1281,7 @@
LocationName.HadesCupTrophyParadoxCups,
LocationName.MusicalOrichalcumPlus,
],
- "HitlistCasual": {
+ "HitlistCasual": [
LocationName.FuturePete,
LocationName.BetwixtandBetweenBondofFlame,
LocationName.GrimReaper2,
@@ -1299,7 +1299,7 @@
LocationName.MCP,
LocationName.Lvl50,
LocationName.Lvl99
- },
+ ],
"Cups": {
LocationName.ProtectBeltPainandPanicCup,
LocationName.SerenityGemPainandPanicCup,
From 260bae359de479348974ad587d696c5bfa8482b5 Mon Sep 17 00:00:00 2001
From: qwint
Date: Wed, 11 Mar 2026 15:37:00 -0500
Subject: [PATCH 50/84] Core: Update .gitignore to include an exe setup.py
downloads (#6031)
---
.gitignore | 1 +
1 file changed, 1 insertion(+)
diff --git a/.gitignore b/.gitignore
index cbc33e5858be..8f9ed6df14fe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,6 +45,7 @@ EnemizerCLI/
/SNI/
/sni-*/
/appimagetool*
+/VC_redist.x64.exe
/host.yaml
/options.yaml
/config.yaml
From d01c9577ab697855ff421fdc6a750fefdc30e4b0 Mon Sep 17 00:00:00 2001
From: Duck <31627079+duckboycool@users.noreply.github.com>
Date: Wed, 11 Mar 2026 16:46:59 -0600
Subject: [PATCH 51/84] CommonClient: Add explicit message for connection
timeout (#5842)
* Change timeout and add timeout-specific message
* Revert open_timeout
---
CommonClient.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/CommonClient.py b/CommonClient.py
index 5fbc0f1b061a..3f98a4eff1d0 100755
--- a/CommonClient.py
+++ b/CommonClient.py
@@ -773,7 +773,7 @@ def gui_error(self, title: str, text: typing.Union[Exception, str]) -> typing.Op
if len(parts) == 1:
parts = title.split(', ', 1)
if len(parts) > 1:
- text = parts[1] + '\n\n' + text
+ text = f"{parts[1]}\n\n{text}" if text else parts[1]
title = parts[0]
# display error
self._messagebox = MessageBox(title, text, error=True)
@@ -896,6 +896,8 @@ def reconnect_hint() -> str:
"May not be running Archipelago on that address or port.")
except websockets.InvalidURI:
ctx.handle_connection_loss("Failed to connect to the multiworld server (invalid URI)")
+ except asyncio.TimeoutError:
+ ctx.handle_connection_loss("Failed to connect to the multiworld server. Connection timed out.")
except OSError:
ctx.handle_connection_loss("Failed to connect to the multiworld server")
except Exception:
From 70fc3e05fbacd1bb58d381d6820a9bdb8099d9db Mon Sep 17 00:00:00 2001
From: Fabian Dill
Date: Thu, 12 Mar 2026 02:48:45 +0100
Subject: [PATCH 52/84] Webhost: port reuse fix & configurable max room timeout
(#6033)
* WebHost: make autolauncher max room timeout configurable
* WebHost: launch rooms with assigned port first
---
WebHostLib/__init__.py | 2 ++
WebHostLib/autolauncher.py | 5 +++--
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py
index d10c17bff8ad..f1ac7ad5585d 100644
--- a/WebHostLib/__init__.py
+++ b/WebHostLib/__init__.py
@@ -46,6 +46,8 @@
app.config["JOB_THRESHOLD"] = 1
# after what time in seconds should generation be aborted, freeing the queue slot. Can be set to None to disable.
app.config["JOB_TIME"] = 600
+# maximum time in seconds since last activity for a room to be hosted
+app.config["MAX_ROOM_TIMEOUT"] = 259200
# memory limit for generator processes in bytes
app.config["GENERATOR_MEMORY_LIMIT"] = 4294967296
diff --git a/WebHostLib/autolauncher.py b/WebHostLib/autolauncher.py
index b48c6a8cbbe1..1a6156450035 100644
--- a/WebHostLib/autolauncher.py
+++ b/WebHostLib/autolauncher.py
@@ -9,7 +9,7 @@
from typing import Any
from uuid import UUID
-from pony.orm import db_session, select, commit, PrimaryKey
+from pony.orm import db_session, select, commit, PrimaryKey, desc
from Utils import restricted_loads, utcnow
from .locker import Locker, AlreadyRunningException
@@ -129,7 +129,8 @@ def keep_running():
with db_session:
rooms = select(
room for room in Room if
- room.last_activity >= utcnow() - timedelta(days=3))
+ room.last_activity >= utcnow() - timedelta(
+ seconds=config["MAX_ROOM_TIMEOUT"])).order_by(desc(Room.last_port))
for room in rooms:
# we have to filter twice, as the per-room timeout can't currently be PonyORM transpiled.
if room.last_activity >= utcnow() - timedelta(seconds=room.timeout + 5):
From 8457ff3e4bd3de154f9d191a2551eb1d916c9c41 Mon Sep 17 00:00:00 2001
From: lepideble <147614625+lepideble@users.noreply.github.com>
Date: Mon, 16 Mar 2026 15:17:54 +0100
Subject: [PATCH 53/84] Factorio: only show fluid boxes on assembling machine 1
when the selected recipe needs fluids (#5412)
---
worlds/factorio/data/mod_template/data-final-fixes.lua | 1 +
1 file changed, 1 insertion(+)
diff --git a/worlds/factorio/data/mod_template/data-final-fixes.lua b/worlds/factorio/data/mod_template/data-final-fixes.lua
index 2ddcd8d8ab60..e45952301986 100644
--- a/worlds/factorio/data/mod_template/data-final-fixes.lua
+++ b/worlds/factorio/data/mod_template/data-final-fixes.lua
@@ -130,6 +130,7 @@ end
data.raw["assembling-machine"]["assembling-machine-1"].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories)
data.raw["assembling-machine"]["assembling-machine-2"].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories)
data.raw["assembling-machine"]["assembling-machine-1"].fluid_boxes = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-2"].fluid_boxes)
+data.raw["assembling-machine"]["assembling-machine-1"].fluid_boxes_off_when_no_fluid_recipe = data.raw["assembling-machine"]["assembling-machine-2"].fluid_boxes_off_when_no_fluid_recipe
if mods["factory-levels"] then
-- Factory-Levels allows the assembling machines to get faster (and depending on settings), more productive at crafting products, the more the
-- assembling machine crafts the product. If the machine crafts enough, it may auto-upgrade to the next tier.
From 2e5356ad05aa0af7585ecd047b1992a4e3103218 Mon Sep 17 00:00:00 2001
From: Fabian Dill
Date: Wed, 18 Mar 2026 03:30:22 +0100
Subject: [PATCH 54/84] Core: other resources guide (#6043)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: Emily <35015090+EmilyV99@users.noreply.github.com>
---
WebHostLib/templates/supportedGames.html | 4 +-
docs/CODEOWNERS | 2 -
setup.py | 1 -
test/general/test_implemented.py | 4 +-
test/general/test_options.py | 2 +-
worlds/apsudoku/__init__.py | 34 ---------------
worlds/apsudoku/docs/en_Sudoku.md | 15 -------
worlds/apsudoku/docs/setup_en.md | 55 ------------------------
worlds/generic/__init__.py | 5 ++-
worlds/generic/docs/other_en.md | 37 ++++++++++++++++
10 files changed, 47 insertions(+), 112 deletions(-)
delete mode 100644 worlds/apsudoku/__init__.py
delete mode 100644 worlds/apsudoku/docs/en_Sudoku.md
delete mode 100644 worlds/apsudoku/docs/setup_en.md
create mode 100644 worlds/generic/docs/other_en.md
diff --git a/WebHostLib/templates/supportedGames.html b/WebHostLib/templates/supportedGames.html
index 759e748056a5..43028721b254 100644
--- a/WebHostLib/templates/supportedGames.html
+++ b/WebHostLib/templates/supportedGames.html
@@ -33,7 +33,9 @@
Currently Supported Games
Below are the games that are currently included with the Archipelago software. To play a game that is not on
this page, please refer to the playing with
- custom worlds section of the setup guide.
+ custom worlds section of the setup guide and the
+ other games and tools guide
+ to find more.
Search for your game below!
diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS
index 46afd3045692..30b61f5c85b8 100644
--- a/docs/CODEOWNERS
+++ b/docs/CODEOWNERS
@@ -19,8 +19,6 @@
# NewSoupVi is acting maintainer, but world belongs to core with the exception of the music
/worlds/apquest/ @NewSoupVi
-# Sudoku (APSudoku)
-/worlds/apsudoku/ @EmilyV99
# Aquaria
/worlds/aquaria/ @tioui
diff --git a/setup.py b/setup.py
index 949b1e3e303b..ebef3880fc61 100644
--- a/setup.py
+++ b/setup.py
@@ -71,7 +71,6 @@
"Ocarina of Time",
"Overcooked! 2",
"Raft",
- "Sudoku",
"Super Mario 64",
"VVVVVV",
"Wargroove",
diff --git a/test/general/test_implemented.py b/test/general/test_implemented.py
index add6e5321e7f..0bc7b62d5b34 100644
--- a/test/general/test_implemented.py
+++ b/test/general/test_implemented.py
@@ -11,7 +11,7 @@ class TestImplemented(unittest.TestCase):
def test_completion_condition(self):
"""Ensure a completion condition is set that has requirements."""
for game_name, world_type in AutoWorldRegister.world_types.items():
- if not world_type.hidden and game_name not in {"Sudoku"}:
+ if not world_type.hidden:
with self.subTest(game_name):
multiworld = setup_solo_multiworld(world_type)
self.assertFalse(multiworld.completion_condition[1](multiworld.state))
@@ -59,7 +59,7 @@ def test_no_failed_world_loads(self):
def test_prefill_items(self):
"""Test that every world can reach every location from allstate before pre_fill."""
for gamename, world_type in AutoWorldRegister.world_types.items():
- if gamename not in ("Archipelago", "Sudoku", "Final Fantasy", "Test Game"):
+ if gamename not in ("Archipelago", "Final Fantasy", "Test Game"):
with self.subTest(gamename):
multiworld = setup_solo_multiworld(world_type, ("generate_early", "create_regions", "create_items",
"set_rules", "connect_entrances", "generate_basic"))
diff --git a/test/general/test_options.py b/test/general/test_options.py
index 6b08c8e9b048..5d69b6820b84 100644
--- a/test/general/test_options.py
+++ b/test/general/test_options.py
@@ -109,7 +109,7 @@ def test_pickle_dumps_default(self):
def test_option_set_keys_random(self):
"""Tests that option sets do not contain 'random' and its variants as valid keys"""
for game_name, world_type in AutoWorldRegister.world_types.items():
- if game_name not in ("Archipelago", "Sudoku", "Super Metroid"):
+ if game_name not in ("Archipelago", "Super Metroid"):
for option_key, option in world_type.options_dataclass.type_hints.items():
if issubclass(option, OptionSet):
with self.subTest(game=game_name, option=option_key):
diff --git a/worlds/apsudoku/__init__.py b/worlds/apsudoku/__init__.py
deleted file mode 100644
index 04422ddb23c6..000000000000
--- a/worlds/apsudoku/__init__.py
+++ /dev/null
@@ -1,34 +0,0 @@
-from typing import Dict
-
-from BaseClasses import Tutorial
-from ..AutoWorld import WebWorld, World
-
-class AP_SudokuWebWorld(WebWorld):
- options_page = False
- theme = 'partyTime'
-
- setup_en = Tutorial(
- tutorial_name='Setup Guide',
- description='A guide to playing APSudoku',
- language='English',
- file_name='setup_en.md',
- link='setup/en',
- authors=['EmilyV']
- )
-
- tutorials = [setup_en]
-
-class AP_SudokuWorld(World):
- """
- Play a little Sudoku while you're in BK mode to maybe get some useful hints
- """
- game = "Sudoku"
- web = AP_SudokuWebWorld()
-
- item_name_to_id: Dict[str, int] = {}
- location_name_to_id: Dict[str, int] = {}
-
- @classmethod
- def stage_assert_generate(cls, multiworld):
- raise Exception("APSudoku cannot be used for generating worlds, the client can instead connect to any slot from any world")
-
diff --git a/worlds/apsudoku/docs/en_Sudoku.md b/worlds/apsudoku/docs/en_Sudoku.md
deleted file mode 100644
index b56af0de79f3..000000000000
--- a/worlds/apsudoku/docs/en_Sudoku.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# APSudoku
-
-## Hint Games
-
-HintGames do not need to be added at the start of a seed, and do not create a 'slot'- instead, you connect the HintGame client to a different game's slot. By playing a HintGame, you can earn hints for the connected slot.
-
-## What is this game?
-
-Play Sudoku puzzles of varying difficulties, earning a hint for each puzzle correctly solved. Harder puzzles are more likely to grant a hint towards a Progression item, though otherwise what hint is granted is random.
-
-## Where is the options page?
-
-There is no options page; this game cannot be used in your .yamls. Instead, the client can connect to any slot in a multiworld.
-
-By using the connected room's Admin Password on the Admin Panel tab, you can configure some settings at any time to affect the entire room. This allows disabling hints entirely, as well as altering the hint odds for each difficulty.
diff --git a/worlds/apsudoku/docs/setup_en.md b/worlds/apsudoku/docs/setup_en.md
deleted file mode 100644
index f80cd4333fe1..000000000000
--- a/worlds/apsudoku/docs/setup_en.md
+++ /dev/null
@@ -1,55 +0,0 @@
-# APSudoku Setup Guide
-
-## Required Software
-- [APSudoku](https://github.com/APSudoku/APSudoku)
-
-## General Concept
-
-This is a HintGame client, which can connect to any multiworld slot, allowing you to play Sudoku to unlock random hints for that slot's locations.
-
-Does not need to be added at the start of a seed, as it does not create any slots of its own, nor does it have any YAML files.
-
-## Installation Procedures
-
-### Windows / Linux
-Go to the latest release from the [github APSudoku Releases page](https://github.com/APSudoku/APSudoku/releases/latest). Download and extract the appropriate file for your platform.
-
-### Web
-Go to the [github pages](apsudoku.github.io) or [itch.io](https://emilyv99.itch.io/apsudoku) site, and play in the browser.
-
-## Joining a MultiWorld Game
-
-1. Run the APSudoku executable.
-2. Under `Settings` → `Connection` at the top-right:
- - Enter the server address and port number
- - Enter the name of the slot you wish to connect to
- - Enter the room password (optional)
- - Select DeathLink related settings (optional)
- - Press `Connect`
-4. Under the `Sudoku` tab
- - Choose puzzle difficulty
- - Click `Start` to generate a puzzle
-5. Try to solve the Sudoku. Click `Check` when done
- - A correct solution rewards you with 1 hint for a location in the world you are connected to
- - An incorrect solution has no penalty, unless DeathLink is enabled (see below)
-
-Info:
-- You can set various settings under `Settings` → `Sudoku`, and can change the colors used under `Settings` → `Theme`.
-- While connected, you can view the `Console` and `Hints` tabs for standard TextClient-like features
-- You can also use the `Tracking` tab to view either a basic tracker or a valid [GodotAP tracker pack](https://github.com/EmilyV99/GodotAP/blob/main/tracker_packs/GET_PACKS.md)
-- While connected, the number of "unhinted" locations for your slot is shown in the upper-left of the the `Sudoku` tab. (If this reads 0, no further hints can be earned for this slot, as every locations is already hinted)
-- Click the various `?` buttons for information on controls/how to play
-
-## Admin Settings
-
-By using the connected room's Admin Password on the Admin Panel tab, you can configure some settings at any time to affect the entire room.
-
-- You can disable APSudoku for the entire room, preventing any hints from being granted.
-- You can customize the reward weights for each difficulty, making progression hints more or less likely, and/or adding a chance to get "no hint" after a solve.
-
-## DeathLink Support
-
-If `DeathLink` is enabled when you click `Connect`:
-- Lose a life if you check an incorrect puzzle (not an _incomplete_ puzzle- if any cells are empty, you get off with a warning), or if you quit a puzzle without solving it (including disconnecting).
-- Your life count is customizable (default 0). Dying with 0 lives left kills linked players AND resets your puzzle.
-- On receiving a DeathLink from another player, your puzzle resets.
diff --git a/worlds/generic/__init__.py b/worlds/generic/__init__.py
index c4aef4f67bb0..4cd80556ccf3 100644
--- a/worlds/generic/__init__.py
+++ b/worlds/generic/__init__.py
@@ -26,7 +26,10 @@ class GenericWeb(WebWorld):
'English', 'setup_en.md', 'setup/en', ['alwaysintreble'])
triggers = Tutorial('Archipelago Triggers Guide', 'A guide to setting up and using triggers in your game settings.',
'English', 'triggers_en.md', 'triggers/en', ['alwaysintreble'])
- tutorials = [setup, mac, commands, advanced_settings, triggers, plando]
+ other_games = Tutorial('Other Games and Tools',
+ 'A guide to additional games and tools that can be used with Archipelago.',
+ 'English', 'other_en.md', 'other/en', ['Berserker'])
+ tutorials = [setup, mac, commands, advanced_settings, triggers, plando, other_games]
class GenericWorld(World):
diff --git a/worlds/generic/docs/other_en.md b/worlds/generic/docs/other_en.md
new file mode 100644
index 000000000000..caf8372170d6
--- /dev/null
+++ b/worlds/generic/docs/other_en.md
@@ -0,0 +1,37 @@
+# Other Games And Tools
+
+This page provides information and links regarding various tools that may be of use with Archipelago, including additional playable games not supported by this website.
+
+You should only download and use files from sources you trust; sources listed here are not officially vetted for safety, so use your own judgement and caution.
+
+## Discord
+
+Currently, Discord is the primary hub for Archipelago; whether it be finding people to play with, developing new game implementations, or finding new playable games.
+
+The [Archipelago Official Discord](https://discord.gg/8Z65BR2) is the main hub, while the [Archipelago After Dark Discord](https://discord.gg/fqvNCCRsu4) houses additional games that may be unrated or 18+ in some territories.
+
+The `#apworld-index` channels in each of these servers contain lists of playable games which should be easily downloadable and playable with an Archipelago installation.
+
+## Wiki
+
+The community-maintained [Archipelago Wiki](https://archipelago.miraheze.org/) has information on many games as well, and acts as a great discord-free source of information.
+
+## Hint Games
+
+Hint Games are a special type of game which are not included as part of the multiworld generation process. Instead, they can log in to an ongoing multiworld, connecting to a slot designated for any game. Rather than earning items for other games in the multiworld, a Hint Game will allow you to earn hints for the slot you are connected to.
+
+Hint Games can be found from sources such as the Discord and the [Hint Game Category](https://archipelago.miraheze.org/wiki/Category:Hint_games) of the wiki, as detailed above.
+
+## Notable Tools
+
+### Options Creator
+
+The Options Creator is included in the Archipelago installation, and is accessible from the Archipelago Launcher. Using this simple GUI tool, you can easily create randomization options for any installed `.apworld` - perfect when using custom worlds you've installed that don't have options pages on the website.
+
+### PopTracker
+
+[PopTracker](https://poptracker.github.io) is a popular tool in Randomizer communities, which many games support via custom PopTracker Packs. Many Archipelago packs include the ability to directly connect to your slot for auto-tracking capabilities. (Check each game's setup guide or Discord channel to see if it has PopTracker compatibility!)
+
+### Universal Tracker
+
+[Universal Tracker](https://github.com/FarisTheAncient/Archipelago/releases?q=Tracker) is a custom tracker client that uses your .yaml files from generation (as well as the .apworld files) to attempt to provide a view of what locations are currently in-logic or not, using the actual generation logic. Specific steps may need to be taken depending on the game, or the use of randomness in your yaml. Support for UT can be found in the [#universal-tracker](https://discord.com/channels/731205301247803413/1367270230635839539) channel of the Archipelago Official Discord.
From fb45a2f87e392a7ea69daa33905b8aee3e83e19d Mon Sep 17 00:00:00 2001
From: Ian Robinson
Date: Wed, 18 Mar 2026 13:54:17 -0400
Subject: [PATCH 55/84] Rule Builder: Fix count resolution when Oring
HasAnyCount (#6048)
---
rule_builder/rules.py | 2 +-
test/general/test_rule_builder.py | 8 ++++++++
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/rule_builder/rules.py b/rule_builder/rules.py
index 816ac9f0b7f0..77a89c96c238 100644
--- a/rule_builder/rules.py
+++ b/rule_builder/rules.py
@@ -527,7 +527,7 @@ def _instantiate(self, world: TWorld) -> Rule.Resolved:
items[item] = 1
elif isinstance(child, HasAnyCount.Resolved):
for item, count in child.item_counts:
- if item not in items or items[item] < count:
+ if item not in items or count < items[item]:
items[item] = count
else:
clauses.append(child)
diff --git a/test/general/test_rule_builder.py b/test/general/test_rule_builder.py
index 52248b604727..81003dcd87f9 100644
--- a/test/general/test_rule_builder.py
+++ b/test/general/test_rule_builder.py
@@ -233,6 +233,14 @@ def get_filler_item_name(self) -> str:
Or(Has("A"), HasAny("B", "C"), HasAnyCount({"D": 1, "E": 1})),
HasAny.Resolved(("A", "B", "C", "D", "E"), player=1),
),
+ (
+ And(HasAllCounts({"A": 1, "B": 2}), HasAllCounts({"A": 2, "B": 2})),
+ HasAllCounts.Resolved((("A", 2), ("B", 2)), player=1),
+ ),
+ (
+ Or(HasAnyCount({"A": 1, "B": 2}), HasAnyCount({"A": 2, "B": 2})),
+ HasAnyCount.Resolved((("A", 1), ("B", 2)), player=1),
+ ),
)
)
class TestSimplify(RuleBuilderTestCase):
From 116ab2286ad95fe4a43fbc06247d4f0ba42e6e34 Mon Sep 17 00:00:00 2001
From: Justus Lind
Date: Sat, 28 Mar 2026 03:36:36 +1000
Subject: [PATCH 56/84] Muse Dash: Add support for Wuthering Waves Pioneer
Podcast and Ay-Aye Horse (#6071)
---
worlds/musedash/MuseDashCollection.py | 2 ++
worlds/musedash/MuseDashData.py | 13 +++++++++++--
worlds/musedash/archipelago.json | 2 +-
worlds/musedash/test/TestDifficultyRanges.py | 3 ++-
4 files changed, 16 insertions(+), 4 deletions(-)
diff --git a/worlds/musedash/MuseDashCollection.py b/worlds/musedash/MuseDashCollection.py
index 4c23a5c64f32..472b4c95a174 100644
--- a/worlds/musedash/MuseDashCollection.py
+++ b/worlds/musedash/MuseDashCollection.py
@@ -15,6 +15,7 @@ class MuseDashCollections:
"Default Music",
"Budget Is Burning: Nano Core",
"Budget Is Burning Vol.1",
+ "Wuthering Waves Pioneer Podcast",
]
MUSE_PLUS_DLC: str = "Muse Plus"
@@ -40,6 +41,7 @@ class MuseDashCollections:
"Heart Message feat. Aoi Tokimori Secret",
"Meow Rock feat. Chun Ge, Yuan Shen",
"Stra Stella Secret",
+ "Musepyoi Legend",
]
song_items = SONG_DATA
diff --git a/worlds/musedash/MuseDashData.py b/worlds/musedash/MuseDashData.py
index 86486a4929b0..6cb294bd98a6 100644
--- a/worlds/musedash/MuseDashData.py
+++ b/worlds/musedash/MuseDashData.py
@@ -696,11 +696,20 @@
"Otsukimi Koete Otsukiai": SongData(2900820, "43-70", "MD Plus Project", True, 6, 8, 10),
"Obenkyou Time": SongData(2900821, "43-71", "MD Plus Project", False, 6, 8, 11),
"Retry Now": SongData(2900822, "43-72", "MD Plus Project", False, 3, 6, 9),
- "Master Bancho's Sushi Class ": SongData(2900823, "93-0", "Welcome to the Blue Hole!", False, None, None, None),
+ "Master Bancho's Sushi Class": SongData(2900823, "93-0", "Welcome to the Blue Hole!", False, None, 7, None),
"CHAOTiC BATTLE": SongData(2900824, "94-0", "Cosmic Radio 2025", False, 7, 9, 11),
"FATAL GAME": SongData(2900825, "94-1", "Cosmic Radio 2025", False, 3, 6, 9),
"Aria": SongData(2900826, "94-2", "Cosmic Radio 2025", False, 4, 6, 9),
"+1 UNKNOWN -NUMBER": SongData(2900827, "94-3", "Cosmic Radio 2025", True, 4, 7, 10),
"To the Beyond, from the Nameless Seaside": SongData(2900828, "94-4", "Cosmic Radio 2025", False, 5, 8, 10),
"REK421": SongData(2900829, "94-5", "Cosmic Radio 2025", True, 7, 9, 11),
-}
+ "Musepyoi Legend": SongData(2900830, "95-0", "Ay-Aye Horse", True, None, None, None),
+ "Not Regret": SongData(2900831, "95-1", "Ay-Aye Horse", False, 7, 9, 11),
+ "-Toryanna-": SongData(2900832, "95-2", "Ay-Aye Horse", True, 4, 6, 9),
+ "Icecream Angels": SongData(2900833, "95-3", "Ay-Aye Horse", False, 3, 6, 9),
+ "MEGA TSKR": SongData(2900834, "95-4", "Ay-Aye Horse", False, 4, 7, 10),
+ "777 Vocal ver.": SongData(2900835, "95-5", "Ay-Aye Horse", False, 7, 9, 11),
+ "Chasing Daylight": SongData(2900836, "96-0", "Wuthering Waves Pioneer Podcast", False, 3, 5, 8),
+ "CATCH ME IF YOU CAN": SongData(2900837, "96-1", "Wuthering Waves Pioneer Podcast", False, 4, 6, 9),
+ "RUNNING FOR YOUR LIFE": SongData(2900838, "96-2", "Wuthering Waves Pioneer Podcast", False, 2, 5, 8),
+}
\ No newline at end of file
diff --git a/worlds/musedash/archipelago.json b/worlds/musedash/archipelago.json
index dea7846b4f38..0580d85e77c1 100644
--- a/worlds/musedash/archipelago.json
+++ b/worlds/musedash/archipelago.json
@@ -1,6 +1,6 @@
{
"game": "Muse Dash",
"authors": ["DeamonHunter"],
- "world_version": "1.5.29",
+ "world_version": "1.5.30",
"minimum_ap_version": "0.6.3"
}
\ No newline at end of file
diff --git a/worlds/musedash/test/TestDifficultyRanges.py b/worlds/musedash/test/TestDifficultyRanges.py
index 27798243a559..f41e80717160 100644
--- a/worlds/musedash/test/TestDifficultyRanges.py
+++ b/worlds/musedash/test/TestDifficultyRanges.py
@@ -10,6 +10,7 @@ class DifficultyRanges(MuseDashTestBase):
"PeroPero in the Universe",
"umpopoff",
"P E R O P E R O Brother Dance",
+ "Master Bancho's Sushi Class",
]
def test_all_difficulty_ranges(self) -> None:
@@ -78,7 +79,7 @@ def test_songs_have_difficulty(self) -> None:
# Some songs are weird and have less than the usual 3 difficulties.
# So this override is to avoid failing on these songs.
- if song_name in ("umpopoff", "P E R O P E R O Brother Dance"):
+ if song_name in ("umpopoff", "P E R O P E R O Brother Dance", "Master Bancho's Sushi Class"):
self.assertTrue(song.easy is None and song.hard is not None and song.master is None,
f"Song '{song_name}' difficulty not set when it should be.")
else:
From 4276c6d6b01542181b79ea4678b5e857fae3e86d Mon Sep 17 00:00:00 2001
From: Phaneros <31861583+MatthewMarinets@users.noreply.github.com>
Date: Fri, 27 Mar 2026 14:45:38 -0700
Subject: [PATCH 57/84] sc2: Fixing random fill errors in unit tests (#6045)
---
worlds/sc2/docs/setup_en.md | 33 ++++++++++++----
worlds/sc2/locations.py | 16 ++------
worlds/sc2/pool_filter.py | 20 +++++-----
worlds/sc2/regions.py | 2 +-
worlds/sc2/rules.py | 10 ++---
worlds/sc2/test/slow_tests.py | 52 +++++++++++++++++++++++++
worlds/sc2/test/test_base.py | 24 ++++++------
worlds/sc2/test/test_generation.py | 62 ++++++++++++++++--------------
worlds/sc2/test/test_regions.py | 23 +++++------
9 files changed, 154 insertions(+), 88 deletions(-)
create mode 100644 worlds/sc2/test/slow_tests.py
diff --git a/worlds/sc2/docs/setup_en.md b/worlds/sc2/docs/setup_en.md
index d6f9f6cda518..e70c15d28fda 100644
--- a/worlds/sc2/docs/setup_en.md
+++ b/worlds/sc2/docs/setup_en.md
@@ -156,15 +156,17 @@ This page includes all data associated with all games.
## How do I join a MultiWorld game?
-1. Run ArchipelagoStarcraft2Client.exe.
+1. Run ArchipelagoLauncher.exe.
- macOS users should instead follow the instructions found at ["Running in macOS"](#running-in-macos) for this step
only.
-2. In the Archipelago tab, type `/connect [server IP]`.
+2. Search for the Starcraft 2 Client in the launcher to open the game-specific client
+ - Alternatively, steps 1 and 2 can be combined by providing the `"Starcraft 2 Client"` launch argument to the launcher.
+3. In the Archipelago tab, type `/connect [server IP]`.
- If you're running through the website, the server IP should be displayed near the top of the room page.
- The server IP may also be typed into the top bar, and then clicking "Connect"
-3. Type your slot name from your YAML when prompted.
-4. If the server has a password, enter that when prompted.
-5. Once connected, switch to the 'StarCraft 2 Launcher' tab in the client. There, you can see all the missions in your
+4. Type your slot name from your YAML when prompted.
+5. If the server has a password, enter that when prompted.
+6. Once connected, switch to the 'StarCraft 2 Launcher' tab in the client. There, you can see all the missions in your
world.
Unreachable missions will have greyed-out text. Completed missions (all locations collected) will have white text.
@@ -173,7 +175,22 @@ Mission buttons will have a color corresponding to the faction you play as in th
Click on an available mission to start it.
-## The game isn't launching when I try to start a mission.
+## Troubleshooting
+
+### I can't connect to my seed.
+
+Rooms on the Archipelago website go to sleep after two hours of inactivity; reload or refresh the room page
+to start them back up.
+When restarting the room, the connection port may change (the numbers after "archipelago.gg:"),
+make sure that is accurate.
+Your slot name should be displayed on the room page as well; make sure that exactly matches the slot name you
+type into your client, and note that it is case-sensitive.
+
+If none of these things solve the problem, visit the [Discord](https://discord.com/invite/8Z65BR2) and check
+the #software-announcements channel to see if there's a listed outage, or visit the #starcraft-2 channel for
+tech support.
+
+### The game isn't launching when I try to start a mission.
Usually, this is caused by the mod files not being downloaded.
Make sure you have run `/download_data` in the Archipelago tab before playing.
@@ -183,12 +200,12 @@ Make sure that you are running an up-to-date version of the client.
Check the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) to
look up what the latest version is (RC releases are not necessary; that stands for "Release Candidate").
-If these things are in order, check the log file for issues (stored at `[Archipelago Directory]/logs/Starcraft2Client.txt`).
+If these things are in order, check the log file for issues (stored at `[Archipelago Directory]/logs/SC2Client_.txt`).
If you can't figure out the log file, visit our [Discord's](https://discord.com/invite/8Z65BR2) tech-support channel
for help.
Please include a specific description of what's going wrong and attach your log file to your message.
-## My keyboard shortcuts profile is not available when I play *StarCraft 2 Archipelago*.
+### My keyboard shortcuts profile is not available when I play *StarCraft 2 Archipelago*.
For your keyboard shortcuts profile to work in Archipelago, you need to copy your shortcuts file from
`Documents/StarCraft II/Accounts/######/Hotkeys` to `Documents/StarCraft II/Hotkeys`.
diff --git a/worlds/sc2/locations.py b/worlds/sc2/locations.py
index 318d52f85611..ddc188bad903 100644
--- a/worlds/sc2/locations.py
+++ b/worlds/sc2/locations.py
@@ -249,7 +249,6 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]:
LocationType.VICTORY,
lambda state: (
logic.terran_common_unit(state)
- and logic.terran_defense_rating(state, True) >= 2
and (adv_tactics or logic.terran_basic_anti_air(state))
),
),
@@ -271,10 +270,7 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]:
"Third Group Rescued",
SC2WOL_LOC_ID_OFFSET + 303,
LocationType.VANILLA,
- lambda state: (
- logic.terran_common_unit(state)
- and logic.terran_defense_rating(state, True) >= 2
- ),
+ logic.terran_common_unit,
),
make_location_data(
SC2Mission.ZERO_HOUR.mission_name,
@@ -320,20 +316,14 @@ def get_locations(world: Optional["SC2World"]) -> Tuple[LocationData, ...]:
"Hold Just a Little Longer",
SC2WOL_LOC_ID_OFFSET + 309,
LocationType.EXTRA,
- lambda state: (
- logic.terran_common_unit(state)
- and logic.terran_defense_rating(state, True) >= 2
- ),
+ logic.terran_common_unit,
),
make_location_data(
SC2Mission.ZERO_HOUR.mission_name,
"Cavalry's on the Way",
SC2WOL_LOC_ID_OFFSET + 310,
LocationType.EXTRA,
- lambda state: (
- logic.terran_common_unit(state)
- and logic.terran_defense_rating(state, True) >= 2
- ),
+ logic.terran_common_unit,
),
make_location_data(
SC2Mission.EVACUATION.mission_name,
diff --git a/worlds/sc2/pool_filter.py b/worlds/sc2/pool_filter.py
index 0ffa08e010c2..abecf8264cca 100644
--- a/worlds/sc2/pool_filter.py
+++ b/worlds/sc2/pool_filter.py
@@ -182,7 +182,7 @@ def attempt_removal(
del self.logical_inventory[item.name]
item.filter_flags |= remove_flag
return ""
-
+
def remove_child_items(
parent_item: StarcraftItem,
remove_flag: ItemFilterFlags = ItemFilterFlags.FilterExcluded,
@@ -247,13 +247,13 @@ def request_minimum_items(group: List[StarcraftItem], requested_minimum) -> None
# Limit the maximum number of upgrades
if max_upgrades_per_unit != -1:
- for group_name, group_items in group_to_item.items():
- self.world.random.shuffle(group_to_item[group])
+ for group_items in group_to_item.values():
+ self.world.random.shuffle(group_items)
cull_items_over_maximum(group_items, max_upgrades_per_unit)
-
+
# Requesting minimum upgrades for items that have already been locked/placed when minimum required
if min_upgrades_per_unit != -1:
- for group_name, group_items in group_to_item.items():
+ for group_items in group_to_item.values():
self.world.random.shuffle(group_items)
request_minimum_items(group_items, min_upgrades_per_unit)
@@ -349,7 +349,7 @@ def item_included(item: StarcraftItem) -> bool:
ItemFilterFlags.Removed not in item.filter_flags
and ((ItemFilterFlags.Unexcludable|ItemFilterFlags.Excluded) & item.filter_flags) != ItemFilterFlags.Excluded
)
-
+
# Actually remove culled items; we won't re-add them
inventory = [
item for item in inventory
@@ -373,7 +373,7 @@ def item_included(item: StarcraftItem) -> bool:
item for item in cullable_items
if not ((ItemFilterFlags.Removed|ItemFilterFlags.Uncullable) & item.filter_flags)
]
-
+
# Handle too many requested
if current_inventory_size - start_inventory_size > inventory_size - filler_amount:
for item in inventory:
@@ -414,7 +414,7 @@ def item_included(item: StarcraftItem) -> bool:
removable_transport_hooks = [item for item in inventory_transport_hooks if not (ItemFilterFlags.Unexcludable & item.filter_flags)]
if len(inventory_transport_hooks) > 1 and removable_transport_hooks:
inventory.remove(removable_transport_hooks[0])
-
+
# Weapon/Armour upgrades
def exclude_wa(prefix: str) -> List[StarcraftItem]:
return [
@@ -439,7 +439,7 @@ def exclude_wa(prefix: str) -> List[StarcraftItem]:
inventory = exclude_wa(item_names.PROTOSS_GROUND_UPGRADE_PREFIX)
if used_item_names.isdisjoint(item_groups.protoss_air_wa):
inventory = exclude_wa(item_names.PROTOSS_AIR_UPGRADE_PREFIX)
-
+
# Part 4: Last-ditch effort to reduce inventory size; upgrades can go in start inventory
current_inventory_size = len(inventory)
precollect_items = current_inventory_size - inventory_size - start_inventory_size - filler_amount
@@ -453,7 +453,7 @@ def exclude_wa(prefix: str) -> List[StarcraftItem]:
for item in promotable[:precollect_items]:
item.filter_flags |= ItemFilterFlags.StartInventory
start_inventory_size += 1
-
+
assert current_inventory_size - start_inventory_size <= inventory_size - filler_amount, (
f"Couldn't reduce inventory to fit. target={inventory_size}, poolsize={current_inventory_size}, "
f"start_inventory={starcraft_item}, filler_amount={filler_amount}"
diff --git a/worlds/sc2/regions.py b/worlds/sc2/regions.py
index 4b02d294d1ba..299fcde3dbee 100644
--- a/worlds/sc2/regions.py
+++ b/worlds/sc2/regions.py
@@ -129,7 +129,7 @@ def adjust_mission_pools(world: 'SC2World', pools: SC2MOGenMissionPools):
if grant_story_tech == GrantStoryTech.option_grant:
# Additional starter mission if player is granted story tech
pools.move_mission(SC2Mission.ENEMY_WITHIN, Difficulty.EASY, Difficulty.STARTER)
- pools.move_mission(SC2Mission.THE_ESCAPE, Difficulty.MEDIUM, Difficulty.STARTER)
+ pools.move_mission(SC2Mission.THE_ESCAPE, Difficulty.EASY, Difficulty.STARTER)
pools.move_mission(SC2Mission.IN_THE_ENEMY_S_SHADOW, Difficulty.MEDIUM, Difficulty.STARTER)
if not war_council_nerfs or grant_story_tech == GrantStoryTech.option_grant:
pools.move_mission(SC2Mission.TEMPLAR_S_RETURN, Difficulty.MEDIUM, Difficulty.STARTER)
diff --git a/worlds/sc2/rules.py b/worlds/sc2/rules.py
index 28a8804e5ed8..8d55c6a4e529 100644
--- a/worlds/sc2/rules.py
+++ b/worlds/sc2/rules.py
@@ -1660,11 +1660,11 @@ def zealot_sentry_slayer_start(self, state: CollectionState) -> bool:
Created mainly for engine of destruction start, but works for other missions with no-build starts.
"""
return state.has_any((
- item_names.ZEALOT_WHIRLWIND,
- item_names.SENTRY_DOUBLE_SHIELD_RECHARGE,
- item_names.SLAYER_PHASE_BLINK,
- item_names.STALKER_INSTIGATOR_SLAYER_DISINTEGRATING_PARTICLES,
- item_names.STALKER_INSTIGATOR_SLAYER_PARTICLE_REFLECTION,
+ item_names.ZEALOT_WHIRLWIND,
+ item_names.SENTRY_DOUBLE_SHIELD_RECHARGE,
+ item_names.SLAYER_PHASE_BLINK,
+ item_names.STALKER_INSTIGATOR_SLAYER_DISINTEGRATING_PARTICLES,
+ item_names.STALKER_INSTIGATOR_SLAYER_PARTICLE_REFLECTION,
), self.player)
# Mission-specific rules
diff --git a/worlds/sc2/test/slow_tests.py b/worlds/sc2/test/slow_tests.py
new file mode 100644
index 000000000000..90b6e7a98205
--- /dev/null
+++ b/worlds/sc2/test/slow_tests.py
@@ -0,0 +1,52 @@
+"""
+Slow-running tests that are run infrequently.
+Run this file explicitly with `python3 -m unittest worlds.sc2.test.slow_tests`
+"""
+from .test_base import Sc2SetupTestBase
+
+from Fill import FillError
+from .. import mission_tables, options
+
+
+class LargeTests(Sc2SetupTestBase):
+ def test_any_starter_mission_works(self) -> None:
+ base_options = {
+ options.OPTION_NAME[options.SelectedRaces]: list(options.SelectedRaces.valid_keys),
+ options.OPTION_NAME[options.RequiredTactics]: options.RequiredTactics.option_standard,
+ options.OPTION_NAME[options.MissionOrder]: options.MissionOrder.option_custom,
+ options.OPTION_NAME[options.ExcludeOverpoweredItems]: True,
+ # options.OPTION_NAME[options.ExtraLocations]: options.ExtraLocations.option_disabled,
+ options.OPTION_NAME[options.VanillaLocations]: options.VanillaLocations.option_disabled,
+ }
+ missions_to_check = [
+ mission for mission in mission_tables.SC2Mission
+ if mission.pool == mission_tables.MissionPools.STARTER
+ ]
+ failed_missions: list[tuple[mission_tables.SC2Mission, int]] = []
+ NUM_ATTEMPTS = 3
+ for mission in missions_to_check:
+ for attempt in range(NUM_ATTEMPTS):
+ mission_options = base_options | {
+ options.OPTION_NAME[options.CustomMissionOrder]: {
+ "Test Campaign": {
+ "Test Layout": {
+ "type": "hopscotch",
+ "size": 25,
+ "goal": True,
+ "missions": [
+ {"index": 0, "mission_pool": [mission.mission_name]}
+ ]
+ }
+ }
+ }
+ }
+ try:
+ self.generate_world(mission_options)
+ self.fill_after_generation()
+ assert self.multiworld.worlds[1].custom_mission_order.get_starting_missions()[0] == mission
+ except FillError as ex:
+ failed_missions.append((mission, self.multiworld.seed))
+ if failed_missions:
+ for failed_mission in failed_missions:
+ print(failed_mission)
+ self.assertFalse(failed_missions)
diff --git a/worlds/sc2/test/test_base.py b/worlds/sc2/test/test_base.py
index f0f778dc798c..f6aaaddabac3 100644
--- a/worlds/sc2/test/test_base.py
+++ b/worlds/sc2/test/test_base.py
@@ -1,4 +1,4 @@
-from typing import *
+from typing import Any, cast
import unittest
import random
from argparse import Namespace
@@ -6,18 +6,11 @@
from Generate import get_seed_name
from worlds import AutoWorld
from test.general import gen_steps, call_all
+from Fill import distribute_items_restrictive
-from test.bases import WorldTestBase
from .. import SC2World, SC2Campaign
-from .. import client
from .. import options
-class Sc2TestBase(WorldTestBase):
- game = client.SC2Context.game
- world: SC2World
- player: ClassVar[int] = 1
- skip_long_tests: bool = True
-
class Sc2SetupTestBase(unittest.TestCase):
"""
@@ -37,10 +30,11 @@ class Sc2SetupTestBase(unittest.TestCase):
PROTOSS_CAMPAIGNS = {
'enabled_campaigns': {SC2Campaign.PROPHECY.campaign_name, SC2Campaign.PROLOGUE.campaign_name, SC2Campaign.LOTV.campaign_name,}
}
- seed: Optional[int] = None
+ seed: int | None = None
game = SC2World.game
player = 1
- def generate_world(self, options: Dict[str, Any]) -> None:
+
+ def generate_world(self, options: dict[str, Any]) -> None:
self.multiworld = MultiWorld(1)
self.multiworld.game[self.player] = self.game
self.multiworld.player_name = {self.player: "Tester"}
@@ -63,3 +57,11 @@ def generate_world(self, options: Dict[str, Any]) -> None:
except Exception as ex:
ex.add_note(f"Seed: {self.multiworld.seed}")
raise
+
+ def fill_after_generation(self) -> None:
+ assert self.multiworld
+ try:
+ distribute_items_restrictive(self.multiworld)
+ except Exception as ex:
+ ex.add_note(f"Seed: {self.multiworld.seed}")
+ raise
diff --git a/worlds/sc2/test/test_generation.py b/worlds/sc2/test/test_generation.py
index 329cd593e1a8..708606f7bcde 100644
--- a/worlds/sc2/test/test_generation.py
+++ b/worlds/sc2/test/test_generation.py
@@ -1,20 +1,24 @@
"""
Unit tests for world generation
"""
-from typing import *
-
+from typing import Any
from .test_base import Sc2SetupTestBase
-from .. import mission_groups, mission_tables, options, locations, SC2Mission, SC2Campaign, SC2Race, unreleased_items, \
- RequiredTactics
+from .. import (
+ mission_groups, mission_tables, options, locations,
+ SC2Mission, SC2Campaign, SC2Race, unreleased_items,
+ RequiredTactics,
+)
from ..item import item_groups, item_tables, item_names
from .. import get_all_missions, get_random_first_mission
-from ..options import EnabledCampaigns, NovaGhostOfAChanceVariant, MissionOrder, ExcludeOverpoweredItems, \
- VanillaItemsOnly, MaximumCampaignSize
+from ..options import (
+ EnabledCampaigns, NovaGhostOfAChanceVariant, MissionOrder, ExcludeOverpoweredItems,
+ VanillaItemsOnly, MaximumCampaignSize,
+)
class TestItemFiltering(Sc2SetupTestBase):
- def test_explicit_locks_excludes_interact_and_set_flags(self):
+ def test_explicit_locks_excludes_interact_and_set_flags(self) -> None:
world_options = {
**self.ALL_CAMPAIGNS,
'locked_items': {
@@ -46,7 +50,7 @@ def test_explicit_locks_excludes_interact_and_set_flags(self):
regen_biosteel_items = [x for x in itempool if x == item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL]
self.assertEqual(len(regen_biosteel_items), 2)
- def test_unexcludes_cancel_out_excludes(self):
+ def test_unexcludes_cancel_out_excludes(self) -> None:
world_options = {
'grant_story_tech': options.GrantStoryTech.option_grant,
'excluded_items': {
@@ -121,7 +125,7 @@ def test_exclude_2_beats_unexclude_1(self) -> None:
itempool = [item.name for item in self.multiworld.itempool]
self.assertNotIn(item_names.MARINE, itempool)
- def test_excluding_groups_excludes_all_items_in_group(self):
+ def test_excluding_groups_excludes_all_items_in_group(self) -> None:
world_options = {
'excluded_items': {
item_groups.ItemGroupNames.BARRACKS_UNITS.lower(): -1,
@@ -133,7 +137,7 @@ def test_excluding_groups_excludes_all_items_in_group(self):
for item_name in item_groups.barracks_units:
self.assertNotIn(item_name, itempool)
- def test_excluding_mission_groups_excludes_all_missions_in_group(self):
+ def test_excluding_mission_groups_excludes_all_missions_in_group(self) -> None:
world_options = {
**self.ZERG_CAMPAIGNS,
'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all,
@@ -164,7 +168,7 @@ def test_excluding_campaigns_excludes_campaign_specific_items(self) -> None:
self.assertNotEqual(item_data.type, item_tables.TerranItemType.Nova_Gear)
self.assertNotEqual(item_name, item_names.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE)
- def test_starter_unit_populates_start_inventory(self):
+ def test_starter_unit_populates_start_inventory(self) -> None:
world_options = {
'enabled_campaigns': {
SC2Campaign.WOL.campaign_name,
@@ -308,7 +312,7 @@ def test_vanilla_items_only_excludes_terran_progressives(self) -> None:
self.generate_world(world_options)
world_items = [(item.name, item_tables.item_table[item.name]) for item in self.multiworld.itempool]
self.assertTrue(world_items)
- occurrences: Dict[str, int] = {}
+ occurrences: dict[str, int] = {}
for item_name, _ in world_items:
if item_name in item_groups.terran_progressive_items:
if item_name in item_groups.nova_equipment:
@@ -528,7 +532,7 @@ def test_deprecated_orbital_command_not_present(self) -> None:
Orbital command got replaced. The item is still there for backwards compatibility.
It shouldn't be generated.
"""
- world_options = {}
+ world_options: dict[str, Any] = {}
self.generate_world(world_options)
itempool = [item.name for item in self.multiworld.itempool]
@@ -595,7 +599,7 @@ def test_disabling_speedrun_locations_removes_them_from_the_pool(self) -> None:
self.assertIn(speedrun_location_name, all_location_names)
self.assertNotIn(speedrun_location_name, world_location_names)
- def test_nco_and_wol_picks_correct_starting_mission(self):
+ def test_nco_and_wol_picks_correct_starting_mission(self) -> None:
world_options = {
'mission_order': MissionOrder.option_vanilla,
'enabled_campaigns': {
@@ -606,7 +610,7 @@ def test_nco_and_wol_picks_correct_starting_mission(self):
self.generate_world(world_options)
self.assertEqual(get_random_first_mission(self.world, self.world.custom_mission_order), mission_tables.SC2Mission.LIBERATION_DAY)
- def test_excluding_mission_short_name_excludes_all_variants_of_mission(self):
+ def test_excluding_mission_short_name_excludes_all_variants_of_mission(self) -> None:
world_options = {
'excluded_missions': [
mission_tables.SC2Mission.ZERO_HOUR.mission_name.split(" (")[0]
@@ -625,7 +629,7 @@ def test_excluding_mission_short_name_excludes_all_variants_of_mission(self):
self.assertNotIn(mission_tables.SC2Mission.ZERO_HOUR_Z, missions)
self.assertNotIn(mission_tables.SC2Mission.ZERO_HOUR_P, missions)
- def test_excluding_mission_variant_excludes_just_that_variant(self):
+ def test_excluding_mission_variant_excludes_just_that_variant(self) -> None:
world_options = {
'excluded_missions': [
mission_tables.SC2Mission.ZERO_HOUR.mission_name
@@ -644,7 +648,7 @@ def test_excluding_mission_variant_excludes_just_that_variant(self):
self.assertIn(mission_tables.SC2Mission.ZERO_HOUR_Z, missions)
self.assertIn(mission_tables.SC2Mission.ZERO_HOUR_P, missions)
- def test_weapon_armor_upgrades(self):
+ def test_weapon_armor_upgrades(self) -> None:
world_options = {
# Vanilla WoL with all missions
'mission_order': options.MissionOrder.option_vanilla,
@@ -682,7 +686,7 @@ def test_weapon_armor_upgrades(self):
self.assertGreaterEqual(len(vehicle_weapon_items), 3)
self.assertEqual(len(other_bundle_items), 0)
- def test_weapon_armor_upgrades_with_bundles(self):
+ def test_weapon_armor_upgrades_with_bundles(self) -> None:
world_options = {
# Vanilla WoL with all missions
'mission_order': options.MissionOrder.option_vanilla,
@@ -720,7 +724,7 @@ def test_weapon_armor_upgrades_with_bundles(self):
self.assertGreaterEqual(len(vehicle_upgrade_items), 3)
self.assertEqual(len(other_bundle_items), 0)
- def test_weapon_armor_upgrades_all_in_air(self):
+ def test_weapon_armor_upgrades_all_in_air(self) -> None:
world_options = {
# Vanilla WoL with all missions
'mission_order': options.MissionOrder.option_vanilla,
@@ -753,7 +757,7 @@ def test_weapon_armor_upgrades_all_in_air(self):
self.assertGreaterEqual(len(vehicle_weapon_items), 3)
self.assertGreaterEqual(len(ship_weapon_items), 3)
- def test_weapon_armor_upgrades_generic_upgrade_missions(self):
+ def test_weapon_armor_upgrades_generic_upgrade_missions(self) -> None:
"""
Tests the case when there aren't enough missions in order to get required weapon/armor upgrades
for logic requirements.
@@ -782,7 +786,7 @@ def test_weapon_armor_upgrades_generic_upgrade_missions(self):
# Under standard tactics you need to place L3 upgrades for available unit classes
self.assertEqual(len(upgrade_items), 3)
- def test_weapon_armor_upgrades_generic_upgrade_missions_no_logic(self):
+ def test_weapon_armor_upgrades_generic_upgrade_missions_no_logic(self) -> None:
"""
Tests the case when there aren't enough missions in order to get required weapon/armor upgrades
for logic requirements.
@@ -813,7 +817,7 @@ def test_weapon_armor_upgrades_generic_upgrade_missions_no_logic(self):
# No logic won't take the fallback to trigger
self.assertEqual(len(upgrade_items), 0)
- def test_weapon_armor_upgrades_generic_upgrade_missions_no_countermeasure_needed(self):
+ def test_weapon_armor_upgrades_generic_upgrade_missions_no_countermeasure_needed(self) -> None:
world_options = {
# Vanilla WoL with all missions
'mission_order': options.MissionOrder.option_vanilla,
@@ -837,7 +841,7 @@ def test_weapon_armor_upgrades_generic_upgrade_missions_no_countermeasure_needed
# No additional starting inventory item placement is needed
self.assertEqual(len(upgrade_items), 0)
- def test_kerrigan_levels_per_mission_triggering_pre_fill(self):
+ def test_kerrigan_levels_per_mission_triggering_pre_fill(self) -> None:
world_options = {
**self.ALL_CAMPAIGNS,
'mission_order': options.MissionOrder.option_custom,
@@ -878,7 +882,7 @@ def test_kerrigan_levels_per_mission_triggering_pre_fill(self):
self.assertGreater(len(kerrigan_1_stacks), 0)
- def test_kerrigan_levels_per_mission_and_generic_upgrades_both_triggering_pre_fill(self):
+ def test_kerrigan_levels_per_mission_and_generic_upgrades_both_triggering_pre_fill(self) -> None:
world_options = {
**self.ALL_CAMPAIGNS,
'mission_order': options.MissionOrder.option_custom,
@@ -925,7 +929,7 @@ def test_kerrigan_levels_per_mission_and_generic_upgrades_both_triggering_pre_fi
self.assertNotIn(item_names.KERRIGAN_LEVELS_70, itempool)
self.assertNotIn(item_names.KERRIGAN_LEVELS_70, starting_inventory)
- def test_locking_required_items(self):
+ def test_locking_required_items(self) -> None:
world_options = {
**self.ALL_CAMPAIGNS,
'mission_order': options.MissionOrder.option_custom,
@@ -962,7 +966,7 @@ def test_locking_required_items(self):
self.assertIn(item_names.KERRIGAN_MEND, itempool)
- def test_fully_balanced_mission_races(self):
+ def test_fully_balanced_mission_races(self) -> None:
"""
Tests whether fully balanced mission race balancing actually is fully balanced.
"""
@@ -1080,7 +1084,7 @@ def test_weapon_armor_upgrade_items_capped_by_max_upgrade_level(self) -> None:
self.generate_world(world_options)
itempool = [item.name for item in self.multiworld.itempool]
- upgrade_item_counts: Dict[str, int] = {}
+ upgrade_item_counts: dict[str, int] = {}
for item_name in itempool:
if item_tables.item_table[item_name].type in (
item_tables.TerranItemType.Upgrade,
@@ -1252,7 +1256,7 @@ def test_unreleased_item_quantity(self) -> None:
self.generate_world(world_options)
itempool = [item.name for item in self.multiworld.itempool]
- items_to_check: List[str] = unreleased_items
+ items_to_check: list[str] = unreleased_items
for item in items_to_check:
self.assertNotIn(item, itempool)
@@ -1273,7 +1277,7 @@ def test_unreleased_item_quantity_locked(self) -> None:
self.generate_world(world_options)
itempool = [item.name for item in self.multiworld.itempool]
- items_to_check: List[str] = unreleased_items
+ items_to_check: list[str] = unreleased_items
for item in items_to_check:
self.assertIn(item, itempool)
diff --git a/worlds/sc2/test/test_regions.py b/worlds/sc2/test/test_regions.py
index 880a02f97374..5d9870d8941b 100644
--- a/worlds/sc2/test/test_regions.py
+++ b/worlds/sc2/test/test_regions.py
@@ -1,9 +1,10 @@
import unittest
-from .test_base import Sc2TestBase
+from .test_base import Sc2SetupTestBase
from .. import mission_tables, SC2Campaign
from .. import options
from ..mission_order.layout_types import Grid
+
class TestGridsizes(unittest.TestCase):
def test_grid_sizes_meet_specs(self):
self.assertTupleEqual((1, 2, 0), Grid.get_grid_dimensions(2))
@@ -24,17 +25,17 @@ def test_grid_sizes_meet_specs(self):
self.assertTupleEqual((5, 7, 2), Grid.get_grid_dimensions(33))
-class TestGridGeneration(Sc2TestBase):
- options = {
- "mission_order": options.MissionOrder.option_grid,
- "excluded_missions": [mission_tables.SC2Mission.ZERO_HOUR.mission_name,],
- "enabled_campaigns": {
- SC2Campaign.WOL.campaign_name,
- SC2Campaign.PROPHECY.campaign_name,
- }
- }
-
+class TestGridGeneration(Sc2SetupTestBase):
def test_size_matches_exclusions(self):
+ world_options = {
+ options.OPTION_NAME[options.MissionOrder]: options.MissionOrder.option_grid,
+ options.OPTION_NAME[options.ExcludedMissions]: [mission_tables.SC2Mission.ZERO_HOUR.mission_name],
+ options.OPTION_NAME[options.EnabledCampaigns]: {
+ SC2Campaign.WOL.campaign_name,
+ SC2Campaign.PROPHECY.campaign_name,
+ }
+ }
+ self.generate_world(world_options)
self.assertNotIn(mission_tables.SC2Mission.ZERO_HOUR.mission_name, self.multiworld.regions)
# WoL has 29 missions. -1 for Zero Hour being excluded, +1 for the automatically-added menu location
self.assertEqual(len(self.multiworld.regions), 29)
From 74f41e3733b9683d49a70c947926ff2f2a26b633 Mon Sep 17 00:00:00 2001
From: qwint
Date: Fri, 27 Mar 2026 18:58:36 -0500
Subject: [PATCH 58/84] Core: Make Generate.main only init logging on __main__
(#6069)
---
Generate.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/Generate.py b/Generate.py
index b51064017063..509bf848d0d3 100644
--- a/Generate.py
+++ b/Generate.py
@@ -87,7 +87,8 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
seed = get_seed(args.seed)
- Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level, add_timestamp=args.log_time)
+ if __name__ == "__main__":
+ Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level, add_timestamp=args.log_time)
random.seed(seed)
seed_name = get_seed_name(random)
From 645f25a94e411509b5a5bdfad1810706dd66e8bd Mon Sep 17 00:00:00 2001
From: Ian Robinson
Date: Sun, 29 Mar 2026 12:29:37 -0400
Subject: [PATCH 59/84] setup.py: add rule_builder.cached_world to included
list (#6070)
---
setup.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/setup.py b/setup.py
index ebef3880fc61..3c40eab59eda 100644
--- a/setup.py
+++ b/setup.py
@@ -657,7 +657,7 @@ def find_lib(lib: str, arch: str, libc: str) -> str | None:
options={
"build_exe": {
"packages": ["worlds", "kivy", "cymem", "websockets", "kivymd"],
- "includes": [],
+ "includes": ["rule_builder.cached_world"],
"excludes": ["numpy", "Cython", "PySide2", "PIL",
"pandas"],
"zip_includes": [],
From cf47cc67c0683228a60b81b07ad7c8a96875bdfc Mon Sep 17 00:00:00 2001
From: black-sliver <59490463+black-sliver@users.noreply.github.com>
Date: Sun, 29 Mar 2026 17:43:26 +0000
Subject: [PATCH 60/84] Clients: remove datapackage from persistent_storage ...
(#6074)
... next time it gets written to.
This makes loading peristent_storage faster in the future.
---
Utils.py | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/Utils.py b/Utils.py
index 627235f24925..cd4af275daba 100644
--- a/Utils.py
+++ b/Utils.py
@@ -345,6 +345,9 @@ def persistent_load() -> Dict[str, Dict[str, Any]]:
try:
with open(path, "r") as f:
storage = unsafe_parse_yaml(f.read())
+ if "datapackage" in storage:
+ del storage["datapackage"]
+ logging.debug("Removed old datapackage from persistent storage")
except Exception as e:
logging.debug(f"Could not read store: {e}")
if storage is None:
@@ -369,11 +372,6 @@ def load_data_package_for_checksum(game: str, checksum: typing.Optional[str]) ->
except Exception as e:
logging.debug(f"Could not load data package: {e}")
- # fall back to old cache
- cache = persistent_load().get("datapackage", {}).get("games", {}).get(game, {})
- if cache.get("checksum") == checksum:
- return cache
-
# cache does not match
return {}
From ffe4c6dd1504166bc96d99237d4177e80c6dc161 Mon Sep 17 00:00:00 2001
From: black-sliver <59490463+black-sliver@users.noreply.github.com>
Date: Sun, 29 Mar 2026 17:44:29 +0000
Subject: [PATCH 61/84] Core, Webhost: update and pin dependency versions
(#6075)
---
WebHostLib/requirements.txt | 24 +++++++++++-----------
requirements.txt | 34 ++++++++++++++++----------------
worlds/alttp/requirements.txt | 4 ++--
worlds/factorio/requirements.txt | 2 +-
4 files changed, 32 insertions(+), 32 deletions(-)
diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt
index c9a923680add..fd194f223221 100644
--- a/WebHostLib/requirements.txt
+++ b/WebHostLib/requirements.txt
@@ -1,14 +1,14 @@
-flask>=3.1.1
-werkzeug>=3.1.3
-pony>=0.7.19; python_version <= '3.12'
+flask==3.1.3
+werkzeug==3.1.6
+pony==0.7.19; python_version <= '3.12'
pony @ git+https://github.com/black-sliver/pony@7feb1221953b7fa4a6735466bf21a8b4d35e33ba#0.7.19; python_version >= '3.13'
-waitress>=3.0.2
-Flask-Caching>=2.3.0
+waitress==3.0.2
+Flask-Caching==2.3.1
Flask-Compress==1.18 # pkg_resources can't resolve the "backports.zstd" dependency of >1.18, breaking ModuleUpdate.py
-Flask-Limiter>=3.12
-Flask-Cors>=6.0.2
-bokeh>=3.6.3
-markupsafe>=3.0.2
-setproctitle>=1.3.5
-mistune>=3.1.3
-docutils>=0.22.2
+Flask-Limiter==4.1.1
+Flask-Cors==6.0.2
+bokeh==3.8.2
+markupsafe==3.0.3
+setproctitle==1.3.7
+mistune==3.2.0
+docutils==0.22.4
diff --git a/requirements.txt b/requirements.txt
index 27bca5c7c29e..3a91b1668029 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,21 +1,21 @@
-colorama>=0.4.6
-websockets>=13.0.1,<14
-PyYAML>=6.0.3
-jellyfish>=1.2.1
-jinja2>=3.1.6
-schema>=0.7.8
-kivy>=2.3.1
-bsdiff4>=1.2.6
-platformdirs>=4.5.0
-certifi>=2025.11.12
-cython>=3.2.1
-cymem>=2.0.13
-orjson>=3.11.4
-typing_extensions>=4.15.0
-pyshortcuts>=1.9.6
-pathspec>=0.12.1
+colorama==0.4.6
+websockets==13.1 # ,<14
+PyYAML==6.0.3
+jellyfish==1.2.1
+jinja2==3.1.6
+schema==0.7.8
+kivy==2.3.1
+bsdiff4==1.2.6
+platformdirs==4.9.4
+certifi==2026.2.25
+cython==3.2.4
+cymem==2.0.13
+orjson==3.11.7
+typing_extensions==4.15.0
+pyshortcuts==1.9.7
+pathspec==1.0.4
kivymd @ git+https://github.com/kivymd/KivyMD@5ff9d0d
kivymd>=2.0.1.dev0
# Legacy world dependencies that custom worlds rely on
-Pymem>=1.13.0
+Pymem==1.14.0
diff --git a/worlds/alttp/requirements.txt b/worlds/alttp/requirements.txt
index 8a96da2e634b..8eccc7e5f398 100644
--- a/worlds/alttp/requirements.txt
+++ b/worlds/alttp/requirements.txt
@@ -1,2 +1,2 @@
-maseya-z3pr>=1.0.0rc1
-xxtea>=3.0.0
+maseya-z3pr==1.0.0rc1
+xxtea==3.7.0
diff --git a/worlds/factorio/requirements.txt b/worlds/factorio/requirements.txt
index 8d684401663b..b2b3804a50a8 100644
--- a/worlds/factorio/requirements.txt
+++ b/worlds/factorio/requirements.txt
@@ -1 +1 @@
-factorio-rcon-py>=2.1.2
+factorio-rcon-py==2.1.3
From 1705620c4f37e86b8414aa3bf70d4fdd1ed29ded Mon Sep 17 00:00:00 2001
From: Duck <31627079+duckboycool@users.noreply.github.com>
Date: Sun, 29 Mar 2026 12:07:55 -0600
Subject: [PATCH 62/84] Launcher: Add konsole to terminal list and rework
launch dialog (#5684)
* Make component launching indicate if no terminal window, add konsole
* Attempt to spell better and remove whitespace
* Update terminal priority
* Make helper for clearing LD_LIBRARY_PATH
* Add handling to linux launch
* Hopefully fix setter
* Apply suggestions from code review
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
---------
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
---
Launcher.py | 49 +++++++++++++++++++++++++++++--------------------
Utils.py | 25 ++++++++++++++++---------
2 files changed, 45 insertions(+), 29 deletions(-)
diff --git a/Launcher.py b/Launcher.py
index cffd96b85d08..0e7d4796c4a8 100644
--- a/Launcher.py
+++ b/Launcher.py
@@ -29,8 +29,8 @@
import settings
import Utils
-from Utils import (init_logging, is_frozen, is_linux, is_macos, is_windows, local_path, messagebox, open_filename,
- user_path)
+from Utils import (env_cleared_lib_path, init_logging, is_frozen, is_linux, is_macos, is_windows, local_path,
+ messagebox, open_filename, user_path)
if __name__ == "__main__":
init_logging('Launcher')
@@ -52,10 +52,7 @@ def open_host_yaml():
webbrowser.open(file)
return
- env = os.environ
- if "LD_LIBRARY_PATH" in env:
- env = env.copy()
- del env["LD_LIBRARY_PATH"] # exe is a system binary, so reset LD_LIBRARY_PATH
+ env = env_cleared_lib_path()
subprocess.Popen([exe, file], env=env)
def open_patch():
@@ -106,10 +103,7 @@ def open_folder(folder_path):
return
if exe:
- env = os.environ
- if "LD_LIBRARY_PATH" in env:
- env = env.copy()
- del env["LD_LIBRARY_PATH"] # exe is a system binary, so reset LD_LIBRARY_PATH
+ env = env_cleared_lib_path()
subprocess.Popen([exe, folder_path], env=env)
else:
logging.warning(f"No file browser available to open {folder_path}")
@@ -202,22 +196,32 @@ def get_exe(component: str | Component) -> Sequence[str] | None:
return [sys.executable, local_path(f"{component.script_name}.py")] if component.script_name else None
-def launch(exe, in_terminal=False):
+def launch(exe: Sequence[str], in_terminal: bool = False) -> bool:
+ """Runs the given command/args in `exe` in a new process.
+
+ If `in_terminal` is True, it will attempt to run in a terminal window,
+ and the return value will indicate whether one was found."""
if in_terminal:
if is_windows:
# intentionally using a window title with a space so it gets quoted and treated as a title
subprocess.Popen(["start", "Running Archipelago", *exe], shell=True)
- return
+ return True
elif is_linux:
- terminal = which('x-terminal-emulator') or which('gnome-terminal') or which('xterm')
+ terminal = which("x-terminal-emulator") or which("konsole") or which("gnome-terminal") or which("xterm")
if terminal:
- subprocess.Popen([terminal, '-e', shlex.join(exe)])
- return
+ # Clear LD_LIB_PATH during terminal startup, but set it again when running command in case it's needed
+ ld_lib_path = os.environ.get("LD_LIBRARY_PATH")
+ lib_path_setter = f"env LD_LIBRARY_PATH={shlex.quote(ld_lib_path)} " if ld_lib_path else ""
+ env = env_cleared_lib_path()
+
+ subprocess.Popen([terminal, "-e", lib_path_setter + shlex.join(exe)], env=env)
+ return True
elif is_macos:
- terminal = [which('open'), '-W', '-a', 'Terminal.app']
+ terminal = [which("open"), "-W", "-a", "Terminal.app"]
subprocess.Popen([*terminal, *exe])
- return
+ return True
subprocess.Popen(exe)
+ return False
def create_shortcut(button: Any, component: Component) -> None:
@@ -406,12 +410,17 @@ def on_start(self):
@staticmethod
def component_action(button):
- MDSnackbar(MDSnackbarText(text="Opening in a new window..."), y=dp(24), pos_hint={"center_x": 0.5},
- size_hint_x=0.5).open()
+ open_text = "Opening in a new window..."
if button.component.func:
+ # Note: if we want to draw the Snackbar before running func, func needs to be wrapped in schedule_once
button.component.func()
else:
- launch(get_exe(button.component), button.component.cli)
+ # if launch returns False, it started the process in background (not in a new terminal)
+ if not launch(get_exe(button.component), button.component.cli) and button.component.cli:
+ open_text = "Running in the background..."
+
+ MDSnackbar(MDSnackbarText(text=open_text), y=dp(24), pos_hint={"center_x": 0.5},
+ size_hint_x=0.5).open()
def _on_drop_file(self, window: Window, filename: bytes, x: int, y: int) -> None:
""" When a patch file is dropped into the window, run the associated component. """
diff --git a/Utils.py b/Utils.py
index cd4af275daba..0210086274f4 100644
--- a/Utils.py
+++ b/Utils.py
@@ -22,7 +22,7 @@
from settings import Settings, get_settings
from time import sleep
-from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union, TypeGuard
+from typing import BinaryIO, Coroutine, Mapping, Optional, Set, Dict, Any, Union, TypeGuard
from yaml import load, load_all, dump
from pathspec import PathSpec, GitIgnoreSpec
from typing_extensions import deprecated
@@ -236,10 +236,7 @@ def open_file(filename: typing.Union[str, "pathlib.Path"]) -> None:
open_command = which("open") if is_macos else (which("xdg-open") or which("gnome-open") or which("kde-open"))
assert open_command, "Didn't find program for open_file! Please report this together with system details."
- env = os.environ
- if "LD_LIBRARY_PATH" in env:
- env = env.copy()
- del env["LD_LIBRARY_PATH"] # exe is a system binary, so reset LD_LIBRARY_PATH
+ env = env_cleared_lib_path()
subprocess.call([open_command, filename], env=env)
@@ -756,6 +753,19 @@ def is_kivy_running() -> bool:
return False
+def env_cleared_lib_path() -> Mapping[str, str]:
+ """
+ Creates a copy of the current environment vars with the LD_LIBRARY_PATH removed if set, as this can interfere when
+ launching something in a subprocess.
+ """
+ env = os.environ
+ if "LD_LIBRARY_PATH" in env:
+ env = env.copy()
+ del env["LD_LIBRARY_PATH"]
+
+ return env
+
+
def _mp_open_filename(res: "multiprocessing.Queue[typing.Optional[str]]", *args: Any) -> None:
if is_kivy_running():
raise RuntimeError("kivy should not be running in multiprocess")
@@ -768,10 +778,7 @@ def _mp_save_filename(res: "multiprocessing.Queue[typing.Optional[str]]", *args:
res.put(save_filename(*args))
def _run_for_stdout(*args: str):
- env = os.environ
- if "LD_LIBRARY_PATH" in env:
- env = env.copy()
- del env["LD_LIBRARY_PATH"] # exe is a system binary, so reset LD_LIBRARY_PATH
+ env = env_cleared_lib_path()
return subprocess.run(args, capture_output=True, text=True, env=env).stdout.split("\n", 1)[0] or None
From 36cf86f2e8b2c91bc942a97dd5fdc1772707d1c8 Mon Sep 17 00:00:00 2001
From: Sebastian <127998064+SebaCape@users.noreply.github.com>
Date: Sun, 29 Mar 2026 15:18:03 -0400
Subject: [PATCH 63/84] Docs: update macOS setup instructions for more
specificity on Python version (#6078)
---
worlds/generic/docs/mac_en.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/worlds/generic/docs/mac_en.md b/worlds/generic/docs/mac_en.md
index 4db48d2abd3a..eaaeb13c159d 100644
--- a/worlds/generic/docs/mac_en.md
+++ b/worlds/generic/docs/mac_en.md
@@ -2,7 +2,7 @@
Archipelago does not have a compiled release on macOS. However, it is possible to run from source code on macOS. This guide expects you to have some experience with running software from the terminal.
## Prerequisite Software
Here is a list of software to install and source code to download.
-1. Python 3.11 "universal2" or newer from the [macOS Python downloads page](https://www.python.org/downloads/macos/).
+1. Python 3.11.9 "universal2" or newer from the [macOS Python downloads page](https://www.python.org/downloads/macos/).
**Python 3.14 is not supported yet.**
2. Xcode from the [macOS App Store](https://apps.apple.com/us/app/xcode/id497799835).
3. The source code from the [Archipelago releases page](https://github.com/ArchipelagoMW/Archipelago/releases).
From 5ca50cd8d34d9f42b2f8ad76c5bf64203a643cb4 Mon Sep 17 00:00:00 2001
From: Bryce Wilson
Date: Sun, 29 Mar 2026 14:10:16 -0700
Subject: [PATCH 64/84] Pokemon Emerald: Fix Latios KeyError (#6056)
---
worlds/pokemon_emerald/__init__.py | 8 ++++++++
worlds/pokemon_emerald/pokemon.py | 4 ++--
worlds/pokemon_emerald/rules.py | 2 +-
3 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py
index 10abed539f70..fb683df90d05 100644
--- a/worlds/pokemon_emerald/__init__.py
+++ b/worlds/pokemon_emerald/__init__.py
@@ -263,6 +263,14 @@ def generate_early(self) -> None:
if self.options.hms == RandomizeHms.option_shuffle:
self.options.local_items.value.update(self.item_name_groups["HM"])
+ # Manually enable Latios as a dexsanity location if we're doing legendary hunt (which confines Latios to
+ # the roamer encounter), the player allows Latios as a valid legendary hunt target, and they didn't also
+ # blacklist Latios to remove its dexsanity location
+ if self.options.goal == Goal.option_legendary_hunt and self.options.dexsanity \
+ and "Latios" in self.options.allowed_legendary_hunt_encounters.value \
+ and emerald_data.constants["SPECIES_LATIOS"] not in self.blacklisted_wilds:
+ self.allowed_dexsanity_species.add(emerald_data.constants["SPECIES_LATIOS"])
+
def create_regions(self) -> None:
from .regions import create_regions
all_regions = create_regions(self)
diff --git a/worlds/pokemon_emerald/pokemon.py b/worlds/pokemon_emerald/pokemon.py
index 73af6c465840..76285d11dab8 100644
--- a/worlds/pokemon_emerald/pokemon.py
+++ b/worlds/pokemon_emerald/pokemon.py
@@ -376,10 +376,10 @@ def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None:
# Actually create the new list of slots and encounter table
new_slots: List[int] = []
- if encounter_type in enabled_encounters:
- world.allowed_dexsanity_species.update(table.slots)
for species_id in table.slots:
new_slots.append(species_old_to_new_map[species_id])
+ if encounter_type in enabled_encounters:
+ world.allowed_dexsanity_species.update(new_slots)
new_encounters[encounter_type] = EncounterTableData(new_slots, table.address)
diff --git a/worlds/pokemon_emerald/rules.py b/worlds/pokemon_emerald/rules.py
index eeadb8bea21a..30ebf72e4d63 100644
--- a/worlds/pokemon_emerald/rules.py
+++ b/worlds/pokemon_emerald/rules.py
@@ -1559,7 +1559,7 @@ def get_location(location: str):
# Legendary hunt prevents Latios from being a wild spawn so the roamer
# can be tracked, and also guarantees that the roamer is a Latios.
if world.options.goal == Goal.option_legendary_hunt and \
- data.constants["SPECIES_LATIOS"] not in world.blacklisted_wilds:
+ data.constants["SPECIES_LATIOS"] in world.allowed_dexsanity_species:
set_rule(
get_location(f"Pokedex - Latios"),
lambda state: state.has("EVENT_ENCOUNTER_LATIOS", world.player)
From 03c9d0717bb1d8a9da416416fc5225beeda04c3c Mon Sep 17 00:00:00 2001
From: Mysteryem
Date: Sun, 29 Mar 2026 22:12:25 +0100
Subject: [PATCH 65/84] Muse Dash: Fix nondeterministic generation with
include_songs (#6040)
The include_songs option is an OptionSet, whose value is a set, but was being iterated to produce self.included_songs. Sets are unordered and may have a different iteration order each time a python process is run. This meant that the order of the elements in self.included_songs could differ even when generating with a fixed seed.
This caused nondeterministic generation with the same seed because create_song_pool() deterministically randomly picks songs from self.included_songs, which could be in a different order each time, so different songs could be picked.
---
worlds/musedash/__init__.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/worlds/musedash/__init__.py b/worlds/musedash/__init__.py
index 239d640e6882..55767cf04b65 100644
--- a/worlds/musedash/__init__.py
+++ b/worlds/musedash/__init__.py
@@ -124,7 +124,8 @@ def handle_plando(self, available_song_keys: List[str], dlc_songs: Set[str]) ->
self.starting_songs = [s for s in start_items if s in song_items]
self.starting_songs = self.md_collection.filter_songs_to_dlc(self.starting_songs, dlc_songs)
- self.included_songs = [s for s in include_songs if s in song_items and s not in self.starting_songs]
+ # Sort first for deterministic iteration order.
+ self.included_songs = [s for s in sorted(include_songs) if s in song_items and s not in self.starting_songs]
self.included_songs = self.md_collection.filter_songs_to_dlc(self.included_songs, dlc_songs)
# Making sure songs chosen for goal are allowed by DLC and remove the chosen from being added to the pool.
From 393ed51203c19b228bbccaa82826be97781e8c89 Mon Sep 17 00:00:00 2001
From: Flit <8645405+FlitPix@users.noreply.github.com>
Date: Sun, 29 Mar 2026 17:16:34 -0400
Subject: [PATCH 66/84] Messenger: Require Wingsuit to traverse Dark Cave
(#6059)
---
worlds/messenger/rules.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/worlds/messenger/rules.py b/worlds/messenger/rules.py
index 7f17232cfbf8..bc1fc6aa9989 100644
--- a/worlds/messenger/rules.py
+++ b/worlds/messenger/rules.py
@@ -192,7 +192,7 @@ def __init__(self, world: "MessengerWorld") -> None:
or (self.has_dart(state) and self.has_wingsuit(state)),
# Dark Cave
"Dark Cave - Right -> Dark Cave - Left":
- lambda state: state.has("Candle", self.player) and self.has_dart(state),
+ lambda state: state.has("Candle", self.player) and self.has_dart(state) and self.has_wingsuit(state),
# Riviere Turquoise
"Riviere Turquoise - Waterfall Shop -> Riviere Turquoise - Flower Flight Checkpoint":
lambda state: self.has_dart(state) or (
From 2d58e7953c0ba1092281f44b49dd5c55a385b9ee Mon Sep 17 00:00:00 2001
From: agilbert1412
Date: Sun, 29 Mar 2026 17:20:00 -0400
Subject: [PATCH 67/84] Stardew valley: Four small fixes (#6055)
* - Fixed the Dr Seuss Bundle asking for tigerseye (mineral) instead of tiger trout (fish)
* - Made blue grass starter more consistent
* - Fragments of the past does not rely on ginger island
* - Removed legacy hard coded strange bun recipe that messed with chefsanity logic
---
worlds/stardew_valley/data/bundles_data/meme_bundles.py | 2 +-
worlds/stardew_valley/data/locations.csv | 4 ++--
worlds/stardew_valley/logic/logic.py | 1 -
3 files changed, 3 insertions(+), 4 deletions(-)
diff --git a/worlds/stardew_valley/data/bundles_data/meme_bundles.py b/worlds/stardew_valley/data/bundles_data/meme_bundles.py
index c3abdca129e8..a9b7f8c7daf1 100644
--- a/worlds/stardew_valley/data/bundles_data/meme_bundles.py
+++ b/worlds/stardew_valley/data/bundles_data/meme_bundles.py
@@ -257,7 +257,7 @@
red_fish_items = [red_mullet, red_snapper, lava_eel, crimsonfish]
blue_fish_items = [anchovy, tuna, sardine, bream, squid, ice_pip, albacore, blue_discus, midnight_squid, spook_fish, glacierfish]
other_fish = [pufferfish, largemouth_bass, smallmouth_bass, rainbow_trout, walleye, perch, carp, catfish, pike, sunfish, herring, eel, octopus, sea_cucumber,
- super_cucumber, ghostfish, stonefish, sandfish, scorpion_carp, flounder, midnight_carp, tigerseye, bullhead, tilapia, chub, dorado, shad,
+ super_cucumber, ghostfish, stonefish, sandfish, scorpion_carp, flounder, midnight_carp, bullhead, tilapia, chub, dorado, shad, tiger_trout,
lingcod, halibut, slimejack, stingray, goby, blobfish, angler, legend, mutant_carp]
dr_seuss_items = [other_fish, [fish.as_amount(2) for fish in other_fish], red_fish_items, blue_fish_items]
dr_seuss_bundle = FixedPriceDeepBundleTemplate(CCRoom.crafts_room, MemeBundleName.dr_seuss, dr_seuss_items, 4, 4)
diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv
index 9c6cffad519a..eea7c611502b 100644
--- a/worlds/stardew_valley/data/locations.csv
+++ b/worlds/stardew_valley/data/locations.csv
@@ -1182,7 +1182,7 @@ id,region,name,tags,content_packs
2104,Fishing,Biome Balance,SPECIAL_ORDER_BOARD,
2105,Haley's House,Rock Rejuvenation,SPECIAL_ORDER_BOARD,
2106,Alex's House,Gifts for George,SPECIAL_ORDER_BOARD,
-2107,Museum,Fragments of the past,"GINGER_ISLAND,SPECIAL_ORDER_BOARD",
+2107,Museum,Fragments of the past,"SPECIAL_ORDER_BOARD",
2108,Saloon,Gus' Famous Omelet,SPECIAL_ORDER_BOARD,
2109,Farm,Crop Order,SPECIAL_ORDER_BOARD,
2110,Railroad,Community Cleanup,SPECIAL_ORDER_BOARD,
@@ -2227,7 +2227,7 @@ id,region,name,tags,content_packs
3530,Farm,Craft Cookout Kit,"CRAFTSANITY,CRAFTSANITY_CRAFT",
3531,Farm,Craft Fish Smoker,"CRAFTSANITY,CRAFTSANITY_CRAFT",
3532,Farm,Craft Dehydrator,"CRAFTSANITY,CRAFTSANITY_CRAFT",
-3533,Farm,Craft Blue Grass Starter,"CRAFTSANITY,CRAFTSANITY_CRAFT,GINGER_ISLAND",
+3533,Farm,Craft Blue Grass Starter,"CRAFTSANITY,CRAFTSANITY_CRAFT,GINGER_ISLAND,REQUIRES_QI_ORDERS",
3534,Farm,Craft Mystic Tree Seed,"CRAFTSANITY,CRAFTSANITY_CRAFT,REQUIRES_MASTERIES",
3535,Farm,Craft Sonar Bobber,"CRAFTSANITY,CRAFTSANITY_CRAFT",
3536,Farm,Craft Challenge Bait,"CRAFTSANITY,CRAFTSANITY_CRAFT,REQUIRES_MASTERIES",
diff --git a/worlds/stardew_valley/logic/logic.py b/worlds/stardew_valley/logic/logic.py
index 37260d1494ad..b72c4fc6621f 100644
--- a/worlds/stardew_valley/logic/logic.py
+++ b/worlds/stardew_valley/logic/logic.py
@@ -297,7 +297,6 @@ def __init__(self, player: int, options: StardewValleyOptions, content: StardewC
Material.stone: self.ability.can_mine_stone(),
Material.wood: self.ability.can_chop_trees(),
Meal.ice_cream: (self.season.has(Season.summer) & self.money.can_spend_at(Region.town, 250)) | self.money.can_spend_at(Region.oasis, 240),
- Meal.strange_bun: self.relationship.has_hearts(NPC.shane, 7) & self.has(Ingredient.wheat_flour) & self.has(Fish.periwinkle) & self.has(ArtisanGood.void_mayonnaise),
MetalBar.copper: self.can_smelt(Ore.copper),
MetalBar.gold: self.can_smelt(Ore.gold),
MetalBar.iridium: self.can_smelt(Ore.iridium),
From a1ed8042674a5d0714391615cbdbe129fa8a881c Mon Sep 17 00:00:00 2001
From: Noa Aarts
Date: Sun, 29 Mar 2026 23:20:24 +0200
Subject: [PATCH 68/84] Stardew Valley: trimmed lucky purple shorts need gold
to make (#6034)
The current logic only requires the shorts and a sewing machine, but a
gold bar is also necessary
---
worlds/stardew_valley/logic/logic.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/worlds/stardew_valley/logic/logic.py b/worlds/stardew_valley/logic/logic.py
index b72c4fc6621f..47b51ce7a627 100644
--- a/worlds/stardew_valley/logic/logic.py
+++ b/worlds/stardew_valley/logic/logic.py
@@ -312,7 +312,7 @@ def __init__(self, player: int, options: StardewValleyOptions, content: StardewC
RetainingSoil.basic: self.money.can_spend_at(Region.pierre_store, 100),
RetainingSoil.quality: self.time.has_year_two & self.money.can_spend_at(Region.pierre_store, 150),
SpecialItem.lucky_purple_shorts: self.special_items.has_purple_shorts(),
- SpecialItem.trimmed_purple_shorts: self.has(SpecialItem.lucky_purple_shorts) & self.has(Machine.sewing_machine),
+ SpecialItem.trimmed_purple_shorts: self.has(SpecialItem.lucky_purple_shorts) & self.has(MetalBar.gold) & self.has(Machine.sewing_machine),
SpecialItem.far_away_stone: self.special_items.has_far_away_stone(),
SpecialItem.solid_gold_lewis: self.special_items.has_solid_gold_lewis(),
SpecialItem.advanced_tv_remote: self.special_items.has_advanced_tv_remote(),
From 139856a5731752bfdac22a5445422fb50d194b8d Mon Sep 17 00:00:00 2001
From: agilbert1412
Date: Sun, 29 Mar 2026 17:21:29 -0400
Subject: [PATCH 69/84] Stardew Valley: Fixed an issue where some specific
option combinations could create more items than locations (#6012)
* - Improved the dynamic locations count algorithm to take into account the nature of various heavy settings in both directions
* - Fixes from Code Review
* - We're only testing for sunday locations, might as well only take sunday locations in the list to test
* - One more slight optimization
* - Added consideration for bundles per room in filler locations counting
* - Registered some more IDs to handle items up to 10
---
worlds/stardew_valley/data/locations.csv | 14 +++++
worlds/stardew_valley/locations.py | 46 +++++++++++---
.../test/TestNumberLocations.py | 15 +++--
.../test/long/TestNumberLocationsLong.py | 62 +++++++++++++++++++
worlds/stardew_valley/test/options/presets.py | 45 ++++++++++++++
5 files changed, 170 insertions(+), 12 deletions(-)
create mode 100644 worlds/stardew_valley/test/long/TestNumberLocationsLong.py
diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv
index eea7c611502b..8c4a521d7705 100644
--- a/worlds/stardew_valley/data/locations.csv
+++ b/worlds/stardew_valley/data/locations.csv
@@ -438,6 +438,8 @@ id,region,name,tags,content_packs
906,Traveling Cart Sunday,Traveling Merchant Sunday Item 6,"TRAVELING_MERCHANT",
907,Traveling Cart Sunday,Traveling Merchant Sunday Item 7,"TRAVELING_MERCHANT",
908,Traveling Cart Sunday,Traveling Merchant Sunday Item 8,"TRAVELING_MERCHANT",
+909,Traveling Cart Sunday,Traveling Merchant Sunday Item 9,"TRAVELING_MERCHANT",
+910,Traveling Cart Sunday,Traveling Merchant Sunday Item 10,"TRAVELING_MERCHANT",
911,Traveling Cart Monday,Traveling Merchant Monday Item 1,"MANDATORY,TRAVELING_MERCHANT",
912,Traveling Cart Monday,Traveling Merchant Monday Item 2,"TRAVELING_MERCHANT",
913,Traveling Cart Monday,Traveling Merchant Monday Item 3,"TRAVELING_MERCHANT",
@@ -446,6 +448,8 @@ id,region,name,tags,content_packs
916,Traveling Cart Monday,Traveling Merchant Monday Item 6,"TRAVELING_MERCHANT",
917,Traveling Cart Monday,Traveling Merchant Monday Item 7,"TRAVELING_MERCHANT",
918,Traveling Cart Monday,Traveling Merchant Monday Item 8,"TRAVELING_MERCHANT",
+919,Traveling Cart Monday,Traveling Merchant Monday Item 9,"TRAVELING_MERCHANT",
+920,Traveling Cart Monday,Traveling Merchant Monday Item 10,"TRAVELING_MERCHANT",
921,Traveling Cart Tuesday,Traveling Merchant Tuesday Item 1,"MANDATORY,TRAVELING_MERCHANT",
922,Traveling Cart Tuesday,Traveling Merchant Tuesday Item 2,"TRAVELING_MERCHANT",
923,Traveling Cart Tuesday,Traveling Merchant Tuesday Item 3,"TRAVELING_MERCHANT",
@@ -454,6 +458,8 @@ id,region,name,tags,content_packs
926,Traveling Cart Tuesday,Traveling Merchant Tuesday Item 6,"TRAVELING_MERCHANT",
927,Traveling Cart Tuesday,Traveling Merchant Tuesday Item 7,"TRAVELING_MERCHANT",
928,Traveling Cart Tuesday,Traveling Merchant Tuesday Item 8,"TRAVELING_MERCHANT",
+929,Traveling Cart Tuesday,Traveling Merchant Tuesday Item 9,"TRAVELING_MERCHANT",
+930,Traveling Cart Tuesday,Traveling Merchant Tuesday Item 10,"TRAVELING_MERCHANT",
931,Traveling Cart Wednesday,Traveling Merchant Wednesday Item 1,"MANDATORY,TRAVELING_MERCHANT",
932,Traveling Cart Wednesday,Traveling Merchant Wednesday Item 2,"TRAVELING_MERCHANT",
933,Traveling Cart Wednesday,Traveling Merchant Wednesday Item 3,"TRAVELING_MERCHANT",
@@ -462,6 +468,8 @@ id,region,name,tags,content_packs
936,Traveling Cart Wednesday,Traveling Merchant Wednesday Item 6,"TRAVELING_MERCHANT",
937,Traveling Cart Wednesday,Traveling Merchant Wednesday Item 7,"TRAVELING_MERCHANT",
938,Traveling Cart Wednesday,Traveling Merchant Wednesday Item 8,"TRAVELING_MERCHANT",
+939,Traveling Cart Wednesday,Traveling Merchant Wednesday Item 9,"TRAVELING_MERCHANT",
+940,Traveling Cart Wednesday,Traveling Merchant Wednesday Item 10,"TRAVELING_MERCHANT",
941,Traveling Cart Thursday,Traveling Merchant Thursday Item 1,"MANDATORY,TRAVELING_MERCHANT",
942,Traveling Cart Thursday,Traveling Merchant Thursday Item 2,"TRAVELING_MERCHANT",
943,Traveling Cart Thursday,Traveling Merchant Thursday Item 3,"TRAVELING_MERCHANT",
@@ -470,6 +478,8 @@ id,region,name,tags,content_packs
946,Traveling Cart Thursday,Traveling Merchant Thursday Item 6,"TRAVELING_MERCHANT",
947,Traveling Cart Thursday,Traveling Merchant Thursday Item 7,"TRAVELING_MERCHANT",
948,Traveling Cart Thursday,Traveling Merchant Thursday Item 8,"TRAVELING_MERCHANT",
+949,Traveling Cart Thursday,Traveling Merchant Thursday Item 9,"TRAVELING_MERCHANT",
+950,Traveling Cart Thursday,Traveling Merchant Thursday Item 10,"TRAVELING_MERCHANT",
951,Traveling Cart Friday,Traveling Merchant Friday Item 1,"MANDATORY,TRAVELING_MERCHANT",
952,Traveling Cart Friday,Traveling Merchant Friday Item 2,"TRAVELING_MERCHANT",
953,Traveling Cart Friday,Traveling Merchant Friday Item 3,"TRAVELING_MERCHANT",
@@ -478,6 +488,8 @@ id,region,name,tags,content_packs
956,Traveling Cart Friday,Traveling Merchant Friday Item 6,"TRAVELING_MERCHANT",
957,Traveling Cart Friday,Traveling Merchant Friday Item 7,"TRAVELING_MERCHANT",
958,Traveling Cart Friday,Traveling Merchant Friday Item 8,"TRAVELING_MERCHANT",
+959,Traveling Cart Friday,Traveling Merchant Friday Item 9,"TRAVELING_MERCHANT",
+960,Traveling Cart Friday,Traveling Merchant Friday Item 10,"TRAVELING_MERCHANT",
961,Traveling Cart Saturday,Traveling Merchant Saturday Item 1,"MANDATORY,TRAVELING_MERCHANT",
962,Traveling Cart Saturday,Traveling Merchant Saturday Item 2,"TRAVELING_MERCHANT",
963,Traveling Cart Saturday,Traveling Merchant Saturday Item 3,"TRAVELING_MERCHANT",
@@ -486,6 +498,8 @@ id,region,name,tags,content_packs
966,Traveling Cart Saturday,Traveling Merchant Saturday Item 6,"TRAVELING_MERCHANT",
967,Traveling Cart Saturday,Traveling Merchant Saturday Item 7,"TRAVELING_MERCHANT",
968,Traveling Cart Saturday,Traveling Merchant Saturday Item 8,"TRAVELING_MERCHANT",
+969,Traveling Cart Saturday,Traveling Merchant Saturday Item 9,"TRAVELING_MERCHANT",
+970,Traveling Cart Saturday,Traveling Merchant Saturday Item 10,"TRAVELING_MERCHANT",
1001,Fishing,Fishsanity: Carp,FISHSANITY,
1002,Fishing,Fishsanity: Herring,FISHSANITY,
1003,Fishing,Fishsanity: Smallmouth Bass,FISHSANITY,
diff --git a/worlds/stardew_valley/locations.py b/worlds/stardew_valley/locations.py
index 613698ac1bfe..a817022e3d5d 100644
--- a/worlds/stardew_valley/locations.py
+++ b/worlds/stardew_valley/locations.py
@@ -1,6 +1,7 @@
import csv
import enum
import logging
+import math
from dataclasses import dataclass
from random import Random
from typing import Optional, Dict, Protocol, List, Iterable
@@ -16,7 +17,7 @@
from .options import ArcadeMachineLocations, SpecialOrderLocations, Museumsanity, \
FestivalLocations, ElevatorProgression, BackpackProgression, FarmType
from .options import StardewValleyOptions, Craftsanity, Chefsanity, Cooksanity, Shipsanity, Monstersanity
-from .options.options import BackpackSize, Moviesanity, Eatsanity, IncludeEndgameLocations, Friendsanity
+from .options.options import BackpackSize, Moviesanity, Eatsanity, IncludeEndgameLocations, Friendsanity, Fishsanity, SkillProgression, Cropsanity
from .strings.ap_names.ap_option_names import WalnutsanityOptionName, SecretsanityOptionName, EatsanityOptionName, ChefsanityOptionName, StartWithoutOptionName
from .strings.backpack_tiers import Backpack
from .strings.goal_names import Goal
@@ -665,19 +666,48 @@ def extend_endgame_locations(randomized_locations: List[LocationData], options:
def extend_filler_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent):
days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
- i = 1
- while len(randomized_locations) < 90:
- location_name = f"Traveling Merchant Sunday Item {i}"
- while any(location.name == location_name for location in randomized_locations):
- i += 1
- location_name = f"Traveling Merchant Sunday Item {i}"
+ number_locations_to_add_per_day = 0
+ min_number_locations = 90 # Under 90 locations we can run out of rooms for the mandatory core items
+ if len(randomized_locations) < min_number_locations:
+ number_locations_to_add = min_number_locations - len(randomized_locations)
+ number_locations_to_add_per_day += math.ceil(number_locations_to_add / 7)
+
+ # These settings generate a lot of empty locations, so they can absorb a lot of items
+ filler_heavy_settings = [options.fishsanity != Fishsanity.option_none,
+ options.shipsanity != Shipsanity.option_none,
+ options.cooksanity != Cooksanity.option_none,
+ options.craftsanity != Craftsanity.option_none,
+ len(options.eatsanity.value) > 0,
+ options.museumsanity == Museumsanity.option_all,
+ options.quest_locations.value >= 0,
+ options.bundle_per_room >= 2]
+ # These settings generate orphan items and can cause too many items, if enabled without a complementary of the filler heavy settings
+ orphan_settings = [len(options.chefsanity.value) > 0,
+ options.friendsanity != Friendsanity.option_none,
+ options.skill_progression == SkillProgression.option_progressive_with_masteries,
+ options.cropsanity != Cropsanity.option_disabled,
+ len(options.start_without.value) > 0,
+ options.bundle_per_room <= -1,
+ options.bundle_per_room <= -2]
+
+ enabled_filler_heavy_settings = len([val for val in filler_heavy_settings if val])
+ enabled_orphan_settings = len([val for val in orphan_settings if val])
+ if enabled_orphan_settings > enabled_filler_heavy_settings:
+ number_locations_to_add_per_day += enabled_orphan_settings - enabled_filler_heavy_settings
+
+ if number_locations_to_add_per_day <= 0:
+ return
+
+ existing_traveling_merchant_locations = [location.name for location in randomized_locations if location.name.startswith("Traveling Merchant Sunday Item ")]
+ start_num_to_add = len(existing_traveling_merchant_locations) + 1
+
+ for i in range(start_num_to_add, start_num_to_add+number_locations_to_add_per_day):
logger.debug(f"Player too few locations, adding Traveling Merchant Items #{i}")
for day in days:
location_name = f"Traveling Merchant {day} Item {i}"
randomized_locations.append(location_table[location_name])
-
def create_locations(location_collector: StardewLocationCollector,
bundle_rooms: List[BundleRoom],
trash_bear_requests: Dict[str, List[str]],
diff --git a/worlds/stardew_valley/test/TestNumberLocations.py b/worlds/stardew_valley/test/TestNumberLocations.py
index b21488733bb1..50b158d282ca 100644
--- a/worlds/stardew_valley/test/TestNumberLocations.py
+++ b/worlds/stardew_valley/test/TestNumberLocations.py
@@ -7,6 +7,13 @@
from ..items.item_data import FILLER_GROUPS
+def get_real_item_count(multiworld):
+ number_items = len([item for item in multiworld.itempool
+ if all(filler_group not in item_table[item.name].groups for filler_group in FILLER_GROUPS) and Group.TRAP not in item_table[
+ item.name].groups and (item.classification & ItemClassification.progression)])
+ return number_items
+
+
class TestLocationGeneration(SVTestBase):
def test_all_location_created_are_in_location_table(self):
@@ -20,8 +27,7 @@ class TestMinLocationAndMaxItem(SVTestBase):
def test_minimal_location_maximal_items_still_valid(self):
valid_locations = self.get_real_locations()
number_locations = len(valid_locations)
- number_items = len([item for item in self.multiworld.itempool
- if all(filler_group not in item_table[item.name].groups for filler_group in FILLER_GROUPS) and Group.TRAP not in item_table[item.name].groups])
+ number_items = get_real_item_count(self.multiworld)
print(f"Stardew Valley - Minimum Locations: {number_locations}, Maximum Items: {number_items} [ISLAND EXCLUDED]")
self.assertGreaterEqual(number_locations, number_items)
@@ -32,8 +38,7 @@ class TestMinLocationAndMaxItemWithIsland(SVTestBase):
def test_minimal_location_maximal_items_with_island_still_valid(self):
valid_locations = self.get_real_locations()
number_locations = len(valid_locations)
- number_items = len([item for item in self.multiworld.itempool
- if all(filler_group not in item_table[item.name].groups for filler_group in FILLER_GROUPS) and Group.TRAP not in item_table[item.name].groups and (item.classification & ItemClassification.progression)])
+ number_items = get_real_item_count(self.multiworld)
print(f"Stardew Valley - Minimum Locations: {number_locations}, Maximum Items: {number_items} [ISLAND INCLUDED]")
self.assertGreaterEqual(number_locations, number_items)
@@ -99,3 +104,5 @@ def test_allsanity_with_mods_has_at_least_locations(self):
f"\n\tPlease update test_allsanity_with_mods_has_at_least_locations"
f"\n\t\tExpected: {expected_locations}"
f"\n\t\tActual: {number_locations}")
+
+
diff --git a/worlds/stardew_valley/test/long/TestNumberLocationsLong.py b/worlds/stardew_valley/test/long/TestNumberLocationsLong.py
new file mode 100644
index 000000000000..9a46547aca1a
--- /dev/null
+++ b/worlds/stardew_valley/test/long/TestNumberLocationsLong.py
@@ -0,0 +1,62 @@
+import unittest
+
+from BaseClasses import ItemClassification
+from ..assertion import get_all_location_names
+from ..bases import skip_long_tests, SVTestCase, solo_multiworld
+from ..options.presets import setting_mins_and_maxes, allsanity_no_mods_7_x_x, get_minsanity_options, default_7_x_x
+from ...items import Group, item_table
+from ...items.item_data import FILLER_GROUPS
+
+if skip_long_tests():
+ raise unittest.SkipTest("Long tests disabled")
+
+
+def get_real_item_count(multiworld):
+ number_items = len([item for item in multiworld.itempool
+ if all(filler_group not in item_table[item.name].groups for filler_group in FILLER_GROUPS) and Group.TRAP not in item_table[
+ item.name].groups and (item.classification & ItemClassification.progression)])
+ return number_items
+
+
+class TestCountsPerSetting(SVTestCase):
+
+ def test_items_locations_counts_per_setting_with_ginger_island(self):
+ option_mins_and_maxes = setting_mins_and_maxes()
+
+ for name in option_mins_and_maxes:
+ values = option_mins_and_maxes[name]
+ if not isinstance(values, list):
+ continue
+ with self.subTest(f"{name}"):
+ highest_variance_items = -1
+ highest_variance_locations = -1
+ for preset in [allsanity_no_mods_7_x_x, default_7_x_x, get_minsanity_options]:
+ lowest_items = 9999
+ lowest_locations = 9999
+ highest_items = -1
+ highest_locations = -1
+ for value in values:
+ world_options = preset()
+ world_options[name] = value
+ with solo_multiworld(world_options, world_caching=False) as (multiworld, _):
+ num_locations = len([loc for loc in get_all_location_names(multiworld) if not loc.startswith("Traveling Merchant")])
+ num_items = get_real_item_count(multiworld)
+ if num_items > highest_items:
+ highest_items = num_items
+ if num_items < lowest_items:
+ lowest_items = num_items
+ if num_locations > highest_locations:
+ highest_locations = num_locations
+ if num_locations < lowest_locations:
+ lowest_locations = num_locations
+
+ variance_items = highest_items - lowest_items
+ variance_locations = highest_locations - lowest_locations
+ if variance_locations > highest_variance_locations:
+ highest_variance_locations = variance_locations
+ if variance_items > highest_variance_items:
+ highest_variance_items = variance_items
+ if highest_variance_locations > highest_variance_items:
+ print(f"Options `{name}` can create up to {highest_variance_locations - highest_variance_items} filler ({highest_variance_locations} locations and up to {highest_variance_items} items)")
+ if highest_variance_locations < highest_variance_items:
+ print(f"Options `{name}` can create up to {highest_variance_items - highest_variance_locations} orphan ({highest_variance_locations} locations and up to {highest_variance_items} items)")
\ No newline at end of file
diff --git a/worlds/stardew_valley/test/options/presets.py b/worlds/stardew_valley/test/options/presets.py
index 92aab191dec9..71ad32bb2012 100644
--- a/worlds/stardew_valley/test/options/presets.py
+++ b/worlds/stardew_valley/test/options/presets.py
@@ -292,3 +292,48 @@ def minimal_locations_maximal_items_with_island():
min_max_options = minimal_locations_maximal_items()
min_max_options.update({options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false})
return min_max_options
+
+
+def setting_mins_and_maxes():
+ low_orphan_options = {
+ options.ArcadeMachineLocations.internal_name: [options.ArcadeMachineLocations.option_disabled, options.ArcadeMachineLocations.option_full_shuffling],
+ options.BackpackProgression.internal_name: [options.BackpackProgression.option_vanilla, options.BackpackProgression.option_progressive],
+ options.BackpackSize.internal_name: [options.BackpackSize.option_1, options.BackpackSize.option_12],
+ options.Booksanity.internal_name: [options.Booksanity.option_none, options.Booksanity.option_power_skill, options.Booksanity.option_power, options.Booksanity.option_all],
+ options.BuildingProgression.internal_name: options.BuildingProgression.option_vanilla_cheap,
+ options.BundlePerRoom.internal_name: [options.BundlePerRoom.option_two_fewer, options.BundlePerRoom.option_four_extra],
+ options.BundlePrice.internal_name: options.BundlePrice.option_normal,
+ options.BundleRandomization.internal_name: options.BundleRandomization.option_remixed,
+ options.Chefsanity.internal_name: [options.Chefsanity.preset_none, options.Chefsanity.preset_all],
+ options.Cooksanity.internal_name: [options.Cooksanity.option_none, options.Cooksanity.option_all],
+ options.Craftsanity.internal_name: [options.Craftsanity.option_none, options.Craftsanity.option_all],
+ options.Cropsanity.internal_name: [options.Cropsanity.option_disabled, options.Cropsanity.option_enabled],
+ options.Eatsanity.internal_name: [options.Eatsanity.preset_none, options.Eatsanity.preset_all],
+ options.ElevatorProgression.internal_name: [options.ElevatorProgression.option_vanilla, options.ElevatorProgression.option_progressive],
+ options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all,
+ options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled,
+ options.ExcludeGingerIsland.internal_name: [options.ExcludeGingerIsland.option_false, options.ExcludeGingerIsland.option_true],
+ options.FarmType.internal_name: [options.FarmType.option_standard, options.FarmType.option_meadowlands],
+ options.FestivalLocations.internal_name: [options.FestivalLocations.option_disabled, options.FestivalLocations.option_hard],
+ options.Fishsanity.internal_name: [options.Fishsanity.option_none, options.Fishsanity.option_all],
+ options.Friendsanity.internal_name: [options.Friendsanity.option_none, options.Friendsanity.option_all_with_marriage],
+ options.FriendsanityHeartSize.internal_name: [1, 8],
+ options.Goal.internal_name: options.Goal.option_allsanity,
+ options.IncludeEndgameLocations.internal_name: [options.IncludeEndgameLocations.option_false, options.IncludeEndgameLocations.option_true],
+ options.Mods.internal_name: frozenset(),
+ options.Monstersanity.internal_name: [options.Monstersanity.option_none, options.Monstersanity.option_one_per_monster],
+ options.Moviesanity.internal_name: [options.Moviesanity.option_none, options.Moviesanity.option_all_movies_and_all_loved_snacks],
+ options.Museumsanity.internal_name: [options.Museumsanity.option_none, options.Museumsanity.option_all],
+ options.NumberOfMovementBuffs.internal_name: [0, 12],
+ options.QuestLocations.internal_name: [-1, 56],
+ options.SeasonRandomization.internal_name: [options.SeasonRandomization.option_disabled, options.SeasonRandomization.option_randomized_not_winter],
+ options.Secretsanity.internal_name: [options.Secretsanity.preset_none, options.Secretsanity.preset_all],
+ options.Shipsanity.internal_name: [options.Shipsanity.option_none, options.Shipsanity.option_everything],
+ options.SkillProgression.internal_name: [options.SkillProgression.option_vanilla, options.SkillProgression.option_progressive_with_masteries],
+ options.SpecialOrderLocations.internal_name: [options.SpecialOrderLocations.option_vanilla, options.SpecialOrderLocations.option_board_qi],
+ options.StartWithout.internal_name: [options.StartWithout.preset_none, options.StartWithout.preset_all],
+ options.ToolProgression.internal_name: [options.ToolProgression.option_vanilla, options.ToolProgression.option_progressive],
+ options.TrapDifficulty.internal_name: options.TrapDifficulty.option_medium,
+ options.Walnutsanity.internal_name: [options.Walnutsanity.preset_none, options.Walnutsanity.preset_all],
+ }
+ return low_orphan_options
From 773f3c4f089a60682c4791eff7f1f95d1bb5e055 Mon Sep 17 00:00:00 2001
From: Alchav <59858495+Alchav@users.noreply.github.com>
Date: Sun, 29 Mar 2026 17:25:46 -0400
Subject: [PATCH 70/84] Super Mario Land 2: Fix Space Zone 2 Logic (#6025)
---
worlds/marioland2/logic.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/worlds/marioland2/logic.py b/worlds/marioland2/logic.py
index 54685f91a035..4ccb2eb4c919 100644
--- a/worlds/marioland2/logic.py
+++ b/worlds/marioland2/logic.py
@@ -478,7 +478,7 @@ def space_zone_2_boss(state, player):
def space_zone_2_coins(state, player, coins):
auto_scroll = is_auto_scroll(state, player, "Space Zone 2")
- reachable_coins = 12
+ reachable_coins = 9
if state.has_any(["Mushroom", "Fire Flower", "Carrot", "Space Physics"], player):
reachable_coins += 15
if state.has("Space Physics", player) or not auto_scroll:
@@ -487,7 +487,7 @@ def space_zone_2_coins(state, player, coins):
state.has("Mushroom", player) and state.has_any(["Fire Flower", "Carrot"], player))):
reachable_coins += 3
if state.has("Space Physics", player):
- reachable_coins += 79
+ reachable_coins += 82
if not auto_scroll:
reachable_coins += 21
return coins <= reachable_coins
From a7a7879df4e61bdb5c061ce72b3c7d60451cef9a Mon Sep 17 00:00:00 2001
From: XxDERProjectxX <114278502+XxDERProjectxX@users.noreply.github.com>
Date: Sun, 29 Mar 2026 16:34:21 -0500
Subject: [PATCH 71/84] Satisfactory: bug fix in __init__.py (#5930)
Solved indentation error to return to intended functionality
---
worlds/satisfactory/__init__.py | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/worlds/satisfactory/__init__.py b/worlds/satisfactory/__init__.py
index fc5c6f2edffe..d105bd268c9f 100644
--- a/worlds/satisfactory/__init__.py
+++ b/worlds/satisfactory/__init__.py
@@ -244,14 +244,14 @@ def extend_hint_information(self, _: dict[int, dict[int, str]]):
or self.options.awesome_logic_placement.value == Placement.starting_inventory:
locations_visible_from_start.update(range(1338700, 1338709)) # ids of shop locations 1 to 10
- location_names_with_useful_items: Iterable[str] = [
- location.name
- for location in self.get_locations()
- if location.address in locations_visible_from_start and location.item \
- and location.item.flags & (ItemClassification.progression | ItemClassification.useful) > 0
- ]
-
- self.options.start_location_hints.value.update(location_names_with_useful_items)
+ location_names_with_useful_items: Iterable[str] = [
+ location.name
+ for location in self.get_locations()
+ if location.address in locations_visible_from_start and location.item \
+ and location.item.flags & (ItemClassification.progression | ItemClassification.useful) > 0
+ ]
+
+ self.options.start_location_hints.value.update(location_names_with_useful_items)
def push_precollected_by_name(self, item_name: str) -> None:
item = self.create_item(item_name)
From 96277fe9be7b072412454bfb8820a3b2cec366df Mon Sep 17 00:00:00 2001
From: el-u <109771707+el-u@users.noreply.github.com>
Date: Sun, 29 Mar 2026 23:37:53 +0200
Subject: [PATCH 72/84] lufia2ac: update CollectionRule import (#5936)
---
worlds/lufia2ac/__init__.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/worlds/lufia2ac/__init__.py b/worlds/lufia2ac/__init__.py
index 96de24a4b6a0..d5b104dde4b6 100644
--- a/worlds/lufia2ac/__init__.py
+++ b/worlds/lufia2ac/__init__.py
@@ -5,11 +5,11 @@
from typing import Any, ClassVar, Dict, Iterator, List, Set, Tuple, Type
import settings
-from BaseClasses import Item, ItemClassification, Location, MultiWorld, Region, Tutorial
+from BaseClasses import CollectionRule, Item, ItemClassification, Location, MultiWorld, Region, Tutorial
from Options import PerGameCommonOptions
from Utils import __version__
from worlds.AutoWorld import WebWorld, World
-from worlds.generic.Rules import add_rule, CollectionRule, set_rule
+from worlds.generic.Rules import add_rule, set_rule
from .Client import L2ACSNIClient # noqa: F401
from .Items import ItemData, ItemType, l2ac_item_name_to_id, l2ac_item_table, L2ACItem, start_id as items_start_id
from .Locations import l2ac_location_name_to_id, L2ACLocation
From 95f696c04f986909c57deade47406dfa731369e9 Mon Sep 17 00:00:00 2001
From: Duck <31627079+duckboycool@users.noreply.github.com>
Date: Sun, 29 Mar 2026 16:19:54 -0600
Subject: [PATCH 73/84] WebHost: Remove space before comma separators in
tutorial authors (#5999)
* Remove space before comma
* Factorio authors update
* Simplify template
---
WebHostLib/templates/tutorialLanding.html | 6 +-----
worlds/factorio/__init__.py | 2 +-
2 files changed, 2 insertions(+), 6 deletions(-)
diff --git a/WebHostLib/templates/tutorialLanding.html b/WebHostLib/templates/tutorialLanding.html
index a96da883b624..ac7f24f95ab5 100644
--- a/WebHostLib/templates/tutorialLanding.html
+++ b/WebHostLib/templates/tutorialLanding.html
@@ -20,11 +20,7 @@ {{ tutorial_name }}
{% for file_name, file_data in tutorial_data.files.items() %}
{{ file_data.language }}
- by
- {% for author in file_data.authors %}
- {{ author }}
- {% if not loop.last %}, {% endif %}
- {% endfor %}
+ by {{ file_data.authors | join(", ") }}
{% endfor %}
diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py
index ceb758669d3a..4483cf0238d4 100644
--- a/worlds/factorio/__init__.py
+++ b/worlds/factorio/__init__.py
@@ -37,7 +37,7 @@ class FactorioWeb(WebWorld):
"English",
"setup_en.md",
"setup/en",
- ["Berserker, Farrak Kilhn"]
+ ["Berserker", "Farrak Kilhn"]
)]
option_groups = option_groups
From 88dc1359600ca48c009e36b2f8109ac06be7f030 Mon Sep 17 00:00:00 2001
From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
Date: Sun, 29 Mar 2026 23:32:06 +0100
Subject: [PATCH 74/84] APQuest: Various fixes (#6079)
* Import Buffer from typing_extensions instead of collections.abc for 3.11 compat
* always re-set sound volumes before playing
* fix game window scaling if parent is vertical
* make default volume lower
---
worlds/apquest/client/custom_views.py | 2 +-
worlds/apquest/client/graphics.py | 2 +-
worlds/apquest/client/sounds.py | 7 +++++--
3 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/worlds/apquest/client/custom_views.py b/worlds/apquest/client/custom_views.py
index 4c1d28c73250..026aa1fc8de8 100644
--- a/worlds/apquest/client/custom_views.py
+++ b/worlds/apquest/client/custom_views.py
@@ -77,7 +77,7 @@ def check_resize(self, _: int, _1: int) -> None:
parent_width, parent_height = self.parent.size
self_width_according_to_parent_height = parent_height * 12 / 11
- self_height_according_to_parent_width = parent_height * 11 / 12
+ self_height_according_to_parent_width = parent_width * 11 / 12
if self_width_according_to_parent_height > parent_width:
self.size = parent_width, self_height_according_to_parent_width
diff --git a/worlds/apquest/client/graphics.py b/worlds/apquest/client/graphics.py
index 535bf84ee852..0e31218c6f02 100644
--- a/worlds/apquest/client/graphics.py
+++ b/worlds/apquest/client/graphics.py
@@ -1,10 +1,10 @@
import pkgutil
-from collections.abc import Buffer
from enum import Enum
from io import BytesIO
from typing import Literal, NamedTuple, Protocol, cast
from kivy.uix.image import CoreImage
+from typing_extensions import Buffer
from CommonClient import logger
diff --git a/worlds/apquest/client/sounds.py b/worlds/apquest/client/sounds.py
index f4c4ab769dc0..1e1999bff25e 100644
--- a/worlds/apquest/client/sounds.py
+++ b/worlds/apquest/client/sounds.py
@@ -1,12 +1,12 @@
import asyncio
import pkgutil
from asyncio import Task
-from collections.abc import Buffer
from pathlib import Path
from typing import cast
from kivy import Config
from kivy.core.audio import Sound, SoundLoader
+from typing_extensions import Buffer
from CommonClient import logger
@@ -85,7 +85,7 @@ def __init__(self) -> None:
def ensure_config(self) -> None:
Config.adddefaultsection("APQuest")
- Config.setdefault("APQuest", "volume", 50)
+ Config.setdefault("APQuest", "volume", 30)
self.set_volume_percentage(Config.getint("APQuest", "volume"))
async def sound_manager_loop(self) -> None:
@@ -149,6 +149,7 @@ def play_jingle(self, audio_filename: str) -> None:
continue
if sound_name == audio_filename:
+ sound.volume = self.volume_percentage / 100
sound.play()
self.update_background_music()
higher_priority_sound_is_playing = True
@@ -213,6 +214,7 @@ def do_fade(self) -> None:
# It ends up feeling better if this just always continues playing quietly after being started.
# Even "fading in at a random spot" is better than restarting the song after a jingle / math trap.
if self.game_started and song.state == "stop":
+ song.volume = self.current_background_music_volume * self.volume_percentage / 100
song.play()
song.seek(0)
continue
@@ -228,6 +230,7 @@ def do_fade(self) -> None:
if self.current_background_music_volume != 0:
if song.state == "stop":
+ song.volume = self.current_background_music_volume * self.volume_percentage / 100
song.play()
song.seek(0)
From 2b46df90b44d62c11988ee792f37bd7b7351f262 Mon Sep 17 00:00:00 2001
From: Jarno
Date: Mon, 30 Mar 2026 00:46:01 +0200
Subject: [PATCH 75/84] Satisfactory: Fixed buildings missing from goal check
(#5772)
---
worlds/satisfactory/__init__.py | 15 +++++++++------
1 file changed, 9 insertions(+), 6 deletions(-)
diff --git a/worlds/satisfactory/__init__.py b/worlds/satisfactory/__init__.py
index d105bd268c9f..06c56eabe5a4 100644
--- a/worlds/satisfactory/__init__.py
+++ b/worlds/satisfactory/__init__.py
@@ -88,16 +88,19 @@ def create_items(self) -> None:
self.items.build_item_pool(self.random, precollected_items, number_of_locations)
def set_rules(self) -> None:
- resource_sink_goal: bool = "AWESOME Sink Points (total)" in self.options.goal_selection \
- or "AWESOME Sink Points (per minute)" in self.options.goal_selection
-
required_parts = set(self.game_logic.space_elevator_phases[self.options.final_elevator_phase.value - 1].keys())
+ required_buildings = set()
+
+ if "Space Elevator Phase" in self.options.goal_selection:
+ required_buildings.add("Space Elevator")
- if resource_sink_goal:
- required_parts.union(self.game_logic.buildings["AWESOME Sink"].inputs)
+ if "AWESOME Sink Points (total)" in self.options.goal_selection \
+ or "AWESOME Sink Points (per minute)" in self.options.goal_selection:
+ required_buildings.add("AWESOME Sink")
self.multiworld.completion_condition[self.player] = \
- lambda state: self.state_logic.can_produce_all(state, required_parts)
+ lambda state: self.state_logic.can_produce_all(state, required_parts) \
+ and self.state_logic.can_build_all(state, required_buildings)
def collect(self, state: CollectionState, item: Item) -> bool:
change = super().collect(state, item)
From bdbf72f148be16aae62eb8d55c9e9fd87ea8ccb7 Mon Sep 17 00:00:00 2001
From: Louis M
Date: Sun, 29 Mar 2026 19:40:05 -0400
Subject: [PATCH 76/84] Aquaria: Fixing bug where Urchin Costume is not a
progression damaging item (#5998)
---
worlds/aquaria/Items.py | 6 +++---
worlds/aquaria/Regions.py | 2 +-
worlds/aquaria/__init__.py | 2 +-
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/worlds/aquaria/Items.py b/worlds/aquaria/Items.py
index 3365c1fa5953..b510b24738cb 100644
--- a/worlds/aquaria/Items.py
+++ b/worlds/aquaria/Items.py
@@ -271,7 +271,7 @@ class ItemNames:
ItemNames.TRIDENT: ItemData(698031, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_trident_head
ItemNames.TURTLE_EGG: ItemData(698032, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_turtle_egg
ItemNames.JELLY_EGG: ItemData(698033, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_upsidedown_seed
- ItemNames.URCHIN_COSTUME: ItemData(698034, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_urchin_costume
+ ItemNames.URCHIN_COSTUME: ItemData(698034, 1, ItemType.PROGRESSION, ItemGroup.COLLECTIBLE), # collectible_urchin_costume
ItemNames.BABY_WALKER: ItemData(698035, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_walker
ItemNames.VEDHA_S_CURE_ALL: ItemData(698036, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Vedha'sCure-All
ItemNames.ZUUNA_S_PEROGI: ItemData(698037, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Zuuna'sperogi
@@ -384,8 +384,8 @@ class ItemNames:
ItemNames.MITHALAS_BANNER, ItemNames.MITHALAS_POT, ItemNames.MUTANT_COSTUME, ItemNames.SEED_BAG,
ItemNames.KING_S_SKULL, ItemNames.SONG_PLANT_SPORE, ItemNames.STONE_HEAD, ItemNames.SUN_KEY,
ItemNames.GIRL_COSTUME, ItemNames.ODD_CONTAINER, ItemNames.TRIDENT, ItemNames.TURTLE_EGG,
- ItemNames.JELLY_EGG, ItemNames.URCHIN_COSTUME, ItemNames.BABY_WALKER,
- ItemNames.RAINBOW_MUSHROOM, ItemNames.RAINBOW_MUSHROOM, ItemNames.RAINBOW_MUSHROOM,
+ ItemNames.JELLY_EGG, ItemNames.BABY_WALKER, ItemNames.RAINBOW_MUSHROOM,
+ ItemNames.RAINBOW_MUSHROOM, ItemNames.RAINBOW_MUSHROOM, ItemNames.FISH_OIL,
ItemNames.LEAF_POULTICE, ItemNames.LEAF_POULTICE, ItemNames.LEAF_POULTICE,
ItemNames.LEECHING_POULTICE, ItemNames.LEECHING_POULTICE, ItemNames.ARCANE_POULTICE,
ItemNames.ROTTEN_MEAT, ItemNames.ROTTEN_MEAT, ItemNames.ROTTEN_MEAT, ItemNames.ROTTEN_MEAT,
diff --git a/worlds/aquaria/Regions.py b/worlds/aquaria/Regions.py
index 781d06e09f1a..b32f9119ec83 100755
--- a/worlds/aquaria/Regions.py
+++ b/worlds/aquaria/Regions.py
@@ -37,7 +37,7 @@ def _has_li(state: CollectionState, player: int) -> bool:
DAMAGING_ITEMS:Iterable[str] = [
ItemNames.ENERGY_FORM, ItemNames.NATURE_FORM, ItemNames.BEAST_FORM,
ItemNames.LI_AND_LI_SONG, ItemNames.BABY_NAUTILUS, ItemNames.BABY_PIRANHA,
- ItemNames.BABY_BLASTER
+ ItemNames.BABY_BLASTER, ItemNames.URCHIN_COSTUME
]
def _has_damaging_item(state: CollectionState, player: int, damaging_items:Iterable[str] = DAMAGING_ITEMS) -> bool:
diff --git a/worlds/aquaria/__init__.py b/worlds/aquaria/__init__.py
index 2997f21d0447..395d349154d4 100644
--- a/worlds/aquaria/__init__.py
+++ b/worlds/aquaria/__init__.py
@@ -76,7 +76,7 @@ class AquariaWorld(World):
item_name_groups = {
"Damage": {ItemNames.ENERGY_FORM, ItemNames.NATURE_FORM, ItemNames.BEAST_FORM,
ItemNames.LI_AND_LI_SONG, ItemNames.BABY_NAUTILUS, ItemNames.BABY_PIRANHA,
- ItemNames.BABY_BLASTER},
+ ItemNames.BABY_BLASTER, ItemNames.URCHIN_COSTUME},
"Light": {ItemNames.SUN_FORM, ItemNames.BABY_DUMBO}
}
"""Grouping item make it easier to find them"""
From ba7ca0bd2378175a6ebbe66d2e75c5c95c970660 Mon Sep 17 00:00:00 2001
From: EdricY
Date: Mon, 30 Mar 2026 08:25:25 -0700
Subject: [PATCH 77/84] Options Creator: bind free text set_value to text
instead of on_text_validate (#5915)
---
OptionsCreator.py | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/OptionsCreator.py b/OptionsCreator.py
index 94ca8ba7acc1..30833993e1d2 100644
--- a/OptionsCreator.py
+++ b/OptionsCreator.py
@@ -384,10 +384,11 @@ def open_dropdown(button):
def create_free_text(self, option: typing.Type[FreeText] | typing.Type[TextChoice], name: str):
text = VisualFreeText(option=option, name=name)
- def set_value(instance):
- self.options[name] = instance.text
+ def set_value(instance, value):
+ self.options[name] = value
- text.bind(on_text_validate=set_value)
+ text.bind(text=set_value)
+ self.options[name] = option.default
return text
def create_choice(self, option: typing.Type[Choice], name: str):
From 58a6407040125cd90346dd2a523bc3d0f01f344f Mon Sep 17 00:00:00 2001
From: James White
Date: Mon, 30 Mar 2026 16:27:10 +0100
Subject: [PATCH 78/84] SMW: Prevent receiving your own traps while aliased
(#5763)
---
worlds/smw/Client.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/worlds/smw/Client.py b/worlds/smw/Client.py
index 85524eb7ad41..ef576a873869 100644
--- a/worlds/smw/Client.py
+++ b/worlds/smw/Client.py
@@ -132,7 +132,7 @@ def on_package(self, ctx: SNIClient, cmd: str, args: dict[str, Any]) -> None:
self.instance_id = time.time()
source_name = args["data"]["source"]
- if "TrapLink" in ctx.tags and "TrapLink" in args["tags"] and source_name != ctx.slot_info[ctx.slot].name:
+ if "TrapLink" in ctx.tags and "TrapLink" in args["tags"] and source_name != ctx.player_names[ctx.slot]:
trap_name: str = args["data"]["trap_name"]
if trap_name not in trap_name_to_value:
# We don't know how to handle this trap, ignore it
From c640d2fa249c3a01e6f56a336b60eb4a4c62e38f Mon Sep 17 00:00:00 2001
From: Ian Robinson
Date: Mon, 30 Mar 2026 12:19:10 -0400
Subject: [PATCH 79/84] Rule Builder: Add field resolvers (#5919)
---
.github/pyright-config.json | 1 +
docs/rule builder.md | 36 +++++++
rule_builder/field_resolvers.py | 162 ++++++++++++++++++++++++++++++
rule_builder/rules.py | 113 +++++++++++++++------
test/general/test_rule_builder.py | 67 ++++++++++--
5 files changed, 341 insertions(+), 38 deletions(-)
create mode 100644 rule_builder/field_resolvers.py
diff --git a/.github/pyright-config.json b/.github/pyright-config.json
index fba044da0652..c5432dbf3cf7 100644
--- a/.github/pyright-config.json
+++ b/.github/pyright-config.json
@@ -3,6 +3,7 @@
"../BizHawkClient.py",
"../Patch.py",
"../rule_builder/cached_world.py",
+ "../rule_builder/field_resolvers.py",
"../rule_builder/options.py",
"../rule_builder/rules.py",
"../test/param.py",
diff --git a/docs/rule builder.md b/docs/rule builder.md
index 4f9102a2ba0e..c3a8fcb6c42b 100644
--- a/docs/rule builder.md
+++ b/docs/rule builder.md
@@ -129,6 +129,42 @@ common_rule_only_on_easy = common_rule & easy_filter
common_rule_skipped_on_easy = common_rule | easy_filter
```
+### Field resolvers
+
+When creating rules you may sometimes need to set a field to a value that depends on the world instance. You can use a `FieldResolver` to define how to populate that field when the rule is being resolved.
+
+There are two build-in field resolvers:
+
+- `FromOption`: Resolves to the value of the given option
+- `FromWorldAttr`: Resolves to the value of the given world instance attribute, can specify a dotted path `a.b.c` to get a nested attribute or dict item
+
+```python
+world.options.mcguffin_count = 5
+world.precalculated_value = 99
+rule = (
+ Has("A", count=FromOption(McguffinCount))
+ | HasGroup("Important items", count=FromWorldAttr("precalculated_value"))
+)
+# Results in Has("A", count=5) | HasGroup("Important items", count=99)
+```
+
+You can define your own resolvers by creating a class that inherits from `FieldResolver`, provides your game name, and implements a `resolve` function:
+
+```python
+@dataclasses.dataclass(frozen=True)
+class FromCustomResolution(FieldResolver, game="MyGame"):
+ modifier: str
+
+ @override
+ def resolve(self, world: "World") -> Any:
+ return some_math_calculation(world, self.modifier)
+
+
+rule = Has("Combat Level", count=FromCustomResolution("combat"))
+```
+
+If you want to support rule serialization and your resolver contains non-serializable properties you may need to override `to_dict` or `from_dict`.
+
## Enabling caching
The rule builder provides a `CachedRuleBuilderWorld` base class for your `World` class that enables caching on your rules.
diff --git a/rule_builder/field_resolvers.py b/rule_builder/field_resolvers.py
new file mode 100644
index 000000000000..1e5def6b44b3
--- /dev/null
+++ b/rule_builder/field_resolvers.py
@@ -0,0 +1,162 @@
+import dataclasses
+import importlib
+from abc import ABC, abstractmethod
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, ClassVar, Self, TypeVar, cast, overload
+
+from typing_extensions import override
+
+from Options import Option
+
+if TYPE_CHECKING:
+ from worlds.AutoWorld import World
+
+
+class FieldResolverRegister:
+ """A container class to contain world custom resolvers"""
+
+ custom_resolvers: ClassVar[dict[str, dict[str, type["FieldResolver"]]]] = {}
+ """
+ A mapping of game name to mapping of resolver name to resolver class
+ to hold custom resolvers implemented by worlds
+ """
+
+ @classmethod
+ def get_resolver_cls(cls, game_name: str, resolver_name: str) -> type["FieldResolver"]:
+ """Returns the world-registered or default resolver with the given name"""
+ custom_resolver_classes = cls.custom_resolvers.get(game_name, {})
+ if resolver_name not in DEFAULT_RESOLVERS and resolver_name not in custom_resolver_classes:
+ raise ValueError(f"Resolver '{resolver_name}' for game '{game_name}' not found")
+ return custom_resolver_classes.get(resolver_name) or DEFAULT_RESOLVERS[resolver_name]
+
+
+@dataclasses.dataclass(frozen=True)
+class FieldResolver(ABC):
+ @abstractmethod
+ def resolve(self, world: "World") -> Any: ...
+
+ def to_dict(self) -> dict[str, Any]:
+ """Returns a JSON compatible dict representation of this resolver"""
+ fields = {field.name: getattr(self, field.name, None) for field in dataclasses.fields(self)}
+ return {
+ "resolver": self.__class__.__name__,
+ **fields,
+ }
+
+ @classmethod
+ def from_dict(cls, data: dict[str, Any]) -> Self:
+ """Returns a new instance of this resolver from a serialized dict representation"""
+ assert data.get("resolver", None) == cls.__name__
+ return cls(**{k: v for k, v in data.items() if k != "resolver"})
+
+ @override
+ def __str__(self) -> str:
+ return self.__class__.__name__
+
+ @classmethod
+ def __init_subclass__(cls, /, game: str) -> None:
+ if game != "Archipelago":
+ custom_resolvers = FieldResolverRegister.custom_resolvers.setdefault(game, {})
+ if cls.__qualname__ in custom_resolvers:
+ raise TypeError(f"Resolver {cls.__qualname__} has already been registered for game {game}")
+ custom_resolvers[cls.__qualname__] = cls
+ elif cls.__module__ != "rule_builder.field_resolvers":
+ raise TypeError("You cannot define custom resolvers for the base Archipelago world")
+
+
+@dataclasses.dataclass(frozen=True)
+class FromOption(FieldResolver, game="Archipelago"):
+ option: type[Option[Any]]
+ field: str = "value"
+
+ @override
+ def resolve(self, world: "World") -> Any:
+ option_name = next(
+ (name for name, cls in world.options.__class__.type_hints.items() if cls is self.option),
+ None,
+ )
+
+ if option_name is None:
+ raise ValueError(
+ f"Cannot find option {self.option.__name__} in options class {world.options.__class__.__name__}"
+ )
+ opt = cast(Option[Any] | None, getattr(world.options, option_name, None))
+ if opt is None:
+ raise ValueError(f"Invalid option: {option_name}")
+ return getattr(opt, self.field)
+
+ @override
+ def to_dict(self) -> dict[str, Any]:
+ return {
+ "resolver": "FromOption",
+ "option": f"{self.option.__module__}.{self.option.__name__}",
+ "field": self.field,
+ }
+
+ @override
+ @classmethod
+ def from_dict(cls, data: dict[str, Any]) -> Self:
+ if "option" not in data:
+ raise ValueError("Missing required option")
+
+ option_path = data["option"]
+ try:
+ option_mod_name, option_cls_name = option_path.rsplit(".", 1)
+ option_module = importlib.import_module(option_mod_name)
+ option = getattr(option_module, option_cls_name, None)
+ except (ValueError, ImportError) as e:
+ raise ValueError(f"Cannot parse option '{option_path}'") from e
+ if option is None or not issubclass(option, Option):
+ raise ValueError(f"Invalid option '{option_path}' returns type '{option}' instead of Option subclass")
+
+ return cls(cast(type[Option[Any]], option), data.get("field", "value"))
+
+ @override
+ def __str__(self) -> str:
+ field = f".{self.field}" if self.field != "value" else ""
+ return f"FromOption({self.option.__name__}{field})"
+
+
+@dataclasses.dataclass(frozen=True)
+class FromWorldAttr(FieldResolver, game="Archipelago"):
+ name: str
+
+ @override
+ def resolve(self, world: "World") -> Any:
+ obj: Any = world
+ for field in self.name.split("."):
+ if obj is None:
+ return None
+ if isinstance(obj, Mapping):
+ obj = obj.get(field, None) # pyright: ignore[reportUnknownMemberType]
+ else:
+ obj = getattr(obj, field, None)
+ return obj
+
+ @override
+ def __str__(self) -> str:
+ return f"FromWorldAttr({self.name})"
+
+
+T = TypeVar("T")
+
+
+@overload
+def resolve_field(field: Any, world: "World", expected_type: type[T]) -> T: ...
+@overload
+def resolve_field(field: Any, world: "World", expected_type: None = None) -> Any: ...
+def resolve_field(field: Any, world: "World", expected_type: type[T] | None = None) -> T | Any:
+ if isinstance(field, FieldResolver):
+ field = field.resolve(world)
+ if expected_type:
+ assert isinstance(field, expected_type), f"Expected type {expected_type} but got {type(field)}"
+ return field
+
+
+DEFAULT_RESOLVERS = {
+ resolver_name: resolver_class
+ for resolver_name, resolver_class in locals().items()
+ if isinstance(resolver_class, type)
+ and issubclass(resolver_class, FieldResolver)
+ and resolver_class is not FieldResolver
+}
diff --git a/rule_builder/rules.py b/rule_builder/rules.py
index 77a89c96c238..07c0607c1fb3 100644
--- a/rule_builder/rules.py
+++ b/rule_builder/rules.py
@@ -7,6 +7,7 @@
from BaseClasses import CollectionState
from NetUtils import JSONMessagePart
+from .field_resolvers import FieldResolver, FieldResolverRegister, resolve_field
from .options import OptionFilter
if TYPE_CHECKING:
@@ -108,11 +109,14 @@ def resolve(self, world: TWorld) -> "Resolved":
def to_dict(self) -> dict[str, Any]:
"""Returns a JSON compatible dict representation of this rule"""
- args = {
- field.name: getattr(self, field.name, None)
- for field in dataclasses.fields(self)
- if field.name not in ("options", "filtered_resolution")
- }
+ args = {}
+ for field in dataclasses.fields(self):
+ if field.name in ("options", "filtered_resolution"):
+ continue
+ value = getattr(self, field.name, None)
+ if isinstance(value, FieldResolver):
+ value = value.to_dict()
+ args[field.name] = value
return {
"rule": self.__class__.__qualname__,
"options": [o.to_dict() for o in self.options],
@@ -124,7 +128,19 @@ def to_dict(self) -> dict[str, Any]:
def from_dict(cls, data: Mapping[str, Any], world_cls: "type[World]") -> Self:
"""Returns a new instance of this rule from a serialized dict representation"""
options = OptionFilter.multiple_from_dict(data.get("options", ()))
- return cls(**data.get("args", {}), options=options, filtered_resolution=data.get("filtered_resolution", False))
+ args = cls._parse_field_resolvers(data.get("args", {}), world_cls.game)
+ return cls(**args, options=options, filtered_resolution=data.get("filtered_resolution", False))
+
+ @classmethod
+ def _parse_field_resolvers(cls, data: Mapping[str, Any], game_name: str) -> dict[str, Any]:
+ result: dict[str, Any] = {}
+ for name, value in data.items():
+ if isinstance(value, dict) and "resolver" in value:
+ resolver_cls = FieldResolverRegister.get_resolver_cls(game_name, value["resolver"]) # pyright: ignore[reportUnknownArgumentType]
+ result[name] = resolver_cls.from_dict(value) # pyright: ignore[reportUnknownArgumentType]
+ else:
+ result[name] = value
+ return result
def __and__(self, other: "Rule[Any] | Iterable[OptionFilter] | OptionFilter") -> "Rule[TWorld]":
"""Combines two rules or a rule and an option filter into an And rule"""
@@ -688,24 +704,24 @@ def _instantiate(self, world: TWorld) -> Rule.Resolved:
class Has(Rule[TWorld], game="Archipelago"):
"""A rule that checks if the player has at least `count` of a given item"""
- item_name: str
+ item_name: str | FieldResolver
"""The item to check for"""
- count: int = 1
+ count: int | FieldResolver = 1
"""The count the player is required to have"""
@override
def _instantiate(self, world: TWorld) -> Rule.Resolved:
return self.Resolved(
- self.item_name,
- self.count,
+ resolve_field(self.item_name, world, str),
+ count=resolve_field(self.count, world, int),
player=world.player,
caching_enabled=getattr(world, "rule_caching_enabled", False),
)
@override
def __str__(self) -> str:
- count = f", count={self.count}" if self.count > 1 else ""
+ count = f", count={self.count}" if isinstance(self.count, FieldResolver) or self.count > 1 else ""
options = f", options={self.options}" if self.options else ""
return f"{self.__class__.__name__}({self.item_name}{count}{options})"
@@ -991,7 +1007,7 @@ def __str__(self) -> str:
class HasAllCounts(Rule[TWorld], game="Archipelago"):
"""A rule that checks if the player has all of the specified counts of the given items"""
- item_counts: dict[str, int]
+ item_counts: Mapping[str, int | FieldResolver]
"""A mapping of item name to count to check for"""
@override
@@ -1002,12 +1018,30 @@ def _instantiate(self, world: TWorld) -> Rule.Resolved:
if len(self.item_counts) == 1:
item = next(iter(self.item_counts))
return Has(item, self.item_counts[item]).resolve(world)
+ item_counts = tuple((name, resolve_field(count, world, int)) for name, count in self.item_counts.items())
return self.Resolved(
- tuple(self.item_counts.items()),
+ item_counts,
player=world.player,
caching_enabled=getattr(world, "rule_caching_enabled", False),
)
+ @override
+ def to_dict(self) -> dict[str, Any]:
+ output = super().to_dict()
+ output["args"]["item_counts"] = {
+ key: value.to_dict() if isinstance(value, FieldResolver) else value
+ for key, value in output["args"]["item_counts"].items()
+ }
+ return output
+
+ @override
+ @classmethod
+ def from_dict(cls, data: Mapping[str, Any], world_cls: "type[World]") -> Self:
+ args = data.get("args", {})
+ item_counts = cls._parse_field_resolvers(args.get("item_counts", {}), world_cls.game)
+ options = OptionFilter.multiple_from_dict(data.get("options", ()))
+ return cls(item_counts, options=options, filtered_resolution=data.get("filtered_resolution", False))
+
@override
def __str__(self) -> str:
items = ", ".join([f"{item} x{count}" for item, count in self.item_counts.items()])
@@ -1096,7 +1130,7 @@ def __str__(self) -> str:
class HasAnyCount(Rule[TWorld], game="Archipelago"):
"""A rule that checks if the player has any of the specified counts of the given items"""
- item_counts: dict[str, int]
+ item_counts: Mapping[str, int | FieldResolver]
"""A mapping of item name to count to check for"""
@override
@@ -1107,12 +1141,30 @@ def _instantiate(self, world: TWorld) -> Rule.Resolved:
if len(self.item_counts) == 1:
item = next(iter(self.item_counts))
return Has(item, self.item_counts[item]).resolve(world)
+ item_counts = tuple((name, resolve_field(count, world, int)) for name, count in self.item_counts.items())
return self.Resolved(
- tuple(self.item_counts.items()),
+ item_counts,
player=world.player,
caching_enabled=getattr(world, "rule_caching_enabled", False),
)
+ @override
+ def to_dict(self) -> dict[str, Any]:
+ output = super().to_dict()
+ output["args"]["item_counts"] = {
+ key: value.to_dict() if isinstance(value, FieldResolver) else value
+ for key, value in output["args"]["item_counts"].items()
+ }
+ return output
+
+ @override
+ @classmethod
+ def from_dict(cls, data: Mapping[str, Any], world_cls: "type[World]") -> Self:
+ args = data.get("args", {})
+ item_counts = cls._parse_field_resolvers(args.get("item_counts", {}), world_cls.game)
+ options = OptionFilter.multiple_from_dict(data.get("options", ()))
+ return cls(item_counts, options=options, filtered_resolution=data.get("filtered_resolution", False))
+
@override
def __str__(self) -> str:
items = ", ".join([f"{item} x{count}" for item, count in self.item_counts.items()])
@@ -1204,13 +1256,13 @@ class HasFromList(Rule[TWorld], game="Archipelago"):
item_names: tuple[str, ...]
"""A tuple of item names to check for"""
- count: int = 1
+ count: int | FieldResolver = 1
"""The number of items the player needs to have"""
def __init__(
self,
*item_names: str,
- count: int = 1,
+ count: int | FieldResolver = 1,
options: Iterable[OptionFilter] = (),
filtered_resolution: bool = False,
) -> None:
@@ -1227,7 +1279,7 @@ def _instantiate(self, world: TWorld) -> Rule.Resolved:
return Has(self.item_names[0], self.count).resolve(world)
return self.Resolved(
self.item_names,
- self.count,
+ count=resolve_field(self.count, world, int),
player=world.player,
caching_enabled=getattr(world, "rule_caching_enabled", False),
)
@@ -1235,7 +1287,7 @@ def _instantiate(self, world: TWorld) -> Rule.Resolved:
@override
@classmethod
def from_dict(cls, data: Mapping[str, Any], world_cls: "type[World]") -> Self:
- args = {**data.get("args", {})}
+ args = cls._parse_field_resolvers(data.get("args", {}), world_cls.game)
item_names = args.pop("item_names", ())
options = OptionFilter.multiple_from_dict(data.get("options", ()))
return cls(*item_names, **args, options=options, filtered_resolution=data.get("filtered_resolution", False))
@@ -1338,13 +1390,13 @@ class HasFromListUnique(Rule[TWorld], game="Archipelago"):
item_names: tuple[str, ...]
"""A tuple of item names to check for"""
- count: int = 1
+ count: int | FieldResolver = 1
"""The number of items the player needs to have"""
def __init__(
self,
*item_names: str,
- count: int = 1,
+ count: int | FieldResolver = 1,
options: Iterable[OptionFilter] = (),
filtered_resolution: bool = False,
) -> None:
@@ -1354,14 +1406,15 @@ def __init__(
@override
def _instantiate(self, world: TWorld) -> Rule.Resolved:
- if len(self.item_names) == 0 or len(self.item_names) < self.count:
+ count = resolve_field(self.count, world, int)
+ if len(self.item_names) == 0 or len(self.item_names) < count:
# match state.has_from_list_unique
return False_().resolve(world)
if len(self.item_names) == 1:
return Has(self.item_names[0]).resolve(world)
return self.Resolved(
self.item_names,
- self.count,
+ count,
player=world.player,
caching_enabled=getattr(world, "rule_caching_enabled", False),
)
@@ -1369,7 +1422,7 @@ def _instantiate(self, world: TWorld) -> Rule.Resolved:
@override
@classmethod
def from_dict(cls, data: Mapping[str, Any], world_cls: "type[World]") -> Self:
- args = {**data.get("args", {})}
+ args = cls._parse_field_resolvers(data.get("args", {}), world_cls.game)
item_names = args.pop("item_names", ())
options = OptionFilter.multiple_from_dict(data.get("options", ()))
return cls(*item_names, **args, options=options, filtered_resolution=data.get("filtered_resolution", False))
@@ -1468,7 +1521,7 @@ class HasGroup(Rule[TWorld], game="Archipelago"):
item_name_group: str
"""The name of the item group containing the items"""
- count: int = 1
+ count: int | FieldResolver = 1
"""The number of items the player needs to have"""
@override
@@ -1477,14 +1530,14 @@ def _instantiate(self, world: TWorld) -> Rule.Resolved:
return self.Resolved(
self.item_name_group,
item_names,
- self.count,
+ count=resolve_field(self.count, world, int),
player=world.player,
caching_enabled=getattr(world, "rule_caching_enabled", False),
)
@override
def __str__(self) -> str:
- count = f", count={self.count}" if self.count > 1 else ""
+ count = f", count={self.count}" if isinstance(self.count, FieldResolver) or self.count > 1 else ""
options = f", options={self.options}" if self.options else ""
return f"{self.__class__.__name__}({self.item_name_group}{count}{options})"
@@ -1542,7 +1595,7 @@ class HasGroupUnique(Rule[TWorld], game="Archipelago"):
item_name_group: str
"""The name of the item group containing the items"""
- count: int = 1
+ count: int | FieldResolver = 1
"""The number of items the player needs to have"""
@override
@@ -1551,14 +1604,14 @@ def _instantiate(self, world: TWorld) -> Rule.Resolved:
return self.Resolved(
self.item_name_group,
item_names,
- self.count,
+ count=resolve_field(self.count, world, int),
player=world.player,
caching_enabled=getattr(world, "rule_caching_enabled", False),
)
@override
def __str__(self) -> str:
- count = f", count={self.count}" if self.count > 1 else ""
+ count = f", count={self.count}" if isinstance(self.count, FieldResolver) or self.count > 1 else ""
options = f", options={self.options}" if self.options else ""
return f"{self.__class__.__name__}({self.item_name_group}{count}{options})"
diff --git a/test/general/test_rule_builder.py b/test/general/test_rule_builder.py
index 81003dcd87f9..85e239175d4d 100644
--- a/test/general/test_rule_builder.py
+++ b/test/general/test_rule_builder.py
@@ -6,8 +6,9 @@
from BaseClasses import CollectionState, Item, ItemClassification, Location, MultiWorld, Region
from NetUtils import JSONMessagePart
-from Options import Choice, FreeText, Option, OptionSet, PerGameCommonOptions, Toggle
+from Options import Choice, FreeText, Option, OptionSet, PerGameCommonOptions, Range, Toggle
from rule_builder.cached_world import CachedRuleBuilderWorld
+from rule_builder.field_resolvers import FieldResolver, FromOption, FromWorldAttr, resolve_field
from rule_builder.options import Operator, OptionFilter
from rule_builder.rules import (
And,
@@ -59,12 +60,20 @@ class SetOption(OptionSet):
valid_keys: ClassVar[set[str]] = {"one", "two", "three"} # pyright: ignore[reportIncompatibleVariableOverride]
+class RangeOption(Range):
+ auto_display_name = True
+ range_start = 1
+ range_end = 10
+ default = 5
+
+
@dataclass
class RuleBuilderOptions(PerGameCommonOptions):
toggle_option: ToggleOption
choice_option: ChoiceOption
text_option: FreeTextOption
set_option: SetOption
+ range_option: RangeOption
GAME_NAME = "Rule Builder Test Game"
@@ -659,14 +668,15 @@ def test_has_all_counts(self) -> None:
self.assertFalse(resolved_rule(self.state))
def test_has_any_count(self) -> None:
- item_counts = {"Item 1": 1, "Item 2": 2}
+ item_counts: dict[str, int | FieldResolver] = {"Item 1": 1, "Item 2": 2}
rule = HasAnyCount(item_counts)
resolved_rule = rule.resolve(self.world)
self.world.register_rule_dependencies(resolved_rule)
for item_name, count in item_counts.items():
item = self.world.create_item(item_name)
- for _ in range(count):
+ num_items = resolve_field(count, self.world, int)
+ for _ in range(num_items):
self.assertFalse(resolved_rule(self.state))
self.state.collect(item)
self.assertTrue(resolved_rule(self.state))
@@ -763,7 +773,7 @@ class TestSerialization(RuleBuilderTestCase):
rule: ClassVar[Rule[Any]] = And(
Or(
- Has("i1", count=4),
+ Has("i1", count=FromOption(RangeOption)),
HasFromList("i2", "i3", "i4", count=2),
HasAnyCount({"i5": 2, "i6": 3}),
options=[OptionFilter(ToggleOption, 0)],
@@ -771,7 +781,7 @@ class TestSerialization(RuleBuilderTestCase):
Or(
HasAll("i7", "i8"),
HasAllCounts(
- {"i9": 1, "i10": 5},
+ {"i9": 1, "i10": FromWorldAttr("instance_data.i10_count")},
options=[OptionFilter(ToggleOption, 1, operator="ne")],
filtered_resolution=True,
),
@@ -811,7 +821,14 @@ class TestSerialization(RuleBuilderTestCase):
"rule": "Has",
"options": [],
"filtered_resolution": False,
- "args": {"item_name": "i1", "count": 4},
+ "args": {
+ "item_name": "i1",
+ "count": {
+ "resolver": "FromOption",
+ "option": "test.general.test_rule_builder.RangeOption",
+ "field": "value",
+ },
+ },
},
{
"rule": "HasFromList",
@@ -848,7 +865,12 @@ class TestSerialization(RuleBuilderTestCase):
},
],
"filtered_resolution": True,
- "args": {"item_counts": {"i9": 1, "i10": 5}},
+ "args": {
+ "item_counts": {
+ "i9": 1,
+ "i10": {"resolver": "FromWorldAttr", "name": "instance_data.i10_count"},
+ }
+ },
},
{
"rule": "CanReachRegion",
@@ -923,7 +945,7 @@ def test_deserialize(self) -> None:
multiworld = setup_solo_multiworld(self.world_cls, steps=(), seed=0)
world = multiworld.worlds[1]
deserialized_rule = world.rule_from_dict(self.rule_dict)
- self.assertEqual(deserialized_rule, self.rule, str(deserialized_rule))
+ self.assertEqual(deserialized_rule, self.rule, f"\n{deserialized_rule}\n{self.rule}")
class TestExplain(RuleBuilderTestCase):
@@ -1342,3 +1364,32 @@ def test_str(self) -> None:
"& False)",
)
assert str(self.resolved_rule) == " ".join(expected)
+
+
+@classvar_matrix(
+ rules=(
+ (
+ Has("A", FromOption(RangeOption)),
+ Has.Resolved("A", count=5, player=1),
+ ),
+ (
+ Has("B", FromWorldAttr("pre_calculated")),
+ Has.Resolved("B", count=3, player=1),
+ ),
+ (
+ Has("C", FromWorldAttr("instance_data.key")),
+ Has.Resolved("C", count=7, player=1),
+ ),
+ )
+)
+class TestFieldResolvers(RuleBuilderTestCase):
+ rules: ClassVar[tuple[Rule[Any], Rule.Resolved]]
+
+ def test_simplify(self) -> None:
+ multiworld = setup_solo_multiworld(self.world_cls, steps=("generate_early",), seed=0)
+ world = multiworld.worlds[1]
+ world.pre_calculated = 3 # pyright: ignore[reportAttributeAccessIssue]
+ world.instance_data = {"key": 7} # pyright: ignore[reportAttributeAccessIssue]
+ rule, expected = self.rules
+ resolved_rule = rule.resolve(world)
+ self.assertEqual(resolved_rule, expected, f"\n{resolved_rule}\n{expected}")
From 2ee20a3ac445a33e51bc160c073734de1fc4358f Mon Sep 17 00:00:00 2001
From: black-sliver <59490463+black-sliver@users.noreply.github.com>
Date: Mon, 30 Mar 2026 19:46:43 +0000
Subject: [PATCH 80/84] CI: set permissions, update and pin actions, CodeQL for
actions (#6073)
* CI: reduce default permissions to minimum
* CI: update pin actions
Most of them. CodeQL and action-gh-release is untouched for now.
Immutable actions and actions/* are pinned to version,
other actions are pinned to hash.
* CI: make use of archive: false in upload-artifact
also set compression level and error behavior for scan-build upload.
* CI: update codeql and enable scanning actions
---
.github/workflows/analyze-modified-files.yml | 6 ++--
.github/workflows/build.yml | 31 ++++++++++----------
.github/workflows/codeql-analysis.yml | 24 ++++++++++-----
.github/workflows/ctest.yml | 4 ++-
.github/workflows/docker.yml | 16 +++++-----
.github/workflows/label-pull-requests.yml | 2 +-
.github/workflows/release.yml | 12 ++++----
.github/workflows/scan-build.yml | 10 +++++--
.github/workflows/strict-type-check.yml | 6 ++--
.github/workflows/unittests.yml | 10 ++++---
10 files changed, 71 insertions(+), 50 deletions(-)
diff --git a/.github/workflows/analyze-modified-files.yml b/.github/workflows/analyze-modified-files.yml
index 862a050c517e..79c4f983a482 100644
--- a/.github/workflows/analyze-modified-files.yml
+++ b/.github/workflows/analyze-modified-files.yml
@@ -14,6 +14,8 @@ env:
BEFORE: ${{ github.event.before }}
AFTER: ${{ github.event.after }}
+permissions: {}
+
jobs:
flake8-or-mypy:
strategy:
@@ -25,7 +27,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6.0.2
- name: "Determine modified files (pull_request)"
if: github.event_name == 'pull_request'
@@ -50,7 +52,7 @@ jobs:
run: |
echo "diff=." >> $GITHUB_ENV
- - uses: actions/setup-python@v5
+ - uses: actions/setup-python@v6.2.0
if: env.diff != ''
with:
python-version: '3.11'
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 772a6c0be359..8ed0c3523c33 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -41,9 +41,9 @@ jobs:
runs-on: windows-latest
steps:
# - copy code below to release.yml -
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6.0.2
- name: Install python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6.2.0
with:
python-version: '~3.12.7'
check-latest: true
@@ -82,7 +82,7 @@ jobs:
# - copy code above to release.yml -
- name: Attest Build
if: ${{ github.event_name == 'workflow_dispatch' }}
- uses: actions/attest-build-provenance@v2
+ uses: actions/attest@v4.1.0
with:
subject-path: |
build/exe.*/ArchipelagoLauncher.exe
@@ -110,18 +110,17 @@ jobs:
cp Players/Templates/VVVVVV.yaml Players/
timeout 30 ./ArchipelagoGenerate
- name: Store 7z
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7.0.0
with:
- name: ${{ env.ZIP_NAME }}
path: dist/${{ env.ZIP_NAME }}
- compression-level: 0 # .7z is incompressible by zip
+ archive: false
if-no-files-found: error
retention-days: 7 # keep for 7 days, should be enough
- name: Store Setup
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7.0.0
with:
- name: ${{ env.SETUP_NAME }}
path: setups/${{ env.SETUP_NAME }}
+ archive: false
if-no-files-found: error
retention-days: 7 # keep for 7 days, should be enough
@@ -129,14 +128,14 @@ jobs:
runs-on: ubuntu-22.04
steps:
# - copy code below to release.yml -
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6.0.2
- name: Install base dependencies
run: |
sudo apt update
sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0
sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below
- name: Get a recent python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6.2.0
with:
python-version: '~3.12.7'
check-latest: true
@@ -173,7 +172,7 @@ jobs:
# - copy code above to release.yml -
- name: Attest Build
if: ${{ github.event_name == 'workflow_dispatch' }}
- uses: actions/attest-build-provenance@v2
+ uses: actions/attest@v4.1.0
with:
subject-path: |
build/exe.*/ArchipelagoLauncher
@@ -204,17 +203,17 @@ jobs:
cp Players/Templates/VVVVVV.yaml Players/
timeout 30 ./ArchipelagoGenerate
- name: Store AppImage
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7.0.0
with:
- name: ${{ env.APPIMAGE_NAME }}
path: dist/${{ env.APPIMAGE_NAME }}
+ archive: false
+ # TODO: decide if we want to also upload the zsync
if-no-files-found: error
retention-days: 7
- name: Store .tar.gz
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7.0.0
with:
- name: ${{ env.TAR_NAME }}
path: dist/${{ env.TAR_NAME }}
- compression-level: 0 # .gz is incompressible by zip
+ archive: false
if-no-files-found: error
retention-days: 7
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 3abbb5f6449f..5751dce8571a 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -17,17 +17,26 @@ on:
paths:
- '**.py'
- '**.js'
- - '.github/workflows/codeql-analysis.yml'
+ - '.github/workflows/*.yml'
+ - '.github/workflows/*.yaml'
+ - '**/action.yml'
+ - '**/action.yaml'
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
paths:
- '**.py'
- '**.js'
- - '.github/workflows/codeql-analysis.yml'
+ - '.github/workflows/*.yml'
+ - '.github/workflows/*.yaml'
+ - '**/action.yml'
+ - '**/action.yaml'
schedule:
- cron: '44 8 * * 1'
+permissions:
+ security-events: write
+
jobs:
analyze:
name: Analyze
@@ -36,18 +45,17 @@ jobs:
strategy:
fail-fast: false
matrix:
- language: [ 'javascript', 'python' ]
- # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
+ language: [ 'javascript', 'python', 'actions' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v6.0.2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@v3
+ uses: github/codeql-action/init@v4.35.1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -58,7 +66,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
- uses: github/codeql-action/autobuild@v3
+ uses: github/codeql-action/autobuild@v4.35.1
# âšī¸ Command-line programs to run using the OS shell.
# đ https://git.io/JvXDl
@@ -72,4 +80,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v3
+ uses: github/codeql-action/analyze@v4.35.1
diff --git a/.github/workflows/ctest.yml b/.github/workflows/ctest.yml
index 610f6d747779..1a39afa11dc7 100644
--- a/.github/workflows/ctest.yml
+++ b/.github/workflows/ctest.yml
@@ -24,6 +24,8 @@ on:
- '**/CMakeLists.txt'
- '.github/workflows/ctest.yml'
+permissions: {}
+
jobs:
ctest:
runs-on: ${{ matrix.os }}
@@ -35,7 +37,7 @@ jobs:
os: [ubuntu-latest, windows-latest]
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6.0.2
- uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756
if: startsWith(matrix.os,'windows')
- uses: Bacondish2023/setup-googletest@49065d1f7a6d21f6134864dd65980fe5dbe06c73
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 0061dd15b000..231fb59dc556 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -19,6 +19,8 @@ on:
env:
REGISTRY: ghcr.io
+permissions: {}
+
jobs:
prepare:
runs-on: ubuntu-latest
@@ -29,7 +31,7 @@ jobs:
package-name: ${{ steps.package.outputs.name }}
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v6.0.2
- name: Set lowercase image name
id: image
@@ -43,7 +45,7 @@ jobs:
- name: Extract metadata
id: meta
- uses: docker/metadata-action@v5
+ uses: docker/metadata-action@v6.0.0
with:
images: ${{ env.REGISTRY }}/${{ steps.image.outputs.name }}
tags: |
@@ -92,13 +94,13 @@ jobs:
cache-scope: arm64
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v6.0.2
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
+ uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Log in to GitHub Container Registry
- uses: docker/login-action@v3
+ uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -115,7 +117,7 @@ jobs:
echo "tags=$(IFS=','; echo "${suffixed[*]}")" >> $GITHUB_OUTPUT
- name: Build and push Docker image
- uses: docker/build-push-action@v5
+ uses: docker/build-push-action@v7.0.0
with:
context: .
file: ./Dockerfile
@@ -135,7 +137,7 @@ jobs:
packages: write
steps:
- name: Log in to GitHub Container Registry
- uses: docker/login-action@v3
+ uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
diff --git a/.github/workflows/label-pull-requests.yml b/.github/workflows/label-pull-requests.yml
index 1675c942bddb..341735e5dd1a 100644
--- a/.github/workflows/label-pull-requests.yml
+++ b/.github/workflows/label-pull-requests.yml
@@ -14,7 +14,7 @@ jobs:
name: 'Apply content-based labels'
runs-on: ubuntu-latest
steps:
- - uses: actions/labeler@v5
+ - uses: actions/labeler@v6.0.1
with:
sync-labels: false
peer_review:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 7f81e5750746..21e1a24b8889 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -48,9 +48,9 @@ jobs:
shell: bash
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
# - code below copied from build.yml -
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6.0.2
- name: Install python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6.2.0
with:
python-version: '~3.12.7'
check-latest: true
@@ -88,7 +88,7 @@ jobs:
echo "SETUP_NAME=$SETUP_NAME" >> $Env:GITHUB_ENV
# - code above copied from build.yml -
- name: Attest Build
- uses: actions/attest-build-provenance@v2
+ uses: actions/attest@v4.1.0
with:
subject-path: |
build/exe.*/ArchipelagoLauncher.exe
@@ -114,14 +114,14 @@ jobs:
- name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
# - code below copied from build.yml -
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6.0.2
- name: Install base dependencies
run: |
sudo apt update
sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0
sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below
- name: Get a recent python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6.2.0
with:
python-version: '~3.12.7'
check-latest: true
@@ -157,7 +157,7 @@ jobs:
echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV
# - code above copied from build.yml -
- name: Attest Build
- uses: actions/attest-build-provenance@v2
+ uses: actions/attest@v4.1.0
with:
subject-path: |
build/exe.*/ArchipelagoLauncher
diff --git a/.github/workflows/scan-build.yml b/.github/workflows/scan-build.yml
index ac842070625f..64f51af4a258 100644
--- a/.github/workflows/scan-build.yml
+++ b/.github/workflows/scan-build.yml
@@ -28,12 +28,14 @@ on:
- 'requirements.txt'
- '.github/workflows/scan-build.yml'
+permissions: {}
+
jobs:
scan-build:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6.0.2
with:
submodules: recursive
- name: Install newer Clang
@@ -45,7 +47,7 @@ jobs:
run: |
sudo apt install clang-tools-19
- name: Get a recent python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6.2.0
with:
python-version: '3.11'
- name: Install dependencies
@@ -59,7 +61,9 @@ jobs:
scan-build-19 --status-bugs -o scan-build-reports -disable-checker deadcode.DeadStores python setup.py build -y
- name: Store report
if: failure()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7.0.0
with:
name: scan-build-reports
path: scan-build-reports
+ compression-level: 9 # highly compressible
+ if-no-files-found: error
diff --git a/.github/workflows/strict-type-check.yml b/.github/workflows/strict-type-check.yml
index 2ccdad8d11af..4a876bf98ebf 100644
--- a/.github/workflows/strict-type-check.yml
+++ b/.github/workflows/strict-type-check.yml
@@ -14,13 +14,15 @@ on:
- ".github/workflows/strict-type-check.yml"
- "**.pyi"
+permissions: {}
+
jobs:
pyright:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6.0.2
- - uses: actions/setup-python@v5
+ - uses: actions/setup-python@v6.2.0
with:
python-version: "3.11"
diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml
index b08b389005ec..cfffa6cc4a51 100644
--- a/.github/workflows/unittests.yml
+++ b/.github/workflows/unittests.yml
@@ -29,6 +29,8 @@ on:
- '!.github/workflows/**'
- '.github/workflows/unittests.yml'
+permissions: {}
+
jobs:
unit:
runs-on: ${{ matrix.os }}
@@ -51,9 +53,9 @@ jobs:
os: macos-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6.0.2
- name: Set up Python ${{ matrix.python.version }}
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6.2.0
with:
python-version: ${{ matrix.python.version }}
- name: Install dependencies
@@ -78,9 +80,9 @@ jobs:
- {version: '3.13'} # current
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6.0.2
- name: Set up Python ${{ matrix.python.version }}
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6.2.0
with:
python-version: ${{ matrix.python.version }}
- name: Install dependencies
From 5360b6bb3727505ce5987b1e1dff316f4d792574 Mon Sep 17 00:00:00 2001
From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
Date: Mon, 30 Mar 2026 23:31:05 +0100
Subject: [PATCH 81/84] The Witness: (Unbeatable seed) Ensure Desert Laser
Redirection is required when the box is rotated (#5889)
* Unbeatable seed: 11 lasers + redirect when the box is rotated
* naming
---
worlds/witness/player_logic.py | 4 ++--
worlds/witness/test/test_lasers.py | 29 +++++++++++++++++++++++++++++
2 files changed, 31 insertions(+), 2 deletions(-)
diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py
index aed6d3da66bb..b24434732ffc 100644
--- a/worlds/witness/player_logic.py
+++ b/worlds/witness/player_logic.py
@@ -527,7 +527,7 @@ def handle_panelhunt_postgame(self, world: "WitnessWorld") -> List[List[str]]:
if chal_lasers > 7:
postgame_adjustments.append([
"Requirement Changes:",
- "0xFFF00 - 11 Lasers - True",
+ "0xFFF00 - 11 Lasers + Redirect - True",
])
if disable_challenge_lasers:
@@ -640,7 +640,7 @@ def make_options_adjustments(self, world: "WitnessWorld") -> None:
if chal_lasers <= 7 or mnt_lasers > 7:
adjustment_linesets_in_order.append([
"Requirement Changes:",
- "0xFFF00 - 11 Lasers - True",
+ "0xFFF00 - 11 Lasers + Redirect - True",
])
if world.options.disable_non_randomized_puzzles:
diff --git a/worlds/witness/test/test_lasers.py b/worlds/witness/test/test_lasers.py
index 5681757161e7..4a71c0d433be 100644
--- a/worlds/witness/test/test_lasers.py
+++ b/worlds/witness/test/test_lasers.py
@@ -216,3 +216,32 @@ def test_doors_to_elevator_paths(self) -> None:
}
self.assert_can_beat_with_minimally(exact_requirement)
+
+
+class LongBoxNeedsAllLasersWhenBoxIsRotated(WitnessTestBase):
+ options = {
+ "puzzle_randomization": "sigma_expert",
+ "shuffle_symbols": True,
+ "shuffle_doors": "mixed",
+ "door_groupings": "off",
+ "shuffle_boat": True,
+ "shuffle_lasers": "anywhere",
+ "disable_non_randomized_puzzles": False,
+ "shuffle_discarded_panels": True,
+ "shuffle_vault_boxes": True,
+ "obelisk_keys": True,
+ "shuffle_EPs": "individual",
+ "EP_difficulty": "eclipse",
+ "shuffle_postgame": False,
+ "victory_condition": "elevator",
+ "mountain_lasers": 11,
+ "challenge_lasers": 11,
+ "early_caves": "off",
+ "elevators_come_to_you": {"Quarry Elevator"},
+ }
+
+ run_default_tests = False
+
+ def test_long_box_needs_all_lasers_when_box_is_rotated(self):
+ long_box_location = self.world.get_location("Mountaintop Box Long Solved")
+ self.assert_dependency_on_event_item(long_box_location, "+1 Laser (Redirected)")
From 3c4af8f43249f34633b4ba69af69f4ef851f9117 Mon Sep 17 00:00:00 2001
From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
Date: Tue, 31 Mar 2026 19:55:52 +0100
Subject: [PATCH 82/84] APQuest: Tap to move (#6082)
* Tap to move
* inputs
* cleanup
* oops
---
worlds/apquest/client/ap_quest_client.kv | 5 +-
worlds/apquest/client/ap_quest_client.py | 61 +++++++++++++-
worlds/apquest/client/custom_views.py | 48 ++++++++++-
worlds/apquest/client/game_manager.py | 41 ++++++++--
worlds/apquest/game/entities.py | 65 ++++++++++-----
worlds/apquest/game/game.py | 83 ++++++++++++++++---
worlds/apquest/game/gameboard.py | 34 ++++++++
worlds/apquest/game/generate_math_problem.py | 1 +
worlds/apquest/game/path_finding.py | 84 ++++++++++++++++++++
9 files changed, 377 insertions(+), 45 deletions(-)
create mode 100644 worlds/apquest/game/path_finding.py
diff --git a/worlds/apquest/client/ap_quest_client.kv b/worlds/apquest/client/ap_quest_client.kv
index 9f4024e62a02..e60136a835f5 100644
--- a/worlds/apquest/client/ap_quest_client.kv
+++ b/worlds/apquest/client/ap_quest_client.kv
@@ -30,7 +30,10 @@
C to fire available Confetti Cannons
Number Keys + Backspace for Math Trap\n
- Rebinding controls might be added in the future :)"""
+ [b]Click to move also works![/b]
+
+ Click/tap Confetti Cannon to fire it
+ Submit Math Trap solution in the command line at the bottom"""
:
orientation: "horizontal"
diff --git a/worlds/apquest/client/ap_quest_client.py b/worlds/apquest/client/ap_quest_client.py
index c1edc8edb4b2..9b6b82b75f46 100644
--- a/worlds/apquest/client/ap_quest_client.py
+++ b/worlds/apquest/client/ap_quest_client.py
@@ -4,8 +4,9 @@
from enum import Enum
from typing import TYPE_CHECKING, Any
-from CommonClient import CommonContext, gui_enabled, logger, server_loop
+from CommonClient import ClientCommandProcessor, CommonContext, logger, server_loop
from NetUtils import ClientStatus
+from Utils import gui_enabled
from ..game.events import ConfettiFired, LocationClearedEvent, MathProblemSolved, MathProblemStarted, VictoryEvent
from ..game.game import Game
@@ -41,6 +42,16 @@ class ConnectionStatus(Enum):
GAME_RUNNING = 3
+class APQuestClientCommandProcessor(ClientCommandProcessor):
+ ctx: "APQuestContext"
+
+ def default(self, raw: str) -> None:
+ if self.ctx.external_math_trap_input(raw):
+ return
+
+ super().default(raw)
+
+
class APQuestContext(CommonContext):
game = "APQuest"
items_handling = 0b111 # full remote
@@ -65,6 +76,7 @@ class APQuestContext(CommonContext):
delay_intro_song: bool
ui: APQuestManager
+ command_processor = APQuestClientCommandProcessor
def __init__(
self, server_address: str | None = None, password: str | None = None, delay_intro_song: bool = False
@@ -244,6 +256,53 @@ def input_and_rerender(self, input_key: Input) -> None:
self.ap_quest_game.input(input_key)
self.render()
+ def queue_auto_move(self, target_x: int, target_y: int) -> None:
+ if self.ap_quest_game is None:
+ return
+ if not self.ap_quest_game.gameboard.ready:
+ return
+ self.ap_quest_game.queue_auto_move(target_x, target_y)
+ self.ui.start_auto_move()
+
+ def do_auto_move_and_rerender(self) -> None:
+ if self.ap_quest_game is None:
+ return
+ if not self.ap_quest_game.gameboard.ready:
+ return
+ changed = self.ap_quest_game.do_auto_move()
+ if changed:
+ self.render()
+
+ def confetti_and_rerender(self) -> None:
+ # Used by tap mode
+ if self.ap_quest_game is None:
+ return
+ if not self.ap_quest_game.gameboard.ready:
+ return
+
+ if self.ap_quest_game.attempt_fire_confetti_cannon():
+ self.render()
+
+ def external_math_trap_input(self, raw: str) -> bool:
+ if self.ap_quest_game is None:
+ return False
+ if not self.ap_quest_game.gameboard.ready:
+ return False
+ if not self.ap_quest_game.active_math_problem:
+ return False
+
+ raw = raw.strip()
+
+ if not raw:
+ return False
+ if not raw.isnumeric():
+ return False
+
+ self.ap_quest_game.math_problem_replace([int(digit) for digit in raw])
+ self.render()
+
+ return True
+
def make_gui(self) -> "type[kvui.GameManager]":
self.load_kv()
return APQuestManager
diff --git a/worlds/apquest/client/custom_views.py b/worlds/apquest/client/custom_views.py
index 026aa1fc8de8..cc44f991d630 100644
--- a/worlds/apquest/client/custom_views.py
+++ b/worlds/apquest/client/custom_views.py
@@ -8,15 +8,17 @@
from kivy.graphics import Color, Triangle
from kivy.graphics.instructions import Canvas
from kivy.input import MotionEvent
+from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
+from kivy.uix.image import Image
+from kivy.uix.widget import Widget
from kivymd.uix.recycleview import MDRecycleView
from CommonClient import logger
from ..game.inputs import Input
-
INPUT_MAP = {
"up": Input.UP,
"w": Input.UP,
@@ -51,8 +53,9 @@ def __init__(self, input_function: Callable[[Input], None], **kwargs: Any) -> No
self.input_function = input_function
self.bind_keyboard()
- def on_touch_down(self, touch: MotionEvent) -> None:
+ def on_touch_down(self, touch: MotionEvent) -> bool | None:
self.bind_keyboard()
+ return super().on_touch_down(touch)
def bind_keyboard(self) -> None:
if self._keyboard is not None:
@@ -203,13 +206,23 @@ def reduce_life(self, dt: float, canvas: Canvas) -> bool:
return True
-class ConfettiView(MDRecycleView):
+class ConfettiView(Widget):
confetti: list[Confetti]
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.confetti = []
+ # Don't eat tap events for the game grid under the confetti view
+ def on_touch_down(self, touch) -> bool:
+ return False
+
+ def on_touch_move(self, touch) -> bool:
+ return False
+
+ def on_touch_up(self, touch) -> bool:
+ return False
+
def check_resize(self, _: int, _1: int) -> None:
parent_width, parent_height = self.parent.size
@@ -254,3 +267,32 @@ class VolumeSliderView(BoxLayout):
class APQuestControlsView(BoxLayout):
pass
+
+
+class TapImage(ButtonBehavior, Image):
+ callback: Callable[[], None]
+
+ def __init__(self, callback: Callable[[], None], **kwargs) -> None:
+ self.callback = callback
+ super().__init__(**kwargs)
+
+ def on_release(self) -> bool:
+ self.callback()
+
+ return True
+
+
+class TapIfConfettiCannonImage(ButtonBehavior, Image):
+ callback: Callable[[], None]
+
+ is_confetti_cannon: bool = False
+
+ def __init__(self, callback: Callable[[], None], **kwargs: dict[str, Any]) -> None:
+ self.callback = callback
+ super().__init__(**kwargs)
+
+ def on_release(self) -> bool:
+ if self.is_confetti_cannon:
+ self.callback()
+
+ return True
diff --git a/worlds/apquest/client/game_manager.py b/worlds/apquest/client/game_manager.py
index 86f4316d12a2..241fbec0ae72 100644
--- a/worlds/apquest/client/game_manager.py
+++ b/worlds/apquest/client/game_manager.py
@@ -6,6 +6,7 @@
# isort: on
from typing import TYPE_CHECKING, Any
+from kivy._clock import ClockEvent
from kivy.clock import Clock
from kivy.uix.gridlayout import GridLayout
from kivy.uix.image import Image
@@ -13,7 +14,16 @@
from kivymd.uix.recycleview import MDRecycleView
from ..game.game import Game
-from .custom_views import APQuestControlsView, APQuestGameView, APQuestGrid, ConfettiView, VolumeSliderView
+from ..game.graphics import Graphic
+from .custom_views import (
+ APQuestControlsView,
+ APQuestGameView,
+ APQuestGrid,
+ ConfettiView,
+ TapIfConfettiCannonImage,
+ TapImage,
+ VolumeSliderView,
+)
from .graphics import PlayerSprite, get_texture
from .sounds import SoundManager
@@ -34,9 +44,11 @@ class APQuestManager(GameManager):
sound_manager: SoundManager
bottom_image_grid: list[list[Image]]
- top_image_grid: list[list[Image]]
+ top_image_grid: list[list[TapImage]]
confetti_view: ConfettiView
+ move_event: ClockEvent | None
+
bottom_grid_is_grass: bool
def __init__(self, *args: Any, **kwargs: Any) -> None:
@@ -45,6 +57,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
self.sound_manager.allow_intro_to_play = not self.ctx.delay_intro_song
self.top_image_grid = []
self.bottom_image_grid = []
+ self.move_event = None
self.bottom_grid_is_grass = False
def allow_intro_song(self) -> None:
@@ -74,7 +87,7 @@ def game_started(self) -> None:
self.sound_manager.game_started = True
def render(self, game: Game, player_sprite: PlayerSprite) -> None:
- self.setup_game_grid_if_not_setup(game.gameboard.size)
+ self.setup_game_grid_if_not_setup(game)
# This calls game.render(), which needs to happen to update the state of math traps
self.render_gameboard(game, player_sprite)
@@ -104,6 +117,8 @@ def render_item_column(self, game: Game) -> None:
for item_graphic, image_row in zip(rendered_item_column, self.top_image_grid, strict=False):
image = image_row[-1]
+ image.is_confetti_cannon = item_graphic == Graphic.CONFETTI_CANNON
+
texture = get_texture(item_graphic)
if texture is None:
image.opacity = 0
@@ -136,23 +151,25 @@ def render_background_game_grid(self, size: tuple[int, int], grass: bool) -> Non
self.bottom_grid_is_grass = grass
- def setup_game_grid_if_not_setup(self, size: tuple[int, int]) -> None:
+ def setup_game_grid_if_not_setup(self, game: Game) -> None:
if self.upper_game_grid.children:
return
self.top_image_grid = []
self.bottom_image_grid = []
- for _row in range(size[1]):
+ size = game.gameboard.size
+
+ for row in range(size[1]):
self.top_image_grid.append([])
self.bottom_image_grid.append([])
- for _column in range(size[0]):
+ for column in range(size[0]):
bottom_image = Image(fit_mode="fill", color=(0.3, 0.3, 0.3))
self.lower_game_grid.add_widget(bottom_image)
self.bottom_image_grid[-1].append(bottom_image)
- top_image = Image(fit_mode="fill")
+ top_image = TapImage(lambda y=row, x=column: self.ctx.queue_auto_move(x, y), fit_mode="fill")
self.upper_game_grid.add_widget(top_image)
self.top_image_grid[-1].append(top_image)
@@ -160,11 +177,19 @@ def setup_game_grid_if_not_setup(self, size: tuple[int, int]) -> None:
image = Image(fit_mode="fill", color=(0.3, 0.3, 0.3))
self.lower_game_grid.add_widget(image)
- image2 = Image(fit_mode="fill", opacity=0)
+ image2 = TapIfConfettiCannonImage(lambda: self.ctx.confetti_and_rerender(), fit_mode="fill", opacity=0)
self.upper_game_grid.add_widget(image2)
self.top_image_grid[-1].append(image2)
+ def start_auto_move(self) -> None:
+ if self.move_event is not None:
+ self.move_event.cancel()
+
+ self.ctx.do_auto_move_and_rerender()
+
+ self.move_event = Clock.schedule_interval(lambda _: self.ctx.do_auto_move_and_rerender(), 0.10)
+
def build(self) -> Layout:
container = super().build()
diff --git a/worlds/apquest/game/entities.py b/worlds/apquest/game/entities.py
index 64b89206f6f6..ae7c7e85bce6 100644
--- a/worlds/apquest/game/entities.py
+++ b/worlds/apquest/game/entities.py
@@ -17,8 +17,10 @@ class Entity:
class InteractableMixin:
+ auto_move_attempt_passing_through = False
+
@abstractmethod
- def interact(self, player: Player) -> None:
+ def interact(self, player: Player) -> bool:
pass
@@ -89,15 +91,16 @@ def open(self) -> None:
self.is_open = True
self.update_solidity()
- def interact(self, player: Player) -> None:
+ def interact(self, player: Player) -> bool:
if self.has_given_content:
- return
+ return False
if self.is_open:
self.give_content(player)
- return
+ return True
self.open()
+ return True
def content_success(self) -> None:
self.update_solidity()
@@ -135,47 +138,59 @@ def graphic(self) -> Graphic:
class KeyDoor(Door, InteractableMixin):
+ auto_move_attempt_passing_through = True
+
closed_graphic = Graphic.KEY_DOOR
- def interact(self, player: Player) -> None:
+ def interact(self, player: Player) -> bool:
if self.is_open:
- return
+ return False
if not player.has_item(Item.KEY):
- return
+ return False
player.remove_item(Item.KEY)
self.open()
+ return True
+
class BreakableBlock(Door, InteractableMixin):
+ auto_move_attempt_passing_through = True
+
closed_graphic = Graphic.BREAKABLE_BLOCK
- def interact(self, player: Player) -> None:
+ def interact(self, player: Player) -> bool:
if self.is_open:
- return
+ return False
if not player.has_item(Item.HAMMER):
- return
+ return False
player.remove_item(Item.HAMMER)
self.open()
+ return True
+
class Bush(Door, InteractableMixin):
+ auto_move_attempt_passing_through = True
+
closed_graphic = Graphic.BUSH
- def interact(self, player: Player) -> None:
+ def interact(self, player: Player) -> bool:
if self.is_open:
- return
+ return False
if not player.has_item(Item.SWORD):
- return
+ return False
self.open()
+ return True
+
class Button(Entity, InteractableMixin):
solid = True
@@ -186,12 +201,13 @@ class Button(Entity, InteractableMixin):
def __init__(self, activates: ActivatableMixin) -> None:
self.activates = activates
- def interact(self, player: Player) -> None:
+ def interact(self, player: Player) -> bool:
if self.activated:
- return
+ return False
self.activated = True
self.activates.activate(player)
+ return True
@property
def graphic(self) -> Graphic:
@@ -240,9 +256,9 @@ def heal_if_not_dead(self) -> None:
return
self.current_health = self.max_health
- def interact(self, player: Player) -> None:
+ def interact(self, player: Player) -> bool:
if self.dead:
- return
+ return False
if player.has_item(Item.SWORD):
self.current_health = max(0, self.current_health - 1)
@@ -250,9 +266,10 @@ def interact(self, player: Player) -> None:
if self.current_health == 0:
if not self.dead:
self.die()
- return
+ return True
player.damage(2)
+ return True
@property
def graphic(self) -> Graphic:
@@ -270,13 +287,15 @@ def die(self) -> None:
self.dead = True
self.solid = not self.has_given_content
- def interact(self, player: Player) -> None:
+ def interact(self, player: Player) -> bool:
if self.dead:
if not self.has_given_content:
self.give_content(player)
- return
+ return True
+ return False
super().interact(player)
+ return True
@property
def graphic(self) -> Graphic:
@@ -303,10 +322,12 @@ class FinalBoss(Enemy):
}
enemy_default_graphic = Graphic.BOSS_1_HEALTH
- def interact(self, player: Player) -> None:
+ def interact(self, player: Player) -> bool:
dead_before = self.dead
- super().interact(player)
+ changed = super().interact(player)
if not dead_before and self.dead:
player.victory()
+
+ return changed
diff --git a/worlds/apquest/game/game.py b/worlds/apquest/game/game.py
index 709e74850ab3..21bebca68137 100644
--- a/worlds/apquest/game/game.py
+++ b/worlds/apquest/game/game.py
@@ -23,6 +23,8 @@ class Game:
active_math_problem: MathProblem | None
active_math_problem_input: list[int] | None
+ auto_target_path: list[tuple[int, int]] = []
+
remotely_received_items: set[tuple[int, int, int]]
def __init__(
@@ -94,29 +96,40 @@ def render_health_and_inventory(self, vertical: bool = False) -> tuple[Graphic,
return tuple(graphics_array)
- def attempt_player_movement(self, direction: Direction) -> None:
+ def attempt_player_movement(self, direction: Direction, cancel_auto_move: bool = True) -> bool:
+ if cancel_auto_move:
+ self.cancel_auto_move()
+
self.player.facing = direction
delta_x, delta_y = direction.value
new_x, new_y = self.player.current_x + delta_x, self.player.current_y + delta_y
- if not self.gameboard.get_entity_at(new_x, new_y).solid:
- self.player.current_x = new_x
- self.player.current_y = new_y
+ if self.gameboard.get_entity_at(new_x, new_y).solid:
+ return False
+
+ self.player.current_x = new_x
+ self.player.current_y = new_y
+ return True
- def attempt_interact(self) -> None:
+ def attempt_interact(self) -> bool:
delta_x, delta_y = self.player.facing.value
entity_x, entity_y = self.player.current_x + delta_x, self.player.current_y + delta_y
entity = self.gameboard.get_entity_at(entity_x, entity_y)
if isinstance(entity, InteractableMixin):
- entity.interact(self.player)
+ return entity.interact(self.player)
+
+ return False
+
+ def attempt_fire_confetti_cannon(self) -> bool:
+ if not self.player.has_item(Item.CONFETTI_CANNON):
+ return False
- def attempt_fire_confetti_cannon(self) -> None:
- if self.player.has_item(Item.CONFETTI_CANNON):
- self.player.remove_item(Item.CONFETTI_CANNON)
- self.queued_events.append(ConfettiFired(self.player.current_x, self.player.current_y))
+ self.player.remove_item(Item.CONFETTI_CANNON)
+ self.queued_events.append(ConfettiFired(self.player.current_x, self.player.current_y))
+ return True
def math_problem_success(self) -> None:
self.active_math_problem = None
@@ -154,6 +167,12 @@ def math_problem_delete(self) -> None:
self.active_math_problem_input.pop()
self.check_math_problem_result()
+ def math_problem_replace(self, input: list[int]) -> None:
+ if self.active_math_problem_input is None:
+ return
+ self.active_math_problem_input = input[:2]
+ self.check_math_problem_result()
+
def input(self, input_key: Input) -> None:
if not self.gameboard.ready:
return
@@ -201,3 +220,47 @@ def receive_item(self, remote_item_id: int, remote_location_id: int, remote_loca
def force_clear_location(self, location_id: int) -> None:
location = Location(location_id)
self.gameboard.force_clear_location(location)
+
+ def cancel_auto_move(self) -> None:
+ self.auto_target_path = []
+
+ def queue_auto_move(self, target_x: int, target_y: int) -> None:
+ self.cancel_auto_move()
+ path = self.gameboard.calculate_shortest_path(self.player.current_x, self.player.current_y, target_x, target_y)
+ self.auto_target_path = path
+
+ def do_auto_move(self) -> bool:
+ if not self.auto_target_path:
+ return False
+
+ target_x, target_y = self.auto_target_path.pop(0)
+ movement = target_x - self.player.current_x, target_y - self.player.current_y
+ direction = Direction(movement)
+ moved = self.attempt_player_movement(direction, cancel_auto_move=False)
+
+ if moved:
+ return True
+
+ # We are attempting to interact with something on the path.
+ # First, make the player face it.
+ if self.player.facing != direction:
+ self.player.facing = direction
+ self.auto_target_path.insert(0, (target_x, target_y))
+ return True
+
+ # If we are facing it, attempt to interact with it.
+ changed = self.attempt_interact()
+
+ if not changed:
+ self.cancel_auto_move()
+ return False
+
+ # If the interaction was successful, and this was the end of the path, stop
+ # (i.e. don't try to attack the attacked enemy over and over until it's dead)
+ if not self.auto_target_path:
+ self.cancel_auto_move()
+ return True
+
+ # If there is more to go, keep going along the path
+ self.auto_target_path.insert(0, (target_x, target_y))
+ return True
diff --git a/worlds/apquest/game/gameboard.py b/worlds/apquest/game/gameboard.py
index ec97491c872f..77688c2929f9 100644
--- a/worlds/apquest/game/gameboard.py
+++ b/worlds/apquest/game/gameboard.py
@@ -15,6 +15,7 @@
EnemyWithLoot,
Entity,
FinalBoss,
+ InteractableMixin,
KeyDoor,
LocationMixin,
Wall,
@@ -23,6 +24,7 @@
from .graphics import DIGIT_TO_GRAPHIC, DIGIT_TO_GRAPHIC_ZERO_EMPTY, MATH_PROBLEM_TYPE_TO_GRAPHIC, Graphic
from .items import Item
from .locations import DEFAULT_CONTENT, Location
+from .path_finding import find_path_or_closest
if TYPE_CHECKING:
from .player import Player
@@ -107,6 +109,21 @@ def render(self, player: Player) -> tuple[tuple[Graphic, ...], ...]:
return tuple(graphics)
+ def as_traversability_bools(self) -> tuple[tuple[bool, ...], ...]:
+ traversability = []
+
+ for y, row in enumerate(self.gameboard):
+ traversable_row = []
+ for x, entity in enumerate(row):
+ traversable_row.append(
+ not entity.solid
+ or (isinstance(entity, InteractableMixin) and entity.auto_move_attempt_passing_through)
+ )
+
+ traversability.append(tuple(traversable_row))
+
+ return tuple(traversability)
+
def render_math_problem(
self, problem: MathProblem, current_input_digits: list[int], current_input_int: int | None
) -> tuple[tuple[Graphic, ...], ...]:
@@ -186,6 +203,23 @@ def force_clear_location(self, location: Location) -> None:
entity = self.remote_entity_by_location_id[location]
entity.force_clear()
+ def calculate_shortest_path(
+ self, source_x: int, source_y: int, target_x: int, target_y: int
+ ) -> list[tuple[int, int]]:
+ gameboard_traversability = self.as_traversability_bools()
+
+ path = find_path_or_closest(gameboard_traversability, source_x, source_y, target_x, target_y)
+
+ if not path:
+ return path
+
+ # If the path stops just short of target, attempt interacting with it at the end
+ if abs(path[-1][0] - target_x) + abs(path[-1][1] - target_y) == 1:
+ if isinstance(self.gameboard[target_y][target_x], InteractableMixin):
+ path.append((target_x, target_y))
+
+ return path[1:] # Cut off starting tile
+
@property
def ready(self) -> bool:
return self.content_filled
diff --git a/worlds/apquest/game/generate_math_problem.py b/worlds/apquest/game/generate_math_problem.py
index eb8ff0f01ec9..b93375e9ac41 100644
--- a/worlds/apquest/game/generate_math_problem.py
+++ b/worlds/apquest/game/generate_math_problem.py
@@ -6,6 +6,7 @@
_random = random.Random()
+
class NumberChoiceConstraints(NamedTuple):
num_1_min: int
num_1_max: int
diff --git a/worlds/apquest/game/path_finding.py b/worlds/apquest/game/path_finding.py
new file mode 100644
index 000000000000..8b3e649b1d77
--- /dev/null
+++ b/worlds/apquest/game/path_finding.py
@@ -0,0 +1,84 @@
+import heapq
+from typing import Generator
+
+Point = tuple[int, int]
+
+
+def heuristic(a: Point, b: Point) -> int:
+ # Manhattan distance (good for 4-directional grids)
+ return abs(a[0] - b[0]) + abs(a[1] - b[1])
+
+
+def reconstruct_path(came_from: dict[Point, Point], current: Point) -> list[Point]:
+ path = [current]
+ while current in came_from:
+ current = came_from[current]
+ path.append(current)
+ path.reverse()
+ return path
+
+
+def find_path_or_closest(
+ grid: tuple[tuple[bool, ...], ...], source_x: int, source_y: int, target_x: int, target_y: int
+) -> list[Point]:
+ start = source_x, source_y
+ goal = target_x, target_y
+
+ rows, cols = len(grid), len(grid[0])
+
+ def in_bounds(p: Point) -> bool:
+ return 0 <= p[0] < rows and 0 <= p[1] < cols
+
+ def passable(p: Point) -> bool:
+ return grid[p[1]][p[0]]
+
+ def neighbors(p: Point) -> Generator[Point, None, None]:
+ x, y = p
+ for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
+ np = (x + dx, y + dy)
+ if in_bounds(np) and passable(np):
+ yield np
+
+ open_heap: list[tuple[int, tuple[int, int]]] = []
+ heapq.heappush(open_heap, (0, start))
+
+ came_from: dict[Point, Point] = {}
+ g_score = {start: 0}
+
+ # Track best fallback node
+ best_node = start
+ best_dist = heuristic(start, goal)
+
+ visited = set()
+
+ while open_heap:
+ _, current = heapq.heappop(open_heap)
+
+ if current in visited:
+ continue
+ visited.add(current)
+
+ # Check if we reached the goal
+ if current == goal:
+ return reconstruct_path(came_from, current)
+
+ # Update "closest node" fallback
+ dist = heuristic(current, goal)
+ if dist < best_dist or (dist == best_dist and g_score[current] < g_score.get(best_node, float("inf"))):
+ best_node = current
+ best_dist = dist
+
+ for neighbor in neighbors(current):
+ tentative_g = g_score[current] + 1 # cost is 1 per move
+
+ if tentative_g < g_score.get(neighbor, float("inf")):
+ came_from[neighbor] = current
+ g_score[neighbor] = tentative_g
+ f_score = tentative_g + heuristic(neighbor, goal)
+ heapq.heappush(open_heap, (f_score, neighbor))
+
+ # Goal not reachable â return path to closest node
+ if best_node is not None:
+ return reconstruct_path(came_from, best_node)
+
+ return []
From 68f25f4642b4f71e935a4de930f484cd4ccf2dfd Mon Sep 17 00:00:00 2001
From: Silvris <58583688+Silvris@users.noreply.github.com>
Date: Wed, 1 Apr 2026 12:18:11 -0500
Subject: [PATCH 83/84] MM3: Bump world version (#6088)
---
worlds/mm3/archipelago.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/worlds/mm3/archipelago.json b/worlds/mm3/archipelago.json
index ed5ecffc6cbe..d9946d34049c 100644
--- a/worlds/mm3/archipelago.json
+++ b/worlds/mm3/archipelago.json
@@ -1,6 +1,6 @@
{
"game": "Mega Man 3",
"authors": ["Silvris"],
- "world_version": "0.1.7",
+ "world_version": "0.1.8",
"minimum_ap_version": "0.6.4"
}
From debe4cf035c7c15efe6fb95f72343af0d420c68c Mon Sep 17 00:00:00 2001
From: Bryce Wilson
Date: Wed, 1 Apr 2026 10:18:42 -0700
Subject: [PATCH 84/84] Pokemon Emerald: Bump version (#6083)
---
worlds/pokemon_emerald/CHANGELOG.md | 13 +++++++++++++
worlds/pokemon_emerald/archipelago.json | 2 +-
2 files changed, 14 insertions(+), 1 deletion(-)
diff --git a/worlds/pokemon_emerald/CHANGELOG.md b/worlds/pokemon_emerald/CHANGELOG.md
index 3921e33400bf..d2173beb10fe 100644
--- a/worlds/pokemon_emerald/CHANGELOG.md
+++ b/worlds/pokemon_emerald/CHANGELOG.md
@@ -1,3 +1,16 @@
+# 2.5.0
+
+### Features
+
+- Added a new option `dexsanity_encounter_types` to enable/disable dexsanity locations based on whether they can be
+found in the allowed encounters. In other words, if Bulbasaur can only be found by fishing and fishing is not enabled,
+a dexsanity location will not be created for Bulbasaur.
+
+### Fixes
+
+- Fixed generator error if Wailord or Relicanth are blacklisted during a dexsanity seed.
+- Fixed generator error if player greatly restricts allowed opponent pokemon while force fully evolved is active.
+
# 2.4.1
### Fixes
diff --git a/worlds/pokemon_emerald/archipelago.json b/worlds/pokemon_emerald/archipelago.json
index ed11b8d8cc8c..753ccb1f33d0 100644
--- a/worlds/pokemon_emerald/archipelago.json
+++ b/worlds/pokemon_emerald/archipelago.json
@@ -1,6 +1,6 @@
{
"game": "Pokemon Emerald",
- "world_version": "2.4.1",
+ "world_version": "2.5.0",
"minimum_ap_version": "0.6.1",
"authors": ["Zunawe"]
}