From 42d24b4869ce5f5e1be34b6cb2202bd55aaa985f Mon Sep 17 00:00:00 2001 From: qwint Date: Fri, 17 Apr 2026 18:23:19 -0500 Subject: [PATCH 01/66] HK: add manifest (#6057) --- worlds/hk/archipelago.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 worlds/hk/archipelago.json diff --git a/worlds/hk/archipelago.json b/worlds/hk/archipelago.json new file mode 100644 index 000000000000..d296027c1698 --- /dev/null +++ b/worlds/hk/archipelago.json @@ -0,0 +1 @@ +{"game": "Hollow Knight", "world_version": "0.0.0"} From bdde2140b328f5d2e3683b60b714700f477b062c Mon Sep 17 00:00:00 2001 From: Duck <31627079+duckboycool@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:24:25 -0600 Subject: [PATCH 02/66] Core: Remove worlds.generic.PlandoItem (#6035) --- Utils.py | 7 ------- worlds/generic/__init__.py | 21 --------------------- 2 files changed, 28 deletions(-) diff --git a/Utils.py b/Utils.py index 0210086274f4..b32d863cdd28 100644 --- a/Utils.py +++ b/Utils.py @@ -450,13 +450,10 @@ def get_unique_identifier(): class RestrictedUnpickler(pickle.Unpickler): - generic_properties_module: Optional[object] - def __init__(self, *args: Any, **kwargs: Any) -> None: super(RestrictedUnpickler, self).__init__(*args, **kwargs) self.options_module = importlib.import_module("Options") self.net_utils_module = importlib.import_module("NetUtils") - self.generic_properties_module = None def find_class(self, module: str, name: str) -> type: if module == "builtins" and name in safe_builtins: @@ -470,10 +467,6 @@ def find_class(self, module: str, name: str) -> type: "SlotType", "NetworkSlot", "HintStatus"}: return getattr(self.net_utils_module, name) # Options and Plando are unpickled by WebHost -> Generate - if module == "worlds.generic" and name == "PlandoItem": - if not self.generic_properties_module: - self.generic_properties_module = importlib.import_module("worlds.generic") - return getattr(self.generic_properties_module, name) # pep 8 specifies that modules should have "all-lowercase names" (options, not Options) if module.lower().endswith("options"): if module == "Options": diff --git a/worlds/generic/__init__.py b/worlds/generic/__init__.py index 4cd80556ccf3..0d4a32c858e3 100644 --- a/worlds/generic/__init__.py +++ b/worlds/generic/__init__.py @@ -52,24 +52,3 @@ def create_item(self, name: str) -> Item: if name == "Nothing": return Item(name, ItemClassification.filler, -1, self.player) raise InvalidItemError(name) - -@deprecated("worlds.generic.PlandoItem is deprecated and will be removed in the next version. " - "Use Options.PlandoItem(s) instead.") -class PlandoItem(NamedTuple): - item: str - location: str - world: Union[bool, str] = False # False -> own world, True -> not own world - from_pool: bool = True # if item should be removed from item pool - force: str = 'silent' # false -> warns if item not successfully placed. true -> errors out on failure to place item. - - def warn(self, warning: str): - if self.force in ['true', 'fail', 'failure', 'none', 'false', 'warn', 'warning']: - logging.warning(f'{warning}') - else: - logging.debug(f'{warning}') - - def failed(self, warning: str, exception=Exception): - if self.force in ['true', 'fail', 'failure']: - raise exception(warning) - else: - self.warn(warning) From 5f9e38b78389278cfb3aa9cf2095455863b31937 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sat, 18 Apr 2026 00:15:37 +0000 Subject: [PATCH 03/66] Test: add test for unpickling NetUtils enums (#5326) * Test: add test for unpickling NetUtils enums This verifies that Utils.ByValue either works or is not required, and once we drop ByValue, this validates that future Python versions do not break our Enums again. * Test: NetUtils enum switch to more direct pickle.dumps It's probably better to use the direct interface in case restricted_dumps does some funky stuff in the future. Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com> * Test: NetUtils enum fix import for change --------- Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com> --- test/netutils/test_enum.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 test/netutils/test_enum.py diff --git a/test/netutils/test_enum.py b/test/netutils/test_enum.py new file mode 100644 index 000000000000..3d7cdd4e332e --- /dev/null +++ b/test/netutils/test_enum.py @@ -0,0 +1,37 @@ +"""Verify that NetUtils' enums work correctly with all supported Python versions.""" + +import pickle +import unittest +from enum import Enum +from typing import Type + +from NetUtils import ClientStatus, HintStatus, SlotType +from Utils import restricted_loads + + +class Base: + class DataEnumTest(unittest.TestCase): + type: Type[Enum] + value: Enum + + def test_unpickle(self) -> None: + """Tests that enums used in multidata or multisave can be pickled and unpickled.""" + pickled = pickle.dumps(self.value) + unpickled = restricted_loads(pickled) + self.assertEqual(unpickled, self.value) + self.assertIsInstance(unpickled, self.type) + + +class HintStatusTest(Base.DataEnumTest): + type = HintStatus + value = HintStatus.HINT_AVOID + + +class ClientStatusTest(Base.DataEnumTest): + type = ClientStatus + value = ClientStatus.CLIENT_GOAL + + +class SlotTypeTest(Base.DataEnumTest): + type = SlotType + value = SlotType.player From 66712bbd87a15f387ec4776515267a32c36bdb12 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sat, 18 Apr 2026 12:48:32 +0100 Subject: [PATCH 04/66] APQuest: Switch to Rule Builder (#5906) * CachedRuleBuilderWorld * CachedRuleBuilderWorld * APQuest Rule Builder finished * Added comment Add a rule to check if the player has a Sword to destroy bushes. * Bump version + typo fix * Update worlds/apquest/rules.py Co-authored-by: Ian Robinson * Address Tchops' review comments --------- Co-authored-by: Ian Robinson --- worlds/apquest/archipelago.json | 4 +- worlds/apquest/rules.py | 173 ++++++++++++++++++-------------- 2 files changed, 99 insertions(+), 78 deletions(-) diff --git a/worlds/apquest/archipelago.json b/worlds/apquest/archipelago.json index 6a6c3ddd02c3..6392e0693041 100644 --- a/worlds/apquest/archipelago.json +++ b/worlds/apquest/archipelago.json @@ -1,6 +1,6 @@ { "game": "APQuest", - "minimum_ap_version": "0.6.4", - "world_version": "1.0.1", + "minimum_ap_version": "0.6.7", + "world_version": "2.0.0", "authors": ["NewSoupVi"] } diff --git a/worlds/apquest/rules.py b/worlds/apquest/rules.py index 533c33d5eac2..02c76daefcb8 100644 --- a/worlds/apquest/rules.py +++ b/worlds/apquest/rules.py @@ -2,12 +2,16 @@ from typing import TYPE_CHECKING -from BaseClasses import CollectionState -from worlds.generic.Rules import add_rule, set_rule +from rule_builder.options import OptionFilter +from rule_builder.rules import Has, HasAll, Rule + +from .options import HardMode if TYPE_CHECKING: from .world import APQuestWorld +HAS_KEY = Has("Key") # Hmm, what could this be? A little foreshadowing perhaps? :) You'll find out if you keep reading! + def set_all_rules(world: APQuestWorld) -> None: # In order for AP to generate an item layout that is actually possible for the player to complete, @@ -26,36 +30,46 @@ def set_all_entrance_rules(world: APQuestWorld) -> None: overworld_to_top_left_room = world.get_entrance("Overworld to Top Left Room") right_room_to_final_boss_room = world.get_entrance("Right Room to Final Boss Room") - # An access rule is a function. We can define this function like any other function. - # This function must accept exactly one parameter: A "CollectionState". - # A CollectionState describes the current progress of the players in the multiworld, i.e. what items they have, - # which regions they've reached, etc. - # In an access rule, we can ask whether the player has a collected a certain item. - # We can do this via the state.has(...) function. - # This function takes an item name, a player number, and an optional count parameter (more on that below) - # Since a rule only takes a CollectionState parameter, but we also need the player number in the state.has call, - # our function needs to be locally defined so that it has access to the player number from the outer scope. - # In our case, we are inside a function that has access to the "world" parameter, so we can use world.player. - def can_destroy_bush(state: CollectionState) -> bool: - return state.has("Sword", world.player) - - # Now we can set our "can_destroy_bush" rule to our entrance which requires slashing a bush to clear the path. - # One way to set rules is via the set_rule() function, which works on both Entrances and Locations. - set_rule(overworld_to_bottom_right_room, can_destroy_bush) - - # Because the function has to be defined locally, most worlds prefer the lambda syntax. - set_rule(overworld_to_top_left_room, lambda state: state.has("Key", world.player)) - - # Conditions can depend on event items. - set_rule(right_room_to_final_boss_room, lambda state: state.has("Top Left Room Button Pressed", world.player)) + # Now, let's make some rules! + # First, let's handle the transition from the overworld to the bottom right room, + # which requires slashing a bush with the Sword. + # For this, we need a rule that says "player has a Sword". + # We can use a "Has"-type rule from the rule_builder module for this. + can_destroy_bush = Has("Sword") + + # Now we can set our "can_destroy_bush" rule to the entrance which requires slashing a bush to clear the path. + # The easiest way to do this is by calling world.set_rule, which works for both Locations and Entrances. + world.set_rule(overworld_to_bottom_right_room, can_destroy_bush) + + # Conditions can also depend on event items. + button_pressed = Has("Top Left Room Button Pressed") + world.set_rule(right_room_to_final_boss_room, button_pressed) # Some entrance rules may only apply if the player enabled certain options. # In our case, if the hammer option is enabled, we need to add the Hammer requirement to the Entrance from # Overworld to the Top Middle Room. if world.options.hammer: overworld_to_top_middle_room = world.get_entrance("Overworld to Top Middle Room") - set_rule(overworld_to_top_middle_room, lambda state: state.has("Hammer", world.player)) - + can_smash_brick = Has("Hammer") + world.set_rule(overworld_to_top_middle_room, can_smash_brick) + + # So far, we've been using "Has" from the Rule Builder to make our rules. + # There is another way to make rules that you will see in a lot of older worlds. + # A rule can just be a function that takes a "state" argument and returns a bool. + # As a demonstration of what that looks like, let's do it with our final Entrance rule: + world.set_rule(overworld_to_top_left_room, lambda state: state.has("Key", world.player)) + # This style is not really recommended anymore, though. + # Notice how you have to explicitly capture world.player here so that the rule applies to the correct player? + # Well, Rule Builder does this part for you, inside of world.set_rule. + # This doesn't just result in shorter code, it also means you can define rules statically (at the module level). + # APQuest opts to create its Rule objects locally, but just to show what this would look like, + # we'll re-set the "Overworld to Top Left Room" rule to a constant defined at the top of this file: + world.set_rule(overworld_to_top_left_room, HAS_KEY) + + # Beyond these structural advantages, + # Rule Builder also allows the core AP code to do a lot of under-the-hood optimizations. + # Rule Builder is quite comprehensive, and even if you have really esoteric rules, + # you can make custom rules by subclassing CustomRule. def set_all_location_rules(world: APQuestWorld) -> None: # Location rules work no differently from Entrance rules. @@ -67,65 +81,72 @@ def set_all_location_rules(world: APQuestWorld) -> None: # So, we need to set requirements on the Locations themselves. # Since combat is a bit more complicated, we'll use this chance to cover some advanced access rule concepts. - # Sometimes, you may want to have different rules depending on the player's chosen options. - # There is a wrong way to do this, and a right way to do this. Let's do the wrong way first. - right_room_enemy = world.get_location("Right Room Enemy Drop") - - # DON'T DO THIS!!!! - set_rule( - right_room_enemy, - lambda state: ( - state.has("Sword", world.player) - and (not world.options.hard_mode or state.has_any(("Shield", "Health Upgrade"), world.player)) - ), - ) - # DON'T DO THIS!!!! - - # Now, what's actually wrong with this? It works perfectly fine, right? - # If hard mode disabled, Sword is enough. If hard mode is enabled, we also need a Shield or a Health Upgrade. - # The access rule we just wrote does this correctly, so what's the problem? - # The problem is performance. - # Most of your world code doesn't need to be perfectly performant, since it just runs once per slot. - # However, access rules in particular are by far the hottest code path in Archipelago. - # An access rule will potentially be called thousands or even millions of times over the course of one generation. - # As a result, access rules are the one place where it's really worth putting in some effort to optimize. - # What's the performance problem here? - # Every time our access rule is called, it has to evaluate whether world.options.hard_mode is True or False. - # Wouldn't it be better if in easy mode, the access rule only checked for Sword to begin with? - # Wouldn't it also be better if in hard mode, it already knew it had to check Shield and Health Upgrade as well? - # Well, we can achieve this by doing the "if world.options.hard_mode" check outside the set_rule call, - # and instead having two *different* set_rule calls depending on which case we're in. + # In "set_all_entrance_rules", we had a rule for a location that doesn't always exist. + # In this case, we had to check for its existence (by checking the player's chosen options) before setting the rule. + # Other times, you may have a situation where a location can have two different rules depending on the options. + # In our case, the enemy in the right room has more health if hard mode is selected, + # so ontop of the Sword, the player will either need one more health or a Shield in hard mode. + # First, let's make our sword condition. + can_defeat_basic_enemy: Rule = Has("Sword") + # Next, we'll check whether hard mode has been chosen in the player options. if world.options.hard_mode: - # If you have multiple conditions, you can obviously chain them via "or" or "and". - # However, there are also the nice helper functions "state.has_any" and "state.has_all". - set_rule( - right_room_enemy, - lambda state: ( - state.has("Sword", world.player) and state.has_any(("Shield", "Health Upgrade"), world.player) - ), - ) - else: - set_rule(right_room_enemy, lambda state: state.has("Sword", world.player)) - - # Another way to chain multiple conditions is via the add_rule function. - # This makes the access rules a bit slower though, so it should only be used if your structure justifies it. - # In our case, it's pretty useful because hard mode and easy mode have different requirements. - final_boss = world.get_location("Final Boss Defeated") + # We'll make the condition for "Has a Shield or a Health Upgrade". + # We can chain two "Has" conditions together with the | operator to make "Has Shield or has Health Upgrade". + can_withstand_a_hit = Has("Shield") | Has("Health Upgrade") - # For the "known" requirements, it's still better to chain them using a normal "and" condition. - add_rule(final_boss, lambda state: state.has_all(("Sword", "Shield"), world.player)) + # Now, we chain this rule to our Sword rule. + # Since we want both conditions to be true, in this case, we have to chain them in an "and" way. + # For this, we can use the & operator. + can_defeat_basic_enemy = can_defeat_basic_enemy & can_withstand_a_hit - if world.options.hard_mode: - # You can check for multiple copies of an item by using the optional count parameter of state.has(). - add_rule(final_boss, lambda state: state.has("Health Upgrade", world.player, 2)) + # Finally, we set our rule onto the Right Room Eney Drop location. + right_room_enemy = world.get_location("Right Room Enemy Drop") + world.set_rule(right_room_enemy, can_defeat_basic_enemy) + + # For the final boss, we also need to chain multiple conditions. + # First of all, you always need a Sword and a Shield. + # So far, we used the | and & operators to chain "Has" rules. + # Instead, we can also use HasAny for an or-chain of items, or HasAll for an and-chain of items. + has_sword_and_shield: Rule = HasAll("Sword", "Shield") + + # In hard mode, the player also needs both Health Upgrades to survive long enough to defeat the boss. + # For this, we can use the optional "count" parameter for "Has". + has_both_health_upgrades = Has("Health Upgrade", count=2) + + # Previously, we used an "if world.options.hard_mode" condition to check if we should apply the extra requirement. + # However, if you're comfortable with boolean logic, there is another way. + # OptionFilter is a rule component which isn't a "Rule" on its own, but when used in a boolean expression with + # rules, it acts like True if the option has the specified value, and acts like False otherwise. + hard_mode_is_off = OptionFilter(HardMode, False) + + # So with this option-checking rule component in hand, we can write our boss condition like this: + can_defeat_final_boss = has_sword_and_shield & (hard_mode_is_off | has_both_health_upgrades) + # If you're not as comfortable with boolean logic, it might be somewhat confusing why this is correct. + # There is nothing wrong with using "if" conditions to check for options, if you find that easier to understand. + + # Finally, we apply the rule to our "Final Boss Defeated" event location. + final_boss = world.get_location("Final Boss Defeated") + world.set_rule(final_boss, can_defeat_final_boss) def set_completion_condition(world: APQuestWorld) -> None: # Finally, we need to set a completion condition for our world, defining what the player needs to win the game. + # For this, we can use world.set_completion_rule. # You can just set a completion condition directly like any other condition, referencing items the player receives: - world.multiworld.completion_condition[world.player] = lambda state: state.has_all(("Sword", "Shield"), world.player) + world.set_completion_rule(HasAll("Sword", "Shield")) # In our case, we went for the Victory event design pattern (see create_events() in locations.py). # So lets undo what we just did, and instead set the completion condition to: - world.multiworld.completion_condition[world.player] = lambda state: state.has("Victory", world.player) + world.set_completion_rule(Has("Victory")) + + +# One final comment about rules: +# If your world exclusively uses Rule Builder rules (like APQuest), it's worth trying CachedRuleBuilderWorld. +# CachedRuleBuilderWorld is a subclass of World that has a bunch of caching magic to make rules faster. +# Just have your world class subclass CachedRuleBuilderWorld instead of World: +# class APQuestWorld(CachedRuleBuilderWorld): ... +# This may speed up your world, or it may make it slower. +# The exact factors are complex and not well understood, but there is no harm in trying it. +# Generate a few seeds and see if there is a noticeable difference! +# If you're wondering, author has checked: APQuest is too simple to see any benefits, so we'll stick with "World". From 0a742b6c984fc7ce03bb52b7a4a4b86b60831403 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sat, 18 Apr 2026 12:49:19 +0100 Subject: [PATCH 05/66] Options: Add more Option unit tests, add generic Option.__eq__, make cull_zeroes available to all OptionCounters #5905 --- Options.py | 39 +++++++++--- test/options/test_option_classes.py | 98 ++++++++++++++++++++++++++++- 2 files changed, 128 insertions(+), 9 deletions(-) diff --git a/Options.py b/Options.py index 57119ff66c1c..a84d5e280e75 100644 --- a/Options.py +++ b/Options.py @@ -212,6 +212,13 @@ def get_option_name(cls, value: T) -> str: else: return cls.name_lookup[value] + def __eq__(self, other: typing.Any) -> bool: + if isinstance(other, self.__class__): + return self.value == other.value + if isinstance(other, Option): + raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}") + return self.value == other + def __int__(self) -> T: return self.value @@ -930,13 +937,34 @@ def __contains__(self, item) -> bool: class OptionCounter(OptionDict): min: int | None = None max: int | None = None + cull_zeroes: bool = False def __init__(self, value: dict[str, int]) -> None: - super(OptionCounter, self).__init__(collections.Counter(value)) + cleaned_dict = {} + + invalid_value_errors = [] + for key, value in value.items(): + if not isinstance(value, (int, float)) or int(value) != value: + invalid_value_errors += [f"Invalid value {value} for key {key}, must be an integer."] + continue + + if self.cull_zeroes and value == 0: + continue + + cleaned_dict[key] = int(value) + + if invalid_value_errors: + type_errors = [f"For option {self.__class__.__name__}:"] + invalid_value_errors + raise TypeError("\n".join(invalid_value_errors)) + + super(OptionCounter, self).__init__(collections.Counter(cleaned_dict)) def verify(self, world: type[World], player_name: str, plando_options: PlandoOptions) -> None: super(OptionCounter, self).verify(world, player_name, plando_options) + self.verify_values() + + def verify_values(self): range_errors = [] if self.max is not None: @@ -959,13 +987,8 @@ def verify(self, world: type[World], player_name: str, plando_options: PlandoOpt class ItemDict(OptionCounter): verify_item_name = True - min = 0 - - def __init__(self, value: dict[str, int]) -> None: - # Backwards compatibility: Cull 0s to make "in" checks behave the same as when this wasn't a OptionCounter - value = {item_name: amount for item_name, amount in value.items() if amount != 0} - - super(ItemDict, self).__init__(value) + # Backwards compatibility: Cull 0s to make "in" checks behave the same as when this wasn't a OptionCounter + cull_zeroes = True class OptionList(Option[typing.List[typing.Any]], VerifyKeys): diff --git a/test/options/test_option_classes.py b/test/options/test_option_classes.py index ca90db88708c..8ca1751fa3e4 100644 --- a/test/options/test_option_classes.py +++ b/test/options/test_option_classes.py @@ -1,6 +1,8 @@ import unittest -from Options import Choice, DefaultOnToggle, Toggle +from collections import Counter + +from Options import Choice, DefaultOnToggle, Toggle, OptionDict, OptionError, OptionSet, OptionList, OptionCounter class TestNumericOptions(unittest.TestCase): @@ -74,3 +76,97 @@ class TestDefaultOnToggle(DefaultOnToggle): self.assertTrue(toggle_string) self.assertTrue(toggle_int) self.assertTrue(toggle_alias) + + +class TestContainerOptions(unittest.TestCase): + def test_option_dict(self): + class TestOptionDict(OptionDict): + valid_keys = frozenset({"A", "B", "C"}) + + unknown_key_init_dict = {"D": "Foo"} + test_option_dict = TestOptionDict(unknown_key_init_dict) + self.assertRaises(OptionError, test_option_dict.verify_keys) + + init_dict = {"A": "foo", "B": "bar"} + test_option_dict = TestOptionDict(init_dict) + + self.assertEqual(test_option_dict, init_dict) # Implicit value comparison + self.assertEqual(test_option_dict["A"], "foo") + self.assertIn("B", test_option_dict) + self.assertNotIn("C", test_option_dict) + self.assertRaises(KeyError, lambda: test_option_dict["C"]) + + def test_option_set(self): + class TestOptionSet(OptionSet): + valid_keys = frozenset({"A", "B", "C"}) + + unknown_key_init_set = {"D"} + test_option_set = TestOptionSet(unknown_key_init_set) + self.assertRaises(OptionError, test_option_set.verify_keys) + + init_set = {"A", "B"} + test_option_set = TestOptionSet(init_set) + + self.assertEqual(test_option_set, init_set) # Implicit value comparison + self.assertIn("B", test_option_set) + self.assertNotIn("C", test_option_set) + + def test_option_list(self): + class TestOptionList(OptionList): + valid_keys = frozenset({"A", "B", "C"}) + + unknown_key_init_list = ["D"] + test_option_list = TestOptionList(unknown_key_init_list) + self.assertRaises(OptionError, test_option_list.verify_keys) + + init_list = ["A", "B"] + test_option_list = TestOptionList(init_list) + + self.assertEqual(test_option_list, init_list) + self.assertIn("B", test_option_list) + self.assertNotIn("C", test_option_list) + + + def test_option_counter(self): + class TestOptionCounter(OptionCounter): + valid_keys = frozenset({"A", "B", "C"}) + + max = 10 + min = 0 + + unknown_key_init_dict = {"D": 5} + test_option_counter = TestOptionCounter(unknown_key_init_dict) + self.assertRaises(OptionError, test_option_counter.verify_keys) + + wrong_value_type_init_dict = {"A": "B"} + self.assertRaises(TypeError, TestOptionCounter, wrong_value_type_init_dict) + + violates_max_init_dict = {"A": 5, "B": 11} + test_option_counter = TestOptionCounter(violates_max_init_dict) + self.assertRaises(OptionError, test_option_counter.verify_values) + + violates_min_init_dict = {"A": -1, "B": 5} + test_option_counter = TestOptionCounter(violates_min_init_dict) + self.assertRaises(OptionError, test_option_counter.verify_values) + + init_dict = {"A": 0, "B": 10} + test_option_counter = TestOptionCounter(init_dict) + self.assertEqual(test_option_counter, Counter(init_dict)) + self.assertIn("A", test_option_counter) + self.assertNotIn("C", test_option_counter) + self.assertEqual(test_option_counter["A"], 0) + self.assertEqual(test_option_counter["B"], 10) + self.assertEqual(test_option_counter["C"], 0) + + def test_culling_option_counter(self): + class TestCullingCounter(OptionCounter): + valid_keys = frozenset({"A", "B", "C"}) + cull_zeroes = True + + init_dict = {"A": 0, "B": 10} + test_option_counter = TestCullingCounter(init_dict) + self.assertNotIn("A", test_option_counter) + self.assertIn("B", test_option_counter) + self.assertNotIn("C", test_option_counter) + self.assertEqual(test_option_counter["A"], 0) # It's still a Counter! cull_zeroes is about "in" checks. + self.assertEqual(test_option_counter, Counter({"B": 10})) From a6c134710262902562e25fd47b470f9848c9725b Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sat, 18 Apr 2026 12:52:38 +0100 Subject: [PATCH 06/66] The Witness: Make "Early Good Item" an OptionSet allowing Symbols, Doors and Obelisk Keys (#3804) * New solution to that plando issue * better * Another warning * better comment * Best of both worlds I guess? * oops * Smarter code reuse * better comment * oop * lint * mypy * player_name * add unit test * oh * Rebrand time baby * This fits on one line * oop * Update worlds/witness/__init__.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * mypy * Reorganize some doors according to medic's suggestions * This should make it much faster (Thanks Medic) * Town Doors works because of Church being a check always, Church Entry feels really bad tho * Only add Desert Entry if there are no control panels stopping you * No overlap here, so why is it a set * Idk everything's kinda good without symbol shuffle * Just make sure, I guess * This makes way more sense doesn't it * Oh, this is probably important * oop * loc * oops 2 * ruff * that was already in there * Change the door picking a bit further * some renaming * slight wording change * Fix * Move it all to a new file * ruff * mypy * . * Make sure we're only adding as many tutorial checks as necessary * ruff * These checks aren't necessary, as the final list gets culled to only existing items anyway. It saves CPU cycles, but this is nicer for future compatibility * Special handling for caves shortcuts * 120 chars * Update worlds/witness/place_early_item.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Clean up Windmill & Theater cases * Make early_symbol_item removed instead * Add early_good_item to presets * replace double None checks with casts * That doesn't exist anymore * Mypy thing * Update the doors again a bit * Pycharm pls * ruff * forgot one * oop * Is it finally right? * Update options.py * Fix with new Panel Keys * Hopefully fix crash when one of the types runs out when the others haven't yet * oops * Medic suggestion * unused import * Update place_early_item.py * Update __init__.py * Update __init__.py * Update options.py * Add possible types to option desc * Make that include all Tutorial (Inside) checks * Update __init__.py * Update test_early_good_item.py * Update test_early_good_item.py * Slight cleanup * fix no tutorial locations being picked up by tutorial location size check --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/witness/__init__.py | 83 ++++++---- worlds/witness/locations.py | 2 +- worlds/witness/options.py | 19 ++- worlds/witness/place_early_item.py | 151 ++++++++++++++++++ worlds/witness/player_items.py | 134 +++++++++++++--- worlds/witness/presets.py | 6 +- .../test/test_disable_non_randomized.py | 2 +- worlds/witness/test/test_early_good_item.py | 109 +++++++++++++ worlds/witness/test/test_lasers.py | 11 +- worlds/witness/test/test_symbol_shuffle.py | 4 +- 10 files changed, 448 insertions(+), 73 deletions(-) create mode 100644 worlds/witness/place_early_item.py create mode 100644 worlds/witness/test/test_early_good_item.py diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index 3d80fd245875..66229600a5db 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -2,10 +2,10 @@ Archipelago init file for The Witness """ import dataclasses -from logging import error, warning +from logging import error, info, warning from typing import Any, Dict, List, Optional, cast -from BaseClasses import CollectionState, Entrance, Location, LocationProgressType, Region, Tutorial +from BaseClasses import CollectionState, Entrance, Item, Location, LocationProgressType, Region, Tutorial from Options import OptionError, PerGameCommonOptions, Toggle from worlds.AutoWorld import WebWorld, World @@ -18,6 +18,7 @@ from .hints import CompactHintData, create_all_hints, make_compact_hint_data, make_laser_hints from .locations import WitnessPlayerLocations from .options import TheWitnessOptions, witness_option_groups +from .place_early_item import place_early_items from .player_items import WitnessItem, WitnessPlayerItems from .player_logic import WitnessPlayerLogic from .presets import witness_option_presets @@ -94,6 +95,7 @@ class WitnessWorld(World): items_placed_early: List[str] own_itempool: List[WitnessItem] + reachable_early_locations: List[str] panel_hunt_required_count: int def _get_slot_data(self) -> Dict[str, Any]: @@ -218,25 +220,6 @@ def create_regions(self) -> None: self.own_itempool.append(dog_puzzle_skip) self.items_placed_early.append("Puzzle Skip") - if self.options.early_symbol_item: - # Pick an early item to place on the tutorial gate. - early_items = [ - item for item in self.player_items.get_early_items() if item in self.player_items.get_mandatory_items() - ] - if early_items: - random_early_item = self.random.choice(early_items) - mode = self.options.puzzle_randomization - if mode == "sigma_expert" or mode == "umbra_variety" or self.options.victory_condition == "panel_hunt": - # In Expert and Variety, only tag the item as early, rather than forcing it onto the gate. - # Same with panel hunt, since the Tutorial Gate Open panel is used for something else - self.multiworld.local_early_items[self.player][random_early_item] = 1 - else: - # Force the item onto the tutorial gate check and remove it from our random pool. - gate_item = self.create_item(random_early_item) - self.get_location("Tutorial Gate Open").place_locked_item(gate_item) - self.own_itempool.append(gate_item) - self.items_placed_early.append(random_early_item) - # There are some really restrictive options in The Witness. # They are rarely played, but when they are, we add some extra sphere 1 locations. # This is done both to prevent generation failures, but also to make the early game less linear. @@ -245,20 +228,39 @@ def create_regions(self) -> None: state = CollectionState(self.multiworld) state.sweep_for_advancements(locations=event_locations) - num_early_locs = sum( - 1 for loc in self.multiworld.get_reachable_locations(state, self.player) - if loc.address and not loc.item + # Adjust the needed size for sphere 1 based on how restrictive the options are in terms of items + + early_locations = [ + location for location in self.multiworld.get_reachable_locations(state, self.player) + if not location.is_event and not location.item + ] + self.reachable_early_locations = [location.name for location in early_locations] + + num_reachable_tutorial_locations = sum( + static_witness_logic.ALL_REGIONS_BY_NAME[ + cast(Region, location.parent_region).name + ].area.name == "Tutorial (Inside)" + for location in early_locations ) # Adjust the needed size for sphere 1 based on how restrictive the options are in terms of items - needed_size = 2 - needed_size += self.options.puzzle_randomization == "sigma_expert" - needed_size += self.options.shuffle_symbols - needed_size += self.options.shuffle_doors != "off" + needed_size_overall = 2 + needed_size_overall += self.options.puzzle_randomization == "sigma_expert" + needed_size_overall += self.options.shuffle_symbols + needed_size_overall += self.options.shuffle_doors != "off" + + needed_size_to_hold_tutorial_items = len( + self.player_items.get_early_items(set(self.player_items.get_mandatory_items())) + ) # Then, add checks in order until the required amount of sphere 1 checks is met. + extra_tutorial_checks = [ + ("Tutorial First Hallway Room", "Tutorial First Hallway Bend"), + ("Tutorial First Hallway", "Tutorial First Hallway Straight") + ] + extra_checks = [ ("Tutorial First Hallway Room", "Tutorial First Hallway Bend"), ("Tutorial First Hallway", "Tutorial First Hallway Straight"), @@ -266,13 +268,30 @@ def create_regions(self) -> None: ("Desert Outside", "Desert Surface 2"), ] - for i in range(num_early_locs, needed_size): + for _ in range(num_reachable_tutorial_locations, needed_size_to_hold_tutorial_items): + if not extra_tutorial_checks: + break + + region, loc = extra_tutorial_checks.pop(0) + extra_checks.pop(0) + self.player_locations.add_location_late(loc) + self.get_region(region).add_locations({loc: self.location_name_to_id[loc]}, WitnessLocation) + self.reachable_early_locations.append(loc) + + player = self.player_name + + info( + f"""Location "{loc}" had to be added to {player}'s world to hold the requested early good items.""" + ) + + for _ in range(len(self.reachable_early_locations), needed_size_overall): if not extra_checks: break region, loc = extra_checks.pop(0) self.player_locations.add_location_late(loc) - self.get_region(region).add_locations({loc: self.location_name_to_id[loc]}) + self.get_region(region).add_locations({loc: self.location_name_to_id[loc]}, WitnessLocation) + self.reachable_early_locations.append(loc) warning( f"""Location "{loc}" had to be added to {self.player_name}'s world @@ -341,6 +360,10 @@ def create_items(self) -> None: self.own_itempool += new_items self.multiworld.itempool += new_items + def fill_hook(self, progitempool: List[Item], _: List[Item], _2: List[Item], + fill_locations: List[Location]) -> None: + place_early_items(self, progitempool, fill_locations) + def fill_slot_data(self) -> Dict[str, Any]: already_hinted_locations = set() diff --git a/worlds/witness/locations.py b/worlds/witness/locations.py index e7f6f94d659e..f4416b72ce12 100644 --- a/worlds/witness/locations.py +++ b/worlds/witness/locations.py @@ -72,5 +72,5 @@ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic) -> N def add_location_late(self, entity_name: str) -> None: entity_hex = static_witness_logic.ENTITIES_BY_NAME[entity_name]["entity_hex"] - self.CHECK_LOCATION_TABLE[entity_hex] = static_witness_locations.get_id(entity_hex) + self.CHECK_LOCATION_TABLE[entity_name] = static_witness_locations.get_id(entity_hex) self.CHECK_PANELHEX_TO_ID[entity_hex] = static_witness_locations.get_id(entity_hex) diff --git a/worlds/witness/options.py b/worlds/witness/options.py index 6a64fdb3d877..546f2a5ae246 100644 --- a/worlds/witness/options.py +++ b/worlds/witness/options.py @@ -13,6 +13,7 @@ OptionSet, PerGameCommonOptions, Range, + Removed, Toggle, Visibility, ) @@ -50,12 +51,19 @@ class EarlyCaves(Choice): alias_on = 2 -class EarlySymbolItem(DefaultOnToggle): +class EarlyGoodItems(OptionSet): """ - Put a random helpful symbol item on an early check, specifically Tutorial Gate Open if it is available early. + Put one random helpful item of each of the chosen types on an early check, specifically a sphere 1 Tutorial location. + If a type is chosen, but no items of that type exist in the itempool, it is skipped. + The possible types are: "Symbol", "Door / Door Panel", "Obelisk Key". + + If there aren't enough sphere 1 Tutorial locations, Tutorial First Hallway Straight and Tutorial First Hallway Bend may be added as locations. + If there still aren't enough sphere 1 Tutorial locations, a random local sphere 1 location is picked. + If no local sphere 1 locations are available, there are no further attempts to place the item. """ - visibility = Visibility.none + valid_keys = {"Symbol", "Door / Door Panel", "Obelisk Key"} + default = frozenset({"Symbol"}) class ShuffleSymbols(DefaultOnToggle): @@ -550,7 +558,7 @@ class TheWitnessOptions(PerGameCommonOptions): panel_hunt_discourage_same_area_factor: PanelHuntDiscourageSameAreaFactor panel_hunt_plando: PanelHuntPlando early_caves: EarlyCaves - early_symbol_item: EarlySymbolItem + early_good_items: EarlyGoodItems elevators_come_to_you: ElevatorsComeToYou trap_percentage: TrapPercentage trap_weights: TrapWeights @@ -565,6 +573,8 @@ class TheWitnessOptions(PerGameCommonOptions): shuffle_dog: ShuffleDog easter_egg_hunt: EasterEggHunt + early_symbol_item: Removed + witness_option_groups = [ OptionGroup("Puzzles & Goal", [ @@ -611,6 +621,7 @@ class TheWitnessOptions(PerGameCommonOptions): LaserHints ]), OptionGroup("Misc", [ + EarlyGoodItems, EarlyCaves, ElevatorsComeToYou, DeathLink, diff --git a/worlds/witness/place_early_item.py b/worlds/witness/place_early_item.py new file mode 100644 index 000000000000..0490dbebf194 --- /dev/null +++ b/worlds/witness/place_early_item.py @@ -0,0 +1,151 @@ +from logging import debug, error +from typing import TYPE_CHECKING, Dict, List, Set, Tuple + +from BaseClasses import CollectionState, Item, Location, LocationProgressType + +from .data import static_logic as static_witness_logic +from .data.utils import cast_not_none + +if TYPE_CHECKING: + from . import WitnessWorld + + +def get_available_early_locations(world: "WitnessWorld") -> List[Location]: + # Pick an early item to put on Tutorial Gate Open. + # Done after plando to avoid conflicting with it. + # Done in fill_hook because multiworld itempool manipulation is not allowed in pre_fill. + + # Prioritize Tutorial locations in a specific order + tutorial_checks_in_order = [ + "Tutorial Gate Open", + "Tutorial Back Left", + "Tutorial Back Right", + "Tutorial Front Left", + "Tutorial First Hallway Straight", + "Tutorial First Hallway Bend", + "Tutorial Patio Floor", + "Tutorial First Hallway EP", + "Tutorial Cloud EP", + "Tutorial Patio Flowers EP", + ] + available_locations = [ + world.get_location(location_name) for location_name in tutorial_checks_in_order + if location_name in world.reachable_early_locations # May not actually be sphere 1 (e.g. Obelisk Keys for EPs) + ] + + # Then, add the rest of sphere 1 in "game order" + available_locations += sorted( + ( + world.get_location(location_name) for location_name in world.reachable_early_locations + if location_name not in tutorial_checks_in_order + ), + key=lambda location_object: static_witness_logic.ENTITIES_BY_NAME[location_object.name]["order"] + ) + + return [ + location for location in available_locations + if not location.item and location.progress_type != LocationProgressType.EXCLUDED + ] + + +def get_eligible_items_by_type_in_random_order(world: "WitnessWorld") -> Dict[str, List[str]]: + eligible_early_items_by_type = world.player_items.get_early_items({item.name for item in world.own_itempool}) + + for item_list in eligible_early_items_by_type.values(): + world.random.shuffle(item_list) + + return eligible_early_items_by_type + + +def grab_own_items_from_itempool(world: "WitnessWorld", itempool: List[Item], ids_to_find: Set[int]) -> List[Item]: + found_early_items = [] + + def keep_or_take_out(item: Item) -> bool: + if item.code not in ids_to_find: + return True # Keep + ids_to_find.remove(item.code) + found_early_items.append(item) + return False # Take out + + local_player = world.player + itempool[:] = [item for item in itempool if item.player != local_player or keep_or_take_out(item)] + + return found_early_items + + +def place_items_onto_locations(world: "WitnessWorld", items: List[Item], + locations: List[Location]) -> Tuple[List[Item], List[Item]]: + fake_state = CollectionState(world.multiworld) + + placed_items = [] + unplaced_items = [] + + for item in items: + location = next( + (location for location in locations if location.can_fill(fake_state, item, check_access=False)), + None, + ) + if location is not None: + location.place_locked_item(item) + placed_items.append(item) + locations.remove(location) + else: + unplaced_items.append(item) + + return placed_items, unplaced_items + + +def place_early_items(world: "WitnessWorld", prog_itempool: List[Item], fill_locations: List[Location]) -> None: + if not world.options.early_good_items.value: + return + + # Get a list of good early locations in a determinstic order + eligible_early_locations = get_available_early_locations(world) + # Get a list of good early items of each desired item type + eligible_early_items_by_type = get_eligible_items_by_type_in_random_order(world) + + if not eligible_early_items_by_type: + return + + while any(eligible_early_items_by_type.values()) and eligible_early_locations: + # Get one item of each type + next_findable_items_dict = { + item_list.pop(): item_type + for item_type, item_list in eligible_early_items_by_type.items() + if item_list + } + + # Get their IDs as a set + next_findable_item_ids = {world.item_name_to_id[item_name] for item_name in next_findable_items_dict} + + # Grab items from itempool + found_early_items = grab_own_items_from_itempool(world, prog_itempool, next_findable_item_ids) + + # Bring found items back into Symbol -> Door -> Obelisk Key order + # The intent is that the Symbol is always on Tutorial Gate Open / generally that the order is predictable + correct_order = {item_name: i for i, item_name in enumerate(next_findable_items_dict)} + found_early_items.sort(key=lambda item: correct_order[item.name]) + + # Place found early items on eligible early locations. + placed_items, unplaced_items = place_items_onto_locations(world, found_early_items, eligible_early_locations) + + for item in placed_items: + debug(f"Placed early good item {item} on early location {item.location}.") + # Item type is satisfied + del eligible_early_items_by_type[next_findable_items_dict[item.name]] + fill_locations.remove(cast_not_none(item.location)) + for item in unplaced_items: + debug(f"Could not find a suitable placement for item {item}.") + + unfilled_types = list(eligible_early_items_by_type) + if unfilled_types: + if not eligible_early_locations: + error( + f'Could not find a suitable location for "early good items" of types {unfilled_types} in ' + f"{world.player_name}'s world. They are excluded or already contain plandoed items.\n" + ) + else: + error( + f"Could not find any \"early good item\" of types {unfilled_types} in {world.player_name}'s world, " + "they were all plandoed elsewhere." + ) diff --git a/worlds/witness/player_items.py b/worlds/witness/player_items.py index d13ebcafdcaf..1be228530471 100644 --- a/worlds/witness/player_items.py +++ b/worlds/witness/player_items.py @@ -198,36 +198,118 @@ def get_filler_items(self, quantity: int) -> Dict[str, int]: return output - def get_early_items(self) -> List[str]: + def get_early_items(self, existing_items: Set[str]) -> Dict[str, List[str]]: """ Returns items that are ideal for placing on extremely early checks, like the tutorial gate. """ - output: Set[str] = set() - if self._world.options.shuffle_symbols: - discards_on = self._world.options.shuffle_discarded_panels - mode = self._world.options.puzzle_randomization.current_key - - output = static_witness_items.ALWAYS_GOOD_SYMBOL_ITEMS | static_witness_items.MODE_SPECIFIC_GOOD_ITEMS[mode] - if discards_on: - output |= static_witness_items.MODE_SPECIFIC_GOOD_DISCARD_ITEMS[mode] - - # Remove items that are mentioned in any plando options. (Hopefully, in the future, plando will get resolved - # before create_items so that we'll be able to check placed items instead of just removing all items mentioned - # regardless of whether or not they actually wind up being manually placed. - for plando_setting in self._world.options.plando_items: - if plando_setting.from_pool: - if isinstance(plando_setting.items, dict): - output -= {item for item, weight in plando_setting.items.items() if weight} + output: Dict[str, List[str]] = {} + + if "Symbol" in self._world.options.early_good_items.value: + good_symbols = ["Dots", "Black/White Squares", "Symmetry", "Shapers", "Stars"] + + if self._world.options.shuffle_discarded_panels: + if self._world.options.puzzle_randomization == "sigma_expert": + good_symbols.append("Arrows") else: - # Assume this is some other kind of iterable. - for inner_item in plando_setting.items: - if isinstance(inner_item, str): - output -= {inner_item} - elif isinstance(inner_item, dict): - output -= {item for item, weight in inner_item.items() if weight} - - # Sort the output for consistency across versions if the implementation changes but the logic does not. - return sorted(output) + good_symbols.append("Triangles") + + # Replace progressive items with their parents. + good_symbols = [ + static_witness_logic.get_parent_progressive_item(item) for item in good_symbols + ] + + output["Symbol"] = [symbol for symbol in good_symbols if symbol in existing_items] + + if "Door / Door Panel" in self._world.options.early_good_items.value: + good_doors = [ + "Desert Doors & Elevator", "Keep Hedge Maze Doors", "Keep Pressure Plates Doors", + "Shadows Lower Doors", "Tunnels Doors", "Quarry Stoneworks Doors", + + "Keep Tower Shortcut (Door)", "Shadows Timed Door", "Tunnels Town Shortcut (Door)", + "Quarry Stoneworks Roof Exit (Door)", + + "Desert Panels", "Keep Hedge Maze Panels", + + "Shadows Door Timer (Panel)", + ] + + # While Caves Shortcuts don't unlock anything in symbol shuffle, you'd still rather have them early. + # But, we need to do some special handling for them. + + if self._world.options.shuffle_doors in ("doors", "mixed"): + # Caves Shortcuts might exist in vanilla/panel doors because of "early caves: add to pool". + # But if the player wanted them early, they would have chosen "early caves: starting inventory". + # This is why we make sure these are "natural" Caves Shortcuts. + good_doors.append("Caves Shortcuts") + + # These two doors are logically equivalent, so we choose a random one as to not give them twice the chance. + good_doors.append( + self._world.random.choice(["Caves Mountain Shortcut (Door)", "Caves Swamp Shortcut (Door)"]) + ) + + if self._world.options.shuffle_vault_boxes and not self._world.options.disable_non_randomized_puzzles: + good_doors.append("Windmill & Theater Doors") + if not self._world.options.shuffle_symbols: + good_doors += [ + "Windmill & Theater Panels", + + "Windmill & Theater Control Panels", + ] + + if self._world.options.shuffle_doors == "doors": # It's not as good in mixed doors because of Light Control + good_doors.append("Desert Light Room Entry (Door)") + + if not self._world.options.shuffle_symbols: + good_doors += [ + "Bunker Doors", "Swamp Doors", "Glass Factory Doors", "Town Doors", + + "Bunker Entry (Door)", "Glass Factory Entry (Door)", "Symmetry Island Lower (Door)", + + "Bunker Panels", "Swamp Panels", "Quarry Outside Panels", "Glass Factory Panels" + + "Glass Factory Entry (Panel)", + ] + + existing_doors = [door for door in good_doors if door in existing_items] + + # On some options combinations with doors, there just aren't a lot of doors that unlock much early. + # In this case, we add some doors that aren't great, but are at least guaranteed to unlock 1 location. + + fallback_doors = [ + "Keep Shadows Shortcut (Door)", # Always Keep Shadows Shortcut Panel + "Keep Shortcuts", # -"- + "Keep Pressure Plates Doors", # -"- + "Keep Hedge Maze 1 (Panel)", # Always Hedge 1 + "Town Maze Stairs (Panel)", # Always Maze Panel + "Shadows Laser Room Doors", # Always Shadows Laser Panel + "Swamp Laser Shortcut (Door)", # Always Swamp Laser + "Town Maze Panels", # Always Town Maze Panel + "Town Doors", # Always Town Church Lattice + "Town Church Entry (Door)", # -"- + "Town Tower Doors", # Always Town Laser + ] + self._world.random.shuffle(fallback_doors) + + while len(existing_doors) < 4 and fallback_doors: + fallback_door = fallback_doors.pop() + if fallback_door in existing_items and fallback_door not in existing_doors: + existing_doors.append(fallback_door) + + output["Door"] = existing_doors + + if "Obelisk Key" in self._world.options.early_good_items.value: + obelisk_keys = [ + "Desert Obelisk Key", "Town Obelisk Key", "Quarry Obelisk Key", + "Treehouse Obelisk Key", "Monastery Obelisk Key", "Mountainside Obelisk Key" + ] + output["Obelisk Key"] = [key for key in obelisk_keys if key in existing_items] + + assert all(item in self._world.item_names for sublist in output.values() for item in sublist), ( + [item for sublist in output.values() for item in sublist if item not in self._world.item_names] + ) + + # Cull empty lists + return {item_type: item_list for item_type, item_list in output.items() if item_list} def get_door_item_ids_in_pool(self) -> List[int]: """ diff --git a/worlds/witness/presets.py b/worlds/witness/presets.py index 81dd28d68d09..934f55b6853b 100644 --- a/worlds/witness/presets.py +++ b/worlds/witness/presets.py @@ -35,7 +35,7 @@ "challenge_lasers": 11, "early_caves": EarlyCaves.option_off, - + "early_good_items": {"Door / Door Panel"}, "elevators_come_to_you": ElevatorsComeToYou.default, "trap_percentage": TrapPercentage.default, @@ -76,7 +76,7 @@ "challenge_lasers": 9, "early_caves": EarlyCaves.option_off, - + "early_good_items": {"Symbol", "Door / Door Panel"}, # Not Obelisk Key bc I want EPs to open slowly in this one "elevators_come_to_you": ElevatorsComeToYou.default, "trap_percentage": TrapPercentage.default, @@ -117,7 +117,7 @@ "challenge_lasers": 9, "early_caves": EarlyCaves.option_off, - + "early_good_items": {"Symbol", "Door / Door Panel", "Obelisk Key"}, "elevators_come_to_you": ElevatorsComeToYou.valid_keys, "trap_percentage": TrapPercentage.default, diff --git a/worlds/witness/test/test_disable_non_randomized.py b/worlds/witness/test/test_disable_non_randomized.py index 00071ec5f6f0..e999fb452d66 100644 --- a/worlds/witness/test/test_disable_non_randomized.py +++ b/worlds/witness/test/test_disable_non_randomized.py @@ -8,7 +8,7 @@ class TestDisableNonRandomized(WitnessTestBase): options = { "disable_non_randomized_puzzles": True, "shuffle_doors": "panels", - "early_symbol_item": False, + "early_good_items": {}, } def test_locations_got_disabled_and_alternate_activation_triggers_work(self) -> None: diff --git a/worlds/witness/test/test_early_good_item.py b/worlds/witness/test/test_early_good_item.py new file mode 100644 index 000000000000..1641b0f1b005 --- /dev/null +++ b/worlds/witness/test/test_early_good_item.py @@ -0,0 +1,109 @@ +from BaseClasses import ItemClassification, LocationProgressType +from Fill import distribute_items_restrictive + +from ..data.utils import cast_not_none +from ..test.bases import WitnessTestBase + + +class TestEarlySymbolItemFalse(WitnessTestBase): + options = { + "early_good_items": {}, + + "shuffle_symbols": True, + "shuffle_doors": "off", + "shuffle_boat": False, + "shuffle_lasers": False, + "obelisk_keys": False, + } + + def setUp(self) -> None: + if self.auto_construct: + self.world_setup(seed=1) # Magic seed to prevent false positive + + def test_early_good_item(self) -> None: + distribute_items_restrictive(self.multiworld) + + gate_open = self.multiworld.get_location("Tutorial Gate Open", 1) + + self.assertFalse( + cast_not_none(gate_open.item).classification & ItemClassification.progression, + "Early Good Item was off, yet a Symbol item ended up on Tutorial Gate Open.", + ) + + +class TestEarlySymbolItemTrue(WitnessTestBase): + options = { + "early_good_items": {"Symbol", "Door / Door Panel", "Obelisk Key"}, + + "shuffle_symbols": True, + "shuffle_doors": "panels", + "shuffle_EPs": "individual", + "obelisk_keys": True, + } + + def setUp(self) -> None: + super().setUp() + + def test_early_good_item(self) -> None: + """ + The items should be in the order: + Symbol item on Tutorial Gate Open + Door item on Tutorial Back Left + Obelisk Key on Tutorial Back Right + """ + + distribute_items_restrictive(self.multiworld) + + gate_open = self.multiworld.get_location("Tutorial Gate Open", 1) + + self.assertTrue(gate_open.item is not None, "Somehow, no item got placed on Tutorial Gate Open.") + + self.assertTrue( + cast_not_none(gate_open.item).name in self.world.item_name_groups["Symbols"], + "Early Good Item was on, yet no Symbol item ended up on Tutorial Gate Open.", + ) + + back_left = self.multiworld.get_location("Tutorial Back Left", 1) + + self.assertTrue(back_left.item is not None, "Somehow, no item got placed on Tutorial Back Left.") + + doors_and_panel_keys = self.world.item_name_groups["Doors"] | self.world.item_name_groups["Panel Keys"] + self.assertTrue( + cast_not_none(back_left.item).name in doors_and_panel_keys, + "Early Good Item was on, yet no Door item ended up on Tutorial Back Left.", + ) + + back_right = self.multiworld.get_location("Tutorial Back Right", 1) + + self.assertTrue(back_right.item is not None, "Somehow, no item got placed on Tutorial Back Right.") + + self.assertTrue( + cast_not_none(back_right.item).name in self.world.item_name_groups["Obelisk Keys"], + "Early Good Item was on, yet no Obelisk Key item ended up on Tutorial Back Right.", + ) + + +class TestEarlySymbolItemTrueButExcluded(WitnessTestBase): + options = { + "shuffle_symbols": True, + "shuffle_doors": "off", + "shuffle_boat": False, + "shuffle_lasers": False, + "obelisk_keys": False, + } + + def setUp(self) -> None: + super().setUp() + self.multiworld.get_location("Tutorial Gate Open", 1).progress_type = LocationProgressType.EXCLUDED + + def test_early_good_item(self) -> None: + distribute_items_restrictive(self.multiworld) + + gate_open = self.multiworld.get_location("Tutorial Gate Open", 1) + + self.assertTrue(gate_open.item is not None, "Somehow, no item got placed on Tutorial Gate Open.") + + self.assertFalse( + cast_not_none(gate_open.item).classification & ItemClassification.progression, + "Tutorial Gate Open was excluded, yet it still received an early Symbol item.", + ) diff --git a/worlds/witness/test/test_lasers.py b/worlds/witness/test/test_lasers.py index 4a71c0d433be..be71979458e8 100644 --- a/worlds/witness/test/test_lasers.py +++ b/worlds/witness/test/test_lasers.py @@ -7,7 +7,7 @@ class TestSymbolsRequiredToWinElevatorNormal(WitnessTestBase): "puzzle_randomization": "sigma_normal", "mountain_lasers": 1, "victory_condition": "elevator", - "early_symbol_item": False, + "early_good_items": {}, } def test_symbols_to_win(self) -> None: @@ -37,7 +37,7 @@ class TestSymbolsRequiredToWinElevatorExpert(WitnessTestBase): "shuffle_lasers": True, "mountain_lasers": 1, "victory_condition": "elevator", - "early_symbol_item": False, + "early_good_items": {}, "puzzle_randomization": "sigma_expert", } @@ -70,7 +70,7 @@ class TestSymbolsRequiredToWinElevatorVanilla(WitnessTestBase): "shuffle_lasers": True, "mountain_lasers": 1, "victory_condition": "elevator", - "early_symbol_item": False, + "early_good_items": {}, "puzzle_randomization": "none", } @@ -101,7 +101,6 @@ class TestSymbolsRequiredToWinElevatorVariety(WitnessTestBase): "shuffle_lasers": True, "mountain_lasers": 1, "victory_condition": "elevator", - "early_symbol_item": False, "puzzle_randomization": "umbra_variety", } @@ -134,7 +133,7 @@ class TestPanelsRequiredToWinElevator(WitnessTestBase): "shuffle_lasers": True, "mountain_lasers": 1, "victory_condition": "elevator", - "early_symbol_item": False, + "early_good_items": {}, "shuffle_symbols": False, "shuffle_doors": "panels", "door_groupings": "off", @@ -163,7 +162,7 @@ class TestDoorsRequiredToWinElevator(WitnessTestBase): "shuffle_lasers": True, "mountain_lasers": 1, "victory_condition": "elevator", - "early_symbol_item": False, + "early_good_items": {}, "shuffle_symbols": False, "shuffle_doors": "doors", "door_groupings": "off", diff --git a/worlds/witness/test/test_symbol_shuffle.py b/worlds/witness/test/test_symbol_shuffle.py index fb1d82081594..836b0e327f7b 100644 --- a/worlds/witness/test/test_symbol_shuffle.py +++ b/worlds/witness/test/test_symbol_shuffle.py @@ -3,7 +3,7 @@ class TestSymbols(WitnessTestBase): options = { - "early_symbol_item": False, + "early_good_items": {}, } def test_progressive_symbols(self) -> None: @@ -53,7 +53,7 @@ class TestSymbolRequirementsMultiworld(WitnessMultiworldTestBase): common_options = { "shuffle_discarded_panels": True, - "early_symbol_item": False, + "early_good_items": {}, } def test_arrows_exist_and_are_required_in_expert_seeds_only(self) -> None: From 0b38065123aeaa485a58f7fa6fa07ab32d7b7e35 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sat, 18 Apr 2026 16:08:21 +0100 Subject: [PATCH 07/66] APQuest: Fix Easy Mode boss having too much health to beat (#6146) * Change the boss' health to 2 in easy mode, adjust boss graphics to reflect this behavior * graphics formatting --- worlds/apquest/client/ap_quest_client.py | 2 +- worlds/apquest/client/game_manager.py | 8 +++---- worlds/apquest/client/graphics.py | 27 ++++++++++++++--------- worlds/apquest/game/gameboard.py | 2 +- worlds/apquest/game/graphics/boss.png | Bin 580 -> 754 bytes 5 files changed, 22 insertions(+), 17 deletions(-) diff --git a/worlds/apquest/client/ap_quest_client.py b/worlds/apquest/client/ap_quest_client.py index bd67cc55ca3b..e875da7b68ca 100644 --- a/worlds/apquest/client/ap_quest_client.py +++ b/worlds/apquest/client/ap_quest_client.py @@ -198,7 +198,7 @@ def render(self) -> None: if self.ap_quest_game is None: raise RuntimeError("Tried to render before self.ap_quest_game was initialized.") - self.ui.render(self.ap_quest_game, self.player_sprite) + self.ui.render(self.ap_quest_game, self.player_sprite, self.hard_mode) self.handle_game_events() def location_checked_side_effects(self, location: int) -> None: diff --git a/worlds/apquest/client/game_manager.py b/worlds/apquest/client/game_manager.py index ed2793da36a9..7d40a36ee3a7 100644 --- a/worlds/apquest/client/game_manager.py +++ b/worlds/apquest/client/game_manager.py @@ -88,23 +88,23 @@ def game_started(self) -> None: self.game_view.force_focus() self.sound_manager.game_started = True - def render(self, game: Game, player_sprite: PlayerSprite) -> None: + def render(self, game: Game, player_sprite: PlayerSprite, hard_mode: bool) -> None: 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) + self.render_gameboard(game, player_sprite, hard_mode) # Only now can we check whether a math problem is active self.render_background_game_grid(game.gameboard.size, game.active_math_problem is None) self.sound_manager.math_trap_active = game.active_math_problem is not None self.render_item_column(game) - def render_gameboard(self, game: Game, player_sprite: PlayerSprite) -> None: + def render_gameboard(self, game: Game, player_sprite: PlayerSprite, hard_mode: bool) -> None: rendered_gameboard = game.render() for gameboard_row, image_row in zip(rendered_gameboard, self.top_image_grid, strict=False): for graphic, image in zip(gameboard_row, image_row[:11], strict=False): - texture = get_texture(graphic, player_sprite) + texture = get_texture(graphic, player_sprite, hard_mode) if texture is None: image.opacity = 0 diff --git a/worlds/apquest/client/graphics.py b/worlds/apquest/client/graphics.py index 0e31218c6f02..9acf7c99878e 100644 --- a/worlds/apquest/client/graphics.py +++ b/worlds/apquest/client/graphics.py @@ -29,6 +29,7 @@ class RelatedTexture(NamedTuple): IMAGE_GRAPHICS: dict[Graphic, str | RelatedTexture] = { + # Inanimates Graphic.WALL: RelatedTexture("inanimates.png", 16, 32, 16, 16), Graphic.BREAKABLE_BLOCK: RelatedTexture("inanimates.png", 32, 32, 16, 16), Graphic.CHEST: RelatedTexture("inanimates.png", 0, 16, 16, 16), @@ -37,29 +38,25 @@ class RelatedTexture(NamedTuple): Graphic.BUTTON_NOT_ACTIVATED: RelatedTexture("inanimates.png", 0, 0, 16, 16), Graphic.BUTTON_ACTIVATED: RelatedTexture("inanimates.png", 16, 0, 16, 16), Graphic.BUTTON_DOOR: RelatedTexture("inanimates.png", 32, 0, 16, 16), - + # Enemies Graphic.NORMAL_ENEMY_1_HEALTH: RelatedTexture("normal_enemy.png", 0, 0, 16, 16), Graphic.NORMAL_ENEMY_2_HEALTH: RelatedTexture("normal_enemy.png", 16, 0, 16, 16), - Graphic.BOSS_5_HEALTH: RelatedTexture("boss.png", 16, 16, 16, 16), Graphic.BOSS_4_HEALTH: RelatedTexture("boss.png", 0, 16, 16, 16), Graphic.BOSS_3_HEALTH: RelatedTexture("boss.png", 32, 32, 16, 16), Graphic.BOSS_2_HEALTH: RelatedTexture("boss.png", 16, 32, 16, 16), Graphic.BOSS_1_HEALTH: RelatedTexture("boss.png", 0, 32, 16, 16), - + # Items Graphic.EMPTY_HEART: RelatedTexture("hearts.png", 0, 0, 16, 16), Graphic.HEART: RelatedTexture("hearts.png", 16, 0, 16, 16), Graphic.HALF_HEART: RelatedTexture("hearts.png", 32, 0, 16, 16), - Graphic.REMOTE_ITEM: RelatedTexture("items.png", 0, 16, 16, 16), Graphic.CONFETTI_CANNON: RelatedTexture("items.png", 16, 16, 16, 16), Graphic.HAMMER: RelatedTexture("items.png", 32, 16, 16, 16), Graphic.KEY: RelatedTexture("items.png", 0, 0, 16, 16), Graphic.SHIELD: RelatedTexture("items.png", 16, 0, 16, 16), Graphic.SWORD: RelatedTexture("items.png", 32, 0, 16, 16), - - Graphic.ITEMS_TEXT: "items_text.png", - + # Numbers Graphic.ZERO: RelatedTexture("numbers.png", 0, 16, 16, 16), Graphic.ONE: RelatedTexture("numbers.png", 16, 16, 16, 16), Graphic.TWO: RelatedTexture("numbers.png", 32, 16, 16, 16), @@ -70,26 +67,29 @@ class RelatedTexture(NamedTuple): Graphic.SEVEN: RelatedTexture("numbers.png", 32, 0, 16, 16), Graphic.EIGHT: RelatedTexture("numbers.png", 48, 0, 16, 16), Graphic.NINE: RelatedTexture("numbers.png", 64, 0, 16, 16), - + # Letters Graphic.LETTER_A: RelatedTexture("letters.png", 0, 16, 16, 16), Graphic.LETTER_E: RelatedTexture("letters.png", 16, 16, 16, 16), Graphic.LETTER_H: RelatedTexture("letters.png", 32, 16, 16, 16), Graphic.LETTER_I: RelatedTexture("letters.png", 0, 0, 16, 16), Graphic.LETTER_M: RelatedTexture("letters.png", 16, 0, 16, 16), Graphic.LETTER_T: RelatedTexture("letters.png", 32, 0, 16, 16), - + # Mathematical symbols Graphic.DIVIDE: RelatedTexture("symbols.png", 0, 16, 16, 16), Graphic.EQUALS: RelatedTexture("symbols.png", 16, 16, 16, 16), Graphic.MINUS: RelatedTexture("symbols.png", 32, 16, 16, 16), Graphic.PLUS: RelatedTexture("symbols.png", 0, 0, 16, 16), Graphic.TIMES: RelatedTexture("symbols.png", 16, 0, 16, 16), + # Other visual-only elements + Graphic.ITEMS_TEXT: "items_text.png", Graphic.NO: RelatedTexture("symbols.png", 32, 0, 16, 16), - Graphic.UNKNOWN: RelatedTexture("symbols.png", 32, 0, 16, 16), # Same as "No" } BACKGROUND_TILE = RelatedTexture("inanimates.png", 0, 32, 16, 16) +EASY_MODE_BOSS_2_HEALTH = RelatedTexture("boss.png", 16, 0, 16, 16) + class PlayerSprite(Enum): HUMAN = 0 @@ -160,13 +160,18 @@ def get_texture_by_identifier(texture_identifier: str | RelatedTexture) -> Textu return sub_texture -def get_texture(graphic: Graphic | Literal["Grass"], player_sprite: PlayerSprite | None = None) -> Texture | None: +def get_texture( + graphic: Graphic | Literal["Grass"], player_sprite: PlayerSprite | None = None, hard_mode: bool = False +) -> Texture | None: if graphic == Graphic.EMPTY: return None if graphic == "Grass": return get_texture_by_identifier(BACKGROUND_TILE) + if graphic == Graphic.BOSS_2_HEALTH and not hard_mode: + return get_texture_by_identifier(EASY_MODE_BOSS_2_HEALTH) + if graphic in IMAGE_GRAPHICS: return get_texture_by_identifier(IMAGE_GRAPHICS[graphic]) diff --git a/worlds/apquest/game/gameboard.py b/worlds/apquest/game/gameboard.py index 77688c2929f9..e034b3f71c26 100644 --- a/worlds/apquest/game/gameboard.py +++ b/worlds/apquest/game/gameboard.py @@ -246,7 +246,7 @@ def create_gameboard(hard_mode: bool, hammer_exists: bool, extra_chest: bool) -> breakable_block = BreakableBlock() if hammer_exists else Empty() normal_enemy = EnemyWithLoot(2 if hard_mode else 1, Location.ENEMY_DROP) - boss = FinalBoss(5 if hard_mode else 3) + boss = FinalBoss(5 if hard_mode else 2) gameboard = ( (Empty(), Empty(), Empty(), Wall(), Empty(), Empty(), Empty(), Wall(), Empty(), Empty(), Empty()), diff --git a/worlds/apquest/game/graphics/boss.png b/worlds/apquest/game/graphics/boss.png index dbcda31048e2b5a46c56d537e69258bbad77cf5c..07a9af35c0f1005e67c19aba06ac376874e54fdd 100644 GIT binary patch delta 731 zcmV<10wn#!1o8!tBYyw^b5ch_0olnce*gdg1ZP1_K>z@;j|==^1poj5AY({UO#lFT zCIA3{ga82g0001h=l}q9FaQARU;qF*m;eA5aGbhPJOBUy24YJ`L;(K){{a7>y{D4^ z000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2kHb83mq-OeSdTq0006uNkl1HT(GMGqKK$>E+>v+yfzNEWZvbKqIvmdn+F z0dFjqtMffWr+-i$zq&*k7U2Eda+H#I@sPdlj0Xoc3fc2N`}%#nXORV*fjd1~3Zhr3^Ptm9L0)Nf%s<0sAhIuspbEMLutq@yN zl_ir4$ncs7Un;c}Bm4}!RxR+Ad>}zaI^!$(z*djf@_}?c--!>J)Me{SWpgc4h;bYUZGW>Ar6t?wu_&y-v14mkVLJIZk z@3-3r!~6TvuEc0Q&~Oh(>-oa|zEm{Y`Q1E(dsK~Qgyj2x$N-BJUluLM_>t(;nR6t8 zpck)LMHsag8t(yxdV5A0QM3O3xqBcVfj`;YmDlsK?*n23zV&=hc>v@|sV)Nb46pzI N002ovPDHLkV1m4>JkqQwfW=x15x$g_iIAIa|Dgw_gMsu{ydvxCx4zzs=GhWBxv;K*`&Jr^NfNX{y4Xw u(Vu4*5G7aO(t^yPzLaDn|KpE~lk^97TAf@fD40M10000 Date: Sat, 18 Apr 2026 17:07:23 +0100 Subject: [PATCH 08/66] The Witness: More control over progressive symbols (#3961) * More control over progressive symbols! * Cleanup crew * lol * Make it configurable how second stage items act on their own * only let independent symbols work if they're not progressive * revert defaults * Better description * comment for reviewers * A complicated docstring for a complicated function * More accurate * This probably works more generically * Actually, this would cause other issues anyway, so no need to make this generic yet * :/ * oops * Change the system to use collect/remove override so that plando and start inventory work correctly * Vi stop doing that thing challenge * Make SecondStateSymbolsActIndependently an OptionSet * jank * oop * this is why we make unit tests I guess * More unit tests * More unit tests * Add note about the absence of Rotated Shapers from the independent symbols optionset * More verbose description * More verbose description * slight reword * Ruff ruff :3 I am a good puppy <3 * Remove invis dots * oops * Remove some unused symbols * display name * linecounting -> discard * Make all progressive chains always work * Unfortunately, this optimisation is now unsafe with plando :( * oops * This is now a possible optimisation * optimise optimise optimise * optimise optimise optimise * fix * ruff * oh * fixed frfr * mypy * Clean up the tests a bit * oop * I actually like this better now, so I'm doing it as default * Put stuff on the actual item class for faster collect/remove * giga oops * Make proguseful work * None item left beef * unnecessary change * formatting * add proguseful test to progressive symbols tests * add proguseful test to progressive symbols tests * clean up collect/remove a bit more * giga lmfao * Put stuff in option groups * Add the new symbol items to hint system * bump req client version * fix soft conflict in unit tests * fix more merge errors --- worlds/witness/__init__.py | 50 ++- worlds/witness/data/WitnessItems.txt | 12 +- .../witness/data/item_definition_classes.py | 2 +- .../witness/data/settings/Symbol_Shuffle.txt | 11 +- .../data/settings/progressive_items.py | 8 + worlds/witness/data/static_items.py | 10 +- worlds/witness/data/static_logic.py | 14 +- worlds/witness/hints.py | 14 +- worlds/witness/options.py | 76 +++- worlds/witness/player_items.py | 61 ++- worlds/witness/player_logic.py | 138 +++++-- worlds/witness/presets.py | 2 +- worlds/witness/rules.py | 17 +- worlds/witness/test/bases.py | 14 + worlds/witness/test/test_symbol_shuffle.py | 390 +++++++++++++++++- 15 files changed, 710 insertions(+), 109 deletions(-) create mode 100644 worlds/witness/data/settings/progressive_items.py diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index 66229600a5db..19b1b800ec64 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -82,7 +82,7 @@ class WitnessWorld(World): item_name_groups = static_witness_items.ITEM_GROUPS location_name_groups = static_witness_locations.AREA_LOCATION_GROUPS - required_client_version = (0, 6, 4) + required_client_version = (0, 6, 8) player_logic: WitnessPlayerLogic player_locations: WitnessPlayerLocations @@ -110,7 +110,7 @@ def _get_slot_data(self) -> Dict[str, Any]: "hunt_entities": [int(h, 16) for h in self.player_logic.HUNT_ENTITIES], "log_ids_to_hints": self.log_ids_to_hints, "laser_ids_to_hints": self.laser_ids_to_hints, - "progressive_item_lists": self.player_items.get_progressive_item_ids_in_pool(), + "progressive_item_lists": self.player_items.get_progressive_item_ids(), "obelisk_side_id_to_EPs": static_witness_logic.OBELISK_SIDE_ID_TO_EP_HEXES, "precompleted_puzzles": [int(h, 16) for h in self.player_logic.EXCLUDED_ENTITIES], "panel_hunt_required_absolute": self.panel_hunt_required_count @@ -429,19 +429,47 @@ def create_item(self, item_name: str) -> WitnessItem: else: item_data = static_witness_items.ITEM_DATA[item_name] - return WitnessItem(item_name, item_data.classification, item_data.ap_code, player=self.player) + item = WitnessItem(item_name, item_data.classification, item_data.ap_code, player=self.player) - def collect(self, state: "CollectionState", item: WitnessItem) -> bool: - changed = super().collect(state, item) - if changed and item.eggs: + item.is_alias_for = static_witness_items.ALL_ITEM_ALIASES.get(item_name, None) + if hasattr(self, "player_items") and self.player_items: + item.progressive_chain = self.player_items.all_progressive_item_lists.get(item_name, None) + + return item + + def collect(self, state: CollectionState, item: WitnessItem) -> bool: + if not super().collect(state, item): + return False + + if item.eggs: state.prog_items[self.player]["Egg"] += item.eggs - return changed - def remove(self, state: "CollectionState", item: WitnessItem) -> bool: - changed = super().remove(state, item) - if changed and item.eggs: + elif item.is_alias_for: + state.prog_items[self.player][item.is_alias_for] += 1 + + elif item.progressive_chain: + index = state.prog_items[self.player][item.name] - 1 + if index < len(item.progressive_chain): + state.prog_items[self.player][item.progressive_chain[index]] += 1 + + return True + + def remove(self, state: CollectionState, item: WitnessItem) -> bool: + if not super().remove(state, item): + return False + + if item.eggs: state.prog_items[self.player]["Egg"] -= item.eggs - return changed + + elif item.is_alias_for: + state.prog_items[self.player][item.is_alias_for] -= 1 + + elif item.progressive_chain: + index = state.prog_items[self.player][item.name] + if index < len(item.progressive_chain): + state.prog_items[self.player][item.progressive_chain[index]] -= 1 + + return True def get_filler_item_name(self) -> str: return "Speed Boost" diff --git a/worlds/witness/data/WitnessItems.txt b/worlds/witness/data/WitnessItems.txt index 57aee28e45b6..4ebbc49669e4 100644 --- a/worlds/witness/data/WitnessItems.txt +++ b/worlds/witness/data/WitnessItems.txt @@ -2,8 +2,8 @@ Symbols: 0 - Dots 1 - Colored Dots 2 - Full Dots -3 - Invisible Dots 5 - Sound Dots +7 - Sparse Dots 10 - Symmetry 20 - Triangles 30 - Eraser @@ -12,12 +12,16 @@ Symbols: 50 - Negative Shapers 60 - Stars 61 - Stars + Same Colored Symbol +67 - Simple Stars 71 - Black/White Squares 72 - Colored Squares 80 - Arrows -200 - Progressive Dots - Dots,Full Dots -210 - Progressive Symmetry - Symmetry,Colored Dots -260 - Progressive Stars - Stars,Stars + Same Colored Symbol +200 - Progressive Dots +240 - Progressive Shapers +210 - Progressive Symmetry +260 - Progressive Stars +270 - Progressive Squares +280 - Progressive Discard Symbols Useful: 510 - Puzzle Skip diff --git a/worlds/witness/data/item_definition_classes.py b/worlds/witness/data/item_definition_classes.py index b095a83abe63..3eb77d7509b0 100644 --- a/worlds/witness/data/item_definition_classes.py +++ b/worlds/witness/data/item_definition_classes.py @@ -35,7 +35,7 @@ class ItemDefinition: @dataclass(frozen=True) class ProgressiveItemDefinition(ItemDefinition): - child_item_names: List[str] + pass @dataclass(frozen=True) diff --git a/worlds/witness/data/settings/Symbol_Shuffle.txt b/worlds/witness/data/settings/Symbol_Shuffle.txt index 253fe98bad42..604d1c106405 100644 --- a/worlds/witness/data/settings/Symbol_Shuffle.txt +++ b/worlds/witness/data/settings/Symbol_Shuffle.txt @@ -1,13 +1,18 @@ Items: Arrows -Progressive Dots +Dots +Sparse Dots +Full Dots Sound Dots -Progressive Symmetry +Symmetry +Colored Dots Triangles Eraser Shapers Rotated Shapers Negative Shapers -Progressive Stars +Stars +Simple Stars +Stars + Same Colored Symbol Black/White Squares Colored Squares \ No newline at end of file diff --git a/worlds/witness/data/settings/progressive_items.py b/worlds/witness/data/settings/progressive_items.py new file mode 100644 index 000000000000..869f0b30fcf5 --- /dev/null +++ b/worlds/witness/data/settings/progressive_items.py @@ -0,0 +1,8 @@ +PROGRESSIVE_SYMBOLS = { + "Progressive Dots": ["Dots", "Full Dots"], + "Progressive Symmetry": ["Symmetry", "Colored Dots"], + "Progressive Stars": ["Stars", "Stars + Same Colored Symbol"], + "Progressive Shapers": ["Shapers", "Rotated Shapers", "Negative Shapers"], + "Progressive Squares": ["Black/White Squares", "Colored Squares"], + "Progressive Discard Symbols": ["Triangles", "Arrows"], +} diff --git a/worlds/witness/data/static_items.py b/worlds/witness/data/static_items.py index c64df741982e..afd592c3bdb2 100644 --- a/worlds/witness/data/static_items.py +++ b/worlds/witness/data/static_items.py @@ -13,7 +13,15 @@ # item list during get_progression_items. _special_usefuls: List[str] = ["Puzzle Skip"] -ALWAYS_GOOD_SYMBOL_ITEMS: Set[str] = {"Dots", "Black/White Squares", "Symmetry", "Shapers", "Stars"} + +ALL_ITEM_ALIASES: Dict[str, str] = { # Keeping this as str->str for now for efficiency + "Sparse Dots": "Dots", + "Simple Stars": "Stars", +} + +ALWAYS_GOOD_SYMBOL_ITEMS: Set[str] = { + "Dots", "Sparse Dots", "Black/White Squares", "Symmetry", "Shapers", "Stars", "Simple Stars" +} MODE_SPECIFIC_GOOD_ITEMS: Dict[str, Set[str]] = { "none": set(), diff --git a/worlds/witness/data/static_logic.py b/worlds/witness/data/static_logic.py index bfe92467fb61..4445f566726a 100644 --- a/worlds/witness/data/static_logic.py +++ b/worlds/witness/data/static_logic.py @@ -294,7 +294,6 @@ def combine_connections(self) -> None: # Item data parsed from WitnessItems.txt ALL_ITEMS: Dict[str, ItemDefinition] = {} -_progressive_lookup: Dict[str, str] = {} def parse_items() -> None: @@ -328,22 +327,13 @@ def parse_items() -> None: # Read filler weights. weight = int(arguments[0]) if len(arguments) >= 1 else 1 ALL_ITEMS[item_name] = WeightedItemDefinition(item_code, current_category, weight) - elif arguments: + elif item_name.startswith("Progressive"): # Progressive items. - ALL_ITEMS[item_name] = ProgressiveItemDefinition(item_code, current_category, arguments) - for child_item in arguments: - _progressive_lookup[child_item] = item_name + ALL_ITEMS[item_name] = ProgressiveItemDefinition(item_code, current_category) else: ALL_ITEMS[item_name] = ItemDefinition(item_code, current_category) -def get_parent_progressive_item(item_name: str) -> str: - """ - Returns the name of the item's progressive parent, if there is one, or the item's name if not. - """ - return _progressive_lookup.get(item_name, item_name) - - @cache_argsless def get_vanilla() -> StaticWitnessLogicObj: return StaticWitnessLogicObj(get_vanilla_logic()) diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index f04c1f6d3738..7f5469a3d56c 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -46,6 +46,8 @@ def get_always_hint_items(world: "WitnessWorld") -> List[str]: "Boat", "Caves Shortcuts", "Progressive Dots", + "Dots", + "Sparse Dots", ] difficulty = world.options.puzzle_randomization @@ -100,7 +102,11 @@ def get_priority_hint_items(world: "WitnessWorld") -> List[str]: if world.options.shuffle_symbols: symbols = [ "Progressive Dots", + "Dots", + "Sparse Dots", "Progressive Stars", + "Stars", + "Simple Stars", "Shapers", "Rotated Shapers", "Negative Shapers", @@ -110,9 +116,15 @@ def get_priority_hint_items(world: "WitnessWorld") -> List[str]: "Black/White Squares", "Colored Squares", "Sound Dots", - "Progressive Symmetry" + "Progressive Symmetry", + "Progressive Shapers", + "Progressive Squares", + "Progressive Discard Symbols", ] + # Only consider symbols that are actually in the pool + symbols = [symbol for symbol in symbols if symbol in world.player_items.item_data] + priority.update(world.random.sample(symbols, 5)) if world.options.shuffle_lasers: diff --git a/worlds/witness/options.py b/worlds/witness/options.py index 546f2a5ae246..2fcd35b64c56 100644 --- a/worlds/witness/options.py +++ b/worlds/witness/options.py @@ -1,8 +1,6 @@ from dataclasses import dataclass from typing import Tuple -from schema import And, Schema - from Options import ( Choice, DefaultOnToggle, @@ -76,6 +74,72 @@ class ShuffleSymbols(DefaultOnToggle): display_name = "Shuffle Symbols" +class ProgressiveSymbols(OptionSet): + """ + Make some symbols progressive, if they exist. + + By default, includes the chains where the second item can't be used without the first. + + Progressive Dots: Dots -> Full Dots + Progressive Symmetry: Symmetry -> Colored Dots + Progressive Stars: Stars -> Stars + Same Colored Symbol + Progressive Squares: Black/White Squares -> Colored Squares + Progressive Shapers: Shapers -> Rotated Shapers -> Negative Shapers + Progressive Discard Symbols: Triangles -> Arrows + """ + display_name = "Progressive Symbols" + + valid_keys = { + "Progressive Dots", + "Progressive Symmetry", + "Progressive Stars", + "Progressive Squares", + "Progressive Shapers", + "Progressive Discard Symbols" + } + + default = frozenset({"Progressive Dots", "Progressive Symmetry", "Progressive Stars"}) + + +class SecondStageSymbolsActIndependently(OptionSet): + """ + Makes certain second stage symbols act independently of first stage symbols if they are not progressive. + + - "Full Dots": "Full Dots" unlocks Full Dots panels even if you don't have "Dots". "Dots" is renamed to "Sparse Dots". + - "Stars + Same Colored Symbol": "Stars + Same Colored Symbol" unlocks Stars + Same Colored Symbol panels even if you don't have "Stars". "Stars" is renamed to "Simlpe Stars". + - "Colored Dots": Removes the Symmetry requirement from the Symmetry Laser panel sets so that Colored Dots can unlock something on their own. This is on by default. + + Rotated Shapers always act independently from Shapers. The ability to make them dependent on Shapers by omitting them in this option may be added in the future. + """ + + valid_keys = { + "Full Dots", + "Stars + Same Colored Symbol", + "Colored Dots", + } + + default = frozenset({"Colored Dots"}) + + visibility = Visibility.template | Visibility.complex_ui + + +class ColoredDotsAreProgressiveDots(Toggle): + """ + Put Colored Dots into the "Progressive Dots" group, after Dots. + This removes Progressive Symmetry. + """ + + visibility = Visibility.template | Visibility.complex_ui + + +class SoundDotsAreProgressiveDots(Toggle): + """ + Put Sound Dots into the "Progressive Dots" group, before Full Dots. + """ + + visibility = Visibility.template | Visibility.complex_ui + + class ShuffleLasers(Choice): """ If on, the 11 lasers are turned into items and will activate on their own upon receiving them. @@ -537,6 +601,10 @@ class PuzzleRandomizationSeed(Range): class TheWitnessOptions(PerGameCommonOptions): puzzle_randomization: PuzzleRandomization shuffle_symbols: ShuffleSymbols + progressive_symbols: ProgressiveSymbols + colored_dots_are_progressive_dots: ColoredDotsAreProgressiveDots + sound_dots_are_progressive_dots: SoundDotsAreProgressiveDots + second_stage_symbols_act_independently: SecondStageSymbolsActIndependently shuffle_doors: ShuffleDoors door_groupings: DoorGroupings shuffle_boat: ShuffleBoat @@ -600,6 +668,10 @@ class TheWitnessOptions(PerGameCommonOptions): ]), OptionGroup("Progression Items", [ ShuffleSymbols, + ProgressiveSymbols, + SecondStageSymbolsActIndependently, + ColoredDotsAreProgressiveDots, + SoundDotsAreProgressiveDots, ShuffleDoors, DoorGroupings, ShuffleLasers, diff --git a/worlds/witness/player_items.py b/worlds/witness/player_items.py index 1be228530471..dd2856a72797 100644 --- a/worlds/witness/player_items.py +++ b/worlds/witness/player_items.py @@ -7,7 +7,6 @@ from BaseClasses import Item, ItemClassification, MultiWorld from .data import static_items as static_witness_items -from .data import static_logic as static_witness_logic from .data.item_definition_classes import ( DoorItemDefinition, ItemCategory, @@ -32,6 +31,8 @@ class WitnessItem(Item): """ game: str = "The Witness" eggs: int = 0 + is_alias_for: str | None = None + progressive_chain: list[str] | None = None @classmethod def make_egg_event(cls, item_name: str, player: int): @@ -55,12 +56,31 @@ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic, self._logic: WitnessPlayerLogic = player_logic self._locations: WitnessPlayerLocations = player_locations + self.replacement_items = {} + # Make item aliases for "Sparse Dots" and "Simple Stars" if necessary + if "Full Dots" in world.options.second_stage_symbols_act_independently: + self.replacement_items["Dots"] = "Sparse Dots" + if "Stars + Same Colored Symbol" in world.options.second_stage_symbols_act_independently: + self.replacement_items["Stars"] = "Simple Stars" + + assert all( + static_witness_items.ALL_ITEM_ALIASES.get(value, None) == key + for key, value in self.replacement_items.items() + ), "A replacement item was used without setting up the alias in static_witness_items.ALL_ITEM_ALIASES" + + self.all_progressive_item_lists = copy.deepcopy(self._logic.THEORETICAL_PROGRESSIVE_LISTS) + self.progressive_item_lists_in_use = copy.deepcopy(self._logic.FINALIZED_PROGRESSIVE_LISTS) + + self.progressive_item_lookup: Dict[str, str] = {} + for progressive_item, chain_items in self.progressive_item_lists_in_use.items(): + self.progressive_item_lookup.update({chain_item: progressive_item for chain_item in chain_items}) + # Duplicate the static item data, then make any player-specific adjustments to classification. self.item_data: Dict[str, ItemData] = copy.deepcopy(static_witness_items.ITEM_DATA) # Remove all progression items that aren't actually in the game. self.item_data = { - name: data for (name, data) in self.item_data.items() + self.replacement_items.get(name, name): data for (name, data) in self.item_data.items() if ItemClassification.progression not in data.classification or name in player_logic.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME } @@ -86,7 +106,7 @@ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic, } for item_name, item_data in progression_dict.items(): if isinstance(item_data.definition, ProgressiveItemDefinition): - num_progression = len(self._logic.PROGRESSIVE_LISTS[item_name]) + num_progression = len(self.progressive_item_lists_in_use[item_name]) self._mandatory_items[item_name] = num_progression else: self._mandatory_items[item_name] = 1 @@ -141,9 +161,15 @@ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic, if self._world.options.puzzle_randomization == "umbra_variety": self._proguseful_items.add("Triangles") - # This needs to be improved when the improved independent&progressive symbols PR is merged - for item in list(self._proguseful_items): - self._proguseful_items.add(static_witness_logic.get_parent_progressive_item(item)) + for progressive_item, progressive_item_chain in player_logic.FINALIZED_PROGRESSIVE_LISTS.items(): + for chain_item in progressive_item_chain: + if chain_item in self._proguseful_items: + self._proguseful_items.add(progressive_item) + break + + for alias_item, real_item in static_witness_items.ALL_ITEM_ALIASES.items(): + if real_item in self._proguseful_items: + self._proguseful_items.add(alias_item) for item_name, item_data in self.item_data.items(): if item_name in self._proguseful_items: @@ -215,7 +241,7 @@ def get_early_items(self, existing_items: Set[str]) -> Dict[str, List[str]]: # Replace progressive items with their parents. good_symbols = [ - static_witness_logic.get_parent_progressive_item(item) for item in good_symbols + self.progressive_item_lookup.get(item, item) for item in good_symbols ] output["Symbol"] = [symbol for symbol in good_symbols if symbol in existing_items] @@ -331,13 +357,14 @@ def get_symbol_ids_not_in_pool(self) -> List[int]: if name not in self.item_data.keys() and data.definition.category is ItemCategory.SYMBOL ] - def get_progressive_item_ids_in_pool(self) -> Dict[int, List[int]]: - output: Dict[int, List[int]] = {} - for item_name, quantity in dict(self._mandatory_items.items()).items(): - item = self.item_data[item_name] - if isinstance(item.definition, ProgressiveItemDefinition): - # Note: we need to reference the static table here rather than the player-specific one because the child - # items were removed from the pool when we pruned out all progression items not in the options. - output[cast_not_none(item.ap_code)] = [cast_not_none(static_witness_items.ITEM_DATA[child_item].ap_code) - for child_item in item.definition.child_item_names] - return output + def get_progressive_item_ids(self) -> Dict[int, List[int]]: + """ + Returns a dict from progressive item IDs to the list of IDs of the base item that they unlock, in order. + """ + return { + cast_not_none(static_witness_items.ITEM_DATA[progressive_item].ap_code): [ + cast_not_none(static_witness_items.ITEM_DATA[base_item].ap_code) + for base_item in corresponding_base_items + ] + for progressive_item, corresponding_base_items in self.all_progressive_item_lists.items() + } diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index b24434732ffc..c369ec623aa3 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -22,6 +22,7 @@ from .data import static_logic as static_witness_logic from .data.definition_classes import ConnectionDefinition, WitnessRule from .data.item_definition_classes import DoorItemDefinition, ItemCategory, ProgressiveItemDefinition +from .data.settings.progressive_items import PROGRESSIVE_SYMBOLS from .data.static_logic import StaticWitnessLogicObj from .data.utils import ( get_boat, @@ -74,12 +75,12 @@ def __init__(self, world: "WitnessWorld", disabled_locations: Set[str], start_in self.UNREACHABLE_REGIONS: Set[str] = set() self.THEORETICAL_BASE_ITEMS: Set[str] = set() - self.THEORETICAL_ITEMS: Set[str] = set() - self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME: Set[str] = set() + self.THEORETICAL_PROGRESSIVE_LISTS: Dict[str, List[str]] = {} + self.ENABLED_PROGRESSIVE_LISTS: Dict[str, List[str]] = {} + self.FINALIZED_PROGRESSIVE_LISTS: Dict[str, List[str]] = {} + self.PARENT_ITEM_COUNT_PER_BASE_ITEM: Dict[str, int] = {} + self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME: Set[str] = set() # No "progressive" conversion yet self.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME: Set[str] = set() - - self.PARENT_ITEM_COUNT_PER_BASE_ITEM: Dict[str, int] = defaultdict(lambda: 1) - self.PROGRESSIVE_LISTS: Dict[str, List[str]] = {} self.DOOR_ITEMS_BY_ID: Dict[str, List[str]] = {} self.FORBIDDEN_DOORS: Set[str] = set() @@ -303,15 +304,13 @@ def make_single_adjustment(self, adj_type: str, line: str) -> None: line_split = line.split(" - ") item_name = line_split[0] + # Do not add progressive items, delete the individual items + assert not isinstance(static_witness_logic.ALL_ITEMS[item_name], ProgressiveItemDefinition) + if item_name not in static_witness_items.ITEM_DATA: raise RuntimeError(f'Item "{item_name}" does not exist.') - self.THEORETICAL_ITEMS.add(item_name) - if isinstance(static_witness_logic.ALL_ITEMS[item_name], ProgressiveItemDefinition): - self.THEORETICAL_BASE_ITEMS.update(cast(ProgressiveItemDefinition, - static_witness_logic.ALL_ITEMS[item_name]).child_item_names) - else: - self.THEORETICAL_BASE_ITEMS.add(item_name) + self.THEORETICAL_BASE_ITEMS.add(item_name) if static_witness_logic.ALL_ITEMS[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]: entity_hexes = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name]).panel_id_hexes @@ -323,13 +322,10 @@ def make_single_adjustment(self, adj_type: str, line: str) -> None: if adj_type == "Remove Items": item_name = line - self.THEORETICAL_ITEMS.discard(item_name) - if isinstance(static_witness_logic.ALL_ITEMS[item_name], ProgressiveItemDefinition): - self.THEORETICAL_BASE_ITEMS.difference_update( - cast(ProgressiveItemDefinition, static_witness_logic.ALL_ITEMS[item_name]).child_item_names - ) - else: - self.THEORETICAL_BASE_ITEMS.discard(item_name) + self.THEORETICAL_BASE_ITEMS.discard(item_name) + + # Do not delete progressive items, delete the individual items + assert not isinstance(static_witness_logic.ALL_ITEMS[item_name], ProgressiveItemDefinition) if static_witness_logic.ALL_ITEMS[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]: entity_hexes = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name]).panel_id_hexes @@ -537,6 +533,52 @@ def handle_panelhunt_postgame(self, world: "WitnessWorld") -> List[List[str]]: return postgame_adjustments + def add_implicit_dependencies_to_requirements(self, dependencies: Dict[str, str]) -> None: + if not dependencies: + return + + for entity, requirement in self.DEPENDENT_REQUIREMENTS_BY_HEX.items(): + if "items" not in requirement: + continue + + new_requirement_options = set() + for requirement_option in requirement["items"]: + changed_requirement_option = set(requirement_option) + for item1, item2 in dependencies.items(): + if item1 in requirement_option: + changed_requirement_option.add(item2) + new_requirement_options.add(frozenset(changed_requirement_option)) + self.DEPENDENT_REQUIREMENTS_BY_HEX[entity]["items"] = frozenset(new_requirement_options) + + def adjust_requirements_for_second_stage_symbols(self, world: "WitnessWorld") -> None: + """ + When playing with non-progressive symbols, + there are some second-stage symbols that can't be used without the first-stage symbol. + However, there is a player option that makes these second stage symbols independent. + + If they are independent, we rename "Dots" to "Sparse Dots" and "Stars" to "Simple Stars" + to drive home the separation of the respective items. + + If they are not independent, we need to add the "Dots" requirement to every "Full Dots" panel, + as well as the "Stars" requirement to every "Stars + Same Colored Symbol" panel. + + Also, if Progressive Symmetry is off and independent symbols are off, a Symmetry requirement is added to the + Symmetry Laser sets. + """ + + implicit_dependencies = {} + + if "Full Dots" not in world.options.second_stage_symbols_act_independently: + implicit_dependencies["Full Dots"] = "Dots" + + if "Stars + Same Colored Symbol" not in world.options.second_stage_symbols_act_independently: + implicit_dependencies["Stars + Same Colored Symbol"] = "Stars" + + if "Colored Dots" not in world.options.second_stage_symbols_act_independently: + implicit_dependencies["Colored Dots"] = "Symmetry" + + self.add_implicit_dependencies_to_requirements(implicit_dependencies) + def set_easter_egg_requirements(self, world: "WitnessWorld") -> None: eggs_per_check, logically_required_eggs_per_check = world.options.easter_egg_hunt.get_step_and_logical_step() @@ -649,6 +691,28 @@ def make_options_adjustments(self, world: "WitnessWorld") -> None: if world.options.shuffle_symbols: adjustment_linesets_in_order.append(get_symbol_shuffle_list()) + self.THEORETICAL_PROGRESSIVE_LISTS = copy.deepcopy(PROGRESSIVE_SYMBOLS) + + self.adjust_requirements_for_second_stage_symbols(world) + + if world.options.colored_dots_are_progressive_dots: + # Insert after Dots + dots_index = self.THEORETICAL_PROGRESSIVE_LISTS["Progressive Dots"].index("Dots") + self.THEORETICAL_PROGRESSIVE_LISTS["Progressive Dots"].insert(dots_index + 1, "Colored Dots") + + # Remove from Progressive Symmetry + self.THEORETICAL_PROGRESSIVE_LISTS["Progressive Symmetry"].remove("Colored Dots") + + if world.options.sound_dots_are_progressive_dots: + # Insert before Full Dots + full_dots_index = self.THEORETICAL_PROGRESSIVE_LISTS["Progressive Dots"].index("Full Dots") + self.THEORETICAL_PROGRESSIVE_LISTS["Progressive Dots"].insert(full_dots_index, "Sound Dots") + + self.ENABLED_PROGRESSIVE_LISTS = { + progressive_item: item_list for progressive_item, item_list in self.THEORETICAL_PROGRESSIVE_LISTS.items() + if progressive_item in world.options.progressive_symbols + } + if world.options.EP_difficulty == "normal": adjustment_linesets_in_order.append(get_ep_easy()) elif world.options.EP_difficulty == "tedious": @@ -962,18 +1026,32 @@ def finalize_items(self) -> None: """ Finalise which items are used in the world, and handle their progressive versions. """ - for item in self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME: - if item not in self.THEORETICAL_ITEMS: - progressive_item_name = static_witness_logic.get_parent_progressive_item(item) - self.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME.add(progressive_item_name) - child_items = cast(ProgressiveItemDefinition, - static_witness_logic.ALL_ITEMS[progressive_item_name]).child_item_names - progressive_list = [child_item for child_item in child_items - if child_item in self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME] - self.PARENT_ITEM_COUNT_PER_BASE_ITEM[item] = progressive_list.index(item) + 1 - self.PROGRESSIVE_LISTS[progressive_item_name] = progressive_list - else: - self.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME.add(item) + + self.FINALIZED_PROGRESSIVE_LISTS = self.ENABLED_PROGRESSIVE_LISTS.copy() + + # Filter non existent base items + self.FINALIZED_PROGRESSIVE_LISTS = { + progressive_item: [ + item for item in base_items if item in self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME + ] + for progressive_item, base_items in self.FINALIZED_PROGRESSIVE_LISTS.items() + } + + # Filter empty chains / chains with only one item (no point in having those) + self.FINALIZED_PROGRESSIVE_LISTS = { + progressive_item: base_items + for progressive_item, base_items in self.FINALIZED_PROGRESSIVE_LISTS.items() + if len(base_items) >= 2 # No point in a single-item progressive chain + } + + # Build PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME with the finalized progressive item replacements in mind + self.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME = self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME.copy() + for progressive_item, base_items in self.FINALIZED_PROGRESSIVE_LISTS.items(): + self.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME.add(progressive_item) + self.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME -= set(base_items) + + for i, base_item in enumerate(base_items): + self.PARENT_ITEM_COUNT_PER_BASE_ITEM[base_item] = i + 1 def solvability_guaranteed(self, entity_hex: str) -> bool: return not ( diff --git a/worlds/witness/presets.py b/worlds/witness/presets.py index 934f55b6853b..170df82172b9 100644 --- a/worlds/witness/presets.py +++ b/worlds/witness/presets.py @@ -123,7 +123,7 @@ "trap_percentage": TrapPercentage.default, "puzzle_skip_amount": 15, "trap_weights": TrapWeights.default, - + "hint_amount": HintAmount.default, "area_hint_percentage": AreaHintPercentage.default, "laser_hints": LaserHints.default, diff --git a/worlds/witness/rules.py b/worlds/witness/rules.py index 545c3e7dd042..5822aadacfa7 100644 --- a/worlds/witness/rules.py +++ b/worlds/witness/rules.py @@ -203,10 +203,7 @@ def _has_item(item: str, world: "WitnessWorld", if item == "Theater to Tunnels": return lambda state: _can_do_theater_to_tunnels(state, world) - actual_item = static_witness_logic.get_parent_progressive_item(item) - needed_amount = player_logic.PARENT_ITEM_COUNT_PER_BASE_ITEM[item] - - simple_rule: SimpleItemRepresentation = SimpleItemRepresentation(actual_item, needed_amount) + simple_rule: SimpleItemRepresentation = SimpleItemRepresentation(item, 1) return simple_rule @@ -214,6 +211,7 @@ def optimize_requirement_option(requirement_option: List[Union[CollectionRule, S -> List[Union[CollectionRule, SimpleItemRepresentation]]: """ This optimises out a requirement like [("Progressive Dots": 1), ("Progressive Dots": 2)] to only the "2" version. + It is unclear how much this does after the recent rework the progressive items, but there is no reason to remove it. """ direct_items = [rule for rule in requirement_option if isinstance(rule, SimpleItemRepresentation)] @@ -231,12 +229,15 @@ def optimize_requirement_option(requirement_option: List[Union[CollectionRule, S def convert_requirement_option(requirement: List[Union[CollectionRule, SimpleItemRepresentation]], - player: int) -> List[CollectionRule]: + world: "WitnessWorld") -> List[CollectionRule]: """ Converts a list of CollectionRules and SimpleItemRepresentations to just a list of CollectionRules. If the list is ONLY SimpleItemRepresentations, we can just return a CollectionRule based on state.has_all_counts() """ + player_logic = world.player_logic + player = world.player + collection_rules = [rule for rule in requirement if not isinstance(rule, SimpleItemRepresentation)] item_rules = [rule for rule in requirement if isinstance(rule, SimpleItemRepresentation)] @@ -251,7 +252,7 @@ def convert_requirement_option(requirement: List[Union[CollectionRule, SimpleIte # Sort the list by which item you are least likely to have (E.g. last stage of progressive item chains) sorted_item_list = sorted( item_counts.keys(), - key=lambda item_name: item_counts[item_name] if ("Progressive" in item_name) else 1.5, + key=lambda item_name: player_logic.PARENT_ITEM_COUNT_PER_BASE_ITEM.get(item_name, 1.5), reverse=True # 1.5 because you are less likely to have a single stage item than one copy of a 2-stage chain # I did some testing and every part of this genuinely gives a tiiiiny performance boost over not having it! @@ -272,8 +273,6 @@ def _meets_item_requirements(requirements: WitnessRule, world: "WitnessWorld") - """ Converts a WitnessRule into a CollectionRule. """ - player = world.player - if requirements == frozenset({frozenset()}): return None @@ -284,7 +283,7 @@ def _meets_item_requirements(requirements: WitnessRule, world: "WitnessWorld") - optimized_rule_conversion = [optimize_requirement_option(sublist) for sublist in rule_conversion] - fully_converted_rules = [convert_requirement_option(sublist, player) for sublist in optimized_rule_conversion] + fully_converted_rules = [convert_requirement_option(sublist, world) for sublist in optimized_rule_conversion] if len(fully_converted_rules) == 1: if len(fully_converted_rules[0]) == 1: diff --git a/worlds/witness/test/bases.py b/worlds/witness/test/bases.py index c3b427851af0..97f3ff921928 100644 --- a/worlds/witness/test/bases.py +++ b/worlds/witness/test/bases.py @@ -122,6 +122,20 @@ def assert_can_beat_with_minimally(self, required_item_counts: Mapping[str, int] ) item_objects.append(removed_item) + def assert_quantities_in_itempool(self, expected_quantities: Mapping[str, int]) -> None: + for item, expected_quantity in expected_quantities.items(): + with self.subTest(f"Verify that there are {expected_quantity} copies of {item} in the itempool."): + found_items = self.get_items_by_name(item) + self.assertEqual(len(found_items), expected_quantity) + + def assert_item_exists_and_is_proguseful(self, item_name: str, proguseful=True): + items = self.get_items_by_name(item_name) + self.assertTrue(items) + if proguseful: + self.assertTrue(all(item.advancement and item.useful for item in items)) + else: + self.assertTrue(all(item.advancement and not item.useful for item in items)) + class WitnessMultiworldTestBase(MultiworldTestBase): options_per_world: List[Dict[str, Any]] diff --git a/worlds/witness/test/test_symbol_shuffle.py b/worlds/witness/test/test_symbol_shuffle.py index 836b0e327f7b..f40e1b30362f 100644 --- a/worlds/witness/test/test_symbol_shuffle.py +++ b/worlds/witness/test/test_symbol_shuffle.py @@ -1,38 +1,394 @@ from ..test.bases import WitnessMultiworldTestBase, WitnessTestBase -class TestSymbols(WitnessTestBase): +class TestProgressiveSymbols(WitnessTestBase): options = { "early_good_items": {}, + "puzzle_randomization": "umbra_variety", + "progressive_symbols": { + "Progressive Dots", + "Progressive Symmetry", + "Progressive Stars", + "Progressive Squares", + "Progressive Shapers", + "Progressive Discard Symbols" + } } def test_progressive_symbols(self) -> None: """ - Test that Dots & Full Dots are correctly replaced by 2x Progressive Dots, + Test that Full Dots are correctly replaced by 2x Progressive Dots, and test that Dots puzzles and Full Dots puzzles require 1 and 2 copies of this item respectively. """ + expected_quantities = { + # Individual items that are replaced by progressive items + "Dots": 0, + "Sparse Dots": 0, + "Full Dots": 0, + "Symmetry": 0, + "Colored Dots": 0, + "Stars": 0, + "Simple Stars": 0, + "Stars + Same Colored Symbol": 0, + "Black/White Squares": 0, + "Colored Squares": 0, + "Shapers": 0, + "Rotated Shapers": 0, + "Negative Shapers": 0, + "Triangles": 0, + "Arrows": 0, + + # Progressive items + "Progressive Dots": 2, + "Progressive Symmetry": 2, + "Progressive Stars": 2, + "Progressive Squares": 2, + "Progressive Shapers": 3, + "Progressive Discard Symbols": 2, + + # Individual items that still exist because they aren't a part of any progressive chain + "Sound Dots": 1, + } + + self.assert_quantities_in_itempool(expected_quantities) + + with self.subTest("Verify that Dots panels need 1 copy of Progressive Dots and Full Dots panel need 2 copies"): + self.collect_all_but("Progressive Dots") + progressive_dots = self.get_items_by_name("Progressive Dots") + self.assertEqual(len(progressive_dots), 2) + + self.assertFalse(self.multiworld.state.can_reach("Tutorial Patio Floor", "Location", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player)) + + self.collect(progressive_dots.pop()) + + self.assertTrue(self.multiworld.state.can_reach("Tutorial Patio Floor", "Location", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player)) + + self.collect(progressive_dots.pop()) + + self.assertTrue(self.multiworld.state.can_reach("Tutorial Patio Floor", "Location", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player)) + + with self.subTest("Verify proguseful status of progressive & alias items"): + self.assert_item_exists_and_is_proguseful("Progressive Symmetry", proguseful=False) + self.assert_item_exists_and_is_proguseful("Sound Dots", proguseful=False) + + self.assert_item_exists_and_is_proguseful("Progressive Dots") + self.assert_item_exists_and_is_proguseful("Progressive Stars") + self.assert_item_exists_and_is_proguseful("Progressive Squares") + self.assert_item_exists_and_is_proguseful("Progressive Shapers") + self.assert_item_exists_and_is_proguseful("Progressive Discard Symbols") + + +class TestIndependentSecondStageSymbols(WitnessTestBase): + options = { + "early_good_items": {}, + "puzzle_randomization": "umbra_variety", + "progressive_symbols": {}, + "second_stage_symbols_act_independently": { + "Full Dots", + "Stars + Same Colored Symbol", + "Colored Dots", + }, + "shuffle_doors": "doors", + } + + def test_independent_second_stage_symbols(self) -> None: + expected_quantities = { + # Progressive items shouldn't exist + "Progressive Dots": 0, + "Progressive Symmetry": 0, + "Progressive Stars": 0, + "Progressive Squares": 0, + "Progressive Shapers": 0, + "Progressive Discard Symbols": 0, + + # Dots and Stars are replaced by Sparse Dots and Simple Stars + "Dots": 0, + "Stars": 0, + "Sparse Dots": 1, + "Simple Stars": 1, + + # None of the symbols are progressive, so they should all exist + "Full Dots": 1, + "Symmetry": 1, + "Colored Dots": 1, + "Stars + Same Colored Symbol": 1, + "Black/White Squares": 1, + "Colored Squares": 1, + "Shapers": 1, + "Rotated Shapers": 1, + "Negative Shapers": 1, + "Triangles": 1, + "Arrows": 1, + "Sound Dots": 1, + } + + self.assert_quantities_in_itempool(expected_quantities) + + with self.subTest("Verify that Full Dots panels only need Full Dots"): + self.collect_by_name("Black/White Squares") + self.collect_by_name("Triangles") + self.collect_by_name("Outside Tutorial Outpost Exit (Door)") + + self.assertFalse( + self.multiworld.state.can_reach("Outside Tutorial Outpost Exit Panel", "Location", self.player) + ) + self.collect_by_name("Full Dots") + self.assertTrue( + self.multiworld.state.can_reach("Outside Tutorial Outpost Exit Panel", "Location", self.player) + ) + + with self.subTest("Verify that Stars + Same Colored Symbol panels only need Stars + Same Colored Symbol"): + self.collect_by_name("Eraser") + self.collect_by_name("Quarry Entry 1 (Door)") + self.collect_by_name("Quarry Entry 2 (Door)") + + self.assertFalse( + self.multiworld.state.can_reach("Quarry Stoneworks Entry Left Panel", "Location", self.player) + ) + self.collect_by_name("Stars + Same Colored Symbol") + self.assertTrue( + self.multiworld.state.can_reach("Quarry Stoneworks Entry Left Panel", "Location", self.player) + ) + + with self.subTest("Verify that non-symmetry Colored Dots panels only need Colored Dots"): + self.collect_by_name("Symmetry Island Lower (Door)") + self.collect_by_name("Symmetry Island Upper (Door)") + + self.assertFalse( + self.multiworld.state.can_reach("Symmetry Island Laser Blue 3", "Location", self.player) + ) + self.collect_by_name("Colored Dots") + self.assertTrue( + self.multiworld.state.can_reach("Symmetry Island Laser Blue 3", "Location", self.player) + ) + + with self.subTest("Verify proguseful status of progressive & alias items"): + self.assert_item_exists_and_is_proguseful("Full Dots", proguseful=False) + self.assert_item_exists_and_is_proguseful("Stars + Same Colored Symbol", proguseful=False) + self.assert_item_exists_and_is_proguseful("Arrows", proguseful=False) # Variety + + self.assert_item_exists_and_is_proguseful("Sparse Dots") + self.assert_item_exists_and_is_proguseful("Simple Stars") + self.assert_item_exists_and_is_proguseful("Triangles") # Variety + + +class TestDependentSecondStageSymbols(WitnessTestBase): + options = { + "early_good_items": {}, + "puzzle_randomization": "umbra_variety", + "progressive_symbols": {}, + "second_stage_symbols_act_independently": {}, + "shuffle_doors": "doors", + } + + def test_dependent_second_stage_symbols(self) -> None: + expected_quantities = { + # Progressive items shouldn't exist + "Progressive Dots": 0, + "Progressive Symmetry": 0, + "Progressive Stars": 0, + "Progressive Squares": 0, + "Progressive Shapers": 0, + "Progressive Discard Symbols": 0, + + # Dots and Stars are NOT replaced by Sparse Dots and Simple Stars + "Dots": 1, + "Stars": 1, + "Sparse Dots": 0, + "Simple Stars": 0, + + # None of the symbols are progressive, so they should all exist + "Full Dots": 1, + "Symmetry": 1, + "Colored Dots": 1, + "Stars + Same Colored Symbol": 1, + "Black/White Squares": 1, + "Colored Squares": 1, + "Shapers": 1, + "Rotated Shapers": 1, + "Negative Shapers": 1, + "Triangles": 1, + "Arrows": 1, + "Sound Dots": 1, + } + + self.assert_quantities_in_itempool(expected_quantities) + + with self.subTest("Verify that Full Dots panels need Dots as well"): + self.collect_by_name("Black/White Squares") + self.collect_by_name("Triangles") + self.collect_by_name("Outside Tutorial Outpost Exit (Door)") + + self.assertFalse( + self.multiworld.state.can_reach("Outside Tutorial Outpost Exit Panel", "Location", self.player)) + self.collect_by_name("Full Dots") + self.assertFalse( + self.multiworld.state.can_reach("Outside Tutorial Outpost Exit Panel", "Location", self.player)) + self.collect_by_name("Dots") + self.assertTrue( + self.multiworld.state.can_reach("Outside Tutorial Outpost Exit Panel", "Location", self.player) + ) + + with self.subTest("Verify that Stars + Same Colored Symbol panels need Stars as well"): + self.collect_by_name("Eraser") + self.collect_by_name("Quarry Entry 1 (Door)") + self.collect_by_name("Quarry Entry 2 (Door)") + + self.assertFalse( + self.multiworld.state.can_reach("Quarry Stoneworks Entry Left Panel", "Location", self.player) + ) + self.collect_by_name("Stars + Same Colored Symbol") + self.assertFalse( + self.multiworld.state.can_reach("Quarry Stoneworks Entry Left Panel", "Location", self.player) + ) + self.collect_by_name("Stars") + self.assertTrue( + self.multiworld.state.can_reach("Quarry Stoneworks Entry Left Panel", "Location", self.player) + ) + + with self.subTest("Verify that non-symmetry Colored Dots panels need Symmetry as well"): + self.collect_by_name("Symmetry Island Lower (Door)") + self.collect_by_name("Symmetry Island Upper (Door)") + + self.assertFalse( + self.multiworld.state.can_reach("Symmetry Island Laser Blue 3", "Location", self.player) + ) + self.collect_by_name("Colored Dots") + self.assertFalse( + self.multiworld.state.can_reach("Symmetry Island Laser Blue 3", "Location", self.player) + ) + self.collect_by_name("Symmetry") + self.assertTrue( + self.multiworld.state.can_reach("Symmetry Island Laser Blue 3", "Location", self.player) + ) + + with self.subTest("Verify proguseful status of progressive & alias items"): + self.assert_item_exists_and_is_proguseful("Full Dots", proguseful=False) + self.assert_item_exists_and_is_proguseful("Stars + Same Colored Symbol", proguseful=False) + self.assert_item_exists_and_is_proguseful("Arrows", proguseful=False) # Variety + + self.assert_item_exists_and_is_proguseful("Dots") + self.assert_item_exists_and_is_proguseful("Stars") + self.assert_item_exists_and_is_proguseful("Triangles") # Variety + + +class TestAlternateProgressiveDots(WitnessTestBase): + options = { + "early_good_items": {}, + "puzzle_randomization": "umbra_variety", + "progressive_symbols": { + "Progressive Dots", + "Progressive Symmetry" + }, + "second_stage_symbols_act_independently": { + "Full Dots", + "Stars + Same Colored Symbol", + "Colored Dots", + }, + "colored_dots_are_progressive_dots": True, + "sound_dots_are_progressive_dots": True, + "shuffle_doors": "doors", + } + + def test_alternate_progressive_dots(self) -> None: + expected_quantities = { + # Progressive Dots chain now has 4 members + "Progressive Dots": 4, + + # Dots items don't exist because Progressive Dots is on. + # For this test, this includes Colored Dots and Sound Dots as well + "Dots": 0, + "Sparse Dots": 0, + "Colored Dots": 0, + "Sound Dots": 0, + "Full Dots": 0, + + # Progressive Symmetry no longer exists, because Colored Dots is part of the Progressive Dots chain instead + "Progressive Symmetry": 0, + + # Other Progressive Symbols don't exist + "Progressive Stars": 0, + "Progressive Squares": 0, + "Progressive Shapers": 0, + "Progressive Discard Symbols": 0, + + # Other standalone items exist, because they are not progressive + "Symmetry": 1, + "Stars + Same Colored Symbol": 1, + "Black/White Squares": 1, + "Colored Squares": 1, + "Shapers": 1, + "Rotated Shapers": 1, + "Negative Shapers": 1, + "Triangles": 1, + "Arrows": 1, + + # This test is set to have independent symbols, so Simple Stars exist instead of Stars + "Stars": 0, + "Simple Stars": 1, + } + + self.assert_quantities_in_itempool(expected_quantities) + + self.collect_all_but(["Progressive Dots", "Symmetry"]) # Skip Symmetry so we can also test a little quirk progressive_dots = self.get_items_by_name("Progressive Dots") - self.assertEqual(len(progressive_dots), 2) + self.assertEqual(len(progressive_dots), 4) + + with self.subTest("Test that one copy of Progressive Dots unlocks Dots panels"): + self.assertFalse(self.multiworld.state.can_reach("Tutorial Patio Floor", "Location", self.player)) + self.assertFalse( + self.multiworld.state.can_reach("Symmetry Island Laser Blue 3", "Location", self.player) + ) + self.assertFalse(self.multiworld.state.can_reach("Jungle Popup Wall 6", "Location", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player)) + + self.collect(progressive_dots.pop()) + + self.assertTrue(self.multiworld.state.can_reach("Tutorial Patio Floor", "Location", self.player)) + self.assertFalse( + self.multiworld.state.can_reach("Symmetry Island Laser Blue 3", "Location", self.player) + ) + self.assertFalse(self.multiworld.state.can_reach("Jungle Popup Wall 6", "Location", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player)) + + with self.subTest("Test that two copies of Progressive Dots unlocks Colored Dots panels"): + self.collect(progressive_dots.pop()) + + self.assertTrue(self.multiworld.state.can_reach("Tutorial Patio Floor", "Location", self.player)) + # Also test here that these "Colored Dots" act independently from Symmetry like they are supposed to + self.assertTrue( + self.multiworld.state.can_reach("Symmetry Island Laser Blue 3", "Location", self.player) + ) + self.assertFalse(self.multiworld.state.can_reach("Jungle Popup Wall 6", "Location", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player)) - self.assertFalse(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player)) - self.assertFalse( - self.multiworld.state.can_reach("Outside Tutorial Outpost Entry Panel", "Location", self.player) - ) + with self.subTest("Test that three copies of Progressive Dots unlocks Sound Dots panels"): + self.collect(progressive_dots.pop()) - self.collect(progressive_dots.pop()) + self.assertTrue(self.multiworld.state.can_reach("Tutorial Patio Floor", "Location", self.player)) + self.assertTrue( + self.multiworld.state.can_reach("Symmetry Island Laser Blue 3", "Location", self.player) + ) + self.assertTrue(self.multiworld.state.can_reach("Jungle Popup Wall 6", "Location", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player)) - self.assertTrue(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player)) - self.assertFalse( - self.multiworld.state.can_reach("Outside Tutorial Outpost Entry Panel", "Location", self.player) - ) + with self.subTest("Test that four copies of Progressive Dots unlocks Full Dots panels"): + self.collect(progressive_dots.pop()) - self.collect(progressive_dots.pop()) + self.assertTrue(self.multiworld.state.can_reach("Tutorial Patio Floor", "Location", self.player)) + self.assertTrue( + self.multiworld.state.can_reach("Symmetry Island Laser Blue 3", "Location", self.player) + ) + self.assertTrue(self.multiworld.state.can_reach("Jungle Popup Wall 6", "Location", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player)) - self.assertTrue(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player)) - self.assertTrue( - self.multiworld.state.can_reach("Outside Tutorial Outpost Entry Panel", "Location", self.player) - ) + with self.subTest("Verify proguseful status of progressive & alias items"): + self.assert_item_exists_and_is_proguseful("Progressive Dots") + self.assert_item_exists_and_is_proguseful("Simple Stars") class TestSymbolRequirementsMultiworld(WitnessMultiworldTestBase): From d41cec64948231d7a8b1115d7cfcd7835a1832c7 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sat, 25 Apr 2026 22:44:48 +0000 Subject: [PATCH 09/66] CI: update softprops/action-gh-release to v3.0.0 (#6162) The update will be required once GH drops support for Node 20. Also enables new options the action has now. --- .github/workflows/release.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 21e1a24b8889..ca155031c673 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,7 +29,7 @@ jobs: - name: Set env run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV # tag x.y.z will become "Archipelago x.y.z" - name: Create Release - uses: softprops/action-gh-release@975c1b265e11dd76618af1c374e7981f9a6ff44a + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 with: draft: true # don't publish right away, especially since windows build is added by hand prerelease: false @@ -97,13 +97,15 @@ jobs: build/exe.*/ArchipelagoServer.exe setups/* - name: Add to Release - uses: softprops/action-gh-release@975c1b265e11dd76618af1c374e7981f9a6ff44a + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 with: draft: true # see above prerelease: false name: Archipelago ${{ env.RELEASE_VERSION }} files: | setups/* + fail_on_unmatched_files: true + overwrite_files: false # Windows release is usually built by hand env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -165,12 +167,14 @@ jobs: build/exe.*/ArchipelagoServer dist/* - name: Add to Release - uses: softprops/action-gh-release@975c1b265e11dd76618af1c374e7981f9a6ff44a + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 with: draft: true # see above prerelease: false name: Archipelago ${{ env.RELEASE_VERSION }} files: | dist/* + fail_on_unmatched_files: true + overwrite_files: false # should never happen; avoids accidentally changing a release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From d2395de9fa269e1dfa9c6b29d912bf802eda9660 Mon Sep 17 00:00:00 2001 From: lepideble <147614625+lepideble@users.noreply.github.com> Date: Mon, 27 Apr 2026 14:06:19 +0200 Subject: [PATCH 10/66] Factorio: move communication savegame to user dir (#5646) --- worlds/factorio/Client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/factorio/Client.py b/worlds/factorio/Client.py index beec7c434e93..5649375916ef 100755 --- a/worlds/factorio/Client.py +++ b/worlds/factorio/Client.py @@ -19,7 +19,7 @@ from CommonClient import ClientCommandProcessor, CommonContext, logger, server_loop, gui_enabled, get_base_parser from MultiServer import mark_raw from NetUtils import ClientStatus, NetworkItem, JSONtoTextParser, JSONMessagePart -from Utils import async_start, get_file_safe_name, is_windows, Version, format_SI_prefix, get_text_between +from Utils import async_start, get_file_safe_name, is_windows, Version, format_SI_prefix, get_text_between, user_path from .settings import FactorioSettings from settings import get_settings @@ -474,7 +474,7 @@ async def get_info(ctx: FactorioContext, rcon_client: factorio_rcon.RCONClient): async def factorio_spinup_server(ctx: FactorioContext) -> bool: - savegame_name = os.path.abspath("Archipelago.zip") + savegame_name = user_path("factorio", "saves", "Archipelago.zip") if not os.path.exists(savegame_name): logger.info(f"Creating savegame {savegame_name}") subprocess.run(( From 9c78edc764d93821136310edaf0b1bb8be564470 Mon Sep 17 00:00:00 2001 From: Ian Robinson Date: Wed, 29 Apr 2026 02:21:09 -0400 Subject: [PATCH 11/66] Rule Builder: Fix rule hash collisions (#6169) * fix rule hash collisions * just let the dict do the work --- rule_builder/rules.py | 9 ++++----- test/general/test_rule_builder.py | 9 +++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/rule_builder/rules.py b/rule_builder/rules.py index 07c0607c1fb3..47f91aff5e16 100644 --- a/rule_builder/rules.py +++ b/rule_builder/rules.py @@ -36,7 +36,7 @@ def hash_impl(self: "Rule.Resolved") -> int: class CustomRuleRegister(type): """A metaclass to contain world custom rules and automatically convert resolved rules to frozen dataclasses""" - resolved_rules: ClassVar[dict[int, "Rule.Resolved"]] = {} + resolved_rules: ClassVar[dict["Rule.Resolved", "Rule.Resolved"]] = {} """A cached of resolved rules to turn each unique one into a singleton""" custom_rules: ClassVar[dict[str, dict[str, type["Rule[Any]"]]]] = {} @@ -64,10 +64,9 @@ def __new__( @override def __call__(cls, *args: Any, **kwds: Any) -> Any: rule = super().__call__(*args, **kwds) - rule_hash = hash(rule) - if rule_hash in cls.resolved_rules: - return cls.resolved_rules[rule_hash] - cls.resolved_rules[rule_hash] = rule + if rule in cls.resolved_rules: + return cls.resolved_rules[rule] + cls.resolved_rules[rule] = rule return rule @classmethod diff --git a/test/general/test_rule_builder.py b/test/general/test_rule_builder.py index 85e239175d4d..682c043f8e09 100644 --- a/test/general/test_rule_builder.py +++ b/test/general/test_rule_builder.py @@ -416,6 +416,15 @@ def test_has_all_hash(self) -> None: rule2 = HasAll("2", "2", "2", "1") self.assertEqual(hash(rule1.resolve(world)), hash(rule2.resolve(world))) + def test_hash_collision(self) -> None: + multiworld = setup_solo_multiworld(self.world_cls, steps=("generate_early",), seed=0) + world = multiworld.worlds[1] + rule1 = Has("A", count=1).resolve(world) + rule2 = Has("A", count=1 << 61).resolve(world) + self.assertEqual(hash(rule1), hash(rule2)) + self.assertNotEqual(rule1, rule2) + self.assertNotEqual(id(rule1), id(rule2)) + class TestCaching(CachedRuleBuilderTestCase): multiworld: MultiWorld # pyright: ignore[reportUninitializedInstanceVariable] From 15561e1e2d0192a2326f9d3e0c0c9165255170a1 Mon Sep 17 00:00:00 2001 From: Mrks <68022469+mrkssr@users.noreply.github.com> Date: Wed, 29 Apr 2026 20:49:19 +0200 Subject: [PATCH 12/66] Generate: Added "quantity" option for player yamls to use multiple times a single yaml (#4948) * Added option for player yamls. * Extended documentation * Minimized value extraction. * Added allow_quantity option to host.yaml * Added option for player yamls. * Extended documentation * Minimized value extraction. * Added allow_quantity option to host.yaml * Update settings.py Co-authored-by: qwint * Update Generate.py Co-authored-by: qwint * Added allow_quantity as application argument. * Quantity > 1, allow_quantity = false -> error instead of silent 1 * Added check for quantity <= 0; reverted settings import change * Update Generate.py --------- Co-authored-by: qwint Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- Generate.py | 12 +++++++++++- settings.py | 7 +++++++ worlds/generic/docs/advanced_settings_en.md | 5 ++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/Generate.py b/Generate.py index 509bf848d0d3..6855a6aaae63 100644 --- a/Generate.py +++ b/Generate.py @@ -40,6 +40,8 @@ def mystery_argparse(argv: list[str] | None = None) -> argparse.Namespace: parser.add_argument('--spoiler', type=int, default=defaults.spoiler) parser.add_argument('--outputpath', default=settings.general_options.output_path, help="Path to output folder. Absolute or relative to cwd.") # absolute or relative to cwd + parser.add_argument('--allow_quantity', action="store_true", default=defaults.allow_quantity, + help='Allows the use of the quantity option in yamls. Default is the set value in the host.yaml.') parser.add_argument('--race', action='store_true', default=defaults.race) parser.add_argument('--meta_file_path', default=defaults.meta_file_path) parser.add_argument('--log_level', default=defaults.loglevel, help='Sets log level') @@ -123,6 +125,7 @@ def main(args=None) -> tuple[argparse.Namespace, int]: player_id: int = 1 player_files: dict[int, str] = {} player_errors: list[str] = [] + allow_quantity = args.allow_quantity for file in os.scandir(args.player_files_path): fname = file.name if file.is_file() and not fname.startswith(".") and not fname.lower().endswith(".ini") and \ @@ -134,7 +137,14 @@ def main(args=None) -> tuple[argparse.Namespace, int]: if yaml is None: logging.warning(f"Ignoring empty yaml document #{doc_idx + 1} in {fname}") else: - weights_for_file.append(yaml) + quantity = yaml.get("quantity", 1) + if quantity <= 0: + raise ValueError("A quantity of 0 or less is invalid. Please change it to at least 1.") + if not allow_quantity and quantity > 1: + raise ValueError("Quantity greater than 1 is deactivated by host settings.") + + for _ in range(quantity): + weights_for_file.append(yaml) weights_cache[fname] = tuple(weights_for_file) except Exception as e: diff --git a/settings.py b/settings.py index 3b3d0deec73f..72ce53d92ca6 100644 --- a/settings.py +++ b/settings.py @@ -644,6 +644,12 @@ class PlayerFilesPath(OptionalUserFolderPath): class Players(int): """amount of players, 0 to infer from player files""" + class AllowQuantity(Bool): + """ + allow players to set an individual quantity for their yaml settings + with 'false' any amounts from the players will be ignored and set to 1 + """ + class WeightsFilePath(str): """ general weights file, within the stated player_files_path location @@ -690,6 +696,7 @@ class PanicMethod(str): enemizer_path: EnemizerPath = EnemizerPath("EnemizerCLI/EnemizerCLI.Core") # + ".exe" is implied on Windows player_files_path: PlayerFilesPath = PlayerFilesPath("Players") players: Players = Players(0) + allow_quantity: AllowQuantity | bool = False weights_file_path: WeightsFilePath = WeightsFilePath("weights.yaml") meta_file_path: MetaFilePath = MetaFilePath("meta.yaml") spoiler: Spoiler = Spoiler(3) diff --git a/worlds/generic/docs/advanced_settings_en.md b/worlds/generic/docs/advanced_settings_en.md index 7dc0e6ba4c9a..6348ce2a5840 100644 --- a/worlds/generic/docs/advanced_settings_en.md +++ b/worlds/generic/docs/advanced_settings_en.md @@ -60,7 +60,7 @@ adding more randomness and "mystery" to your options. Every configurable setting Currently, there are only a few options that are root options. Everything else should be nested within one of these root options or in some cases nested within other nested options. The only options that should exist in root -are `description`, `name`, `game`, `requires`, and the name of the games you want options for. +are `description`, `name`, `game`, `quantity`, `requires`, and the name of the games you want options for. * `description` is ignored by the generator and is simply a good way for you to organize if you have multiple files using this to detail the intention of the file. @@ -78,6 +78,9 @@ are `description`, `name`, `game`, `requires`, and the name of the games you wan * `game` is where either your chosen game goes or, if you would like, can be filled with multiple games each with different weights. +* `quantity` is the amount of times this yaml should be used when generating. This option is optional, the default value is 1. + To ensure that the name is unique with a value of at least two, the keywords from above must be used. + * `requires` details different requirements from the generator for the YAML to work as you expect it to. Generally this is good for detailing the version of Archipelago this YAML was prepared for. If it is rolled on an older version, options may be missing and as such it will not work as expected. If any plando is used in the file then requiring it From 01033940d6a915643293de1c0c5c3f35d2c93c47 Mon Sep 17 00:00:00 2001 From: Duck <31627079+duckboycool@users.noreply.github.com> Date: Wed, 29 Apr 2026 12:55:32 -0600 Subject: [PATCH 13/66] MMBN3: Add spaces in concatenated string#5689 --- MMBN3Client.py | 4 ++-- worlds/mmbn3/Rom.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/MMBN3Client.py b/MMBN3Client.py index 31c6b309b8d7..bd36fae4b806 100644 --- a/MMBN3Client.py +++ b/MMBN3Client.py @@ -241,8 +241,8 @@ async def gba_sync_task(ctx: MMBN3Context): await ctx.server_auth(False) else: if not ctx.version_warning: - logger.warning(f"Your Lua script is version {reported_version}, expected {script_version}." - "Please update to the latest version." + logger.warning(f"Your Lua script is version {reported_version}, expected {script_version}. " + "Please update to the latest version. " "Your connection to the Archipelago server will not be accepted.") ctx.version_warning = True except asyncio.TimeoutError: diff --git a/worlds/mmbn3/Rom.py b/worlds/mmbn3/Rom.py index 347375c50356..7cb26455a930 100644 --- a/worlds/mmbn3/Rom.py +++ b/worlds/mmbn3/Rom.py @@ -341,7 +341,7 @@ def get_base_rom_bytes(file_name: str = "") -> bytes: basemd5 = hashlib.md5() basemd5.update(base_rom_bytes) if CHECKSUM_BLUE != basemd5.hexdigest(): - raise Exception('Supplied Base Rom does not match US GBA Blue Version.' + raise Exception('Supplied Base Rom does not match US GBA Blue Version. ' 'Please provide the correct ROM version') get_base_rom_bytes.base_rom_bytes = base_rom_bytes From c851eff521ab4a8033357ae0817d34e84ae7248e Mon Sep 17 00:00:00 2001 From: el-u <109771707+el-u@users.noreply.github.com> Date: Wed, 29 Apr 2026 20:56:11 +0200 Subject: [PATCH 14/66] lufia2ac/docs: fix some errors (#5618) Co-authored-by: Ludwig <80899010+wordfcuk@users.noreply.github.com> --- worlds/lufia2ac/__init__.py | 2 +- worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md | 4 ++-- worlds/lufia2ac/docs/setup_en.md | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/worlds/lufia2ac/__init__.py b/worlds/lufia2ac/__init__.py index d5b104dde4b6..ae8f86262c77 100644 --- a/worlds/lufia2ac/__init__.py +++ b/worlds/lufia2ac/__init__.py @@ -48,7 +48,7 @@ class L2ACWorld(World): """ The Ancient Cave is a roguelike dungeon crawling game built into the RGP Lufia II. Face 99 floors of ever harder to beat monsters, - random items and find new companions on the way to face the Royal + random items and find new companions on the way to face the Master Jelly in the end. Can you beat it? """ game: ClassVar[str] = "Lufia II Ancient Cave" diff --git a/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md b/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md index 4b5bf3f318fa..c78fae05fd48 100644 --- a/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md +++ b/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md @@ -9,8 +9,8 @@ config file. As you may or may not know, randomization was already a core feature of the Ancient Cave in Lufia II, basically being a whole game within a game. The Ancient Cave has 99 floors with increasingly hard enemies, red chests and blue chests. At -the end of the Ancient Cave you get to fight the Royal Jelly... if you make it that far. You cannot lose the Royal -Jelly fight as it kills itself after giving you three rounds to try and kill it (or manage to vanquish your own party, +the end of the Ancient Cave you get to fight the Master Jelly... if you make it that far. The Master +Jelly gives you three rounds to try and kill it (or manage to vanquish your own party, whichever one you can manage). The Randomizer allows you to set different goals and modify the game in several other ways diff --git a/worlds/lufia2ac/docs/setup_en.md b/worlds/lufia2ac/docs/setup_en.md index adb6e4ff616d..761c6a138ad6 100644 --- a/worlds/lufia2ac/docs/setup_en.md +++ b/worlds/lufia2ac/docs/setup_en.md @@ -56,7 +56,6 @@ If you would like to validate your config file to make sure it works, you may do 4. You will be presented with a server page, from which you can download your patch file. 5. Double-click on your patch file, and SNIClient will launch automatically, create your ROM from the patch file, and open your emulator for you. -6. Since this is a single-player game, you will no longer need the client, so feel free to close it. ## Joining a MultiWorld Game From b716096a4b16d8d2aab9717859de0ecebbc087ac Mon Sep 17 00:00:00 2001 From: Oshroth Date: Thu, 30 Apr 2026 05:01:23 +1000 Subject: [PATCH 15/66] SML2: Send logic affecting randomiser options as slot_data (#5751) * Send logic affecting randomiser options as slot_data * SML2 - Universal Tracker support * Remove debug logs * Only check generation_is_fake once --------- Co-authored-by: alchav --- worlds/marioland2/__init__.py | 115 +++++++++++++++++++++++----------- worlds/marioland2/logic.py | 17 +++-- 2 files changed, 92 insertions(+), 40 deletions(-) diff --git a/worlds/marioland2/__init__.py b/worlds/marioland2/__init__.py index ea1354db6e65..82ce2ea5b0bf 100644 --- a/worlds/marioland2/__init__.py +++ b/worlds/marioland2/__init__.py @@ -56,6 +56,9 @@ class MarioLand2World(World): web = MarioLand2WebWorld() + ut_can_gen_without_yaml = True + glitches_item_name = "ut_glitch" + item_name_groups = { "Level Progression": { item_name for item_name in items if item_name.endswith(("Progression", "Secret", "Secret 1", "Secret 2")) @@ -94,42 +97,51 @@ def __init__(self, world, player: int): self.max_coin_locations = {} self.sprite_data = {} self.coin_fragments_required = 0 + self.ut = False def generate_early(self): - self.sprite_data = deepcopy(level_sprites) - if self.options.randomize_enemies: - randomize_enemies(self.sprite_data, self.random) - if self.options.randomize_platforms: - randomize_platforms(self.sprite_data, self.random) + if hasattr(self.multiworld, "re_gen_passthrough") and self.game in self.multiworld.re_gen_passthrough: + self.ut = True + for key, value in self.multiworld.re_gen_passthrough[self.game].items(): + if hasattr(self.options, key): + getattr(self.options, key).value = value + else: + setattr(self, key, value) + else: + self.sprite_data = deepcopy(level_sprites) - if self.options.marios_castle_midway_bell: - self.sprite_data["Mario's Castle"][35]["sprite"] = "Midway Bell" + if self.options.marios_castle_midway_bell: + self.sprite_data["Mario's Castle"][35]["sprite"] = "Midway Bell" + if self.options.randomize_enemies: + randomize_enemies(self.sprite_data, self.random) + if self.options.randomize_platforms: + randomize_platforms(self.sprite_data, self.random) - if self.options.auto_scroll_chances == "vanilla": - self.auto_scroll_levels = [int(i in [19, 25, 30]) for i in range(32)] - else: - self.auto_scroll_levels = [int(self.random.randint(1, 100) <= self.options.auto_scroll_chances) - for _ in range(32)] - - self.auto_scroll_levels[level_name_to_id["Mario's Castle"]] = 0 - unbeatable_scroll_levels = ["Tree Zone 3", "Macro Zone 2", "Space Zone 1", "Turtle Zone 2", "Pumpkin Zone 2"] - if not self.options.shuffle_midway_bells: - unbeatable_scroll_levels.append("Pumpkin Zone 1") - for level, i in enumerate(self.auto_scroll_levels): - if i == 1: - if self.options.auto_scroll_mode in ("global_cancel_item", "level_cancel_items"): - self.auto_scroll_levels[level] = 2 - elif self.options.auto_scroll_mode == "chaos": - if (self.options.accessibility == "full" - and level_id_to_name[level] in unbeatable_scroll_levels): + if self.options.auto_scroll_chances == "vanilla": + self.auto_scroll_levels = [int(i in [19, 25, 30]) for i in range(32)] + else: + self.auto_scroll_levels = [int(self.random.randint(1, 100) <= self.options.auto_scroll_chances) + for _ in range(32)] + + self.auto_scroll_levels[level_name_to_id["Mario's Castle"]] = 0 + unbeatable_scroll_levels = ["Tree Zone 3", "Macro Zone 2", "Space Zone 1", "Turtle Zone 2", "Pumpkin Zone 2"] + if not self.options.shuffle_midway_bells: + unbeatable_scroll_levels.append("Pumpkin Zone 1") + for level, i in enumerate(self.auto_scroll_levels): + if i == 1: + if self.options.auto_scroll_mode in ("global_cancel_item", "level_cancel_items"): self.auto_scroll_levels[level] = 2 - else: - self.auto_scroll_levels[level] = self.random.randint(1, 3) - elif (self.options.accessibility == "full" - and level_id_to_name[level] in unbeatable_scroll_levels): - self.auto_scroll_levels[level] = 0 - if self.auto_scroll_levels[level] == 1 and "trap" in self.options.auto_scroll_mode.current_key: - self.auto_scroll_levels[level] = 3 + elif self.options.auto_scroll_mode == "chaos": + if (self.options.accessibility == "full" + and level_id_to_name[level] in unbeatable_scroll_levels): + self.auto_scroll_levels[level] = 2 + else: + self.auto_scroll_levels[level] = self.random.randint(1, 3) + elif (self.options.accessibility == "full" + and level_id_to_name[level] in unbeatable_scroll_levels): + self.auto_scroll_levels[level] = 0 + if self.auto_scroll_levels[level] == 1 and "trap" in self.options.auto_scroll_mode.current_key: + self.auto_scroll_levels[level] = 3 def create_regions(self): menu_region = Region("Menu", self.player, self.multiworld) @@ -178,7 +190,10 @@ def create_regions(self): wario.place_locked_item(MarioLand2Item("Wario Defeated", ItemClassification.progression, None, self.player)) if self.options.coinsanity: - coinsanity_checks = self.options.coinsanity_checks.value + if hasattr(self.multiworld, "generation_is_fake"): + coinsanity_checks = self.options.coinsanity_checks.range_end + else: + coinsanity_checks = self.options.coinsanity_checks.value self.num_coin_locations = [[region, 1] for region in created_regions if region != "Mario's Castle"] self.max_coin_locations = {region: len(coins_coords[region]) for region in created_regions if region != "Mario's Castle"} @@ -423,12 +438,40 @@ def create_items(self): self.multiworld.itempool += [self.create_item(item_name) for _ in range(count)] def fill_slot_data(self): - return { - "energy_link": self.options.energy_link.value - } + # Expose settings for accurate tracker logic + shark_count: int = [ + self.multiworld.worlds[self.player].sprite_data["Turtle Zone 1"][i]["sprite"] + for i in (27, 28) + ].count("Shark") + crane_count: int = [ + self.multiworld.worlds[self.player].sprite_data["Mario Zone 3"][i]["sprite"] + for i in (17, 18, 25) + ].count("Claw Grabber") + options_dict = self.options.as_dict( + "energy_link", + "shuffle_golden_coins", + "required_golden_coins", + "coinsanity", + "shuffle_midway_bells", + "marios_castle_midway_bell", + "shuffle_pipe_traversal", + "auto_scroll_mode" + ) + options_dict.update({ + "auto_scroll_levels": self.auto_scroll_levels, + "turtle_zone_1_shark_count": shark_count, + "mario_zone_3_crane_count": crane_count, + "coin_fragments_required": self.coin_fragments_required + }) + return options_dict + + @staticmethod + def interpret_slot_data(slot_data): + return slot_data def create_item(self, name: str) -> Item: - return MarioLand2Item(name, items[name], self.item_name_to_id[name], self.player) + return MarioLand2Item(name, items[name] if name in items else ItemClassification.progression, + self.item_name_to_id[name] if name in self.item_name_to_id else None, self.player) def get_filler_item_name(self): return "1 Coin" diff --git a/worlds/marioland2/logic.py b/worlds/marioland2/logic.py index 4ccb2eb4c919..ea4fc7cdd3c3 100644 --- a/worlds/marioland2/logic.py +++ b/worlds/marioland2/logic.py @@ -5,6 +5,8 @@ def is_auto_scroll(state, player, level): level_id = level_name_to_id[level] if state.has_any(["Cancel Auto Scroll", f"Cancel Auto Scroll - {level}"], player): return False + if state.has("ut_glitch", player) and state.multiworld.worlds[player].auto_scroll_levels[level_id] == 3: + return state.has(f"Auto Scroll - {level}", player) return state.multiworld.worlds[player].auto_scroll_levels[level_id] > 0 @@ -287,8 +289,12 @@ def mario_zone_3_coins(state, player, coins): if state.has("Carrot", player): reachable_spike_coins = 15 else: - sprites = state.multiworld.worlds[player].sprite_data["Mario Zone 3"] - reachable_spike_coins = min(3, len({sprites[i]["sprite"] == "Claw Grabber" for i in (17, 18, 25)}) + if state.multiworld.worlds[player].ut: + claw_grabbers = state.multiworld.worlds[player].mario_zone_3_crane_count + else: + sprites = state.multiworld.worlds[player].sprite_data["Mario Zone 3"] + claw_grabbers = len({sprites[i]["sprite"] == "Claw Grabber" for i in (17, 18, 25)}) + reachable_spike_coins = min(3, claw_grabbers + state.has("Mushroom", player) + state.has("Fire Flower", player)) * 5 reachable_coins += reachable_spike_coins if not auto_scroll: @@ -309,8 +315,11 @@ def mario_zone_4_coins(state, player, coins): def not_blocked_by_sharks(state, player): - sharks = [state.multiworld.worlds[player].sprite_data["Turtle Zone 1"][i]["sprite"] - for i in (27, 28)].count("Shark") + if state.multiworld.worlds[player].ut: + sharks = state.multiworld.worlds[player].turtle_zone_1_shark_count + else: + sharks = [state.multiworld.worlds[player].sprite_data["Turtle Zone 1"][i]["sprite"] + for i in (27, 28)].count("Shark") if state.has("Carrot", player) or not sharks: return True if sharks == 2: From 59674926edc98ca758db7ff12a6ef4c6496bdd77 Mon Sep 17 00:00:00 2001 From: UCSA Date: Wed, 29 Apr 2026 21:07:32 +0200 Subject: [PATCH 16/66] Docs: added an italian translation of the setup guide for Muse Dash (#5834) * Create setup_it.md Created an italian translation of the setup for muse dash. I could not find direct references to translating a setup so I am assuming this is the correct way to submit a translation. While I am a native in italian and I would consider myself fluent in english, I am not a translator so mistakes could have been made. * Update __init__.py --- worlds/musedash/__init__.py | 11 ++++++- worlds/musedash/docs/setup_it.md | 55 ++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 worlds/musedash/docs/setup_it.md diff --git a/worlds/musedash/__init__.py b/worlds/musedash/__init__.py index 55767cf04b65..6e64adf02496 100644 --- a/worlds/musedash/__init__.py +++ b/worlds/musedash/__init__.py @@ -33,7 +33,16 @@ class MuseDashWebWorld(WebWorld): ["Shiny"] ) - tutorials = [setup_en, setup_es] + setup_it = Tutorial( + setup_en.tutorial_name, + setup_en.description, + "Italiano", + "setup_it.md", + "setup/it", + ["UCSA"] + ) + + tutorials = [setup_en, setup_es, setup_it] options_presets = MuseDashPresets option_groups = md_option_groups diff --git a/worlds/musedash/docs/setup_it.md b/worlds/musedash/docs/setup_it.md new file mode 100644 index 000000000000..22ace7bb51bf --- /dev/null +++ b/worlds/musedash/docs/setup_it.md @@ -0,0 +1,55 @@ +# Guida al Setup di Muse Dash per Archipelago + +## Links +- [Pagina Principale](../../../../games/Muse%20Dash/info/en) +- [Opzioni](../../../../games/Muse%20Dash/player-options) + +## Software Richiesto + +- Windows 8 o più recente. +- Muse Dash: [Disponibile su Steam](https://store.steampowered.com/app/774171/Muse_Dash/) + - \[Facoltativo\] DLC [Muse Plus]: [Disponibile su Steam](https://store.steampowered.com/app/2593750/Muse_Dash_Muse_Plus/) +- Melon Loader: [GitHub](https://github.com/LavaGang/MelonLoader/releases/latest) + - L'installer potrebbe richiedere .Net Framework 4.8: [Download](https://dotnet.microsoft.com/it-it/download/dotnet-framework/net48) +- .NET Desktop Runtime 6.0.XX (Se non installato in precedenza): [Download](https://dotnet.microsoft.com/it-it/download/dotnet/6.0) +- Muse Dash Archipelago Mod: [GitHub](https://github.com/DeamonHunter/ArchipelagoMuseDash/releases/latest) + +## Installare la mod di Archipelago per Muse Dash + +1. Scarica [MelonLoader.Installer.exe](https://github.com/LavaGang/MelonLoader/releases/latest) ed eseguilo. +2. Seleziona la scheda "automated", premi select e naviga fino a `MuseDash.exe`. + - Puoi trovare la cartella tramite steam premendo tasto destro sul gioco nella tua libreria e scegliendo *Gestisci→Sfoglia i file locali* + - Se clicki sulla barra nella parte superiore della finestra, che ti dice la cartella attuale, questa ti darà un percorso che potrai copiare. + Se copi questo percorso nella finestra creata da **MelonLoader** il programma navigherà automaticamente fino a quella cartella. +3. Seleziona v0.7.0. e premi "install". +4. Esegui il gioco una volta e aspetta fino alla comparsa del menù iniziale di Muse Dash prima di chiuderlo. +5. Scarica l'ultima versione della [Muse Dash Archipelago Mod](https://github.com/DeamonHunter/ArchipelagoMuseDash/releases/latest) + ed estraila nella cartella `/Mods/` appena creata nella cartella di installazione di Muse Dash. + - Tutti i file devono essere nella cartella `/Mods/` e non in una cartella al suo interno. + +Se hai installato tutto correttamente, dovrebbe apparire un bottone in basso a destra che ti permetterà di effettuare il login ad un server di Archipelago. + +## Generare una Sessione MultiWorld +1. Visita la pagina [Player Options](/games/Muse%20Dash/player-options) e configura a tuo piacimento le opzioni specifiche per il gioco. +2. Esporta il tuo file yaml e usalo per generare una nuova sessione randomizzata + - (Per istruizioni su come generare una nuova sessione di Archipelago, fai riferimento alla [Archipelago Web Guide](/tutorial/Archipelago/setup/en)) + +## Entrare in una Sessione MultiWorld + +1. Esegui Muse Dash e supera la schermata iniziale. Premi il bottone in basso a destra. +2. Inserisci i dettagli della sessione di Archipelago, come l'indirizzo del server con la sua porta (per esempio, archipelago.gg:38381), nome utente e password. +3. Se tutto è stato inserito correttamente, la finestra dovrebbe scomparire e dovrebbe apparire il menù principale. + Una volta entrato nella schermata di selezione canzoni dovrebbe esserne disponibile un numero ridotto. + +## Risoluzione Problemi + +### No Support Module Loaded + +Questo errore avviene quando Melon Loader non è in grado di trovare i file necessari per eseguire le mod. Generalmente ci sono due cause principali per questo errore: +un errore nella generazione dei file quando il gioco è stato avviato con Melon Loader per la prima volta o la rimozione di file dopo la generazione da parte di un antivirus. + +Per risolvere questo problema devi per prima cosa rimuovere Melon Loader da Muse Dash. +Puoi fare ciò eliminando la cartella di Melon Loader all'interno della cartella di Muse Dash, dopodichè puoi seguire nuovamente i passaggi per l'installazione. + +Se continui ad avere lo stesso problema e stai usando un antivirus, prova a disattivarlo temporaneamente quando esegui Muse Dash per la prima volta +o aggiungi la cartella di Muse Dash alla whitelist. From 78937054de3e5d7cfb473cb116048e7c8aab7648 Mon Sep 17 00:00:00 2001 From: Yussur Mustafa Oraji Date: Wed, 29 Apr 2026 21:07:52 +0200 Subject: [PATCH 17/66] sm64ex: Recommend Flatpak for Linux users (#5897) --- worlds/sm64ex/docs/setup_en.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/worlds/sm64ex/docs/setup_en.md b/worlds/sm64ex/docs/setup_en.md index 62669a65eb20..bb4040ae1dae 100644 --- a/worlds/sm64ex/docs/setup_en.md +++ b/worlds/sm64ex/docs/setup_en.md @@ -21,16 +21,16 @@ First, install [MSYS](https://www.msys2.org/) as described on the page. DO NOT I It is extremely encouraged to use the default install directory! Then continue to `Using the Launcher` -*Linux Preparations* +*Linux / SteamOS Preparations* -You will need to install some dependencies before using the launcher. -The launcher itself needs `qt6`, `patch` and `git`, and building the game requires `sdl2 glew cmake python make` (If you install `jsoncpp` as well, it will be linked dynamically). -Then continue to `Using the Launcher` +The easiest installation method is using the Flatpak. +If using Flatpak, no dependencies or other preparations are necessary. +Otherwise, install `qt6`, `patch`, `git`, `sdl2`, `glew`, `cmake`, `python` and `make`. *Using the Launcher* 1. Go to the page linked for SM64AP-Launcher, and press on the topmost entry. -2. Scroll down, and download the zip file for your OS. +2. Scroll down, and download the zip file for Windows, or the Flatpak for Linux / SteamOS. 3. Unpack the zip file in an empty folder. 4. Run the Launcher. On first start, press `Check Requirements`, which will guide you through the rest of the needed steps. - Windows: If you did not use the default install directory for MSYS, close this window, check `Show advanced options` and reopen using `Re-check Requirements`. You can then set the path manually. From 0601494e39caeb0fc82f6a49fe299acf61d6fac9 Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Wed, 29 Apr 2026 20:08:16 +0100 Subject: [PATCH 18/66] Pokemon RB: Fix mutating global item_groups in get_filler_item_name() (#5947) The `banned_items` variable was the `"Unique"`list within the `item_groups` global. `get_filler_item_name()` could then mutate `banned_items` through `banned_items.append("Poke Doll")` and `banned_items += item_groups["Vending Machine Drinks"]`, causing the contents of the `item_groups` global to be mutated. This has been fixed by making `banned_items` a shallow copy of `item_groups["Unique"]`. --- worlds/pokemon_rb/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index 0810ec5dc748..c8ecdeaefc57 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -629,7 +629,7 @@ def get_filler_item_name(self) -> str: if (combined_traps > 0 and self.random.randint(1, 100) <= self.options.trap_percentage.value): return self.select_trap() - banned_items = item_groups["Unique"] + banned_items = item_groups["Unique"].copy() if (((not self.options.tea) or "Saffron City" not in [self.fly_map, self.town_map_fly_map]) and (not self.options.door_shuffle)): # under these conditions, you should never be able to reach the Copycat or Pokémon Tower without being From 4346191c55ccf877aa2756d7601b5224798d54a7 Mon Sep 17 00:00:00 2001 From: Ixrec Date: Wed, 29 Apr 2026 20:10:26 +0100 Subject: [PATCH 19/66] Docs: Add a warning to world api.md#events that you want StatusUpdate, not events, for actually sending the goal (#5957) --- docs/world api.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/world api.md b/docs/world api.md index 4e2354930445..4d3a248b867f 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -327,6 +327,11 @@ reject the placement of an item there. ### Events (or "generation-only items/locations") +> **Warning:** If you're trying to tell the Archipelago server that the player has achieved their goal, you want to send +a [StatusUpdate packet](network%20protocol.md#statusupdate), or however [your client library](network%20protocol.md) +wraps it. Despite the popularity of "victory events" during generation, events have nothing to do with how goals are +triggered during gameplay. + An event item or location is one that only exists during multiworld generation; the server is never made aware of them. Event locations can never be checked by the player, and event items cannot be received during play. From d5cc2011384f3e03959e371c357faff9fd92d8f3 Mon Sep 17 00:00:00 2001 From: CookieCat <81494827+CookieCat45@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:11:36 -0400 Subject: [PATCH 20/66] AHIT: Update FAQ in setup guide (#5975) --- worlds/ahit/docs/setup_en.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/worlds/ahit/docs/setup_en.md b/worlds/ahit/docs/setup_en.md index 167c6c2faa24..214b76940f15 100644 --- a/worlds/ahit/docs/setup_en.md +++ b/worlds/ahit/docs/setup_en.md @@ -50,16 +50,17 @@ make sure ***Enable Developer Console*** is checked in Game Settings and press t ## FAQ/Common Issues -### The game is not connecting when starting a new save! -For unknown reasons, the mod will randomly disable itself in the mod menu. To fix this, go to the Mods menu -(rocket icon) in-game, and re-enable the mod. +### The game is crashing on startup repeatedly! +This is a common issue on older versions of the game, caused by the game failing to interface with the Steam Workshop. +To fix it you can try the following (from least to most effort required) +- Subscribe to any random workshop mod, then unsubscribe from it +- Restart Steam +- Restart your computer +- Delete the game's config directory from the files `steamapps/common/HatinTime/HatinTimeGame/Config` then verify the game files +- Reinstall the game ### Why do relics disappear from the stands in the Spaceship after they're completed? This is intentional behaviour. Because of how randomizer logic works, there is no way to predict the order that a player will place their relics. Since there are a limited amount of relic stands in the Spaceship, relics are removed after being completed to allow for the placement of more relics without being potentially locked out. The level that the relic set unlocked will stay unlocked. - -### When I start a new save file, the intro cinematic doesn't get skipped, Hat Kid's body is missing and the mod doesn't work! -There is a bug on older versions of A Hat in Time that causes save file creation to fail to work properly -if you have too many save files. Delete them and it should fix the problem. From 73856f63c84e51656b2c49083499d582e3dce542 Mon Sep 17 00:00:00 2001 From: Sophira Date: Wed, 29 Apr 2026 19:12:04 +0000 Subject: [PATCH 21/66] Docs: Mention APQuest as a reference (#6000) The APQuest implementation of an APWorld is incredibly useful and should really have a mention here. --- docs/adding games.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/adding games.md b/docs/adding games.md index a977109bde9f..b751ad193ba6 100644 --- a/docs/adding games.md +++ b/docs/adding games.md @@ -92,8 +92,9 @@ 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 -regarding the API can be found in the [world api doc](/docs/world%20api.md). Before publishing, make sure to also -check out [world maintainer.md](/docs/world%20maintainer.md). +regarding the API can be found in the [world api doc](/docs/world%20api.md), and the [APQuest](/worlds/apquest/) world +is a complete world implementation that functions as an introduction to world development. Before publishing, make sure +to also check out [world maintainer.md](/docs/world%20maintainer.md). ### Hard Requirements From a48bf0195f5c0687086746ea89d097f220eea741 Mon Sep 17 00:00:00 2001 From: Ian Robinson Date: Wed, 29 Apr 2026 15:14:34 -0400 Subject: [PATCH 22/66] Docs: Make RB OptionFilter docs clearer (#6028) * add more docs for option filters * reword the whole things * style guide --- docs/rule builder.md | 218 +++++++++++++++++++++++++++++++------------ 1 file changed, 159 insertions(+), 59 deletions(-) diff --git a/docs/rule builder.md b/docs/rule builder.md index c3a8fcb6c42b..ab4270c2611c 100644 --- a/docs/rule builder.md +++ b/docs/rule builder.md @@ -1,6 +1,7 @@ # Rule Builder -This document describes the API provided for the rule builder. Using this API provides you with with a simple interface to define rules and the following advantages: +This document describes the API provided for the rule builder. Using this API provides you with with a simple interface +to define rules and the following advantages: - Rule classes that avoid all the common pitfalls - Logic optimization @@ -12,13 +13,21 @@ This document describes the API provided for the rule builder. Using this API pr The rule builder consists of 3 main parts: -1. The rules, which are classes that inherit from `rule_builder.rules.Rule`. These are what you write for your logic. They can be combined and take into account your world's options. There are a number of default rules listed below, and you can create as many custom rules for your world as needed. When assigning the rules to a location or entrance they must be resolved. -1. Resolved rules, which are classes that inherit from `rule_builder.rules.Rule.Resolved`. These are the optimized rules specific to one player that are set as a location or entrance's access rule. You generally shouldn't be directly creating these but they'll be created when assigning rules to locations or entrances. These are what power the human-readable logic explanations. -1. The optional rule builder world subclass `CachedRuleBuilderWorld`, which is a class your world can inherit from instead of `World`. It adds a caching system to the rules that will lazy evaluate and cache the result. +1. The rules, which are classes that inherit from `rule_builder.rules.Rule`. These are what you write for your logic. + They can be combined and take into account your world's options. There are a number of default rules listed below, + and you can create as many custom rules for your world as needed. When assigning the rules to a location or entrance + they must be resolved. +2. Resolved rules, which are classes that inherit from `rule_builder.rules.Rule.Resolved`. These are the optimized rules + specific to one player that are set as a location or entrance's access rule. You generally shouldn't be directly + creating these but they'll be created when assigning rules to locations or entrances. These are what power the + human-readable logic explanations. +3. The optional rule builder world subclass `CachedRuleBuilderWorld`, which is a class your world can inherit from + instead of `World`. It adds a caching system to the rules that will lazy evaluate and cache the result. ## Usage -For the most part the only difference in usage is instead of writing lambdas for your logic, you write static Rule objects. You then must use `world.set_rule` to assign the rule to a location or entrance. +For the most part the only difference in usage is instead of writing lambdas for your logic, you write static Rule +objects. You then must use `world.set_rule` to assign the rule to a location or entrance. ```python # In your world's create_regions method @@ -40,18 +49,22 @@ The rule builder comes with a number of rules by default: - `HasFromList`: Checks that the player has some number of given items - `HasFromListUnique`: Checks that the player has some number of given items, ignoring duplicates of the same item - `HasGroup`: Checks that the player has some number of items from a given item group -- `HasGroupUnique`: Checks that the player has some number of items from a given item group, ignoring duplicates of the same item +- `HasGroupUnique`: Checks that the player has some number of items from a given item group, ignoring duplicates of the + same item - `CanReachLocation`: Checks that the player can logically reach the given location - `CanReachRegion`: Checks that the player can logically reach the given region - `CanReachEntrance`: Checks that the player can logically reach the given entrance -You can combine these rules together to describe the logic required for something. For example, to check if a player either has `Movement ability` or they have both `Key 1` and `Key 2`, you can do: +You can combine these rules together to describe the logic required for something. For example, to check if a player +either has `Movement ability` or they have both `Key 1` and `Key 2`, you can do: ```python rule = Has("Movement ability") | HasAll("Key 1", "Key 2") ``` -> ⚠️ Composing rules with the `and` and `or` keywords will not work. You must use the bitwise `&` and `|` operators. In order to catch mistakes, the rule builder will not let you do boolean operations. As a consequence, in order to check if a rule is defined you must use `if rule is not None`. +> ⚠️ Composing rules with the `and` and `or` keywords will not work. You must use the bitwise `&` and `|` operators. In +> order to catch mistakes, the rule builder will not let you do boolean operations. As a consequence, in order to check +> if a rule is defined you must use `if rule is not None`. ### Assigning rules @@ -61,13 +74,16 @@ When assigning the rule you must use the `set_rule` helper to correctly resolve self.set_rule(location_or_entrance, rule) ``` -There is also a `create_entrance` helper that will resolve the rule, check if it's `False`, and if not create the entrance and set the rule. This allows you to skip creating entrances that will never be valid. You can also specify `force_creation=True` if you would like to create the entrance even if the rule is `False`. +There is also a `create_entrance` helper that will resolve the rule, check if it's `False`, and if not create the +entrance and set the rule. This allows you to skip creating entrances that will never be valid. You can also specify +`force_creation=True` if you would like to create the entrance even if the rule is `False`. ```python self.create_entrance(from_region, to_region, rule) ``` -> ⚠️ If you use a `CanReachLocation` rule on an entrance, you will either have to create the locations first, or specify the location's parent region name with the `parent_region_name` argument of `CanReachLocation`. +> ⚠️ If you use a `CanReachLocation` rule on an entrance, you will either have to create the locations first, or specify +> the location's parent region name with the `parent_region_name` argument of `CanReachLocation`. You can also set a rule for your world's completion condition: @@ -77,21 +93,42 @@ self.set_completion_rule(rule) ### Restricting options -Every rule allows you to specify which options it's applicable for. You can provide the argument `options` which is an iterable of `OptionFilter` instances. Rules that pass the options check will be resolved as normal, and those that fail will be resolved as `False`. +Every rule allows you to specify which options it's applicable for. You can provide the argument `options` which is an +iterable of `OptionFilter` instances. When resolved, if no filters are provided or all of them pass then the rule will +resolve as normal. Otherwise, the rule will be replaced with `True` or `False` depending on what `filtered_resolution` +is set to, which defaults to `False`. -If you want a comparison that isn't equals, you can specify with the `operator` argument. The following operators are allowed: +```python +rule1 = Has( + "Fast Travel Spell", + options=[OptionFilter(RandoFastTravel, RandoFastTravel.option_true)], +) +rule2 = Has( + "Starting Party Member", + options=[OptionFilter(RandoParty, 1)], # option attributes are suggested but any value works + filtered_resolution=True, +) +``` -- `eq`: `==` -- `ne`: `!=` -- `gt`: `>` -- `lt`: `<` -- `ge`: `>=` -- `le`: `<=` -- `contains`: `in` +If you want a comparison that isn't equals, you can specify with the `operator` argument. The following operators are +allowed: -By default rules that are excluded by their options will default to `False`. If you want to default to `True` instead, you can specify `filtered_resolution=True` on your rule. +- `eq`: `option_value == filter_value` +- `ne`: `option_value != filter_value` +- `gt`: `option_value > filter_value` +- `lt`: `option_value < filter_value` +- `ge`: `option_value >= filter_value` +- `le`: `option_value <= filter_value` +- `in`: `option_value in filter_value` +- `contains`: `filter_value in option_value` (note reversed operands) -To check if the player can reach a switch, or if they've received the switch item if switches are randomized: +```python +rule1 = Has("Movement Ability", options=[OptionFilter(SkipsLevel, SkipsLevel.option_hard, operator="lt")]) +rule2 = Has("Item", options=[OptionFilter(ChoiceOption, [1, 5], operator="in")]) +``` + +To check if the player has received the switch item if switches are randomized, or if they can reach the switch when not +randomized: ```python rule = ( @@ -120,7 +157,7 @@ rule = ( ) ``` -You can also use the & and | operators to apply options to rules: +For convenience, you can also use the `&` and `|` operators to apply options to rules: ```python common_rule = Has("A") @@ -129,14 +166,22 @@ common_rule_only_on_easy = common_rule & easy_filter common_rule_skipped_on_easy = common_rule | easy_filter ``` +Combining the above, you can easily bypass a requirement based on option choices: + +```python +rule = Has("Some Upgrade") | OptionFilter(CombatDifficulty, CombatDifficulty.option_medium, operator="ge") +``` + ### 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. +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 +- `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 @@ -148,7 +193,8 @@ rule = ( # 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: +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) @@ -163,24 +209,30 @@ class FromCustomResolution(FieldResolver, game="MyGame"): 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`. +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. +The rule builder provides a `CachedRuleBuilderWorld` base class for your `World` class that enables caching on your +rules. ```python class MyWorld(CachedRuleBuilderWorld): game = "My Game" ``` -If your world's logic is very simple and you don't have many nested rules, the caching system may have more overhead cost than time it saves. You'll have to benchmark your own world to see if it should be enabled or not. +If your world's logic is very simple and you don't have many nested rules, the caching system may have more overhead +cost than time it saves. You'll have to benchmark your own world to see if it should be enabled or not. ### Item name mapping -If you have multiple real items that map to a single logic item, add a `item_mapping` class dict to your world that maps actual item names to real item names so the cache system knows what to invalidate. +If you have multiple real items that map to a single logic item, add a `item_mapping` class dict to your world that maps +actual item names to real item names so the cache system knows what to invalidate. -For example, if you have multiple `Currency x` items on locations, but your rules only check a singular logical `Currency` item, eg `Has("Currency", 1000)`, you'll want to map each numerical currency item to the single logical `Currency`. +For example, if you have multiple `Currency x` items on locations, but your rules only check a singular logical +`Currency` item, eg `Has("Currency", 1000)`, you'll want to map each numerical currency item to the single logical +`Currency`. ```python class MyWorld(CachedRuleBuilderWorld): @@ -194,9 +246,13 @@ class MyWorld(CachedRuleBuilderWorld): ## Defining custom rules -You can create a custom rule by creating a class that inherits from `Rule` or any of the default rules. You must provide the game name as an argument to the class. It's recommended to use the `@dataclass` decorator to reduce boilerplate, and to also provide your world as a type argument to add correct type checking to the `_instantiate` method. +You can create a custom rule by creating a class that inherits from `Rule` or any of the default rules. You must provide +the game name as an argument to the class. It's recommended to use the `@dataclass` decorator to reduce boilerplate, and +to also provide your world as a type argument to add correct type checking to the `_instantiate` method. -You must provide or inherit a `Resolved` child class that defines an `_evaluate` method. This class will automatically be converted into a frozen `dataclass`. If your world has caching enabled you may need to define one or more dependencies functions as outlined below. +You must provide or inherit a `Resolved` child class that defines an `_evaluate` method. This class will automatically +be converted into a frozen `dataclass`. If your world has caching enabled you may need to define one or more +dependencies functions as outlined below. To add a rule that checks if the user has enough mcguffins to goal, with a randomized requirement: @@ -245,7 +301,10 @@ class ComplicatedFilter(Rule["MyWorld"], game="My Game"): ### Item dependencies -If your world inherits from `CachedRuleBuilderWorld` and there are items that when collected will affect the result of your rule evaluation, it must define an `item_dependencies` function that returns a mapping of the item name to the id of your rule. These dependencies will be combined to inform the caching system. It may be worthwhile to define this function even when caching is disabled as more things may use it in the future. +If your world inherits from `CachedRuleBuilderWorld` and there are items that when collected will affect the result of +your rule evaluation, it must define an `item_dependencies` function that returns a mapping of the item name to the id +of your rule. These dependencies will be combined to inform the caching system. It may be worthwhile to define this +function even when caching is disabled as more things may use it in the future. ```python @dataclasses.dataclass() @@ -262,7 +321,10 @@ All of the default `Has*` rules define this function already. ### Region dependencies -If your custom rule references other regions, it must define a `region_dependencies` function that returns a mapping of region names to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the caching system if applicable. +If your custom rule references other regions, it must define a `region_dependencies` function that returns a mapping of +region names to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These +dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the +caching system if applicable. ```python @dataclasses.dataclass() @@ -279,7 +341,10 @@ The default `CanReachLocation`, `CanReachRegion`, and `CanReachEntrance` rules d ### Location dependencies -If your custom rule references other locations, it must define a `location_dependencies` function that returns a mapping of the location name to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the caching system if applicable. +If your custom rule references other locations, it must define a `location_dependencies` function that returns a mapping +of the location name to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These +dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the +caching system if applicable. ```python @dataclasses.dataclass() @@ -296,7 +361,10 @@ The default `CanReachLocation` rule defines this function already. ### Entrance dependencies -If your custom rule references other entrances, it must define a `entrance_dependencies` function that returns a mapping of the entrance name to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the caching system if applicable. +If your custom rule references other entrances, it must define a `entrance_dependencies` function that returns a mapping +of the entrance name to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These +dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the +caching system if applicable. ```python @dataclasses.dataclass() @@ -313,9 +381,13 @@ The default `CanReachEntrance` rule defines this function already. ### Rule explanations -Resolved rules have a default implementation for `explain_json` and `explain_str` functions. The former optionally accepts a `CollectionState` and returns a list of `JSONMessagePart` appropriate for `print_json` in a client. It will display a human-readable message that explains what the rule requires. The latter is similar but returns a string. It is useful when debugging. There is also a `__str__` method defined to check what a rule is without a state. +Resolved rules have a default implementation for `explain_json` and `explain_str` functions. The former optionally +accepts a `CollectionState` and returns a list of `JSONMessagePart` appropriate for `print_json` in a client. It will +display a human-readable message that explains what the rule requires. The latter is similar but returns a string. It is +useful when debugging. There is also a `__str__` method defined to check what a rule is without a state. -To implement a custom message with a custom rule, override the `explain_json` and/or `explain_str` method on your `Resolved` class: +To implement a custom message with a custom rule, override the `explain_json` and/or `explain_str` method on your +`Resolved` class: ```python class MyRule(Rule, game="My Game"): @@ -352,22 +424,35 @@ class MyRule(Rule, game="My Game"): ### Cache control -By default your custom rule will work through the cache system as any other rule if caching is enabled. There are two class attributes on the `Resolved` class you can override to change this behavior. +By default your custom rule will work through the cache system as any other rule if caching is enabled. There are two +class attributes on the `Resolved` class you can override to change this behavior. -- `force_recalculate`: Setting this to `True` will cause your custom rule to skip going through the caching system and always recalculate when being evaluated. When a rule with this flag enabled is composed with `And` or `Or` it will cause any parent rules to always force recalculate as well. Use this flag when it's difficult to determine when your rule should be marked as stale. -- `skip_cache`: Setting this to `True` will also cause your custom rule to skip going through the caching system when being evaluated. However, it will **not** affect any other rules when composed with `And` or `Or`, so it must still define its `*_dependencies` functions as required. Use this flag when the evaluation of this rule is trivial and the overhead of the caching system will slow it down. +- `force_recalculate`: Setting this to `True` will cause your custom rule to skip going through the caching system and + always recalculate when being evaluated. When a rule with this flag enabled is composed with `And` or `Or` it will + cause any parent rules to always force recalculate as well. Use this flag when it's difficult to determine when your + rule should be marked as stale. +- `skip_cache`: Setting this to `True` will also cause your custom rule to skip going through the caching system when + being evaluated. However, it will **not** affect any other rules when composed with `And` or `Or`, so it must still + define its `*_dependencies` functions as required. Use this flag when the evaluation of this rule is trivial and the + overhead of the caching system will slow it down. ### Caveats -- Ensure you are passing `caching_enabled=True` in your `_instantiate` function when creating resolved rule instances if your world has opted into caching. +- Ensure you are passing `caching_enabled=True` in your `_instantiate` function when creating resolved rule instances if + your world has opted into caching. - Resolved rules are forced to be frozen dataclasses. They and all their attributes must be immutable and hashable. -- If your rule creates child rules ensure they are being resolved through the world rather than creating `Resolved` instances directly. +- If your rule creates child rules ensure they are being resolved through the world rather than creating `Resolved` + instances directly. ## Serialization -The rule builder is intended to be written first in Python for optimization and type safety. To facilitate exporting the rules to a client or tracker, rules have a `to_dict` method that returns a JSON-compatible dict. Since the location and entrance logic structure varies greatly from world to world, the actual JSON dumping is left up to the world dev. +The rule builder is intended to be written first in Python for optimization and type safety. To facilitate exporting the +rules to a client or tracker, rules have a `to_dict` method that returns a JSON-compatible dict. Since the location and +entrance logic structure varies greatly from world to world, the actual JSON dumping is left up to the world dev. -The dict contains a `rule` key with the name of the rule, an `options` key with the rule's list of option filters, and an `args` key that contains any other arguments the individual rule has. For example, this is what a simple `Has` rule would look like: +The dict contains a `rule` key with the name of the rule, an `options` key with the rule's list of option filters, and +an `args` key that contains any other arguments the individual rule has. For example, this is what a simple `Has` rule +would look like: ```python { @@ -380,7 +465,8 @@ The dict contains a `rule` key with the name of the rule, an `options` key with } ``` -For `And` and `Or` rules, instead of an `args` key, they have a `children` key containing a list of their child rules in the same serializable format: +For `And` and `Or` rules, instead of an `args` key, they have a `children` key containing a list of their child rules in +the same serializable format: ```python { @@ -464,7 +550,8 @@ class BasicLogicRule(Rule, game="My Game"): } ``` -If your logic has been done in custom JSON first, you can define a `from_dict` class method on your rules to parse it correctly: +If your logic has been done in custom JSON first, you can define a `from_dict` class method on your rules to parse it +correctly: ```python class BasicLogicRule(Rule, game="My Game"): @@ -485,10 +572,14 @@ These are properties and helpers that are available to you in your world. #### Methods - `rule_from_dict(data)`: Create a rule instance from a deserialized dict representation -- `register_rule_builder_dependencies()`: Register all rules that depend on location or entrance access with the inherited dependencies, gets called automatically after set_rules -- `set_rule(spot: Location | Entrance, rule: Rule)`: Resolve a rule, register its dependencies, and set it on the given location or entrance +- `register_rule_builder_dependencies()`: Register all rules that depend on location or entrance access with the + inherited dependencies, gets called automatically after set_rules +- `set_rule(spot: Location | Entrance, rule: Rule)`: Resolve a rule, register its dependencies, and set it on the given + location or entrance - `set_completion_rule(rule: Rule)`: Sets the completion condition for this world -- `create_entrance(from_region: Region, to_region: Region, rule: Rule | None, name: str | None = None, force_creation: bool = False)`: Attempt to create an entrance from `from_region` to `to_region`, skipping creation if `rule` is defined and evaluates to `False_()` unless force_creation is `True` +- `create_entrance(from_region: Region, to_region: Region, rule: Rule | None, name: str | None = None, force_creation: bool = False)`: + Attempt to create an entrance from `from_region` to `to_region`, skipping creation if `rule` is defined and evaluates + to `False_()` unless force_creation is `True` #### CachedRuleBuilderWorld Properties @@ -501,18 +592,27 @@ The following property is only available when inheriting from `CachedRuleBuilder These are properties and helpers that you can use or override for custom rules. - `_instantiate(world: World)`: Create a new resolved rule instance, override for custom rules as required -- `to_dict()`: Create a JSON-compatible dict representation of this rule, override if you want to customize your rule's serialization -- `from_dict(data, world_cls: type[World])`: Return a new rule instance from a deserialized representation, override if you've overridden `to_dict` +- `to_dict()`: Create a JSON-compatible dict representation of this rule, override if you want to customize your rule's + serialization +- `from_dict(data, world_cls: type[World])`: Return a new rule instance from a deserialized representation, override if + you've overridden `to_dict` - `__str__()`: Basic string representation of a rule, useful for debugging #### Resolved rule API - `player: int`: The slot this rule is resolved for -- `_evaluate(state: CollectionState)`: Evaluate this rule against the given state, override this to define the logic for this rule -- `item_dependencies()`: A mapping of item name to set of ids, override this if your custom rule depends on item collection -- `region_dependencies()`: A mapping of region name to set of ids, override this if your custom rule depends on reaching regions -- `location_dependencies()`: A mapping of location name to set of ids, override this if your custom rule depends on reaching locations -- `entrance_dependencies()`: A mapping of entrance name to set of ids, override this if your custom rule depends on reaching entrances -- `explain_json(state: CollectionState | None = None)`: Return a list of printJSON messages describing this rule's logic (and if state is defined its evaluation) in a human readable way, override to explain custom rules -- `explain_str(state: CollectionState | None = None)`: Return a string describing this rule's logic (and if state is defined its evaluation) in a human readable way, override to explain custom rules, more useful for debugging +- `_evaluate(state: CollectionState)`: Evaluate this rule against the given state, override this to define the logic for + this rule +- `item_dependencies()`: A mapping of item name to set of ids, override this if your custom rule depends on item + collection +- `region_dependencies()`: A mapping of region name to set of ids, override this if your custom rule depends on reaching + regions +- `location_dependencies()`: A mapping of location name to set of ids, override this if your custom rule depends on + reaching locations +- `entrance_dependencies()`: A mapping of entrance name to set of ids, override this if your custom rule depends on + reaching entrances +- `explain_json(state: CollectionState | None = None)`: Return a list of printJSON messages describing this rule's logic + (and if state is defined its evaluation) in a human readable way, override to explain custom rules +- `explain_str(state: CollectionState | None = None)`: Return a string describing this rule's logic (and if state is + defined its evaluation) in a human readable way, override to explain custom rules, more useful for debugging - `__str__()`: A string describing this rule's logic without its evaluation, override to explain custom rules From af39d7926a2d1943103f095568d5a2147aedc358 Mon Sep 17 00:00:00 2001 From: threeandthreee Date: Wed, 29 Apr 2026 15:15:57 -0400 Subject: [PATCH 23/66] LADX: doc update (#6050) * update gfxmod doc * romless generation * Update setup_en.md Co-authored-by: RoobyRoo --------- Co-authored-by: RoobyRoo --- worlds/ladx/__init__.py | 7 ++++--- worlds/ladx/docs/setup_en.md | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index e36ff77a2484..d19ddab0b795 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -80,9 +80,10 @@ class DisplayMsgs(settings.Bool): class GfxModFile(settings.FilePath): """ - Gfxmod file, get it from upstream: https://github.com/daid/LADXR/tree/master/gfx - Only .bin or .bdiff files - The same directory will be checked for a matching text modification file + Gfxmod file, select one from the `Archipelago/data/sprites/ladx/` folder, + or make your own. You can generate a template here: https://ladx-gfx.3and3.dev/ + Only .bin or .bdiff files. + Extended spritesheets from the upstream randomizer are not supported. """ def browse(self, filetypes=None, **kwargs): filetypes = [("Binary / Patch files", [".bin", ".bdiff"])] diff --git a/worlds/ladx/docs/setup_en.md b/worlds/ladx/docs/setup_en.md index a7f9f87ef9ad..ed4c0921ef83 100644 --- a/worlds/ladx/docs/setup_en.md +++ b/worlds/ladx/docs/setup_en.md @@ -12,8 +12,8 @@ 1. Download and install [Archipelago](). **The installer file is located in the assets section at the bottom of the version information.** -2. The first time you do local generation or patch your game, you will be asked to locate your base ROM file. - This is your Links Awakening DX ROM file. This only needs to be done once.. +2. The first time you patch your game, you will be asked to locate your base ROM file. + This is your Links Awakening DX ROM file. This only needs to be done once. 3. You should assign your emulator as your default program for launching ROM files. From a0236b2d74044a1bfbbfd41807f2f125ea0d36e9 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:18:21 -0400 Subject: [PATCH 24/66] Jak1: yamlless UT support (#6066) --- worlds/jakanddaxter/__init__.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index 9a2cb30293f0..f51545aa57cd 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -232,6 +232,9 @@ class JakAndDaxterWorld(World): power_cell_thresholds_minus_one: list[int] trap_weights: tuple[list[str], list[int]] + # UT Yaml-less flag + ut_can_gen_without_yaml = True + # Store these dictionaries for speed improvements. level_to_regions: dict[str, list[JakAndDaxterRegion]] # Contains all levels and regions. level_to_orb_regions: dict[str, list[JakAndDaxterRegion]] # Contains only regions which contain orbs. @@ -243,6 +246,15 @@ def generate_early(self) -> None: self.level_to_regions = defaultdict(list) self.level_to_orb_regions = defaultdict(list) + # Implement Universal Tracker support - reset all options to those from UT's gen if applicable. + if hasattr(self.multiworld, "re_gen_passthrough"): + if jak1_name in self.multiworld.re_gen_passthrough: + for key, val in self.multiworld.re_gen_passthrough[jak1_name].items(): + try: + getattr(self.options, key).value = val + except AttributeError: + pass + # Cache the power cell threshold values for quicker reference. self.power_cell_thresholds = [ self.options.fire_canyon_cell_count.value, From be9c7b1728ea1a52dfcf8bce6f6e286d7d6303c1 Mon Sep 17 00:00:00 2001 From: River Buizel <4911928+rocket0634@users.noreply.github.com> Date: Wed, 29 Apr 2026 12:18:59 -0700 Subject: [PATCH 25/66] KH2: Update setup_en.md (#6129) --- worlds/kh2/docs/setup_en.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/kh2/docs/setup_en.md b/worlds/kh2/docs/setup_en.md index 55bdaa5bf175..66b648a4accc 100644 --- a/worlds/kh2/docs/setup_en.md +++ b/worlds/kh2/docs/setup_en.md @@ -14,7 +14,7 @@ Kingdom Hearts II Final Mix from the [Epic Games Store](https://store.epicgames. 2. Lua Backend from the OpenKH Mod Manager 3. Install the mod `KH2FM-Mods-Num/GoA-ROM-Edition` using OpenKH Mod Manager - Needed for Archipelago - 1. [ArchipelagoKH2Client.exe](https://github.com/ArchipelagoMW/Archipelago/releases) + 1. [Archipelago Launcher](https://github.com/ArchipelagoMW/Archipelago/releases) 2. Install the Archipelago Companion mod from `JaredWeakStrike/APCompanion` using OpenKH Mod Manager 3. Install the mod from `TopazTK/KH2-ArchipelagoEnablers` using OpenKH Mod manager 1. Do Note that if you have `KH2FM-Mods-equations19/auto-save` OR `KH2FM-Mods-equations19/soft-reset` you should download `TopazTK/KH2-ArchipelagoEnablersLITE` instead @@ -58,7 +58,7 @@ After Installing the seed click "Mod Loader -> Build/Build and Run". Every slot ## Using the KH2 Client -Start the game through OpenKH Mod Manager. If starting a new run, enter the Garden of Assemblage from a new save. If returning to a run, load the save and enter the Garden of Assemblage. Then run the [ArchipelagoKH2Client.exe](https://github.com/ArchipelagoMW/Archipelago/releases).
+Start the game through OpenKH Mod Manager. If starting a new run, enter the Garden of Assemblage from a new save. If returning to a run, load the save and enter the Garden of Assemblage. Then run the [ArchipelagoLauncher.exe](https://github.com/ArchipelagoMW/Archipelago/releases) and select the KH2 Client.
When you successfully connect to the server the client will automatically hook into the game to send/receive checks.
If the client ever loses connection to the game, it will also disconnect from the server and you will need to reconnect.
From 31f5a6c4ea8a60a970b8455812b8c1d3b531a751 Mon Sep 17 00:00:00 2001 From: Sophia Caspe Date: Wed, 29 Apr 2026 12:19:32 -0700 Subject: [PATCH 26/66] Docs: Rule Builder Filtered Example Fix (#6152) --- docs/rule builder.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/rule builder.md b/docs/rule builder.md index ab4270c2611c..829ab763d73d 100644 --- a/docs/rule builder.md +++ b/docs/rule builder.md @@ -152,8 +152,8 @@ If you would like to provide option filters when reusing or composing rules, you common_rule = Has("A") | HasAny("B", "C") ... rule = ( - Filtered(common_rule, options=[OptionFilter(Opt, 0)]), - | Filtered(Has("X") | CanReachRegion("Y"), options=[OptionFilter(Opt, 1)]), + Filtered(common_rule, options=[OptionFilter(Opt, 0)]) + | Filtered(Has("X") | CanReachRegion("Y"), options=[OptionFilter(Opt, 1)]) ) ``` From 74a0cd60223c33af068b76397a85d2eb5cce32d1 Mon Sep 17 00:00:00 2001 From: qwint Date: Wed, 29 Apr 2026 14:21:42 -0500 Subject: [PATCH 27/66] MultiServer: update help doc on auto_shutdown#5917 --- MultiServer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index ed14b6506ff5..28ab853cccf6 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -2633,8 +2633,8 @@ def parse_args() -> argparse.Namespace: goal: !remaining can be used after goal completion ''') parser.add_argument('--auto_shutdown', default=defaults["auto_shutdown"], type=int, - help="automatically shut down the server after this many minutes without new location checks. " - "0 to keep running. Not yet implemented.") + help="automatically shut down the server after this many seconds without new location checks. " + "0 to keep running.") parser.add_argument('--use_embedded_options', action="store_true", help='retrieve release, remaining and hint options from the multidata file,' ' instead of host.yaml') From 8ac335f92db9d7710dc69742ed961990776761e8 Mon Sep 17 00:00:00 2001 From: Ixrec Date: Wed, 29 Apr 2026 20:22:35 +0100 Subject: [PATCH 28/66] Docs: Rewrite get_filler_item_name's docstring to avoid implying it's for filling all unfilled locations #5956 --- worlds/AutoWorld.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 04f0b61ff8b9..ddc8ff2d2a4d 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -512,7 +512,9 @@ def create_item(self, name: str) -> "Item": def get_filler_item_name(self) -> str: """ - Called when the item pool needs to be filled with additional items to match location count. + If core AP removes an item from your item pool, this method is called to choose a replacement item + so item count and location count remain equal. + For example: plando, item_links and start_inventory_from_pool are features that may cause this. Any returned item name must be for a "repeatable" item, i.e. one that it's okay to generate arbitrarily many of. For most worlds this will be one or more of your filler items, but the classification of these items From d36ba62243978185a97e67a1e04d3c51801ee36d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Bolduc?= <16137441+Jouramie@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:24:10 -0400 Subject: [PATCH 29/66] The Messenger: Add missing rules for Key of Strength#6124 --- worlds/messenger/connections.py | 5 ++++- worlds/messenger/regions.py | 2 +- worlds/messenger/rules.py | 14 +++++++------- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/worlds/messenger/connections.py b/worlds/messenger/connections.py index 84f7f9b24281..490803e9596f 100644 --- a/worlds/messenger/connections.py +++ b/worlds/messenger/connections.py @@ -316,7 +316,7 @@ "Searing Mega Shard Shop": [ "Searing Crags - Falling Rocks Shop", "Searing Crags - Before Final Climb Shop", - "Searing Crags - Key of Strength Shop", + "Searing Crags - Key of Strength Room", ], "Before Final Climb Shop": [ "Searing Crags - Raining Rocks Checkpoint", @@ -330,6 +330,9 @@ "Searing Crags - Top", ], "Key of Strength Shop": [ + "Searing Crags - Key of Strength Room", + ], + "Key of Strength Room": [ "Searing Crags - Searing Mega Shard Shop", ], "Triple Ball Spinner Checkpoint": [ diff --git a/worlds/messenger/regions.py b/worlds/messenger/regions.py index d53b84fe3401..eb668356ca08 100644 --- a/worlds/messenger/regions.py +++ b/worlds/messenger/regions.py @@ -96,7 +96,7 @@ "Searing Crags - Power Thistle", "Searing Crags - Astral Tea Leaves", ], - "Searing Crags - Key of Strength Shop": [ + "Searing Crags - Key of Strength Room": [ "Searing Crags - Key of Strength", ], "Searing Crags - Portal": [ diff --git a/worlds/messenger/rules.py b/worlds/messenger/rules.py index bc1fc6aa9989..2f1299932fc5 100644 --- a/worlds/messenger/rules.py +++ b/worlds/messenger/rules.py @@ -108,17 +108,18 @@ def __init__(self, world: "MessengerWorld") -> None: "Searing Crags - Right -> Searing Crags - Portal": lambda state: self.has_tabi(state) and self.has_wingsuit(state), "Searing Crags - Colossuses Shop -> Searing Crags - Key of Strength Shop": - lambda state: state.has("Power Thistle", self.player) - and (self.has_dart(state) - or (self.has_wingsuit(state) - and self.can_destroy_projectiles(state))), + lambda state: state.has("Power Thistle", self.player), + "Searing Crags - Key of Strength Shop -> Searing Crags - Key of Strength Room": + lambda state: self.has_dart(state) + or (self.has_wingsuit(state) + and self.can_destroy_projectiles(state)), "Searing Crags - Falling Rocks Shop -> Searing Crags - Searing Mega Shard Shop": self.has_dart, "Searing Crags - Searing Mega Shard Shop -> Searing Crags - Before Final Climb Shop": lambda state: self.has_dart(state) or self.can_destroy_projectiles(state), "Searing Crags - Searing Mega Shard Shop -> Searing Crags - Falling Rocks Shop": self.has_dart, - "Searing Crags - Searing Mega Shard Shop -> Searing Crags - Key of Strength Shop": + "Searing Crags - Searing Mega Shard Shop -> Searing Crags - Key of Strength Room": self.false, "Searing Crags - Before Final Climb Shop -> Searing Crags - Colossuses Shop": self.has_dart, @@ -406,7 +407,7 @@ def __init__(self, world: "MessengerWorld") -> None: lambda state: self.has_dart(state) or (self.can_destroy_projectiles(state) and (self.has_wingsuit(state) or self.can_dboost(state))), - "Searing Crags - Searing Mega Shard Shop -> Searing Crags - Key of Strength Shop": + "Searing Crags - Searing Mega Shard Shop -> Searing Crags - Key of Strength Room": lambda state: self.can_leash(state) or self.has_windmill(state), "Searing Crags - Before Final Climb Shop -> Searing Crags - Colossuses Shop": self.true, @@ -512,7 +513,6 @@ def __init__(self, world: "MessengerWorld") -> None: self.location_rules = { "Bamboo Creek - Claustro": self.has_wingsuit, - "Searing Crags - Key of Strength": self.has_wingsuit, "Sunken Shrine - Key of Love": lambda state: state.has_all({"Sun Crest", "Moon Crest"}, self.player), "Searing Crags - Pyro": self.has_tabi, "Underworld - Key of Chaos": self.has_tabi, From fd8f3fcd8ba83f7fe881203de182161f92e9d435 Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Wed, 29 Apr 2026 20:24:41 +0100 Subject: [PATCH 30/66] The Messenger: Add missing indirect conditions (#6101) --- worlds/messenger/rules.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/worlds/messenger/rules.py b/worlds/messenger/rules.py index 2f1299932fc5..23f2bf32c0a2 100644 --- a/worlds/messenger/rules.py +++ b/worlds/messenger/rules.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING -from BaseClasses import CollectionState, CollectionRule +from BaseClasses import CollectionState, CollectionRule, Region from worlds.generic.Rules import add_rule, allow_self_locking_items from .constants import NOTES, PHOBEKINS from .options import MessengerAccessibility @@ -13,6 +13,7 @@ class MessengerRules: player: int world: "MessengerWorld" connection_rules: dict[str, CollectionRule] + indirect_conditions: dict[str, list[Region]] region_rules: dict[str, CollectionRule] location_rules: dict[str, CollectionRule] maximum_price: int @@ -220,6 +221,16 @@ def __init__(self, world: "MessengerWorld") -> None: lambda state: self.can_dboost(state) or self.has_dart(state), } + # dict of connection names and the regions checked in the requirements to traverse the exit + self.indirect_conditions = { + "Howling Grotto - Breezy Crushers Checkpoint -> Howling Grotto - Crushing Pits Shop": [ + self.world.get_region("Howling Grotto - Emerald Golem Shop") + ], + "Glacial Peak - Left -> Elemental Skylands - Air Shmup": [ + self.world.get_location("Quillshroom Marsh - Queen of Quills").parent_region + ], + } + self.location_rules = { # hq "Money Wrench": self.can_shop, @@ -365,6 +376,8 @@ def set_messenger_rules(self) -> None: for entrance_name, rule in self.connection_rules.items(): entrance = multiworld.get_entrance(entrance_name, self.player) entrance.access_rule = rule + for region in self.indirect_conditions.get(entrance_name, ()): + multiworld.register_indirect_condition(region, entrance) for loc in multiworld.get_locations(self.player): if loc.name in self.location_rules: loc.access_rule = self.location_rules[loc.name] From 90b72c0fa5b0389fff9261f60b323a9d53e1a0aa Mon Sep 17 00:00:00 2001 From: palex00 <32203971+palex00@users.noreply.github.com> Date: Wed, 29 Apr 2026 21:25:14 +0200 Subject: [PATCH 31/66] Docs: Add missing space to plando doc #6060 --- worlds/generic/docs/plando_en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/generic/docs/plando_en.md b/worlds/generic/docs/plando_en.md index 625601d96b38..e8b4666fd028 100644 --- a/worlds/generic/docs/plando_en.md +++ b/worlds/generic/docs/plando_en.md @@ -99,7 +99,7 @@ case-sensitive. You can also use item groups and location groups that are define ## Item Plando Examples ```yaml - plando_items: + plando_items: # Example block - Pokémon Red and Blue - items: Potion: 3 From 9da3c2999022c734cf09516471cd294efd41d168 Mon Sep 17 00:00:00 2001 From: Seldom <38388947+Seldom-SE@users.noreply.github.com> Date: Wed, 29 Apr 2026 12:25:38 -0700 Subject: [PATCH 32/66] Terraria: fix failing to generate with Calamity enabled and full accessibility on Mechanical Bosses goal #5787 --- worlds/terraria/Rules.dsv | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/worlds/terraria/Rules.dsv b/worlds/terraria/Rules.dsv index 6bc1183e598d..9fded3719817 100644 --- a/worlds/terraria/Rules.dsv +++ b/worlds/terraria/Rules.dsv @@ -382,6 +382,9 @@ Hardmode Forge; ; Adamantite Bar; ; (Hardmode Forge & Adamantite Ore) | Wall of Flesh; Adamantite Pickaxe; Pickaxe(180); Hardmode Anvil & Adamantite Bar; Forbidden Armor; Armor Minions(2); Hardmode Anvil & Adamantite Bar & Forbidden Fragment; +Infernal Suevite; Calamity; @pickaxe(150) | Brimstone Elemental; +Unholy Core; Calamity; (Infernal Suevite & Hellstone) | Brimstone Elemental; +Ruin Medallion; Calamity; (Hardmode Anvil & Coin of Deceit & Unholy Core & Essence of Havoc) | Dark Matter Sheath; Aquatic Scourge; Calamity | Location | Item; Cragmaw Mire; Calamity | Location | Item; #Acid Rain Tier 2; Nuclear Fuel Rod; Calamity | Minions(1); #Cragmaw Mire | Star-Tainted Generator; @@ -411,9 +414,6 @@ Topped Off; Achievement; Old One's Army Tier 2; Location | Item; #Old One's Army Tier 1 & ((Wall of Flesh & @mech_boss(1)) | #Old One's Army Tier 3); // Brimstone Elemental -Infernal Suevite; Calamity; @pickaxe(150) | Brimstone Elemental; -Unholy Core; Calamity; (Infernal Suevite & Hellstone) | Brimstone Elemental; -Ruin Medallion; Calamity; (Hardmode Anvil & Coin of Deceit & Unholy Core & Essence of Havoc) | Dark Matter Sheath; // The Destroyer Soul of Might; ; #The Destroyer | Avenger Emblem | Light Disc | (@calamity & (Mechanical Glove | Celestial Emblem)); From 18750620997b7963f221f55dadd91d99da795417 Mon Sep 17 00:00:00 2001 From: qwint Date: Wed, 29 Apr 2026 15:29:41 -0500 Subject: [PATCH 33/66] Undertale: Handle zero arg /auto_patch (#5130) --- UndertaleClient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UndertaleClient.py b/UndertaleClient.py index b0efce206ae5..bd9936f7c005 100644 --- a/UndertaleClient.py +++ b/UndertaleClient.py @@ -49,7 +49,7 @@ def _cmd_auto_patch(self, steaminstall: typing.Optional[str] = None): if isinstance(self.ctx, UndertaleContext): os.makedirs(name=Utils.user_path("Undertale"), exist_ok=True) tempInstall = steaminstall - if not os.path.isfile(os.path.join(tempInstall, "data.win")): + if tempInstall and not os.path.isfile(os.path.join(tempInstall, "data.win")): tempInstall = None if tempInstall is None: tempInstall = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale" From 798eeaad91cbfbd40b6cae6934f3d9f24376114a Mon Sep 17 00:00:00 2001 From: Justus Lind Date: Fri, 1 May 2026 19:29:59 +1000 Subject: [PATCH 34/66] Muse Dash: Update to DASH AND SHOOT (#6163) --- worlds/musedash/MuseDashData.py | 7 +++++++ worlds/musedash/archipelago.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/worlds/musedash/MuseDashData.py b/worlds/musedash/MuseDashData.py index 6cb294bd98a6..70dae6b9a4d1 100644 --- a/worlds/musedash/MuseDashData.py +++ b/worlds/musedash/MuseDashData.py @@ -712,4 +712,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), + "Denpa-tic Imaginary Girl": SongData(2900839, "43-73", "MD Plus Project", False, 4, 6, 9), + "Don't Fight The Music": SongData(2900840, "97-0", "DASH AND SHOOT!", False, 6, 8, 10), + "Viyella's Scream": SongData(2900841, "97-1", "DASH AND SHOOT!", False, 7, 9, 11), + "Final Flash Flight": SongData(2900842, "97-2", "DASH AND SHOOT!", False, 5, 7, 10), + "YURUSHITE": SongData(2900843, "97-3", "DASH AND SHOOT!", False, 6, 8, 11), + "girls.exe": SongData(2900844, "97-4", "DASH AND SHOOT!", False, 6, 8, 11), + "Baqeela": SongData(2900845, "97-5", "DASH AND SHOOT!", False, 6, 8, 10), } \ No newline at end of file diff --git a/worlds/musedash/archipelago.json b/worlds/musedash/archipelago.json index 0580d85e77c1..49114bcf9e87 100644 --- a/worlds/musedash/archipelago.json +++ b/worlds/musedash/archipelago.json @@ -1,6 +1,6 @@ { "game": "Muse Dash", "authors": ["DeamonHunter"], - "world_version": "1.5.30", + "world_version": "1.5.32", "minimum_ap_version": "0.6.3" } \ No newline at end of file From 234b0581c9a3791230e8c08c2d34591748bc207c Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Fri, 1 May 2026 05:30:35 -0400 Subject: [PATCH 35/66] ALttP: Fix starting mail and shield equipment from start inventory (#6154) --- worlds/alttp/Rom.py | 75 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 61 insertions(+), 14 deletions(-) diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index 8acff214f24b..c58083b5da14 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -1331,6 +1331,13 @@ def chunk(l, n): starting_max_arrows = 30 startingstate = CollectionState(multiworld) + has_blue_shield = False + has_red_shield = False + has_mirror_shield = False + progressive_shields = 0 + has_blue_mail = False + has_red_mail = False + progressive_mail = 0 if startingstate.has('Silver Bow', player): equip[0x340] = 1 @@ -1359,18 +1366,6 @@ def chunk(l, n): elif startingstate.has('Fighter Sword', player): equip[0x359] = 1 - if startingstate.has('Mirror Shield', player): - equip[0x35A] = 3 - elif startingstate.has('Red Shield', player): - equip[0x35A] = 2 - elif startingstate.has('Blue Shield', player): - equip[0x35A] = 1 - - if startingstate.has('Red Mail', player): - equip[0x35B] = 2 - elif startingstate.has('Blue Mail', player): - equip[0x35B] = 1 - if startingstate.has('Magic Upgrade (1/4)', player): equip[0x37B] = 2 equip[0x36E] = 0x80 @@ -1383,8 +1378,6 @@ def chunk(l, n): if item.name in {'Bow', 'Silver Bow', 'Silver Arrows', 'Progressive Bow', 'Progressive Bow (Alt)', 'Titans Mitts', 'Power Glove', 'Progressive Glove', 'Golden Sword', 'Tempered Sword', 'Master Sword', 'Fighter Sword', 'Progressive Sword', - 'Mirror Shield', 'Red Shield', 'Blue Shield', 'Progressive Shield', - 'Red Mail', 'Blue Mail', 'Progressive Mail', 'Magic Upgrade (1/4)', 'Magic Upgrade (1/2)', 'Triforce Piece'}: continue @@ -1489,9 +1482,63 @@ def chunk(l, n): if item.name != 'Piece of Heart' or equip[0x36B] == 0: equip[0x36C] = min(equip[0x36C] + 0x08, 0xA0) equip[0x36D] = min(equip[0x36D] + 0x08, 0xA0) + elif item.name == 'Blue Shield': + has_blue_shield = True + continue + elif item.name == 'Red Shield': + has_red_shield = True + continue + elif item.name == 'Mirror Shield': + has_mirror_shield = True + continue + elif item.name == 'Progressive Shield': + progressive_shields += 1 + continue + elif item.name == 'Blue Mail': + has_blue_mail = True + continue + elif item.name == 'Red Mail': + has_red_mail = True + continue + elif item.name == 'Progressive Mail': + progressive_mail += 1 + continue else: raise RuntimeError(f'Unsupported item in starting equipment: {item.name}') + for _ in range(progressive_shields): + if has_mirror_shield: + continue + if has_red_shield and local_world.difficulty_requirements.progressive_shield_limit >= 3: + has_mirror_shield = True + continue + if has_blue_shield and local_world.difficulty_requirements.progressive_shield_limit >= 2: + has_red_shield = True + continue + if local_world.difficulty_requirements.progressive_shield_limit >= 1: + has_blue_shield = True + + for _ in range(progressive_mail): + if has_red_mail: + continue + if has_blue_mail and local_world.difficulty_requirements.progressive_armor_limit >= 2: + has_red_mail = True + continue + if local_world.difficulty_requirements.progressive_armor_limit >= 1: + has_blue_mail = True + + if has_mirror_shield: + equip[0x35A] = 3 + elif has_red_shield: + equip[0x35A] = 2 + elif has_blue_shield: + equip[0x35A] = 1 + + if has_red_mail: + equip[0x35B] = 2 + elif has_blue_mail: + equip[0x35B] = 1 + equip[0x343] = min(equip[0x343], starting_max_bombs) rom.write_byte(0x180034, starting_max_bombs) equip[0x377] = min(equip[0x377], starting_max_arrows) From a68109f5a7fe9971b5b002491de0be301889c05c Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 1 May 2026 22:38:46 +0200 Subject: [PATCH 36/66] WebHost: Add deletion of old content to cleanup function (#6119) --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- WebHostLib/__init__.py | 2 + WebHostLib/autolauncher.py | 11 +++- test/webhost/__init__.py | 5 ++ test/webhost/test_cleanup.py | 107 +++++++++++++++++++++++++++++++++++ 4 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 test/webhost/test_cleanup.py diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index 9459845a1d55..b3dc203a3de0 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -48,6 +48,8 @@ app.config["JOB_TIME"] = 600 # maximum time in seconds since last activity for a room to be hosted app.config["MAX_ROOM_TIMEOUT"] = 259200 +# minimum time in days since last activity for a room to be deleted. 0 to disable. +app.config["ROOM_AUTO_DELETE"] = 0 # memory limit for generator processes in bytes app.config["GENERATOR_MEMORY_LIMIT"] = 4294967296 diff --git a/WebHostLib/autolauncher.py b/WebHostLib/autolauncher.py index 1a6156450035..165e08a103fb 100644 --- a/WebHostLib/autolauncher.py +++ b/WebHostLib/autolauncher.py @@ -100,13 +100,18 @@ def init_generator(config: dict[str, Any]) -> None: db.generate_mapping() -def cleanup(): - """delete unowned user-content""" +def cleanup(config: dict[str, Any]): + """delete unowned or old user-content""" + auto_delete: int = config.get("ROOM_AUTO_DELETE", 0) with db_session: # >>> bool(uuid.UUID(int=0)) # True rooms = Room.select(lambda room: room.owner == UUID(int=0)).delete(bulk=True) seeds = Seed.select(lambda seed: seed.owner == UUID(int=0) and not seed.rooms).delete(bulk=True) + if auto_delete > 0: + cutoff = utcnow() - timedelta(days=auto_delete) + rooms += Room.select(lambda room: room.last_activity < cutoff).delete(bulk=True) + seeds += Seed.select(lambda seed: not seed.rooms and seed.creation_time < cutoff).delete(bulk=True) slots = Slot.select(lambda slot: not slot.seed).delete(bulk=True) # Command gets deleted by ponyorm Cascade Delete, as Room is Required if rooms or seeds or slots: @@ -118,7 +123,7 @@ def keep_running(): stop_event = _stop_event try: with Locker("autohost"): - cleanup() + cleanup(config) hosters = [] for x in range(config["HOSTERS"]): hoster = MultiworldInstance(config, x) diff --git a/test/webhost/__init__.py b/test/webhost/__init__.py index 2eb340722a3a..f6181516ceac 100644 --- a/test/webhost/__init__.py +++ b/test/webhost/__init__.py @@ -33,4 +33,9 @@ def setUpClass(cls) -> None: cls.app = raw_app def setUp(self) -> None: + from WebHostLib.models import db + from pony.orm import db_session + with db_session: + for entity in db.entities.values(): + entity.select().delete(bulk=True) self.client = self.app.test_client() diff --git a/test/webhost/test_cleanup.py b/test/webhost/test_cleanup.py new file mode 100644 index 000000000000..fc121983c906 --- /dev/null +++ b/test/webhost/test_cleanup.py @@ -0,0 +1,107 @@ +from datetime import timedelta +from uuid import UUID, uuid4 +from pony.orm import db_session, commit + +from Utils import utcnow +from WebHostLib.autolauncher import cleanup +from WebHostLib.models import Room, Seed, Slot +from . import TestBase + + +class TestCleanup(TestBase): + def test_cleanup_unowned(self) -> None: + with db_session: + s1 = Seed(id=uuid4(), multidata=b"", owner=UUID(int=0)) + Room(id=uuid4(), owner=UUID(int=0), seed=s1) + + s2 = Seed(id=uuid4(), multidata=b"", owner=uuid4()) # Owned + Room(id=uuid4(), owner=UUID(int=0), seed=s2) # Unowned room of owned seed + + Seed(id=uuid4(), multidata=b"", owner=UUID(int=0)) # Unowned seed with no rooms + + commit() + + cleanup({"ROOM_AUTO_DELETE": 0}) + + with db_session: + self.assertEqual(Room.select().count(), 0) # Both rooms were unowned + self.assertEqual(Seed.select().count(), 1) # s2 is owned + self.assertIsNotNone(Seed.get(id=s2.id)) + + def test_cleanup_auto_delete(self) -> None: + now = utcnow() + old_time = now - timedelta(days=10) + recent_time = now - timedelta(days=2) + + with db_session: + # Case 1: Old room, owned + s1 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time) + r1 = Room(id=uuid4(), owner=uuid4(), seed=s1, last_activity=old_time) + + # Case 2: Recent room, owned + s2 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time) + r2 = Room(id=uuid4(), owner=uuid4(), seed=s2, last_activity=recent_time) + + # Case 3: Old seed, no rooms, owned + s3 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time) + + # Case 4: Recent seed, no rooms, owned + s4 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=recent_time) + + # Case 5: Old seed with recent room (should not be deleted) + s5 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time) + r5 = Room(id=uuid4(), owner=uuid4(), seed=s5, last_activity=recent_time) + + commit() + + # Delete items older than 5 days + cleanup({"ROOM_AUTO_DELETE": 5}) + + with db_session: + self.assertIsNone(Room.get(id=r1.id), "Old room should be deleted") + self.assertIsNotNone(Room.get(id=r2.id), "Recent room should NOT be deleted") + self.assertIsNone(Seed.get(id=s3.id), "Old seed without rooms should be deleted") + self.assertIsNotNone(Seed.get(id=s4.id), "Recent seed without rooms should NOT be deleted") + self.assertIsNotNone(Seed.get(id=s5.id), "Old seed with recent room should NOT be deleted") + self.assertIsNotNone(Room.get(id=r5.id), "Recent room for old seed should NOT be deleted") + + # Seeds are deleted if they have NO rooms AND are old. + # After r1 is deleted, s1 has no rooms. Since it's old, it should be deleted. + self.assertIsNone(Seed.get(id=s1.id), "Old seed whose only room was deleted should be deleted") + + def test_cleanup_disabled(self) -> None: + now = utcnow() + old_time = now - timedelta(days=10) + + with db_session: + s1 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time) + r1 = Room(id=uuid4(), owner=uuid4(), seed=s1, last_activity=old_time) + commit() + + cleanup({"ROOM_AUTO_DELETE": 0}) + + with db_session: + self.assertIsNotNone(Room.get(id=r1.id), "Room should NOT be deleted when auto-delete is 0") + self.assertIsNotNone(Seed.get(id=s1.id), "Seed should NOT be deleted when auto-delete is 0") + + def test_cleanup_slots(self) -> None: + now = utcnow() + old_time = now - timedelta(days=10) + + with db_session: + s1 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time) + slot1 = Slot(player_id=1, player_name="P1", seed=s1, game="TestGame") + + s2 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=now) + slot2 = Slot(player_id=2, player_name="P2", seed=s2, game="TestGame") + + commit() + + # Delete items older than 5 days + cleanup({"ROOM_AUTO_DELETE": 5}) + + with db_session: + self.assertIsNone(Seed.get(id=s1.id), "Old seed should be deleted") + self.assertIsNone(Slot.get(id=slot1.id), "Slot of deleted seed should be deleted") + self.assertIsNotNone(Seed.get(id=s2.id), "Recent seed should NOT be deleted") + self.assertIsNotNone(Slot.get(id=slot2.id), "Slot of recent seed should NOT be deleted") From 4486510fbc9406b7135377c2a39bf622d87092d8 Mon Sep 17 00:00:00 2001 From: qwint Date: Sat, 2 May 2026 10:53:37 -0500 Subject: [PATCH 37/66] Options: make template generation log the failing world if one fails (#6176) --- Options.py | 45 ++++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/Options.py b/Options.py index a84d5e280e75..ec5fb2fb7ef2 100644 --- a/Options.py +++ b/Options.py @@ -1856,27 +1856,30 @@ def yaml_dump_scalar(scalar) -> str: for game_name, world in AutoWorldRegister.world_types.items(): if not world.hidden or generate_hidden: - presets = world.web.options_presets.copy() - presets.update({"": {}}) - - option_groups = get_option_groups(world) - for name, preset in presets.items(): - res = template.render( - option_groups=option_groups, - __version__=__version__, - game=game_name, - world_version=world.world_version.as_simple_string(), - yaml_dump=yaml_dump_scalar, - dictify_range=dictify_range, - cleandoc=cleandoc, - preset_name=name, - preset=preset, - ) - preset_name = f" - {name}" if name else "" - with open(os.path.join(preset_folder if name else target_folder, - get_file_safe_name(game_name + preset_name) + ".yaml"), - "w", encoding="utf-8-sig") as f: - f.write(res) + try: + presets = world.web.options_presets.copy() + presets.update({"": {}}) + + option_groups = get_option_groups(world) + for name, preset in presets.items(): + res = template.render( + option_groups=option_groups, + __version__=__version__, + game=game_name, + world_version=world.world_version.as_simple_string(), + yaml_dump=yaml_dump_scalar, + dictify_range=dictify_range, + cleandoc=cleandoc, + preset_name=name, + preset=preset, + ) + preset_name = f" - {name}" if name else "" + with open(os.path.join(preset_folder if name else target_folder, + get_file_safe_name(game_name + preset_name) + ".yaml"), + "w", encoding="utf-8-sig") as f: + f.write(res) + except Exception as ex: + raise Exception(f"Template generation failed for world {game_name}") from ex def dump_player_options(multiworld: MultiWorld) -> None: From 6d9d340c719c0eeaadf5f56a8878acd5540acba7 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Tue, 5 May 2026 00:30:47 -0500 Subject: [PATCH 38/66] Launcher: display window for failed world loads if any (#6058) --- Generate.py | 3 ++- Launcher.py | 35 ++++++++++++++++++++++++++++++++ data/launcher.kv | 9 ++++++++ test/general/test_implemented.py | 2 +- worlds/__init__.py | 9 ++++---- 5 files changed, 52 insertions(+), 6 deletions(-) diff --git a/Generate.py b/Generate.py index 6855a6aaae63..ff76b3c6490b 100644 --- a/Generate.py +++ b/Generate.py @@ -585,7 +585,8 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b raise Exception(f"Invalid game: {ret.game}") if ret.game not in AutoWorldRegister.world_types: from worlds import failed_world_loads - picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + failed_world_loads, limit=1)[0] + picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + list(failed_world_loads.keys()), + limit=1)[0] if picks[0] in failed_world_loads: raise Exception(f"No functional world found to handle game {ret.game}. " f"Did you mean '{picks[0]}' ({picks[1]}% sure)? " diff --git a/Launcher.py b/Launcher.py index 0e7d4796c4a8..d1e076b4c3ad 100644 --- a/Launcher.py +++ b/Launcher.py @@ -36,6 +36,7 @@ init_logging('Launcher') from worlds.LauncherComponents import Component, components, icon_paths, SuffixIdentifier, Type +from worlds import failed_world_loads def open_host_yaml(): @@ -275,6 +276,7 @@ class Launcher(ThemedApp): search_box: MDTextField = ObjectProperty(None) cards: list[LauncherCard] current_filter: Sequence[str | Type] | None + failed_worlds: bool = bool(failed_world_loads) def __init__(self, ctx=None, components=None, args=None): self.title = self.base_title + " " + Utils.__version__ @@ -422,6 +424,39 @@ def component_action(button): MDSnackbar(MDSnackbarText(text=open_text), y=dp(24), pos_hint={"center_x": 0.5}, size_hint_x=0.5).open() + @staticmethod + def copy_to_clipboard(text): + from kivy.core.clipboard import Clipboard + Clipboard.copy(text) + MDSnackbar(MDSnackbarText(text="Copied to clipboard."), y=dp(24), pos_hint={"center_x": 0.5}, + size_hint_x=0.5).open() + + def display_failed(self): + """Display a dialog showing the exceptions produced by any world that failed to load during + initialization.""" + if not self.failed_worlds: + return + from kivymd.uix.dialog import MDDialog, MDDialogIcon, MDDialogHeadlineText, MDDialogContentContainer + from kivymd.uix.divider import MDDivider + from kivymd.uix.list import MDListItem, MDListItemHeadlineText, MDListItemSupportingText + entries = [] + for world, reason in failed_world_loads.items(): + entries.append(MDListItem( + MDListItemHeadlineText(text=world), + MDListItemSupportingText(text=reason), + on_release=lambda x, r=reason: self.copy_to_clipboard(r) + )) + dialog = MDDialog( + MDDialogIcon(icon="alert"), + MDDialogHeadlineText(text="Failed World Loads"), + MDDialogContentContainer( + MDDivider(), + *entries, + orientation="vertical", + ) + ) + dialog.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. """ file, component = identify(filename.decode()) diff --git a/data/launcher.kv b/data/launcher.kv index 1cb4e84ab519..a52214a7a4d4 100644 --- a/data/launcher.kv +++ b/data/launcher.kv @@ -140,6 +140,15 @@ MDFloatLayout: MDNavigationDrawerDivider: + MDBoxLayout: + orientation: "horizontal" + MDIconButton: + icon: "alert" if app.failed_worlds else "" + theme_text_color: "Custom" + text_color: "D23C42" + disabled: not app.failed_worlds + on_release: app.display_failed() + MDGridLayout: id: main_layout diff --git a/test/general/test_implemented.py b/test/general/test_implemented.py index 0bc7b62d5b34..0906852201b4 100644 --- a/test/general/test_implemented.py +++ b/test/general/test_implemented.py @@ -54,7 +54,7 @@ def test_slot_data(self): def test_no_failed_world_loads(self): if failed_world_loads: - self.fail(f"The following worlds failed to load: {failed_world_loads}") + self.fail(f"The following worlds failed to load: {failed_world_loads.keys()}") def test_prefill_items(self): """Test that every world can reach every location from allstate before pre_fill.""" diff --git a/worlds/__init__.py b/worlds/__init__.py index dd2d83a27ecd..190710068844 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -33,7 +33,7 @@ ] -failed_world_loads: List[str] = [] +failed_world_loads: dict[str, str] = {} @dataclasses.dataclass(order=True) @@ -68,8 +68,9 @@ def load(self) -> bool: print(f"Could not load world {self}:", file=file_like) traceback.print_exc(file=file_like) file_like.seek(0) - logging.exception(file_like.read()) - failed_world_loads.append(os.path.basename(self.path).rsplit(".", 1)[0]) + reason = file_like.read() + logging.exception(reason) + failed_world_loads[os.path.basename(self.path).rsplit(".", 1)[0]] = reason return False @@ -128,7 +129,7 @@ def load_apworlds() -> None: def fail_world(game_name: str, reason: str, add_as_failed_to_load: bool = True) -> None: if add_as_failed_to_load: - failed_world_loads.append(game_name) + failed_world_loads[game_name] = reason logging.warning(reason) for apworld_source in apworlds: From b59e52a103b33e67c13e8e0ddc5236edce805458 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Tue, 5 May 2026 10:14:19 -0400 Subject: [PATCH 39/66] Options: Remove the "" from the start inventory examples (#6165) --------- Co-authored-by: Emily <35015090+EmilyV99@users.noreply.github.com> --- Options.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Options.py b/Options.py index ec5fb2fb7ef2..47f4b1d45c14 100644 --- a/Options.py +++ b/Options.py @@ -1469,7 +1469,7 @@ class NonLocalItems(ItemSet): class StartInventory(ItemDict): - """Start with the specified amount of these items. Example: "Bomb: 1" """ + """Start with the specified amount of these items. Example: {Bomb: 1, Arrow: 3} """ verify_item_name = True display_name = "Start Inventory" rich_text_doc = True @@ -1477,7 +1477,7 @@ class StartInventory(ItemDict): class StartInventoryPool(StartInventory): - """Start with the specified amount of these items and don't place them in the world. Example: "Bomb: 1" + """Start with the specified amount of these items and don't place them in the world. Example: {Bomb: 1, Arrow: 3} The game decides what the replacement items will be. """ From 2bd572c23d1e7e2a98701769fab67f9118d2ed08 Mon Sep 17 00:00:00 2001 From: josephwhite <22449090+josephwhite@users.noreply.github.com> Date: Sat, 9 May 2026 10:46:53 -0400 Subject: [PATCH 40/66] Super Mario 64: Basic testing (#4335) --------- Co-authored-by: Yussur Mustafa Oraji Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- worlds/sm64ex/Regions.py | 8 + worlds/sm64ex/Rules.py | 60 +++---- worlds/sm64ex/__init__.py | 2 +- worlds/sm64ex/test/__init__.py | 0 worlds/sm64ex/test/bases.py | 8 + worlds/sm64ex/test/test_access.py | 81 +++++++++ worlds/sm64ex/test/test_options.py | 278 +++++++++++++++++++++++++++++ 7 files changed, 403 insertions(+), 34 deletions(-) create mode 100644 worlds/sm64ex/test/__init__.py create mode 100644 worlds/sm64ex/test/bases.py create mode 100644 worlds/sm64ex/test/test_access.py create mode 100644 worlds/sm64ex/test/test_options.py diff --git a/worlds/sm64ex/Regions.py b/worlds/sm64ex/Regions.py index 2a8206b0cbd2..796a07621799 100644 --- a/worlds/sm64ex/Regions.py +++ b/worlds/sm64ex/Regions.py @@ -79,6 +79,14 @@ class SM64Region(Region): sm64_entrances_to_level = {**sm64_paintings_to_level, **sm64_secrets_to_level } sm64_level_to_entrances = {**sm64_level_to_paintings, **sm64_level_to_secrets } +# Levels with at least one star without a movement rule +# Currently excluding WF, HMC, WDW, TTM, THI, TTC, and RR +valid_move_randomizer_start_courses = [ + "Bob-omb Battlefield", "Jolly Roger Bay", "Cool, Cool Mountain", + "Big Boo's Haunt", "Lethal Lava Land", "Shifting Sand Land", + "Dire, Dire Docks", "Snowman's Land" +] + def create_regions(multiworld: MultiWorld, options: SM64Options, player: int): regSS = Region("Menu", player, multiworld, "Castle Area") create_default_locs(regSS, locSS_table) diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py index 92ee61bda4a7..e97c2aacd00f 100644 --- a/worlds/sm64ex/Rules.py +++ b/worlds/sm64ex/Rules.py @@ -5,7 +5,8 @@ from .Locations import location_table from .Options import SM64Options from .Regions import connect_regions, SM64Levels, sm64_level_to_paintings, sm64_paintings_to_level,\ -sm64_level_to_secrets, sm64_secrets_to_level, sm64_entrances_to_level, sm64_level_to_entrances +sm64_level_to_secrets, sm64_secrets_to_level, sm64_entrances_to_level, sm64_level_to_entrances,\ +valid_move_randomizer_start_courses from .Items import action_item_data_table def shuffle_dict_keys(multiworld: MultiWorld, dictionary: dict) -> dict: @@ -28,40 +29,33 @@ def fix_reg(entrance_map: Dict[SM64Levels, str], entrance: SM64Levels, invalid_r def set_rules(multiworld: MultiWorld, options: SM64Options, player: int, area_connections: dict, star_costs: dict, move_rando_bitvec: int): randomized_level_to_paintings = sm64_level_to_paintings.copy() randomized_level_to_secrets = sm64_level_to_secrets.copy() - valid_move_randomizer_start_courses = [ - "Bob-omb Battlefield", "Jolly Roger Bay", "Cool, Cool Mountain", - "Big Boo's Haunt", "Lethal Lava Land", "Shifting Sand Land", - "Dire, Dire Docks", "Snowman's Land" - ] # Excluding WF, HMC, WDW, TTM, THI, TTC, and RR - if options.area_rando >= 1: # Some randomization is happening, randomize Courses - randomized_level_to_paintings = shuffle_dict_keys(multiworld,sm64_level_to_paintings) - # If not shuffling later, ensure a valid start course on move randomizer - if options.area_rando < 3 and move_rando_bitvec > 0: - swapdict = randomized_level_to_paintings.copy() - invalid_start_courses = {course for course in randomized_level_to_paintings.values() if course not in valid_move_randomizer_start_courses} - fix_reg(randomized_level_to_paintings, SM64Levels.BOB_OMB_BATTLEFIELD, invalid_start_courses, swapdict, multiworld) - fix_reg(randomized_level_to_paintings, SM64Levels.WHOMPS_FORTRESS, invalid_start_courses, swapdict, multiworld) - - if options.area_rando == 2: # Randomize Secrets as well + + if options.area_rando > options.area_rando.option_Off: # Some randomization is happening, randomize Courses + randomized_level_to_paintings = shuffle_dict_keys(multiworld, sm64_level_to_paintings) + + if options.area_rando == options.area_rando.option_Courses_and_Secrets_Separate: # Randomize Secrets as well randomized_level_to_secrets = shuffle_dict_keys(multiworld, sm64_level_to_secrets) - randomized_entrances = {**randomized_level_to_paintings, **randomized_level_to_secrets} - if options.area_rando == 3: # Randomize Courses and Secrets in one pool + + randomized_entrances = {**randomized_level_to_paintings, **randomized_level_to_secrets} # Concatenate courses and secrets for rest + + if options.area_rando == options.area_rando.option_Courses_and_Secrets: # Randomize Courses and Secrets in one pool randomized_entrances = shuffle_dict_keys(multiworld, randomized_entrances) - # Guarantee first entrance is a course - swapdict = randomized_entrances.copy() - if move_rando_bitvec == 0: - fix_reg(randomized_entrances, SM64Levels.BOB_OMB_BATTLEFIELD, sm64_secrets_to_level.keys(), swapdict, multiworld) - else: - invalid_start_courses = {course for course in randomized_entrances.values() if course not in valid_move_randomizer_start_courses} - fix_reg(randomized_entrances, SM64Levels.BOB_OMB_BATTLEFIELD, invalid_start_courses, swapdict, multiworld) - fix_reg(randomized_entrances, SM64Levels.WHOMPS_FORTRESS, invalid_start_courses, swapdict, multiworld) - # Guarantee BITFS is not mapped to DDD - fix_reg(randomized_entrances, SM64Levels.BOWSER_IN_THE_FIRE_SEA, {"Dire, Dire Docks"}, swapdict, multiworld) - # Guarantee COTMC is not mapped to HMC, cuz thats impossible. If BitFS -> HMC, also no COTMC -> DDD. - if randomized_entrances[SM64Levels.BOWSER_IN_THE_FIRE_SEA] == "Hazy Maze Cave": - fix_reg(randomized_entrances, SM64Levels.CAVERN_OF_THE_METAL_CAP, {"Hazy Maze Cave", "Dire, Dire Docks"}, swapdict, multiworld) - else: - fix_reg(randomized_entrances, SM64Levels.CAVERN_OF_THE_METAL_CAP, {"Hazy Maze Cave"}, swapdict, multiworld) + + # Now, fix assignment if necessary + swapdict = randomized_entrances.copy() + if move_rando_bitvec == 0: + fix_reg(randomized_entrances, SM64Levels.BOB_OMB_BATTLEFIELD, sm64_secrets_to_level.keys(), swapdict, multiworld) + else: + invalid_start_courses = {course for course in randomized_entrances.values() if course not in valid_move_randomizer_start_courses} + fix_reg(randomized_entrances, SM64Levels.BOB_OMB_BATTLEFIELD, invalid_start_courses, swapdict, multiworld) + fix_reg(randomized_entrances, SM64Levels.WHOMPS_FORTRESS, invalid_start_courses, swapdict, multiworld) + # Guarantee BITFS is not mapped to DDD + fix_reg(randomized_entrances, SM64Levels.BOWSER_IN_THE_FIRE_SEA, {"Dire, Dire Docks"}, swapdict, multiworld) + # Guarantee COTMC is not mapped to HMC, cuz thats impossible. If BitFS -> HMC, also no COTMC -> DDD. + if randomized_entrances[SM64Levels.BOWSER_IN_THE_FIRE_SEA] == "Hazy Maze Cave": + fix_reg(randomized_entrances, SM64Levels.CAVERN_OF_THE_METAL_CAP, {"Hazy Maze Cave", "Dire, Dire Docks"}, swapdict, multiworld) + else: + fix_reg(randomized_entrances, SM64Levels.CAVERN_OF_THE_METAL_CAP, {"Hazy Maze Cave"}, swapdict, multiworld) # Destination Format: LVL | AREA with LVL = LEVEL_x, AREA = Area as used in sm64 code # Cast to int to not rely on availability of SM64Levels enum. Will cause crash in MultiServer otherwise diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index f1208d2059e0..138425e43d2c 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -139,7 +139,7 @@ def create_items(self): if self.move_rando_bitvec & (1 << itemdata.code - double_jump_bitvec_offset)] def generate_basic(self): - if not (self.options.buddy_checks): + if not self.options.buddy_checks: self.multiworld.get_location("BoB: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock BoB")) self.multiworld.get_location("WF: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock WF")) self.multiworld.get_location("JRB: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock JRB")) diff --git a/worlds/sm64ex/test/__init__.py b/worlds/sm64ex/test/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/worlds/sm64ex/test/bases.py b/worlds/sm64ex/test/bases.py new file mode 100644 index 000000000000..b19a5c0f25f4 --- /dev/null +++ b/worlds/sm64ex/test/bases.py @@ -0,0 +1,8 @@ +from test.bases import WorldTestBase + +from .. import SM64World + + +class SM64TestBase(WorldTestBase): + game = "Super Mario 64" + world: SM64World diff --git a/worlds/sm64ex/test/test_access.py b/worlds/sm64ex/test/test_access.py new file mode 100644 index 000000000000..32695da259ff --- /dev/null +++ b/worlds/sm64ex/test/test_access.py @@ -0,0 +1,81 @@ +from .bases import SM64TestBase +from .. import Options +from ..Regions import sm64_entrances_to_level, sm64_level_to_entrances + +# Access to Locations/Areas/Entrances by Power Star count in star_cost +class StarCostAccessTestBase(SM64TestBase): + run_default_tests = False + options = { + "progressive_keys": Options.ProgressiveKeys.option_false, + "enable_locked_paintings": Options.EnableLockedPaintings.option_false, + # Test for access would mean access to entrance/painting, + # not level itself for the sake of entrance rando. + "area_rando": Options.AreaRandomizer.option_Courses_and_Secrets, + } + + def test_BoB_entrance_access(self): + # Always accessible, no stars needed. + bob_level_id = sm64_entrances_to_level["Bob-omb Battlefield"] + bob_entrance_id = self.world.area_connections[bob_level_id] + bob_entrance = sm64_level_to_entrances[bob_entrance_id] + self.assertTrue(self.can_reach_region(bob_entrance)) + + def test_MIPS1_access(self): + # Requires Basement Key and Power Stars calculated in star_costs["MIPS1Cost"] + self.assertFalse(self.can_reach_location("MIPS 1")) + self.collect([self.get_item_by_name("Basement Key")]) + self.assertFalse(self.can_reach_location("MIPS 1")) + mips1_cost = self.world.star_costs["MIPS1Cost"] + self.collect([self.get_item_by_name("Power Star")] * mips1_cost) + self.assertTrue(self.can_reach_location("MIPS 1")) + + def test_MIPS2_access(self): + # Requires Basement Key and Power Stars calculated in star_costs["MIPS2Cost"] + self.assertFalse(self.can_reach_location("MIPS 2")) + self.collect([self.get_item_by_name("Basement Key")]) + self.assertFalse(self.can_reach_location("MIPS 2")) + mips2_cost = self.world.star_costs["MIPS2Cost"] + self.collect([self.get_item_by_name("Power Star")] * mips2_cost) + self.assertTrue(self.can_reach_location("MIPS 2")) + + def test_BitDW_entrance_access(self): + # Requires Power Stars calculated in star_costs["FirstBowserDoorCost"] + bitdw_level_id = sm64_entrances_to_level["Bowser in the Dark World"] + bitdw_entrance_id = self.world.area_connections[bitdw_level_id] + bitdw_entrance = sm64_level_to_entrances[bitdw_entrance_id] + self.assertFalse(self.can_reach_region(bitdw_entrance)) + bitdw_cost = self.world.star_costs["FirstBowserDoorCost"] + self.collect([self.get_item_by_name("Power Star")] * bitdw_cost) + self.assertTrue(self.can_reach_region(bitdw_entrance)) + + # Since BitFS is locked behind "DDD: Board Bowser's Sub", we just need to test DDD. + def test_DDD_entrance_access(self): + # Requires Basement Key and Power Stars calculated in star_costs["BasementDoorCost"] + ddd_level_id = sm64_entrances_to_level["Dire, Dire Docks"] + ddd_entrance_id = self.world.area_connections[ddd_level_id] + ddd_entrance = sm64_level_to_entrances[ddd_entrance_id] + self.assertFalse(self.can_reach_region(ddd_entrance)) + self.collect([self.get_item_by_name("Basement Key")]) + self.assertFalse(self.can_reach_region(ddd_entrance)) + bitfs_cost = self.world.star_costs["BasementDoorCost"] + self.collect([self.get_item_by_name("Power Star")] * bitfs_cost) + self.assertTrue(self.can_reach_region(ddd_entrance)) + + def test_Floor3_access(self): + # Requires Second Floor Key and Power Stars calculated in star_costs["SecondFloorDoorCost"] + self.assertFalse(self.can_reach_region("Third Floor")) + self.collect([self.get_item_by_name("Second Floor Key")]) + self.assertFalse(self.can_reach_region("Third Floor")) + floor3_cost = self.world.star_costs["StarsToFinish"] + self.collect([self.get_item_by_name("Power Star")] * floor3_cost) + self.assertTrue(self.can_reach_region("Third Floor")) + + def test_BitS_entrance_access(self): + # Requires Second Floor Key and Power Stars calculated in star_costs["StarsToFinish"] + self.assertFalse(self.can_reach_region("Bowser in the Sky")) + self.collect([self.get_item_by_name("Second Floor Key")]) + self.assertFalse(self.can_reach_region("Bowser in the Sky")) + bits_cost = self.world.star_costs["StarsToFinish"] + self.collect([self.get_item_by_name("Power Star")] * bits_cost) + self.assertTrue(self.can_reach_region("Third Floor")) + self.assertTrue(self.can_reach_region("Bowser in the Sky")) diff --git a/worlds/sm64ex/test/test_options.py b/worlds/sm64ex/test/test_options.py new file mode 100644 index 000000000000..2a55b31723b5 --- /dev/null +++ b/worlds/sm64ex/test/test_options.py @@ -0,0 +1,278 @@ +from .bases import SM64TestBase +from .. import Options +from ..Locations import loc100Coin_table, location_table +from ..Regions import sm64_entrances_to_level, sm64_level_to_paintings, sm64_level_to_secrets, \ + valid_move_randomizer_start_courses + +valid_move_randomizer_start_entrances = { + level: entrance + for (level, entrance) in sm64_entrances_to_level.items() + if level in valid_move_randomizer_start_courses +} + + +# Coin Star Logic +class EnableCoinStarsTestBase(SM64TestBase): + options = { + "enable_coin_stars": Options.EnableCoinStars.option_on + } + + # Ensure Coin Star locations are created + def test_coin_star_locations(self): + possible_locations = self.world.location_names + for loc in loc100Coin_table: + # Use subtest to force all locations to be tested + with self.subTest("Location created", location=loc): + self.assertIn(loc, possible_locations) + + +class DisableCoinStarsTestBase(SM64TestBase): + options = { + "enable_coin_stars": Options.EnableCoinStars.option_off + } + + # Ensure Coin Star locations are not created + def test_coin_star_locations(self): + possible_locations = self.world.get_locations() + for loc in loc100Coin_table: + # Use subtest to force all locations to be tested + with self.subTest("Location not created", location=loc): + self.assertNotIn(loc, possible_locations) + + +class VanillaCoinStarsTestBase(SM64TestBase): + options = { + "enable_coin_stars": Options.EnableCoinStars.option_vanilla + } + + # Ensure Coin Star locations are created + def test_coin_star_locations(self): + possible_locations = self.world.location_names + for loc in loc100Coin_table: + # Use subtest to force all locations to be tested + with self.subTest("Location created", location=loc): + self.assertIn(loc, possible_locations) + + # Vanilla Coin Stars should give the player their own Power Stars + def test_items_in_coin_star_locations(self): + for loc in loc100Coin_table: + # Use subtest to force all locations to be tested + with self.subTest("Location created", location=loc): + item_in_loc = self.world.get_location(loc).item + self.assertEqual(item_in_loc.name, "Power Star") + # By default, these test bases are single player multiworld. + # In any other case, we should test that they belong to their respective worlds. + + +# Exclamation Boxes +class ExclamationBoxesOnTestBase(SM64TestBase): + options = { + "exclamation_boxes": Options.ExclamationBoxes.option_true, + } + + +class ExclamationBoxesOffTestBase(SM64TestBase): + options = { + "exclamation_boxes": Options.ExclamationBoxes.option_false, + } + + # Should populate the boxes with the players own 1Up Mushrooms + def test_items_in_exclamation_box_locations(self): + # Get 1Up Block locations + loc1ups_table = {name for name in location_table.keys() if "1Up Block" in name} + for loc in loc1ups_table: + # Use subtest to force all locations to be tested + with self.subTest("Location has own 1Up Mushroom.", location=loc): + item_in_loc = self.world.get_location(loc).item + self.assertEqual(item_in_loc.name, "1Up Mushroom") + # By default, these test bases are single player multiworld. + # In any other case, we should test that they belong to their respective worlds. + + +# Entrance Randomizer +class EntranceRandoOffTestBase(SM64TestBase): + options = { + "area_rando": Options.AreaRandomizer.option_Off + } + + # Ensure entrance rando disabled + def test_BoB_entrance(self): + bob_level_id = sm64_entrances_to_level["Bob-omb Battlefield"] + self.assertEqual(self.world.area_connections[bob_level_id], bob_level_id) + + def test_BitFS_entrance(self): + bitfs_level_id = sm64_entrances_to_level["Bowser in the Fire Sea"] + self.assertEqual(self.world.area_connections[bitfs_level_id], bitfs_level_id) + + +class EntranceRandoCourseTestBase(SM64TestBase): + options = { + "area_rando": Options.AreaRandomizer.option_Courses_Only + } + + def test_BoB_entrance(self): + bob_level_id = sm64_entrances_to_level["Bob-omb Battlefield"] + # BoB goes to a painting, not a secret + self.assertNotIn(self.world.area_connections[bob_level_id], sm64_level_to_secrets.keys()) + self.assertIn(self.world.area_connections[bob_level_id], sm64_level_to_paintings.keys()) + + def test_BitFS_entrance(self): + bitfs_level_id = sm64_entrances_to_level["Bowser in the Fire Sea"] + # BitFS is a secret (aka not a course), unaffected by Course Only entrance rando. + self.assertEqual(self.world.area_connections[bitfs_level_id], bitfs_level_id) + + +class EntranceRandoSeparateTestBase(SM64TestBase): + options = { + "area_rando": Options.AreaRandomizer.option_Courses_and_Secrets_Separate + } + + def test_BoB_entrance(self): + bob_level_id = sm64_entrances_to_level["Bob-omb Battlefield"] + # BoB goes to a painting, not a secret + self.assertNotIn(self.world.area_connections[bob_level_id], sm64_level_to_secrets.keys()) + self.assertIn(self.world.area_connections[bob_level_id], sm64_level_to_paintings.keys()) + + def test_BitFS_entrance(self): + bitfs_level_id = sm64_entrances_to_level["Bowser in the Fire Sea"] + # BitFS goes to a secret, not a painting + self.assertIn(self.world.area_connections[bitfs_level_id], sm64_level_to_secrets.keys()) + self.assertNotIn(self.world.area_connections[bitfs_level_id], sm64_level_to_paintings.keys()) + # BitFS does not go to DDD + self.assertIsNot(self.world.area_connections[bitfs_level_id], sm64_entrances_to_level["Dire, Dire Docks"]) + + +class EntranceRandoAllTestBase(SM64TestBase): + options = { + "area_rando": Options.AreaRandomizer.option_Courses_and_Secrets + } + + def test_BitFS_entrance(self): + bitfs_level_id = sm64_entrances_to_level["Bowser in the Fire Sea"] + # BitFS does not go to DDD + self.assertIsNot(self.world.area_connections[bitfs_level_id], sm64_entrances_to_level["Dire, Dire Docks"]) + + +# Completion Type +class CompletionLastBowserTestBase(SM64TestBase): + options = { + "completion_type": Options.CompletionType.option_Last_Bowser_Stage + } + + +class CompletionAllBowserTestBase(SM64TestBase): + options = { + "completion_type": Options.CompletionType.option_All_Bowser_Stages + } + + +# Option Combos + + +# Smallest Power Star count possible +class MinimumStarsPossibleTestBase(SM64TestBase): + options = { + "amount_of_stars": Options.AmountOfStars.range_start, + "enable_move_rando": Options.EnableMoveRandomizer.option_true, + "exclamation_boxes": Options.ExclamationBoxes.option_false, + "enable_coin_stars": Options.EnableCoinStars.option_off + } + + # There will be less Power Stars than filler with this low of a star count + def test_stars_vs_filler(self): + filler_count = len(self.get_items_by_name("1Up Mushroom")) + star_count = len(self.get_items_by_name("Power Star")) + self.assertGreater(filler_count, star_count) + + +# Entrance + Move Randos +class CourseEntrancesMoveTestBase(SM64TestBase): + options = { + "enable_move_rando": Options.EnableMoveRandomizer.option_true, + "area_rando": Options.AreaRandomizer.option_Courses_Only + } + + def test_BoB_entrance(self): + bob_level_id = sm64_entrances_to_level["Bob-omb Battlefield"] + # BoB goes to a course, not a secret. + self.assertNotIn(self.world.area_connections[bob_level_id], sm64_level_to_secrets.keys()) + self.assertIn(self.world.area_connections[bob_level_id], sm64_level_to_paintings.keys()) + # BoB goes to level with at least one star without a movement rule. + self.assertIn(self.world.area_connections[bob_level_id], valid_move_randomizer_start_entrances.values()) + + def test_BitFS_entrance(self): + bitfs_level_id = sm64_entrances_to_level["Bowser in the Fire Sea"] + # BitFS is a secret (aka not a course), unaffected by Course Only entrance rando. + self.assertEqual(self.world.area_connections[bitfs_level_id], bitfs_level_id) + + def test_WF_entrance(self): + wf_level_id = sm64_entrances_to_level["Whomp's Fortress"] + # WF goes to level with at least one star without a movement rule. + self.assertIn(self.world.area_connections[wf_level_id], valid_move_randomizer_start_entrances.values()) + + +class SeparateEntrancesMoveTestBase(SM64TestBase): + options = { + "enable_move_rando": Options.EnableMoveRandomizer.option_true, + "area_rando": Options.AreaRandomizer.option_Courses_and_Secrets_Separate + } + + def test_BoB_entrance(self): + bob_level_id = sm64_entrances_to_level["Bob-omb Battlefield"] + # BoB goes to a course, not a secret. + self.assertNotIn(self.world.area_connections[bob_level_id], sm64_level_to_secrets.keys()) + self.assertIn(self.world.area_connections[bob_level_id], sm64_level_to_paintings.keys()) + # BoB goes to level with at least one star without a movement rule. + self.assertIn(self.world.area_connections[bob_level_id], valid_move_randomizer_start_entrances.values()) + + def test_BitFS_entrance(self): + bitfs_level_id = sm64_entrances_to_level["Bowser in the Fire Sea"] + # BitFS does not go to DDD. + self.assertIsNot(self.world.area_connections[bitfs_level_id], sm64_entrances_to_level["Dire, Dire Docks"]) + + def test_WF_entrance(self): + wf_level_id = sm64_entrances_to_level["Whomp's Fortress"] + # WF goes to level with at least one star without a movement rule. + self.assertIn(self.world.area_connections[wf_level_id], valid_move_randomizer_start_entrances.values()) + + +class AllEntrancesMoveTestBase(SM64TestBase): + options = { + "enable_move_rando": Options.EnableMoveRandomizer.option_true, + "area_rando": Options.AreaRandomizer.option_Courses_and_Secrets + } + + def test_BoB_entrance(self): + bob_level_id = sm64_entrances_to_level["Bob-omb Battlefield"] + # BoB goes to level with at least one star without a movement rule. + self.assertIn(self.world.area_connections[bob_level_id], valid_move_randomizer_start_entrances.values()) + + def test_BitFS_entrance(self): + bitfs_level_id = sm64_entrances_to_level["Bowser in the Fire Sea"] + # BitFS does not go to DDD. + self.assertIsNot(self.world.area_connections[bitfs_level_id], sm64_entrances_to_level["Dire, Dire Docks"]) + + def test_WF_entrance(self): + wf_level_id = sm64_entrances_to_level["Whomp's Fortress"] + # WF goes to level with at least one star without a movement rule. + self.assertIn(self.world.area_connections[wf_level_id], valid_move_randomizer_start_entrances.values()) + + def test_CotMC_entrance(self): + cotmc_level_id = sm64_entrances_to_level["Cavern of the Metal Cap"] + # CotMC does not go to HMC. + self.assertIsNot(self.world.area_connections[cotmc_level_id], sm64_entrances_to_level["Hazy Maze Cave"]) + # If BitFS -> HMC, CotMC does not go to DDD. + bitfs_level_id = sm64_entrances_to_level["Bowser in the Fire Sea"] + if self.world.area_connections[bitfs_level_id] == sm64_entrances_to_level["Hazy Maze Cave"]: + self.assertIsNot(self.world.area_connections[cotmc_level_id], sm64_entrances_to_level["Dire, Dire Docks"]) + + +# No Strict Requirements +class NoStrictRequirementsTestBase(SM64TestBase): + options = { + "enable_move_rando": Options.EnableMoveRandomizer.option_true, + "buddy_checks": Options.BuddyChecks.option_true, + "strict_move_requirements": Options.StrictMoveRequirements.option_false, + "strict_cap_requirements": Options.StrictCapRequirements.option_false, + "strict_cannon_requirements": Options.StrictCannonRequirements.option_false, + } From 29a6f40c2b1c126d886f079982e0397209b10480 Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Sat, 9 May 2026 15:50:34 +0100 Subject: [PATCH 41/66] Blasphemous: Increase logic performance (#4585) --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/blasphemous/Items.py | 3 + worlds/blasphemous/Rules.py | 649 +++++++++++++++++++-------------- worlds/blasphemous/__init__.py | 25 +- 3 files changed, 408 insertions(+), 269 deletions(-) diff --git a/worlds/blasphemous/Items.py b/worlds/blasphemous/Items.py index 4843a99f2d12..70c006402ad3 100644 --- a/worlds/blasphemous/Items.py +++ b/worlds/blasphemous/Items.py @@ -739,6 +739,9 @@ class ItemDict(TypedDict): "Broken Left Eye of the Traitor"} } +# Because each item is only in a single group, a reverse lookup table from each item to its group can be created. +group_table_reverse: Dict[str, str] = {item: group for group, items in group_table.items() for item in items} + tears_list: List[str] = [ "Tears of Atonement (500)", "Tears of Atonement (625)", diff --git a/worlds/blasphemous/Rules.py b/worlds/blasphemous/Rules.py index 119e618dc22f..c22857da5297 100644 --- a/worlds/blasphemous/Rules.py +++ b/worlds/blasphemous/Rules.py @@ -1,5 +1,6 @@ -from typing import Dict, List, Tuple, Any, Callable, TYPE_CHECKING +from typing import Dict, List, Tuple, Any, Callable, TYPE_CHECKING, Mapping from BaseClasses import CollectionState +from worlds.generic.Rules import CollectionRule if TYPE_CHECKING: from . import BlasphemousWorld @@ -7,30 +8,145 @@ BlasphemousWorld = object +# Depending on a player's options, some logic can either always be True, or always be False. +# When combining rules together in load_rule(), optimizations can be made by checking whether a rule being combined is +# _always or _never. +def _always(state: CollectionState): + return True + + +def _never(state: CollectionState): + return False + + +def _bool_rule(b) -> CollectionRule: + """Small helper to return the appropriate rule function for a rule that can be pre-calculated""" + if b: + return _always + else: + return _never + + +# Player strengths required to logically beat bosses. +# Mapping is an immutable type, so type hints should warn if attempts are made to modify it. +BOSS_STRENGTHS: Mapping[str, float] = { + "warden": -0.10, + "ten-piedad": 0.05, + "charred-visage": 0.20, + "tres-angustias": 0.15, + "esdras": 0.25, + "melquiades": 0.25, + "exposito": 0.30, + "quirce": 0.35, + "crisanta": 0.50, + "isidora": 0.70, + "sierpes": 0.70, + "amanecida": 0.60, + "laudes": 0.60, + "perpetua": -0.05, + "legionary": 0.20 +} + + class BlasRules: player: int world: BlasphemousWorld string_rules: Dict[str, Callable[[CollectionState], bool]] + upwarp_skips_allowed: bool + mourning_skip_allowed: bool + enemy_skips_allowed: bool + obscure_skips_allowed: bool + precise_skips_allowed: bool + can_enemy_bounce: bool + + # Player strengths required to logically beat bosses, adjusted by the player's difficulty option. + boss_strengths: Mapping[str, float] + + can_enemy_upslash: CollectionRule + can_air_stall: CollectionRule + can_dawn_jump: CollectionRule + can_dive_laser: CollectionRule + can_survive_poison_1: CollectionRule + can_survive_poison_2: CollectionRule + can_survive_poison_3: CollectionRule + def __init__(self, world: "BlasphemousWorld") -> None: self.player = world.player self.world = world self.multiworld = world.multiworld self.indirect_conditions: List[Tuple[str, str]] = [] + difficulty = world.options.difficulty.value + + # Rules that can be fully or partially pre-calculated based on world.options. + + # Special Skips + self.upwarp_skips_allowed = difficulty >= 2 + self.mourning_skip_allowed = difficulty >= 2 + self.enemy_skips_allowed = difficulty >= 2 and not world.options.enemy_randomizer.value + self.obscure_skips_allowed = difficulty >= 2 + self.precise_skips_allowed = difficulty >= 2 + + if difficulty >= 2: + # Beating bosses ends up in logic earlier. + self.boss_strengths = {boss: strength - 0.1 for boss, strength in BOSS_STRENGTHS.items()} + elif difficulty >= 1: + self.boss_strengths = BOSS_STRENGTHS + else: + # Beating bosses ends up in logic later. + self.boss_strengths = {boss: strength + 0.1 for boss, strength in BOSS_STRENGTHS.items()} + + # Enemy tech + if self.enemy_skips_allowed: + self.can_enemy_bounce = True + self.can_enemy_upslash = lambda state: self.combo(state) >= 2 + else: + self.can_enemy_bounce = False + self.can_enemy_upslash = _never + + # Movement tech + if difficulty >= 1: + self.can_air_stall = self.ranged + self.can_dawn_jump = lambda state: self.dawn_heart(state) and self.dash(state) + else: + self.can_air_stall = _never + self.can_dawn_jump = _never + + # Breakable tech + if difficulty >= 2: + self.can_dive_laser = lambda state: self.dive(state) >= 3 + else: + self.can_dive_laser = _never + + # Lung tech + if difficulty >= 2: + self.can_survive_poison_1 = _always + self.can_survive_poison_2 = lambda state: self.lung(state) or self.tiento(state) + self.can_survive_poison_3 = lambda state: self.lung(state) or (self.tiento(state) + and self.total_fervour(state) >= 120) + elif difficulty >= 1: + self.can_survive_poison_1 = lambda state: self.lung(state) or self.tiento(state) + self.can_survive_poison_2 = lambda state: self.lung(state) or self.tiento(state) + self.can_survive_poison_3 = self.lung + else: + self.can_survive_poison_1 = self.lung + self.can_survive_poison_2 = self.lung + self.can_survive_poison_3 = self.lung + + # BrandenEK/Blasphemous.Randomizer/ItemRando/BlasphemousInventory.cs - self.string_rules = { + self.string_rules: dict[str, CollectionRule] = { # Visibility flags - "DoubleJump": lambda state: bool(self.world.options.purified_hand), - "NormalLogic": lambda state: self.world.options.difficulty >= 1, - "NormalLogicAndDoubleJump": lambda state: self.world.options.difficulty >= 1 \ - and bool(self.world.options.purified_hand), - "HardLogic": lambda state: self.world.options.difficulty >= 2, - "HardLogicAndDoubleJump": lambda state: self.world.options.difficulty >= 2 \ - and bool(self.world.options.purified_hand), - "EnemySkips": self.enemy_skips_allowed, - "EnemySkipsAndDoubleJump": lambda state: self.enemy_skips_allowed(state) \ - and bool(self.world.options.purified_hand), + "DoubleJump": _bool_rule(self.world.options.purified_hand.value), + "NormalLogic": _bool_rule(self.world.options.difficulty.value >= 1), + "NormalLogicAndDoubleJump": _bool_rule(self.world.options.difficulty.value >= 1 + and bool(self.world.options.purified_hand.value)), + "HardLogic": _bool_rule(self.world.options.difficulty.value >= 2), + "HardLogicAndDoubleJump": _bool_rule(self.world.options.difficulty.value >= 2 + and bool(self.world.options.purified_hand.value)), + "EnemySkips": _bool_rule(self.enemy_skips_allowed), + "EnemySkipsAndDoubleJump": _bool_rule(self.enemy_skips_allowed and self.world.options.purified_hand.value), # Relics "blood": self.blood, @@ -52,20 +168,20 @@ def __init__(self, world: "BlasphemousWorld") -> None: "cherubs20": lambda state: self.cherubs(state) >= 20, "cherubs38": lambda state: self.cherubs(state) >= 38, - "bones4": lambda state: self.bones(state) >= 4, - "bones8": lambda state: self.bones(state) >= 8, - "bones12": lambda state: self.bones(state) >= 12, - "bones16": lambda state: self.bones(state) >= 16, - "bones20": lambda state: self.bones(state) >= 20, - "bones24": lambda state: self.bones(state) >= 24, - "bones28": lambda state: self.bones(state) >= 28, - "bones30": lambda state: self.bones(state) >= 30, - "bones32": lambda state: self.bones(state) >= 32, - "bones36": lambda state: self.bones(state) >= 36, - "bones40": lambda state: self.bones(state) >= 40, - "bones44": lambda state: self.bones(state) >= 44, - - "tears0": lambda state: True, + "bones4": lambda state: self.bones(state, 4), + "bones8": lambda state: self.bones(state, 8), + "bones12": lambda state: self.bones(state, 12), + "bones16": lambda state: self.bones(state, 16), + "bones20": lambda state: self.bones(state, 20), + "bones24": lambda state: self.bones(state, 24), + "bones28": lambda state: self.bones(state, 28), + "bones30": lambda state: self.bones(state, 30), + "bones32": lambda state: self.bones(state, 32), + "bones36": lambda state: self.bones(state, 36), + "bones40": lambda state: self.bones(state, 40), + "bones44": lambda state: self.bones(state, 44), + + "tears0": _always, # Special items "dash": self.dash, @@ -118,13 +234,13 @@ def __init__(self, world: "BlasphemousWorld") -> None: # skip "dive" # skip "lunge" "chargeBeam": self.charge_beam, - "rangedAttack": lambda state: self.ranged(state) > 0, + "rangedAttack": self.ranged, # Main quest - "holyWounds3": lambda state: self.holy_wounds(state) >= 3, - "masks1": lambda state: self.masks(state) >= 1, - "masks2": lambda state: self.masks(state) >= 2, - "masks3": lambda state: self.masks(state) >= 3, + "holyWounds3": lambda state: self.holy_wounds(state, 3), + "masks1": lambda state: self.masks(state, 1), + "masks2": lambda state: self.masks(state, 2), + "masks3": lambda state: self.masks(state, 3), "guiltBead": self.guilt_bead, # LOTL quest @@ -133,17 +249,17 @@ def __init__(self, world: "BlasphemousWorld") -> None: "hatchedEgg": self.hatched_egg, # Tirso quest - "herbs1": lambda state: self.herbs(state) >= 1, - "herbs2": lambda state: self.herbs(state) >= 2, - "herbs3": lambda state: self.herbs(state) >= 3, - "herbs4": lambda state: self.herbs(state) >= 4, - "herbs5": lambda state: self.herbs(state) >= 5, - "herbs6": lambda state: self.herbs(state) >= 6, + "herbs1": lambda state: self.herbs(state, 1), + "herbs2": lambda state: self.herbs(state, 2), + "herbs3": lambda state: self.herbs(state, 3), + "herbs4": lambda state: self.herbs(state, 4), + "herbs5": lambda state: self.herbs(state, 5), + "herbs6": lambda state: self.herbs(state, 6), # Tentudia quest - "tentudiaRemains1": lambda state: self.tentudia_remains(state) >= 1, - "tentudiaRemains2": lambda state: self.tentudia_remains(state) >= 2, - "tentudiaRemains3": lambda state: self.tentudia_remains(state) >= 3, + "tentudiaRemains1": lambda state: self.tentudia_remains(state, 1), + "tentudiaRemains2": lambda state: self.tentudia_remains(state, 2), + "tentudiaRemains3": lambda state: self.tentudia_remains(state, 3), # Gemino quest "emptyThimble": self.empty_thimble, @@ -151,7 +267,7 @@ def __init__(self, world: "BlasphemousWorld") -> None: "driedFlowers": self.dried_flowers, # Altasgracias quest - "ceremonyItems3": lambda state: self.ceremony_items(state) >= 3, + "ceremonyItems3": lambda state: self.ceremony_items(state, 3), "egg": self.egg, # Redento quest @@ -159,13 +275,13 @@ def __init__(self, world: "BlasphemousWorld") -> None: # skip "knots", not actually used # Cleofas quest - "marksOfRefuge3": lambda state: self.marks_of_refuge(state) >= 3, + "marksOfRefuge3": lambda state: self.marks_of_refuge(state, 3), "cord": self.cord, # Crisanta quest "scapular": self.scapular, "trueHeart": self.true_heart, - "traitorEyes2": lambda state: self.traitor_eyes(state) >= 2, + "traitorEyes2": lambda state: self.traitor_eyes(state, 2), # Jibrael quest "bell": self.bell, @@ -190,32 +306,32 @@ def __init__(self, world: "BlasphemousWorld") -> None: "canSurvivePoison3": self.can_survive_poison_3, # Enemy tech - "canEnemyBounce": self.can_enemy_bounce, + "canEnemyBounce": _bool_rule(self.can_enemy_bounce), "canEnemyUpslash": self.can_enemy_upslash, # Reaching rooms - "guiltRooms1": lambda state: self.guilt_rooms(state) >= 1, - "guiltRooms2": lambda state: self.guilt_rooms(state) >= 2, - "guiltRooms3": lambda state: self.guilt_rooms(state) >= 3, - "guiltRooms4": lambda state: self.guilt_rooms(state) >= 4, - "guiltRooms5": lambda state: self.guilt_rooms(state) >= 5, - "guiltRooms6": lambda state: self.guilt_rooms(state) >= 6, - "guiltRooms7": lambda state: self.guilt_rooms(state) >= 7, - - "swordRooms1": lambda state: self.sword_rooms(state) >= 1, - "swordRooms2": lambda state: self.sword_rooms(state) >= 2, - "swordRooms3": lambda state: self.sword_rooms(state) >= 3, - "swordRooms4": lambda state: self.sword_rooms(state) >= 4, - "swordRooms5": lambda state: self.sword_rooms(state) >= 5, - "swordRooms6": lambda state: self.sword_rooms(state) >= 6, - "swordRooms7": lambda state: self.sword_rooms(state) >= 7, - - "redentoRooms2": lambda state: self.redento_rooms(state) >= 2, - "redentoRooms3": lambda state: self.redento_rooms(state) >= 3, - "redentoRooms4": lambda state: self.redento_rooms(state) >= 4, - "redentoRooms5": lambda state: self.redento_rooms(state) >= 5, - - "miriamRooms5": lambda state: self.miriam_rooms(state) >= 5, + "guiltRooms1": lambda state: self.guilt_rooms(state, 1), + "guiltRooms2": lambda state: self.guilt_rooms(state, 2), + "guiltRooms3": lambda state: self.guilt_rooms(state, 3), + "guiltRooms4": lambda state: self.guilt_rooms(state, 4), + "guiltRooms5": lambda state: self.guilt_rooms(state, 5), + "guiltRooms6": lambda state: self.guilt_rooms(state, 6), + "guiltRooms7": lambda state: self.guilt_rooms(state, 7), + + "swordRooms1": lambda state: self.sword_rooms(state, 1), + "swordRooms2": lambda state: self.sword_rooms(state, 2), + "swordRooms3": lambda state: self.sword_rooms(state, 3), + "swordRooms4": lambda state: self.sword_rooms(state, 4), + "swordRooms5": lambda state: self.sword_rooms(state, 5), + "swordRooms6": lambda state: self.sword_rooms(state, 6), + "swordRooms7": lambda state: self.sword_rooms(state, 7), + + "redentoRooms2": lambda state: self.redento_rooms(state, 2), + "redentoRooms3": lambda state: self.redento_rooms(state, 3), + "redentoRooms4": lambda state: self.redento_rooms(state, 4), + "redentoRooms5": lambda state: self.redento_rooms(state, 5), + + "miriamRooms5": self.all_miriam_rooms, "amanecidaRooms1": lambda state: self.amanecida_rooms(state) >= 1, "amanecidaRooms2": lambda state: self.amanecida_rooms(state) >= 2, @@ -254,11 +370,11 @@ def __init__(self, world: "BlasphemousWorld") -> None: "openedBotSSLadder": self.opened_botss_ladder, # Special skips - "upwarpSkipsAllowed": self.upwarp_skips_allowed, - "mourningSkipAllowed": self.mourning_skip_allowed, - "enemySkipsAllowed": self.enemy_skips_allowed, - "obscureSkipsAllowed": self.obscure_skips_allowed, - "preciseSkipsAllowed": self.precise_skips_allowed, + "upwarpSkipsAllowed": _bool_rule(self.upwarp_skips_allowed), + "mourningSkipAllowed": _bool_rule(self.mourning_skip_allowed), + "enemySkipsAllowed": _bool_rule(self.enemy_skips_allowed), + "obscureSkipsAllowed": _bool_rule(self.obscure_skips_allowed), + "preciseSkipsAllowed": _bool_rule(self.precise_skips_allowed), # Bosses "canBeatBrotherhoodBoss": self.can_beat_brotherhood_boss, @@ -498,30 +614,74 @@ def req_is_region(self, string: str) -> bool: def load_rule(self, obj_is_region: bool, name: str, obj: Dict[str, Any]) -> Callable[[CollectionState], bool]: clauses = [] + clauses_are_impossible_if_empty = False + rule_indirect_conditions = [] for clause in obj["logic"]: reqs = [] + clause_indirect_conditions = [] + clause_is_impossible = False for req in clause["item_requirements"]: if self.req_is_region(req): if obj_is_region: # add to indirect conditions if object and requirement are doors - self.indirect_conditions.append((req, f"{name} -> {obj['target']}")) + clause_indirect_conditions.append((req, f"{name} -> {obj['target']}")) reqs.append(lambda state, req=req: state.can_reach_region(req, self.player)) else: + string_rule = self.string_rules[req] + if string_rule is _never: + # This clause is not possible with the options this player has chosen. + clause_is_impossible = True + break + elif string_rule is _always: + # Don't need to add a rule that is always True with the options this player has chosen. + # Continue to the next requirement. + continue if obj_is_region and req in self.indirect_regions: # add to indirect conditions if object is door and requirement has list of regions for region in self.indirect_regions[req]: - self.indirect_conditions.append((region, f"{name} -> {obj['target']}")) + clause_indirect_conditions.append((region, f"{name} -> {obj['target']}")) reqs.append(self.string_rules[req]) + if clause_is_impossible: + # At least one clause was impossible, so if all clauses were impossible, the entire rule is impossible. + clauses_are_impossible_if_empty = True + # Continue to the next clause. + continue + rule_indirect_conditions.extend(clause_indirect_conditions) + + # Combine the requirements if there are multiple. + # Requirements are AND-ed together. if len(reqs) == 1: clauses.append(reqs[0]) else: - clauses.append(lambda state, reqs=reqs: all(req(state) for req in reqs)) + def req_func(state, reqs=reqs): + for req in reqs: + if not req(state): + return False + return True + clauses.append(req_func) + + # Combine the clauses if there are multiple. + # Clauses are OR-ed together. if not clauses: - return lambda state: True + # There is no need to register the indirect conditions if it turns out the rule is impossible or always + # possible. + rule_indirect_conditions.clear() + if clauses_are_impossible_if_empty: + to_return = _never + else: + to_return = _always elif len(clauses) == 1: - return clauses[0] + to_return = clauses[0] else: - return lambda state: any(clause(state) for clause in clauses) + def clause_func(state, clauses=clauses): + for clause in clauses: + if clause(state): + return True + return False + to_return = clause_func + # Update the list of indirect conditions to add. + self.indirect_conditions.extend(rule_indirect_conditions) + return to_return # Relics def blood(self, state: CollectionState) -> bool: @@ -565,8 +725,10 @@ def wood_key(self, state: CollectionState) -> bool: def cherubs(self, state: CollectionState) -> int: return state.count("Child of Moonlight", self.player) - def bones(self, state: CollectionState) -> int: - return state.count_group_unique("bones", self.player) + def bones(self, state: CollectionState, count: int) -> bool: + # Count of unique items in the "bones" item group that have been collected into state. + # BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed. + return state.has("bones", self.player, count) # def tears(): @@ -594,7 +756,7 @@ def dawn_heart(self, state: CollectionState) -> bool: # Health boosts def flasks(self, state: CollectionState) -> int: - doors = { + doors = ( "D01Z05S05[SW]", "D02Z02S04[W]", "D03Z02S08[W]", @@ -602,10 +764,11 @@ def flasks(self, state: CollectionState) -> int: "D04Z02S13[W]", "D05Z01S08[NW]", "D20Z01S07[NE]" - } - - return state.count("Empty Bile Vessel", self.player) \ - if sum(state.can_reach_region(door, self.player) for door in doors) >= 1 else 0 + ) + for door in doors: + if state.can_reach_region(door, self.player): + return state.count("Empty Bile Vessel", self.player) + return 0 def quicksilver(self, state: CollectionState) -> int: return state.count("Quicksilver", self.player) if state.can_reach_region("D01Z05S01[W]", self.player) else 0 @@ -613,7 +776,7 @@ def quicksilver(self, state: CollectionState) -> int: # Puzzles def red_wax(self, state: CollectionState) -> int: return state.count("Bead of Red Wax", self.player) - + def blue_wax(self, state: CollectionState) -> int: return state.count("Bead of Blue Wax", self.player) @@ -670,7 +833,7 @@ def any_small_prayer(self, state: CollectionState) -> bool: or self.cante(state) or self.cantina(state) or self.tiento(state) - or state.has_any({ + or state.has_any(( "Campanillero to the Sons of the Aurora", "Mirabras of the Return to Port", "Romance to the Crimson Mist", @@ -678,7 +841,7 @@ def any_small_prayer(self, state: CollectionState) -> bool: "Seguiriya to your Eyes like Stars", "Verdiales of the Forsaken Hamlet", "Zambra to the Resplendent Crown" - }, self.player) + ), self.player) ) def pillar(self, state: CollectionState) -> bool: @@ -710,8 +873,8 @@ def combo(self, state: CollectionState) -> int: def charged(self, state: CollectionState) -> int: return state.count("Charged Skill", self.player) - def ranged(self, state: CollectionState) -> int: - return state.count("Ranged Skill", self.player) + def ranged(self, state: CollectionState) -> bool: + return state.has("Ranged Skill", self.player) def dive(self, state: CollectionState) -> int: return state.count("Dive Skill", self.player) @@ -723,11 +886,15 @@ def charge_beam(self, state: CollectionState) -> bool: return self.charged(state) >= 3 # Main quest - def holy_wounds(self, state: CollectionState) -> int: - return state.count_group_unique("wounds", self.player) + def holy_wounds(self, state: CollectionState, count: int) -> bool: + # Count of unique items in the "wounds" item group that have been collected into state. + # BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed. + return state.has("wounds", self.player, count) - def masks(self, state: CollectionState) -> int: - return state.count_group_unique("masks", self.player) + def masks(self, state: CollectionState, count: int) -> bool: + # Count of unique items in the "masks" item group that have been collected into state. + # BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed. + return state.has("masks", self.player, count) def guilt_bead(self, state: CollectionState) -> bool: return state.has("Weight of True Guilt", self.player) @@ -743,12 +910,16 @@ def hatched_egg(self, state: CollectionState) -> bool: return state.has("Hatched Egg of Deformity", self.player) # Tirso quest - def herbs(self, state: CollectionState) -> int: - return state.count_group_unique("tirso", self.player) + def herbs(self, state: CollectionState, count: int) -> bool: + # Count of unique items in the "tirso" item group that have been collected into state. + # BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed. + return state.has("tirso", self.player, count) # Tentudia quest - def tentudia_remains(self, state: CollectionState) -> int: - return state.count_group_unique("tentudia", self.player) + def tentudia_remains(self, state: CollectionState, count: int) -> bool: + # Count of unique items in the "tentudia" item group that have been collected into state. + # BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed. + return state.has("tentudia", self.player, count) # Gemino quest def empty_thimble(self, state: CollectionState) -> bool: @@ -761,23 +932,29 @@ def dried_flowers(self, state: CollectionState) -> bool: return state.has("Dried Flowers bathed in Tears", self.player) # Altasgracias quest - def ceremony_items(self, state: CollectionState) -> int: - return state.count_group_unique("egg", self.player) + def ceremony_items(self, state: CollectionState, count: int) -> bool: + # Count of unique items in the "egg" item group that have been collected into state. + # BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed. + return state.has("egg", self.player, count) def egg(self, state: CollectionState) -> bool: return state.has("Egg of Deformity", self.player) # Redento quest - def limestones(self, state: CollectionState) -> int: - return state.count_group_unique("toe", self.player) + def limestones(self, state: CollectionState, count: int) -> bool: + # Count of unique items in the "toe" item group that have been collected into state. + # BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed. + return state.has("toe", self.player, count) def knots(self, state: CollectionState) -> int: return state.count("Knot of Rosary Rope", self.player) if state.can_reach_region("D17Z01S07[NW]", self.player)\ else 0 # Cleofas quest - def marks_of_refuge(self, state: CollectionState) -> int: - return state.count_group_unique("marks", self.player) + def marks_of_refuge(self, state: CollectionState, count: int) -> bool: + # Count of unique items in the "marks" item group that have been collected into state. + # BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed. + return state.has("marks", self.player, count) def cord(self, state: CollectionState) -> bool: return state.has("Cord of the True Burying", self.player) @@ -789,8 +966,10 @@ def scapular(self, state: CollectionState) -> bool: def true_heart(self, state: CollectionState) -> bool: return state.has("Apodictic Heart of Mea Culpa", self.player) - def traitor_eyes(self, state: CollectionState) -> int: - return state.count_group_unique("eye", self.player) + def traitor_eyes(self, state: CollectionState, count: int) -> bool: + # Count of unique items in the "eye" item group that have been collected into state. + # BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed. + return state.has("eye", self.player, count) # Jibrael quest def bell(self, state: CollectionState) -> bool: @@ -800,19 +979,6 @@ def verses(self, state: CollectionState) -> int: return state.count("Verses Spun from Gold", self.player) # Movement tech - def can_air_stall(self, state: CollectionState) -> bool: - return ( - self.ranged(state) > 0 - and self.world.options.difficulty >= 1 - ) - - def can_dawn_jump(self, state: CollectionState) -> bool: - return ( - self.dawn_heart(state) - and self.dash(state) - and self.world.options.difficulty >= 1 - ) - def can_water_jump(self, state: CollectionState) -> bool: return ( self.nail(state) @@ -828,12 +994,6 @@ def can_break_holes(self, state: CollectionState) -> bool: or self.can_use_any_prayer(state) ) - def can_dive_laser(self, state: CollectionState) -> bool: - return ( - self.dive(state) >= 3 - and self.world.options.difficulty >= 2 - ) - # Root tech def can_walk_on_root(self, state: CollectionState) -> bool: return self.root(state) @@ -844,40 +1004,6 @@ def can_climb_on_root(self, state: CollectionState) -> bool: and self.wall_climb(state) ) - # Lung tech - def can_survive_poison_1(self, state: CollectionState) -> bool: - return ( - self.lung(state) - or self.world.options.difficulty >= 1 - and self.tiento(state) - or self.world.options.difficulty >= 2 - ) - - def can_survive_poison_2(self, state: CollectionState) -> bool: - return ( - self.lung(state) - or self.world.options.difficulty >= 1 - and self.tiento(state) - ) - - def can_survive_poison_3(self, state: CollectionState) -> bool: - return ( - self.lung(state) - or self.world.options.difficulty >= 2 - and self.tiento(state) - and self.total_fervour(state) >= 120 - ) - - # Enemy tech - def can_enemy_bounce(self, state: CollectionState) -> bool: - return self.enemy_skips_allowed(state) - - def can_enemy_upslash(self, state: CollectionState) -> bool: - return ( - self.combo(state) >= 2 - and self.enemy_skips_allowed(state) - ) - # Crossing gaps def can_cross_gap_1(self, state: CollectionState) -> bool: return ( @@ -1021,7 +1147,7 @@ def broke_jondo_bell_e(self, state: CollectionState) -> bool: or state.can_reach_region("D03Z02S03[E]", self.player) and ( self.can_cross_gap_5(state) - or self.can_enemy_bounce(state) + or self.can_enemy_bounce and self.can_cross_gap_3(state) ) ) @@ -1067,25 +1193,6 @@ def opened_botss_ladder(self, state: CollectionState) -> bool: or state.can_reach_region("D17BZ02S01[FrontR]", self.player) ) - # Special skips - def upwarp_skips_allowed(self, state: CollectionState) -> bool: - return self.world.options.difficulty >= 2 - - def mourning_skip_allowed(self, state: CollectionState) -> bool: - return self.world.options.difficulty >= 2 - - def enemy_skips_allowed(self, state: CollectionState) -> bool: - return ( - self.world.options.difficulty >= 2 - and not self.world.options.enemy_randomizer - ) - - def obscure_skips_allowed(self, state: CollectionState) -> bool: - return self.world.options.difficulty >= 2 - - def precise_skips_allowed(self, state: CollectionState) -> bool: - return self.world.options.difficulty >= 2 - # Bosses def can_beat_brotherhood_boss(self, state: CollectionState) -> bool: return ( @@ -1183,18 +1290,18 @@ def can_beat_mourning_boss(self, state: CollectionState) -> bool: and state.can_reach_region("D20Z02S07[W]", self.player) ) - def can_beat_graveyard_boss(self, state: CollectionState) -> bool: + def can_beat_graveyard_boss(self, state: CollectionState, player_strength: float | None = None) -> bool: return ( - self.has_boss_strength(state, "amanecida") + self.has_boss_strength(state, "amanecida", player_strength) and self.wall_climb(state) and state.can_reach_region("D01Z06S01[Santos]", self.player) and state.can_reach_region("D02Z03S18[NW]", self.player) and state.can_reach_region("D02Z02S03[NE]", self.player) ) - def can_beat_jondo_boss(self, state: CollectionState) -> bool: + def can_beat_jondo_boss(self, state: CollectionState, player_strength: float | None = None) -> bool: return ( - self.has_boss_strength(state, "amanecida") + self.has_boss_strength(state, "amanecida", player_strength) and state.can_reach_region("D01Z06S01[Santos]", self.player) and ( state.can_reach_region("D20Z01S06[NE]", self.player) @@ -1206,9 +1313,9 @@ def can_beat_jondo_boss(self, state: CollectionState) -> bool: ) ) - def can_beat_patio_boss(self, state: CollectionState) -> bool: + def can_beat_patio_boss(self, state: CollectionState, player_strength: float | None = None) -> bool: return ( - self.has_boss_strength(state, "amanecida") + self.has_boss_strength(state, "amanecida", player_strength) and state.can_reach_region("D01Z06S01[Santos]", self.player) and state.can_reach_region("D06Z01S02[W]", self.player) and ( @@ -1218,9 +1325,9 @@ def can_beat_patio_boss(self, state: CollectionState) -> bool: ) ) - def can_beat_wall_boss(self, state: CollectionState) -> bool: + def can_beat_wall_boss(self, state: CollectionState, player_strength: float | None = None) -> bool: return ( - self.has_boss_strength(state, "amanecida") + self.has_boss_strength(state, "amanecida", player_strength) and state.can_reach_region("D01Z06S01[Santos]", self.player) and state.can_reach_region("D09Z01S09[Cell24]", self.player) and ( @@ -1244,8 +1351,7 @@ def can_beat_perpetua(self, state: CollectionState) -> bool: def can_beat_legionary(self, state: CollectionState) -> bool: return self.has_boss_strength(state, "legionary") - - def has_boss_strength(self, state: CollectionState, boss: str) -> bool: + def get_player_strength(self, state: CollectionState) -> float: life: int = state.count("Life Upgrade", self.player) sword: int = state.count("Mea Culpa Upgrade", self.player) fervour: int = state.count("Fervour Upgrade", self.player) @@ -1259,30 +1365,16 @@ def has_boss_strength(self, state: CollectionState, boss: str) -> bool: + min(8, flasks) * 0.15 / 8 + min(5, quicksilver) * 0.15 / 5 ) + return player_strength - bosses: Dict[str, float] = { - "warden": -0.10, - "ten-piedad": 0.05, - "charred-visage": 0.20, - "tres-angustias": 0.15, - "esdras": 0.25, - "melquiades": 0.25, - "exposito": 0.30, - "quirce": 0.35, - "crisanta": 0.50, - "isidora": 0.70, - "sierpes": 0.70, - "amanecida": 0.60, - "laudes": 0.60, - "perpetua": -0.05, - "legionary": 0.20 - } - boss_strength: float = bosses[boss] - return player_strength >= (boss_strength - 0.10 if self.world.options.difficulty >= 2 else - (boss_strength if self.world.options.difficulty >= 1 else boss_strength + 0.10)) + def has_boss_strength(self, state: CollectionState, boss: str, player_strength: float | None = None) -> bool: + if player_strength is None: + return self.get_player_strength(state) >= self.boss_strengths[boss] + else: + return player_strength >= self.boss_strengths[boss] - def guilt_rooms(self, state: CollectionState) -> int: - doors = [ + def guilt_rooms(self, state: CollectionState, count: int) -> bool: + doors = ( "D01Z04S01[NE]", "D02Z02S11[W]", "D03Z03S02[NE]", @@ -1290,20 +1382,25 @@ def guilt_rooms(self, state: CollectionState) -> int: "D05Z01S05[NE]", "D09Z01S05[W]", "D17Z01S04[W]", - ] + ) - return sum(state.can_reach_region(door, self.player) for door in doors) - - def sword_rooms(self, state: CollectionState) -> int: - doors = [ - ["D01Z02S07[E]", "D01Z02S02[SW]"], - ["D20Z01S04[E]", "D01Z05S23[W]"], - ["D02Z03S02[NE]"], - ["D04Z02S21[NE]"], - ["D05Z01S21[NW]"], - ["D06Z01S15[NE]"], - ["D17Z01S07[SW]"] - ] + total: int = 0 + for door in doors: + total += state.can_reach_region(door, self.player) + if total >= count: + return True + return False + + def sword_rooms(self, state: CollectionState, count: int) -> bool: + doors = ( + ("D01Z02S07[E]", "D01Z02S02[SW]"), + ("D20Z01S04[E]", "D01Z05S23[W]"), + ("D02Z03S02[NE]",), + ("D04Z02S21[NE]",), + ("D05Z01S21[NW]",), + ("D06Z01S15[NE]",), + ("D17Z01S07[SW]",) + ) total: int = 0 for subdoors in doors: @@ -1311,72 +1408,90 @@ def sword_rooms(self, state: CollectionState) -> int: if state.can_reach_region(door, self.player): total += 1 break + if total >= count: + return True - return total + return False - def redento_rooms(self, state: CollectionState) -> int: - if ( - state.can_reach_region("D03Z01S04[E]", self.player) - or state.can_reach_region("D03Z02S10[N]", self.player) + def redento_rooms(self, state: CollectionState, count: int) -> bool: + if not ( + state.can_reach_region("D03Z01S04[E]", self.player) + or state.can_reach_region("D03Z02S10[N]", self.player) ): - if ( + # Realistically, count should never be zero or negative. + return count < 1 + + if count == 1: + return True + + if not ( state.can_reach_region("D17Z01S05[S]", self.player) or state.can_reach_region("D17BZ02S01[FrontR]", self.player) - ): - if ( - state.can_reach_region("D01Z03S04[E]", self.player) - or state.can_reach_region("D08Z01S01[W]", self.player) - ): - if ( - state.can_reach_region("D04Z01S03[E]", self.player) - or state.can_reach_region("D04Z02S01[W]", self.player) - or state.can_reach_region("D06Z01S18[-Cherubs]", self.player) - ): - if ( - self.knots(state) >= 1 - and self.limestones(state) >= 3 - and ( - state.can_reach_region("D04Z02S08[E]", self.player) - or state.can_reach_region("D04BZ02S01[Redento]", self.player) - ) - ): - return 5 - return 4 - return 3 - return 2 - return 1 - return 0 - - def miriam_rooms(self, state: CollectionState) -> int: - doors = [ + ): + return False + + if count == 2: + return True + + if not (state.can_reach_region("D01Z03S04[E]", self.player) + or state.can_reach_region("D08Z01S01[W]", self.player)): + return False + + if count == 3: + return True + + if not (state.can_reach_region("D04Z01S03[E]", self.player) + or state.can_reach_region("D04Z02S01[W]", self.player) + or state.can_reach_region("D06Z01S18[-Cherubs]", self.player)): + return False + + if count == 4: + return True + + if not ( + self.knots(state) >= 1 + and self.limestones(state, 3) + and (state.can_reach_region("D04Z02S08[E]", self.player) + or state.can_reach_region("D04BZ02S01[Redento]", self.player)) + ): + return False + + return count == 5 + + def all_miriam_rooms(self, state: CollectionState) -> bool: + doors = ( "D02Z03S07[NWW]", "D03Z03S07[NW]", "D04Z04S01[E]", "D05Z01S06[W]", "D06Z01S17[E]" - ] + ) - return sum(state.can_reach_region(door, self.player) for door in doors) + for door in doors: + if not state.can_reach_region(door, self.player): + return False + return True def amanecida_rooms(self, state: CollectionState) -> int: + player_strength = self.get_player_strength(state) total: int = 0 - if self.can_beat_graveyard_boss(state): + if self.can_beat_graveyard_boss(state, player_strength): total += 1 - if self.can_beat_jondo_boss(state): + if self.can_beat_jondo_boss(state, player_strength): total += 1 - if self.can_beat_patio_boss(state): + if self.can_beat_patio_boss(state, player_strength): total += 1 - if self.can_beat_wall_boss(state): + if self.can_beat_wall_boss(state, player_strength): total += 1 return total def chalice_rooms(self, state: CollectionState) -> int: - doors = [ - ["D03Z01S02[E]", "D01Z05S02[W]", "D20Z01S03[N]"], - ["D05Z01S11[SE]", "D05Z02S02[NW]"], - ["D09Z01S09[E]", "D09Z01S10[W]", "D09Z01S08[SE]", "D09Z01S02[SW]"] - ] + doors = ( + ("D03Z01S02[E]", "D01Z05S02[W]", "D20Z01S03[N]"), + ("D05Z01S11[SE]", "D05Z02S02[NW]"), + ("D09Z01S09[E]", "D09Z01S10[W]", "D09Z01S08[SE]", "D09Z01S02[SW]") + ) total: int = 0 for subdoors in doors: diff --git a/worlds/blasphemous/__init__.py b/worlds/blasphemous/__init__.py index 9dffc6c6d286..42ed49dbe071 100644 --- a/worlds/blasphemous/__init__.py +++ b/worlds/blasphemous/__init__.py @@ -1,9 +1,9 @@ from typing import Dict, List, Set, Any from collections import Counter -from BaseClasses import Region, Location, Item, Tutorial, ItemClassification +from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, CollectionState from Options import OptionError from worlds.AutoWorld import World, WebWorld -from .Items import base_id, item_table, group_table, tears_list, reliquary_set +from .Items import base_id, item_table, group_table, tears_list, reliquary_set, group_table_reverse from .Locations import location_names from .Rules import BlasRules from worlds.generic.Rules import set_rule @@ -216,6 +216,27 @@ def place_items_from_dict(self, option_dict: Dict[str, str]): for loc, item in option_dict.items(): self.get_location(loc).place_locked_item(self.create_item(item)) + def collect(self, state: CollectionState, item: Item) -> bool: + changed = super().collect(state, item) + if changed: + name = item.name + if name in group_table_reverse and state.count(name, self.player) == 1: + # Count was 0 before super().collect(). + group_name = group_table_reverse[name] + # Increase unique count for items in this group. + state.prog_items[self.player][group_name] += 1 + return changed + + def remove(self, state: CollectionState, item: Item) -> bool: + changed = super().remove(state, item) + if changed: + name = item.name + if name in group_table_reverse and state.count(name, self.player) == 0: + # Count was 1 before super().remove(). + group_name = group_table_reverse[name] + # Decrease unique count for items in this group. + state.prog_items[self.player][group_name] -= 1 + return changed def create_regions(self) -> None: multiworld = self.multiworld From e0810022f830195fe2921fd10b14132a45569fa2 Mon Sep 17 00:00:00 2001 From: josephwhite <22449090+josephwhite@users.noreply.github.com> Date: Sat, 9 May 2026 10:51:56 -0400 Subject: [PATCH 42/66] WebHost: Fix adding weighted text options (#5116) --- WebHostLib/static/assets/weightedOptions.js | 22 +++++++++++++++---- .../templates/weightedOptions/macros.html | 12 +++++----- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/WebHostLib/static/assets/weightedOptions.js b/WebHostLib/static/assets/weightedOptions.js index 0417ab174b0e..4aa99963d5d6 100644 --- a/WebHostLib/static/assets/weightedOptions.js +++ b/WebHostLib/static/assets/weightedOptions.js @@ -123,12 +123,26 @@ window.addEventListener('load', () => { }); const addRangeRow = (optionName) => { - const inputQuery = `input[type=number][data-option="${optionName}"].range-option-value`; + const inputQuery = `input[data-option="${optionName}"]`; const inputTarget = document.querySelector(inputQuery); const newValue = inputTarget.value; - if (!/^-?\d+$/.test(newValue)) { - alert('Range values must be a positive or negative integer!'); - return; + switch (inputTarget.type) { + case 'number': + if (!/^-?\d+$/.test(newValue)) { + alert('Range values must be a positive or negative integer!'); + return; + } + break; + case 'text': + if (newValue === "") { + alert('Range values for text must be a non-empty string!'); + return; + } + break; + default: + console.error(`Found unsupported input type: ${inputTarget.type}`); + return; + break; } inputTarget.value = ''; const tBody = document.querySelector(`table[data-option="${optionName}"].range-rows tbody`); diff --git a/WebHostLib/templates/weightedOptions/macros.html b/WebHostLib/templates/weightedOptions/macros.html index 1d485a24def8..0c2099297395 100644 --- a/WebHostLib/templates/weightedOptions/macros.html +++ b/WebHostLib/templates/weightedOptions/macros.html @@ -71,10 +71,10 @@
This option allows custom values only. Please enter your desired values below.
- - + +
- +
{% if option.default %} {{ RangeRow(option_name, option, option.default, option.default) }} @@ -88,11 +88,11 @@
Custom values are also allowed for this option. To create one, enter it into the input box below.
- - + +
-
+
{% for id, name in option.name_lookup.items() %} {% if name != 'random' %} From d0407a74d65ed1cdc9519f4de996406316225f0a Mon Sep 17 00:00:00 2001 From: Christina <123173340+Paperkoopa@users.noreply.github.com> Date: Sat, 9 May 2026 16:54:20 +0200 Subject: [PATCH 43/66] Docs: Added new documentation for DS3's enemy randomization (random_enemy_preset) (#5738) --------- Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> --- worlds/dark_souls_3/Options.py | 4 +- worlds/dark_souls_3/docs/en_Dark Souls III.md | 12 +- .../docs/enemy-randomization_en.md | 411 ++++++++++++++++++ worlds/dark_souls_3/docs/items_en.md | 4 +- worlds/dark_souls_3/docs/locations_en.md | 4 +- worlds/dark_souls_3/docs/setup_en.md | 7 + 6 files changed, 436 insertions(+), 6 deletions(-) create mode 100644 worlds/dark_souls_3/docs/enemy-randomization_en.md diff --git a/worlds/dark_souls_3/Options.py b/worlds/dark_souls_3/Options.py index 52222e4d3981..afe1dd450a62 100644 --- a/worlds/dark_souls_3/Options.py +++ b/worlds/dark_souls_3/Options.py @@ -296,11 +296,9 @@ class ImpatientMimicsOption(Toggle): class RandomEnemyPresetOption(OptionDict): """The YAML preset for the static enemy randomizer. - See the static randomizer documentation in `randomizer\\presets\\README.txt` for details. + See the online enemy randomization documentation for all available options. Include this as nested YAML. For example: - .. code-block:: YAML - random_enemy_preset: RemoveSource: Ancient Wyvern; Darkeater Midir DontRandomize: Iudex Gundyr diff --git a/worlds/dark_souls_3/docs/en_Dark Souls III.md b/worlds/dark_souls_3/docs/en_Dark Souls III.md index 06227226aafe..ea7934a6981f 100644 --- a/worlds/dark_souls_3/docs/en_Dark Souls III.md +++ b/worlds/dark_souls_3/docs/en_Dark Souls III.md @@ -1,9 +1,11 @@ # Dark Souls III -Game Page | [Items] | [Locations] +Game Page | [Setup] | [Items] | [Locations] | [Enemy Randomization] +[Setup]: /tutorial/Dark%20Souls%20III/setup/en [Items]: /tutorial/Dark%20Souls%20III/items/en [Locations]: /tutorial/Dark%20Souls%20III/locations/en +[Enemy Randomization]: /tutorial/Dark%20Souls%20III/enemy-randomization/en ## What do I need to do to randomize DS3? @@ -138,6 +140,14 @@ Check out the [item guide], which explains the named groups available for items. [item guide]: /tutorial/Dark%20Souls%20III/items/en +## How can I change what enemies get randomized? + +The [enemy randomization guide] explains how to further customize enemy randomization +for challenge runs or convenience. You can target specific enemies or entire +categories and even remove annoying enemy types outright. + +[enemy randomization guide]: /tutorial/Dark%20Souls%20III/enemy-randomization/en + ## What's new from 2.x.x? Version 3.0.0 of the Dark Souls III Archipelago client has a number of diff --git a/worlds/dark_souls_3/docs/enemy-randomization_en.md b/worlds/dark_souls_3/docs/enemy-randomization_en.md new file mode 100644 index 000000000000..dd545872250f --- /dev/null +++ b/worlds/dark_souls_3/docs/enemy-randomization_en.md @@ -0,0 +1,411 @@ +# Dark Souls III Enemy Randomization + +[Game Page] | [Setup] | [Items] | [Locations] | Enemy Randomization + +[Game Page]: /games/Dark%20Souls%20III/info/en +[Setup]: /tutorial/Dark%20Souls%20III/setup/en +[Items]: /tutorial/Dark%20Souls%20III/items/en +[Locations]: /tutorial/Dark%20Souls%20III/locations/en + +If `randomize_enemies` in your Dark Souls 3 player config YAML is enabled, bosses, minibosses and basic enemies will +be shuffled with themselves respectively. + +To further customize enemy randomization beyond that, there is a section called `random_enemy_preset`. + +This tutorial will show all the ways how to configure that preset. + +## Table of Contents +- [The Basics](#the-basics) +- [Individual Assignments](#individual-assignments) +- [Pools](#pools) + * [Pool Groups](#pool-groups) + + [RandomByType](#randombytype) + * [Weights](#weights) +- [Settings](#settings) + * [Boss](#boss) + * [Miniboss](#miniboss) + * [Basic](#basic) + + [BuffBasicEnemiesAsBosses](#buffbasicenemiesasbosses) + * [Enemies](#enemies) + * [DontRandomize](#dontrandomize) + * [RemoveSource](#removesource) + * [OopsAll](#oopsall) +- [Enemy Categories](#enemy-categories) + +## The Basics + +There are two main ways to assign an enemy to be randomized: [individual enemy assignments](#individual-assignments) +to target a singular enemy placement and setting up [Pools](#pools) to target a category of enemies. + +Custom pools are recommended unless you specifically want to single out one enemy placement. + +All bosses also have their own category, so individual assignment is not necessary in those cases. + +Be aware of correct indentation of your YAML file. Every example in this document will need to be nested under the +`random_enemy_preset:` section. + +Disable the preset by leaving just empty brackets `{}`. Like usual with YAML, you can add comments by using `#`. + +For further examples, check out the "presets" folder of the standalone randomizer. + +## Individual Assignments + +Individual enemy assignment allows you to target individual enemies, rather than a category as under pools. + +This overrides pools and any other configuration, will usually ignore progression, and can possibly cause you to have to +fight Yhorm the Giant without Storm Ruler. + +You use it in the [`Enemies`](#enemies) section by selecting a specific enemy using its unique +ID, or its specific name followed by its ID. + +See the '/randomizer/preset/Template.txt' file of the static randomizer for all available IDs. + +There are also some special target names available for individual assignments: + +- `any`: This is the default and allows any enemy in the pool to appear there. + +- `norandom`: Assigns an enemy to itself. This has the same effect as adding the enemy name to [`DontRandomize`](#dontrandomize). + +## Pools + +A pool is a collection of enemies. A pool can both be a randomization target and an eligible group of random enemies to +be drawn from for randomization. See [Enemy Categories](#enemy-categories) for all available pools. + +Pool assignment generally respects progression, like requiring Storm Ruler to be accessible before Yhorm the Giant. + +By default, using a boss as another boss, or a miniboss as another miniboss, takes the source enemy out of the default +pool for that category, so each enemy will still be used once if possible. However, the enemy can still appear more +than once if used in a custom pool. + +### Pool Groups + +Pools can be joined into a pool group by joining several names, separated by a semicolon. + +```yaml +# All basic enemies are just different hollows now +Basic: +- Weight: 100 + Pool: Hollow Soldiers; Large Hollow Soldiers +``` + +#### RandomByType + +By default, selection will be random across all eligible enemies. In our example above it would select from: + +- Hollow Soldier +- Road of Sacrifices Hollow Soldier +- Cathedral Hollow Soldier +- Lothric Castle Hollow Soldier +- Grand Archives Hollow Soldier + +and + +- Large Hollow Soldier +- Cathedral Large Hollow Soldier +- Lothric Castle Large Hollow Soldier + +However, this would make it more likely to select a regular soldier instead of a large one (5 out of 8), just because +there are fewer entries in the latter category. + +You can specify `RandomByType: true` to select randomly from the list itself (Hollow Soldiers, Large Hollow Soldiers) +and make our previous example a true 50/50 split. + +```yaml +# All basic enemies are just different hollows now +Basic: +- Weight: 100 + Pool: Hollow Soldiers; Large Hollow Soldiers + RandomByType: true # To make it truly 50/50 between the categories +``` + +### Weights + +Weights can be used to select multiple different outcomes within a pool, weighted to give different probabilities each. + +Weights don't necessarily have to add up to 100, but doing it that way makes estimating probabilities very intuitive. + +```yaml +Boss: +- Weight: 79 # 79% of bosses will still be bosses + Pool: default +- Weight: 20 # Replace 20% of all bosses with minibosses + Pool: Miniboss +- Weight: 1 # Replace 1% of all bosses with regular enemies. It's always funny + Pool: Basic +``` + +Be aware that weights will not work in the [`Enemies`](#enemies) section. + +## Settings + +### Boss + +This setting indicates which enemies can be used as replacements for bosses. +By default, this is the pool of all 29 bosses. + +```yaml +Boss: +- Weight: 80 + Pool: default +- Weight: 20 # Replace 20% of all bosses with minibosses + Pool: Miniboss +``` + +### Miniboss + +This setting indicates which enemies can be used as replacements for minibosses. +By default, this is the pool of all 32 minibosses (including duplicates). + +```yaml +Miniboss: +- Weight: 80 + Pool: default +- Weight: 20 # Replace 20% of all minibosses with bosses + Pool: Boss +``` + +### Basic + +This setting indicates which enemies can be used as replacements for all other enemies, so non-bosses and non-minibosses. +By default, this is the pool of all ~2000 basic enemies (including duplicates). + +```yaml +Basic: +- Weight: 94 + Pool: default +- Weight: 5 # Replace 5% of all basic enemies with minibosses + Pool: Miniboss +- Weight: 1 # Replace 1% of all basic enemies with bosses + Pool: Boss +``` + +#### BuffBasicEnemiesAsBosses + +If enabled, this causes basic enemies to become a lot stronger when randomized into the slot of a boss. + +```yaml +Boss: +- Weight: 100 # All bosses are just basic enemies... + Pool: Basic + +BuffBasicEnemiesAsBosses: true # ...but they are strong +``` + +### Enemies + +Under the `Enemies:` setting you can add more nuanced replacements of random enemies. +There are two ways you can adjust enemies: +- Assign to a group of enemies using their category pool (see [Enemy Categories](#enemy-categories)) +- Assign to one specifc enemy by using its number (see [Individual Assignments](#individual-assignments)) + +```yaml +Enemies: + # Replace only the very first Ravenous Crystal Lizard with the final boss + Ravenous Crystal Lizard 4000380: Lords of Cinder + + # Replace all regular soldiers with skeletons or small crabs + Hollow Soldiers: Skeletons; Lesser Crab + + # Knights remain knights, but variants (i.e. weapons) are still shuffled within the category + High Wall Lothric Knight: High Wall Lothric Knight +``` + +### DontRandomize + +A semicolon-separated list of enemies or enemy types to not randomize (assign to themselves). +It is taken out of its default pool and also custom pools in this case, but it can still be assigned to +[individual enemies](#individual-assignments). + +```yaml +DontRandomize: Iudex Gundyr # Iudex Gundyr will be at his vanilla location + +Boss: +- Weight: 100 + Pool: default # Boss slots other than Iudex Gundyr will never become him +``` + +### RemoveSource + +A semicolon-separated list of enemies or enemy types to remove from all pools. +It can still be assigned to individual enemies. +This is overridden by [`DontRandomize`](#dontrandomize) directives. + +```yaml +# Remove the most annoying enemies from all pools +RemoveSource: Bridge Darkeater Midir; Ancient Wyvern Mob; Curse-rotted Greatwood; High Lord Wolnir; Carthus Sandworm +``` + +### OopsAll + +Assigning an enemy or a pool to `OopsAll` sets all pools to that specific enemy or category of enemy. This can still be +overridden using [individual enemy assginments](#individual-assignments), but otherwise every enemy is replaced by +this setting. + +```yaml +# This run suddenly got very spooky +OopsAll: Skeletons +``` + +--- + +## Enemy Categories + +The following enemy category pools are available: + +- Any +- Bosses +- Minibosses +- Bosses and Minibosses +- Basic +- Abyss Watchers +- Aldrich, Devourer of Gods +- Ancient Wyvern +- Ancient Wyvern Mob +- Angel Pilgrim +- Basilisk +- Black Knight +- Blackflame Friede +- Boreal Outrider Knight +- Bridge Darkeater Midir +- Cage Spider +- Carthus Sandworm +- Cathedral Evangelist +- Cathedral Knight +- Cemetery Hollow +- Champion Gundyr +- Champion's Gravetender and Gravetender Greatwolf +- Consumed King Oceiros +- Corpse-grub +- Corvian +- Corvian Knight +- Corvian Settler +- Crabs + - Lesser Crab + - Greater Crab + - Ariandel Greater Crab +- Crystal Lizard +- Crystal Sage +- Crystal Sage in Archives +- Curse-rotted Greatwood +- Dancer of the Boreal Valley +- Darkeater Midir +- Darkwraith +- Deacon + - Cathedral Deacon + - Wide Deacon + - Irirthyll Deacon + - Irirthyll Tall Deacon +- Deacons of the Deep +- Deep Accursed +- Demon +- Demon Cleric +- Demon Prince +- Demonic Statue +- Dragonslayer Armour +- Dreg Heap Thrall +- Elder Ghru +- Father Ariandel +- Farron Follower +- Fire Witch +- Gargoyles + - Profaned Capital Gargoyle + - Archives Gargoyle +- Ghru Grunt +- Giant Fly +- Giant Slave +- Grand Archives Scholar +- Grave Warden +- Halflight, Spear of the Church +- Harald Legion Knight +- High Lord Wolnir +- Hobbled Cleric +- Hollow Manservant +- Hollow Soldiers + - Hollow Soldier + - Road of Sacrifices Hollow Soldier + - Cathedral Hollow Soldier + - Lothric Castle Hollow Soldier + - Grand Archives Hollow Soldier +- Hound Rat +- Infested Corpse +- Irirthyll Dungeon Peasant Hollow +- Irithyll Giant Slave +- Irithyll Starved Hound +- Irithyllian Slave +- Iudex Gundyr +- Jailer +- Judicator +- King of the Storm +- Large Hollow Soldiers + - Large Hollow Soldier + - Cathedral Large Hollow Soldier + - Lothric Castle Large Hollow Soldier +- Large Hound Rat +- Large Serpent-Man +- Large Starved Hound +- Locust Preacher +- Lords of Cinder (actually called "Soul of Cinder" ingame) +- Lorian, Elder Prince +- Lothric Knights + - High Wall Lothric Knight + - Lothric Castle Lothric Knight + - Dreg Heap Lothric Knight + - Red-Eyed Lothric Knight +- Lothric Priest +- Lothric, Younger Prince +- Lycanthrope +- Lycanthrope Hunter +- Maggot Belly Starved Hound +- Millwood Knight +- Mimic Chest +- Monstrosity of Sin +- Murkman +- Murkman Summoner +- Nameless King +- Old Demon King +- Passive Locust Preacher +- Peasant Hollow +- Poisonhorn Bug +- Pontiff Knight +- Pontiff Sulyvahn +- Pus of Man +- Ravenous Crystal Lizard +- Reanimated Corpse +- Ringed City Cleric +- Ringed Knight +- Road of Sacrifices Sorcerer +- Rock Lizard +- Rotten Slug +- Serpent-Man +- Serpent-Man Summoner +- Sewer Centipede +- Silver Knight +- Sister Friede +- Skeletons + - Skeleton + - Bonewheel Skeleton + - Carthus Curved Sword Skeleton + - Carthus Shotel Skeleton + - Ringed City Skeleton +- Slave Knight Gael + - Slave Knight Gael 1 + - Slave Knight Gael 2 +- Small Locust Preacher +- Smouldering Ghru Grunt +- Starved Hound +- Stray Demon +- Sulyvahn's Beast +- Thrall +- Tree Woman +- Vordt of the Boreal Valley +- Winged Knight +- Wolves + - Smaller Wolf + - Larger Wolf + - Greatwolf +- Wretch +- Writhing Flesh + - Catacombs Writhing Flesh + - Smouldering Writhing Flesh + - Anor Londo Writhing Flesh +- Yhorm the Giant diff --git a/worlds/dark_souls_3/docs/items_en.md b/worlds/dark_souls_3/docs/items_en.md index b9de5e500a96..88557faeab1a 100644 --- a/worlds/dark_souls_3/docs/items_en.md +++ b/worlds/dark_souls_3/docs/items_en.md @@ -1,9 +1,11 @@ # Dark Souls III Items -[Game Page] | Items | [Locations] +[Game Page] | [Setup] | Items | [Locations] | [Enemy Randomization] [Game Page]: /games/Dark%20Souls%20III/info/en +[Setup]: /tutorial/Dark%20Souls%20III/setup/en [Locations]: /tutorial/Dark%20Souls%20III/locations/en +[Enemy Randomization]: /tutorial/Dark%20Souls%20III/enemy-randomization/en ## Item Groups diff --git a/worlds/dark_souls_3/docs/locations_en.md b/worlds/dark_souls_3/docs/locations_en.md index 4f0160a96d99..71deabe579d6 100644 --- a/worlds/dark_souls_3/docs/locations_en.md +++ b/worlds/dark_souls_3/docs/locations_en.md @@ -1,9 +1,11 @@ # Dark Souls III Locations -[Game Page] | [Items] | Locations +[Game Page] | [Setup] | [Items] | Locations | [Enemy Randomization] [Game Page]: /games/Dark%20Souls%20III/info/en +[Setup]: /tutorial/Dark%20Souls%20III/setup/en [Items]: /tutorial/Dark%20Souls%20III/items/en +[Enemy Randomization]: /tutorial/Dark%20Souls%20III/enemy-randomization/en ## Table of Contents diff --git a/worlds/dark_souls_3/docs/setup_en.md b/worlds/dark_souls_3/docs/setup_en.md index 7edf0d54e101..a72bf19dc25f 100644 --- a/worlds/dark_souls_3/docs/setup_en.md +++ b/worlds/dark_souls_3/docs/setup_en.md @@ -1,5 +1,12 @@ # Dark Souls III Randomizer Setup Guide +[Game Page] | Setup | [Items] | [Locations] | [Enemy Randomization] + +[Game Page]: /games/Dark%20Souls%20III/info/en +[Items]: /tutorial/Dark%20Souls%20III/items/en +[Locations]: /tutorial/Dark%20Souls%20III/locations/en +[Enemy Randomization]: /tutorial/Dark%20Souls%20III/enemy-randomization/en + ## Required Software - [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/) From 4a28888a6610d24f7c7bfe6909826e418f8f6221 Mon Sep 17 00:00:00 2001 From: Ishigh1 Date: Sat, 9 May 2026 16:56:10 +0200 Subject: [PATCH 44/66] Rule Builder: Implement AtLeast (#6085) --------- Co-authored-by: Ian Robinson --- docs/rule builder.md | 1 + rule_builder/rules.py | 136 +++++++++++++++++++++++++- test/general/test_rule_builder.py | 157 ++++++++++++++++++++++++------ 3 files changed, 263 insertions(+), 31 deletions(-) diff --git a/docs/rule builder.md b/docs/rule builder.md index 829ab763d73d..8768e6447fa5 100644 --- a/docs/rule builder.md +++ b/docs/rule builder.md @@ -41,6 +41,7 @@ The rule builder comes with a number of rules by default: - `False_`: Always returns false - `And`: Checks that all child rules are true (also provided by `&` operator) - `Or`: Checks that at least one child rule is true (also provided by `|` operator) +- `AtLeast`: Checks that at least some count of rules is true - `Has`: Checks that the player has the given item with the given count (default 1) - `HasAll`: Checks that the player has all given items - `HasAny`: Checks that the player has at least one of the given items diff --git a/rule_builder/rules.py b/rule_builder/rules.py index 47f91aff5e16..d940eeb386ff 100644 --- a/rule_builder/rules.py +++ b/rule_builder/rules.py @@ -425,13 +425,142 @@ def entrance_dependencies(self) -> dict[str, set[int]]: return combined_deps +class AtLeast(NestedRule[TWorld], game="Archipelago"): + """A rule that returns true when at least N child rules evaluate as true""" + + count: int | FieldResolver + + def __init__( + self, + count: int | FieldResolver, + *children: Rule[TWorld], + options: Iterable[OptionFilter] = (), + filtered_resolution: bool = False, + ) -> None: + super().__init__(*children, options=options, filtered_resolution=filtered_resolution) + self.count = count + + @override + def _instantiate(self, world: TWorld) -> Rule.Resolved: + count = resolve_field(self.count, world, int) + if count == 0: + return True_().resolve(world) + + children_to_process = [c.resolve(world) for c in self.children] + return AtLeast.from_resolved(count, world, children_to_process) + + @classmethod + def from_resolved(cls, count: int, world: TWorld, children_to_process: list[Rule.Resolved]) -> Rule.Resolved: + clauses: list[Rule.Resolved] = [] + + while children_to_process: + child = children_to_process.pop(0) + if child.always_true: + if count == 1: + return child + count -= 1 + continue + if child.always_false: + # falses can be ignored + continue + + clauses.append(child) + + if len(clauses) < count: + return False_().resolve(world) + if count == 1: + # Switch to Or which has more optimized handling + return Or.from_resolved(world, clauses) + if count == len(clauses): + # Switch to And which has more optimized handling + return And.from_resolved(world, clauses) + return AtLeast.Resolved( + tuple(clauses), + count=count, + player=world.player, + caching_enabled=getattr(world, "rule_caching_enabled", False), + ) + + @override + def to_dict(self) -> dict[str, Any]: + output = super().to_dict() + count = self.count + output["count"] = count.to_dict() if isinstance(count, FieldResolver) else count + return output + + @override + @classmethod + def from_dict(cls, data: Mapping[str, Any], world_cls: "type[World]") -> Self: + args = cls._parse_field_resolvers(data, world_cls.game) + options = OptionFilter.multiple_from_dict(data.get("options", ())) + children = [world_cls.rule_from_dict(c) for c in data.get("children", ())] + return cls( + args.pop("count"), + *children, + options=options, + filtered_resolution=data.get("filtered_resolution", False), + ) + + class Resolved(NestedRule.Resolved): + count: int + + @override + def _evaluate(self, state: CollectionState) -> bool: + count = self.count + for rule in self.children: + if rule(state): + if count == 1: + return True + count -= 1 + return False + + @override + def explain_json(self, state: CollectionState | None = None) -> list[JSONMessagePart]: + messages: list[JSONMessagePart] = [] + if state is None: + messages = [ + {"type": "text", "text": "At least "}, + {"type": "color", "color": "cyan", "text": str(self.count)}, + {"type": "text", "text": " of ("}, + ] + else: + satisfied_count = sum(1 if child(state) else 0 for child in self.children) + messages = [ + {"type": "text", "text": "At least "}, + {"type": "color", "color": "cyan", "text": f"{satisfied_count}/{self.count}"}, + {"type": "text", "text": " of ("}, + ] + for i, child in enumerate(self.children): + if i > 0: + messages.append({"type": "text", "text": ", "}) + messages.extend(child.explain_json(state)) + messages.append({"type": "text", "text": ")"}) + return messages + + @override + def explain_str(self, state: CollectionState | None = None) -> str: + clauses = ", ".join([c.explain_str(state) for c in self.children]) + if state is None: + return f"At least {self.count} of ({clauses})" + satisfied_count = sum(1 if child(state) else 0 for child in self.children) + return f"At least {satisfied_count}/{self.count} of ({clauses})" + + @override + def __str__(self) -> str: + clauses = ", ".join([str(c) for c in self.children]) + return f"At least {self.count} of ({clauses})" + + @dataclasses.dataclass(init=False) class And(NestedRule[TWorld], game="Archipelago"): """A rule that only returns true when all child rules evaluate as true""" @override def _instantiate(self, world: TWorld) -> Rule.Resolved: - children_to_process = [c.resolve(world) for c in self.children] + return And.from_resolved(world, [c.resolve(world) for c in self.children]) + + @classmethod + def from_resolved(cls, world: TWorld, children_to_process: list[Rule.Resolved]) -> Rule.Resolved: clauses: list[Rule.Resolved] = [] items: dict[str, int] = {} true_rule: Rule.Resolved | None = None @@ -518,7 +647,10 @@ class Or(NestedRule[TWorld], game="Archipelago"): @override def _instantiate(self, world: TWorld) -> Rule.Resolved: - children_to_process = [c.resolve(world) for c in self.children] + return Or.from_resolved(world, [c.resolve(world) for c in self.children]) + + @classmethod + def from_resolved(cls, world: TWorld, children_to_process: list[Rule.Resolved]) -> Rule.Resolved: clauses: list[Rule.Resolved] = [] items: dict[str, int] = {} diff --git a/test/general/test_rule_builder.py b/test/general/test_rule_builder.py index 682c043f8e09..191ba3cba718 100644 --- a/test/general/test_rule_builder.py +++ b/test/general/test_rule_builder.py @@ -12,6 +12,7 @@ from rule_builder.options import Operator, OptionFilter from rule_builder.rules import ( And, + AtLeast, CanReachEntrance, CanReachLocation, CanReachRegion, @@ -250,6 +251,40 @@ def get_filler_item_name(self) -> str: Or(HasAnyCount({"A": 1, "B": 2}), HasAnyCount({"A": 2, "B": 2})), HasAnyCount.Resolved((("A", 1), ("B", 2)), player=1), ), + ( + AtLeast(0, Has("A")), + True_.Resolved(player=1), + ), + ( + AtLeast(3, True_(), Has("A"), Has("B"), Has("C")), + AtLeast.Resolved( + (Has.Resolved("A", player=1), Has.Resolved("B", player=1), Has.Resolved("C", player=1)), 2, player=1 + ), + ), + ( + AtLeast(2, False_(), Has("A"), Has("B"), Has("C")), + AtLeast.Resolved( + (Has.Resolved("A", player=1), Has.Resolved("B", player=1), Has.Resolved("C", player=1)), 2, player=1 + ), + ), + ( + AtLeast(2, True_(), True_(), Has("A")), + True_.Resolved(player=1), + ), + ( + AtLeast(3, Has("A"), Has("B")), + False_.Resolved(player=1), + ), + ( + # This test will fail when Or(Rule, Rule) will be optimized to Rule + AtLeast(1, Rule(), Rule()), + Or.Resolved((Rule.Resolved(player=1), Rule.Resolved(player=1)), player=1), + ), + ( + # This test will fail when And(Rule, Rule) will be optimized to Rule + AtLeast(2, Rule(), Rule()), + And.Resolved((Rule.Resolved(player=1), Rule.Resolved(player=1)), player=1), + ), ) ) class TestSimplify(RuleBuilderTestCase): @@ -631,6 +666,24 @@ def test_has(self) -> None: self.state.remove(item) self.assertFalse(resolved_rule(self.state)) + def test_at_least(self) -> None: + # Has has to be relied on as True_ and False_ would be optimized out + rule = AtLeast(2, Has("Item 1"), Has("Item 1"), Has("Item 2"), Has("Item 3")) + resolved_rule = rule.resolve(self.world) + self.world.register_rule_dependencies(resolved_rule) + item1 = self.world.create_item("Item 1") + item2 = self.world.create_item("Item 2") + item3 = self.world.create_item("Item 3") + self.assertFalse(resolved_rule(self.state)) + self.state.collect(item1) + self.assertTrue(resolved_rule(self.state)) + self.state.collect(item2) + self.assertTrue(resolved_rule(self.state)) + self.state.remove(item1) + self.assertFalse(resolved_rule(self.state)) + self.state.collect(item3) + self.assertTrue(resolved_rule(self.state)) + def test_has_all(self) -> None: rule = HasAll("Item 1", "Item 2") resolved_rule = rule.resolve(self.world) @@ -806,8 +859,13 @@ class TestSerialization(RuleBuilderTestCase): OptionFilter(ChoiceOption, ChoiceOption.option_second, "ge"), ], ), + AtLeast( + FromWorldAttr("instance_data.at_least_requirement"), + Has("i15", count=2), + HasGroup("g2", count=3), + ), CanReachEntrance("e1"), - HasGroupUnique("g2", count=5), + HasGroupUnique("g3", count=5), ) rule_dict: ClassVar[dict[str, Any]] = { @@ -931,6 +989,29 @@ class TestSerialization(RuleBuilderTestCase): }, ], }, + { + "rule": "AtLeast", + "options": [], + "filtered_resolution": False, + "count": {"resolver": "FromWorldAttr", "name": "instance_data.at_least_requirement"}, + "children": [ + { + "rule": "Has", + "options": [], + "filtered_resolution": False, + "args": { + "item_name": "i15", + "count": 2, + }, + }, + { + "rule": "HasGroup", + "options": [], + "filtered_resolution": False, + "args": {"item_name_group": "g2", "count": 3}, + }, + ], + }, { "rule": "CanReachEntrance", "options": [], @@ -941,7 +1022,7 @@ class TestSerialization(RuleBuilderTestCase): "rule": "HasGroupUnique", "options": [], "filtered_resolution": False, - "args": {"item_name_group": "g2", "count": 5}, + "args": {"item_name_group": "g3", "count": 5}, }, ], } @@ -973,9 +1054,15 @@ class TestExplain(RuleBuilderTestCase): ), player=1, ), - HasAllCounts.Resolved((("Item 6", 1), ("Item 7", 5)), player=1), - HasAnyCount.Resolved((("Item 8", 2), ("Item 9", 3)), player=1), - HasFromList.Resolved(("Item 10", "Item 11", "Item 12"), count=2, player=1), + AtLeast.Resolved( + children=( + HasAllCounts.Resolved((("Item 6", 1), ("Item 7", 5)), player=1), + HasAnyCount.Resolved((("Item 8", 2), ("Item 9", 3)), player=1), + HasFromList.Resolved(("Item 10", "Item 11", "Item 12"), count=2, player=1), + ), + count=2, + player=1, + ), HasFromListUnique.Resolved(("Item 13", "Item 14"), player=1), HasGroup.Resolved("Group 1", ("Item 15", "Item 16", "Item 17"), player=1), HasGroupUnique.Resolved("Group 2", ("Item 18", "Item 19"), count=2, player=1), @@ -1040,6 +1127,9 @@ def test_explain_json_with_state_no_items(self) -> None: {"type": "text", "text": ")"}, {"type": "text", "text": ")"}, {"type": "text", "text": " & "}, + {"type": "text", "text": "At least "}, + {"type": "color", "color": "cyan", "text": "0/2"}, + {"type": "text", "text": " of ("}, {"type": "text", "text": "Missing "}, {"type": "color", "color": "cyan", "text": "some"}, {"type": "text", "text": " of ("}, @@ -1050,7 +1140,7 @@ def test_explain_json_with_state_no_items(self) -> None: {"type": "color", "color": "salmon", "text": "Item 7"}, {"type": "text", "text": " x5"}, {"type": "text", "text": ")"}, - {"type": "text", "text": " & "}, + {"type": "text", "text": ", "}, {"type": "text", "text": "Missing "}, {"type": "color", "color": "cyan", "text": "all"}, {"type": "text", "text": " of ("}, @@ -1061,7 +1151,7 @@ def test_explain_json_with_state_no_items(self) -> None: {"type": "color", "color": "salmon", "text": "Item 9"}, {"type": "text", "text": " x3"}, {"type": "text", "text": ")"}, - {"type": "text", "text": " & "}, + {"type": "text", "text": ", "}, {"type": "text", "text": "Has "}, {"type": "color", "color": "salmon", "text": "0/2"}, {"type": "text", "text": " items from ("}, @@ -1072,6 +1162,7 @@ def test_explain_json_with_state_no_items(self) -> None: {"type": "text", "text": ", "}, {"type": "color", "color": "salmon", "text": "Item 12"}, {"type": "text", "text": ")"}, + {"type": "text", "text": ")"}, {"type": "text", "text": " & "}, {"type": "text", "text": "Has "}, {"type": "color", "color": "salmon", "text": "0/1"}, @@ -1138,6 +1229,9 @@ def test_explain_json_with_state_all_items(self) -> None: {"type": "text", "text": ")"}, {"type": "text", "text": ")"}, {"type": "text", "text": " & "}, + {"type": "text", "text": "At least "}, + {"type": "color", "color": "cyan", "text": "3/2"}, + {"type": "text", "text": " of ("}, {"type": "text", "text": "Has "}, {"type": "color", "color": "cyan", "text": "all"}, {"type": "text", "text": " of ("}, @@ -1148,7 +1242,7 @@ def test_explain_json_with_state_all_items(self) -> None: {"type": "color", "color": "green", "text": "Item 7"}, {"type": "text", "text": " x5"}, {"type": "text", "text": ")"}, - {"type": "text", "text": " & "}, + {"type": "text", "text": ", "}, {"type": "text", "text": "Has "}, {"type": "color", "color": "cyan", "text": "some"}, {"type": "text", "text": " of ("}, @@ -1159,7 +1253,7 @@ def test_explain_json_with_state_all_items(self) -> None: {"type": "color", "color": "green", "text": "Item 9"}, {"type": "text", "text": " x3"}, {"type": "text", "text": ")"}, - {"type": "text", "text": " & "}, + {"type": "text", "text": ", "}, {"type": "text", "text": "Has "}, {"type": "color", "color": "green", "text": "30/2"}, {"type": "text", "text": " items from ("}, @@ -1170,6 +1264,7 @@ def test_explain_json_with_state_all_items(self) -> None: {"type": "text", "text": ", "}, {"type": "color", "color": "green", "text": "Item 12"}, {"type": "text", "text": ")"}, + {"type": "text", "text": ")"}, {"type": "text", "text": " & "}, {"type": "text", "text": "Has "}, {"type": "color", "color": "green", "text": "2/1"}, @@ -1204,7 +1299,7 @@ def test_explain_json_with_state_all_items(self) -> None: {"type": "color", "color": "salmon", "text": "False"}, {"type": "text", "text": ")"}, ] - assert self.resolved_rule.explain_json(self.state) == expected + self.assertEqual(self.resolved_rule.explain_json(self.state), expected) def test_explain_json_without_state(self) -> None: expected: list[JSONMessagePart] = [ @@ -1232,6 +1327,9 @@ def test_explain_json_without_state(self) -> None: {"type": "text", "text": ")"}, {"type": "text", "text": ")"}, {"type": "text", "text": " & "}, + {"type": "text", "text": "At least "}, + {"type": "color", "color": "cyan", "text": "2"}, + {"type": "text", "text": " of ("}, {"type": "text", "text": "Has "}, {"type": "color", "color": "cyan", "text": "all"}, {"type": "text", "text": " of ("}, @@ -1241,7 +1339,7 @@ def test_explain_json_without_state(self) -> None: {"type": "item_name", "flags": 1, "text": "Item 7", "player": 1}, {"type": "text", "text": " x5"}, {"type": "text", "text": ")"}, - {"type": "text", "text": " & "}, + {"type": "text", "text": ", "}, {"type": "text", "text": "Has "}, {"type": "color", "color": "cyan", "text": "any"}, {"type": "text", "text": " of ("}, @@ -1251,7 +1349,7 @@ def test_explain_json_without_state(self) -> None: {"type": "item_name", "flags": 1, "text": "Item 9", "player": 1}, {"type": "text", "text": " x3"}, {"type": "text", "text": ")"}, - {"type": "text", "text": " & "}, + {"type": "text", "text": ", "}, {"type": "text", "text": "Has "}, {"type": "color", "color": "cyan", "text": "2"}, {"type": "text", "text": "x items from ("}, @@ -1261,6 +1359,7 @@ def test_explain_json_without_state(self) -> None: {"type": "text", "text": ", "}, {"type": "item_name", "flags": 1, "text": "Item 12", "player": 1}, {"type": "text", "text": ")"}, + {"type": "text", "text": ")"}, {"type": "text", "text": " & "}, {"type": "text", "text": "Has "}, {"type": "color", "color": "cyan", "text": "1"}, @@ -1294,16 +1393,16 @@ def test_explain_json_without_state(self) -> None: {"type": "color", "color": "salmon", "text": "False"}, {"type": "text", "text": ")"}, ] - assert self.resolved_rule.explain_json() == expected + self.assertEqual(self.resolved_rule.explain_json(), expected) def test_explain_str_with_state_no_items(self) -> None: expected = ( "((Missing 4x Item 1", "| Missing some of (Missing: Item 2, Item 3)", "| Missing all of (Missing: Item 4, Item 5))", - "& Missing some of (Missing: Item 6 x1, Item 7 x5)", - "& Missing all of (Missing: Item 8 x2, Item 9 x3)", - "& Has 0/2 items from (Missing: Item 10, Item 11, Item 12)", + "& At least 0/2 of (Missing some of (Missing: Item 6 x1, Item 7 x5),", + "Missing all of (Missing: Item 8 x2, Item 9 x3),", + "Has 0/2 items from (Missing: Item 10, Item 11, Item 12))", "& Has 0/1 unique items from (Missing: Item 13, Item 14)", "& Has 0/1 items from Group 1", "& Has 0/2 unique items from Group 2", @@ -1313,7 +1412,7 @@ def test_explain_str_with_state_no_items(self) -> None: "& True", "& False)", ) - assert self.resolved_rule.explain_str(self.state) == " ".join(expected) + self.assertEqual(self.resolved_rule.explain_str(self.state), " ".join(expected)) def test_explain_str_with_state_all_items(self) -> None: self._collect_all() @@ -1322,9 +1421,9 @@ def test_explain_str_with_state_all_items(self) -> None: "((Has 4x Item 1", "| Has all of (Found: Item 2, Item 3)", "| Has some of (Found: Item 4, Item 5))", - "& Has all of (Found: Item 6 x1, Item 7 x5)", - "& Has some of (Found: Item 8 x2, Item 9 x3)", - "& Has 30/2 items from (Found: Item 10, Item 11, Item 12)", + "& At least 3/2 of (Has all of (Found: Item 6 x1, Item 7 x5),", + "Has some of (Found: Item 8 x2, Item 9 x3),", + "Has 30/2 items from (Found: Item 10, Item 11, Item 12))", "& Has 2/1 unique items from (Found: Item 13, Item 14)", "& Has 30/1 items from Group 1", "& Has 2/2 unique items from Group 2", @@ -1334,16 +1433,16 @@ def test_explain_str_with_state_all_items(self) -> None: "& True", "& False)", ) - assert self.resolved_rule.explain_str(self.state) == " ".join(expected) + self.assertEqual(self.resolved_rule.explain_str(self.state), " ".join(expected)) def test_explain_str_without_state(self) -> None: expected = ( "((Has 4x Item 1", "| Has all of (Item 2, Item 3)", "| Has any of (Item 4, Item 5))", - "& Has all of (Item 6 x1, Item 7 x5)", - "& Has any of (Item 8 x2, Item 9 x3)", - "& Has 2x items from (Item 10, Item 11, Item 12)", + "& At least 2 of (Has all of (Item 6 x1, Item 7 x5),", + "Has any of (Item 8 x2, Item 9 x3),", + "Has 2x items from (Item 10, Item 11, Item 12))", "& Has a unique item from (Item 13, Item 14)", "& Has an item from Group 1", "& Has 2x unique items from Group 2", @@ -1353,16 +1452,16 @@ def test_explain_str_without_state(self) -> None: "& True", "& False)", ) - assert self.resolved_rule.explain_str() == " ".join(expected) + self.assertEqual(self.resolved_rule.explain_str(), " ".join(expected)) def test_str(self) -> None: expected = ( "((Has 4x Item 1", "| Has all of (Item 2, Item 3)", "| Has any of (Item 4, Item 5))", - "& Has all of (Item 6 x1, Item 7 x5)", - "& Has any of (Item 8 x2, Item 9 x3)", - "& Has 2x items from (Item 10, Item 11, Item 12)", + "& At least 2 of (Has all of (Item 6 x1, Item 7 x5),", + "Has any of (Item 8 x2, Item 9 x3),", + "Has 2x items from (Item 10, Item 11, Item 12))", "& Has a unique item from (Item 13, Item 14)", "& Has an item from Group 1", "& Has 2x unique items from Group 2", @@ -1372,7 +1471,7 @@ def test_str(self) -> None: "& True", "& False)", ) - assert str(self.resolved_rule) == " ".join(expected) + self.assertEqual(str(self.resolved_rule), " ".join(expected)) @classvar_matrix( From 06dc1b897cc6e58fc8b5d438dee51a67aedc59fe Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Sat, 9 May 2026 07:58:01 -0700 Subject: [PATCH 45/66] Pokemon Emerald: Switch to rule builder (#5923) --- worlds/pokemon_emerald/locations.py | 2 +- worlds/pokemon_emerald/options.py | 2 +- worlds/pokemon_emerald/pokemon.py | 4 +- worlds/pokemon_emerald/rules.py | 1925 +++++++-------------------- worlds/pokemon_emerald/util.py | 2 +- 5 files changed, 469 insertions(+), 1466 deletions(-) diff --git a/worlds/pokemon_emerald/locations.py b/worlds/pokemon_emerald/locations.py index fd8d0ebc7d58..8150d2b2448f 100644 --- a/worlds/pokemon_emerald/locations.py +++ b/worlds/pokemon_emerald/locations.py @@ -177,7 +177,7 @@ def set_legendary_cave_entrances(world: "PokemonEmeraldWorld") -> None: terra_cave_location_location = world.multiworld.get_location("TERRA_CAVE_LOCATION", world.player) terra_cave_location_location.item = None terra_cave_location_location.place_locked_item(world.create_event(terra_cave_location_name)) - + marine_cave_location_name = world.random.choice([ "MARINE_CAVE_ROUTE_105_1", "MARINE_CAVE_ROUTE_105_2", diff --git a/worlds/pokemon_emerald/options.py b/worlds/pokemon_emerald/options.py index 9529be877ebe..ebd36898ab25 100644 --- a/worlds/pokemon_emerald/options.py +++ b/worlds/pokemon_emerald/options.py @@ -594,7 +594,7 @@ class NormalizeEncounterRates(Toggle): Make every slot on an encounter table approximately equally likely. This does NOT mean each species is equally likely. In the vanilla game, each species may occupy more than one slot, and slots vary in probability. - + Species will still occupy the same slots as vanilla, but the slots will be equally weighted. The minimum encounter rate will be 8% (higher in water). """ display_name = "Normalize Encounter Rates" diff --git a/worlds/pokemon_emerald/pokemon.py b/worlds/pokemon_emerald/pokemon.py index 76285d11dab8..d51b80f35302 100644 --- a/worlds/pokemon_emerald/pokemon.py +++ b/worlds/pokemon_emerald/pokemon.py @@ -502,7 +502,7 @@ def randomize_learnsets(world: "PokemonEmeraldWorld") -> None: species.learnset = new_learnset - + def randomize_starters(world: "PokemonEmeraldWorld") -> None: if world.options.starters == RandomizeStarters.option_vanilla: return @@ -682,7 +682,7 @@ def randomize_misc_pokemon(world: "PokemonEmeraldWorld") -> None: ] if should_match_bst: candidates = filter_species_by_nearby_bst(candidates, sum(original_species.base_stats)) - + player_filtered_candidates = [ species for species in candidates diff --git a/worlds/pokemon_emerald/rules.py b/worlds/pokemon_emerald/rules.py index 30ebf72e4d63..ce3d48ca1542 100644 --- a/worlds/pokemon_emerald/rules.py +++ b/worlds/pokemon_emerald/rules.py @@ -1,14 +1,16 @@ """ Logic rule definitions for Pokemon Emerald """ -from typing import TYPE_CHECKING, Callable, Dict +from collections import defaultdict +from typing import TYPE_CHECKING, Literal -from BaseClasses import CollectionState -from worlds.generic.Rules import add_rule, set_rule +from rule_builder.rules import (Rule, CanReachEntrance, Has, HasAll, HasAny, HasFromListUnique, HasGroupUnique, + OptionFilter, True_) from .data import LocationCategory, NATIONAL_ID_TO_SPECIES_ID, NUM_REAL_SPECIES, data from .locations import PokemonEmeraldLocation -from .options import DarkCavesRequireFlash, EliteFourRequirement, NormanRequirement, Goal +from .options import (DarkCavesRequireFlash, EliteFourRequirement, NormanRequirement, Goal, ModifyRoute118, + ExtraBoulders, ExtraBumpySlope, RemoveRoadblocks) if TYPE_CHECKING: from . import PokemonEmeraldWorld @@ -17,23 +19,18 @@ # Rules are organized by town/route/dungeon and ordered approximately # by when you would first reach that place in a vanilla playthrough. def set_rules(world: "PokemonEmeraldWorld") -> None: - hm_rules: Dict[str, Callable[[CollectionState], bool]] = {} + entrance_rules: defaultdict[str, Rule] = defaultdict(True_) + location_rules: defaultdict[str, Rule] = defaultdict(True_) + + hm_rules: dict[str, Rule] = {} for hm, badges in world.hm_requirements.items(): if isinstance(badges, list): - hm_rules[hm] = lambda state, hm=hm, badges=badges: \ - state.has(hm, world.player) and state.has_all(badges, world.player) + hm_rules[hm] = Has(hm) & HasAll(*badges) else: - hm_rules[hm] = lambda state, hm=hm, badges=badges: \ - state.has(hm, world.player) and state.has_group_unique("Badge", world.player, badges) - - def has_acro_bike(state: CollectionState): - return state.has("Acro Bike", world.player) - - def has_mach_bike(state: CollectionState): - return state.has("Mach Bike", world.player) + hm_rules[hm] = Has(hm) & HasGroupUnique("Badge", badges) - def defeated_n_gym_leaders(state: CollectionState, n: int) -> bool: - return state.has_from_list_unique([ + def create_defeated_n_gym_leaders_rule(n: int) -> Rule: + return HasFromListUnique( "EVENT_DEFEAT_ROXANNE", "EVENT_DEFEAT_BRAWLY", "EVENT_DEFEAT_WATTSON", @@ -42,7 +39,8 @@ def defeated_n_gym_leaders(state: CollectionState, n: int) -> bool: "EVENT_DEFEAT_WINONA", "EVENT_DEFEAT_TATE_AND_LIZA", "EVENT_DEFEAT_JUAN", - ], world.player, n) + count=n + ) huntable_legendary_events = [ f"EVENT_ENCOUNTER_{key}" @@ -63,1485 +61,573 @@ def defeated_n_gym_leaders(state: CollectionState, n: int) -> bool: if name in world.options.allowed_legendary_hunt_encounters.value ] - def encountered_n_legendaries(state: CollectionState, n: int) -> bool: - return state.has_from_list_unique(huntable_legendary_events, world.player, n) - - def get_entrance(entrance: str): - return world.multiworld.get_entrance(entrance, world.player) - - def get_location(location: str): - if location in data.locations: - location = data.locations[location].label - - return world.multiworld.get_location(location, world.player) - - if world.options.goal == Goal.option_champion: - completion_condition = lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) - elif world.options.goal == Goal.option_steven: - completion_condition = lambda state: state.has("EVENT_DEFEAT_STEVEN", world.player) - elif world.options.goal == Goal.option_norman: - completion_condition = lambda state: state.has("EVENT_DEFEAT_NORMAN", world.player) - elif world.options.goal == Goal.option_legendary_hunt: - completion_condition = lambda state: encountered_n_legendaries(state, world.options.legendary_hunt_count.value) - - world.multiworld.completion_condition[world.player] = completion_condition + world.set_completion_rule( + (OptionFilter(Goal, Goal.option_champion) & Has("EVENT_DEFEAT_CHAMPION")) | + (OptionFilter(Goal, Goal.option_steven) & Has("EVENT_DEFEAT_STEVEN")) | + (OptionFilter(Goal, Goal.option_norman) & Has("EVENT_DEFEAT_NORMAN")) | + (OptionFilter(Goal, Goal.option_legendary_hunt) & HasFromListUnique( + *huntable_legendary_events, + count=world.options.legendary_hunt_count.value, + )) + ) if world.options.legendary_hunt_catch: - set_rule(get_location("EVENT_ENCOUNTER_GROUDON"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)) - set_rule(get_location("EVENT_ENCOUNTER_KYOGRE"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)) - set_rule(get_location("EVENT_ENCOUNTER_RAYQUAZA"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)) - set_rule(get_location("EVENT_ENCOUNTER_LATIAS"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)) + location_rules["EVENT_ENCOUNTER_GROUDON"] = Has("EVENT_DEFEAT_CHAMPION") + location_rules["EVENT_ENCOUNTER_KYOGRE"] = Has("EVENT_DEFEAT_CHAMPION") + location_rules["EVENT_ENCOUNTER_RAYQUAZA"] = Has("EVENT_DEFEAT_CHAMPION") + location_rules["EVENT_ENCOUNTER_LATIAS"] = Has("EVENT_DEFEAT_CHAMPION") # Latios already only requires defeating the champion and access to Route 117 - # set_rule(get_location("EVENT_ENCOUNTER_LATIOS"), - # lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)) - set_rule(get_location("EVENT_ENCOUNTER_REGIROCK"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)) - set_rule(get_location("EVENT_ENCOUNTER_REGICE"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)) - set_rule(get_location("EVENT_ENCOUNTER_REGISTEEL"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)) - set_rule(get_location("EVENT_ENCOUNTER_MEW"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)) - set_rule(get_location("EVENT_ENCOUNTER_DEOXYS"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)) - set_rule(get_location("EVENT_ENCOUNTER_HO_OH"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)) - set_rule(get_location("EVENT_ENCOUNTER_LUGIA"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)) + # location_rules["EVENT_ENCOUNTER_LATIOS"] = Has("EVENT_DEFEAT_CHAMPION") + location_rules["EVENT_ENCOUNTER_REGIROCK"] = Has("EVENT_DEFEAT_CHAMPION") + location_rules["EVENT_ENCOUNTER_REGICE"] = Has("EVENT_DEFEAT_CHAMPION") + location_rules["EVENT_ENCOUNTER_REGISTEEL"] = Has("EVENT_DEFEAT_CHAMPION") + location_rules["EVENT_ENCOUNTER_MEW"] = Has("EVENT_DEFEAT_CHAMPION") + location_rules["EVENT_ENCOUNTER_DEOXYS"] = Has("EVENT_DEFEAT_CHAMPION") + location_rules["EVENT_ENCOUNTER_HO_OH"] = Has("EVENT_DEFEAT_CHAMPION") + location_rules["EVENT_ENCOUNTER_LUGIA"] = Has("EVENT_DEFEAT_CHAMPION") # Sky - set_rule( - get_entrance("REGION_LITTLEROOT_TOWN/MAIN -> REGION_SKY"), - hm_rules["HM02 Fly"] - ) - set_rule( - get_entrance("REGION_SKY -> REGION_LITTLEROOT_TOWN/MAIN"), - lambda state: state.has("EVENT_VISITED_LITTLEROOT_TOWN", world.player) - ) - set_rule( - get_entrance("REGION_SKY -> REGION_OLDALE_TOWN/MAIN"), - lambda state: state.has("EVENT_VISITED_OLDALE_TOWN", world.player) - ) - set_rule( - get_entrance("REGION_SKY -> REGION_PETALBURG_CITY/MAIN"), - lambda state: state.has("EVENT_VISITED_PETALBURG_CITY", world.player) - ) - set_rule( - get_entrance("REGION_SKY -> REGION_RUSTBORO_CITY/MAIN"), - lambda state: state.has("EVENT_VISITED_RUSTBORO_CITY", world.player) - ) - set_rule( - get_entrance("REGION_SKY -> REGION_DEWFORD_TOWN/MAIN"), - lambda state: state.has("EVENT_VISITED_DEWFORD_TOWN", world.player) - ) - set_rule( - get_entrance("REGION_SKY -> REGION_SLATEPORT_CITY/MAIN"), - lambda state: state.has("EVENT_VISITED_SLATEPORT_CITY", world.player) - ) - set_rule( - get_entrance("REGION_SKY -> REGION_MAUVILLE_CITY/MAIN"), - lambda state: state.has("EVENT_VISITED_MAUVILLE_CITY", world.player) - ) - set_rule( - get_entrance("REGION_SKY -> REGION_VERDANTURF_TOWN/MAIN"), - lambda state: state.has("EVENT_VISITED_VERDANTURF_TOWN", world.player) - ) - set_rule( - get_entrance("REGION_SKY -> REGION_FALLARBOR_TOWN/MAIN"), - lambda state: state.has("EVENT_VISITED_FALLARBOR_TOWN", world.player) - ) - set_rule( - get_entrance("REGION_SKY -> REGION_LAVARIDGE_TOWN/MAIN"), - lambda state: state.has("EVENT_VISITED_LAVARIDGE_TOWN", world.player) - ) - set_rule( - get_entrance("REGION_SKY -> REGION_FORTREE_CITY/MAIN"), - lambda state: state.has("EVENT_VISITED_FORTREE_CITY", world.player) - ) - set_rule( - get_entrance("REGION_SKY -> REGION_LILYCOVE_CITY/MAIN"), - lambda state: state.has("EVENT_VISITED_LILYCOVE_CITY", world.player) - ) - set_rule( - get_entrance("REGION_SKY -> REGION_MOSSDEEP_CITY/MAIN"), - lambda state: state.has("EVENT_VISITED_MOSSDEEP_CITY", world.player) - ) - set_rule( - get_entrance("REGION_SKY -> REGION_SOOTOPOLIS_CITY/EAST"), - lambda state: state.has("EVENT_VISITED_SOOTOPOLIS_CITY", world.player) - ) - set_rule( - get_entrance("REGION_SKY -> REGION_EVER_GRANDE_CITY/SOUTH"), - lambda state: state.has("EVENT_VISITED_EVER_GRANDE_CITY", world.player) - ) + entrance_rules["REGION_LITTLEROOT_TOWN/MAIN -> REGION_SKY"] = hm_rules["HM02 Fly"] + entrance_rules["REGION_SKY -> REGION_LITTLEROOT_TOWN/MAIN"] = Has("EVENT_VISITED_LITTLEROOT_TOWN") + entrance_rules["REGION_SKY -> REGION_OLDALE_TOWN/MAIN"] = Has("EVENT_VISITED_OLDALE_TOWN") + entrance_rules["REGION_SKY -> REGION_PETALBURG_CITY/MAIN"] = Has("EVENT_VISITED_PETALBURG_CITY") + entrance_rules["REGION_SKY -> REGION_RUSTBORO_CITY/MAIN"] = Has("EVENT_VISITED_RUSTBORO_CITY") + entrance_rules["REGION_SKY -> REGION_DEWFORD_TOWN/MAIN"] = Has("EVENT_VISITED_DEWFORD_TOWN") + entrance_rules["REGION_SKY -> REGION_SLATEPORT_CITY/MAIN"] = Has("EVENT_VISITED_SLATEPORT_CITY") + entrance_rules["REGION_SKY -> REGION_MAUVILLE_CITY/MAIN"] = Has("EVENT_VISITED_MAUVILLE_CITY") + entrance_rules["REGION_SKY -> REGION_VERDANTURF_TOWN/MAIN"] = Has("EVENT_VISITED_VERDANTURF_TOWN") + entrance_rules["REGION_SKY -> REGION_FALLARBOR_TOWN/MAIN"] = Has("EVENT_VISITED_FALLARBOR_TOWN") + entrance_rules["REGION_SKY -> REGION_LAVARIDGE_TOWN/MAIN"] = Has("EVENT_VISITED_LAVARIDGE_TOWN") + entrance_rules["REGION_SKY -> REGION_FORTREE_CITY/MAIN"] = Has("EVENT_VISITED_FORTREE_CITY") + entrance_rules["REGION_SKY -> REGION_LILYCOVE_CITY/MAIN"] = Has("EVENT_VISITED_LILYCOVE_CITY") + entrance_rules["REGION_SKY -> REGION_MOSSDEEP_CITY/MAIN"] = Has("EVENT_VISITED_MOSSDEEP_CITY") + entrance_rules["REGION_SKY -> REGION_SOOTOPOLIS_CITY/EAST"] = Has("EVENT_VISITED_SOOTOPOLIS_CITY") + entrance_rules["REGION_SKY -> REGION_EVER_GRANDE_CITY/SOUTH"] = Has("EVENT_VISITED_EVER_GRANDE_CITY") # Littleroot Town - set_rule( - get_location("NPC_GIFT_RECEIVED_SS_TICKET"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) - ) - set_rule( - get_location("NPC_GIFT_RECEIVED_AURORA_TICKET"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) - ) - set_rule( - get_location("NPC_GIFT_RECEIVED_EON_TICKET"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) - ) - set_rule( - get_location("NPC_GIFT_RECEIVED_MYSTIC_TICKET"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) - ) - set_rule( - get_location("NPC_GIFT_RECEIVED_OLD_SEA_MAP"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) - ) + location_rules["NPC_GIFT_RECEIVED_SS_TICKET"] = Has("EVENT_DEFEAT_CHAMPION") + location_rules["NPC_GIFT_RECEIVED_AURORA_TICKET"] = Has("EVENT_DEFEAT_CHAMPION") + location_rules["NPC_GIFT_RECEIVED_EON_TICKET"] = Has("EVENT_DEFEAT_CHAMPION") + location_rules["NPC_GIFT_RECEIVED_MYSTIC_TICKET"] = Has("EVENT_DEFEAT_CHAMPION") + location_rules["NPC_GIFT_RECEIVED_OLD_SEA_MAP"] = Has("EVENT_DEFEAT_CHAMPION") # Route 102 - set_rule( - get_entrance("REGION_ROUTE102/MAIN -> REGION_ROUTE102/POND"), - hm_rules["HM03 Surf"] - ) + entrance_rules["REGION_ROUTE102/MAIN -> REGION_ROUTE102/POND"] = hm_rules["HM03 Surf"] # Route 103 - set_rule( - get_entrance("REGION_ROUTE103/EAST -> REGION_ROUTE103/WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_ROUTE103/WEST -> REGION_ROUTE103/WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_ROUTE103/EAST -> REGION_ROUTE103/EAST_TREE_MAZE"), - hm_rules["HM01 Cut"] - ) + entrance_rules["REGION_ROUTE103/EAST -> REGION_ROUTE103/WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_ROUTE103/WEST -> REGION_ROUTE103/WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_ROUTE103/EAST -> REGION_ROUTE103/EAST_TREE_MAZE"] = hm_rules["HM01 Cut"] # Petalburg City - set_rule( - get_entrance("REGION_PETALBURG_CITY/MAIN -> REGION_PETALBURG_CITY/SOUTH_POND"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_PETALBURG_CITY/MAIN -> REGION_PETALBURG_CITY/NORTH_POND"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_location("NPC_GIFT_RECEIVED_HM_SURF"), - lambda state: state.has("EVENT_DEFEAT_NORMAN", world.player) - ) - if world.options.norman_requirement == NormanRequirement.option_badges: - set_rule( - get_entrance("MAP_PETALBURG_CITY_GYM:2/MAP_PETALBURG_CITY_GYM:3"), - lambda state: state.has_group_unique("Badge", world.player, world.options.norman_count.value) - ) - set_rule( - get_entrance("MAP_PETALBURG_CITY_GYM:5/MAP_PETALBURG_CITY_GYM:6"), - lambda state: state.has_group_unique("Badge", world.player, world.options.norman_count.value) - ) - else: - set_rule( - get_entrance("MAP_PETALBURG_CITY_GYM:2/MAP_PETALBURG_CITY_GYM:3"), - lambda state: defeated_n_gym_leaders(state, world.options.norman_count.value) + location_rules["NPC_GIFT_RECEIVED_HM_SURF"] = Has("EVENT_DEFEAT_NORMAN") + + entrance_rules["REGION_PETALBURG_CITY/MAIN -> REGION_PETALBURG_CITY/SOUTH_POND"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_PETALBURG_CITY/MAIN -> REGION_PETALBURG_CITY/NORTH_POND"] = hm_rules["HM03 Surf"] + entrance_rules["MAP_PETALBURG_CITY_GYM:2/MAP_PETALBURG_CITY_GYM:3"] = ( + ( + OptionFilter(NormanRequirement, NormanRequirement.option_badges) & + HasGroupUnique("Badge", world.options.norman_count.value) + ) | + ( + OptionFilter(NormanRequirement, NormanRequirement.option_gyms) & + create_defeated_n_gym_leaders_rule(world.options.norman_count.value) ) - set_rule( - get_entrance("MAP_PETALBURG_CITY_GYM:5/MAP_PETALBURG_CITY_GYM:6"), - lambda state: defeated_n_gym_leaders(state, world.options.norman_count.value) + ) + entrance_rules["MAP_PETALBURG_CITY_GYM:5/MAP_PETALBURG_CITY_GYM:6"] = ( + ( + OptionFilter(NormanRequirement, NormanRequirement.option_badges) & + HasGroupUnique("Badge", world.options.norman_count.value) + ) | + ( + OptionFilter(NormanRequirement, NormanRequirement.option_gyms) & + create_defeated_n_gym_leaders_rule(world.options.norman_count.value) ) + ) # Route 104 - set_rule( - get_entrance("REGION_ROUTE104/SOUTH -> REGION_ROUTE104/SOUTH_WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_ROUTE104/NORTH -> REGION_ROUTE104/NORTH_POND"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_ROUTE104/NORTH -> REGION_ROUTE104/TREE_ALCOVE_2"), - hm_rules["HM01 Cut"] - ) - set_rule( - get_entrance("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN"), - lambda state: state.has("EVENT_TALK_TO_MR_STONE", world.player) - ) + entrance_rules["REGION_ROUTE104/SOUTH -> REGION_ROUTE104/SOUTH_WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_ROUTE104/NORTH -> REGION_ROUTE104/NORTH_POND"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_ROUTE104/NORTH -> REGION_ROUTE104/TREE_ALCOVE_2"] = hm_rules["HM01 Cut"] + entrance_rules["REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN"] = Has("EVENT_TALK_TO_MR_STONE") # Petalburg Woods - set_rule( - get_entrance("REGION_PETALBURG_WOODS/WEST_PATH -> REGION_PETALBURG_WOODS/EAST_PATH"), - hm_rules["HM01 Cut"] - ) + entrance_rules["REGION_PETALBURG_WOODS/WEST_PATH -> REGION_PETALBURG_WOODS/EAST_PATH"] = hm_rules["HM01 Cut"] # Rustboro City - set_rule( - get_location("EVENT_RETURN_DEVON_GOODS"), - lambda state: state.has("EVENT_RECOVER_DEVON_GOODS", world.player) - ) + location_rules["EVENT_RETURN_DEVON_GOODS"] = Has("EVENT_RECOVER_DEVON_GOODS") if world.options.trainersanity: - set_rule( - get_location("TRAINER_BRENDAN_RUSTBORO_MUDKIP_REWARD"), - lambda state: state.has("EVENT_RETURN_DEVON_GOODS", world.player) - ) + location_rules["TRAINER_BRENDAN_RUSTBORO_MUDKIP_REWARD"] = Has("EVENT_RETURN_DEVON_GOODS") # Devon Corp - set_rule( - get_entrance("MAP_RUSTBORO_CITY_DEVON_CORP_1F:2/MAP_RUSTBORO_CITY_DEVON_CORP_2F:0"), - lambda state: state.has("EVENT_RETURN_DEVON_GOODS", world.player) - ) + entrance_rules["MAP_RUSTBORO_CITY_DEVON_CORP_1F:2/MAP_RUSTBORO_CITY_DEVON_CORP_2F:0"] = Has("EVENT_RETURN_DEVON_GOODS") # Route 116 - set_rule( - get_entrance("REGION_ROUTE116/WEST -> REGION_ROUTE116/WEST_ABOVE_LEDGE"), - hm_rules["HM01 Cut"] + entrance_rules["REGION_ROUTE116/WEST -> REGION_ROUTE116/WEST_ABOVE_LEDGE"] = hm_rules["HM01 Cut"] + entrance_rules["REGION_ROUTE116/EAST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"] = ( + HasAll("EVENT_DEFEAT_CHAMPION", "TERRA_CAVE_ROUTE_116_1", "EVENT_DEFEAT_SHELLY") ) - set_rule( - get_entrance("REGION_ROUTE116/EAST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) - and state.has("TERRA_CAVE_ROUTE_116_1", world.player) - and state.has("EVENT_DEFEAT_SHELLY", world.player) - ) - set_rule( - get_entrance("REGION_ROUTE116/WEST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) - and state.has("TERRA_CAVE_ROUTE_116_2", world.player) - and state.has("EVENT_DEFEAT_SHELLY", world.player) + entrance_rules["REGION_ROUTE116/WEST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"] = ( + HasAll("EVENT_DEFEAT_CHAMPION", "TERRA_CAVE_ROUTE_116_2", "EVENT_DEFEAT_SHELLY") ) # Rusturf Tunnel - set_rule( - get_entrance("REGION_RUSTURF_TUNNEL/WEST -> REGION_RUSTURF_TUNNEL/EAST"), - hm_rules["HM06 Rock Smash"] - ) - set_rule( - get_entrance("REGION_RUSTURF_TUNNEL/EAST -> REGION_RUSTURF_TUNNEL/WEST"), - hm_rules["HM06 Rock Smash"] - ) - set_rule( - get_location("NPC_GIFT_RECEIVED_HM_STRENGTH"), - hm_rules["HM06 Rock Smash"] - ) - set_rule( - get_location("EVENT_RECOVER_DEVON_GOODS"), - lambda state: state.has("EVENT_DEFEAT_ROXANNE", world.player) - ) + location_rules["NPC_GIFT_RECEIVED_HM_STRENGTH"] = hm_rules["HM06 Rock Smash"] + location_rules["EVENT_RECOVER_DEVON_GOODS"] = Has("EVENT_DEFEAT_ROXANNE") + + entrance_rules["REGION_RUSTURF_TUNNEL/WEST -> REGION_RUSTURF_TUNNEL/EAST"] = hm_rules["HM06 Rock Smash"] + entrance_rules["REGION_RUSTURF_TUNNEL/EAST -> REGION_RUSTURF_TUNNEL/WEST"] = hm_rules["HM06 Rock Smash"] # Route 115 - set_rule( - get_entrance("REGION_ROUTE115/SOUTH_BELOW_LEDGE -> REGION_ROUTE115/SEA"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_ROUTE115/SOUTH_BEACH_NEAR_CAVE -> REGION_ROUTE115/SEA"), - hm_rules["HM03 Surf"] + entrance_rules["REGION_ROUTE115/SOUTH_BELOW_LEDGE -> REGION_ROUTE115/SEA"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_ROUTE115/SOUTH_BEACH_NEAR_CAVE -> REGION_ROUTE115/SEA"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_ROUTE115/SOUTH_ABOVE_LEDGE -> REGION_ROUTE115/SOUTH_BEHIND_ROCK"] = hm_rules["HM06 Rock Smash"] + entrance_rules["REGION_ROUTE115/NORTH_BELOW_SLOPE -> REGION_ROUTE115/SEA"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_ROUTE115/NORTH_BELOW_SLOPE -> REGION_ROUTE115/NORTH_ABOVE_SLOPE"] = Has("Mach Bike") + entrance_rules["REGION_ROUTE115/NORTH_BELOW_SLOPE -> REGION_TERRA_CAVE_ENTRANCE/MAIN"] = ( + HasAll("EVENT_DEFEAT_CHAMPION", "TERRA_CAVE_ROUTE_115_1", "EVENT_DEFEAT_SHELLY") ) - set_rule( - get_entrance("REGION_ROUTE115/SOUTH_ABOVE_LEDGE -> REGION_ROUTE115/SOUTH_BEHIND_ROCK"), - hm_rules["HM06 Rock Smash"] + entrance_rules["REGION_ROUTE115/NORTH_ABOVE_SLOPE -> REGION_TERRA_CAVE_ENTRANCE/MAIN"] = ( + HasAll("EVENT_DEFEAT_CHAMPION", "TERRA_CAVE_ROUTE_115_2", "EVENT_DEFEAT_SHELLY") ) - set_rule( - get_entrance("REGION_ROUTE115/NORTH_BELOW_SLOPE -> REGION_ROUTE115/SEA"), - hm_rules["HM03 Surf"] + entrance_rules["REGION_ROUTE115/SOUTH_BEACH_NEAR_CAVE -> REGION_ROUTE115/SOUTH_ABOVE_LEDGE"] = ( + (OptionFilter(ExtraBoulders, ExtraBoulders.option_true) & hm_rules["HM04 Strength"]) | + (OptionFilter(ExtraBoulders, ExtraBoulders.option_false)) ) - set_rule( - get_entrance("REGION_ROUTE115/NORTH_BELOW_SLOPE -> REGION_ROUTE115/NORTH_ABOVE_SLOPE"), - has_mach_bike + entrance_rules["REGION_ROUTE115/SOUTH_ABOVE_LEDGE -> REGION_ROUTE115/SOUTH_BEACH_NEAR_CAVE"] = ( + (OptionFilter(ExtraBoulders, ExtraBoulders.option_true) & hm_rules["HM04 Strength"]) | + (OptionFilter(ExtraBoulders, ExtraBoulders.option_false)) ) - set_rule( - get_entrance("REGION_ROUTE115/NORTH_BELOW_SLOPE -> REGION_TERRA_CAVE_ENTRANCE/MAIN"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) - and state.has("TERRA_CAVE_ROUTE_115_1", world.player) - and state.has("EVENT_DEFEAT_SHELLY", world.player) - ) - set_rule( - get_entrance("REGION_ROUTE115/NORTH_ABOVE_SLOPE -> REGION_TERRA_CAVE_ENTRANCE/MAIN"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) - and state.has("TERRA_CAVE_ROUTE_115_2", world.player) - and state.has("EVENT_DEFEAT_SHELLY", world.player) + entrance_rules["REGION_ROUTE115/SOUTH_BELOW_LEDGE -> REGION_ROUTE115/SOUTH_ABOVE_LEDGE"] = ( + OptionFilter(ExtraBumpySlope, ExtraBumpySlope.option_true) & Has("Acro Bike") ) - if world.options.extra_boulders: - set_rule( - get_entrance("REGION_ROUTE115/SOUTH_BEACH_NEAR_CAVE -> REGION_ROUTE115/SOUTH_ABOVE_LEDGE"), - hm_rules["HM04 Strength"] - ) - set_rule( - get_entrance("REGION_ROUTE115/SOUTH_ABOVE_LEDGE -> REGION_ROUTE115/SOUTH_BEACH_NEAR_CAVE"), - hm_rules["HM04 Strength"] - ) - - if world.options.extra_bumpy_slope: - set_rule( - get_entrance("REGION_ROUTE115/SOUTH_BELOW_LEDGE -> REGION_ROUTE115/SOUTH_ABOVE_LEDGE"), - has_acro_bike - ) - else: - set_rule( - get_entrance("REGION_ROUTE115/SOUTH_BELOW_LEDGE -> REGION_ROUTE115/SOUTH_ABOVE_LEDGE"), - lambda state: False - ) - # Route 105 - set_rule( - get_entrance("REGION_UNDERWATER_ROUTE105/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"), - lambda state: hm_rules["HM08 Dive"](state) - and state.has("EVENT_DEFEAT_CHAMPION", world.player) - and state.has("MARINE_CAVE_ROUTE_105_1", world.player) - and state.has("EVENT_DEFEAT_SHELLY", world.player) - ) - set_rule( - get_entrance("REGION_UNDERWATER_ROUTE105/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"), - lambda state: hm_rules["HM08 Dive"](state) - and state.has("EVENT_DEFEAT_CHAMPION", world.player) - and state.has("MARINE_CAVE_ROUTE_105_2", world.player) - and state.has("EVENT_DEFEAT_SHELLY", world.player) + entrance_rules["REGION_UNDERWATER_ROUTE105/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"] = ( + hm_rules["HM08 Dive"] & HasAll("EVENT_DEFEAT_CHAMPION", "MARINE_CAVE_ROUTE_105_1", "EVENT_DEFEAT_SHELLY") ) - set_rule( - get_entrance("MAP_ROUTE105:0/MAP_ISLAND_CAVE:0"), - lambda state: state.has("EVENT_UNDO_REGI_SEAL", world.player) + entrance_rules["REGION_UNDERWATER_ROUTE105/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"] = ( + hm_rules["HM08 Dive"] & HasAll("EVENT_DEFEAT_CHAMPION", "MARINE_CAVE_ROUTE_105_2", "EVENT_DEFEAT_SHELLY") ) + entrance_rules["MAP_ROUTE105:0/MAP_ISLAND_CAVE:0"] = Has("EVENT_UNDO_REGI_SEAL") # Route 106 - set_rule( - get_entrance("REGION_ROUTE106/EAST -> REGION_ROUTE106/SEA"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_ROUTE106/WEST -> REGION_ROUTE106/SEA"), - hm_rules["HM03 Surf"] - ) + entrance_rules["REGION_ROUTE106/EAST -> REGION_ROUTE106/SEA"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_ROUTE106/WEST -> REGION_ROUTE106/SEA"] = hm_rules["HM03 Surf"] # Dewford Town - entrance = get_entrance("REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE109/BEACH") - set_rule( - entrance, - lambda state: - state.can_reach("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN", "Entrance", world.player) - and state.has("EVENT_TALK_TO_MR_STONE", world.player) - and state.has("EVENT_DELIVER_LETTER", world.player) + entrance_rules["REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE109/BEACH"] = ( + CanReachEntrance("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN") & + HasAll("EVENT_TALK_TO_MR_STONE", "EVENT_DELIVER_LETTER") ) - world.multiworld.register_indirect_condition( - get_entrance("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN").parent_region, entrance) - set_rule( - get_entrance("REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN"), - lambda state: - state.can_reach("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN", "Entrance", world.player) - and state.has("EVENT_TALK_TO_MR_STONE", world.player) - ) - set_rule( - get_entrance("REGION_DEWFORD_TOWN/MAIN -> REGION_DEWFORD_TOWN/WATER"), - hm_rules["HM03 Surf"] + entrance_rules["REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN"] = ( + CanReachEntrance("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN") & + Has("EVENT_TALK_TO_MR_STONE") ) + entrance_rules["REGION_DEWFORD_TOWN/MAIN -> REGION_DEWFORD_TOWN/WATER"] = hm_rules["HM03 Surf"] # Granite Cave - set_rule( - get_entrance("REGION_GRANITE_CAVE_STEVENS_ROOM/MAIN -> REGION_GRANITE_CAVE_STEVENS_ROOM/LETTER_DELIVERED"), - lambda state: state.has("Letter", world.player) - ) - set_rule( - get_entrance("REGION_GRANITE_CAVE_B1F/LOWER -> REGION_GRANITE_CAVE_B1F/UPPER"), - has_mach_bike - ) + entrance_rules["REGION_GRANITE_CAVE_STEVENS_ROOM/MAIN -> REGION_GRANITE_CAVE_STEVENS_ROOM/LETTER_DELIVERED"] = Has("Letter") + entrance_rules["REGION_GRANITE_CAVE_B1F/LOWER -> REGION_GRANITE_CAVE_B1F/UPPER"] = Has("Mach Bike") # Route 107 - set_rule( - get_entrance("REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE107/MAIN"), - hm_rules["HM03 Surf"] - ) + entrance_rules["REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE107/MAIN"] = hm_rules["HM03 Surf"] # Route 109 - entrance = get_entrance("REGION_ROUTE109/BEACH -> REGION_DEWFORD_TOWN/MAIN") - set_rule( - entrance, - lambda state: - state.can_reach("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN", "Entrance", world.player) - and state.can_reach("REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE109/BEACH", "Entrance", world.player) - and state.has("EVENT_TALK_TO_MR_STONE", world.player) - and state.has("EVENT_DELIVER_LETTER", world.player) - ) - world.multiworld.register_indirect_condition( - get_entrance("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN").parent_region, entrance) - set_rule( - get_entrance("REGION_ROUTE109/BEACH -> REGION_ROUTE109/SEA"), - hm_rules["HM03 Surf"] + entrance_rules["REGION_ROUTE109/BEACH -> REGION_DEWFORD_TOWN/MAIN"] = ( + CanReachEntrance("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN") & + CanReachEntrance("REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE109/BEACH") & + HasAll("EVENT_TALK_TO_MR_STONE", "EVENT_DELIVER_LETTER") ) + entrance_rules["REGION_ROUTE109/BEACH -> REGION_ROUTE109/SEA"] = hm_rules["HM03 Surf"] # Slateport City - set_rule( - get_entrance("REGION_SLATEPORT_CITY/MAIN -> REGION_SLATEPORT_CITY/WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_location("EVENT_TALK_TO_DOCK"), - lambda state: state.has("Devon Goods", world.player) - ) - set_rule( - get_entrance("MAP_SLATEPORT_CITY:5,7/MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_1F:0,1"), - lambda state: state.has("EVENT_TALK_TO_DOCK", world.player) - ) - set_rule( - get_location("EVENT_AQUA_STEALS_SUBMARINE"), - lambda state: state.has("EVENT_RELEASE_GROUDON", world.player) - ) - set_rule( - get_entrance("REGION_SLATEPORT_CITY_HARBOR/MAIN -> REGION_SS_TIDAL_CORRIDOR/MAIN"), - lambda state: state.has("S.S. Ticket", world.player) - ) + location_rules["EVENT_TALK_TO_DOCK"] = Has("Devon Goods") + location_rules["EVENT_AQUA_STEALS_SUBMARINE"] = Has("EVENT_RELEASE_GROUDON") + + entrance_rules["REGION_SLATEPORT_CITY/MAIN -> REGION_SLATEPORT_CITY/WATER"] = hm_rules["HM03 Surf"] + entrance_rules["MAP_SLATEPORT_CITY:5,7/MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_1F:0,1"] = Has("EVENT_TALK_TO_DOCK") + entrance_rules["REGION_SLATEPORT_CITY_HARBOR/MAIN -> REGION_SS_TIDAL_CORRIDOR/MAIN"] = Has("S.S. Ticket") # Route 110 - set_rule( - get_entrance("REGION_ROUTE110/MAIN -> REGION_ROUTE110/SOUTH_WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_ROUTE110/MAIN -> REGION_ROUTE110/NORTH_WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE/WEST -> REGION_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE/EAST"), - lambda state: has_acro_bike(state) or has_mach_bike(state) + entrance_rules["REGION_ROUTE110/MAIN -> REGION_ROUTE110/SOUTH_WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_ROUTE110/MAIN -> REGION_ROUTE110/NORTH_WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE/WEST -> REGION_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE/EAST"] = HasAny("Acro Bike", "Mach Bike") + entrance_rules["REGION_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE/WEST -> REGION_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE/EAST"] = HasAny("Acro Bike", "Mach Bike") + entrance_rules["REGION_ROUTE110/SOUTH -> REGION_ROUTE110/MAIN"] = ( + OptionFilter(RemoveRoadblocks, "Route 110 Aqua Grunts", "contains") | Has("EVENT_RESCUE_CAPT_STERN") ) - set_rule( - get_entrance("REGION_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE/WEST -> REGION_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE/EAST"), - lambda state: has_acro_bike(state) or has_mach_bike(state) + entrance_rules["REGION_ROUTE110/MAIN -> REGION_ROUTE110/SOUTH"] = ( + OptionFilter(RemoveRoadblocks, "Route 110 Aqua Grunts", "contains") | Has("EVENT_RESCUE_CAPT_STERN") ) - if "Route 110 Aqua Grunts" not in world.options.remove_roadblocks.value: - set_rule( - get_entrance("REGION_ROUTE110/SOUTH -> REGION_ROUTE110/MAIN"), - lambda state: state.has("EVENT_RESCUE_CAPT_STERN", world.player) - ) - set_rule( - get_entrance("REGION_ROUTE110/MAIN -> REGION_ROUTE110/SOUTH"), - lambda state: state.has("EVENT_RESCUE_CAPT_STERN", world.player) - ) # Trick House - set_rule( - get_entrance("REGION_ROUTE110_TRICK_HOUSE_PUZZLE1/ENTRANCE -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE1/REWARDS"), - hm_rules["HM01 Cut"] - ) - set_rule( - get_entrance("REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE2/ENTRANCE"), - lambda state: state.has("Dynamo Badge", world.player) and state.has("EVENT_COMPLETE_TRICK_HOUSE_1", world.player) - ) - set_rule( - get_entrance("REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE3/ENTRANCE"), - lambda state: state.has("Heat Badge", world.player) and state.has("EVENT_COMPLETE_TRICK_HOUSE_2", world.player) - ) - set_rule( - get_entrance("REGION_ROUTE110_TRICK_HOUSE_PUZZLE3/ENTRANCE -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE3/REWARDS"), - hm_rules["HM06 Rock Smash"] - ) - set_rule( - get_entrance("REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE4/ENTRANCE"), - lambda state: state.has("Balance Badge", world.player) and state.has("EVENT_COMPLETE_TRICK_HOUSE_3", world.player) - ) - set_rule( - get_entrance("REGION_ROUTE110_TRICK_HOUSE_PUZZLE4/ENTRANCE -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE4/REWARDS"), - hm_rules["HM04 Strength"] - ) - set_rule( - get_entrance("REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE5/ENTRANCE"), - lambda state: state.has("Feather Badge", world.player) and state.has("EVENT_COMPLETE_TRICK_HOUSE_4", world.player) - ) - set_rule( - get_entrance("REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE6/ENTRANCE"), - lambda state: state.has("Mind Badge", world.player) and state.has("EVENT_COMPLETE_TRICK_HOUSE_5", world.player) - ) - set_rule( - get_entrance("REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE7/ENTRANCE"), - lambda state: state.has("Rain Badge", world.player) and state.has("EVENT_COMPLETE_TRICK_HOUSE_6", world.player) - ) - set_rule( - get_entrance("REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE8/ENTRANCE"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and state.has("EVENT_COMPLETE_TRICK_HOUSE_7", world.player) - ) + entrance_rules["REGION_ROUTE110_TRICK_HOUSE_PUZZLE1/ENTRANCE -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE1/REWARDS"] = hm_rules["HM01 Cut"] + entrance_rules["REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE2/ENTRANCE"] = HasAll("Dynamo Badge", "EVENT_COMPLETE_TRICK_HOUSE_1") + entrance_rules["REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE3/ENTRANCE"] = HasAll("Heat Badge", "EVENT_COMPLETE_TRICK_HOUSE_2") + entrance_rules["REGION_ROUTE110_TRICK_HOUSE_PUZZLE3/ENTRANCE -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE3/REWARDS"] = hm_rules["HM06 Rock Smash"] + entrance_rules["REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE4/ENTRANCE"] = HasAll("Balance Badge", "EVENT_COMPLETE_TRICK_HOUSE_3") + entrance_rules["REGION_ROUTE110_TRICK_HOUSE_PUZZLE4/ENTRANCE -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE4/REWARDS"] = hm_rules["HM04 Strength"] + entrance_rules["REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE5/ENTRANCE"] = HasAll("Feather Badge", "EVENT_COMPLETE_TRICK_HOUSE_4") + entrance_rules["REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE6/ENTRANCE"] = HasAll("Mind Badge", "EVENT_COMPLETE_TRICK_HOUSE_5") + entrance_rules["REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE7/ENTRANCE"] = HasAll("Rain Badge", "EVENT_COMPLETE_TRICK_HOUSE_6") + entrance_rules["REGION_ROUTE110_TRICK_HOUSE_ENTRANCE/MAIN -> REGION_ROUTE110_TRICK_HOUSE_PUZZLE8/ENTRANCE"] = HasAll("EVENT_DEFEAT_CHAMPION", "EVENT_COMPLETE_TRICK_HOUSE_7") # Mauville City - set_rule( - get_location("NPC_GIFT_GOT_BASEMENT_KEY_FROM_WATTSON"), - lambda state: state.has("EVENT_DEFEAT_NORMAN", world.player) - ) - set_rule( - get_location("NPC_GIFT_RECEIVED_COIN_CASE"), - lambda state: state.has("EVENT_BUY_HARBOR_MAIL", world.player) - ) + location_rules["NPC_GIFT_GOT_BASEMENT_KEY_FROM_WATTSON"] = Has("EVENT_DEFEAT_NORMAN") + location_rules["NPC_GIFT_RECEIVED_COIN_CASE"] = Has("EVENT_BUY_HARBOR_MAIL") # Route 117 - set_rule( - get_entrance("REGION_ROUTE117/MAIN -> REGION_ROUTE117/PONDS"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_location("EVENT_ENCOUNTER_LATIOS"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) - ) + location_rules["EVENT_ENCOUNTER_LATIOS"] = Has("EVENT_DEFEAT_CHAMPION") + + entrance_rules["REGION_ROUTE117/MAIN -> REGION_ROUTE117/PONDS"] = hm_rules["HM03 Surf"] # Route 111 - set_rule( - get_entrance("REGION_ROUTE111/MIDDLE -> REGION_ROUTE111/DESERT"), - lambda state: state.has("Go Goggles", world.player) - ) - set_rule( - get_entrance("REGION_ROUTE111/NORTH -> REGION_ROUTE111/DESERT"), - lambda state: state.has("Go Goggles", world.player) - ) - set_rule( - get_entrance("REGION_ROUTE111/NORTH -> REGION_ROUTE111/ABOVE_SLOPE"), - has_mach_bike - ) - set_rule( - get_entrance("REGION_ROUTE111/MIDDLE -> REGION_ROUTE111/SOUTH"), - hm_rules["HM06 Rock Smash"] - ) - set_rule( - get_entrance("REGION_ROUTE111/SOUTH -> REGION_ROUTE111/SOUTH_POND"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_ROUTE111/SOUTH -> REGION_ROUTE111/MIDDLE"), - hm_rules["HM06 Rock Smash"] - ) - set_rule( - get_entrance("MAP_ROUTE111:4/MAP_TRAINER_HILL_ENTRANCE:0"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) - ) - set_rule( - get_entrance("MAP_ROUTE111:1/MAP_DESERT_RUINS:0"), - lambda state: state.has("EVENT_UNDO_REGI_SEAL", world.player) - ) - set_rule( - get_entrance("MAP_DESERT_RUINS:0/MAP_ROUTE111:1"), - hm_rules["HM06 Rock Smash"] - ) + entrance_rules["REGION_ROUTE111/MIDDLE -> REGION_ROUTE111/DESERT"] = Has("Go Goggles") + entrance_rules["REGION_ROUTE111/NORTH -> REGION_ROUTE111/DESERT"] = Has("Go Goggles") + entrance_rules["REGION_ROUTE111/NORTH -> REGION_ROUTE111/ABOVE_SLOPE"] = Has("Mach Bike") + entrance_rules["REGION_ROUTE111/MIDDLE -> REGION_ROUTE111/SOUTH"] = hm_rules["HM06 Rock Smash"] + entrance_rules["REGION_ROUTE111/SOUTH -> REGION_ROUTE111/SOUTH_POND"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_ROUTE111/SOUTH -> REGION_ROUTE111/MIDDLE"] = hm_rules["HM06 Rock Smash"] + entrance_rules["MAP_ROUTE111:4/MAP_TRAINER_HILL_ENTRANCE:0"] = Has("EVENT_DEFEAT_CHAMPION") + entrance_rules["MAP_ROUTE111:1/MAP_DESERT_RUINS:0"] = Has("EVENT_UNDO_REGI_SEAL") + entrance_rules["MAP_DESERT_RUINS:0/MAP_ROUTE111:1"] = hm_rules["HM06 Rock Smash"] # Route 112 - if "Route 112 Magma Grunts" not in world.options.remove_roadblocks.value: - set_rule( - get_entrance("REGION_ROUTE112/SOUTH_EAST -> REGION_ROUTE112/CABLE_CAR_STATION_ENTRANCE"), - lambda state: state.has("EVENT_MAGMA_STEALS_METEORITE", world.player) - ) - set_rule( - get_entrance("REGION_ROUTE112/CABLE_CAR_STATION_ENTRANCE -> REGION_ROUTE112/SOUTH_EAST"), - lambda state: state.has("EVENT_MAGMA_STEALS_METEORITE", world.player) - ) + entrance_rules["REGION_ROUTE112/SOUTH_EAST -> REGION_ROUTE112/CABLE_CAR_STATION_ENTRANCE"] = ( + OptionFilter(RemoveRoadblocks, "Route 112 Magma Grunts", "contains") | Has("EVENT_MAGMA_STEALS_METEORITE") + ) + entrance_rules["REGION_ROUTE112/CABLE_CAR_STATION_ENTRANCE -> REGION_ROUTE112/SOUTH_EAST"] = ( + OptionFilter(RemoveRoadblocks, "Route 112 Magma Grunts", "contains") | Has("EVENT_MAGMA_STEALS_METEORITE") + ) # Fiery Path - set_rule( - get_entrance("REGION_FIERY_PATH/MAIN -> REGION_FIERY_PATH/BEHIND_BOULDER"), - hm_rules["HM04 Strength"] - ) + entrance_rules["REGION_FIERY_PATH/MAIN -> REGION_FIERY_PATH/BEHIND_BOULDER"] = hm_rules["HM04 Strength"] # Route 114 - set_rule( - get_entrance("REGION_ROUTE114/MAIN -> REGION_ROUTE114/WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_ROUTE114/WATER -> REGION_ROUTE114/ABOVE_WATERFALL"), - hm_rules["HM07 Waterfall"] - ) - set_rule( - get_entrance("MAP_ROUTE114_FOSSIL_MANIACS_TUNNEL:2/MAP_DESERT_UNDERPASS:0"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) + entrance_rules["REGION_ROUTE114/MAIN -> REGION_ROUTE114/WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_ROUTE114/WATER -> REGION_ROUTE114/ABOVE_WATERFALL"] = hm_rules["HM07 Waterfall"] + entrance_rules["MAP_ROUTE114_FOSSIL_MANIACS_TUNNEL:2/MAP_DESERT_UNDERPASS:0"] = Has("EVENT_DEFEAT_CHAMPION") + entrance_rules["REGION_ROUTE114/ABOVE_WATERFALL -> REGION_TERRA_CAVE_ENTRANCE/MAIN"] = ( + HasAll("EVENT_DEFEAT_CHAMPION", "TERRA_CAVE_ROUTE_114_1", "EVENT_DEFEAT_SHELLY") ) - set_rule( - get_entrance("REGION_ROUTE114/ABOVE_WATERFALL -> REGION_TERRA_CAVE_ENTRANCE/MAIN"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) - and state.has("TERRA_CAVE_ROUTE_114_1", world.player) - and state.has("EVENT_DEFEAT_SHELLY", world.player) - ) - set_rule( - get_entrance("REGION_ROUTE114/MAIN -> REGION_TERRA_CAVE_ENTRANCE/MAIN"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) - and state.has("TERRA_CAVE_ROUTE_114_2", world.player) - and state.has("EVENT_DEFEAT_SHELLY", world.player) + entrance_rules["REGION_ROUTE114/MAIN -> REGION_TERRA_CAVE_ENTRANCE/MAIN"] = ( + HasAll("EVENT_DEFEAT_CHAMPION", "TERRA_CAVE_ROUTE_114_2", "EVENT_DEFEAT_SHELLY") ) # Meteor Falls - set_rule( - get_entrance("REGION_METEOR_FALLS_1F_1R/MAIN -> REGION_METEOR_FALLS_1F_1R/WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_METEOR_FALLS_1F_1R/WATER -> REGION_METEOR_FALLS_1F_1R/WATER_ABOVE_WATERFALL"), - hm_rules["HM07 Waterfall"] - ) - set_rule( - get_entrance("REGION_METEOR_FALLS_1F_1R/ABOVE_WATERFALL -> REGION_METEOR_FALLS_1F_1R/WATER_ABOVE_WATERFALL"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("MAP_METEOR_FALLS_1F_1R:5/MAP_METEOR_FALLS_STEVENS_CAVE:0"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) - ) - set_rule( - get_entrance("REGION_METEOR_FALLS_1F_2R/LEFT_SPLIT -> REGION_METEOR_FALLS_1F_2R/LEFT_SPLIT_WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_METEOR_FALLS_1F_2R/RIGHT_SPLIT -> REGION_METEOR_FALLS_1F_2R/RIGHT_SPLIT_WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_METEOR_FALLS_B1F_1R/HIGHEST_LADDER -> REGION_METEOR_FALLS_B1F_1R/WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_METEOR_FALLS_B1F_1R/NORTH_SHORE -> REGION_METEOR_FALLS_B1F_1R/WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_METEOR_FALLS_B1F_1R/SOUTH_SHORE -> REGION_METEOR_FALLS_B1F_1R/WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_METEOR_FALLS_B1F_2R/ENTRANCE -> REGION_METEOR_FALLS_B1F_2R/WATER"), - hm_rules["HM03 Surf"] - ) + entrance_rules["REGION_METEOR_FALLS_1F_1R/MAIN -> REGION_METEOR_FALLS_1F_1R/WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_METEOR_FALLS_1F_1R/WATER -> REGION_METEOR_FALLS_1F_1R/WATER_ABOVE_WATERFALL"] = hm_rules["HM07 Waterfall"] + entrance_rules["REGION_METEOR_FALLS_1F_1R/ABOVE_WATERFALL -> REGION_METEOR_FALLS_1F_1R/WATER_ABOVE_WATERFALL"] = hm_rules["HM03 Surf"] + entrance_rules["MAP_METEOR_FALLS_1F_1R:5/MAP_METEOR_FALLS_STEVENS_CAVE:0"] = Has("EVENT_DEFEAT_CHAMPION") + entrance_rules["REGION_METEOR_FALLS_1F_2R/LEFT_SPLIT -> REGION_METEOR_FALLS_1F_2R/LEFT_SPLIT_WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_METEOR_FALLS_1F_2R/RIGHT_SPLIT -> REGION_METEOR_FALLS_1F_2R/RIGHT_SPLIT_WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_METEOR_FALLS_B1F_1R/HIGHEST_LADDER -> REGION_METEOR_FALLS_B1F_1R/WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_METEOR_FALLS_B1F_1R/NORTH_SHORE -> REGION_METEOR_FALLS_B1F_1R/WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_METEOR_FALLS_B1F_1R/SOUTH_SHORE -> REGION_METEOR_FALLS_B1F_1R/WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_METEOR_FALLS_B1F_2R/ENTRANCE -> REGION_METEOR_FALLS_B1F_2R/WATER"] = hm_rules["HM03 Surf"] # Jagged Pass - set_rule( - get_entrance("REGION_JAGGED_PASS/BOTTOM -> REGION_JAGGED_PASS/MIDDLE"), - has_acro_bike - ) - set_rule( - get_entrance("REGION_JAGGED_PASS/MIDDLE -> REGION_JAGGED_PASS/TOP"), - has_acro_bike - ) - set_rule( - get_entrance("MAP_JAGGED_PASS:4/MAP_MAGMA_HIDEOUT_1F:0"), - lambda state: state.has("Magma Emblem", world.player) - ) + entrance_rules["REGION_JAGGED_PASS/BOTTOM -> REGION_JAGGED_PASS/MIDDLE"] = Has("Acro Bike") + entrance_rules["REGION_JAGGED_PASS/MIDDLE -> REGION_JAGGED_PASS/TOP"] = Has("Acro Bike") + entrance_rules["MAP_JAGGED_PASS:4/MAP_MAGMA_HIDEOUT_1F:0"] = Has("Magma Emblem") # Lavaridge Town - set_rule( - get_location("NPC_GIFT_RECEIVED_GO_GOGGLES"), - lambda state: state.has("EVENT_DEFEAT_FLANNERY", world.player) - ) + location_rules["NPC_GIFT_RECEIVED_GO_GOGGLES"] = Has("EVENT_DEFEAT_FLANNERY") # Mirage Tower - set_rule( - get_entrance("REGION_MIRAGE_TOWER_2F/TOP -> REGION_MIRAGE_TOWER_2F/BOTTOM"), - has_mach_bike - ) - set_rule( - get_entrance("REGION_MIRAGE_TOWER_2F/BOTTOM -> REGION_MIRAGE_TOWER_2F/TOP"), - has_mach_bike - ) - set_rule( - get_entrance("REGION_MIRAGE_TOWER_3F/TOP -> REGION_MIRAGE_TOWER_3F/BOTTOM"), - hm_rules["HM06 Rock Smash"] - ) - set_rule( - get_entrance("REGION_MIRAGE_TOWER_3F/BOTTOM -> REGION_MIRAGE_TOWER_3F/TOP"), - hm_rules["HM06 Rock Smash"] - ) - set_rule( - get_entrance("REGION_MIRAGE_TOWER_4F/MAIN -> REGION_MIRAGE_TOWER_4F/FOSSIL_PLATFORM"), - hm_rules["HM06 Rock Smash"] - ) + entrance_rules["REGION_MIRAGE_TOWER_2F/TOP -> REGION_MIRAGE_TOWER_2F/BOTTOM"] = Has("Mach Bike") + entrance_rules["REGION_MIRAGE_TOWER_2F/BOTTOM -> REGION_MIRAGE_TOWER_2F/TOP"] = Has("Mach Bike") + entrance_rules["REGION_MIRAGE_TOWER_3F/TOP -> REGION_MIRAGE_TOWER_3F/BOTTOM"] = hm_rules["HM06 Rock Smash"] + entrance_rules["REGION_MIRAGE_TOWER_3F/BOTTOM -> REGION_MIRAGE_TOWER_3F/TOP"] = hm_rules["HM06 Rock Smash"] + entrance_rules["REGION_MIRAGE_TOWER_4F/MAIN -> REGION_MIRAGE_TOWER_4F/FOSSIL_PLATFORM"] = hm_rules["HM06 Rock Smash"] # Abandoned Ship - set_rule( - get_entrance("REGION_ABANDONED_SHIP_ROOMS_B1F/CENTER -> REGION_ABANDONED_SHIP_UNDERWATER1/MAIN"), - hm_rules["HM08 Dive"] - ) - set_rule( - get_entrance("REGION_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS/MAIN -> REGION_ABANDONED_SHIP_UNDERWATER2/MAIN"), - hm_rules["HM08 Dive"] - ) - set_rule( - get_entrance("MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:0/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:0"), - lambda state: state.has("Room 1 Key", world.player) - ) - set_rule( - get_entrance("MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:1/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:2"), - lambda state: state.has("Room 2 Key", world.player) - ) - set_rule( - get_entrance("MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:3/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:6"), - lambda state: state.has("Room 4 Key", world.player) - ) - set_rule( - get_entrance("MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:5/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:8"), - lambda state: state.has("Room 6 Key", world.player) - ) - set_rule( - get_entrance("MAP_ABANDONED_SHIP_CORRIDORS_B1F:5/MAP_ABANDONED_SHIP_ROOM_B1F:0"), - lambda state: state.has("Storage Key", world.player) - ) + entrance_rules["REGION_ABANDONED_SHIP_ROOMS_B1F/CENTER -> REGION_ABANDONED_SHIP_UNDERWATER1/MAIN"] = hm_rules["HM08 Dive"] + entrance_rules["REGION_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS/MAIN -> REGION_ABANDONED_SHIP_UNDERWATER2/MAIN"] = hm_rules["HM08 Dive"] + entrance_rules["MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:0/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:0"] = Has("Room 1 Key") + entrance_rules["MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:1/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:2"] = Has("Room 2 Key") + entrance_rules["MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:3/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:6"] = Has("Room 4 Key") + entrance_rules["MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS:5/MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS:8"] = Has("Room 6 Key") + entrance_rules["MAP_ABANDONED_SHIP_CORRIDORS_B1F:5/MAP_ABANDONED_SHIP_ROOM_B1F:0"] = Has("Storage Key") # New Mauville - set_rule( - get_entrance("MAP_NEW_MAUVILLE_ENTRANCE:1/MAP_NEW_MAUVILLE_INSIDE:0"), - lambda state: state.has("Basement Key", world.player) - ) + entrance_rules["MAP_NEW_MAUVILLE_ENTRANCE:1/MAP_NEW_MAUVILLE_INSIDE:0"] = Has("Basement Key") # Route 118 - if world.options.modify_118: - set_rule( - get_entrance("REGION_ROUTE118/WEST -> REGION_ROUTE118/EAST"), - has_acro_bike - ) - set_rule( - get_entrance("REGION_ROUTE118/EAST -> REGION_ROUTE118/WEST"), - has_acro_bike - ) - set_rule( - get_entrance("REGION_ROUTE118/WEST_WATER -> REGION_ROUTE118/EAST_WATER"), - lambda state: False - ) - set_rule( - get_entrance("REGION_ROUTE118/EAST_WATER -> REGION_ROUTE118/WEST_WATER"), - lambda state: False - ) - else: - set_rule( - get_entrance("REGION_ROUTE118/WEST -> REGION_ROUTE118/EAST"), - lambda state: False - ) - set_rule( - get_entrance("REGION_ROUTE118/EAST -> REGION_ROUTE118/WEST"), - lambda state: False - ) - - set_rule( - get_entrance("REGION_ROUTE118/WEST -> REGION_ROUTE118/WEST_WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_ROUTE118/EAST -> REGION_ROUTE118/EAST_WATER"), - hm_rules["HM03 Surf"] + entrance_rules["REGION_ROUTE118/WEST -> REGION_ROUTE118/EAST"] = ( + OptionFilter(ModifyRoute118, ModifyRoute118.option_true) & Has("Acro Bike") ) - set_rule( - get_entrance("REGION_ROUTE118/EAST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) - and state.has("TERRA_CAVE_ROUTE_118_1", world.player) - and state.has("EVENT_DEFEAT_SHELLY", world.player) + entrance_rules["REGION_ROUTE118/EAST -> REGION_ROUTE118/WEST"] = ( + OptionFilter(ModifyRoute118, ModifyRoute118.option_true) & Has("Acro Bike") ) - set_rule( - get_entrance("REGION_ROUTE118/WEST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) - and state.has("TERRA_CAVE_ROUTE_118_2", world.player) - and state.has("EVENT_DEFEAT_SHELLY", world.player) + entrance_rules["REGION_ROUTE118/WEST_WATER -> REGION_ROUTE118/EAST_WATER"] = ( + OptionFilter(ModifyRoute118, ModifyRoute118.option_false) & True_() ) - - # Route 119 - set_rule( - get_entrance("REGION_ROUTE119/LOWER -> REGION_ROUTE119/LOWER_WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_ROUTE119/LOWER -> REGION_ROUTE119/LOWER_ACROSS_RAILS"), - has_acro_bike + entrance_rules["REGION_ROUTE118/EAST_WATER -> REGION_ROUTE118/WEST_WATER"] = ( + OptionFilter(ModifyRoute118, ModifyRoute118.option_false) & True_() ) - set_rule( - get_entrance("REGION_ROUTE119/LOWER_ACROSS_RAILS -> REGION_ROUTE119/LOWER"), - has_acro_bike + entrance_rules["REGION_ROUTE118/WEST -> REGION_ROUTE118/WEST_WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_ROUTE118/EAST -> REGION_ROUTE118/EAST_WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_ROUTE118/EAST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"] = ( + HasAll("EVENT_DEFEAT_CHAMPION", "TERRA_CAVE_ROUTE_118_1", "EVENT_DEFEAT_SHELLY") ) - set_rule( - get_entrance("REGION_ROUTE119/UPPER -> REGION_ROUTE119/MIDDLE_RIVER"), - hm_rules["HM03 Surf"] + entrance_rules["REGION_ROUTE118/WEST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"] = ( + HasAll("EVENT_DEFEAT_CHAMPION", "TERRA_CAVE_ROUTE_118_2", "EVENT_DEFEAT_SHELLY") ) - set_rule( - get_entrance("REGION_ROUTE119/MIDDLE_RIVER -> REGION_ROUTE119/ABOVE_WATERFALL"), - hm_rules["HM07 Waterfall"] - ) - set_rule( - get_entrance("REGION_ROUTE119/ABOVE_WATERFALL -> REGION_ROUTE119/MIDDLE_RIVER"), - hm_rules["HM07 Waterfall"] + + # Route 119 + entrance_rules["REGION_ROUTE119/LOWER -> REGION_ROUTE119/LOWER_WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_ROUTE119/LOWER -> REGION_ROUTE119/LOWER_ACROSS_RAILS"] = Has("Acro Bike") + entrance_rules["REGION_ROUTE119/LOWER_ACROSS_RAILS -> REGION_ROUTE119/LOWER"] = Has("Acro Bike") + entrance_rules["REGION_ROUTE119/UPPER -> REGION_ROUTE119/MIDDLE_RIVER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_ROUTE119/MIDDLE_RIVER -> REGION_ROUTE119/ABOVE_WATERFALL"] = hm_rules["HM07 Waterfall"] + entrance_rules["REGION_ROUTE119/ABOVE_WATERFALL -> REGION_ROUTE119/MIDDLE_RIVER"] = hm_rules["HM07 Waterfall"] + entrance_rules["REGION_ROUTE119/ABOVE_WATERFALL -> REGION_ROUTE119/ABOVE_WATERFALL_ACROSS_RAILS"] = Has("Acro Bike") + entrance_rules["REGION_ROUTE119/MIDDLE -> REGION_ROUTE119/UPPER"] = ( + OptionFilter(RemoveRoadblocks, "Route 119 Aqua Grunts", "contains") | Has("EVENT_DEFEAT_SHELLY") ) - set_rule( - get_entrance("REGION_ROUTE119/ABOVE_WATERFALL -> REGION_ROUTE119/ABOVE_WATERFALL_ACROSS_RAILS"), - has_acro_bike + entrance_rules["REGION_ROUTE119/UPPER -> REGION_ROUTE119/MIDDLE"] = ( + OptionFilter(RemoveRoadblocks, "Route 119 Aqua Grunts", "contains") | Has("EVENT_DEFEAT_SHELLY") ) - if "Route 119 Aqua Grunts" not in world.options.remove_roadblocks.value: - set_rule( - get_entrance("REGION_ROUTE119/MIDDLE -> REGION_ROUTE119/UPPER"), - lambda state: state.has("EVENT_DEFEAT_SHELLY", world.player) - ) - set_rule( - get_entrance("REGION_ROUTE119/UPPER -> REGION_ROUTE119/MIDDLE"), - lambda state: state.has("EVENT_DEFEAT_SHELLY", world.player) - ) # Fortree City - set_rule( - get_entrance("REGION_FORTREE_CITY/MAIN -> REGION_FORTREE_CITY/BEFORE_GYM"), - lambda state: state.has("Devon Scope", world.player) - ) - set_rule( - get_entrance("REGION_FORTREE_CITY/BEFORE_GYM -> REGION_FORTREE_CITY/MAIN"), - lambda state: state.has("Devon Scope", world.player) - ) + entrance_rules["REGION_FORTREE_CITY/MAIN -> REGION_FORTREE_CITY/BEFORE_GYM"] = Has("Devon Scope") + entrance_rules["REGION_FORTREE_CITY/BEFORE_GYM -> REGION_FORTREE_CITY/MAIN"] = Has("Devon Scope") # Route 120 - set_rule( - get_entrance("REGION_ROUTE120/NORTH -> REGION_ROUTE120/NORTH_POND_SHORE"), - lambda state: state.has("Devon Scope", world.player) - ) - set_rule( - get_entrance("REGION_ROUTE120/NORTH_POND_SHORE -> REGION_ROUTE120/NORTH"), - lambda state: state.has("Devon Scope", world.player) - ) - set_rule( - get_entrance("REGION_ROUTE120/NORTH_POND_SHORE -> REGION_ROUTE120/NORTH_POND"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_ROUTE120/SOUTH -> REGION_ROUTE120/SOUTH_ALCOVE"), - hm_rules["HM01 Cut"] - ) - set_rule( - get_entrance("REGION_ROUTE120/SOUTH -> REGION_ROUTE120/SOUTH_PONDS"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_ROUTE120/SOUTH_ALCOVE -> REGION_ROUTE120/SOUTH"), - hm_rules["HM01 Cut"] - ) - set_rule( - get_entrance("MAP_ROUTE120:0/MAP_ANCIENT_TOMB:0"), - lambda state: state.has("EVENT_UNDO_REGI_SEAL", world.player) - ) - set_rule( - get_entrance("MAP_ANCIENT_TOMB:1/MAP_ANCIENT_TOMB:2"), - hm_rules["HM05 Flash"] - ) + entrance_rules["REGION_ROUTE120/NORTH -> REGION_ROUTE120/NORTH_POND_SHORE"] = Has("Devon Scope") + entrance_rules["REGION_ROUTE120/NORTH_POND_SHORE -> REGION_ROUTE120/NORTH"] = Has("Devon Scope") + entrance_rules["REGION_ROUTE120/NORTH_POND_SHORE -> REGION_ROUTE120/NORTH_POND"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_ROUTE120/SOUTH -> REGION_ROUTE120/SOUTH_ALCOVE"] = hm_rules["HM01 Cut"] + entrance_rules["REGION_ROUTE120/SOUTH -> REGION_ROUTE120/SOUTH_PONDS"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_ROUTE120/SOUTH_ALCOVE -> REGION_ROUTE120/SOUTH"] = hm_rules["HM01 Cut"] + entrance_rules["MAP_ROUTE120:0/MAP_ANCIENT_TOMB:0"] = Has("EVENT_UNDO_REGI_SEAL") + entrance_rules["MAP_ANCIENT_TOMB:1/MAP_ANCIENT_TOMB:2"] = hm_rules["HM05 Flash"] # Route 121 - set_rule( - get_entrance("REGION_ROUTE121/EAST -> REGION_ROUTE121/WEST"), - hm_rules["HM01 Cut"] - ) - set_rule( - get_entrance("REGION_ROUTE121/EAST -> REGION_ROUTE121/WATER"), - hm_rules["HM03 Surf"] - ) + entrance_rules["REGION_ROUTE121/EAST -> REGION_ROUTE121/WEST"] = hm_rules["HM01 Cut"] + entrance_rules["REGION_ROUTE121/EAST -> REGION_ROUTE121/WATER"] = hm_rules["HM03 Surf"] # Safari Zone - set_rule( - get_entrance("MAP_ROUTE121_SAFARI_ZONE_ENTRANCE:0,1/MAP_SAFARI_ZONE_SOUTH:0"), - lambda state: state.has("Pokeblock Case", world.player) - ) - set_rule( - get_entrance("REGION_SAFARI_ZONE_NORTHWEST/MAIN -> REGION_SAFARI_ZONE_NORTHWEST/POND"), - hm_rules["HM03 Surf"] + entrance_rules["MAP_ROUTE121_SAFARI_ZONE_ENTRANCE:0,1/MAP_SAFARI_ZONE_SOUTH:0"] = Has("Pokeblock Case") + entrance_rules["REGION_SAFARI_ZONE_NORTHWEST/MAIN -> REGION_SAFARI_ZONE_NORTHWEST/POND"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_SAFARI_ZONE_SOUTH/MAIN -> REGION_SAFARI_ZONE_NORTH/MAIN"] = Has("Acro Bike") + entrance_rules["REGION_SAFARI_ZONE_SOUTHWEST/MAIN -> REGION_SAFARI_ZONE_NORTHWEST/MAIN"] = Has("Mach Bike") + entrance_rules["REGION_SAFARI_ZONE_SOUTHWEST/MAIN -> REGION_SAFARI_ZONE_SOUTHWEST/POND"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_SAFARI_ZONE_SOUTHEAST/MAIN -> REGION_SAFARI_ZONE_SOUTHEAST/WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_SAFARI_ZONE_SOUTH/MAIN -> REGION_SAFARI_ZONE_SOUTHEAST/MAIN"] = ( + OptionFilter(RemoveRoadblocks, "Safari Zone Construction Workers", "contains") | Has("EVENT_DEFEAT_CHAMPION") ) - set_rule( - get_entrance("REGION_SAFARI_ZONE_SOUTH/MAIN -> REGION_SAFARI_ZONE_NORTH/MAIN"), - has_acro_bike - ) - set_rule( - get_entrance("REGION_SAFARI_ZONE_SOUTHWEST/MAIN -> REGION_SAFARI_ZONE_NORTHWEST/MAIN"), - has_mach_bike - ) - set_rule( - get_entrance("REGION_SAFARI_ZONE_SOUTHWEST/MAIN -> REGION_SAFARI_ZONE_SOUTHWEST/POND"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_SAFARI_ZONE_SOUTHEAST/MAIN -> REGION_SAFARI_ZONE_SOUTHEAST/WATER"), - hm_rules["HM03 Surf"] - ) - if "Safari Zone Construction Workers" not in world.options.remove_roadblocks.value: - set_rule( - get_entrance("REGION_SAFARI_ZONE_SOUTH/MAIN -> REGION_SAFARI_ZONE_SOUTHEAST/MAIN"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) - ) # Route 122 - set_rule( - get_entrance("REGION_ROUTE122/MT_PYRE_ENTRANCE -> REGION_ROUTE122/SEA"), - hm_rules["HM03 Surf"] - ) + entrance_rules["REGION_ROUTE122/MT_PYRE_ENTRANCE -> REGION_ROUTE122/SEA"] = hm_rules["HM03 Surf"] # Route 123 - set_rule( - get_entrance("REGION_ROUTE123/EAST -> REGION_ROUTE122/SEA"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_ROUTE123/EAST -> REGION_ROUTE123/POND"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_ROUTE123/EAST -> REGION_ROUTE123/EAST_BEHIND_TREE"), - hm_rules["HM01 Cut"] - ) + entrance_rules["REGION_ROUTE123/EAST -> REGION_ROUTE122/SEA"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_ROUTE123/EAST -> REGION_ROUTE123/POND"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_ROUTE123/EAST -> REGION_ROUTE123/EAST_BEHIND_TREE"] = hm_rules["HM01 Cut"] # Lilycove City - set_rule( - get_entrance("REGION_LILYCOVE_CITY/MAIN -> REGION_LILYCOVE_CITY/SEA"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_LILYCOVE_CITY_HARBOR/MAIN -> REGION_SS_TIDAL_CORRIDOR/MAIN"), - lambda state: state.has("S.S. Ticket", world.player) + entrance_rules["REGION_LILYCOVE_CITY/MAIN -> REGION_LILYCOVE_CITY/SEA"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_LILYCOVE_CITY_HARBOR/MAIN -> REGION_SS_TIDAL_CORRIDOR/MAIN"] = Has("S.S. Ticket") + entrance_rules["REGION_LILYCOVE_CITY_HARBOR/MAIN -> REGION_SOUTHERN_ISLAND_EXTERIOR/MAIN"] = Has("Eon Ticket") + entrance_rules["REGION_LILYCOVE_CITY_HARBOR/MAIN -> REGION_FARAWAY_ISLAND_ENTRANCE/MAIN"] = Has("Old Sea Map") + entrance_rules["REGION_LILYCOVE_CITY_HARBOR/MAIN -> REGION_BIRTH_ISLAND_HARBOR/MAIN"] = Has("Aurora Ticket") + entrance_rules["REGION_LILYCOVE_CITY_HARBOR/MAIN -> REGION_NAVEL_ROCK_HARBOR/MAIN"] = Has("Mystic Ticket") + entrance_rules["REGION_LILYCOVE_CITY/SEA -> REGION_ROUTE124/MAIN"] = ( + OptionFilter(RemoveRoadblocks, "Lilycove City Wailmer", "contains") | Has("EVENT_CLEAR_AQUA_HIDEOUT") ) - set_rule( - get_entrance("REGION_LILYCOVE_CITY_HARBOR/MAIN -> REGION_SOUTHERN_ISLAND_EXTERIOR/MAIN"), - lambda state: state.has("Eon Ticket", world.player) - ) - set_rule( - get_entrance("REGION_LILYCOVE_CITY_HARBOR/MAIN -> REGION_FARAWAY_ISLAND_ENTRANCE/MAIN"), - lambda state: state.has("Old Sea Map", world.player) - ) - set_rule( - get_entrance("REGION_LILYCOVE_CITY_HARBOR/MAIN -> REGION_BIRTH_ISLAND_HARBOR/MAIN"), - lambda state: state.has("Aurora Ticket", world.player) - ) - set_rule( - get_entrance("REGION_LILYCOVE_CITY_HARBOR/MAIN -> REGION_NAVEL_ROCK_HARBOR/MAIN"), - lambda state: state.has("Mystic Ticket", world.player) + entrance_rules["REGION_ROUTE124/MAIN -> REGION_LILYCOVE_CITY/SEA"] = ( + OptionFilter(RemoveRoadblocks, "Lilycove City Wailmer", "contains") | Has("EVENT_CLEAR_AQUA_HIDEOUT") ) - if "Lilycove City Wailmer" not in world.options.remove_roadblocks.value: - set_rule( - get_entrance("REGION_LILYCOVE_CITY/SEA -> REGION_ROUTE124/MAIN"), - lambda state: state.has("EVENT_CLEAR_AQUA_HIDEOUT", world.player) - ) - set_rule( - get_entrance("REGION_ROUTE124/MAIN -> REGION_LILYCOVE_CITY/SEA"), - lambda state: state.has("EVENT_CLEAR_AQUA_HIDEOUT", world.player) - ) - # Magma Hideout - set_rule( - get_entrance("REGION_MAGMA_HIDEOUT_1F/ENTRANCE -> REGION_MAGMA_HIDEOUT_1F/MAIN"), - hm_rules["HM04 Strength"] - ) - set_rule( - get_entrance("REGION_MAGMA_HIDEOUT_1F/MAIN -> REGION_MAGMA_HIDEOUT_1F/ENTRANCE"), - hm_rules["HM04 Strength"] - ) + entrance_rules["REGION_MAGMA_HIDEOUT_1F/ENTRANCE -> REGION_MAGMA_HIDEOUT_1F/MAIN"] = hm_rules["HM04 Strength"] + entrance_rules["REGION_MAGMA_HIDEOUT_1F/MAIN -> REGION_MAGMA_HIDEOUT_1F/ENTRANCE"] = hm_rules["HM04 Strength"] # Aqua Hideout - if "Aqua Hideout Grunts" not in world.options.remove_roadblocks.value: - set_rule( - get_entrance("REGION_AQUA_HIDEOUT_1F/WATER -> REGION_AQUA_HIDEOUT_1F/MAIN"), - lambda state: state.has("EVENT_AQUA_STEALS_SUBMARINE", world.player) - ) - set_rule( - get_entrance("REGION_AQUA_HIDEOUT_1F/MAIN -> REGION_AQUA_HIDEOUT_1F/WATER"), - lambda state: hm_rules["HM03 Surf"](state) and state.has("EVENT_AQUA_STEALS_SUBMARINE", world.player) - ) - - # Route 124 - set_rule( - get_entrance("REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/BIG_AREA"), - hm_rules["HM08 Dive"] - ) - set_rule( - get_entrance("REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/SMALL_AREA_1"), - hm_rules["HM08 Dive"] - ) - set_rule( - get_entrance("REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/SMALL_AREA_2"), - hm_rules["HM08 Dive"] - ) - set_rule( - get_entrance("REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/SMALL_AREA_3"), - hm_rules["HM08 Dive"] + entrance_rules["REGION_AQUA_HIDEOUT_1F/WATER -> REGION_AQUA_HIDEOUT_1F/MAIN"] = ( + OptionFilter(RemoveRoadblocks, "Aqua Hideout Grunts", "contains") | Has("EVENT_AQUA_STEALS_SUBMARINE") ) - set_rule( - get_entrance("REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/TUNNEL_1"), - hm_rules["HM08 Dive"] - ) - set_rule( - get_entrance("REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/TUNNEL_2"), - hm_rules["HM08 Dive"] - ) - set_rule( - get_entrance("REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/TUNNEL_3"), - hm_rules["HM08 Dive"] - ) - set_rule( - get_entrance("REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/TUNNEL_4"), - hm_rules["HM08 Dive"] - ) - set_rule( - get_entrance("REGION_ROUTE124/NORTH_ENCLOSED_AREA_1 -> REGION_UNDERWATER_ROUTE124/TUNNEL_1"), - hm_rules["HM08 Dive"] - ) - set_rule( - get_entrance("REGION_ROUTE124/NORTH_ENCLOSED_AREA_2 -> REGION_UNDERWATER_ROUTE124/TUNNEL_1"), - hm_rules["HM08 Dive"] - ) - set_rule( - get_entrance("REGION_ROUTE124/NORTH_ENCLOSED_AREA_3 -> REGION_UNDERWATER_ROUTE124/TUNNEL_2"), - hm_rules["HM08 Dive"] - ) - set_rule( - get_entrance("REGION_ROUTE124/SOUTH_ENCLOSED_AREA_1 -> REGION_UNDERWATER_ROUTE124/TUNNEL_3"), - hm_rules["HM08 Dive"] - ) - set_rule( - get_entrance("REGION_ROUTE124/SOUTH_ENCLOSED_AREA_2 -> REGION_UNDERWATER_ROUTE124/TUNNEL_3"), - hm_rules["HM08 Dive"] - ) - set_rule( - get_entrance("REGION_ROUTE124/SOUTH_ENCLOSED_AREA_3 -> REGION_UNDERWATER_ROUTE124/TUNNEL_4"), - hm_rules["HM08 Dive"] + entrance_rules["REGION_AQUA_HIDEOUT_1F/MAIN -> REGION_AQUA_HIDEOUT_1F/WATER"] = ( + OptionFilter(RemoveRoadblocks, "Aqua Hideout Grunts", "contains") | + (hm_rules["HM03 Surf"] & Has("EVENT_AQUA_STEALS_SUBMARINE")) ) + # Route 124 + entrance_rules["REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/BIG_AREA"] = hm_rules["HM08 Dive"] + entrance_rules["REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/SMALL_AREA_1"] = hm_rules["HM08 Dive"] + entrance_rules["REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/SMALL_AREA_2"] = hm_rules["HM08 Dive"] + entrance_rules["REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/SMALL_AREA_3"] = hm_rules["HM08 Dive"] + entrance_rules["REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/TUNNEL_1"] = hm_rules["HM08 Dive"] + entrance_rules["REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/TUNNEL_2"] = hm_rules["HM08 Dive"] + entrance_rules["REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/TUNNEL_3"] = hm_rules["HM08 Dive"] + entrance_rules["REGION_ROUTE124/MAIN -> REGION_UNDERWATER_ROUTE124/TUNNEL_4"] = hm_rules["HM08 Dive"] + entrance_rules["REGION_ROUTE124/NORTH_ENCLOSED_AREA_1 -> REGION_UNDERWATER_ROUTE124/TUNNEL_1"] = hm_rules["HM08 Dive"] + entrance_rules["REGION_ROUTE124/NORTH_ENCLOSED_AREA_2 -> REGION_UNDERWATER_ROUTE124/TUNNEL_1"] = hm_rules["HM08 Dive"] + entrance_rules["REGION_ROUTE124/NORTH_ENCLOSED_AREA_3 -> REGION_UNDERWATER_ROUTE124/TUNNEL_2"] = hm_rules["HM08 Dive"] + entrance_rules["REGION_ROUTE124/SOUTH_ENCLOSED_AREA_1 -> REGION_UNDERWATER_ROUTE124/TUNNEL_3"] = hm_rules["HM08 Dive"] + entrance_rules["REGION_ROUTE124/SOUTH_ENCLOSED_AREA_2 -> REGION_UNDERWATER_ROUTE124/TUNNEL_3"] = hm_rules["HM08 Dive"] + entrance_rules["REGION_ROUTE124/SOUTH_ENCLOSED_AREA_3 -> REGION_UNDERWATER_ROUTE124/TUNNEL_4"] = hm_rules["HM08 Dive"] + # Mossdeep City - set_rule( - get_entrance("REGION_MOSSDEEP_CITY/MAIN -> REGION_MOSSDEEP_CITY/WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_MOSSDEEP_CITY/MAIN -> REGION_ROUTE124/MAIN"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_MOSSDEEP_CITY/MAIN -> REGION_ROUTE125/SEA"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_MOSSDEEP_CITY/MAIN -> REGION_ROUTE127/MAIN"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_location("EVENT_DEFEAT_MAXIE_AT_SPACE_STATION"), - lambda state: state.has("EVENT_DEFEAT_TATE_AND_LIZA", world.player) - ) - set_rule( - get_location("EVENT_STEVEN_GIVES_DIVE"), - lambda state: state.has("EVENT_DEFEAT_MAXIE_AT_SPACE_STATION", world.player) - ) - set_rule( - get_location("NPC_GIFT_RECEIVED_HM_DIVE"), - lambda state: state.has("EVENT_DEFEAT_MAXIE_AT_SPACE_STATION", world.player) - ) + location_rules["EVENT_DEFEAT_MAXIE_AT_SPACE_STATION"] = Has("EVENT_DEFEAT_TATE_AND_LIZA") + location_rules["EVENT_STEVEN_GIVES_DIVE"] = Has("EVENT_DEFEAT_MAXIE_AT_SPACE_STATION") + location_rules["NPC_GIFT_RECEIVED_HM_DIVE"] = Has("EVENT_DEFEAT_MAXIE_AT_SPACE_STATION") + + entrance_rules["REGION_MOSSDEEP_CITY/MAIN -> REGION_MOSSDEEP_CITY/WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_MOSSDEEP_CITY/MAIN -> REGION_ROUTE124/MAIN"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_MOSSDEEP_CITY/MAIN -> REGION_ROUTE125/SEA"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_MOSSDEEP_CITY/MAIN -> REGION_ROUTE127/MAIN"] = hm_rules["HM03 Surf"] # Route 125 - set_rule( - get_entrance("REGION_UNDERWATER_ROUTE125/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"), - lambda state: hm_rules["HM08 Dive"](state) - and state.has("EVENT_DEFEAT_CHAMPION", world.player) - and state.has("MARINE_CAVE_ROUTE_125_1", world.player) - and state.has("EVENT_DEFEAT_SHELLY", world.player) + entrance_rules["REGION_UNDERWATER_ROUTE125/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"] = ( + hm_rules["HM08 Dive"] & HasAll("EVENT_DEFEAT_CHAMPION", "MARINE_CAVE_ROUTE_125_1", "EVENT_DEFEAT_SHELLY") ) - set_rule( - get_entrance("REGION_UNDERWATER_ROUTE125/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"), - lambda state: hm_rules["HM08 Dive"](state) - and state.has("EVENT_DEFEAT_CHAMPION", world.player) - and state.has("MARINE_CAVE_ROUTE_125_2", world.player) - and state.has("EVENT_DEFEAT_SHELLY", world.player) + entrance_rules["REGION_UNDERWATER_ROUTE125/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"] = ( + hm_rules["HM08 Dive"] & HasAll("EVENT_DEFEAT_CHAMPION", "MARINE_CAVE_ROUTE_125_2", "EVENT_DEFEAT_SHELLY") ) # Shoal Cave - set_rule( - get_entrance("REGION_SHOAL_CAVE_ENTRANCE_ROOM/SOUTH -> REGION_SHOAL_CAVE_ENTRANCE_ROOM/HIGH_TIDE_WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_SHOAL_CAVE_ENTRANCE_ROOM/NORTH_WEST_CORNER -> REGION_SHOAL_CAVE_ENTRANCE_ROOM/HIGH_TIDE_WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_SHOAL_CAVE_ENTRANCE_ROOM/NORTH_EAST_CORNER -> REGION_SHOAL_CAVE_ENTRANCE_ROOM/HIGH_TIDE_WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_SHOAL_CAVE_INNER_ROOM/HIGH_TIDE_EAST_MIDDLE_GROUND -> REGION_SHOAL_CAVE_INNER_ROOM/SOUTH_EAST_WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_SHOAL_CAVE_INNER_ROOM/HIGH_TIDE_EAST_MIDDLE_GROUND -> REGION_SHOAL_CAVE_INNER_ROOM/EAST_WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_SHOAL_CAVE_INNER_ROOM/HIGH_TIDE_EAST_MIDDLE_GROUND -> REGION_SHOAL_CAVE_INNER_ROOM/NORTH_WEST_WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_SHOAL_CAVE_INNER_ROOM/SOUTH_WEST_CORNER -> REGION_SHOAL_CAVE_INNER_ROOM/NORTH_WEST_WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_SHOAL_CAVE_INNER_ROOM/RARE_CANDY_PLATFORM -> REGION_SHOAL_CAVE_INNER_ROOM/SOUTH_EAST_WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM/NORTH_WEST -> REGION_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM/EAST"), - hm_rules["HM04 Strength"] - ) - set_rule( - get_entrance("REGION_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM/EAST -> REGION_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM/NORTH_WEST"), - hm_rules["HM04 Strength"] - ) + entrance_rules["REGION_SHOAL_CAVE_ENTRANCE_ROOM/SOUTH -> REGION_SHOAL_CAVE_ENTRANCE_ROOM/HIGH_TIDE_WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_SHOAL_CAVE_ENTRANCE_ROOM/NORTH_WEST_CORNER -> REGION_SHOAL_CAVE_ENTRANCE_ROOM/HIGH_TIDE_WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_SHOAL_CAVE_ENTRANCE_ROOM/NORTH_EAST_CORNER -> REGION_SHOAL_CAVE_ENTRANCE_ROOM/HIGH_TIDE_WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_SHOAL_CAVE_INNER_ROOM/HIGH_TIDE_EAST_MIDDLE_GROUND -> REGION_SHOAL_CAVE_INNER_ROOM/SOUTH_EAST_WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_SHOAL_CAVE_INNER_ROOM/HIGH_TIDE_EAST_MIDDLE_GROUND -> REGION_SHOAL_CAVE_INNER_ROOM/EAST_WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_SHOAL_CAVE_INNER_ROOM/HIGH_TIDE_EAST_MIDDLE_GROUND -> REGION_SHOAL_CAVE_INNER_ROOM/NORTH_WEST_WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_SHOAL_CAVE_INNER_ROOM/SOUTH_WEST_CORNER -> REGION_SHOAL_CAVE_INNER_ROOM/NORTH_WEST_WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_SHOAL_CAVE_INNER_ROOM/RARE_CANDY_PLATFORM -> REGION_SHOAL_CAVE_INNER_ROOM/SOUTH_EAST_WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM/NORTH_WEST -> REGION_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM/EAST"] = hm_rules["HM04 Strength"] + entrance_rules["REGION_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM/EAST -> REGION_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM/NORTH_WEST"] = hm_rules["HM04 Strength"] # Route 126 - set_rule( - get_entrance("REGION_ROUTE126/MAIN -> REGION_UNDERWATER_ROUTE126/MAIN"), - hm_rules["HM08 Dive"] - ) - set_rule( - get_entrance("REGION_ROUTE126/MAIN -> REGION_UNDERWATER_ROUTE126/SMALL_AREA_2"), - hm_rules["HM08 Dive"] - ) - set_rule( - get_entrance("REGION_ROUTE126/NEAR_ROUTE_124 -> REGION_UNDERWATER_ROUTE126/TUNNEL"), - hm_rules["HM08 Dive"] - ) - set_rule( - get_entrance("REGION_ROUTE126/NORTH_WEST_CORNER -> REGION_UNDERWATER_ROUTE126/TUNNEL"), - hm_rules["HM08 Dive"] - ) - set_rule( - get_entrance("REGION_ROUTE126/WEST -> REGION_UNDERWATER_ROUTE126/MAIN"), - hm_rules["HM08 Dive"] - ) - set_rule( - get_entrance("REGION_ROUTE126/WEST -> REGION_UNDERWATER_ROUTE126/SMALL_AREA_1"), - hm_rules["HM08 Dive"] - ) + entrance_rules["REGION_ROUTE126/MAIN -> REGION_UNDERWATER_ROUTE126/MAIN"] = hm_rules["HM08 Dive"] + entrance_rules["REGION_ROUTE126/MAIN -> REGION_UNDERWATER_ROUTE126/SMALL_AREA_2"] = hm_rules["HM08 Dive"] + entrance_rules["REGION_ROUTE126/NEAR_ROUTE_124 -> REGION_UNDERWATER_ROUTE126/TUNNEL"] = hm_rules["HM08 Dive"] + entrance_rules["REGION_ROUTE126/NORTH_WEST_CORNER -> REGION_UNDERWATER_ROUTE126/TUNNEL"] = hm_rules["HM08 Dive"] + entrance_rules["REGION_ROUTE126/WEST -> REGION_UNDERWATER_ROUTE126/MAIN"] = hm_rules["HM08 Dive"] + entrance_rules["REGION_ROUTE126/WEST -> REGION_UNDERWATER_ROUTE126/SMALL_AREA_1"] = hm_rules["HM08 Dive"] # Sootopolis City - set_rule( - get_entrance("REGION_SOOTOPOLIS_CITY/WATER -> REGION_UNDERWATER_SOOTOPOLIS_CITY/MAIN"), - hm_rules["HM08 Dive"] - ) - set_rule( - get_entrance("REGION_SOOTOPOLIS_CITY/EAST -> REGION_SOOTOPOLIS_CITY/WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_SOOTOPOLIS_CITY/WEST -> REGION_SOOTOPOLIS_CITY/WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_SOOTOPOLIS_CITY/ISLAND -> REGION_SOOTOPOLIS_CITY/WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("MAP_SOOTOPOLIS_CITY:3/MAP_CAVE_OF_ORIGIN_ENTRANCE:0"), - lambda state: state.has("EVENT_RELEASE_KYOGRE", world.player) - ) - set_rule( - get_entrance("MAP_SOOTOPOLIS_CITY:2/MAP_SOOTOPOLIS_CITY_GYM_1F:0"), - lambda state: state.has("EVENT_RAYQUAZA_STOPS_FIGHT", world.player) - ) - set_rule( - get_location("NPC_GIFT_RECEIVED_HM_WATERFALL"), - lambda state: state.has("EVENT_RAYQUAZA_STOPS_FIGHT", world.player) - ) - set_rule( - get_location("EVENT_RAYQUAZA_STOPS_FIGHT"), - lambda state: state.has("EVENT_RELEASE_KYOGRE", world.player) - ) + location_rules["NPC_GIFT_RECEIVED_HM_WATERFALL"] = Has("EVENT_RAYQUAZA_STOPS_FIGHT") + location_rules["EVENT_RAYQUAZA_STOPS_FIGHT"] = Has("EVENT_RELEASE_KYOGRE") + + entrance_rules["REGION_SOOTOPOLIS_CITY/WATER -> REGION_UNDERWATER_SOOTOPOLIS_CITY/MAIN"] = hm_rules["HM08 Dive"] + entrance_rules["REGION_SOOTOPOLIS_CITY/EAST -> REGION_SOOTOPOLIS_CITY/WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_SOOTOPOLIS_CITY/WEST -> REGION_SOOTOPOLIS_CITY/WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_SOOTOPOLIS_CITY/ISLAND -> REGION_SOOTOPOLIS_CITY/WATER"] = hm_rules["HM03 Surf"] + entrance_rules["MAP_SOOTOPOLIS_CITY:3/MAP_CAVE_OF_ORIGIN_ENTRANCE:0"] = Has("EVENT_RELEASE_KYOGRE") + entrance_rules["MAP_SOOTOPOLIS_CITY:2/MAP_SOOTOPOLIS_CITY_GYM_1F:0"] = Has("EVENT_RAYQUAZA_STOPS_FIGHT") # Route 127 - set_rule( - get_entrance("REGION_ROUTE127/MAIN -> REGION_UNDERWATER_ROUTE127/MAIN"), - hm_rules["HM08 Dive"] - ) - set_rule( - get_entrance("REGION_ROUTE127/MAIN -> REGION_UNDERWATER_ROUTE127/TUNNEL"), - hm_rules["HM08 Dive"] - ) - set_rule( - get_entrance("REGION_ROUTE127/MAIN -> REGION_UNDERWATER_ROUTE127/AREA_1"), - hm_rules["HM08 Dive"] + entrance_rules["REGION_ROUTE127/MAIN -> REGION_UNDERWATER_ROUTE127/MAIN"] = hm_rules["HM08 Dive"] + entrance_rules["REGION_ROUTE127/MAIN -> REGION_UNDERWATER_ROUTE127/TUNNEL"] = hm_rules["HM08 Dive"] + entrance_rules["REGION_ROUTE127/MAIN -> REGION_UNDERWATER_ROUTE127/AREA_1"] = hm_rules["HM08 Dive"] + entrance_rules["REGION_ROUTE127/MAIN -> REGION_UNDERWATER_ROUTE127/AREA_2"] = hm_rules["HM08 Dive"] + entrance_rules["REGION_ROUTE127/MAIN -> REGION_UNDERWATER_ROUTE127/AREA_3"] = hm_rules["HM08 Dive"] + entrance_rules["REGION_ROUTE127/ENCLOSED_AREA -> REGION_UNDERWATER_ROUTE127/TUNNEL"] = hm_rules["HM08 Dive"] + entrance_rules["REGION_UNDERWATER_ROUTE127/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"] = ( + hm_rules["HM08 Dive"] & HasAll("EVENT_DEFEAT_CHAMPION", "MARINE_CAVE_ROUTE_127_1", "EVENT_DEFEAT_SHELLY") ) - set_rule( - get_entrance("REGION_ROUTE127/MAIN -> REGION_UNDERWATER_ROUTE127/AREA_2"), - hm_rules["HM08 Dive"] - ) - set_rule( - get_entrance("REGION_ROUTE127/MAIN -> REGION_UNDERWATER_ROUTE127/AREA_3"), - hm_rules["HM08 Dive"] - ) - set_rule( - get_entrance("REGION_ROUTE127/ENCLOSED_AREA -> REGION_UNDERWATER_ROUTE127/TUNNEL"), - hm_rules["HM08 Dive"] - ) - set_rule( - get_entrance("REGION_UNDERWATER_ROUTE127/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"), - lambda state: hm_rules["HM08 Dive"](state) - and state.has("EVENT_DEFEAT_CHAMPION", world.player) - and state.has("MARINE_CAVE_ROUTE_127_1", world.player) - and state.has("EVENT_DEFEAT_SHELLY", world.player) - ) - set_rule( - get_entrance("REGION_UNDERWATER_ROUTE127/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"), - lambda state: hm_rules["HM08 Dive"](state) - and state.has("EVENT_DEFEAT_CHAMPION", world.player) - and state.has("MARINE_CAVE_ROUTE_127_2", world.player) - and state.has("EVENT_DEFEAT_SHELLY", world.player) + entrance_rules["REGION_UNDERWATER_ROUTE127/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"] = ( + hm_rules["HM08 Dive"] & HasAll("EVENT_DEFEAT_CHAMPION", "MARINE_CAVE_ROUTE_127_2", "EVENT_DEFEAT_SHELLY") ) # Route 128 - set_rule( - get_entrance("REGION_ROUTE128/MAIN -> REGION_UNDERWATER_ROUTE128/MAIN"), - hm_rules["HM08 Dive"] - ) - set_rule( - get_entrance("REGION_ROUTE128/MAIN -> REGION_UNDERWATER_ROUTE128/AREA_1"), - hm_rules["HM08 Dive"] - ) - set_rule( - get_entrance("REGION_ROUTE128/MAIN -> REGION_UNDERWATER_ROUTE128/AREA_2"), - hm_rules["HM08 Dive"] - ) + entrance_rules["REGION_ROUTE128/MAIN -> REGION_UNDERWATER_ROUTE128/MAIN"] = hm_rules["HM08 Dive"] + entrance_rules["REGION_ROUTE128/MAIN -> REGION_UNDERWATER_ROUTE128/AREA_1"] = hm_rules["HM08 Dive"] + entrance_rules["REGION_ROUTE128/MAIN -> REGION_UNDERWATER_ROUTE128/AREA_2"] = hm_rules["HM08 Dive"] # Seafloor Cavern - set_rule( - get_entrance("REGION_SEAFLOOR_CAVERN_ENTRANCE/MAIN -> REGION_SEAFLOOR_CAVERN_ENTRANCE/WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_SEAFLOOR_CAVERN_ENTRANCE/WATER -> REGION_UNDERWATER_SEAFLOOR_CAVERN/MAIN"), - hm_rules["HM08 Dive"] - ) - set_rule( - get_entrance("REGION_SEAFLOOR_CAVERN_ROOM1/SOUTH -> REGION_SEAFLOOR_CAVERN_ROOM1/NORTH"), - lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state) - ) - set_rule( - get_entrance("REGION_SEAFLOOR_CAVERN_ROOM1/NORTH -> REGION_SEAFLOOR_CAVERN_ROOM1/SOUTH"), - hm_rules["HM04 Strength"] - ) - set_rule( - get_entrance("REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM2/NORTH_WEST"), - hm_rules["HM04 Strength"] - ) - set_rule( - get_entrance("REGION_SEAFLOOR_CAVERN_ROOM2/NORTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_WEST"), - hm_rules["HM04 Strength"] - ) - set_rule( - get_entrance("REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_EAST"), - hm_rules["HM06 Rock Smash"] - ) - set_rule( - get_entrance("REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_EAST -> REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_WEST"), - hm_rules["HM06 Rock Smash"] - ) - set_rule( - get_entrance("REGION_SEAFLOOR_CAVERN_ROOM2/NORTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM2/NORTH_EAST"), - lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state) - ) - set_rule( - get_entrance("REGION_SEAFLOOR_CAVERN_ROOM2/NORTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_EAST"), - lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state) - ) - set_rule( - get_entrance("REGION_SEAFLOOR_CAVERN_ROOM5/NORTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM5/EAST"), - lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state) - ) - set_rule( - get_entrance("REGION_SEAFLOOR_CAVERN_ROOM5/EAST -> REGION_SEAFLOOR_CAVERN_ROOM5/NORTH_WEST"), - hm_rules["HM04 Strength"] - ) - set_rule( - get_entrance("REGION_SEAFLOOR_CAVERN_ROOM5/NORTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM5/SOUTH_WEST"), - lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state) - ) - set_rule( - get_entrance("REGION_SEAFLOOR_CAVERN_ROOM5/SOUTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM5/NORTH_WEST"), - lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state) - ) - set_rule( - get_entrance("REGION_SEAFLOOR_CAVERN_ROOM6/NORTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM6/WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_SEAFLOOR_CAVERN_ROOM6/SOUTH -> REGION_SEAFLOOR_CAVERN_ROOM6/WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_SEAFLOOR_CAVERN_ROOM7/SOUTH -> REGION_SEAFLOOR_CAVERN_ROOM7/WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_SEAFLOOR_CAVERN_ROOM7/NORTH -> REGION_SEAFLOOR_CAVERN_ROOM7/WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_SEAFLOOR_CAVERN_ROOM8/NORTH -> REGION_SEAFLOOR_CAVERN_ROOM8/SOUTH"), - hm_rules["HM04 Strength"] - ) - set_rule( - get_entrance("REGION_SEAFLOOR_CAVERN_ROOM8/SOUTH -> REGION_SEAFLOOR_CAVERN_ROOM8/NORTH"), - hm_rules["HM04 Strength"] + entrance_rules["REGION_SEAFLOOR_CAVERN_ENTRANCE/MAIN -> REGION_SEAFLOOR_CAVERN_ENTRANCE/WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_SEAFLOOR_CAVERN_ENTRANCE/WATER -> REGION_UNDERWATER_SEAFLOOR_CAVERN/MAIN"] = hm_rules["HM08 Dive"] + entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM1/SOUTH -> REGION_SEAFLOOR_CAVERN_ROOM1/NORTH"] = hm_rules["HM06 Rock Smash"] & hm_rules["HM04 Strength"] + entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM1/NORTH -> REGION_SEAFLOOR_CAVERN_ROOM1/SOUTH"] = hm_rules["HM04 Strength"] + entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM2/NORTH_WEST"] = hm_rules["HM04 Strength"] + entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM2/NORTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_WEST"] = hm_rules["HM04 Strength"] + entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_EAST"] = hm_rules["HM06 Rock Smash"] + entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_EAST -> REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_WEST"] = hm_rules["HM06 Rock Smash"] + entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM2/NORTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM2/NORTH_EAST"] = hm_rules["HM06 Rock Smash"] & hm_rules["HM04 Strength"] + entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM2/NORTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM2/SOUTH_EAST"] = hm_rules["HM06 Rock Smash"] & hm_rules["HM04 Strength"] + entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM5/NORTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM5/EAST"] = hm_rules["HM06 Rock Smash"] & hm_rules["HM04 Strength"] + entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM5/EAST -> REGION_SEAFLOOR_CAVERN_ROOM5/NORTH_WEST"] = hm_rules["HM04 Strength"] + entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM5/NORTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM5/SOUTH_WEST"] = hm_rules["HM06 Rock Smash"] & hm_rules["HM04 Strength"] + entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM5/SOUTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM5/NORTH_WEST"] = hm_rules["HM06 Rock Smash"] & hm_rules["HM04 Strength"] + entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM6/NORTH_WEST -> REGION_SEAFLOOR_CAVERN_ROOM6/WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM6/SOUTH -> REGION_SEAFLOOR_CAVERN_ROOM6/WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM7/SOUTH -> REGION_SEAFLOOR_CAVERN_ROOM7/WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM7/NORTH -> REGION_SEAFLOOR_CAVERN_ROOM7/WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM8/NORTH -> REGION_SEAFLOOR_CAVERN_ROOM8/SOUTH"] = hm_rules["HM04 Strength"] + entrance_rules["REGION_SEAFLOOR_CAVERN_ROOM8/SOUTH -> REGION_SEAFLOOR_CAVERN_ROOM8/NORTH"] = hm_rules["HM04 Strength"] + entrance_rules["MAP_SEAFLOOR_CAVERN_ENTRANCE:1/MAP_SEAFLOOR_CAVERN_ROOM1:0"] = ( + OptionFilter(RemoveRoadblocks, "Seafloor Cavern Aqua Grunt", "contains") | Has("EVENT_STEVEN_GIVES_DIVE") ) - if "Seafloor Cavern Aqua Grunt" not in world.options.remove_roadblocks.value: - set_rule( - get_entrance("MAP_SEAFLOOR_CAVERN_ENTRANCE:1/MAP_SEAFLOOR_CAVERN_ROOM1:0"), - lambda state: state.has("EVENT_STEVEN_GIVES_DIVE", world.player) - ) # Route 129 - set_rule( - get_entrance("REGION_UNDERWATER_ROUTE129/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"), - lambda state: hm_rules["HM08 Dive"](state) - and state.has("EVENT_DEFEAT_CHAMPION", world.player) - and state.has("MARINE_CAVE_ROUTE_129_1", world.player) - and state.has("EVENT_DEFEAT_SHELLY", world.player) + entrance_rules["REGION_UNDERWATER_ROUTE129/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"] = ( + hm_rules["HM08 Dive"] & HasAll("EVENT_DEFEAT_CHAMPION", "MARINE_CAVE_ROUTE_129_1", "EVENT_DEFEAT_SHELLY") ) - set_rule( - get_entrance("REGION_UNDERWATER_ROUTE129/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"), - lambda state: hm_rules["HM08 Dive"](state) - and state.has("EVENT_DEFEAT_CHAMPION", world.player) - and state.has("MARINE_CAVE_ROUTE_129_2", world.player) - and state.has("EVENT_DEFEAT_SHELLY", world.player) + entrance_rules["REGION_UNDERWATER_ROUTE129/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"] = ( + hm_rules["HM08 Dive"] & HasAll("EVENT_DEFEAT_CHAMPION", "MARINE_CAVE_ROUTE_129_2", "EVENT_DEFEAT_SHELLY") ) # Pacifidlog Town - set_rule( - get_entrance("REGION_PACIFIDLOG_TOWN/MAIN -> REGION_PACIFIDLOG_TOWN/WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_PACIFIDLOG_TOWN/MAIN -> REGION_ROUTE131/MAIN"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_PACIFIDLOG_TOWN/MAIN -> REGION_ROUTE132/EAST"), - hm_rules["HM03 Surf"] - ) + entrance_rules["REGION_PACIFIDLOG_TOWN/MAIN -> REGION_PACIFIDLOG_TOWN/WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_PACIFIDLOG_TOWN/MAIN -> REGION_ROUTE131/MAIN"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_PACIFIDLOG_TOWN/MAIN -> REGION_ROUTE132/EAST"] = hm_rules["HM03 Surf"] # Sky Pillar - set_rule( - get_entrance("MAP_SKY_PILLAR_OUTSIDE:1/MAP_SKY_PILLAR_1F:0"), - lambda state: state.has("EVENT_RELEASE_KYOGRE", world.player) - ) - add_rule( - get_location("EVENT_ENCOUNTER_RAYQUAZA"), - lambda state: state.has("EVENT_RAYQUAZA_STOPS_FIGHT", world.player) - ) - set_rule( - get_entrance("REGION_SKY_PILLAR_2F/RIGHT -> REGION_SKY_PILLAR_2F/LEFT"), - has_mach_bike - ) - set_rule( - get_entrance("REGION_SKY_PILLAR_2F/LEFT -> REGION_SKY_PILLAR_2F/RIGHT"), - has_mach_bike - ) - set_rule( - get_entrance("REGION_SKY_PILLAR_4F/MAIN -> REGION_SKY_PILLAR_4F/ABOVE_3F_TOP_CENTER"), - has_mach_bike - ) + location_rules["EVENT_ENCOUNTER_RAYQUAZA"] = Has("EVENT_RAYQUAZA_STOPS_FIGHT") + + entrance_rules["MAP_SKY_PILLAR_OUTSIDE:1/MAP_SKY_PILLAR_1F:0"] = Has("EVENT_RELEASE_KYOGRE") + entrance_rules["REGION_SKY_PILLAR_2F/RIGHT -> REGION_SKY_PILLAR_2F/LEFT"] = Has("Mach Bike") + entrance_rules["REGION_SKY_PILLAR_2F/LEFT -> REGION_SKY_PILLAR_2F/RIGHT"] = Has("Mach Bike") + entrance_rules["REGION_SKY_PILLAR_4F/MAIN -> REGION_SKY_PILLAR_4F/ABOVE_3F_TOP_CENTER"] = Has("Mach Bike") # Route 134 - set_rule( - get_entrance("REGION_ROUTE134/MAIN -> REGION_UNDERWATER_ROUTE134/MAIN"), - hm_rules["HM08 Dive"] - ) - set_rule( - get_location("EVENT_UNDO_REGI_SEAL"), - lambda state: state.has("CATCH_SPECIES_WAILORD", world.player) and state.has("CATCH_SPECIES_RELICANTH", world.player) - ) - set_rule( - get_entrance("REGION_SEALED_CHAMBER_OUTER_ROOM/MAIN -> REGION_SEALED_CHAMBER_OUTER_ROOM/CRUMBLED_WALL"), - lambda state: state.has("EVENT_MOVE_TUTOR_DIG", world.player) - ) + location_rules["EVENT_UNDO_REGI_SEAL"] = HasAll("CATCH_SPECIES_WAILORD", "CATCH_SPECIES_RELICANTH") + + entrance_rules["REGION_ROUTE134/MAIN -> REGION_UNDERWATER_ROUTE134/MAIN"] = hm_rules["HM08 Dive"] + entrance_rules["REGION_SEALED_CHAMBER_OUTER_ROOM/MAIN -> REGION_SEALED_CHAMBER_OUTER_ROOM/CRUMBLED_WALL"] = Has("EVENT_MOVE_TUTOR_DIG") # Ever Grande City - set_rule( - get_entrance("REGION_EVER_GRANDE_CITY/SEA -> REGION_EVER_GRANDE_CITY/SOUTH"), - hm_rules["HM07 Waterfall"] - ) - set_rule( - get_entrance("REGION_EVER_GRANDE_CITY/SOUTH -> REGION_EVER_GRANDE_CITY/SEA"), - hm_rules["HM03 Surf"] - ) + entrance_rules["REGION_EVER_GRANDE_CITY/SEA -> REGION_EVER_GRANDE_CITY/SOUTH"] = hm_rules["HM07 Waterfall"] + entrance_rules["REGION_EVER_GRANDE_CITY/SOUTH -> REGION_EVER_GRANDE_CITY/SEA"] = hm_rules["HM03 Surf"] # Victory Road - set_rule( - get_entrance("REGION_VICTORY_ROAD_B1F/SOUTH_WEST_MAIN -> REGION_VICTORY_ROAD_B1F/SOUTH_WEST_LADDER_UP"), - lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state) - ) - set_rule( - get_entrance("REGION_VICTORY_ROAD_B1F/SOUTH_WEST_LADDER_UP -> REGION_VICTORY_ROAD_B1F/SOUTH_WEST_MAIN"), - lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state) - ) - set_rule( - get_entrance("REGION_VICTORY_ROAD_B1F/MAIN_UPPER -> REGION_VICTORY_ROAD_B1F/MAIN_LOWER_EAST"), - lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state) - ) - set_rule( - get_entrance("REGION_VICTORY_ROAD_B1F/MAIN_LOWER_EAST -> REGION_VICTORY_ROAD_B1F/MAIN_LOWER_WEST"), - hm_rules["HM06 Rock Smash"] - ) - set_rule( - get_entrance("REGION_VICTORY_ROAD_B1F/MAIN_LOWER_WEST -> REGION_VICTORY_ROAD_B1F/MAIN_LOWER_EAST"), - lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state) - ) - set_rule( - get_entrance("REGION_VICTORY_ROAD_B1F/MAIN_LOWER_WEST -> REGION_VICTORY_ROAD_B1F/MAIN_UPPER"), - lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state) - ) - set_rule( - get_entrance("REGION_VICTORY_ROAD_B2F/LOWER_WEST -> REGION_VICTORY_ROAD_B2F/LOWER_WEST_WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_VICTORY_ROAD_B2F/LOWER_WEST_ISLAND -> REGION_VICTORY_ROAD_B2F/LOWER_WEST_WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_VICTORY_ROAD_B2F/LOWER_EAST -> REGION_VICTORY_ROAD_B2F/LOWER_EAST_WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_VICTORY_ROAD_B2F/LOWER_WEST_WATER -> REGION_VICTORY_ROAD_B2F/UPPER_WATER"), - hm_rules["HM07 Waterfall"] - ) - set_rule( - get_entrance("REGION_VICTORY_ROAD_B2F/LOWER_EAST_WATER -> REGION_VICTORY_ROAD_B2F/UPPER_WATER"), - hm_rules["HM07 Waterfall"] - ) - set_rule( - get_entrance("REGION_VICTORY_ROAD_B2F/UPPER -> REGION_VICTORY_ROAD_B2F/UPPER_WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_VICTORY_ROAD_B2F/UPPER -> REGION_VICTORY_ROAD_B2F/LOWER_EAST_WATER"), - hm_rules["HM03 Surf"] - ) + entrance_rules["REGION_VICTORY_ROAD_B1F/SOUTH_WEST_MAIN -> REGION_VICTORY_ROAD_B1F/SOUTH_WEST_LADDER_UP"] = hm_rules["HM06 Rock Smash"] & hm_rules["HM04 Strength"] + entrance_rules["REGION_VICTORY_ROAD_B1F/SOUTH_WEST_LADDER_UP -> REGION_VICTORY_ROAD_B1F/SOUTH_WEST_MAIN"] = hm_rules["HM06 Rock Smash"] & hm_rules["HM04 Strength"] + entrance_rules["REGION_VICTORY_ROAD_B1F/MAIN_UPPER -> REGION_VICTORY_ROAD_B1F/MAIN_LOWER_EAST"] = hm_rules["HM06 Rock Smash"] & hm_rules["HM04 Strength"] + entrance_rules["REGION_VICTORY_ROAD_B1F/MAIN_LOWER_EAST -> REGION_VICTORY_ROAD_B1F/MAIN_LOWER_WEST"] = hm_rules["HM06 Rock Smash"] + entrance_rules["REGION_VICTORY_ROAD_B1F/MAIN_LOWER_WEST -> REGION_VICTORY_ROAD_B1F/MAIN_LOWER_EAST"] = hm_rules["HM06 Rock Smash"] & hm_rules["HM04 Strength"] + entrance_rules["REGION_VICTORY_ROAD_B1F/MAIN_LOWER_WEST -> REGION_VICTORY_ROAD_B1F/MAIN_UPPER"] = hm_rules["HM06 Rock Smash"] & hm_rules["HM04 Strength"] + entrance_rules["REGION_VICTORY_ROAD_B2F/LOWER_WEST -> REGION_VICTORY_ROAD_B2F/LOWER_WEST_WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_VICTORY_ROAD_B2F/LOWER_WEST_ISLAND -> REGION_VICTORY_ROAD_B2F/LOWER_WEST_WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_VICTORY_ROAD_B2F/LOWER_EAST -> REGION_VICTORY_ROAD_B2F/LOWER_EAST_WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_VICTORY_ROAD_B2F/LOWER_WEST_WATER -> REGION_VICTORY_ROAD_B2F/UPPER_WATER"] = hm_rules["HM07 Waterfall"] + entrance_rules["REGION_VICTORY_ROAD_B2F/LOWER_EAST_WATER -> REGION_VICTORY_ROAD_B2F/UPPER_WATER"] = hm_rules["HM07 Waterfall"] + entrance_rules["REGION_VICTORY_ROAD_B2F/UPPER -> REGION_VICTORY_ROAD_B2F/UPPER_WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_VICTORY_ROAD_B2F/UPPER -> REGION_VICTORY_ROAD_B2F/LOWER_EAST_WATER"] = hm_rules["HM03 Surf"] # Pokemon League - if world.options.elite_four_requirement == EliteFourRequirement.option_badges: - set_rule( - get_entrance("REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F/MAIN -> REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F/BEHIND_BADGE_CHECKERS"), - lambda state: state.has_group_unique("Badge", world.player, world.options.elite_four_count.value) - ) - else: - set_rule( - get_entrance("REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F/MAIN -> REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F/BEHIND_BADGE_CHECKERS"), - lambda state: defeated_n_gym_leaders(state, world.options.elite_four_count.value) + entrance_rules["REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F/MAIN -> REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F/BEHIND_BADGE_CHECKERS"] = ( + ( + OptionFilter(EliteFourRequirement, EliteFourRequirement.option_badges) & + HasGroupUnique("Badge", world.options.elite_four_count.value) + ) | + ( + OptionFilter(EliteFourRequirement, EliteFourRequirement.option_gyms) & + create_defeated_n_gym_leaders_rule(world.options.elite_four_count.value) ) + ) # Battle Frontier - set_rule( - get_entrance("REGION_BATTLE_FRONTIER_OUTSIDE_WEST/DOCK -> REGION_SS_TIDAL_CORRIDOR/MAIN"), - lambda state: state.has("S.S. Ticket", world.player) - ) - set_rule( - get_entrance("REGION_BATTLE_FRONTIER_OUTSIDE_WEST/CAVE_ENTRANCE -> REGION_BATTLE_FRONTIER_OUTSIDE_WEST/WATER"), - hm_rules["HM03 Surf"] - ) - set_rule( - get_entrance("REGION_BATTLE_FRONTIER_OUTSIDE_EAST/MAIN -> REGION_BATTLE_FRONTIER_OUTSIDE_EAST/ABOVE_WATERFALL"), - lambda state: state.has("Wailmer Pail", world.player) and hm_rules["HM03 Surf"](state) - ) - set_rule( - get_entrance("REGION_BATTLE_FRONTIER_OUTSIDE_EAST/ABOVE_WATERFALL -> REGION_BATTLE_FRONTIER_OUTSIDE_EAST/MAIN"), - lambda state: state.has("Wailmer Pail", world.player) - ) - set_rule( - get_entrance("REGION_BATTLE_FRONTIER_OUTSIDE_EAST/WATER -> REGION_BATTLE_FRONTIER_OUTSIDE_EAST/ABOVE_WATERFALL"), - hm_rules["HM07 Waterfall"] - ) + entrance_rules["REGION_BATTLE_FRONTIER_OUTSIDE_WEST/DOCK -> REGION_SS_TIDAL_CORRIDOR/MAIN"] = Has("S.S. Ticket") + entrance_rules["REGION_BATTLE_FRONTIER_OUTSIDE_WEST/CAVE_ENTRANCE -> REGION_BATTLE_FRONTIER_OUTSIDE_WEST/WATER"] = hm_rules["HM03 Surf"] + entrance_rules["REGION_BATTLE_FRONTIER_OUTSIDE_EAST/MAIN -> REGION_BATTLE_FRONTIER_OUTSIDE_EAST/ABOVE_WATERFALL"] = Has("Wailmer Pail") & hm_rules["HM03 Surf"] + entrance_rules["REGION_BATTLE_FRONTIER_OUTSIDE_EAST/ABOVE_WATERFALL -> REGION_BATTLE_FRONTIER_OUTSIDE_EAST/MAIN"] = Has("Wailmer Pail") + entrance_rules["REGION_BATTLE_FRONTIER_OUTSIDE_EAST/WATER -> REGION_BATTLE_FRONTIER_OUTSIDE_EAST/ABOVE_WATERFALL"] = hm_rules["HM07 Waterfall"] # Pokedex Rewards if world.options.dexsanity: @@ -1551,183 +637,100 @@ def get_location(location: str): if species.species_id in world.blacklisted_wilds or species.species_id not in world.allowed_dexsanity_species: continue - set_rule( - get_location(f"Pokedex - {species.label}"), - lambda state, species_name=species.name: state.has(f"CATCH_{species_name}", world.player) - ) + location_rules[f"Pokedex - {species.label}"] = Has(f"CATCH_{species.name}") # 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"] in world.allowed_dexsanity_species: - set_rule( - get_location(f"Pokedex - Latios"), - lambda state: state.has("EVENT_ENCOUNTER_LATIOS", world.player) - ) + location_rules[f"Pokedex - Latios"] = Has("EVENT_ENCOUNTER_LATIOS") # Overworld Items if world.options.overworld_items: # Route 117 - set_rule( - get_location("ITEM_ROUTE_117_REVIVE"), - hm_rules["HM01 Cut"] - ) + location_rules["ITEM_ROUTE_117_REVIVE"] = hm_rules["HM01 Cut"] # Route 114 - set_rule( - get_location("ITEM_ROUTE_114_PROTEIN"), - hm_rules["HM06 Rock Smash"] - ) + location_rules["ITEM_ROUTE_114_PROTEIN"] = hm_rules["HM06 Rock Smash"] # Victory Road - set_rule( - get_location("ITEM_VICTORY_ROAD_B1F_FULL_RESTORE"), - lambda state: hm_rules["HM06 Rock Smash"](state) and hm_rules["HM04 Strength"](state) - ) + location_rules["ITEM_VICTORY_ROAD_B1F_FULL_RESTORE"] = hm_rules["HM06 Rock Smash"] & hm_rules["HM04 Strength"] # Hidden Items if world.options.hidden_items: # Route 120 - set_rule( - get_location("HIDDEN_ITEM_ROUTE_120_RARE_CANDY_1"), - hm_rules["HM01 Cut"] - ) + location_rules["HIDDEN_ITEM_ROUTE_120_RARE_CANDY_1"] = hm_rules["HM01 Cut"] # Route 121 - set_rule( - get_location("HIDDEN_ITEM_ROUTE_121_NUGGET"), - hm_rules["HM01 Cut"] - ) + location_rules["HIDDEN_ITEM_ROUTE_121_NUGGET"] = hm_rules["HM01 Cut"] # NPC Gifts if world.options.npc_gifts: # Littleroot Town - set_rule( - get_location("NPC_GIFT_RECEIVED_AMULET_COIN"), - lambda state: state.has("EVENT_TALK_TO_MR_STONE", world.player) and state.has("Balance Badge", world.player) - ) + location_rules["NPC_GIFT_RECEIVED_AMULET_COIN"] = HasAll("EVENT_TALK_TO_MR_STONE", "Balance Badge") # Route 104 - set_rule( - get_location("NPC_GIFT_RECEIVED_WHITE_HERB"), - lambda state: state.has("Dynamo Badge", world.player) and state.has("EVENT_MEET_FLOWER_SHOP_OWNER", world.player) - ) + location_rules["NPC_GIFT_RECEIVED_WHITE_HERB"] = HasAll("Dynamo Badge", "EVENT_MEET_FLOWER_SHOP_OWNER") # Devon Corp - set_rule( - get_location("NPC_GIFT_RECEIVED_EXP_SHARE"), - lambda state: state.has("EVENT_DELIVER_LETTER", world.player) - ) + location_rules["NPC_GIFT_RECEIVED_EXP_SHARE"] = Has("EVENT_DELIVER_LETTER") # Route 116 - set_rule( - get_location("NPC_GIFT_RECEIVED_REPEAT_BALL"), - lambda state: state.has("EVENT_RESCUE_CAPT_STERN", world.player) - ) + location_rules["NPC_GIFT_RECEIVED_REPEAT_BALL"] = Has("EVENT_RESCUE_CAPT_STERN") # Dewford Town - set_rule( - get_location("NPC_GIFT_RECEIVED_TM_SLUDGE_BOMB"), - lambda state: state.has("EVENT_DEFEAT_NORMAN", world.player) - ) + location_rules["NPC_GIFT_RECEIVED_TM_SLUDGE_BOMB"] = Has("EVENT_DEFEAT_NORMAN") # Slateport City - set_rule( - get_location("NPC_GIFT_RECEIVED_DEEP_SEA_TOOTH"), - lambda state: state.has("EVENT_AQUA_STEALS_SUBMARINE", world.player) - and state.has("Scanner", world.player) - and state.has("Mind Badge", world.player) + location_rules["NPC_GIFT_RECEIVED_DEEP_SEA_TOOTH"] = ( + HasAll("EVENT_AQUA_STEALS_SUBMARINE", "Scanner", "Mind Badge") ) - set_rule( - get_location("NPC_GIFT_RECEIVED_DEEP_SEA_SCALE"), - lambda state: state.has("EVENT_AQUA_STEALS_SUBMARINE", world.player) - and state.has("Scanner", world.player) - and state.has("Mind Badge", world.player) + location_rules["NPC_GIFT_RECEIVED_DEEP_SEA_SCALE"] = ( + HasAll("EVENT_AQUA_STEALS_SUBMARINE", "Scanner", "Mind Badge") ) # Mauville City - set_rule( - get_location("NPC_GIFT_GOT_TM_THUNDERBOLT_FROM_WATTSON"), - lambda state: state.has("EVENT_DEFEAT_NORMAN", world.player) and state.has("EVENT_TURN_OFF_GENERATOR", world.player) + location_rules["NPC_GIFT_GOT_TM_THUNDERBOLT_FROM_WATTSON"] = ( + HasAll("EVENT_DEFEAT_NORMAN", "EVENT_TURN_OFF_GENERATOR") ) # Fallarbor Town - set_rule( - get_location("NPC_GIFT_RECEIVED_TM_RETURN"), - lambda state: state.has("EVENT_RECOVER_METEORITE", world.player) and state.has("Meteorite", world.player) - ) + location_rules["NPC_GIFT_RECEIVED_TM_RETURN"] = HasAll("EVENT_RECOVER_METEORITE", "Meteorite") # Fortree City - set_rule( - get_location("NPC_GIFT_RECEIVED_MENTAL_HERB"), - lambda state: state.has("EVENT_WINGULL_QUEST_2", world.player) - ) + location_rules["NPC_GIFT_RECEIVED_MENTAL_HERB"] = Has("EVENT_WINGULL_QUEST_2") # Add Itemfinder requirement to hidden items if world.options.require_itemfinder: for location in world.multiworld.get_locations(world.player): assert isinstance(location, PokemonEmeraldLocation) if location.key is not None and data.locations[location.key].category == LocationCategory.HIDDEN_ITEM: - add_rule( - location, - lambda state: state.has("Itemfinder", world.player) - ) + location_rules[location.key] &= Has("Itemfinder") # Add Flash requirements to dark caves # Granite Cave if world.options.require_flash in [DarkCavesRequireFlash.option_only_granite_cave, DarkCavesRequireFlash.option_both]: - add_rule( - get_entrance("MAP_GRANITE_CAVE_1F:2/MAP_GRANITE_CAVE_B1F:1"), - hm_rules["HM05 Flash"] - ) - add_rule( - get_entrance("MAP_GRANITE_CAVE_B1F:3/MAP_GRANITE_CAVE_B2F:1"), - hm_rules["HM05 Flash"] - ) + entrance_rules["MAP_GRANITE_CAVE_1F:2/MAP_GRANITE_CAVE_B1F:1"] &= hm_rules["HM05 Flash"] + entrance_rules["MAP_GRANITE_CAVE_B1F:3/MAP_GRANITE_CAVE_B2F:1"] &= hm_rules["HM05 Flash"] # Victory Road if world.options.require_flash in [DarkCavesRequireFlash.option_only_victory_road, DarkCavesRequireFlash.option_both]: - add_rule( - get_entrance("MAP_VICTORY_ROAD_1F:2/MAP_VICTORY_ROAD_B1F:5"), - hm_rules["HM05 Flash"] - ) - add_rule( - get_entrance("MAP_VICTORY_ROAD_1F:4/MAP_VICTORY_ROAD_B1F:4"), - hm_rules["HM05 Flash"] - ) - add_rule( - get_entrance("MAP_VICTORY_ROAD_1F:3/MAP_VICTORY_ROAD_B1F:2"), - hm_rules["HM05 Flash"] - ) - add_rule( - get_entrance("MAP_VICTORY_ROAD_B1F:3/MAP_VICTORY_ROAD_B2F:1"), - hm_rules["HM05 Flash"] - ) - add_rule( - get_entrance("MAP_VICTORY_ROAD_B1F:1/MAP_VICTORY_ROAD_B2F:2"), - hm_rules["HM05 Flash"] - ) - add_rule( - get_entrance("MAP_VICTORY_ROAD_B1F:6/MAP_VICTORY_ROAD_B2F:3"), - hm_rules["HM05 Flash"] - ) - add_rule( - get_entrance("MAP_VICTORY_ROAD_B1F:0/MAP_VICTORY_ROAD_B2F:0"), - hm_rules["HM05 Flash"] - ) - add_rule( - get_entrance("MAP_VICTORY_ROAD_B2F:3/MAP_VICTORY_ROAD_B1F:6"), - hm_rules["HM05 Flash"] - ) - add_rule( - get_entrance("MAP_VICTORY_ROAD_B2F:2/MAP_VICTORY_ROAD_B1F:1"), - hm_rules["HM05 Flash"] - ) - add_rule( - get_entrance("MAP_VICTORY_ROAD_B2F:0/MAP_VICTORY_ROAD_B1F:0"), - hm_rules["HM05 Flash"] - ) - add_rule( - get_entrance("MAP_VICTORY_ROAD_B2F:1/MAP_VICTORY_ROAD_B1F:3"), - hm_rules["HM05 Flash"] - ) + entrance_rules["MAP_VICTORY_ROAD_1F:2/MAP_VICTORY_ROAD_B1F:5"] &= hm_rules["HM05 Flash"] + entrance_rules["MAP_VICTORY_ROAD_1F:4/MAP_VICTORY_ROAD_B1F:4"] &= hm_rules["HM05 Flash"] + entrance_rules["MAP_VICTORY_ROAD_1F:3/MAP_VICTORY_ROAD_B1F:2"] &= hm_rules["HM05 Flash"] + entrance_rules["MAP_VICTORY_ROAD_B1F:3/MAP_VICTORY_ROAD_B2F:1"] &= hm_rules["HM05 Flash"] + entrance_rules["MAP_VICTORY_ROAD_B1F:1/MAP_VICTORY_ROAD_B2F:2"] &= hm_rules["HM05 Flash"] + entrance_rules["MAP_VICTORY_ROAD_B1F:6/MAP_VICTORY_ROAD_B2F:3"] &= hm_rules["HM05 Flash"] + entrance_rules["MAP_VICTORY_ROAD_B1F:0/MAP_VICTORY_ROAD_B2F:0"] &= hm_rules["HM05 Flash"] + entrance_rules["MAP_VICTORY_ROAD_B2F:3/MAP_VICTORY_ROAD_B1F:6"] &= hm_rules["HM05 Flash"] + entrance_rules["MAP_VICTORY_ROAD_B2F:2/MAP_VICTORY_ROAD_B1F:1"] &= hm_rules["HM05 Flash"] + entrance_rules["MAP_VICTORY_ROAD_B2F:0/MAP_VICTORY_ROAD_B1F:0"] &= hm_rules["HM05 Flash"] + entrance_rules["MAP_VICTORY_ROAD_B2F:1/MAP_VICTORY_ROAD_B1F:3"] &= hm_rules["HM05 Flash"] + + for name, rule in entrance_rules.items(): + world.set_rule(world.get_entrance(name), rule) + + for name, rule in location_rules.items(): + if name in data.locations: + name = data.locations[name].label + world.set_rule(world.get_location(name), rule) diff --git a/worlds/pokemon_emerald/util.py b/worlds/pokemon_emerald/util.py index 3215113075e9..268ba9bf5fac 100644 --- a/worlds/pokemon_emerald/util.py +++ b/worlds/pokemon_emerald/util.py @@ -100,7 +100,7 @@ def get_encounter_type_label(encounter_type: EncounterType, slot: int) -> str: 8: "Super Rod", 9: "Super Rod", }[slot] - + return { EncounterType.LAND: 'Land', EncounterType.WATER: 'Water', From 7a5acfeceb99c66a6e2f6933ef80f2ca42f0448b Mon Sep 17 00:00:00 2001 From: Duck <31627079+duckboycool@users.noreply.github.com> Date: Sat, 9 May 2026 08:59:05 -0600 Subject: [PATCH 46/66] Core: Make manifest accessible as a world attribute (#6122) --- worlds/AutoWorld.py | 2 ++ worlds/__init__.py | 12 +++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index ddc8ff2d2a4d..468706a34741 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -353,6 +353,8 @@ class World(metaclass=AutoWorldRegister): """path it was loaded from""" world_version: ClassVar[Version] = Version(0, 0, 0) """Optional world version loaded from archipelago.json""" + manifest: ClassVar[dict[str, Any]] = {} + """Mapping of the world's archipelago.json manifest. Use game and world_version attrs instead for those values.""" def __init__(self, multiworld: "MultiWorld", player: int): assert multiworld is not None diff --git a/worlds/__init__.py b/worlds/__init__.py index 190710068844..8684b7424957 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -11,7 +11,7 @@ from pathlib import Path from types import ModuleType from typing import List, Sequence -from zipfile import BadZipFile +from zipfile import ZipFile, BadZipFile from NetUtils import DataPackage from Utils import local_path, user_path, Version, version_tuple, tuplize_version, messagebox @@ -119,6 +119,7 @@ def load(self) -> bool: game = manifest.get("game") if game in AutoWorldRegister.world_types: AutoWorldRegister.world_types[game].world_version = tuplize_version(manifest.get("world_version", "0.0.0")) + AutoWorldRegister.world_types[game].manifest = manifest if apworlds: # encapsulation for namespace / gc purposes @@ -200,6 +201,15 @@ def find_spec( # world could fail to load at this point if apworld.world_version: AutoWorldRegister.world_types[apworld.game].world_version = apworld.world_version + + assert apworld.path + with ZipFile(apworld.path, "r") as zf: + manifest = apworld.read_contents(zf) + # version/compatible_version shouldn't be needed by world, makes it consistent with folder world + manifest.pop("version", None) + manifest.pop("compatible_version", None) + AutoWorldRegister.world_types[apworld.game].manifest = manifest + load_apworlds() del load_apworlds From 3fcd337f65fb6950b2017e58174c67051079b6d0 Mon Sep 17 00:00:00 2001 From: Duck <31627079+duckboycool@users.noreply.github.com> Date: Sat, 9 May 2026 09:00:00 -0600 Subject: [PATCH 47/66] WebHost: Add authors to supported games page (#6123) --- WebHostLib/templates/supportedGames.html | 3 +++ docs/apworld specification.md | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/WebHostLib/templates/supportedGames.html b/WebHostLib/templates/supportedGames.html index 43028721b254..128278c4b277 100644 --- a/WebHostLib/templates/supportedGames.html +++ b/WebHostLib/templates/supportedGames.html @@ -68,6 +68,9 @@

Currently Supported Games

Report a Bug {% endif %} + {% if "authors" in world.manifest %} +

Authors: {{ world.manifest["authors"] | join(", ") }}

+ {% endif %} {% endfor %} {% endblock %} diff --git a/docs/apworld specification.md b/docs/apworld specification.md index 7e13d6ccd34d..64d273b03871 100644 --- a/docs/apworld specification.md +++ b/docs/apworld specification.md @@ -35,8 +35,8 @@ There are also the following optional fields: * `world_version` - an arbitrary version for that world in order to only load the newest valid world. An APWorld without a world_version is always treated as older than one with a version (**Must** use exactly the format `"major.minor.build"`, e.g. `1.0.0`) -* `authors` - a list of authors, to eventually be displayed in various user-facing places such as WebHost and - package managers. Should always be a list of strings. +* `authors` - a list of authors of the world. Displayed in user-facing places like the Supported Games page + on WebHost. Should always be a list of strings. If the APWorld is packaged as an `.apworld` zip file, it also needs to have `version` and `compatible_version`, which refer to the version of the APContainer packaging scheme defined in [Files.py](../worlds/Files.py). From 55a1b12cb78926e9308114c347996c7a5c763252 Mon Sep 17 00:00:00 2001 From: Duck <31627079+duckboycool@users.noreply.github.com> Date: Sat, 9 May 2026 09:01:22 -0600 Subject: [PATCH 48/66] CommonClient: Make hint cost (shown in server label) update (#6149) --- CommonClient.py | 6 +++++- kvui.py | 19 ++++++++++--------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 3f98a4eff1d0..2da41f836078 100755 --- a/CommonClient.py +++ b/CommonClient.py @@ -1069,7 +1069,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict): if "players" in args: ctx.consume_players_package(args["players"]) if "hint_points" in args: - ctx.hint_points = args['hint_points'] + ctx.hint_points = args["hint_points"] if "checked_locations" in args: checked = set(args["checked_locations"]) ctx.checked_locations |= checked @@ -1077,6 +1077,10 @@ async def process_server_cmd(ctx: CommonContext, args: dict): if "permissions" in args: ctx.update_permissions(args["permissions"]) + # Update hint info for local display + if "hint_cost" in args: + ctx.hint_cost = int(args["hint_cost"]) + elif cmd == 'Print': ctx.on_print(args) diff --git a/kvui.py b/kvui.py index 14d8d1852878..8fcf9a75426a 100644 --- a/kvui.py +++ b/kvui.py @@ -363,15 +363,16 @@ def get_text(self): text += "\nPermissions:" for permission_name, permission_data in ctx.permissions.items(): text += f"\n {permission_name}: {permission_data}" - if ctx.hint_cost is not None and ctx.total_locations: - min_cost = int(ctx.server_version >= (0, 3, 9)) - text += f"\nA new !hint costs {ctx.hint_cost}% of checks made. " \ - f"For you this means every " \ - f"{max(min_cost, int(ctx.hint_cost * 0.01 * ctx.total_locations))} " \ - "location checks." \ - f"\nYou currently have {ctx.hint_points} points." - elif ctx.hint_cost == 0: - text += "\n!hint is free to use." + if ctx.total_locations and ctx.hint_cost is not None: + if ctx.hint_cost == 0: + text += "\n!hint is free to use." + else: + min_cost = int(ctx.server_version >= (0, 3, 9)) + text += f"\nA new !hint costs {ctx.hint_cost}% of checks made. " \ + f"For you this means every " \ + f"{max(min_cost, int(ctx.hint_cost * 0.01 * ctx.total_locations))} " \ + "location checks." \ + f"\nYou currently have {ctx.hint_points} points." if ctx.stored_data and "_read_race_mode" in ctx.stored_data: text += "\nRace mode is enabled." \ if ctx.stored_data["_read_race_mode"] else "\nRace mode is disabled." From 0cd81ff5001547647a8daefe334c9d695f9b62c8 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Sat, 9 May 2026 11:07:46 -0400 Subject: [PATCH 49/66] A Link to the Past: We Have Enemizer at Home (#6153) --- .dockerignore | 1 - Dockerfile | 27 - docs/deploy using containers.md | 9 - docs/running from source.md | 10 - inno_setup.iss | 4 +- settings.py | 5 - setup.py | 5 +- worlds/alttp/EnemizerPatches.py | 478 +++++ worlds/alttp/EnemyShuffle.py | 1707 +++++++++++++++++ worlds/alttp/ItemPool.py | 3 + worlds/alttp/PotShuffle.py | 125 ++ worlds/alttp/Rom.py | 282 +-- worlds/alttp/__init__.py | 37 +- worlds/alttp/enemizer_data/README.md | 27 + worlds/alttp/enemizer_data/__init__.py | 1 + worlds/alttp/enemizer_data/base_patch_data.py | 71 + .../enemizer_data/dungeon_sprite_addresses.py | 202 ++ .../enemizer_data/enemy_room_metadata.py | 106 + .../enemy_sprite_requirements.py | 295 +++ .../enemizer_data/overworld_enemy_metadata.py | 131 ++ .../alttp/enemizer_data/pot_shuffle_data.py | 107 ++ worlds/alttp/enemizer_data/symbols.py | 171 ++ worlds/alttp/test/TestEnemizerPatches.py | 308 +++ worlds/alttp/test/TestEnemyShuffle.py | 834 ++++++++ worlds/alttp/test/TestPotShuffle.py | 56 + 25 files changed, 4704 insertions(+), 298 deletions(-) create mode 100644 worlds/alttp/EnemizerPatches.py create mode 100644 worlds/alttp/EnemyShuffle.py create mode 100644 worlds/alttp/PotShuffle.py create mode 100644 worlds/alttp/enemizer_data/README.md create mode 100644 worlds/alttp/enemizer_data/__init__.py create mode 100644 worlds/alttp/enemizer_data/base_patch_data.py create mode 100644 worlds/alttp/enemizer_data/dungeon_sprite_addresses.py create mode 100644 worlds/alttp/enemizer_data/enemy_room_metadata.py create mode 100644 worlds/alttp/enemizer_data/enemy_sprite_requirements.py create mode 100644 worlds/alttp/enemizer_data/overworld_enemy_metadata.py create mode 100644 worlds/alttp/enemizer_data/pot_shuffle_data.py create mode 100644 worlds/alttp/enemizer_data/symbols.py create mode 100644 worlds/alttp/test/TestEnemizerPatches.py create mode 100644 worlds/alttp/test/TestEnemyShuffle.py create mode 100644 worlds/alttp/test/TestPotShuffle.py diff --git a/.dockerignore b/.dockerignore index 982e411032c6..0cc03e22e444 100644 --- a/.dockerignore +++ b/.dockerignore @@ -46,7 +46,6 @@ dist /prof/ README.html .vs/ -EnemizerCLI/ /Players/ /SNI/ /sni-*/ diff --git a/Dockerfile b/Dockerfile index 363478988c96..9740806565fc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,23 +1,5 @@ # hadolint global ignore=SC1090,SC1091 -# Source -FROM scratch AS release -WORKDIR /release -ADD https://github.com/Ijwu/Enemizer/releases/latest/download/ubuntu.16.04-x64.zip Enemizer.zip - -# Enemizer -FROM alpine:3.21 AS enemizer -ARG TARGETARCH -WORKDIR /release -COPY --from=release /release/Enemizer.zip . - -# No release for arm architecture. Skip. -RUN if [ "$TARGETARCH" = "amd64" ]; then \ - apk add unzip=6.0-r15 --no-cache && \ - unzip -u Enemizer.zip -d EnemizerCLI && \ - chmod -R 777 EnemizerCLI; \ - else touch EnemizerCLI; fi - # Cython builder stage FROM python:3.12 AS cython-builder @@ -81,15 +63,6 @@ RUN apt-get purge -y \ g++ && \ apt-get autoremove -y -# Copy necessary components -COPY --from=enemizer /release/EnemizerCLI /tmp/EnemizerCLI - -# No release for arm architecture. Skip. -RUN if [ "$TARGETARCH" = "amd64" ]; then \ - cp -r /tmp/EnemizerCLI EnemizerCLI; \ - fi; \ - rm -rf /tmp/EnemizerCLI - # Define health check HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ CMD curl -f http://localhost:${PORT:-80} || exit 1 diff --git a/docs/deploy using containers.md b/docs/deploy using containers.md index 6db38d443ffb..f2f86d428925 100644 --- a/docs/deploy using containers.md +++ b/docs/deploy using containers.md @@ -77,15 +77,6 @@ Changes made to `docker-compose.yaml` can be applied by running `docker compose It is possible to carry out these deployment steps on Windows under [Windows Subsystem for Linux](https://learn.microsoft.com/en-us/windows/wsl/install). -## Optional: A Link to the Past Enemizer - -Only required to generate seeds that include A Link to the Past with certain options enabled. You will receive an -error if it is required. -Enemizer can be enabled on `x86_64` platform architecture, and is included in the image build process. Enemizer requires a version 1.0 Japanese "Zelda no Densetsu" `.sfc` rom file to be placed in the application directory: -`docker run archipelago -v "/path/to/zelda.sfc:/app/Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"`. -Enemizer is not currently available for `aarch64`. - - ## Optional: Git Building the image requires a local copy of the ArchipelagoMW source code. diff --git a/docs/running from source.md b/docs/running from source.md index dbb251696191..52cc492112fa 100644 --- a/docs/running from source.md +++ b/docs/running from source.md @@ -78,16 +78,6 @@ first generate the binary distribution and then run `python setup.py bdist_appim put an `appimagetool` into the directory you run the command from, rename it to `appimagetool` and make it executable. -## Optional: A Link to the Past Enemizer - -Only required to generate seeds that include A Link to the Past with certain options enabled. You will receive an -error if it is required. - -You can get the latest Enemizer release at [Enemizer Github releases](https://github.com/Ijwu/Enemizer/releases). -It should be dropped as "EnemizerCLI" into the root folder of the project. Alternatively, you can point the Enemizer -setting in host.yaml at your Enemizer executable. - - ## Optional: SNI [SNI](https://github.com/alttpo/sni/blob/main/README.md) is required to use SNIClient. If not integrated into the project, it has to be started manually. diff --git a/inno_setup.iss b/inno_setup.iss index a325d30f5c12..50c400550ba8 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -57,9 +57,8 @@ Name: "custom"; Description: "Custom installation"; Flags: iscustom NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify; [Files] -Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; -Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall [Icons] @@ -83,7 +82,6 @@ Type: files; Name: "{app}\*.exe" Type: files; Name: "{app}\data\lua\connector_pkmn_rb.lua" Type: files; Name: "{app}\data\lua\connector_ff1.lua" Type: filesandordirs; Name: "{app}\SNI\lua*" -Type: filesandordirs; Name: "{app}\EnemizerCLI*" #include "installdelete.iss" [Registry] diff --git a/settings.py b/settings.py index 72ce53d92ca6..46377389aaa8 100644 --- a/settings.py +++ b/settings.py @@ -633,10 +633,6 @@ class LogNetwork(IntEnum): class GeneratorOptions(Group): """Options for Generation""" - class EnemizerPath(LocalFilePath): - """Location of your Enemizer CLI, available here: https://github.com/Ijwu/Enemizer/releases""" - is_exe = True - class PlayerFilesPath(OptionalUserFolderPath): """Folder from which the player yaml files are pulled from""" # created on demand, so marked as optional @@ -693,7 +689,6 @@ class PanicMethod(str): start_inventory -> Move remaining items to start_inventory, generate additional filler items to fill locations. """ - enemizer_path: EnemizerPath = EnemizerPath("EnemizerCLI/EnemizerCLI.Core") # + ".exe" is implied on Windows player_files_path: PlayerFilesPath = PlayerFilesPath("Players") players: Players = Players(0) allow_quantity: AllowQuantity | bool = False diff --git a/setup.py b/setup.py index 3c40eab59eda..9aa240462ba9 100644 --- a/setup.py +++ b/setup.py @@ -201,7 +201,7 @@ def resolve_icon(icon_name: str): icon=resolve_icon(c.icon), )) -extra_data = ["LICENSE", "data", "EnemizerCLI", "SNI"] +extra_data = ["LICENSE", "data", "SNI"] extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else [] @@ -456,9 +456,8 @@ def run(self) -> None: for world_directory in folders_to_remove) else: # make sure extra programs are executable - enemizer_exe = self.buildfolder / 'EnemizerCLI/EnemizerCLI.Core' sni_exe = self.buildfolder / 'SNI/sni' - extra_exes = (enemizer_exe, sni_exe) + extra_exes = (sni_exe,) for extra_exe in extra_exes: if extra_exe.is_file(): extra_exe.chmod(0o755) diff --git a/worlds/alttp/EnemizerPatches.py b/worlds/alttp/EnemizerPatches.py new file mode 100644 index 000000000000..9d7618c075b9 --- /dev/null +++ b/worlds/alttp/EnemizerPatches.py @@ -0,0 +1,478 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from functools import lru_cache +import hashlib +import random +from typing import TYPE_CHECKING, Optional + +from Utils import pc_to_snes, snes_to_pc +from .enemizer_data.base_patch_data import ENEMIZER_BASE_PATCHES +from .enemizer_data.symbols import ENEMIZER_SYMBOLS + +if TYPE_CHECKING: + from . import ALTTPWorld + from .Rom import LocalRom + + +@dataclass(frozen=True) +class BossPatchData: + pointer: tuple[int, int] + graphics: int + sprite_array: tuple[int, ...] + + +@dataclass(frozen=True) +class DungeonBossPatchData: + room_id: int + sprite_pointer_address: int + shell_x: int + shell_y: int + clear_layer2: bool = False + extra_sprites: tuple[int, ...] = () + gt_sprite_write_address: Optional[int] = None + + +@dataclass +class RoomObjectTable: + header_byte_0: int + header_byte_1: int + layer_1_objects: list[bytes] = field(default_factory=list) + layer_1_doors: list[bytes] = field(default_factory=list) + layer_2_objects: list[bytes] = field(default_factory=list) + layer_2_doors: list[bytes] = field(default_factory=list) + layer_3_objects: list[bytes] = field(default_factory=list) + layer_3_doors: list[bytes] = field(default_factory=list) + + @classmethod + def from_rom(cls, rom: "LocalRom", start_address: int) -> "RoomObjectTable": + table = cls(rom.read_byte(start_address), rom.read_byte(start_address + 1)) + layers = ( + (table.layer_1_objects, table.layer_1_doors), + (table.layer_2_objects, table.layer_2_doors), + (table.layer_3_objects, table.layer_3_doors), + ) + index = start_address + 2 + + for objects, doors in layers: + is_door = False + while True: + if rom.read_bytes(index, 2) == bytearray((0xF0, 0xFF)): + is_door = True + index += 2 + continue + if rom.read_bytes(index, 2) == bytearray((0xFF, 0xFF)): + index += 2 + break + if is_door: + doors.append(bytes(rom.read_bytes(index, 2))) + index += 2 + else: + objects.append(bytes(rom.read_bytes(index, 3))) + index += 3 + + return table + + def add_shell(self, x: int, y: int, clear_layer_2: bool, shell_id: int) -> None: + self.header_byte_0 = 0xF0 + if clear_layer_2: + self.layer_2_objects.clear() + self.layer_2_objects.append(_build_subtype_3_object(x, y, shell_id)) + + def remove_shell(self, shell_id: int) -> None: + self.layer_2_objects = [obj for obj in self.layer_2_objects if _object_id(obj) != shell_id] + + def to_bytes(self) -> bytes: + output = bytearray((self.header_byte_0, self.header_byte_1)) + output.extend(self._serialize_layer(self.layer_1_objects, self.layer_1_doors, is_last_layer=False)) + output.extend(self._serialize_layer(self.layer_2_objects, self.layer_2_doors, is_last_layer=False)) + output.extend(self._serialize_layer(self.layer_3_objects, self.layer_3_doors, is_last_layer=True)) + return bytes(output) + + @staticmethod + def _serialize_layer(objects: list[bytes], doors: list[bytes], is_last_layer: bool) -> bytes: + output = bytearray() + for obj in objects: + output.extend(obj) + if is_last_layer or doors: + output.extend((0xF0, 0xFF)) + for door in doors: + output.extend(door) + output.extend((0xFF, 0xFF)) + return bytes(output) + + +BOSS_PATCH_DATA: dict[str, BossPatchData] = { + "Armos": BossPatchData((0x87, 0xE8), 9, (0x05, 0x04, 0x53, 0x05, 0x07, 0x53, 0x05, 0x0A, 0x53, + 0x08, 0x0A, 0x53, 0x08, 0x07, 0x53, 0x08, 0x04, 0x53, + 0x08, 0xE7, 0x19)), + "Arrghus": BossPatchData((0x97, 0xD9), 20, (0x07, 0x07, 0x8C, 0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D, + 0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D, + 0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D, + 0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D, + 0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D)), + "Blind": BossPatchData((0x54, 0xE6), 32, (0x05, 0x09, 0xCE)), + "Helmasaur": BossPatchData((0x49, 0xE0), 21, (0x06, 0x07, 0x92)), + "Kholdstare": BossPatchData((0x01, 0xEA), 22, (0x05, 0x07, 0xA3, 0x05, 0x07, 0xA4, 0x05, 0x07, 0xA2)), + "Lanmola": BossPatchData((0xCB, 0xDC), 11, (0x07, 0x06, 0x54, 0x07, 0x09, 0x54, 0x09, 0x07, 0x54)), + "Moldorm": BossPatchData((0xC3, 0xD9), 12, (0x09, 0x09, 0x09)), + "Mothula": BossPatchData((0x31, 0xDC), 26, (0x06, 0x08, 0x88)), + "Trinexx": BossPatchData((0xBA, 0xE5), 23, (0x05, 0x07, 0xCB, 0x05, 0x07, 0xCC, 0x05, 0x07, 0xCD)), + "Vitreous": BossPatchData((0x57, 0xE4), 22, (0x05, 0x07, 0xBD)), +} + +DUNGEON_BOSS_PATCH_DATA: dict[tuple[str, Optional[str]], DungeonBossPatchData] = { + ("Eastern Palace", None): DungeonBossPatchData(200, 0x04D7BE, 0x2B, 0x28), + ("Desert Palace", None): DungeonBossPatchData(51, 0x04D694, 0x0B, 0x28), + ("Tower of Hera", None): DungeonBossPatchData(7, 0x04D63C, 0x18, 0x16), + ("Palace of Darkness", None): DungeonBossPatchData(90, 0x04D6E2, 0x2B, 0x28), + ("Swamp Palace", None): DungeonBossPatchData(6, 0x04D63A, 0x0B, 0x28), + ("Skull Woods", None): DungeonBossPatchData(41, 0x04D680, 0x2B, 0x28), + ("Thieves Town", None): DungeonBossPatchData(172, 0x04D786, 0x2B, 0x28, clear_layer2=True), + ("Ice Palace", None): DungeonBossPatchData(222, 0x04D7EA, 0x2B, 0x08, clear_layer2=True), + ("Misery Mire", None): DungeonBossPatchData(144, 0x04D74E, 0x0B, 0x28, clear_layer2=True), + ("Turtle Rock", None): DungeonBossPatchData(164, 0x04D776, 0x0B, 0x28, clear_layer2=True), + ("Ganons Tower", "bottom"): DungeonBossPatchData( + 28, 0x04D666, 0x2B, 0x28, extra_sprites=(0x07, 0x07, 0xE3, 0x07, 0x08, 0xE3, 0x08, 0x07, 0xE3, 0x08, 0x08, 0xE3), + gt_sprite_write_address=0x04D87E, + ), + ("Ganons Tower", "middle"): DungeonBossPatchData( + 108, 0x04D706, 0x0B, 0x28, extra_sprites=(0x18, 0x17, 0xD1, 0x1C, 0x03, 0xC5), gt_sprite_write_address=0x04D8B6, + ), + ("Ganons Tower", "top"): DungeonBossPatchData(77, 0x04D6C8, 0x18, 0x16), +} + +TRINEXX_SHELL_OBJECT_ID = 0xFF2 +KHOLDSTARE_SHELL_OBJECT_ID = 0xF95 +TRINEXX_VANILLA_ROOM_ID = 164 +KHOLDSTARE_VANILLA_ROOM_ID = 222 +ENEMY_HP_TABLE_ADDRESS = 0x6B173 +ENEMY_DAMAGE_TABLE_ADDRESS = 0x6B266 +HIDDEN_ENEMY_CHANCE_POOL_ADDRESS = 0xD7BBB +DAMAGE_GROUP_TABLE_ADDRESS = 0x3742D +RETRO_ARROW_REPLACEMENT_CHECK_ADDRESS = 0x301FC +RETRO_RUPEE_REPLACEMENT_SPRITE_ID = 0xDA +ARROW_REFILL_5_SPRITE_ID = 0xE1 +THIEF_SPRITE_ID = 0xC4 +THIEF_DEFAULT_HP = 4 +VANILLA_HIDDEN_ENEMY_CHANCE_POOL = ( + 0x01, 0x01, 0x01, 0x01, 0x0F, 0x01, 0x01, 0x12, + 0x10, 0x01, 0x01, 0x01, 0x11, 0x01, 0x01, 0x03, +) +RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL = ( + 0x01, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x12, + 0x0F, 0x01, 0x0F, 0x0F, 0x11, 0x0F, 0x0F, 0x03, +) +EXCLUDED_ENEMY_TABLE_SPRITE_IDS = frozenset({ + 0x09, 0x53, 0x54, 0x70, 0x7A, 0x7B, 0x88, 0x89, 0x8C, 0x8D, 0x92, + 0xA2, 0xA3, 0xA4, 0xBD, 0xBE, 0xBF, 0xCB, 0xCC, 0xCD, 0xCE, 0xD6, 0xD7, +}) +ENEMY_HEALTH_RANGE_BY_KEY = { + "easy": (1, 4), + "normal": (2, 15), + "hard": (2, 25), + "expert": (4, 50), +} + +_ENEMIZER_SYMBOLS: Optional[dict[str, int]] = None + +BOSS_GFX_SHEET_INDEXES = { + "Agahnim1": 0x8D, + "Agahnim2": 0xB5, + "Agahnim3": 0xC8, + "Agahnim4": 0xB6, + "ArmosKnight1": 0x90, + "Ganon1": 0x94, + "Ganon2": 0xA6, + "Ganon3": 0xB4, + "Ganon4": 0xB8, + "Moldorm1": 0xA3, + "Lanmola1": 0xA4, + "Arrghus1": 0xAC, + "Mothula1": 0xAB, + "Helmasaure1": 0xAD, + "Helmasaure2": 0xB1, + "Blind1": 0xAE, + "Kholdstare1": 0xAF, + "Vitreous1": 0xB0, + "Trinexx1": 0xB2, + "Trinexx2": 0xB3, +} + +BOSS_GFX_TABLE = { + "Agahnim1": (21, 190, 228), + "Agahnim2": (22, 255, 135), + "Agahnim3": (23, 220, 101), + "Agahnim4": (23, 132, 92), + "ArmosKnight1": (21, 206, 27), + "Ganon1": (21, 227, 160), + "Ganon2": (22, 186, 55), + "Ganon3": (22, 250, 199), + "Ganon4": (23, 142, 33), + "Moldorm1": (22, 175, 152), + "Lanmola1": (22, 180, 23), + "Arrghus1": (22, 214, 147), + "Mothula1": (22, 210, 84), + "Helmasaure1": (22, 219, 114), + "Helmasaure2": (22, 239, 177), + "Blind1": (22, 224, 90), + "Kholdstare1": (22, 230, 31), + "Vitreous1": (22, 235, 9), + "Trinexx1": (22, 243, 89), + "Trinexx2": (22, 246, 35), +} + +TRINEXX_ICE_FLOOR_ROUTINE_ADDRESS = 0x04B37E +TRINEXX_ICE_PROJECTILE_TILE_ADDRESS = 0xE7A5 +TILE_TRAP_FLOOR_TILE_ADDRESS = 0xF3BED + + +def apply_enemizer_base_patch(rom: "LocalRom") -> None: + for address, patch_data in _load_enemizer_base_patches(): + rom.write_bytes(address, patch_data) + _apply_trinexx_room_fixes(rom) + +def patch_bosses(world: "ALTTPWorld", rom: "LocalRom") -> None: + dungeon_header_base = _get_enemizer_symbol("room_header_table") + moved_room_object_base = _get_enemizer_symbol("modified_room_object_table") + gt_dungeon_name = "Ganons Tower" if world.options.mode != "inverted" else "Inverted Ganons Tower" + gt_dungeon = world.dungeons[gt_dungeon_name] + + placements = ( + (world.dungeons["Eastern Palace"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Eastern Palace", None)]), + (world.dungeons["Desert Palace"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Desert Palace", None)]), + (world.dungeons["Tower of Hera"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Tower of Hera", None)]), + (world.dungeons["Palace of Darkness"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Palace of Darkness", None)]), + (world.dungeons["Swamp Palace"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Swamp Palace", None)]), + (world.dungeons["Skull Woods"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Skull Woods", None)]), + (world.dungeons["Thieves Town"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Thieves Town", None)]), + (world.dungeons["Ice Palace"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Ice Palace", None)]), + (world.dungeons["Misery Mire"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Misery Mire", None)]), + (world.dungeons["Turtle Rock"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Turtle Rock", None)]), + (gt_dungeon.bosses["bottom"].enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Ganons Tower", "bottom")]), + (gt_dungeon.bosses["middle"].enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Ganons Tower", "middle")]), + (gt_dungeon.bosses["top"].enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Ganons Tower", "top")]), + ) + + modified_room_tables: dict[int, RoomObjectTable] = {} + + for boss_name, dungeon_data in placements: + boss_data = BOSS_PATCH_DATA[boss_name] + rom.write_bytes(dungeon_data.sprite_pointer_address, boss_data.pointer) + rom.write_byte(dungeon_header_base + (dungeon_data.room_id * 14) + 3, boss_data.graphics) + + if boss_name == "Trinexx" and dungeon_data.room_id != TRINEXX_VANILLA_ROOM_ID: + room_table = _get_room_object_table(rom, modified_room_tables, dungeon_data.room_id) + room_table.add_shell( + dungeon_data.shell_x, + dungeon_data.shell_y - 2, + dungeon_data.clear_layer2, + TRINEXX_SHELL_OBJECT_ID, + ) + rom.write_byte(dungeon_header_base + (dungeon_data.room_id * 14), 0x60) + rom.write_byte(dungeon_header_base + (dungeon_data.room_id * 14) + 4, 0x04) + + if boss_name == "Kholdstare" and dungeon_data.room_id != KHOLDSTARE_VANILLA_ROOM_ID: + room_table = _get_room_object_table(rom, modified_room_tables, dungeon_data.room_id) + room_table.add_shell( + dungeon_data.shell_x, + dungeon_data.shell_y, + dungeon_data.clear_layer2, + KHOLDSTARE_SHELL_OBJECT_ID, + ) + rom.write_byte(dungeon_header_base + (dungeon_data.room_id * 14), 0xE0) + rom.write_byte(dungeon_header_base + (dungeon_data.room_id * 14) + 4, 0x01) + + if boss_name != "Trinexx" and dungeon_data.room_id == TRINEXX_VANILLA_ROOM_ID: + _get_room_object_table(rom, modified_room_tables, dungeon_data.room_id).remove_shell(TRINEXX_SHELL_OBJECT_ID) + + if boss_name != "Kholdstare" and dungeon_data.room_id == KHOLDSTARE_VANILLA_ROOM_ID: + _get_room_object_table(rom, modified_room_tables, dungeon_data.room_id).remove_shell(KHOLDSTARE_SHELL_OBJECT_ID) + + if dungeon_data.gt_sprite_write_address is not None: + _write_gt_boss_sprite_block(rom, dungeon_data, boss_data) + + write_address = moved_room_object_base + for room_id in sorted(modified_room_tables): + table_bytes = modified_room_tables[room_id].to_bytes() + _write_room_object_pointer(rom, room_id, write_address) + rom.write_bytes(write_address, table_bytes) + write_address += len(table_bytes) + + rom.write_byte(0x1B0101, 0x01) + rom.write_byte(0x04DE81, 0x00) + if world.dungeons["Thieves Town"].boss.enemizer_name == "Blind": + rom.write_byte(0x04DE81, 0x06) + rom.write_byte(0x1B0101, 0x00) + + +def _get_room_object_table(rom: "LocalRom", cache: dict[int, RoomObjectTable], room_id: int) -> RoomObjectTable: + room_table = cache.get(room_id) + if room_table is not None: + return room_table + + pointer_address = 0xF8000 + (room_id * 3) + snes_address_bytes = rom.read_bytes(pointer_address, 3) + snes_address = (snes_address_bytes[2] << 16) | (snes_address_bytes[1] << 8) | snes_address_bytes[0] + room_table = RoomObjectTable.from_rom(rom, snes_to_pc(snes_address)) + cache[room_id] = room_table + return room_table + + +def _write_gt_boss_sprite_block(rom: "LocalRom", dungeon_data: DungeonBossPatchData, boss_data: BossPatchData) -> None: + assert dungeon_data.gt_sprite_write_address is not None + rom.write_int16(dungeon_data.sprite_pointer_address, dungeon_data.gt_sprite_write_address) + + sprite_block = bytearray((0x00,)) + sprite_block.extend(boss_data.sprite_array) + if dungeon_data.room_id == 28 and boss_data.pointer == BOSS_PATCH_DATA["Arrghus"].pointer: + sprite_block.extend(dungeon_data.extra_sprites[:6]) + else: + sprite_block.extend(dungeon_data.extra_sprites) + sprite_block.append(0xFF) + rom.write_bytes(dungeon_data.gt_sprite_write_address, sprite_block) + + +def _write_room_object_pointer(rom: "LocalRom", room_id: int, pc_address: int) -> None: + snes_address = pc_to_snes(pc_address) + pointer_address = 0xF8000 + (room_id * 3) + rom.write_bytes(pointer_address, ( + snes_address & 0xFF, + (snes_address >> 8) & 0xFF, + (snes_address >> 16) & 0xFF, + )) + + +def _build_subtype_3_object(x: int, y: int, object_id: int) -> bytes: + return bytes(( + ((x << 2) & 0xFC) | (object_id & 0x03), + ((y << 2) & 0xFC) | ((object_id >> 2) & 0x03), + 0xF0 | ((object_id >> 4) & 0x0F), + )) + + +def _object_id(object_bytes: bytes) -> Optional[int]: + if len(object_bytes) != 3: + return None + if object_bytes[0] >= 0xFC: + return (object_bytes[2] & 0x3F) + 0x100 + if object_bytes[2] >= 0xF8: + return 0xF00 | ((object_bytes[2] & 0x0F) << 4) | ((object_bytes[1] & 0x03) << 2) | (object_bytes[0] & 0x03) + return object_bytes[2] + + +def _set_enemizer_flag(rom: "LocalRom", symbol_name: str, enabled: bool) -> None: + rom.write_byte(_get_enemizer_symbol(symbol_name), 0x01 if enabled else 0x00) + + +def _apply_killable_thief(rom: "LocalRom") -> None: + rom.write_byte(_get_enemizer_symbol("notItemSprite_Mimic") + 4, THIEF_SPRITE_ID) + thief_hp_address = ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID + if rom.read_byte(thief_hp_address) != 0xFF: + rom.write_byte(thief_hp_address, THIEF_DEFAULT_HP) + + +def _randomize_enemy_health(rom: "LocalRom", rng: random.Random, enemy_health_key: str) -> None: + min_hp, max_hp = ENEMY_HEALTH_RANGE_BY_KEY[enemy_health_key] + for sprite_id in range(0xF3): + hp_address = ENEMY_HP_TABLE_ADDRESS + sprite_id + if rom.read_byte(hp_address) == 0xFF or sprite_id in EXCLUDED_ENEMY_TABLE_SPRITE_IDS: + continue + rom.write_byte(hp_address, rng.randrange(min_hp, max_hp)) + + +def _randomize_enemy_damage(rom: "LocalRom", rng: random.Random, allow_zero_damage: bool) -> None: + for sprite_id in range(0xF3): + if sprite_id in EXCLUDED_ENEMY_TABLE_SPRITE_IDS: + continue + new_damage = rng.randrange(8) + if not allow_zero_damage and new_damage == 2: + continue + rom.write_byte(ENEMY_DAMAGE_TABLE_ADDRESS + sprite_id, new_damage) + + +def _shuffle_damage_groups( + rom: "LocalRom", + rng: random.Random, + *, + chaos_mode: bool, + allow_zero_damage: bool, +) -> None: + min_damage = 0 if allow_zero_damage else 4 + max_damage = 64 if chaos_mode else 32 + + for group_id in range(10): + green_mail_damage = rng.randrange(min_damage, max_damage) + if chaos_mode: + blue_mail_damage = rng.randrange(min_damage, max_damage) + red_mail_damage = rng.randrange(min_damage, max_damage) + else: + blue_mail_damage = green_mail_damage * 3 // 4 + red_mail_damage = green_mail_damage * 3 // 8 + group_address = DAMAGE_GROUP_TABLE_ADDRESS + (group_id * 3) + rom.write_bytes(group_address, (green_mail_damage, blue_mail_damage, red_mail_damage)) + + +def _update_hidden_enemy_item_table_for_retro_mode(rom: "LocalRom") -> None: + if rom.read_byte(RETRO_ARROW_REPLACEMENT_CHECK_ADDRESS) != RETRO_RUPEE_REPLACEMENT_SPRITE_ID: + return + + item_table_address = _get_enemizer_symbol("sprite_bush_spawn_item_table") + for index in range(22): + if rom.read_byte(item_table_address + index) == ARROW_REFILL_5_SPRITE_ID: + rom.write_byte(item_table_address + index, RETRO_RUPEE_REPLACEMENT_SPRITE_ID) + + +def _apply_trinexx_room_fixes(rom: "LocalRom") -> None: + # Match original Enemizer's unconditional Trinexx ice-floor removal so + # blue-head projectiles do not create solid walls in non-vanilla rooms. + rom.write_bytes(TRINEXX_ICE_FLOOR_ROUTINE_ADDRESS, (0xEA, 0xEA, 0xEA, 0xEA)) + + +def _apply_randomized_tile_trap_floor_tile(rom: "LocalRom") -> None: + # Original Enemizer's RandomizeTileTrapFloorTile option changes the tile + # left behind by flying floor tile traps. AP does not currently expose or + # call this option, so keep the implementation isolated and unused. + rom.write_bytes(TRINEXX_ICE_PROJECTILE_TILE_ADDRESS, (0x88, 0x01)) + rom.write_byte(TILE_TRAP_FLOOR_TILE_ADDRESS, 0x12) + + +def _make_native_enemizer_rng(world: "ALTTPWorld") -> random.Random: + seed_material = "|".join(( + str(world.multiworld.seed), + world.multiworld.seed_name, + str(world.player), + _option_key(world.options.enemy_health), + _option_key(world.options.enemy_damage), + str(int(bool(world.options.enemy_shuffle))), + str(int(bool(world.options.bush_shuffle))), + str(int(bool(world.options.killable_thieves))), + )) + seed = int.from_bytes(hashlib.sha256(seed_material.encode("utf-8")).digest()[:8], "big") + return random.Random(seed) + + +@lru_cache(maxsize=1) +def _load_enemizer_base_patches() -> tuple[tuple[int, bytes], ...]: + return tuple( + (entry.address, entry.patch_data) + for entry in ENEMIZER_BASE_PATCHES + ) + + +def _option_key(option: object) -> str: + return str(getattr(option, "current_key", option)) + + +def _get_enemizer_symbol(symbol_name: str) -> int: + global _ENEMIZER_SYMBOLS + if _ENEMIZER_SYMBOLS is None: + _ENEMIZER_SYMBOLS = _load_enemizer_symbols() + return _ENEMIZER_SYMBOLS[symbol_name] + + +def _load_enemizer_symbols() -> dict[str, int]: + return { + name: snes_to_pc(snes_address) + for name, snes_address in ENEMIZER_SYMBOLS.items() + } diff --git a/worlds/alttp/EnemyShuffle.py b/worlds/alttp/EnemyShuffle.py new file mode 100644 index 000000000000..3f09920169d4 --- /dev/null +++ b/worlds/alttp/EnemyShuffle.py @@ -0,0 +1,1707 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional, TYPE_CHECKING + +from Utils import snes_to_pc + +from .EnemizerPatches import apply_enemizer_base_patch +from .Rom import LocalRom, get_base_rom_path +from .enemizer_data.dungeon_sprite_addresses import DUNGEON_SPRITE_ADDRESSES, KEYED_SPRITE_ID_ADDRESSES +from .enemizer_data.enemy_room_metadata import ( + BOSS_ROOM_IDS, + DONT_RANDOMIZE_ROOM_IDS, + NO_SPECIAL_ENEMIES_STANDARD_ROOM_IDS, + ROOM_GROUP_REQUIREMENTS, + SHUTTER_ROOM_IDS, + WATER_ROOM_IDS, +) +from .enemizer_data.enemy_sprite_requirements import ENEMY_SPRITE_REQUIREMENTS +from .enemizer_data.overworld_enemy_metadata import ( + AREA_IDS, + DO_NOT_RANDOMIZE_AREA_IDS, + FORCED_GROUP_REQUIREMENTS, +) +from .enemizer_data.symbols import ENEMIZER_SYMBOLS + +if TYPE_CHECKING: + from . import ALTTPWorld + from .Rom import LocalRom + + +DUNGEON_HEADER_POINTER_TABLE_BASE = 0x271E2 +DUNGEON_SPRITE_POINTER_TABLE_BASE = 0x4D62E +OVERWORLD_SPRITE_POINTER_TABLE_BASE = 0x4C901 +OVERWORLD_AREA_GRAPHICS_BLOCK_BASE = 0x7A81 +ROOM_HEADER_BANK_LOCATION = 0xB5E7 +SPRITE_GROUP_BASE_ADDRESS = 0x5B97 +TOTAL_SPRITE_GROUPS = 144 +TOTAL_DUNGEON_ROOMS = 0x128 + +SPRITE_OVERLORD_MASK = 0xE0 +SPRITE_OVERLORD_REMOVE_MASK = 0x1F +SPRITE_SUBTYPE_BYTE_0_MASK = 0x60 +KEY_SPRITE_ID = 0xE4 +BIG_KEY_SPRITE_ID = 0xE5 +WALLMASTER_SPRITE_ID = 0x90 +STAL_SPRITE_ID = 0xD3 +FLOPPING_FISH_SPRITE_ID = 0xD2 +OW_FALLING_ROCKS_SPRITE_ID = 0xF4 +OW_WALLMASTER_TO_HOULIHAN_SPRITE_ID = 0xFB +WATER_TEKTITE_SPRITE_ID = 0x81 +POTENTIAL_SUBGROUP_0 = (22, 31, 47, 14) +POTENTIAL_SUBGROUP_1 = (44, 30, 32) +POTENTIAL_SUBGROUP_2 = (12, 18, 23, 24, 28, 46, 34, 35, 39, 40, 38, 41, 36, 37, 42) +POTENTIAL_SUBGROUP_3 = (17, 16, 27, 20, 82, 83) +GUARD_SUBGROUP_1_DUNGEON_GROUP_IDS = frozenset((1, 2, 3, 4)) +SELECTED_BOSS_GROUP_REQUIREMENTS = { + "Armos": (9, 83), + "Lanmola": (11, 84), + "Moldorm": (12, 9), + "Arrghus": (20, 140), + "Helmasaur": (21, 146), + "Kholdstare": (22, 162), + "Vitreous": (22, 189), + "Trinexx": (23, 203), + "Mothula": (26, 136), + "Blind": (32, 206), +} + +@dataclass(frozen=True) +class RoomGroupRequirement: + group_id: Optional[int] + subgroup_0: Optional[int] + subgroup_1: Optional[int] + subgroup_2: Optional[int] + subgroup_3: Optional[int] + rooms: tuple[int, ...] + + +@dataclass(frozen=True) +class OverworldGroupRequirement: + group_id: Optional[int] + subgroup_0: Optional[int] + subgroup_1: Optional[int] + subgroup_2: Optional[int] + subgroup_3: Optional[int] + areas: tuple[int, ...] + + +@dataclass +class DungeonSpriteGroup: + group_id: int + dungeon_group_id: int + subgroup_0: int + subgroup_1: int + subgroup_2: int + subgroup_3: int + preserve_subgroup_0: bool = False + preserve_subgroup_1: bool = False + preserve_subgroup_2: bool = False + preserve_subgroup_3: bool = False + + +@dataclass(frozen=True) +class EnemySpriteRequirement: + sprite_name: str + sprite_id: int + boss: bool + overlord: bool + do_not_randomize: bool + killable: bool + npc: bool + never_use_dungeon: bool + never_use_overworld: bool + cannot_have_key: bool + is_object: bool + absorbable: bool + is_water_sprite: bool + is_enemy_sprite: bool + group_ids: tuple[int, ...] + subgroup_0: tuple[int, ...] + subgroup_1: tuple[int, ...] + subgroup_2: tuple[int, ...] + subgroup_3: tuple[int, ...] + parameters: Optional[int] + special_glitched: bool + excluded_rooms: tuple[int, ...] + dont_randomize_rooms: tuple[int, ...] + spawnable_rooms: tuple[int, ...] + + +@dataclass(frozen=True) +class DungeonEnemySprite: + address: int + byte_0: int + byte_1: int + sprite_id: int + is_overlord: bool + has_key: bool + + @property + def is_on_bg2(self) -> bool: + return bool(self.byte_0 & 0x80) + + @property + def hm_param(self) -> int: + return ((self.byte_0 & 0x60) >> 2) | ((self.byte_1 & 0xE0) >> 5) + + @property + def y_coord_pixels(self) -> int: + return (self.byte_0 & 0x1F) * 16 + + @property + def x_coord_pixels(self) -> int: + return (self.byte_1 & 0x1F) * 16 + + +@dataclass(frozen=True) +class DungeonEnemyRoom: + room_id: int + room_header_address: int + sprite_table_address: int + graphics_block_id: int + tag_1: int + tag_2: int + sort_sprites_value: int + sprites: tuple[DungeonEnemySprite, ...] + required_group_id: Optional[int] + required_subgroup_0: tuple[int, ...] + required_subgroup_1: tuple[int, ...] + required_subgroup_2: tuple[int, ...] + required_subgroup_3: tuple[int, ...] + is_shutter_room: bool + is_water_room: bool + do_not_randomize: bool + no_special_enemies_standard: bool + + +@dataclass(frozen=True) +class RandomizedDungeonEnemySprite: + address: int + byte_0: int + byte_1: int + original_sprite_id: int + sprite_id: int + is_overlord: bool + has_key: bool + + +@dataclass(frozen=True) +class RandomizedDungeonEnemyRoom: + room_id: int + room_header_address: int + sprite_table_address: int + original_graphics_block_id: int + graphics_block_id: int + tag_1: int + tag_2: int + sort_sprites_value: int + sprites: tuple[RandomizedDungeonEnemySprite, ...] + skipped_randomization: bool + + +@dataclass(frozen=True) +class OverworldEnemySprite: + address: int + y_coord: int + x_coord: int + sprite_id: int + + +@dataclass(frozen=True) +class OverworldEnemyArea: + area_id: int + sprite_table_address: int + graphics_block_address: int + graphics_block_id: int + bush_sprite_id: int + sprites: tuple[OverworldEnemySprite, ...] + do_not_randomize: bool + + +@dataclass(frozen=True) +class RandomizedOverworldEnemySprite: + address: int + y_coord: int + x_coord: int + original_sprite_id: int + sprite_id: int + + +@dataclass(frozen=True) +class RandomizedOverworldEnemyArea: + area_id: int + sprite_table_address: int + graphics_block_address: int + original_graphics_block_id: int + graphics_block_id: int + original_bush_sprite_id: int + bush_sprite_id: int + sprites: tuple[RandomizedOverworldEnemySprite, ...] + skipped_randomization: bool + + +@dataclass(frozen=True) +class EnemyShuffleState: + dungeon_rooms: dict[int, DungeonEnemyRoom] + overworld_areas: dict[int, OverworldEnemyArea] + sprite_groups: dict[int, DungeonSpriteGroup] + sprite_requirements: tuple[EnemySpriteRequirement, ...] + room_group_requirements: tuple[RoomGroupRequirement, ...] + overworld_group_requirements: tuple[OverworldGroupRequirement, ...] + shutter_room_ids: frozenset[int] + water_room_ids: frozenset[int] + dont_randomize_room_ids: frozenset[int] + no_special_enemies_standard_room_ids: frozenset[int] + boss_room_ids: frozenset[int] + dont_randomize_overworld_area_ids: frozenset[int] + randomized_dungeon_rooms: dict[int, RandomizedDungeonEnemyRoom] + randomized_overworld_areas: dict[int, RandomizedOverworldEnemyArea] + + +def generate_enemy_shuffle_state(world: "ALTTPWorld") -> EnemyShuffleState: + rom_bytes = _get_base_patched_rom_bytes() + moved_header_bank = _get_enemizer_symbol("moved_room_header_bank_value_address") + bush_spawn_table_address = _get_enemizer_symbol("sprite_bush_spawn_table_overworld") + metadata = _load_enemy_room_metadata() + overworld_metadata = _load_overworld_enemy_metadata() + sprite_requirements = _load_enemy_sprite_requirements() + dungeon_rooms = { + room.room_id: room + for room in _read_dungeon_rooms(rom_bytes, moved_header_bank, metadata) + } + overworld_areas = { + area.area_id: area + for area in _read_overworld_areas(rom_bytes, bush_spawn_table_address, overworld_metadata) + } + sprite_groups = { + group.group_id: group + for group in _read_sprite_groups(rom_bytes) + } + _setup_required_dungeon_groups(world, sprite_groups, metadata["room_requirements"]) + _apply_selected_boss_group_requirements(world, sprite_groups, sprite_requirements) + _randomize_dungeon_groups(world, sprite_groups) + randomized_dungeon_rooms = _randomize_dungeon_rooms( + world, + dungeon_rooms, + sprite_groups, + sprite_requirements, + ) + _setup_required_overworld_groups(sprite_groups, overworld_metadata["forced_group_requirements"]) + _randomize_overworld_groups(world, sprite_groups) + randomized_overworld_areas = _randomize_overworld_areas( + world, + overworld_areas, + sprite_groups, + sprite_requirements, + overworld_metadata["forced_group_requirements"], + ) + state = EnemyShuffleState( + dungeon_rooms=dungeon_rooms, + overworld_areas=overworld_areas, + sprite_groups=sprite_groups, + sprite_requirements=sprite_requirements, + room_group_requirements=metadata["room_requirements"], + overworld_group_requirements=overworld_metadata["forced_group_requirements"], + shutter_room_ids=metadata["shutter_room_ids"], + water_room_ids=metadata["water_room_ids"], + dont_randomize_room_ids=metadata["dont_randomize_room_ids"], + no_special_enemies_standard_room_ids=metadata["no_special_enemies_standard_room_ids"], + boss_room_ids=metadata["boss_room_ids"], + dont_randomize_overworld_area_ids=overworld_metadata["do_not_randomize_area_ids"], + randomized_dungeon_rooms=randomized_dungeon_rooms, + randomized_overworld_areas=randomized_overworld_areas, + ) + validate_enemy_shuffle_state(state, is_standard_mode=world.options.mode == "standard") + return state + + +def _get_base_patched_rom_bytes() -> bytes: + patched_rom_bytes = getattr(_get_base_patched_rom_bytes, "patched_rom_bytes", None) + if patched_rom_bytes is None: + patched_rom = LocalRom(get_base_rom_path()) + apply_enemizer_base_patch(patched_rom) + patched_rom_bytes = bytes(patched_rom.buffer) + _get_base_patched_rom_bytes.patched_rom_bytes = patched_rom_bytes + return patched_rom_bytes + + +def _read_dungeon_rooms(rom_bytes: bytes, moved_header_bank_address: int, metadata: dict[str, object]) -> list[DungeonEnemyRoom]: + rooms: list[DungeonEnemyRoom] = [] + room_header_bank = _get_room_header_bank(rom_bytes, moved_header_bank_address) + dungeon_sprite_metadata = _load_dungeon_sprite_metadata() + shutter_room_ids = metadata["shutter_room_ids"] + water_room_ids = metadata["water_room_ids"] + dont_randomize_room_ids = metadata["dont_randomize_room_ids"] + no_special_enemies_standard_room_ids = metadata["no_special_enemies_standard_room_ids"] + room_requirements = metadata["room_requirements"] + + for room_id in range(TOTAL_DUNGEON_ROOMS): + room_header_address = _read_room_header_address(rom_bytes, room_id, room_header_bank) + sprite_table_address = _read_room_sprite_table_address(rom_bytes, room_id) + merged_requirement = _merge_room_requirements(room_id, room_requirements) + rooms.append( + DungeonEnemyRoom( + room_id=room_id, + room_header_address=room_header_address, + sprite_table_address=sprite_table_address, + graphics_block_id=rom_bytes[room_header_address + 3], + tag_1=rom_bytes[room_header_address + 5], + tag_2=rom_bytes[room_header_address + 6], + sort_sprites_value=rom_bytes[sprite_table_address], + sprites=_read_room_sprites(rom_bytes, room_id, sprite_table_address, dungeon_sprite_metadata), + required_group_id=merged_requirement.group_id, + required_subgroup_0=merged_requirement.subgroup_0, + required_subgroup_1=merged_requirement.subgroup_1, + required_subgroup_2=merged_requirement.subgroup_2, + required_subgroup_3=merged_requirement.subgroup_3, + is_shutter_room=room_id in shutter_room_ids, + is_water_room=room_id in water_room_ids, + do_not_randomize=room_id in dont_randomize_room_ids, + no_special_enemies_standard=room_id in no_special_enemies_standard_room_ids, + ) + ) + + return rooms + + +def _get_room_header_bank(rom_bytes: bytes, moved_header_bank_address: int) -> int: + if 0 <= moved_header_bank_address < len(rom_bytes): + moved_header_bank = rom_bytes[moved_header_bank_address] + if moved_header_bank: + return moved_header_bank + return rom_bytes[ROOM_HEADER_BANK_LOCATION] + + +def _read_sprite_groups(rom_bytes: bytes) -> tuple[DungeonSpriteGroup, ...]: + groups = [] + for group_id in range(TOTAL_SPRITE_GROUPS): + groups.append( + DungeonSpriteGroup( + group_id=group_id, + dungeon_group_id=group_id - 0x40, + subgroup_0=rom_bytes[SPRITE_GROUP_BASE_ADDRESS + (group_id * 4)], + subgroup_1=rom_bytes[SPRITE_GROUP_BASE_ADDRESS + (group_id * 4) + 1], + subgroup_2=rom_bytes[SPRITE_GROUP_BASE_ADDRESS + (group_id * 4) + 2], + subgroup_3=rom_bytes[SPRITE_GROUP_BASE_ADDRESS + (group_id * 4) + 3], + ) + ) + return tuple(groups) + + +def _setup_required_dungeon_groups( + world: "ALTTPWorld", + sprite_groups: dict[int, DungeonSpriteGroup], + room_requirements: tuple[RoomGroupRequirement, ...], +) -> None: + for requirement in room_requirements: + if requirement.group_id is None: + continue + group = sprite_groups.get(requirement.group_id + 0x40) + if group is None: + continue + _apply_required_subgroups(group, requirement) + + merged_room_requirements = { + room_id: _merge_room_requirements(room_id, room_requirements) + for requirement in room_requirements + for room_id in requirement.rooms + } + + for merged_requirement in merged_room_requirements.values(): + if merged_requirement.group_id is not None: + continue + if _has_preserved_group_for_room_requirement(sprite_groups, merged_requirement): + continue + + possible_groups = [ + group for group in sprite_groups.values() + if 0 < group.dungeon_group_id < 60 + and ( + not group.preserve_subgroup_0 + or not group.preserve_subgroup_1 + or not group.preserve_subgroup_2 + or not group.preserve_subgroup_3 + ) + and (not merged_requirement.subgroup_0 or not group.preserve_subgroup_0) + and (not merged_requirement.subgroup_1 or not group.preserve_subgroup_1) + and (not merged_requirement.subgroup_2 or not group.preserve_subgroup_2) + and (not merged_requirement.subgroup_3 or not group.preserve_subgroup_3) + ] + if not possible_groups: + continue + + selected_group = world.random.choice(possible_groups) + _apply_merged_room_requirement(selected_group, merged_requirement) + + +def _apply_selected_boss_group_requirements( + world: "ALTTPWorld", + sprite_groups: dict[int, DungeonSpriteGroup], + sprite_requirements: tuple[EnemySpriteRequirement, ...], +) -> None: + requirement_by_sprite_id = {requirement.sprite_id: requirement for requirement in sprite_requirements} + for boss_name in _get_selected_boss_names(world): + boss_group_data = SELECTED_BOSS_GROUP_REQUIREMENTS.get(boss_name) + if boss_group_data is None: + continue + dungeon_group_id, sprite_id = boss_group_data + group = sprite_groups.get(dungeon_group_id + 0x40) + requirement = requirement_by_sprite_id.get(sprite_id) + if group is None or requirement is None: + continue + _apply_selected_boss_requirement(group, requirement) + + +def _get_selected_boss_names(world: "ALTTPWorld") -> tuple[str, ...]: + dungeons = getattr(world, "dungeons", None) + if not dungeons: + return tuple() + + gt_dungeon_name = "Ganons Tower" if world.options.mode != "inverted" else "Inverted Ganons Tower" + gt_dungeon = dungeons.get(gt_dungeon_name) + gt_bosses = getattr(gt_dungeon, "bosses", {}) if gt_dungeon is not None else {} + + selected_bosses = [ + dungeons["Eastern Palace"].boss.enemizer_name, + dungeons["Desert Palace"].boss.enemizer_name, + dungeons["Tower of Hera"].boss.enemizer_name, + dungeons["Palace of Darkness"].boss.enemizer_name, + dungeons["Swamp Palace"].boss.enemizer_name, + dungeons["Skull Woods"].boss.enemizer_name, + dungeons["Thieves Town"].boss.enemizer_name, + dungeons["Ice Palace"].boss.enemizer_name, + dungeons["Misery Mire"].boss.enemizer_name, + dungeons["Turtle Rock"].boss.enemizer_name, + ] + for gt_slot in ("bottom", "middle", "top"): + if gt_slot in gt_bosses: + selected_bosses.append(gt_bosses[gt_slot].enemizer_name) + return tuple(selected_bosses) + + +def _apply_selected_boss_requirement(group: DungeonSpriteGroup, requirement: EnemySpriteRequirement) -> None: + if requirement.subgroup_0: + group.subgroup_0 = requirement.subgroup_0[0] + group.preserve_subgroup_0 = True + if requirement.subgroup_1: + group.subgroup_1 = requirement.subgroup_1[0] + group.preserve_subgroup_1 = True + if requirement.subgroup_2: + group.subgroup_2 = requirement.subgroup_2[0] + group.preserve_subgroup_2 = True + if requirement.subgroup_3: + group.subgroup_3 = requirement.subgroup_3[0] + group.preserve_subgroup_3 = True + + +def _setup_required_overworld_groups( + sprite_groups: dict[int, DungeonSpriteGroup], + overworld_group_requirements: tuple[OverworldGroupRequirement, ...], +) -> None: + for requirement in overworld_group_requirements: + if requirement.group_id is None: + continue + group = sprite_groups.get(requirement.group_id) + if group is None: + continue + if ( + requirement.subgroup_0 is None + and requirement.subgroup_1 is None + and requirement.subgroup_2 is None + and requirement.subgroup_3 is None + ): + group.preserve_subgroup_0 = True + group.preserve_subgroup_1 = True + group.preserve_subgroup_2 = True + group.preserve_subgroup_3 = True + continue + _apply_required_subgroups(group, requirement) + + +def _apply_required_subgroups(group: DungeonSpriteGroup, requirement: RoomGroupRequirement | OverworldGroupRequirement) -> None: + if requirement.subgroup_0 is not None: + group.subgroup_0 = requirement.subgroup_0 + group.preserve_subgroup_0 = True + if requirement.subgroup_1 is not None: + group.subgroup_1 = requirement.subgroup_1 + group.preserve_subgroup_1 = True + if requirement.subgroup_2 is not None: + group.subgroup_2 = requirement.subgroup_2 + group.preserve_subgroup_2 = True + if requirement.subgroup_3 is not None: + group.subgroup_3 = requirement.subgroup_3 + group.preserve_subgroup_3 = True + + +def _apply_merged_room_requirement(group: DungeonSpriteGroup, requirement: MergedRoomRequirement) -> None: + if requirement.subgroup_0: + group.subgroup_0 = requirement.subgroup_0[0] + group.preserve_subgroup_0 = True + if requirement.subgroup_1: + group.subgroup_1 = requirement.subgroup_1[0] + group.preserve_subgroup_1 = True + if requirement.subgroup_2: + group.subgroup_2 = requirement.subgroup_2[0] + group.preserve_subgroup_2 = True + if requirement.subgroup_3: + group.subgroup_3 = requirement.subgroup_3[0] + group.preserve_subgroup_3 = True + + +def _has_preserved_group_for_room_requirement( + sprite_groups: dict[int, DungeonSpriteGroup], + requirement: MergedRoomRequirement, +) -> bool: + for group in sprite_groups.values(): + if not (0 < group.dungeon_group_id < 60): + continue + if requirement.subgroup_0 and (group.subgroup_0 != requirement.subgroup_0[0] or not group.preserve_subgroup_0): + continue + if requirement.subgroup_1 and (group.subgroup_1 != requirement.subgroup_1[0] or not group.preserve_subgroup_1): + continue + if requirement.subgroup_2 and (group.subgroup_2 != requirement.subgroup_2[0] or not group.preserve_subgroup_2): + continue + if requirement.subgroup_3 and (group.subgroup_3 != requirement.subgroup_3[0] or not group.preserve_subgroup_3): + continue + return True + return False + + +def _randomize_dungeon_groups(world: "ALTTPWorld", sprite_groups: dict[int, DungeonSpriteGroup]) -> None: + for group in sprite_groups.values(): + if not (0 < group.dungeon_group_id < 60): + continue + if not group.preserve_subgroup_1 and group.dungeon_group_id in GUARD_SUBGROUP_1_DUNGEON_GROUP_IDS: + group.preserve_subgroup_1 = True + group.subgroup_1 = world.random.choice((73, 13)) + if not group.preserve_subgroup_0: + group.subgroup_0 = world.random.choice(POTENTIAL_SUBGROUP_0) + if not group.preserve_subgroup_1: + group.subgroup_1 = world.random.choice(POTENTIAL_SUBGROUP_1) + if not group.preserve_subgroup_2: + group.subgroup_2 = world.random.choice(POTENTIAL_SUBGROUP_2) + if not group.preserve_subgroup_3: + group.subgroup_3 = world.random.choice(POTENTIAL_SUBGROUP_3) + + +def _randomize_overworld_groups(world: "ALTTPWorld", sprite_groups: dict[int, DungeonSpriteGroup]) -> None: + for group in sprite_groups.values(): + if not (0 < group.group_id < 0x40): + continue + if not group.preserve_subgroup_0: + group.subgroup_0 = world.random.choice(POTENTIAL_SUBGROUP_0) + if not group.preserve_subgroup_1: + group.subgroup_1 = world.random.choice(POTENTIAL_SUBGROUP_1) + if not group.preserve_subgroup_2: + group.subgroup_2 = world.random.choice(POTENTIAL_SUBGROUP_2) + if not group.preserve_subgroup_3: + group.subgroup_3 = world.random.choice(POTENTIAL_SUBGROUP_3) + + +def _read_room_header_address(rom_bytes: bytes, room_id: int, room_header_bank: int) -> int: + pointer_address = DUNGEON_HEADER_POINTER_TABLE_BASE + (room_id * 2) + snes_address = ( + rom_bytes[pointer_address] + | (rom_bytes[pointer_address + 1] << 8) + | (room_header_bank << 16) + ) + return snes_to_pc(snes_address) + + +def _read_room_sprite_table_address(rom_bytes: bytes, room_id: int) -> int: + pointer_address = DUNGEON_SPRITE_POINTER_TABLE_BASE + (room_id * 2) + snes_address = ( + rom_bytes[pointer_address] + | (rom_bytes[pointer_address + 1] << 8) + | (0x09 << 16) + ) + return snes_to_pc(snes_address) + + +def _read_overworld_areas( + rom_bytes: bytes, + bush_spawn_table_address: int, + metadata: dict[str, object], +) -> list[OverworldEnemyArea]: + areas: list[OverworldEnemyArea] = [] + do_not_randomize_area_ids = metadata["do_not_randomize_area_ids"] + + for area_id in metadata["area_ids"]: + sprite_table_address = _read_overworld_sprite_table_address(rom_bytes, area_id) + graphics_block_address = _get_overworld_graphics_block_address(area_id) + areas.append( + OverworldEnemyArea( + area_id=area_id, + sprite_table_address=sprite_table_address, + graphics_block_address=graphics_block_address, + graphics_block_id=rom_bytes[graphics_block_address], + bush_sprite_id=rom_bytes[bush_spawn_table_address + area_id], + sprites=_read_overworld_sprites(rom_bytes, sprite_table_address), + do_not_randomize=area_id in do_not_randomize_area_ids, + ) + ) + + return areas + + +def _read_overworld_sprite_table_address(rom_bytes: bytes, area_id: int) -> int: + pointer_address = OVERWORLD_SPRITE_POINTER_TABLE_BASE + (area_id * 2) + snes_address = ( + rom_bytes[pointer_address] + | (rom_bytes[pointer_address + 1] << 8) + | (0x09 << 16) + ) + return snes_to_pc(snes_address) + + +def _get_overworld_graphics_block_address(area_id: int) -> int: + if area_id in {0x80, 0x81}: + return 0x16576 + (area_id - 0x80) + if area_id in {0x110, 0x111}: + return 0x16576 + (area_id - 0x110) + + address = OVERWORLD_AREA_GRAPHICS_BLOCK_BASE + area_id + if 0x40 <= area_id < 0x80: + address += 0x40 + if 0x90 <= area_id < 0x110: + address -= 0x50 + return address + + +def _read_overworld_sprites(rom_bytes: bytes, sprite_table_address: int) -> tuple[OverworldEnemySprite, ...]: + sprites: list[OverworldEnemySprite] = [] + index = sprite_table_address + while rom_bytes[index] != 0xFF: + sprites.append( + OverworldEnemySprite( + address=index, + y_coord=rom_bytes[index], + x_coord=rom_bytes[index + 1], + sprite_id=rom_bytes[index + 2], + ) + ) + index += 3 + return tuple(sprites) + + +def _read_room_sprites( + rom_bytes: bytes, + room_id: int, + sprite_table_address: int, + dungeon_sprite_metadata: dict[str, object], +) -> tuple[DungeonEnemySprite, ...]: + sprites: list[DungeonEnemySprite] = [] + keyed_sprite_id_addresses = dungeon_sprite_metadata["keyed_sprite_id_addresses"] + editable_sprite_id_addresses = dungeon_sprite_metadata["room_sprite_id_addresses"].get(room_id) + + if editable_sprite_id_addresses is None: + sprite_addresses = [] + index = sprite_table_address + 1 # byte 0 is sort-sprites metadata + while rom_bytes[index] != 0xFF: + sprite_addresses.append(index) + index += 3 + else: + sprite_addresses = [sprite_id_address - 2 for sprite_id_address in editable_sprite_id_addresses] + + seen_sprite_addresses: set[int] = set() + unique_sprite_addresses = [] + for address in sprite_addresses: + if address in seen_sprite_addresses: + continue + seen_sprite_addresses.add(address) + unique_sprite_addresses.append(address) + + for index in unique_sprite_addresses: + byte_0 = rom_bytes[index] + byte_1 = rom_bytes[index + 1] + sprite_id = rom_bytes[index + 2] + is_overlord = (byte_1 & SPRITE_OVERLORD_MASK) == SPRITE_OVERLORD_MASK and ( + (byte_0 & SPRITE_SUBTYPE_BYTE_0_MASK) != SPRITE_SUBTYPE_BYTE_0_MASK + ) + if not is_overlord and sprite_id not in {KEY_SPRITE_ID, WALLMASTER_SPRITE_ID}: + byte_0 &= 0x9F + byte_1 &= SPRITE_OVERLORD_REMOVE_MASK + has_key = (index + 2) in keyed_sprite_id_addresses + sprites.append( + DungeonEnemySprite( + address=index, + byte_0=byte_0, + byte_1=byte_1, + sprite_id=sprite_id + (0x100 if is_overlord else 0), + is_overlord=is_overlord, + has_key=has_key, + ) + ) + + return tuple(sprites) + + +def _get_enemizer_symbol(symbol_name: str) -> int: + return snes_to_pc(ENEMIZER_SYMBOLS[symbol_name]) + + +def _load_enemy_room_metadata() -> dict[str, object]: + return { + "shutter_room_ids": SHUTTER_ROOM_IDS, + "water_room_ids": WATER_ROOM_IDS, + "dont_randomize_room_ids": DONT_RANDOMIZE_ROOM_IDS, + "no_special_enemies_standard_room_ids": NO_SPECIAL_ENEMIES_STANDARD_ROOM_IDS, + "boss_room_ids": BOSS_ROOM_IDS, + "room_requirements": tuple( + RoomGroupRequirement( + group_id=requirement.group_id, + subgroup_0=requirement.subgroup_0, + subgroup_1=requirement.subgroup_1, + subgroup_2=requirement.subgroup_2, + subgroup_3=requirement.subgroup_3, + rooms=requirement.rooms, + ) + for requirement in ROOM_GROUP_REQUIREMENTS + ), + } + + +def _load_dungeon_sprite_metadata() -> dict[str, object]: + return { + "room_sprite_id_addresses": { + room.room_id: room.sprite_id_addresses + for room in DUNGEON_SPRITE_ADDRESSES + }, + "keyed_sprite_id_addresses": KEYED_SPRITE_ID_ADDRESSES, + } + + +def _load_enemy_sprite_requirements() -> tuple[EnemySpriteRequirement, ...]: + return tuple( + EnemySpriteRequirement( + sprite_name=entry.sprite_name, + sprite_id=entry.sprite_id, + boss=entry.boss, + overlord=entry.overlord, + do_not_randomize=entry.do_not_randomize, + killable=entry.killable, + npc=entry.npc, + never_use_dungeon=entry.never_use_dungeon, + never_use_overworld=entry.never_use_overworld, + cannot_have_key=entry.cannot_have_key, + is_object=entry.is_object, + absorbable=entry.absorbable, + is_water_sprite=entry.is_water_sprite, + is_enemy_sprite=entry.is_enemy_sprite, + group_ids=entry.group_ids, + subgroup_0=entry.subgroup_0, + subgroup_1=entry.subgroup_1, + subgroup_2=entry.subgroup_2, + subgroup_3=entry.subgroup_3, + parameters=entry.parameters, + special_glitched=entry.special_glitched, + excluded_rooms=entry.excluded_rooms, + dont_randomize_rooms=entry.dont_randomize_rooms, + spawnable_rooms=entry.spawnable_rooms, + ) + for entry in ENEMY_SPRITE_REQUIREMENTS + ) + + +def _load_overworld_enemy_metadata() -> dict[str, object]: + return { + "area_ids": AREA_IDS, + "do_not_randomize_area_ids": DO_NOT_RANDOMIZE_AREA_IDS, + "forced_group_requirements": tuple( + OverworldGroupRequirement( + group_id=requirement.group_id, + subgroup_0=requirement.subgroup_0, + subgroup_1=requirement.subgroup_1, + subgroup_2=requirement.subgroup_2, + subgroup_3=requirement.subgroup_3, + areas=requirement.areas, + ) + for requirement in FORCED_GROUP_REQUIREMENTS + ), + } + + +@dataclass(frozen=True) +class MergedRoomRequirement: + group_id: Optional[int] + subgroup_0: tuple[int, ...] + subgroup_1: tuple[int, ...] + subgroup_2: tuple[int, ...] + subgroup_3: tuple[int, ...] + + +def _merge_room_requirements(room_id: int, room_requirements: tuple[RoomGroupRequirement, ...]) -> MergedRoomRequirement: + group_id: Optional[int] = None + subgroup_0: list[int] = [] + subgroup_1: list[int] = [] + subgroup_2: list[int] = [] + subgroup_3: list[int] = [] + + for requirement in room_requirements: + if room_id not in requirement.rooms: + continue + if requirement.group_id is not None: + group_id = requirement.group_id + if requirement.subgroup_0 is not None: + subgroup_0.append(requirement.subgroup_0) + if requirement.subgroup_1 is not None: + subgroup_1.append(requirement.subgroup_1) + if requirement.subgroup_2 is not None: + subgroup_2.append(requirement.subgroup_2) + if requirement.subgroup_3 is not None: + subgroup_3.append(requirement.subgroup_3) + + return MergedRoomRequirement( + group_id=group_id, + subgroup_0=tuple(subgroup_0), + subgroup_1=tuple(subgroup_1), + subgroup_2=tuple(subgroup_2), + subgroup_3=tuple(subgroup_3), + ) + + +def get_room_do_not_update_requirements(state: EnemyShuffleState, room: DungeonEnemyRoom) -> tuple[EnemySpriteRequirement, ...]: + room_sprite_ids = {sprite.sprite_id for sprite in room.sprites} + return tuple( + requirement for requirement in state.sprite_requirements + if (requirement.do_not_randomize or room.room_id in requirement.dont_randomize_rooms) + and requirement.sprite_id in room_sprite_ids + and can_spawn_in_room(requirement, room) + ) + + +def get_possible_dungeon_sprite_groups(state: EnemyShuffleState, room: DungeonEnemyRoom) -> tuple[DungeonSpriteGroup, ...]: + do_not_update = get_room_do_not_update_requirements(state, room) + usable_groups = tuple( + group for group in state.sprite_groups.values() + if 0 < group.dungeon_group_id < 60 + and _get_possible_enemy_requirements_for_group(state, room, group) + ) + needs_key = any(sprite.has_key for sprite in room.sprites) + needs_killable = room.is_shutter_room + needs_water = room.is_water_room + room_requirements = _get_requirements_for_usable_dungeon_enemies(state) + water_requirements = tuple(requirement for requirement in room_requirements if requirement.is_water_sprite) + killable_requirements = tuple( + requirement for requirement in state.sprite_requirements + if _is_effectively_killable(requirement) and requirement.sprite_id != STAL_SPRITE_ID + ) + key_requirements = tuple(requirement for requirement in killable_requirements if not requirement.cannot_have_key) + + if ( + not needs_key and not needs_killable and not needs_water + and not do_not_update + and room.required_group_id is None + and not room.required_subgroup_0 + and not room.required_subgroup_1 + and not room.required_subgroup_2 + and not room.required_subgroup_3 + ): + return _get_unconstrained_possible_dungeon_sprite_groups(usable_groups, room_requirements, water_requirements) + + return tuple( + group for group in usable_groups + if ( + (not do_not_update or _build_requirement_group_matcher(do_not_update)(group)) + and _group_matches_room_requirement(group, room) + and ( + lambda possible_requirements: ( + (not needs_killable or any( + _is_effectively_killable(requirement) and requirement.sprite_id != STAL_SPRITE_ID + for requirement in _filter_requirements_for_room_water_state(room, possible_requirements) + )) + and (not needs_key or any( + _is_effectively_killable(requirement) + and not requirement.cannot_have_key + and requirement.sprite_id != STAL_SPRITE_ID + for requirement in _filter_requirements_for_room_water_state(room, possible_requirements) + )) + and (not needs_water or any( + requirement.is_water_sprite + for requirement in _filter_requirements_for_room_water_state(room, possible_requirements) + )) + ) + )(_get_possible_enemy_requirements_for_group(state, room, group)) + ) + ) + + +def can_spawn_in_room(requirement: EnemySpriteRequirement, room: DungeonEnemyRoom) -> bool: + return ( + room.room_id not in requirement.excluded_rooms + and (not requirement.spawnable_rooms or room.room_id in requirement.spawnable_rooms) + and (requirement.sprite_id != WALLMASTER_SPRITE_ID or room.room_id < 0x100) + ) + + +def _get_requirements_for_usable_dungeon_enemies(state: EnemyShuffleState) -> tuple[EnemySpriteRequirement, ...]: + return tuple( + requirement for requirement in state.sprite_requirements + if not requirement.npc + and requirement.is_enemy_sprite + and not requirement.boss + and not requirement.overlord + and not requirement.is_object + and not requirement.absorbable + and not requirement.never_use_dungeon + ) + + +def _get_requirements_for_usable_overworld_enemies(state: EnemyShuffleState) -> tuple[EnemySpriteRequirement, ...]: + return tuple( + requirement for requirement in state.sprite_requirements + if not requirement.npc + and requirement.is_enemy_sprite + and not requirement.boss + and not requirement.overlord + and not requirement.is_object + and not requirement.absorbable + and not requirement.never_use_overworld + ) + + +def _filter_requirements_for_room_water_state( + room: DungeonEnemyRoom, + requirements: tuple[EnemySpriteRequirement, ...], +) -> tuple[EnemySpriteRequirement, ...]: + if room.is_water_room: + return tuple(requirement for requirement in requirements if requirement.is_water_sprite) + return tuple(requirement for requirement in requirements if not requirement.is_water_sprite) + + +def _is_effectively_killable(requirement: EnemySpriteRequirement) -> bool: + return requirement.killable or requirement.sprite_id == WATER_TEKTITE_SPRITE_ID + + +def _get_effectively_killable_sprite_ids(requirements: tuple[EnemySpriteRequirement, ...]) -> set[int]: + return { + requirement.sprite_id for requirement in requirements + if _is_effectively_killable(requirement) and requirement.sprite_id != STAL_SPRITE_ID + } + + +def _get_unconstrained_possible_dungeon_sprite_groups( + usable_groups: tuple[DungeonSpriteGroup, ...], + room_requirements: tuple[EnemySpriteRequirement, ...], + water_requirements: tuple[EnemySpriteRequirement, ...], +) -> tuple[DungeonSpriteGroup, ...]: + water_subgroup_3 = set(_flatten_requirement_values(water_requirements, "subgroup_3")) + included_group_ids = set(_flatten_requirement_values(room_requirements, "group_ids")) + included_subgroup_0 = set(_flatten_requirement_values(room_requirements, "subgroup_0")) + included_subgroup_1 = set(_flatten_requirement_values(room_requirements, "subgroup_1")) + included_subgroup_2 = set(_flatten_requirement_values(room_requirements, "subgroup_2")) + included_subgroup_3 = { + subgroup for subgroup in _flatten_requirement_values(room_requirements, "subgroup_3") + if subgroup not in water_subgroup_3 and subgroup not in {54, 80} + } + + return tuple( + group for group in usable_groups + if group.group_id in included_group_ids + or group.subgroup_0 in included_subgroup_0 + or group.subgroup_1 in included_subgroup_1 + or group.subgroup_2 in included_subgroup_2 + or group.subgroup_3 in included_subgroup_3 + ) + + +def _build_requirement_group_matcher(requirements: tuple[EnemySpriteRequirement, ...]): + allowed_group_ids = set(_flatten_requirement_values(requirements, "group_ids")) + allowed_subgroup_0 = set(_flatten_requirement_values(requirements, "subgroup_0")) + allowed_subgroup_1 = set(_flatten_requirement_values(requirements, "subgroup_1")) + allowed_subgroup_2 = set(_flatten_requirement_values(requirements, "subgroup_2")) + allowed_subgroup_3 = set(_flatten_requirement_values(requirements, "subgroup_3")) + + def matches(group: DungeonSpriteGroup) -> bool: + return ( + not allowed_group_ids or group.group_id in allowed_group_ids + ) and ( + not allowed_subgroup_0 or group.subgroup_0 in allowed_subgroup_0 + ) and ( + not allowed_subgroup_1 or group.subgroup_1 in allowed_subgroup_1 + ) and ( + not allowed_subgroup_2 or group.subgroup_2 in allowed_subgroup_2 + ) and ( + not allowed_subgroup_3 or group.subgroup_3 in allowed_subgroup_3 + ) + + return matches + + +def _build_overworld_requirement_group_matcher(requirements: tuple[EnemySpriteRequirement, ...]): + allowed_group_ids = set(_flatten_requirement_values(requirements, "group_ids")) + allowed_subgroup_0 = set(_flatten_requirement_values(requirements, "subgroup_0")) + allowed_subgroup_1 = set(_flatten_requirement_values(requirements, "subgroup_1")) + allowed_subgroup_2 = set(_flatten_requirement_values(requirements, "subgroup_2")) + allowed_subgroup_3 = set(_flatten_requirement_values(requirements, "subgroup_3")) + + def matches(group: DungeonSpriteGroup) -> bool: + return ( + not allowed_group_ids or group.group_id in allowed_group_ids + ) and ( + not allowed_subgroup_0 or group.subgroup_0 in allowed_subgroup_0 + ) and ( + not allowed_subgroup_1 or group.subgroup_1 in allowed_subgroup_1 + ) and ( + not allowed_subgroup_2 or group.subgroup_2 in allowed_subgroup_2 + ) and ( + not allowed_subgroup_3 or group.subgroup_3 in allowed_subgroup_3 + ) + + return matches + + +def _build_requirement_group_presence_matcher(requirements: tuple[EnemySpriteRequirement, ...]): + allowed_group_ids = set(_flatten_requirement_values(requirements, "group_ids")) + allowed_subgroup_0 = set(_flatten_requirement_values(requirements, "subgroup_0")) + allowed_subgroup_1 = set(_flatten_requirement_values(requirements, "subgroup_1")) + allowed_subgroup_2 = set(_flatten_requirement_values(requirements, "subgroup_2")) + allowed_subgroup_3 = set(_flatten_requirement_values(requirements, "subgroup_3")) + + def matches(group: DungeonSpriteGroup) -> bool: + return ( + group.group_id in allowed_group_ids + or group.subgroup_0 in allowed_subgroup_0 + or group.subgroup_1 in allowed_subgroup_1 + or group.subgroup_2 in allowed_subgroup_2 + or group.subgroup_3 in allowed_subgroup_3 + ) + + return matches + + +def _flatten_requirement_values(requirements: tuple[EnemySpriteRequirement, ...], attribute: str) -> tuple[int, ...]: + return tuple( + value + for requirement in requirements + for value in getattr(requirement, attribute) + ) + + +def _group_matches_room_requirement(group: DungeonSpriteGroup, room: DungeonEnemyRoom) -> bool: + return ( + (room.required_group_id is None or room.required_group_id == group.dungeon_group_id) + and (not room.required_subgroup_0 or group.subgroup_0 in room.required_subgroup_0) + and (not room.required_subgroup_1 or group.subgroup_1 in room.required_subgroup_1) + and (not room.required_subgroup_2 or group.subgroup_2 in room.required_subgroup_2) + and (not room.required_subgroup_3 or group.subgroup_3 in room.required_subgroup_3) + ) + + +def get_overworld_do_not_update_requirements( + state: EnemyShuffleState, + area: OverworldEnemyArea, +) -> tuple[EnemySpriteRequirement, ...]: + area_sprite_ids = {sprite.sprite_id for sprite in area.sprites} + return tuple( + requirement for requirement in state.sprite_requirements + if requirement.do_not_randomize and requirement.sprite_id in area_sprite_ids + ) + + +def get_possible_overworld_sprite_groups( + state: EnemyShuffleState, + area: OverworldEnemyArea, +) -> tuple[DungeonSpriteGroup, ...]: + usable_groups = tuple( + group for group in state.sprite_groups.values() + if 0 < group.group_id < 0x40 + and _get_possible_enemy_requirements_for_overworld_group(state, group) + ) + do_not_update = get_overworld_do_not_update_requirements(state, area) + if not do_not_update: + return usable_groups + + do_not_update_matcher = _build_overworld_requirement_group_matcher(do_not_update) + return tuple(group for group in usable_groups if do_not_update_matcher(group)) + + +def _get_possible_enemy_requirements_for_group( + state: EnemyShuffleState, + room: DungeonEnemyRoom, + group: DungeonSpriteGroup, +) -> tuple[EnemySpriteRequirement, ...]: + dungeon_requirements = _get_requirements_for_usable_dungeon_enemies(state) + return tuple( + requirement for requirement in dungeon_requirements + if can_spawn_in_room(requirement, room) + and ( + not requirement.group_ids or group.dungeon_group_id in requirement.group_ids + ) + and (not requirement.subgroup_0 or group.subgroup_0 in requirement.subgroup_0) + and (not requirement.subgroup_1 or group.subgroup_1 in requirement.subgroup_1) + and (not requirement.subgroup_2 or group.subgroup_2 in requirement.subgroup_2) + and (not requirement.subgroup_3 or group.subgroup_3 in requirement.subgroup_3) + ) + + +def _get_randomizable_sprites_in_room( + state: EnemyShuffleState, + room: DungeonEnemyRoom, +) -> tuple[DungeonEnemySprite, ...]: + randomizable_sprite_ids = { + requirement.sprite_id for requirement in state.sprite_requirements + if not requirement.do_not_randomize and room.room_id not in requirement.dont_randomize_rooms + } + return tuple(sprite for sprite in room.sprites if sprite.sprite_id in randomizable_sprite_ids) + + +def _get_possible_enemy_requirements_for_overworld_group( + state: EnemyShuffleState, + group: DungeonSpriteGroup, +) -> tuple[EnemySpriteRequirement, ...]: + overworld_requirements = _get_requirements_for_usable_overworld_enemies(state) + return tuple( + requirement for requirement in overworld_requirements + if ( + not requirement.group_ids or group.group_id in requirement.group_ids + ) + and (not requirement.subgroup_0 or group.subgroup_0 in requirement.subgroup_0) + and (not requirement.subgroup_1 or group.subgroup_1 in requirement.subgroup_1) + and (not requirement.subgroup_2 or group.subgroup_2 in requirement.subgroup_2) + and (not requirement.subgroup_3 or group.subgroup_3 in requirement.subgroup_3) + ) + + +def _get_randomizable_sprites_in_overworld_area( + state: EnemyShuffleState, + area: OverworldEnemyArea, +) -> tuple[OverworldEnemySprite, ...]: + randomizable_sprite_ids = { + requirement.sprite_id for requirement in state.sprite_requirements + if not requirement.do_not_randomize + } + return tuple(sprite for sprite in area.sprites if sprite.sprite_id in randomizable_sprite_ids) + + +def _randomize_dungeon_rooms( + world: "ALTTPWorld", + dungeon_rooms: dict[int, DungeonEnemyRoom], + sprite_groups: dict[int, DungeonSpriteGroup], + sprite_requirements: tuple[EnemySpriteRequirement, ...], +) -> dict[int, RandomizedDungeonEnemyRoom]: + state = EnemyShuffleState( + dungeon_rooms=dungeon_rooms, + overworld_areas={}, + sprite_groups=sprite_groups, + sprite_requirements=sprite_requirements, + room_group_requirements=tuple(), + overworld_group_requirements=tuple(), + shutter_room_ids=frozenset(room.room_id for room in dungeon_rooms.values() if room.is_shutter_room), + water_room_ids=frozenset(room.room_id for room in dungeon_rooms.values() if room.is_water_room), + dont_randomize_room_ids=frozenset(room.room_id for room in dungeon_rooms.values() if room.do_not_randomize), + no_special_enemies_standard_room_ids=frozenset( + room.room_id for room in dungeon_rooms.values() if room.no_special_enemies_standard + ), + boss_room_ids=frozenset(), + dont_randomize_overworld_area_ids=frozenset(), + randomized_dungeon_rooms={}, + randomized_overworld_areas={}, + ) + randomized_rooms: dict[int, RandomizedDungeonEnemyRoom] = {} + + for room_id in sorted(dungeon_rooms): + room = dungeon_rooms[room_id] + skip_randomization = room.do_not_randomize or ( + world.options.mode == "standard" and room.no_special_enemies_standard + ) + + selected_group = sprite_groups.get(room.graphics_block_id + 0x40) + if not skip_randomization: + possible_groups = get_possible_dungeon_sprite_groups(state, room) + if possible_groups: + selected_group = world.random.choice(possible_groups) + + if selected_group is None: + selected_group = sprite_groups[room.graphics_block_id + 0x40] + + randomized_rooms[room_id] = _randomize_room_sprites( + world, + state, + room, + selected_group, + skip_randomization, + ) + + return randomized_rooms + + +def _randomize_overworld_areas( + world: "ALTTPWorld", + overworld_areas: dict[int, OverworldEnemyArea], + sprite_groups: dict[int, DungeonSpriteGroup], + sprite_requirements: tuple[EnemySpriteRequirement, ...], + forced_group_requirements: tuple[OverworldGroupRequirement, ...], +) -> dict[int, RandomizedOverworldEnemyArea]: + state = EnemyShuffleState( + dungeon_rooms={}, + overworld_areas=overworld_areas, + sprite_groups=sprite_groups, + sprite_requirements=sprite_requirements, + room_group_requirements=tuple(), + overworld_group_requirements=forced_group_requirements, + shutter_room_ids=frozenset(), + water_room_ids=frozenset(), + dont_randomize_room_ids=frozenset(), + no_special_enemies_standard_room_ids=frozenset(), + boss_room_ids=frozenset(), + dont_randomize_overworld_area_ids=frozenset(area.area_id for area in overworld_areas.values() if area.do_not_randomize), + randomized_dungeon_rooms={}, + randomized_overworld_areas={}, + ) + randomized_areas: dict[int, RandomizedOverworldEnemyArea] = {} + + for area_id in sorted(overworld_areas): + area = overworld_areas[area_id] + selected_group = sprite_groups.get(area.graphics_block_id) + if not area.do_not_randomize: + possible_groups = get_possible_overworld_sprite_groups(state, area) + if possible_groups: + selected_group = world.random.choice(possible_groups) + + forced_group = _get_forced_overworld_group(area.area_id, forced_group_requirements, sprite_groups) + if forced_group is not None: + selected_group = forced_group + if selected_group is None: + selected_group = sprite_groups[area.graphics_block_id] + + randomized_areas[area_id] = _randomize_overworld_area_sprites( + world, + state, + area, + selected_group, + area.do_not_randomize, + ) + + return randomized_areas + + +def _get_forced_overworld_group( + area_id: int, + forced_group_requirements: tuple[OverworldGroupRequirement, ...], + sprite_groups: dict[int, DungeonSpriteGroup], +) -> Optional[DungeonSpriteGroup]: + for requirement in forced_group_requirements: + if area_id not in requirement.areas or requirement.group_id is None: + continue + return sprite_groups.get(requirement.group_id) + return None + + +def _randomize_room_sprites( + world: "ALTTPWorld", + state: EnemyShuffleState, + room: DungeonEnemyRoom, + selected_group: DungeonSpriteGroup, + skip_randomization: bool, +) -> RandomizedDungeonEnemyRoom: + randomized_sprites = list(_clone_room_sprites(room)) + + if not skip_randomization: + possible_requirements = _get_possible_enemy_requirements_for_group(state, room, selected_group) + sprites_to_update = _get_randomizable_sprites_in_room(state, room) + sprites_to_update_addresses = {sprite.address for sprite in sprites_to_update} + + if possible_requirements: + water_sprite_ids = [ + requirement.sprite_id for requirement in possible_requirements + if requirement.is_water_sprite + ] + + if room.is_water_room: + if water_sprite_ids: + replacement_water_sprite_ids = water_sprite_ids + if room.is_shutter_room: + killable_water_sprite_ids = [ + requirement.sprite_id for requirement in possible_requirements + if requirement.is_water_sprite + and _is_effectively_killable(requirement) + and requirement.sprite_id != STAL_SPRITE_ID + ] + if killable_water_sprite_ids: + replacement_water_sprite_ids = killable_water_sprite_ids + for sprite in randomized_sprites: + if sprite.address in sprites_to_update_addresses: + _set_randomized_sprite_id( + randomized_sprites, + sprite.address, + world.random.choice(replacement_water_sprite_ids), + ) + return _build_randomized_room(room, selected_group, randomized_sprites, False) + + non_water_requirements = _filter_requirements_for_room_water_state(room, possible_requirements) + possible_sprite_ids = [requirement.sprite_id for requirement in non_water_requirements] + if not possible_sprite_ids: + return _build_randomized_room(room, selected_group, randomized_sprites, False) + killable_sprite_ids = [ + requirement.sprite_id for requirement in non_water_requirements + if _is_effectively_killable(requirement) and requirement.sprite_id != STAL_SPRITE_ID + ] + killable_key_sprite_ids = [ + requirement.sprite_id for requirement in non_water_requirements + if _is_effectively_killable(requirement) and not requirement.cannot_have_key and requirement.sprite_id != STAL_SPRITE_ID + ] + stal_count = 0 + + for sprite in sprites_to_update: + replacement_sprite_id: int + if sprite.has_key and killable_key_sprite_ids: + replacement_sprite_id = world.random.choice(killable_key_sprite_ids) + elif room.is_shutter_room and killable_sprite_ids: + replacement_sprite_id = world.random.choice(killable_sprite_ids) + elif not room.is_shutter_room and world.random.randrange(100) < 5: + replacement_sprite_id = STAL_SPRITE_ID + else: + replacement_sprite_id = world.random.choice(possible_sprite_ids) + + _set_randomized_sprite_id(randomized_sprites, sprite.address, replacement_sprite_id) + + if replacement_sprite_id == STAL_SPRITE_ID: + stal_count += 1 + if stal_count > 2: + possible_sprite_ids = [sprite_id for sprite_id in possible_sprite_ids if sprite_id != STAL_SPRITE_ID] + + return _build_randomized_room(room, selected_group, randomized_sprites, skip_randomization) + + +def _randomize_overworld_area_sprites( + world: "ALTTPWorld", + state: EnemyShuffleState, + area: OverworldEnemyArea, + selected_group: DungeonSpriteGroup, + skip_randomization: bool, +) -> RandomizedOverworldEnemyArea: + randomized_sprites = list(_clone_overworld_area_sprites(area)) + bush_sprite_id = area.bush_sprite_id + + if not skip_randomization: + possible_requirements = _get_possible_enemy_requirements_for_overworld_group(state, selected_group) + possible_sprite_ids = [requirement.sprite_id for requirement in possible_requirements] + sprites_to_update = _get_randomizable_sprites_in_overworld_area(state, area) + sprites_to_update_addresses = {sprite.address for sprite in sprites_to_update} + + if possible_sprite_ids: + for sprite in sprites_to_update: + _set_randomized_overworld_sprite_id( + randomized_sprites, + sprite.address, + world.random.choice(possible_sprite_ids), + ) + + flopping_fish_addresses = [ + sprite.address for sprite in randomized_sprites + if sprite.address in sprites_to_update_addresses and sprite.sprite_id == FLOPPING_FISH_SPRITE_ID + ] + if len(flopping_fish_addresses) > 1: + non_fish_sprite_ids = [ + sprite_id for sprite_id in possible_sprite_ids if sprite_id != FLOPPING_FISH_SPRITE_ID + ] + for address in flopping_fish_addresses[1:]: + if non_fish_sprite_ids: + _set_randomized_overworld_sprite_id( + randomized_sprites, + address, + world.random.choice(non_fish_sprite_ids), + ) + + bush_candidates = [ + requirement.sprite_id for requirement in possible_requirements + if not requirement.overlord + ] + if bush_candidates: + bush_sprite_id = world.random.choice(bush_candidates) + + return RandomizedOverworldEnemyArea( + area_id=area.area_id, + sprite_table_address=area.sprite_table_address, + graphics_block_address=area.graphics_block_address, + original_graphics_block_id=area.graphics_block_id, + graphics_block_id=selected_group.group_id, + original_bush_sprite_id=area.bush_sprite_id, + bush_sprite_id=bush_sprite_id, + sprites=tuple(randomized_sprites), + skipped_randomization=skip_randomization, + ) + + +def _clone_room_sprites(room: DungeonEnemyRoom) -> list[RandomizedDungeonEnemySprite]: + return [ + RandomizedDungeonEnemySprite( + address=sprite.address, + byte_0=sprite.byte_0, + byte_1=sprite.byte_1, + original_sprite_id=sprite.sprite_id, + sprite_id=sprite.sprite_id, + is_overlord=sprite.is_overlord, + has_key=sprite.has_key, + ) + for sprite in room.sprites + ] + + +def _clone_overworld_area_sprites(area: OverworldEnemyArea) -> list[RandomizedOverworldEnemySprite]: + return [ + RandomizedOverworldEnemySprite( + address=sprite.address, + y_coord=sprite.y_coord, + x_coord=sprite.x_coord, + original_sprite_id=sprite.sprite_id, + sprite_id=sprite.sprite_id, + ) + for sprite in area.sprites + ] + + +def _set_randomized_sprite_id( + randomized_sprites: list[RandomizedDungeonEnemySprite], + address: int, + sprite_id: int, +) -> None: + for index, sprite in enumerate(randomized_sprites): + if sprite.address != address: + continue + randomized_sprites[index] = RandomizedDungeonEnemySprite( + address=sprite.address, + byte_0=sprite.byte_0, + byte_1=sprite.byte_1, + original_sprite_id=sprite.original_sprite_id, + sprite_id=sprite_id, + is_overlord=sprite.is_overlord, + has_key=sprite.has_key, + ) + return + + +def _set_randomized_overworld_sprite_id( + randomized_sprites: list[RandomizedOverworldEnemySprite], + address: int, + sprite_id: int, +) -> None: + for index, sprite in enumerate(randomized_sprites): + if sprite.address != address: + continue + randomized_sprites[index] = RandomizedOverworldEnemySprite( + address=sprite.address, + y_coord=sprite.y_coord, + x_coord=sprite.x_coord, + original_sprite_id=sprite.original_sprite_id, + sprite_id=sprite_id, + ) + return + + +def _build_randomized_room( + room: DungeonEnemyRoom, + selected_group: DungeonSpriteGroup, + sprites: list[RandomizedDungeonEnemySprite], + skipped_randomization: bool, +) -> RandomizedDungeonEnemyRoom: + return RandomizedDungeonEnemyRoom( + room_id=room.room_id, + room_header_address=room.room_header_address, + sprite_table_address=room.sprite_table_address, + original_graphics_block_id=room.graphics_block_id, + graphics_block_id=selected_group.dungeon_group_id, + tag_1=room.tag_1, + tag_2=room.tag_2, + sort_sprites_value=room.sort_sprites_value, + sprites=tuple(sprites), + skipped_randomization=skipped_randomization, + ) + + +def validate_enemy_shuffle_state(state: EnemyShuffleState, is_standard_mode: bool) -> None: + for room_id, room in state.dungeon_rooms.items(): + randomized_room = state.randomized_dungeon_rooms[room_id] + _validate_dungeon_room(state, room, randomized_room, is_standard_mode) + + for area_id, area in state.overworld_areas.items(): + randomized_area = state.randomized_overworld_areas[area_id] + _validate_overworld_area(state, area, randomized_area) + + +def _validate_dungeon_room( + state: EnemyShuffleState, + room: DungeonEnemyRoom, + randomized_room: RandomizedDungeonEnemyRoom, + is_standard_mode: bool, +) -> None: + selected_group = state.sprite_groups.get(randomized_room.graphics_block_id + 0x40) + if selected_group is None: + raise ValueError(f"Enemy shuffle produced unknown dungeon sprite group {randomized_room.graphics_block_id} for room {room.room_id}") + + skipped = room.do_not_randomize or (is_standard_mode and room.no_special_enemies_standard) + if skipped and randomized_room.graphics_block_id != room.graphics_block_id: + raise ValueError(f"Enemy shuffle changed skipped room {room.room_id} graphics block") + + if not skipped: + possible_groups = get_possible_dungeon_sprite_groups(state, room) + if possible_groups and selected_group not in possible_groups: + raise ValueError(f"Enemy shuffle selected illegal sprite group {selected_group.group_id} for room {room.room_id}") + + possible_requirements = _get_possible_enemy_requirements_for_group(state, room, selected_group) + possible_sprite_ids = {requirement.sprite_id for requirement in possible_requirements} + killable_sprite_ids = _get_effectively_killable_sprite_ids(possible_requirements) + killable_key_sprite_ids = { + requirement.sprite_id for requirement in possible_requirements + if _is_effectively_killable(requirement) and not requirement.cannot_have_key and requirement.sprite_id != STAL_SPRITE_ID + } + water_sprite_ids = { + requirement.sprite_id for requirement in possible_requirements + if requirement.is_water_sprite + } + if not room.is_water_room: + possible_sprite_ids -= water_sprite_ids + killable_sprite_ids -= water_sprite_ids + killable_key_sprite_ids -= water_sprite_ids + do_not_randomize_sprite_ids = { + requirement.sprite_id for requirement in state.sprite_requirements + if requirement.do_not_randomize or room.room_id in requirement.dont_randomize_rooms + } + randomized_by_address = {sprite.address: sprite for sprite in randomized_room.sprites} + + for original_sprite in room.sprites: + randomized_sprite = randomized_by_address[original_sprite.address] + if original_sprite.sprite_id in do_not_randomize_sprite_ids and randomized_sprite.sprite_id != original_sprite.sprite_id: + raise ValueError(f"Enemy shuffle changed do-not-randomize sprite in room {room.room_id} at {hex(original_sprite.address)}") + + if original_sprite.sprite_id in do_not_randomize_sprite_ids or skipped: + continue + + if room.is_water_room: + if randomized_sprite.sprite_id not in water_sprite_ids: + raise ValueError(f"Enemy shuffle placed non-water enemy {hex(randomized_sprite.sprite_id)} in water room {room.room_id}") + continue + if randomized_sprite.sprite_id in water_sprite_ids: + raise ValueError(f"Enemy shuffle placed water enemy {hex(randomized_sprite.sprite_id)} in non-water room {room.room_id}") + + if original_sprite.has_key: + if randomized_sprite.sprite_id not in killable_key_sprite_ids: + raise ValueError(f"Enemy shuffle placed invalid key enemy {hex(randomized_sprite.sprite_id)} in room {room.room_id}") + continue + + if room.is_shutter_room and randomized_sprite.sprite_id not in killable_sprite_ids: + raise ValueError(f"Enemy shuffle placed non-killable shutter enemy {hex(randomized_sprite.sprite_id)} in room {room.room_id}") + + if randomized_sprite.sprite_id != STAL_SPRITE_ID and randomized_sprite.sprite_id not in possible_sprite_ids: + raise ValueError(f"Enemy shuffle placed illegal sprite {hex(randomized_sprite.sprite_id)} in room {room.room_id}") + + if room.is_shutter_room and _get_randomizable_sprites_in_room(state, room): + all_killable_sprite_ids = _get_effectively_killable_sprite_ids( + _filter_requirements_for_room_water_state(room, state.sprite_requirements) + ) + randomized_sprite_ids = {sprite.sprite_id for sprite in randomized_room.sprites} + if not (randomized_sprite_ids & all_killable_sprite_ids): + raise ValueError(f"Enemy shuffle left shutter room {room.room_id} without any killable enemies") + + +def _validate_overworld_area( + state: EnemyShuffleState, + area: OverworldEnemyArea, + randomized_area: RandomizedOverworldEnemyArea, +) -> None: + selected_group = state.sprite_groups.get(randomized_area.graphics_block_id) + if selected_group is None: + raise ValueError(f"Enemy shuffle produced unknown overworld sprite group {randomized_area.graphics_block_id} for area {hex(area.area_id)}") + + if area.do_not_randomize and randomized_area.graphics_block_id != area.graphics_block_id: + raise ValueError(f"Enemy shuffle changed skipped overworld area {hex(area.area_id)} graphics block") + + forced_group = _get_forced_overworld_group(area.area_id, state.overworld_group_requirements, state.sprite_groups) + if forced_group is not None and randomized_area.graphics_block_id != forced_group.group_id: + raise ValueError(f"Enemy shuffle failed forced overworld group for area {hex(area.area_id)}") + + if not area.do_not_randomize and forced_group is None: + possible_groups = get_possible_overworld_sprite_groups(state, area) + if possible_groups and selected_group not in possible_groups: + raise ValueError(f"Enemy shuffle selected illegal overworld group {selected_group.group_id} for area {hex(area.area_id)}") + + possible_requirements = _get_possible_enemy_requirements_for_overworld_group(state, selected_group) + possible_sprite_ids = {requirement.sprite_id for requirement in possible_requirements} + bush_sprite_ids = { + requirement.sprite_id for requirement in possible_requirements + if not requirement.overlord + } + known_sprite_ids = {requirement.sprite_id for requirement in state.sprite_requirements} + do_not_randomize_sprite_ids = { + requirement.sprite_id for requirement in state.sprite_requirements + if requirement.do_not_randomize + } + randomized_by_address = {sprite.address: sprite for sprite in randomized_area.sprites} + + for original_sprite in area.sprites: + randomized_sprite = randomized_by_address[original_sprite.address] + if original_sprite.sprite_id not in known_sprite_ids: + continue + if original_sprite.sprite_id in do_not_randomize_sprite_ids and randomized_sprite.sprite_id != original_sprite.sprite_id: + raise ValueError(f"Enemy shuffle changed do-not-randomize overworld sprite in area {hex(area.area_id)} at {hex(original_sprite.address)}") + if original_sprite.sprite_id in do_not_randomize_sprite_ids or area.do_not_randomize: + continue + if randomized_sprite.sprite_id not in possible_sprite_ids: + raise ValueError(f"Enemy shuffle placed illegal overworld sprite {hex(randomized_sprite.sprite_id)} in area {hex(area.area_id)}") + + randomizable_addresses = {sprite.address for sprite in _get_randomizable_sprites_in_overworld_area(state, area)} + non_fish_sprite_ids = possible_sprite_ids - {FLOPPING_FISH_SPRITE_ID} + if non_fish_sprite_ids and sum( + 1 for sprite in randomized_area.sprites + if sprite.address in randomizable_addresses and sprite.sprite_id == FLOPPING_FISH_SPRITE_ID + ) > 1: + raise ValueError(f"Enemy shuffle placed multiple flopping fish in area {hex(area.area_id)}") + + if area.do_not_randomize and randomized_area.bush_sprite_id != area.bush_sprite_id: + raise ValueError(f"Enemy shuffle changed skipped overworld bush sprite in area {hex(area.area_id)}") + if not area.do_not_randomize and bush_sprite_ids and randomized_area.bush_sprite_id not in bush_sprite_ids: + raise ValueError(f"Enemy shuffle placed illegal bush enemy {hex(randomized_area.bush_sprite_id)} in area {hex(area.area_id)}") + + +def apply_enemy_shuffle(rom: "LocalRom", state: EnemyShuffleState) -> None: + for group in state.sprite_groups.values(): + _write_sprite_group(rom, group) + + for room in state.randomized_dungeon_rooms.values(): + rom.write_byte(room.room_header_address + 3, room.graphics_block_id) + for sprite in room.sprites: + _write_dungeon_sprite(rom, sprite) + + rom.write_byte(0x04CF4F, 0x10) + for area in state.randomized_overworld_areas.values(): + rom.write_byte(area.graphics_block_address, area.graphics_block_id) + for sprite in area.sprites: + _write_overworld_sprite(rom, sprite) + + bush_spawn_table_address = _get_enemizer_symbol("sprite_bush_spawn_table_overworld") + for area in state.randomized_overworld_areas.values(): + rom.write_byte(bush_spawn_table_address + area.area_id, area.bush_sprite_id) + + +def _write_sprite_group(rom: "LocalRom", group: DungeonSpriteGroup) -> None: + address = SPRITE_GROUP_BASE_ADDRESS + (group.group_id * 4) + rom.write_byte(address, group.subgroup_0) + rom.write_byte(address + 1, group.subgroup_1) + rom.write_byte(address + 2, group.subgroup_2) + rom.write_byte(address + 3, group.subgroup_3) + + +def _write_dungeon_sprite(rom: "LocalRom", sprite: RandomizedDungeonEnemySprite) -> None: + sprite_id = sprite.sprite_id + byte_1 = sprite.byte_1 + + if sprite_id == WALLMASTER_SPRITE_ID: + sprite_id = 0x09 + byte_1 |= SPRITE_OVERLORD_MASK + + rom.write_byte(sprite.address, sprite.byte_0) + rom.write_byte(sprite.address + 1, byte_1) + rom.write_byte(sprite.address + 2, sprite_id & 0xFF) + + +def _write_overworld_sprite(rom: "LocalRom", sprite: RandomizedOverworldEnemySprite) -> None: + sprite_id = sprite.sprite_id + if sprite_id == OW_FALLING_ROCKS_SPRITE_ID: + rom.write_byte(sprite.address, 0) + rom.write_byte(sprite.address + 1, 0) + if sprite_id == WALLMASTER_SPRITE_ID: + sprite_id = OW_WALLMASTER_TO_HOULIHAN_SPRITE_ID + rom.write_byte(sprite.address + 2, sprite_id & 0xFF) diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index c7dc7a694848..5b9d443b5dcd 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -9,6 +9,7 @@ from .Shops import TakeAny, total_shop_slots, set_up_shops, shop_table_by_location, ShopType from .Bosses import place_bosses from .Dungeons import get_dungeon_item_pool_player +from .EnemyShuffle import generate_enemy_shuffle_state from .EntranceShuffle import connect_entrance from .Items import item_factory, GetBeemizerItem, trap_replaceable, item_name_groups from .Options import small_key_shuffle, compass_shuffle, big_key_shuffle, map_shuffle, TriforcePiecesMode, LTTPBosses @@ -511,6 +512,8 @@ def cut_item(items, item_to_cut, minimum_items): world.options.turtle_rock_medallion.current_key.title()) place_bosses(world) + if world.options.enemy_shuffle: + world.enemy_shuffle_state = generate_enemy_shuffle_state(world) multiworld.itempool += items diff --git a/worlds/alttp/PotShuffle.py b/worlds/alttp/PotShuffle.py new file mode 100644 index 000000000000..f1f8825983a2 --- /dev/null +++ b/worlds/alttp/PotShuffle.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from Utils import snes_to_pc +from .enemizer_data.pot_shuffle_data import POT_ROOMS + +if TYPE_CHECKING: + from . import ALTTPWorld + from .Rom import LocalRom + + +POT_ITEM_POINTER_TABLE = 0xDB67 +POT_KEY = 0x08 +POT_ARROW = 0x09 +POT_BLUE_RUPEE = 0x07 +POT_SWITCH = 0x88 +POT_HOLE = 0x80 + + +@dataclass(frozen=True) +class PotData: + x: int + y: int + reserved: int + + +@dataclass(frozen=True) +class PotRoomData: + room_id: int + pots: tuple[PotData, ...] + items: tuple[int, ...] + + +@dataclass(frozen=True) +class FilledPot: + x: int + y: int + item: int + + +def generate_pot_shuffle(world: "ALTTPWorld") -> dict[int, tuple[FilledPot, ...]]: + room_data = _load_pot_room_data() + shuffled_pots: dict[int, tuple[FilledPot, ...]] = {} + + for room in room_data: + room_items = [item for item in room.items if item != POT_HOLE] + if world.options.retro_bow: + room_items = [POT_BLUE_RUPEE if item == POT_ARROW else item for item in room_items] + + empty_pots: list[PotData] = [] + filled_pots: list[FilledPot] = [] + + for pot in room.pots: + if pot.reserved == 3: + filled_pots.append(FilledPot(pot.x, pot.y, POT_HOLE)) + else: + empty_pots.append(pot) + + while POT_KEY in room_items: + candidate_indices = [index for index, pot in enumerate(empty_pots) if pot.reserved == 1] + if not candidate_indices: + break + pot_index = world.random.choice(candidate_indices) + pot = empty_pots.pop(pot_index) + room_items.remove(POT_KEY) + filled_pots.append(FilledPot(pot.x, pot.y, POT_KEY)) + + while POT_SWITCH in room_items: + candidate_indices = [index for index, pot in enumerate(empty_pots) if pot.reserved == 2] + if not candidate_indices: + break + pot_index = world.random.choice(candidate_indices) + pot = empty_pots.pop(pot_index) + room_items.remove(POT_SWITCH) + filled_pots.append(FilledPot(pot.x, pot.y, POT_SWITCH)) + + while room_items and empty_pots: + pot_index = world.random.randrange(len(empty_pots)) + item_index = world.random.randrange(len(room_items)) + pot = empty_pots.pop(pot_index) + item = room_items.pop(item_index) + filled_pots.append(FilledPot(pot.x, pot.y, item)) + + shuffled_pots[room.room_id] = tuple(filled_pots) + + return shuffled_pots + + +def apply_pot_shuffle(rom: "LocalRom", shuffled_pots: dict[int, tuple[FilledPot, ...]]) -> None: + for room_id, pots in shuffled_pots.items(): + pointer_address = POT_ITEM_POINTER_TABLE + (room_id * 2) + snes_address = rom.read_byte(pointer_address) | (rom.read_byte(pointer_address + 1) << 8) | (0x01 << 16) + address = snes_to_pc(snes_address) + for index, pot in enumerate(pots): + rom.write_bytes(address + (index * 3), (pot.x, pot.y, pot.item)) + + +def get_unique_pot_item_position( + shuffled_pots: dict[int, tuple[FilledPot, ...]], + room_id: int, + item: int, +) -> tuple[int, int]: + positions = [ + (pot.x, pot.y) + for pot in shuffled_pots.get(room_id, ()) + if pot.item == item + ] + if len(positions) != 1: + raise ValueError( + f"Expected exactly one pot item {hex(item)} in room {hex(room_id)}, found {len(positions)}" + ) + return positions[0] + + +def _load_pot_room_data() -> tuple[PotRoomData, ...]: + return tuple( + PotRoomData( + room_id=room.room_id, + pots=tuple(PotData(x=pot.x, y=pot.y, reserved=pot.reserved) for pot in room.pots), + items=room.items, + ) + for room in POT_ROOMS + ) diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index c58083b5da14..78cb33993d92 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -15,7 +15,6 @@ import os import random import struct -import subprocess import threading import concurrent.futures import bsdiff4 @@ -53,9 +52,6 @@ except: xxtea = None -enemizer_logger = logging.getLogger("Enemizer") - - class LocalRom: def __init__(self, file, patch=True, vanillaRom=None, name=None, hash=None): @@ -179,43 +175,6 @@ def write_int32s(self, startaddress: int, values): self.write_int32(startaddress + (i * 4), value) -check_lock = threading.Lock() - - -def check_enemizer(enemizercli): - if getattr(check_enemizer, "done", None): - return - if not os.path.exists(enemizercli) and not os.path.exists(enemizercli + ".exe"): - raise Exception(f"Enemizer not found at {enemizercli}, please install it. " - f"Such as https://github.com/Ijwu/Enemizer/releases") - - with check_lock: - # some time may have passed since the lock was acquired, as such a quick re-check doesn't hurt - if getattr(check_enemizer, "done", None): - return - wanted_version = (7, 1, 0) - # version info is saved on the lib, for some reason - library_info = os.path.join(os.path.dirname(enemizercli), "EnemizerCLI.Core.deps.json") - with open(library_info) as f: - info = json.load(f) - - for lib in info["libraries"]: - if lib.startswith("EnemizerLibrary/"): - version = lib.split("/")[-1] - version = tuple(int(element) for element in version.split(".")) - enemizer_logger.debug(f"Found Enemizer version {version}") - if version < wanted_version: - raise Exception( - f"Enemizer found at {enemizercli} is outdated ({version}) < ({wanted_version}), " - f"please update your Enemizer. " - f"Such as from https://github.com/Ijwu/Enemizer/releases") - break - else: - raise Exception(f"Could not find Enemizer library version information in {library_info}") - - check_enemizer.done = True - - def apply_random_sprite_on_event(rom: LocalRom, sprite, local_random, allow_random_on_event, sprite_pool): userandomsprites = False if sprite and not isinstance(sprite, Sprite): @@ -282,174 +241,6 @@ def apply_random_sprite_on_event(rom: LocalRom, sprite, local_random, allow_rand rom.write_bytes(0x307000 + (i * 0x8000), sprite.palette) rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette) - -def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory): - player = world.player - check_enemizer(enemizercli) - randopatch_path = os.path.abspath(os.path.join(output_directory, f'enemizer_randopatch_{player}.sfc')) - options_path = os.path.abspath(os.path.join(output_directory, f'enemizer_options_{player}.json')) - enemizer_output_path = os.path.abspath(os.path.join(output_directory, f'enemizer_output_{player}.sfc')) - - # write options file for enemizer - options = { - 'RandomizeEnemies': world.options.enemy_shuffle.value, - 'RandomizeEnemiesType': 3, - 'RandomizeBushEnemyChance': world.options.bush_shuffle.value, - 'RandomizeEnemyHealthRange': world.options.enemy_health != 'default', - 'RandomizeEnemyHealthType': {'default': 0, 'easy': 0, 'normal': 1, 'hard': 2, 'expert': 3}[ - world.options.enemy_health.current_key], - 'OHKO': False, - 'RandomizeEnemyDamage': world.options.enemy_damage != 'default', - 'AllowEnemyZeroDamage': True, - 'ShuffleEnemyDamageGroups': world.options.enemy_damage != 'default', - 'EnemyDamageChaosMode': world.options.enemy_damage == 'chaos', - 'EasyModeEscape': world.options.mode == "standard", - 'EnemiesAbsorbable': False, - 'AbsorbableSpawnRate': 10, - 'AbsorbableTypes': { - 'FullMagic': True, 'SmallMagic': True, 'Bomb_1': True, 'BlueRupee': True, 'Heart': True, 'BigKey': True, - 'Key': True, - 'Fairy': True, 'Arrow_10': True, 'Arrow_5': True, 'Bomb_8': True, 'Bomb_4': True, 'GreenRupee': True, - 'RedRupee': True - }, - 'BossMadness': False, - 'RandomizeBosses': True, - 'RandomizeBossesType': 0, - 'RandomizeBossHealth': False, - 'RandomizeBossHealthMinAmount': 0, - 'RandomizeBossHealthMaxAmount': 300, - 'RandomizeBossDamage': False, - 'RandomizeBossDamageMinAmount': 0, - 'RandomizeBossDamageMaxAmount': 200, - 'RandomizeBossBehavior': False, - 'RandomizeDungeonPalettes': False, - 'SetBlackoutMode': False, - 'RandomizeOverworldPalettes': False, - 'RandomizeSpritePalettes': False, - 'SetAdvancedSpritePalettes': False, - 'PukeMode': False, - 'NegativeMode': False, - 'GrayscaleMode': False, - 'GenerateSpoilers': False, - 'RandomizeLinkSpritePalette': False, - 'RandomizePots': world.options.pot_shuffle.value, - 'ShuffleMusic': False, - 'BootlegMagic': True, - 'CustomBosses': False, - 'AndyMode': False, - 'HeartBeepSpeed': 0, - 'AlternateGfx': False, - 'ShieldGraphics': "shield_gfx/normal.gfx", - 'SwordGraphics': "sword_gfx/normal.gfx", - 'BeeMizer': False, - 'BeesLevel': 0, - 'RandomizeTileTrapPattern': False, - 'RandomizeTileTrapFloorTile': False, - 'AllowKillableThief': world.options.killable_thieves.value, - 'RandomizeSpriteOnHit': False, - 'DebugMode': False, - 'DebugForceEnemy': False, - 'DebugForceEnemyId': 0, - 'DebugForceBoss': False, - 'DebugForceBossId': 0, - 'DebugOpenShutterDoors': False, - 'DebugForceEnemyDamageZero': False, - 'DebugShowRoomIdInRupeeCounter': False, - 'UseManualBosses': True, - 'ManualBosses': { - 'EasternPalace': world.dungeons["Eastern Palace"].boss.enemizer_name, - 'DesertPalace': world.dungeons["Desert Palace"].boss.enemizer_name, - 'TowerOfHera': world.dungeons["Tower of Hera"].boss.enemizer_name, - 'AgahnimsTower': 'Agahnim', - 'PalaceOfDarkness': world.dungeons["Palace of Darkness"].boss.enemizer_name, - 'SwampPalace': world.dungeons["Swamp Palace"].boss.enemizer_name, - 'SkullWoods': world.dungeons["Skull Woods"].boss.enemizer_name, - 'ThievesTown': world.dungeons["Thieves Town"].boss.enemizer_name, - 'IcePalace': world.dungeons["Ice Palace"].boss.enemizer_name, - 'MiseryMire': world.dungeons["Misery Mire"].boss.enemizer_name, - 'TurtleRock': world.dungeons["Turtle Rock"].boss.enemizer_name, - 'GanonsTower1': - world.dungeons["Ganons Tower" if world.options.mode != 'inverted' else - "Inverted Ganons Tower"].bosses['bottom'].enemizer_name, - 'GanonsTower2': - world.dungeons["Ganons Tower" if world.options.mode != 'inverted' else - "Inverted Ganons Tower"].bosses['middle'].enemizer_name, - 'GanonsTower3': - world.dungeons["Ganons Tower" if world.options.mode != 'inverted' else - "Inverted Ganons Tower"].bosses['top'].enemizer_name, - 'GanonsTower4': 'Agahnim2', - 'Ganon': 'Ganon', - } - } - - rom.write_to_file(randopatch_path) - - with open(options_path, 'w') as f: - json.dump(options, f) - - max_enemizer_tries = 5 - for i in range(max_enemizer_tries): - enemizer_seed = str(world.random.randint(0, 999999999)) - enemizer_command = [os.path.abspath(enemizercli), - '--rom', randopatch_path, - '--seed', enemizer_seed, - '--binary', - '--enemizer', options_path, - '--output', enemizer_output_path] - - p_open = subprocess.Popen(enemizer_command, - cwd=os.path.dirname(enemizercli), - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - universal_newlines=True) - - enemizer_logger.debug( - f"Enemizer attempt {i + 1} of {max_enemizer_tries} for player {player} using enemizer seed {enemizer_seed}") - for stdout_line in iter(p_open.stdout.readline, ""): - if i == max_enemizer_tries - 1: - enemizer_logger.warning(stdout_line.rstrip()) - else: - enemizer_logger.debug(stdout_line.rstrip()) - p_open.stdout.close() - - return_code = p_open.wait() - if return_code: - if i == max_enemizer_tries - 1: - raise subprocess.CalledProcessError(return_code, enemizer_command) - continue - - for j in range(i + 1, max_enemizer_tries): - world.random.randint(0, 999999999) - # Sacrifice all remaining random numbers that would have been used for unused enemizer tries. - # This allows for future enemizer bug fixes to NOT affect the rest of the seed's randomness - break - - rom.read_from_file(enemizer_output_path) - os.remove(enemizer_output_path) - - if world.dungeons["Thieves Town"].boss.enemizer_name == "Blind": - rom.write_byte(0x04DE81, 6) - rom.write_byte(0x1B0101, 0) # Do not close boss room door on entry. - - # Moblins attached to "key drop" locations crash the game when dropping their item when Key Drop Shuffle is on. - # Replace them with a Slime enemy if they are placed. - if world.options.key_drop_shuffle: - key_drop_enemies = { - 0x4DA20, 0x4DA5C, 0x4DB7F, 0x4DD73, 0x4DDC3, 0x4DE07, 0x4E201, - 0x4E20A, 0x4E326, 0x4E4F7, 0x4E687, 0x4E70C, 0x4E7C8, 0x4E7FA - } - for enemy in key_drop_enemies: - if rom.read_byte(enemy) == 0x12: - logging.debug(f"Moblin found and replaced at {enemy} in world {player}") - rom.write_byte(enemy, 0x8F) - - for used in (randopatch_path, options_path): - try: - os.remove(used) - except OSError: - pass - - tile_list_lock = threading.Lock() _tile_collection_table = [] @@ -795,9 +586,13 @@ def get_nonnative_item_sprite(code: int) -> int: # https://discord.com/channels/731205301247803413/827141303330406408/852102450822905886 -def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool): +def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int): local_random = multiworld.worlds[player].random local_world = multiworld.worlds[player] + enemized = bool(local_world.options.boss_shuffle or local_world.options.enemy_shuffle + or local_world.options.enemy_health != 'default' or local_world.options.enemy_damage != 'default' + or local_world.options.pot_shuffle or local_world.options.bush_shuffle + or local_world.options.killable_thieves) # patch items @@ -1757,6 +1552,71 @@ def get_reveal_bytes(itemName): if encoded_players > ROM_PLAYER_LIMIT: rom.write_bytes(0x195FFC + ((ROM_PLAYER_LIMIT - 1) * 32), hud_format_text("Archipelago")) + if enemized: + from . import EnemizerPatches as enemizer_patches + from .EnemyShuffle import apply_enemy_shuffle + from .PotShuffle import apply_pot_shuffle + + enemizer_patches.apply_enemizer_base_patch(rom) + + enemy_shuffle_enabled = bool(local_world.options.enemy_shuffle) + bush_shuffle_enabled = bool(local_world.options.bush_shuffle) + enemy_health_key = enemizer_patches._option_key(local_world.options.enemy_health) + enemy_damage_key = enemizer_patches._option_key(local_world.options.enemy_damage) + + if enemy_shuffle_enabled or bush_shuffle_enabled: + enemizer_patches._set_enemizer_flag(rom, "EnemizerFlags_randomize_bushes", True) + hidden_enemy_chance_pool = ( + enemizer_patches.RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL + if bush_shuffle_enabled + else enemizer_patches.VANILLA_HIDDEN_ENEMY_CHANCE_POOL + ) + rom.write_bytes(enemizer_patches.HIDDEN_ENEMY_CHANCE_POOL_ADDRESS, hidden_enemy_chance_pool) + enemizer_patches._update_hidden_enemy_item_table_for_retro_mode(rom) + + if enemy_shuffle_enabled: + enemizer_patches._set_enemizer_flag(rom, "EnemizerFlags_randomize_sprites", True) + enemizer_patches._set_enemizer_flag(rom, "EnemizerFlags_enable_mimic_override", True) + enemizer_patches._set_enemizer_flag(rom, "EnemizerFlags_enable_terrorpin_ai_fix", True) + rom.write_bytes(0x1F2D5, (0x54, 0x9C)) + rom.write_byte(0x1F2E5, 0xB0) + rom.write_byte(0x1F2EB, 0xD0) + + if local_world.options.killable_thieves: + enemizer_patches._apply_killable_thief(rom) + + if enemy_health_key != "default" or enemy_damage_key != "default": + rng = enemizer_patches._make_native_enemizer_rng(local_world) + else: + rng = None + + if enemy_health_key != "default": + assert rng is not None + enemizer_patches._randomize_enemy_health(rom, rng, enemy_health_key) + + if enemy_damage_key != "default": + assert rng is not None + enemizer_patches._randomize_enemy_damage(rom, rng, allow_zero_damage=True) + enemizer_patches._shuffle_damage_groups( + rom, + rng, + chaos_mode=enemy_damage_key == "chaos", + allow_zero_damage=True, + ) + + enemy_shuffle_state = getattr(local_world, "enemy_shuffle_state", None) + if local_world.options.enemy_shuffle and enemy_shuffle_state is not None: + apply_enemy_shuffle(rom, enemy_shuffle_state) + + if local_world.options.boss_shuffle: + # Boss shuffle must run after enemy shuffle so boss room sprite pointers + # and graphics block IDs are not restored to the enemy-shuffled room values. + enemizer_patches.patch_bosses(local_world, rom) + + pot_shuffle_state = getattr(local_world, "pot_shuffle_state", None) + if local_world.options.pot_shuffle and pot_shuffle_state is not None: + apply_pot_shuffle(rom, pot_shuffle_state) + # Write title screen Code hashint = int(rom.get_hash(), 16) code = [ @@ -1907,7 +1767,7 @@ def apply_oof_sfx(rom: LocalRom, oof: str): rom.write_bytes(0x12803A, oof_bytes) rom.write_bytes(0x12803A + len(oof_bytes), [0xEB, 0xEB]) - # Enemizer patch: prevent Enemizer from overwriting $3188 in SPC memory with an unused sound effect ("WHAT") + # Preserve SPC $3188 instead of writing the unused "WHAT" sound effect there. rom.write_bytes(0x13000D, [0x00, 0x00, 0x00, 0x08]) diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 2b99162837da..187ce6791888 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -14,9 +14,10 @@ from .ItemPool import generate_itempool, difficulties from .Items import item_init_table, item_name_groups, item_table, GetBeemizerItem from .Options import ALTTPOptions, small_key_shuffle +from .PotShuffle import generate_pot_shuffle from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions, lookup_vanilla_location_to_entrance, \ is_main_entrance, key_drop_data -from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \ +from .Rom import LocalRom, patch_rom, patch_race_rom, apply_rom_settings, \ get_hash_string, get_base_rom_path, LttPDeltaPatch from .Rules import set_rules from .Shops import create_shops, Shop, push_shop_inventories, ShopType, price_rate_display, price_type_display_name @@ -253,17 +254,6 @@ class ALTTPWorld(World): create_items = generate_itempool - _enemizer_path: typing.ClassVar[typing.Optional[str]] = None - - @property - def enemizer_path(self) -> str: - # TODO: directly use settings - cls = self.__class__ - if cls._enemizer_path is None: - cls._enemizer_path = settings.get_settings().generator.enemizer_path - assert isinstance(cls._enemizer_path, str) - return cls._enemizer_path - # custom instance vars dungeon_local_item_names: typing.Set[str] dungeon_specific_item_names: typing.Set[str] @@ -305,6 +295,8 @@ def __init__(self, *args, **kwargs): self.required_medallions = ["Ether", "Quake"] self.escape_assist = [] self.shops = [] + self.enemy_shuffle_state = None + self.pot_shuffle_state = None self.logical_heart_containers = 10 self.logical_heart_pieces = 24 super(ALTTPWorld, self).__init__(*args, **kwargs) @@ -316,10 +308,6 @@ def stage_assert_generate(cls, multiworld: MultiWorld): raise FileNotFoundError(rom_file) if multiworld.is_race: import xxtea # noqa - for player in multiworld.get_game_players(cls.game): - if multiworld.worlds[player].use_enemizer: - check_enemizer(multiworld.worlds[player].enemizer_path) - break def generate_early(self): multiworld = self.multiworld @@ -339,6 +327,9 @@ def generate_early(self): self.waterfall_fairy_bottle_fill = self.random.choice(bottle_options) self.pyramid_fairy_bottle_fill = self.random.choice(bottle_options) + if self.options.pot_shuffle: + self.pot_shuffle_state = generate_pot_shuffle(self) + if self.options.mode == 'standard': if self.options.small_key_shuffle: if (self.options.small_key_shuffle not in @@ -564,13 +555,6 @@ def stage_pre_fill(cls, world): def stage_generate_output(cls, multiworld, output_directory): push_shop_inventories(multiworld) - @property - def use_enemizer(self) -> bool: - return bool(self.options.boss_shuffle or self.options.enemy_shuffle - or self.options.enemy_health != 'default' or self.options.enemy_damage != 'default' - or self.options.pot_shuffle or self.options.bush_shuffle - or self.options.killable_thieves) - def generate_output(self, output_directory: str): multiworld = self.multiworld player = self.player @@ -578,14 +562,9 @@ def generate_output(self, output_directory: str): self.pushed_shop_inventories.wait() try: - use_enemizer = self.use_enemizer - rom = LocalRom(get_base_rom_path()) - patch_rom(multiworld, rom, player, use_enemizer) - - if use_enemizer: - patch_enemizer(self, rom, self.enemizer_path, output_directory) + patch_rom(multiworld, rom, player) if multiworld.is_race: patch_race_rom(rom, multiworld, player) diff --git a/worlds/alttp/enemizer_data/README.md b/worlds/alttp/enemizer_data/README.md new file mode 100644 index 000000000000..f2e9371ead90 --- /dev/null +++ b/worlds/alttp/enemizer_data/README.md @@ -0,0 +1,27 @@ +These modules are vendored/generated from the upstream Enemizer compiled release and source that were already present +locally in `/home/alchav/PycharmProjects/Archipelago/EnemizerCLI` and `/home/alchav/PycharmProjects/Archipelago/Enemizer`. + +Source details: + +- Upstream project: `Ijwu/Enemizer` +- Release family: `7.1` +- Library version from `EnemizerCLI/EnemizerCLI.Core.deps.json`: `EnemizerLibrary/7.1.0` + +Vendored data modules: + +- `base_patch_data.py` +- `symbols.py` +- `enemy_room_metadata.py` +- `enemy_sprite_requirements.py` +- `overworld_enemy_metadata.py` +- `dungeon_sprite_addresses.py` +- `pot_shuffle_data.py` + +Purpose: + +- `base_patch_data.py` contains the generated base patch Enemizer applies before feature-specific randomization. +- `symbols.py` contains the assembled symbol map consumed by Enemizer's runtime code for ROM addresses. +- `enemy_room_metadata.py` and `overworld_enemy_metadata.py` contain room and area grouping/randomization constraints. +- `enemy_sprite_requirements.py` contains the sprite metadata used by the native enemy shuffle implementation. +- `dungeon_sprite_addresses.py` contains dungeon sprite slot metadata derived from Enemizer's source tables and keyed-enemy address list. +- `pot_shuffle_data.py` contains the native pot shuffle room/item source data. diff --git a/worlds/alttp/enemizer_data/__init__.py b/worlds/alttp/enemizer_data/__init__.py new file mode 100644 index 000000000000..7a18008b018a --- /dev/null +++ b/worlds/alttp/enemizer_data/__init__.py @@ -0,0 +1 @@ +"""Native ALTTP Enemizer data modules.""" diff --git a/worlds/alttp/enemizer_data/base_patch_data.py b/worlds/alttp/enemizer_data/base_patch_data.py new file mode 100644 index 000000000000..5fd784539340 --- /dev/null +++ b/worlds/alttp/enemizer_data/base_patch_data.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +from typing import NamedTuple + + +class BasePatchData(NamedTuple): + address: int + patch_data: bytes + +ENEMIZER_BASE_PATCHES = ( + BasePatchData(address=208, patch_data=b'\\l\x956'), + BasePatchData(address=2304, patch_data=b'k'), + BasePatchData(address=2317, patch_data=b'"\x92\x9a6'), + BasePatchData(address=2335, patch_data=b'"\x98\x9a6'), + BasePatchData(address=65816, patch_data=b'"\x87\x9a6\xea\xea'), + BasePatchData(address=66134, patch_data=b'"C\x976'), + BasePatchData(address=67657, patch_data=b'"1\x976'), + BasePatchData(address=67961, patch_data=b'"C\x976'), + BasePatchData(address=68630, patch_data=b'"C\x976'), + BasePatchData(address=70456, patch_data=b'"C\x976'), + BasePatchData(address=192354, patch_data=b'"\xbe\x996'), + BasePatchData(address=197241, patch_data=b'\xea\xea\xea\xea\xea\xea\xea\xea\xea\xea"\xf5\x946\xea'), + BasePatchData(address=198603, patch_data=b'\xb6\x91'), + BasePatchData(address=201142, patch_data=b'"\x1a\x9a6'), + BasePatchData(address=224264, patch_data=b'"N\x9a6\xea'), + BasePatchData(address=224678, patch_data=b'"f\x9a6'), + BasePatchData(address=229578, patch_data=b'"\xb2\x996\xea\xea\xea\xd0$\xea\xea'), + BasePatchData(address=312587, patch_data=b'"S\xb76\xea\xb7\x00'), + BasePatchData(address=312600, patch_data=b'\xb7\x00'), + BasePatchData(address=312616, patch_data=b'\xb7\x00'), + BasePatchData(address=312625, patch_data=b'\x85\n'), + BasePatchData(address=312628, patch_data=b'\xb7\x00'), + BasePatchData(address=312635, patch_data=b'e\n'), + BasePatchData(address=312646, patch_data=b'\xb7\x00'), + BasePatchData(address=312655, patch_data=b'\xb7\x00'), + BasePatchData(address=317847, patch_data=b'\x00\x07\x07\x8c\x07\x07\x8d\x07\x07\x8d\x07\x07\x8d\x07\x07\x8d\x07\x07\x8d\x07\x07\x8d\x07\x07\x8d\x07\x07\x8d\x07\x07\x8d\x07\x07\x8d\x07\x07\x8d\x07\x07\x8d\x07\x07\x8d\xff\x00\t\t\t\xff'), + BasePatchData(address=318243, patch_data=b'\x00\x05\x04S\x05\x07S\x05\nS\x08\nS\x08\x07S\x08\x04S\x08\xe7\x19\x07\x07\xe3\x07\x08\xe3\x08\x07\xe3\x08\x08\xe3\xff'), + BasePatchData(address=318513, patch_data=b'\x00\x06\x08\x88\xff'), + BasePatchData(address=318667, patch_data=b'\x00\x07\x06T\x07\tT\t\x07T\xff'), + BasePatchData(address=319262, patch_data=b'\x00\t\t\t\xff'), + BasePatchData(address=319561, patch_data=b'\x00\x06\x07\x92\xff'), + BasePatchData(address=319934, patch_data=b'\x00\x07\x06T\x07\tT\t\x07T\x18\x17\xd1\x1c\x03\xc5\xff'), + BasePatchData(address=320599, patch_data=b'\x00\x05\x07\xbd\xff'), + BasePatchData(address=320954, patch_data=b'\x00\x05\x07\xcb\x05\x07\xcc\x05\x07\xcd\xff'), + BasePatchData(address=321108, patch_data=b'\x00\x05\t\xce\xff'), + BasePatchData(address=321671, patch_data=b'\x00\x05\x04S\x05\x07S\x05\nS\x08\nS\x08\x07S\x08\x04S\x08\xe7\x19\xff'), + BasePatchData(address=322049, patch_data=b'\x00\x05\x07\xa3\x05\x07\xa4\x05\x07\xa2\xff'), + BasePatchData(address=439998, patch_data=b'\x00'), + BasePatchData(address=440745, patch_data=b'\x00'), + BasePatchData(address=819304, patch_data=b'\x881\xfc8'), + BasePatchData(address=958593, patch_data=b'\\\x82\xb76'), + BasePatchData(address=961895, patch_data=b'"\x9c\x996'), + BasePatchData(address=972942, patch_data=b'"\xe7\x996\xea\xea\xea\xea\xea\xea\xea\xea'), + BasePatchData(address=973746, patch_data=b'\xa2\x07'), + BasePatchData(address=974570, patch_data=b'"z\xb76'), + BasePatchData(address=986033, patch_data=b'\x95\xc7'), + BasePatchData(address=988440, patch_data=b'"\x87\x996'), + BasePatchData(address=996017, patch_data=b'"\x1a\x976\xea'), + BasePatchData(address=999751, patch_data=b'"\x02\x9a6\xea\xea\xea\xea\xea'), + BasePatchData(address=1024348, patch_data=b'\xff\xff\xff\xff\xf0\xffa\x18\xff\xff'), + BasePatchData(address=1245184, patch_data=b'\t\x00\xe1>\x7f\x7f\x00\x00\x19\xff\xf0p\x04t\x07\x881\x00\x00\x00\x00\x00\x00\x00\x00\x00\x90\x00\x00\x00\n#]r\xc6\xa4\xe5\xc3\xa0D\xa0\x0cO \xa4\xee^\x1cM\x11\xf2\xd3J\xa0\xb4B\x02=!\x00 \xe0\xb0\xf2\x1d\xce\xef\x15~\xcd-\xcc}\xb1xj2\xd2\x00\x0e\xb0\x9f\xee\xee\xbd\xd7RF$\xb4\x1d\x0f\x1f\x0b]\x1e.\x04\xb0\xf4\xe5/\x0e\xd8\xe9\xad\xee\xa4\x01-\xf5?G\xd3O\xd5\xb0O\xfe\n\xda\xdf\x01\xc2\x10\xa8l\x02\xf2\x96\xfe\x87\xd5\x10\xa49C\x17\x10\x15\xd1&\xee\xac\xf1\xc3\xe4\x92D\xef!h\xa4\x10\xa2\xcf)\x10\xec\x11\xf0\xac\xd1\xf7\x05\x8cr\xb3\xbeA\xa4\xe8\xd4\x00\x91!c\x0fs\xa4\x04\xcb-\xf9\xa1;|A\xb4/\xd5\x10\x0f\xf2P\xed\r\xb40\xbd-\x13\x022\xfe%\xb8\xb0\xed}\x1f\xe3\xf4\xc0O\xb4\x10\x00\x11\x10\xb2\x0e\xef\xa3\xa4a\xdcMg:t\xe6\xe3\xb8\xdf\xb4A\xc4\xecq\r\xe3\xbc\xd3\x0b \xd0\xf2 -\xf6\xbc\xf0\xef\xf3\x01\xfa?<=\xb4\x10\xf1\x06\x01\x0cU\x12\r\xc0-,\xbd\xbb\xdd\xf02A\xcc{\x1f\xff>\xf2\xe1"\xe0\xb42\xb6\xeeL+\xb6\xde\xfc\xbcP1\x91<5\x94\xfdN\xb4\xd2\xbd\xceB\x13\xef`#\xb4\xf2\xd1\xd9<\xa3\xf0qO\xb4\x07\xde<\xd4\xcd\xfc,C\xb0\xe6b%\x04!\xedR\xe1\xb4\xdb\x14\x02\xf5\xb0L#\xe1\xb4?\xee\xec\xa6\x1dq\xe2^\xb4\xf1K\xf4\x9d*\xff42\xb4=`\xa2\x0c\x12\xf9\xf3\xbe\xb0\xef\x17R5@\x01\xd1^\xb0\xcf\xba\xbe\xefS\xe5A4\xb4\xe01\x8d\x1a\x15\xd23\xf2\xbc+\x15\x90@\x84\x1c%\x1c\xbc3\xa2I\x04\xfe\x12\xbem\xbc\xd6K?\xc1@\xa2=\x15\xb0;\xed\xab\xde\xf3>\x05"\xb4.\x03)\xdf\xc2?\x13/\xa4\x16\x00<\xe1\xf9\xc9\x93g\xa8\xa5\xdcR\x80[O\x187\xb0\x89\xd0&P63/\xd4\xb4\xfb\xe2\xa3/%\x0b#\xd3\xb4<\xd6\xfc\xfe\xa1_\x03\x1c\xb4D\xc2]\xc4\xfb\xf1\xaeP\xb4\x06\x1b\'\xc0\\\xc6\x08\x0c\xb0\x88\xfe\xc7m\x05`q\xc3\xb0n\xbe\x98\x01\xc2N\xf3\xf0\xcc.\xd5\x0b1\xa2{\x03\xfb\xb4$\xd1Q\xc4\xd9 \xbel\xb45\xfd3\xe2\xe0\x04\xf9\x0f\xb4\xd0}\xf7\xfb2\xd4+\xe5\xb0=\xff\x99/\xd4=\x03\xee\xb0b\x17N\xe0\x98/\xc4\x1b\xb4S\xc2L\x07\xeb\xef\xceq\xb4\xc5\nS\xe2-\xe7\t\xd0\xb0\x882\xd3,%\x03O\xf5\xb4,\xa0\xdfU\x0c\x0f\xe5=\xb8>\x15)\xb2MW\x8c=\xbc\xf7\x1b\x00\xf5J\xeb\x13\x15\xb8\t\x12\xc3[\xd3\xf3L\xad\xb0\x9a\x93_\xf1\xc1R\xf0\xe0\xb0w\xeb\xc9\xb7P\x0e\xb2R\xbc\xf3\xe5X\xa1@&\xbb\xf4\xbc\xf3+\xd3!M\xabE\x11\xa4P\x8bBA\xac6s\x1c\xb8\x95a\xe1\xedO\xe1\x0e3\xb4B\x1e\x9a\x14\x03/\x01\xfe\xb0\xec\xc1Fu\xa9\xdd\xf4\x0e\xb0!\x11\xfc\xe1Gq\x9a\xed\xb05\xdd #\x0b\xf2\x15c\xac\x93w\xe8B\xb6\xea1\xf3\xbc-1\xbc\xe54\xd1\t\x14\xb4\xf0\xd4A\x0cP\xb8A\x16\xbc\xcf\xde0!\xd5\xff\xff\x86\xb4\x117\xef\xfc\xf0\x12\xe5\x12\xb0a\x99\xc0$\x111\x0e\xdb\xb0\xd23R\xec\xab$!.\xa4?\xcasD\x0f\x1e\x88\xd7\xacy\x8e\x13J\x04\x04(\xd7\xbc\xe2\xccc<\r\xd0P\x02\xbc\x0e\x1f\x0f\xa3B/\xcf\xb4\xb4\x1111\x00\r\x82\x07\x12\xb4\n\xef\x01\x12"\x10\x0b\x93\xb8\xf2\xb7\x8d3\xd3:\x17\x90\xb0"\x0e\xa9\x172\x0f\xcd\xf1\xb0#B\x10\xf2C\xc8\xd4S\xa8\xf0\x04^\xee1\r\x1c\xa0\xbcd\x0b\x1e\xd1O\x13\xef\xf0\xb0>\x8a\xb3U=\xba\xde\x13\xbc\xf0\xf0\x1a\x06"\xa4\xd9S\xbc\xe2L\xf0*#\xd2\xf4\x00\xa0[\xca\xa0De\x0e\xd0v\xb0\xfa\xb2c\xec\xd11\xf0\x12\xb0\x11#N\xa8\xc5T\xfe\xde\xb0\xff\xf143/\x99\xb2T\xbc\xfe\x02\x11/\xff\x00\x195\xb8\x01\xb5\x9do@\xff\x0e\x10\xbc\x1b\xf6z\xc2\r\x13\xf2,\xc4\xff\x1f!\xdf\xb7?\xcf0\xb0\xe0\xf7D\xdf\xf53\x08\x90\xbc\t\xc7:\r}0\xc5\xc3\xb0+\x8a\xf72\x1f\xcd\xc03\xb0!\x13\x18\x9e5\x02\x0e\xdc\xb8!9\x10\x02\x19EX\xb2\xb4\x1c\xe4\xc4\x1d\xcdC!\xba\xb4G-\xb0P\xf33,!\xb4\xd1\x0e\x8a\x06\x0b\xd3@\xce\xbc1\x02\xff\x02\xeb\xf6n\xa0\xb4\xe0\xdf\xf33/\xf2\r\x92\xb4"\xce\xfa\xee\'\x1d\x02\xf0\xbc\x1d\x07;\xc1l\xb3a\xdc\xbc\xd2=>\xc1b\xc9F\x18\xc4\xc1P\x0e\xf3\x10\x1e\xefb\xb4\xb8E\r\xd4A\xc1Q\xfc\xb8\x0bf\x1a\xc4\x1e\x1dS\xbf\xbc\x00/\xfb6K\xa5\x00\xc2\xbc\x17\x8e@\xdc\x7f\xe3\xf4\xdd\xbc\xb3_\xd39@\x02\xa6\x86\xb4d\x0e\xa11\xb4\x7f\xa1@\xb0<\xb0\xb8\xc0\xda\xb1\x1f\xfe\xb4M\xb3/9\xffw\xfcU\xb4\t\xc0O\x10\r\xb0\nV\xb4+\xb4-\x0f%\xe5,\x0e\xb0\x0e\xcdb\x00\xe0\xf3Fw\xb4\xff\xeb\xe4\xec\xb7M\xd1\x11\xb0\x9c\xfd~2\xeet_v\xb0Oe\xfc\x9d\xfd\xf1.\xd5\xb0J\xce\xdd\xcd<\xb8\x02\xf2\xb0AMB\xe13CUQ\xb010\xde\xdf\xee\xc9\x80>\xb4\x12\x1b\x95/\x11#\x04\x10\xb4\xf8T\x91^\xdc\xd4\xe2\xe2\xb4m\xd3\xdb\x1e^\xe6\x1d>\xbcN\xd3.\x1f\x0f\xf6\xdd}\xb0SF=\x1d\x9b\x9b\xfb\xf1\xb4\xde\x13\xdaf\x903\xff\x05\xb8\x96\xdeL\xf2\xf3\xa6L\xc5\xb42\xda/\x911\xd0o\xb4\xb41.\x10\xf4\xed\x1d1\xd3\xb4N\x0e\x1d\xf5\xea0\xee?\xb4\x0fj\xd2\xf1\xfdQ\xe2r\xb4\x0fm\x91>\xdd\x0f\xf3\x0e\xa03\x13/\xac,\x9b\xae\xc8\xbc2\xe0\x00\xe1Z\xe5\x00\x01\xa4\xe2\x14\x8c\x1e\xfe\x90/\xf1\xa0\x8c\xd7\t\xae\xbe\xdc\xbfe\xb4\x04\x01\xfe\xe1\x00\xdf\xf0@\xa4\xd3_\xff\xab\x05\xaaO\xf1\xbc\x1e_\xfd\x12\xf1\xfbA\x1f\xa4O\x0fx\xd4<\xed\xcfp\xa4\xacA=\xbc?\x11\xf2W\xa4 t\xed\xfc\xd2\x08\xa5\x01\xb0(\xf1D\x13\r\xfe\x0c\x1b\xb8D\xd2<\xb5\x04\xa4\xc0"\xa4\x13\xe5\x0c\x19a\x8c\xf0\xe3\xb0+\xbd\x0f\xff\xbb\xcd\xde\xdf\xac\x1ek[\xa3\xf4\xd6\x8bs\xa4\x03=?-\x02\xa0\xee\xe1\xa4\x1d\xd3VN\xddb\x1f\xd0\xa4\x14\x1f2\xbe\x0f?\xcb\xb4\x945\x88\xe7M\xbf\xa0^7\xa4$\xf4cK\xbe\x11\xfd\xae\xac\x13\xf0\x01\x0cMM\xe1\xb5\xa8\x00\xee&\xfd\xfd]n\xb3\xa4\xf2! \x1e\xbe!\xfe\x8d\x94\xf7#\x8e1\xfe\xf4\x90\xd1\xac0\xe2\xe5\x0f\xdd@\xf2\xb0\x94\xcd \xf0\x1f\xfbd\x1b\xb8\xa4\xf2\x0e\xfe4O\xff\x10P\xa4"\xf2\x01@\x1d\xc0\x1e\xf9\x94\x8f\xf5\xed\xe0\x1e\xd2N\x0e\x98\'\xc0\xe3\x17\x80\x85]=\xa4\xcf\xef\x01\x02\xdf\xe2A\xfd\x9c\xa7=.\xf5\xe4\xc2\x10\xf5\x98\xd2X>\x15\xd2\xbeA\xd0\x9c\x93P>\r-=P\xde\x98\xf2|\x1e\xf3m\n\xf4\x01\x94\x1c\x1d\xae\xef<\xeb\x170\xa4\xfc\xe0\xe0\x01\xee\xb3\x7f|\xb4\xf1#\xf3.\x1a\x7f\r-\xa4\xcc\xe2\xd0\x0f\xf0\x93\x05\x02\xa4\xd1\xfds\xf3-Q \x00\x94\xa1\x14\xed\xa2\xce\x1d\x0f[\xac \xb4\x00\x02\xc3\xe2\x0e>\xac\x00_\xf2\xef\x01\x11\xe0\xfe\xa8\xe2\x13\xc1\x1e<\\\xf1.\x94\xf7\xd0\xd6\x113cB\xe0\x9c\xd7\xee\xf1\xe0\x01\r-B\x98+\xde\x10"\xe0,\xf1/\x943#W5\xf2#?\xfe\x88\xa3\xb22\x0f\x13\xbe\x0e%\x8cJ\xff\xc6\x11\xe3\x021\xee\x8c\x1f\x1fm\xc3\x90\x02\xe1\x04\x8c\x1f\xfa\xc2\xd3?/\r\xf4|$%\'^\x83\x05o\xa8x\x0e\xa1\x10A\x01/\x8d\x03\x88B\xdb\xf0\xe1O\xf4\xf0\x1e|\\Z@=\xb0\x8f/\xf5\x84C\x1e\xb8\x9c\xbf\x02\xfe\xfe|5"Ec\xfd\xd6$\x0ex\x8e\x0b\xe2O!\x00\xee\xc3y\x15\x0e\xac\x0f\xe21\x13\xf0\x00\x00\x00\x08'), + BasePatchData(address=1343488, patch_data=b'kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk'), + BasePatchData(address=1769728, patch_data=b'\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'), + BasePatchData(address=1770352, patch_data=b'\x00\xd8\xe3\xd86'), + BasePatchData(address=1774837, patch_data=b'\x84\r\xaf\x00\x816\xd0\x12\xc0\x04\xd0\n"q\xba\r)\x03\x18i\x13\xa8\xb9\xf3\x81k\xdaZ\x8bK\xab\xc0\x04\xd0#"q\xba\r)\x03\xa8\xb9p\x83\x82C\x00\x00\xd9>y\xd9\xdc\xd8\xda\xe4\xe1\xdc\xd8\xdf\xe0\x0bB\xd3A\xd4\xd9\xe3\xd8\xc0\x0f\xf0\r\xc0\x11\xf0\t\xc0\x10\xf0\x05\xb9%\x95\x80\x1c\xaf\n\x04~\xa8\xaf\xc5\xf3~\xc9\x03\x90\x0c\xaf\n\x04~\xc9@\xb0\x04\x18i\x90\xa8\xb9 \x81\xabz\xfakH\x08\xe2 \xaf\xb0P\x7f\xf0$)\x01\xd0\n\xaf\xb0P\x7f)\x02\xd0\x0e\x80\x16"\xa5\x956\xa9\x00\x8f\xb0P\x7f\x80\n"\x17\x966\xa9\x00\x8f\xb0P\x7f(h\x8b\xa9\x00\x00[\\\xd5\x80\x00H\xad\x00CH\xad\x01CH\xad\x02CH\xad\x03CH\xad\x04CH\xad\x05CH\xad\x06CH\xa9\x80\x8d\x15!\xa9\x00\x8d\x16!\xa94\x8d\x17!\xa9\x01\x8d\x00C\xa9\x18\x8d\x01C\xa9\x9a\x8d\x02C\xa9\xc7\x8d\x03C\xa96\x8d\x04C\xa9\x00\x8d\x05C\xa9\x10\x8d\x06C\xa9\x01\x8d\x0bBh\x8d\x06Ch\x8d\x05Ch\x8d\x04Ch\x8d\x03Ch\x8d\x02Ch\x8d\x01Ch\x8d\x00ChkH\xad\x00CH\xad\x01CH\xad\x02CH\xad\x03CH\xad\x04CH\xad\x05CH\xad\x06CH\xa9\x80\x8d\x15!\xa9\x00\x8d\x16!\xa94\x8d\x17!\xa9\x01\x8d\x00C\xa9\x18\x8d\x01C\xa9\x9a\x8d\x02C\xa9\xd7\x8d\x03C\xa96\x8d\x04C\xa9\x00\x8d\x05C\xa9\x08\x8d\x06C\xa9\x01\x8d\x0bBh\x8d\x06Ch\x8d\x05Ch\x8d\x04Ch\x8d\x03Ch\x8d\x02Ch\x8d\x01Ch\x8d\x00ChH\xad\x00CH\xad\x01CH\xad\x02CH\xad\x03CH\xad\x04CH\xad\x05CH\xad\x06CH\xa9\x80\x8d\x15!\xa9\xa0\x8d\x16!\xa9:\x8d\x17!\xa9\x01\x8d\x00C\xa9\x18\x8d\x01C\xa9\x9a\x8d\x02C\xa9\xdf\x8d\x03C\xa96\x8d\x04C\xa9\xc0\x8d\x05C\xa9\x00\x8d\x06C\xa9\x01\x8d\x0bBh\x8d\x06Ch\x8d\x05Ch\x8d\x04Ch\x8d\x03Ch\x8d\x02Ch\x8d\x01Ch\x8d\x00Chk\xa9\x10\x85\xbc\xaf\x03\x816\xf0\x0b"q\xba\r)\x1f\x18i`\x85\xbc\xa9\x00\x8f\xb1P\x7f"=\x89\x00kH\xaf\x06\x816\xd0\x07h)\x03\x9d\xe0\rkhH)\x03\x9d\xe0\rhk\xaf\xf3\x0c~\xf0\x07\xa9\x05\x85\x11\x9c\xf3\x0c"\x00\x80\x07k"\x14\xc1\t\xa5\xa0\xa6\xa1\xc9\x07\xd0\x0f\xe0\x00\xd0\x0b"N\xc4\t"\x14\xc1\t\x82\xdc\x00\xc9\xc8\xd0\x0b"N\xc4\t"\x14\xc1\t\x82s\x01\xc9)\xd0\x16"N\xc4\t"\x14\xc1\t\xa9\x07\x8d\x00\x0b\x9c(\x0b\xee\x08\x0b\x82Y\x01\xc93\xd0\x0b"N\xc4\t"\x14\xc1\t\x82\x9d\x01\xc9Z\xd0\x0b"N\xc4\t"\x14\xc1\t\x82;\x01\xc9\x90\xd0\x0b"N\xc4\t"\x14\xc1\t\x82\x7f\x01\xc9\xac\xd0\x1d"N\xc4\t"\x14\xc1\t\xaf\x01\x816\xf0\x0c\xeeh\x04\x9c\x8e\x06\x9c\x90\x06\xee\xf3\x0c\x82\x0b\x01\xc9\x06\xd0\x0f\xe0\x00\xd0\x0b"N\xc4\t"\x14\xc1\t\x82K\x01\xc9\xde\xd0\x0b"N\xc4\t"\x14\xc1\t\x82\x96\x00\xc9\xa4\xd0\x0b"N\xc4\t"\x14\xc1\t\x82-\x01\xc9\x1c\xd0\x0f\xe0\x00\xd0\x0b"N\xc4\t"\x14\xc1\t\x82\xc7\x00\xc9l\xd0\x0b"N\xc4\t"\x14\xc1\t\x82\x0b\x01\xc9M\xd0\x0b"N\xc4\t"\x14\xc1\t\x82\x03\x00\x82L\x01\xa2\x00\xbd \x0e\xc9\xe3\xd0\x02\x80\x1e\xc9\xd1\xd0\x02\x80\x18\xc9\xc5\xd0\x02\x80\x12\xbd\x10\r\x18ih\x9d\x10\r\xbd\x00\r\x18ih\x9d\x00\r\xe8\xe0\x10\xd0\xd4\xa2\x00\xbd\x00\x0b\xc9\xe3\xd0\x02\x80\x12\xbd\x08\x0b\x18ih\x9d\x08\x0b\xbd\x18\x0b\x18ih\x9d\x18\x0b\xe8\xe0\x08\xd0\xe0\x82\xf9\x00\xa2\x00\xbd \x0e\xc9\xe3\xd0\x02\x80\x1e\xc9\xd1\xd0\x02\x80\x18\xc9\xc5\xd0\x02\x80\x12\xbd \r\x18i\x00\x9d \r\xbd0\r\x18i\x01\x9d0\r\xe8\xe0\x10\xd0\xd4\xa2\x00\xbd\x00\x0b\xc9\xe3\xd0\x02\x80\x12\xbd\x10\x0b\x18i\x01\x9d\x10\x0b\xbd \x0b\x18i\x00\x9d \x0b\xe8\xe0\x08\xd0\xe0\x82\xa6\x00\xa2\x00\xbd \x0e\xc9\xe3\xd0\x02\x80\x1e\xc9\xd1\xd0\x02\x80\x18\xc9\xc5\xd0\x02\x80\x12\xbd \r\x18i\x01\x9d \r\xbd0\r\x18i\x01\x9d0\r\xe8\xe0\x10\xd0\xd4\xa2\x00\xbd\x00\x0b\xc9\xe3\xd0\x02\x80\x12\xbd\x10\x0b\x18i\x01\x9d\x10\x0b\xbd \x0b\x18i\x01\x9d \x0b\xe8\xe0\x08\xd0\xe0\x82S\x00\xa2\x00\xbd \x0e\xc9\xe3\xd0\x02\x80\x1e\xc9\xd1\xd0\x02\x80\x18\xc9\xc5\xd0\x02\x80\x12\xbd \r\x18i\x01\x9d \r\xbd0\r\x18i\x00\x9d0\r\xe8\xe0\x10\xd0\xd4\xa2\x00\xbd\x00\x0b\xc9\xe3\xd0\x02\x80\x12\xbd\x10\x0b\x18i\x00\x9d\x10\x0b\xbd \x0b\x18i\x01\x9d \x0b\xe8\xe0\x08\xd0\xe0\x82\x00\x00k\xad\xba\x0c\xd0\x0b\xa9\x01\x8d\xba\x0c\xa9\x01\x8f\xb0P\x7f"\x7f\xd9\rk\xad\xba\x0c\xd0\x0b\xa9\x01\x8d\xba\x0c\xa9\x02\x8f\xb0P\x7f\xa9\x03\x9d\xc0\rk\xc5\x00\x90\x058\xe5\x00\x80\x02\xa9\x00kH\xa9x\x9d\x10\r\x9d\x00\r\xa5#\x9d0\r\xa5!\x9d \r\xa5\xa0\xc9\x07\xd0\n\xa5"\x9d\x10\r\xa5 \x9d\x00\rh"\xbd\x84\x06k\xda\xaf\x02\x816\xaa\xa5\x90\x18i\x04\x00\x85\x90\xa5\x92\x18i\x01\x00\x85\x92\xca\x10\xed\xfak\xbd\xe0\r\x1a)\x03\x9d\xe0\r\xbd\xa0\r\x1a\x9d\xa0\r\xc9 \x90\x03\x9e\xd0\rk\xaf\x05\x816\xd0\x05"\xfa\xc6\x1ek\xbd \x0e\xc9\xb8\xf0\x05"\x1a\xc7\x1ek\xa9\x83\x9d \x0e"\x18\xb8\r\xa9\xb8\x9d \x0e\xbd\xaa\x0c)\xfb\t\x80\x9d\xaa\x0c"\r\xc7\x1ek\xaf\x05\x816\xf0\x0c\xbd \x0e\xc9\xb8\xd0\x05\xa9\x83\x9d \x0e\xbd \x0e\xc9zk\xbd \x0e\xc9\xb8\xf0\r\xaf\x05\x816\xf0\x0c\xbd \x0e\xc9\xb8\xd0\x08\xa9\x83L\x82\x9a\xbd \x0e\xc2 \n\nk"\x9e\x9a6\xa9\x00\x8f\x11\xc0~kx"\x88\x88\x00kx"\x88\x88\x00k\xaf\xb1P\x7f\xf0\x01k\xa9\x01\x8f\xb1P\x7fx\x9c\x00B\x9c\x0cB\x9c6\x01\xa9\xff\x8d@!\xa9\x00\x85\x00\xa9\x80\x85\x01\xa9&\x85\x02"\x88\x88\x00\xa9\x81\x8d\x00BXk'), + BasePatchData(address=1783505, patch_data=b'\x8bK\xab\x08\xc20\xa0\x00\x00\xafY\xf3~)\xff\x00\n\xaa\xbd\t\xb7\xaa\xbf\xd1\x9a6\xda\xbb\x9f\x00\x90~\xfa\xbf\xd1\x9c6\xda\xbb\x9f\x80\x91~\xfa\xe8\xe8\xc8\xc8\xc0\x80\x01\x90\xe1(\xabk\x00\x00\x00\x00\x00\x04\x00\x08\x00\x0c\x8bK\xab\x08\xc20\xa0\x00\x03\xafZ\xf3~)\xff\x00\n\xaa\xbdK\xb7\xaa\xbf\xd1\xaa6\xda\xbb\x9f\x00\x90~\xfa\xbf\xd1\xac6\xda\xbb\x9f\xc0\x90~\xfa\xe8\xe8\xc8\xc8\xc0\xc0\x03\x90\xe1(\xabk\x00\x00\x00\x00\x00\x04\x00\x08\x85\x01\xa0\x00\x00\xa9\t\x85\x02k\xa9y"]\xf6\x1d0\x14\xa5"\x99\x10\r\xa5#\x990\r\xa5 \x99\x00\r\xa5!\x99 \rk\x9e\xba\x0c"\x18\xb8\rk\xa5\xa0\xc9\xac\xd0\x0e\xaf\x81\xde\t\xf0\x08\xaf\xcc\xf3~\\\x85\xa0\x1d\\\x90\xa0\x1d'), + BasePatchData(address=1787802, patch_data=b'\xcf=\xef^\xb7o\x9fp\x88x\xc7?g\x9fp\xaf\x00\x00\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00\xfc\x03\xc7xc\xbd1\xde\xd8\xef\xf6\xe7\xf6g\xf6\xa7\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00\x08\x00\x08\x00\xc0\x00\xe0\x00\x7f\x00\xbf\x80\xb8\x00\xb7\x07\xa8\x0f\x9f\x10\xff\x00\xff\x00\xff\x00\x7f\x00\x7f\x00x\x00p\x00`\x00\x00\x00\x00\x00\x1c\x00\xbe\x00\xe3\x00]\x1c\xa2\xbe\xff@\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xe3\x00A\x00\x00\x00\xdf\x10\xe0\x00~\x00=\x01;\x025\x065\x065\x06\xe0\x00\xff\x00\xff\x00\xfe\x00\xfc\x00\xf8\x00\xf8\x00\xf8\x00\xfc\x03\xce\xf1\xd7\xf8\xeb\x0c\xfc\xcf\x1e\xe7\xff\xfb?\xc0\x00\x00\x00\x00\x00\x00\x10\x00\xc0\x00\x00\x00\xf8\x00\x00\x00\xf7\x07\xef\x0f\xee\x0el\x0c\xb7\x87\xbb\x83\xdf\xc0\xbf\xe0\xf8\x00\xf0\x00\xf1\x00\xf3\x00x\x00|\x00?\x00\x1f\x00\x0f\x00\x0f\x00g`d`\t\x01\x9b\x83\xf5\x07\xe9\x0f\xff\x00\xff\x00\x9f\x00\x9f\x00\xfe\x00|\x00\xf8\x00\xf0\x00\xef\x0f\xf2\x03\xfd\x01\x8f\x00wp\xfb\xf8\xef\xec\xcf\xcc\xf0\x00\xfc\x00\xfe\x00\xff\x00\x8f\x00\x07\x00\x13\x003\x00\x07\x043030\x06\x00\xcd\x01\xf2\x03\xe4\x07\xef\x0f\xfb\x00\xcf\x00\xcf\x00\xff\x00\xfe\x00\xfc\x00\xf8\x00\xf0\x00\xff\x00\xff\x7f\xffo\xefV\xfeY\xefM\xe5G\xf7W\x00\x00\x7f\x00o\x00F\x00@\x00P\x00X\x00H\x00\xff\x00\xe0\xdf\xdc\xc1[\xdc\xa5\xee\x02\x07Ss\x8b\xfb\x00\x00\x00\x00\x00\x00 \x00\x10\x00\xf8\x00\x8c\x00\x04\x00\xd4\xf7\xea\xef\x15\x1e;\xfc\xf6\xf9|\x83\xc6;\x86\xfb\x08\x00\x10\x00\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe7W\xe5W\xed_\xed_\xfdO\xf5O\xf8G\xe4[H\x00H\x00@\x00@\x00`\x00`\x00`\x00@\x00\xd2\xd6\xf3\xf7\xe5\xf7\xea\xfb\x80\xfcE\xc7\xba\x83\xed\xa9)\x00\x08\x00\x08\x00\x04\x00\x03\x008\x00D\x00\x12\x00\xabn\xaam\xabm\xabm\xabm\xbbm\x9bU\xabm\x10\x00\x10\x00\x10\x00\x10\x00\x10\x00\x10\x00(\x00\x10\x002\xeel\xdc\xda\xfb\x90\xb7\x117\x146\x132)9\x01\x00\x03\x00\x04\x00H\x00\xc8\x00\xc9\x00\xcd\x00\xc6\x00\x00\x0f\x00\x03\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x0c\x00\x03\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00Of\xbf\xe6\x19\xcag\xf4S4\xed*\xd7Z\xab\xb6\x98\x00\x18\x004\x00\x08\x00\xc8\x00\xd0\x00\xa0\x00@\x00C\xc0\x89H\x9c\\\x9eR\x8eR\x9cJ\xbcj\xae\\?\x007\x00#\x00!\x00!\x00!\x00\x01\x00\x01\x00\x7f\x80\x80\xff\x03\x0269\x7fg?!\x99\x1e\xc0\x00\x00\x00\x00\x00\xfc\x00\xc0\x00\x80\x00\xc0\x00\xe0\x00\xff\x00\xf0o\xfao\xfao\xf0n\x90o\xb0/\xf8o\xf8o\x00\x00\n\x00\n\x00\x01\x00\r\x00B\x00\x0b\x00\t\x008\xc7\x00\x7f\x03\xff\x84\xfc\x06\xfe!\xff\xa0\xffF{\x00\x00\x83\x00\x07\x00\xcf\x00\x8f\x00G\x00\x83\x00\xe6\x00\xff\x00\xf3\xfb\xf7\xff\xff\x00c\xff\x00\xff`\xff\x00\xef\x00\x00\x04\x00\x00\x00\x00\x00k\x00\x08\x00f\x00\x1b\x00\x12\xbe\x01\xff\x86\x7f\x80\x7f\x80\x7f\x19\xfe)\xef$\xe7[\x00\x15\x00\x01\x00\x00\x00\x18\x00=\x00\x7f\x00~\x00\xc8\xe0[\xf3\xd4w\xe4w\x94W\xa8kr\xb3\xde8\x1f\x00\x0c\x00\x08\x00\x08\x00(\x00\x14\x00\x0c\x00\x07\x00\x03\x0f\xe2\xefg\xeeg\xee\xed\xeeM\xce\x96\x9d+<\xf0\x00\x10\x00\x10\x00\x10\x00\x10\x000\x00`\x00\xc0\x00\xbd\xc2\xf3\xfc\x16\xf7k{\xc5\x0598C~@~\x00\x00\x00\x00\x08\x00\x84\x00\xfa\x00\xc7\x00\x81\x00\x81\x00J~||yy\x02\x03\r\xff>\xff\xf3\xfc\xbd\xc2\x81\x00\x83\x00\x86\x00\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\xefX\xecZ\xecZ\xeb]\xef]\xf7N\xfbW\xfdZ@\x00A\x00A\x00@\x00@\x00`\x00@\x00@\x00\xfe<_=\xbe\x82\xcfLP\xb0\xbf\xc0\xfbgY\xb7\x01\x00\x80\x00A\x000\x00\x0f\x00\x00\x00\x00\x00\x00\x00.\xd9:\xd5\xb0\xcf\xe7\x1fL\xbcZ\xb8d\xb0\xf19\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x07\x00\x0f\x00\x06\x00\xa7\xe5\xbd\xfa[~c>\xb5\x86KL\xfe\x81\x8ds\x18\x00\x00\x00\x80\x00\xc0\x00x\x00\xb0\x00\x00\x00\x00\x00(%QD\xa0\x85H\r\x90\x1d`}\x80\xfd\x00\xfd\x1b\x003\x00b\x00\xc2\x00\x82\x00\x02\x00\x06\x00\x06\x007\xce\xab\xda\xfb\x9a\xd76\xabnS\xde\xa7\xbe\x076\x00\x00\x04\x00\x04\x00\x08\x00\x10\x00 \x00@\x00\xc8\x00\xdf0\xc88\xe7_\xf0o\xffp\xff\x7f\xbf?\xff\x00\x00\x00\x07\x00\x00\x00 \x000\x00?\x00@\x00\x00\x00\xff\x00\xfd\xc3Z~\x81\x00\x00~\x00~\x00~\x00~\x00~\x00~\x00\x00\x00\x10\x908\xa8\x1c\x94\x0e\xca\x07\xe5\x03\xf2\x01\xff\x00\x00\x00\x00\x10\x00\x08\x00\x04\x00\x02\x00\x81\x00\xc0\x00\xff\x00\\D.#\x17\x11\x0b\x08\x05\x06\x82\x8f\xc0\x7f\x00\x00\x18\x00\x0c\x00\x06\x00\x03\x00\x01\x00\x01\x00\x83\x00\xff\x00\x00\xff\xc0\x00\x17\xf9\x0b\x08\x05\x04\x02\x02\x01\x01\x00\x00\xf0\x00\xff\x00\x06\x00\x03\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\xff\x03\x00\x00\xff\x80\x80\xc0@\xe0 p\x10\xb8\x88\x0f\x00\xff\x00\x00\x00\x00\x00\x80\x00\xc0\x00`\x000\x00\x00\x00\x0e\xee\x03s\x00\xbc\x80\xdf@\xef\xa0\xf7\xd0\xfb\xfe\x001\x00\x1c\x00\x0f\x00\x07\x00\x03\x00\x01\x00\x00\x00\xe8\xfd\xf4\xfe\xfa\xff\xfd\x7f~??\x1f\x1f\x0f\x0f\x07\x00\x00\x00\x00\x00\x00\x80\x00@\x00 \x00\x10\x00\x08\x00\xef]\xf7M\xfae\xffp\xff|\xff\x7f\x80\x7f\xff\x00@\x00`\x00p\x00x\x00\x7f\x00\x7f\x00\x7f\x00\x00\x00\xea\xd7;\'\xd9\xd7z\xf7\xfb\x07\xf8\xf6\x04\xfb\xff\x00\x00\x00\xc0\x00 \x00\x00\x00\x00\x00\xf1\x00\xf8\x00\x00\x00\xff\x00\x90p\xafo\xdb\\\xb4;\xa97\xf3\x0f\xe7\x1c\x00\x00\x0f\x00\x1f\x008\x00p\x00`\x00\x00\x00\x00\x00\xff\x00\x00\x00\xff\xff\xff\x00\x18\xe7/\xdf\xd8\xb8\xf32\x00\x00\xff\x00\xff\x00\x00\x00\x00\x00\x00\x00\x07\x00\x0c\x00\x00\xfd\x00\xfd\x00\xfd\x00}\x00\x1d\x00\r\x00\x01\x00\x00\x06\x00\x06\x00\x8f\x00\xff\x00\x7f\x00\x1f\x00\x0f\x00\x07\x00\xff\x00\x05\x06\xfb\xf8\xbfx]\xb2\xab\xd6[f\xf7\x0e\x00\x00\xf8\x00\xfc\x008\x00\x10\x00\x00\x00\x80\x00\x00\x00P\xdf\x04\xdf\xaf\xff\xc0\xff\x85\xe5\x00\xc0\x02\x82\x00\x00 \x00 \x00\x00\x00\x00\x00\x02\x00\x02\x00\x00\x00\x00\x00(\xef\x12\xff\x93\xff\x07\xffS\xdf!\'\x00\x03\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\xf8g\xf0o\xf3h\xf3d\x93d\xb3$\xf3d\xf9n\x00\x00\x00\x00\x04\x00\x08\x00\x08\x00H\x00\x08\x00\x00\x00x\x87\x87\xffL\xff\xbb|\x87H\x97X\xdf\x10\xe78\x00\x00\x00\x00\x00\x00\x00\x000\x00 \x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x01\x00\x03\x00\x07\x00\x00\x00\x00\x00\x1f\x1fbcD\xc68\xfd\x00\xf1\x00\x8d\x00\x00\x1f\x00`\x00\x80\x00\x01\x00\x83\x00\xcf\x00\xf3\x00\x00\x00{;\xfd\xdd\xfe.\xdf\xf7\xef\xfbw};>\x7f\x00\xc4\x00"\x00\xd1\x00\x08\x00\x04\x00\x02\x00\x01\x00\x00\x00\xff\xfd\x01\x00\x00\x00\x00\x00\x80\x80\xc0\xc0\xe0\xe0\xff\x00\x02\x00\x01\x00\x00\x00\x80\x00@\x00 \x00\x10\x00\x00\x00\xf4\xfe\xfa\xff\xfd\x7f~??\x1f\x1f\x0f\x0f\x07\xff\x00\x00\x00\x00\x00\x80\x00@\x00 \x00\x10\x00\x08\x00\x00\x03\x00\x03\x00\x03\x00\x03\x00\x06\x01\x01\x06\x06\x08\x08\x06\x00\x06\x00\x07\x00\x07\x00\x0f\x00\x0e\x00\x08\x00\x10\x00\x06\xf6\x08\xe8\x10\x90`a\x80\x82\x00\x05\x00\n\x00\x14\x08\x00p\x00\xe8\x00\x88\x00\x05\x00\x02\x00\x04\x00\x08\x00\x00-\x00X\x01\xb9\x00`\x00\xd0\x00\xa0\x00@\x00\xc0\x16\x00/\x00\\\x00\xac\x00H\x00\x98\x00(\x000\x00 \xe0\x10\xf0\x08\xf8\x04|\x0f\x1f\x00`\x00o`h\x00\x00\x00\x00\x80\x00\x80\x00`\x00\x1f\x00\x10\x00\x10\x00\x1d\x1a\x1a\x1d\x1d\x1e8?\xe0\xff\x00\x00\x00\xff\xb8\x88\x05\x00\x02\x00\x01\x00\x00\x00\x00\x00\xff\x00\x00\x000\x00\xf0p\xf88|\x9c\xbeN_\xa7\x00\x00\x00\xff\x00\x00\x88\x00\xc4\x00b\x00\xb1\x00X\x00\xff\x00\x00\x00\x00\x00\x00\x80\x00\x80\x00\x80\xc0@\xc0\xe0`r0\xb7\x17\xd7\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x00@\x00 \x00\x01\x01\x00\x01\x00\x01\x03\x02\x03\x07\x06N\x0c\xed\xe8\xeb\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x02\x00\x04\x00\xf0i\xf3o\x99g\xacc\xdf8\xee\x1dw\x8e3\xce\x06\x00\x00\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x18\xef\x18\xff\x80\xff\xc0\xff\x00p\xef0\xef\x0f\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x01\x00\x02\x00\x02\x00\x03\x00\x00\x06\x01\x01\x0e\x0e\x100 \xe0F\xc7x\xfe!\xf9\x0f\x00\x1e\x000\x00\xc0\x00\x00\x00\x00\x00\x01\x00\x06\x00p}\x90\x99\x04\x15\x0cm\x1c\x9dhm\x80\x89\x00\x15\x82\x00\x06\x00\n\x00\x12\x00b\x00\x82\x00\x06\x00\n\x00\x1d\x1f\x0e\x0f\x07\x07\x03\x03\x01\x01\x00\x00\x80\x80@\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0p\xf88|\x9c\xbe\xce\xdf\xe7\xef\xf3wy;<\x88\x00\xc4\x00b\x001\x00\x18\x00\x0c\x00\x06\x00\x03\x00\x07\x03\x03\x01\x01\x00\x00\x00\x00\x00\x80\x80\xc0\xc0\xe0\xe0\x04\x00\x02\x00\x01\x00\x00\x00\x80\x00@\x00 \x00\x10\x00\x10\x10 ` `\x13s\x0c\x7f\x00~\x00}\x00z`\x00\x80\x00\x80\x00\x80\x00\x80\x00\x81\x00\xc2\x00\xcc\x00\x00)\x00S\x80\xa3\x04\xa69}\x00\xf9\x01\xc9\x11\x11\x10\x00 \x00@\x00A\x00\x82\x00\x06\x00\x04\x00\x08\x00\x00\x80\x00\x00\x00\x00\x01\x01\x81\x81\x02\x03<\xbf\x00\x7fP\x00\x90\x00\xa0\x00\xa0\x00@\x00@\x00@\x00\x80\x00\x80\xe8\x80\xe8\x84\xec\x02\xee\x02\xee\x02\xee\x01\xef\x01\xef\x10\x00\x10\x00\x10\x00\x10\x00\x10\x00\x10\x000\x00p\x00\xdc\xc4\xae\xa2\x17\x11\x0b\x08\x05\x042\x129)\x1c\x14\x18\x00\x0c\x00\x06\x00\x03\x00\x01\x00 \x00\x10\x00\x08\x00\x00\x00\x00\x00\x00\x00\x80\x80\xc0@\xe0 p\x10\xb8\x88\x00\x00\x00\x00\x00\x00\x00\x00\x80\x00\xc0\x00`\x000\x00\x01\x01\x00\x00\x00\x00\x00\x00\x01\x01\x06\x06\x18\x18\x08\x08\x02\x00\x04\x00\x04\x00\t\x00\x0e\x00\x18\x00 \x00\x10\x00\x98\x98\x00\x00\x18\x18xx\xb0\xb000ppdl\x06\x00\x1c\x00d\x00\x84\x00\x08\x00\x08\x00\x08\x00\x10\x00\x00\x13\x00\x12\x00\x18\x01\x19\x03\x1a\x03\x19&2Dt\x0f\x00\x0f\x00\x07\x00\x06\x00\x05\x00\x06\x00\x0c\x00\x08\x00XI\xb0\x91\xe0\xa1\xc0A\x80\x83\x04\x04\n\x08\x14\x12\x90\x00 \x00B\x00\x84\x00\x08\x00\x13\x00\x07\x00\r\x00\x00\xc0\x00\xb0\x04\xa0\x08@ ` \xe0\x18\xf8\x07\xff?\x00@\x00D\x00\x88\x00\x80\x00\x00\x00\x80\x00\xc0\x00\x00\x03\x00\r \x05\x10\x02\x04\x06\x04\x07\x18\x1f\xe0\xff\xfc\x00\x02\x00"\x00\x11\x00\x01\x00\x00\x00\x01\x00\x03\x00\x00\xff@\xc0\x1f\x9f?\xbf?\xbf?\xbf?\xbf?\xbf\x00\x00?\x00`\x00@\x00@\x00@\x00@\x00@\x00\x0c\x0c\x07\x07\x07\x07\x03\x03\x01\x01\x01\x01\x00\x00\x00\x00\x10\x00\x08\x00\x08\x00\x04\x00\x02\x00\x02\x00\x01\x00\x01\x00co\xe0\xef\xc0\xdf\xc0\xdf\xc0\xdf\xc0\xdf\x80\xbe\x80\xbe\x10\x00\x10\x00 \x00 \x00 \x00!\x00C\x00O\x00\x000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xde\x00\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00t\x00h\x03S\x0c\x0f\x00\x0f\x00\x03\x00\x00\x00\x00\xf8\x00\xf0\x00\xe0\x00`\x00\x10\x00\x0f\x00\x03\x00\x03\x00!!\xc3\xe3\x02\xc2\x06\x86\x18\x98dd\x9c\x9cll\x10\x00\x10\x00!\x00A\x00\xc7\x00\x9a\x00b\x00\x82\x00\x00\x03\x1c<\x03#\x00\x80\x00\x91\x00O\x00\'\x00\'\xfc\x00\xc3\x00\xc0\x00`\x00`\x000\x00\x1f\x00\x1f\x00\x00\xef\x00\x0f\x80\xef\x10\xd3\x10\xd1"\xa0&\xa3LE\xf8\x00\xf8\x00<\x00n\x00c\x00\xc3\x00\xc4\x00\x88\x00\x8e\x8a\x87\x85C\xc2A\xc10\xf0\x0c\xfc\x03\x7f\x00\x9f\x04\x00\x02\x00\x01\x00\x00\x00\x00\x00\x80\x00\xe0\x00x\x00]E)!\x92\x93\xceO\xec\xafp_\xc0\xfe\x00\xf9\x18\x00\x08\x00\x00\x00\x80\x00@\x00!\x00\x07\x00\x1e\x00\x03\xff\x1c\xff7\xf8N\xf1\x9c\xe38\xc7`\x9f\x18\xff\x03\x00\x1f\x00?\x00\x7f\x00\xff\x00\xff\x00\xff\x00\xff\x00\xb8\xff\x1c\xff\x02\xff\x17\xef\x10\xefx\x87\xf0\x0f\xe0\x1f\xf8\x00\xfc\x00\xfe\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\x00\xff\x00\xff\xaa\xff\xff\xff\xff\xff\x1f\xff?\xff\x1f\xff\x00\x00\x00\x00\x00\x00\xf0\x00\xfc\x00\xfe\x00\xff\x00\xff\x00\x00\xff\x00\xff\xaf\xff\xff\xff\xf0\xff\xec\xf3\x9d\xe3\x1a\xe7\x00\x00\x00\x00\x07\x00\x0f\x00\x1f\x00\x7f\x00\xff\x00\xff\x00\x00\xff\x00\xff\xaa\xff\xff\xff\xff\xff\x1f\xff\xff\xff?\xff\x00\x00\x00\x00\x80\x00\xf0\x00\xf8\x00\xfc\x00\xfe\x00\xff\x00\r\xfe\x01\xfe\x83|\xc78\xc78\xcf0\xde!\xdf \xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xc1?\x8cs\x8cs\x98g8\xc7q\x8e\xf3\x0c\xf7\x08\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xe7\xff\x0c\xff\x10\xff\x06\xf9\x9f`\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\x04\xffr\xfd\xfe\xf9\x1f\xfc#\xde\xa0_\xf2\r\xf7\x08\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\x1d\xffx\xff\xde9\xef\x19\xff\x08\xf7\x08w\x88w\x88\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\xff\xff\xff\xfe\xff\xbf\xff\x0e\xff\x8d~\xcb<\xf7\x18\xe0\x00\xf0\x00\xf8\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\xff\xfb\xff\xe9\xff\x98\xff(\xf7\x99g=\xc3~\x81>\x00\x7f\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\xff\xff\xff\xf5\xf5p\xf0\xf8\xf8\x9c\xfc\xd2.\xfe\x01\x00\x00\x80\x00\xea\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x00\xff\xaa\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xffUU\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\x00\x00\x00\x00\x03\x00\x06\x02\x0c\x04\x19\t3\x12&\x04\x00\x00\x00\x00\x00\x00\x01\x00\x03\x00\x06\x00\x0c\x00\x18\x00\x00\x00\x00\x00\xc0\x00`@0 \x98\x90\xccHd \x00\x00\x00\x00\x00\x00\x80\x00\xc0\x00`\x000\x00\x18\x00<\x00$\x00$\x00$\x00$\x00$\x00$\x00<\x00\x00\x00\x18\x00\x18\x00\x18\x00\x18\x00\x18\x00\x18\x00\x00\x00\xe7$\xc3B\x81\x81\x00\x00\x00\x00\x81\x81\xc3B\xe7$\x18\x00<\x00~\x00\xff\x00\xff\x00~\x00<\x00\x18\x00\x00\x00\x01\x00\x06\x00\t\x01\x0f\x07\x0f\x07\x1f\x0f\x1e\x0f\x00\x00\x01\x00\x07\x00\x0e\x00\x08\x00\x08\x00\x10\x00\x10\x00\xd8\x1f\x0c\x0f\xea\xef\xf9\xff\xfb\xff\xfe\xff\xfd\xff\xfe\xff\xf8\x00\xfc\x00\x1a\x00\t\x00\x0b\x00\x0e\x00\r\x00\x0e\x007\xce\x16\xefP\xef]\xe2\xdf\xe0\xcb\xf6\x8d\xf2\x87\xf8?\x00?\x00\x7f\x00\x7f\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x07\xf3\x0f\xf0\x0f\xfe\x01\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\x9a~\x95|\xaax\xcb9\xd5?\xed\x1f\xb5O\x15\xef\xf9\x00\xf2\x00\xe5\x00\xe4\x00\xf0\x00\xfc\x00\xfc\x00\xfc\x00\n\x07\n\x07\x05\x03\x04\x03\x04\x03\x02\x01\x01\x00\x00\x00\x0f\x00\x0f\x00\x07\x00\x07\x00\x07\x00\x03\x00\x01\x00\x00\x00\xff\x00\xff\x7f\xffo\xefV\xfeY\xefM\xe5G\xf7W\x00\x00\x7f\x00o\x00F\x00@\x00P\x00X\x00H\x00\xff\x00\xe0\xdf\xdc\x1f<\x1f \x1f\x10\x00\x10\x00\x12\x002\x006\x00=\x003\x00?\x00?\xff?\xff\xc4\xff\x86\xff\x03\xff\x03\xfe\x07\xf8\x0f\xf1\x17\x00\x1b\x00=\x00~\x00\xff\x00\xff\x00\xff\x00\xff\x00\x87\xf8\x8f\xf0?\xcc\x7f\xbe\xcf\x7f\xc7\xff\xe0\xff\xf0\xff\xff\x00\xff\x00\xff\x00\xdf\x00\x8f\x00\x87\x00\xc3\x00\xe7\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xfc\x83p\x8f8\xdf\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xfc\x00\xf0\x00\xf8\x00U\xefU\xefU\xef\x95o\x15\xef\x0c\xf7\x04\xfb\x04\xfb\xfc\x00\xfc\x00\xfc\x00\xfc\x00\x1c\x00\x0c\x00\x04\x00\x0c\x00\x80\x7f\xb2\x7f\xa1~\x83|\x86y\x8cs\x8cs\x98g\xf0\x00\xfa\x00\xfd\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xe7W\xe5W\xed_\xed_\xfdO\xf5O\xf8G\xe4[H\x00H\x00@\x00@\x00`\x00`\x00`\x00@\x00\xd2\xd6\xf3\xf7\xe5\xf7\xea\xfb\x80\xfcE\xc7\xba\x83\xed\xa9)\x00\x08\x00\x08\x00\x04\x00\x03\x008\x00D\x00\x12\x00\xae\xe4\xe3j\xe3i\xd1D\xa0\x02\xdf\x00\xaf\x1f\xa3c\x13\x00\x15\x00\x16\x00;\x00}\x00`\x00@\x00\x1c\x00\xba\xc6E|\xbb9\xd6\x92\xedD\xffDUEEE\x01\x00\x83\x00\xc6\x00m\x00\xbb\x00\xba\x00\xba\x00\xba\x00\xd3\xc3\xd9\xc1\xb5\x81c\x00\xc3\x00\x83\x01\x87\x03\xff\x04<\x00>\x00~\x00\xfe\x00\xfe\x00\xfe\x00\xfc\x00\xf8\x00\xc4f\xa2\xb3p9\xa8-PE\xec%\xf4\xd5\xb8\xad\x99\x00L\x00\xc6\x00\xd2\x00\xba\x00\x1a\x00\n\x00B\x00\xbc\xfd|}xy\xf8y`a;\x01\xbf\xe1\x8e\xe2\x02\x00\x82\x00\x86\x00\x86\x00\x9e\x00\xfe\x00\x1e\x00\x1d\x00\x00\x00\x00\x00\x00\x00\x01\x00\x03\x01\x06\x03\x0c\x07\x1e\x0f\x00\x00\x00\x00\x00\x00\x01\x00\x02\x00\x04\x00\x0c\x00\x1e\x00\x00\x00\x07\x00y\x00\xf2r\xaf\xef\x17\xff$\xff\x01\xff\x00\x00\x07\x00~\x00\x8d\x00\x10\x00\x00\x00\x00\x00\x00\x00\x01\x00\xc7\x01\xfb\x07u\x0f\xa9\x97\xc9\xd7j\xf6\x8a\xf6\x01\x00\xc6\x00<\x00\x9c\x00\\\x00<\x00\x1d\x00}\x00 \x1f \x1f1\x0e\x12\r\x13\r\x17\x0b\x17\x0b\x15\x0b?\x00?\x00?\x00\x1e\x00\x1f\x00\x1f\x00\x1f\x00\x1d\x00?\xc1w\x9b\xf7{\xcb\xf7\x0b\xf7\x8d\xf3\x86\xf9\x87\xf8\xff\x00\xe7\x00\x87\x00\x0f\x00\x0f\x00\xbf\x00\xff\x00\xff\x00\xe0\xff\xe0\xff\xc0\xff\xc9\xfe\x82\xfd\x84\xfb\x08\xf7\xd1/\xff\x00\xff\x00\xff\x00\xff\x00\xf7\x00\xfd\x00\xfd\x00\xfd\x00<\xdf~\xbf|\xbf\xf8?x\xbf9\xdf\xd0\xef\xe8\xf7\xfc\x00\xfe\x00\xfd\x00\xfb\x00\xfb\x00\xff\x00\xff\x00\xff\x00\x06\xfb\x06\xfb\x07\xfb\x8b\xf7\x8b\xf7\x13\xef\x11\xef!\xdf\x1e\x00\xfe\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00p\x8f\xe8\x17\x84{b\xfdy\xfe=\xfe\x1c\xff\x10\xff\x7f\x00\xff\x00\xbf\x00\x7f\x00\x7f\x00?\x00_\x00\xff\x00\xefX\xecZ\xecZ\xeb]\xef]\xf7N\xfbW\xfdZ@\x00A\x00A\x00@\x00@\x00`\x00@\x00@\x00\xfe<_=\xbe\x82\xcfLP\xb0\xbf\xc0\xfbgY\xb7\x01\x00\x80\x00A\x000\x00\x0f\x00\x00\x00\x00\x00\x00\x00\x0c%)#+&\x86\x0c\xe6\x0c6\x0c.4^t\xd2\x00\xd4\x00\xd1\x00\xf3\x00\xf3\x00\xf3\x00\xc3\x00\x83\x00\xc6\x83\xbb\x01E8\xba|\xfeD\xfeD\xfeD}\xa3|\x00\xc6\x00\x83\x00\x01\x00\x01\x00\x01\x00\x19\x00\x18\x00n\xfagjwvwrwr;9\x1d\x1c\x0e\x0e\x05\x00\x95\x00\x89\x00\x8d\x00\x8d\x00\xc6\x00\xe3\x00\xf1\x00E|T\xfc\x8a\xee\xc5g\xc2s\xc1i\xc8l\xc8n\x82\x00\x03\x00\x11\x00\x98\x00\x8c\x00\x96\x00\x93\x00\x91\x00\x00\x00\x03\x00\x07\x03\x0c\x07\x0c\x07\x1c\x0f\x1f\x0c<\x03\x00\x00\x03\x00\x04\x00\x08\x00\x08\x00\x18\x00\x1f\x00<\x00~\x1f\xff?\xdf?o\x9fo\x9f3\xcf\x9cc_\xa4~\x00\xff\x00\xff\x00\x7f\x00\x7f\x00\x7f\x00\xff\x00\x7f\x00\x00\xff\x80\xff\xc0\xff\xd4\xff\xc8\xff\x80\xff\x81\xfe\x0f\xf1\x01\x00\x87\x00\xdf\x00\xff\x00\xfb\x00\xfb\x00\xff\x00\xff\x00~\x81\xe7~\x91~\xb1~\x85~\x93~\xc3~~\x81\x00\x00~\x00~\x00~\x00~\x00~\x00~\x00\x00\x00\x0f\x07\x0f\x07\x1e\x0f\x1e\x07<\x03?\x00\x15\x0e\x12\x0f\x0f\x00\x0f\x00\x1f\x00\x1f\x00?\x00?\x00\x17\x00\x1b\x00\x07\xf8\x0f\xf0\x1f\xe08\xc7\xf3\x1f\xf9?\x9a\x7f\x88\x7f\xff\x00\xff\x00\xff\x00\xff\x00\xf3\x00\xf9\x00\xd8\x00\xe8\x00\xf3\x0f\xf7\x0f\xa5_-\xdf.\xdf\xee\xdf\xec\xdfh\x9f\xfd\x00\xfd\x00\xfd\x00\xfd\x00\xfe\x00\xfe\x00\xfd\x00{\x00\xf7\xf8\xf8\xff\xff\xff\xde\xff\xb9\xff\x02\xfb\x0c\xff2\xff\xff\x00\xff\x00\xff\x00\xde\x00\x99\x00\x07\x00\xfd\x00\xff\x00\xe1\x1f`\x9f"\xdf#\xdf\x13\xef\x11\xef\x10\xef\x18\xe7\xff\x00\xff\x00\x7f\x00\xff\x00\xff\x00\xff\x00\xff\x00\xff\x00\xa1\xff\xc1\xff\x10\xff3\xfc\xff\x01\xfa\x1d\xed>H\xbf\xbf\x00\xff\x00\xff\x00\xff\x00\xff\x00\xef\x00\xcf\x00\xcf\x00\xef]\xf7M\xfae\xffp\xff|\xff\x7f\x80\x7f\xff\x00@\x00`\x00p\x00x\x00\x7f\x00\x7f\x00\x7f\x00\x00\x00\xea\xd7;\'\xd9\xd7z\xf7\xfb\x07\xf8\xf6\x04\xfb\xff\x00\x00\x00\xc0\x00 \x00\x00\x00\x00\x00\xf1\x00\xf8\x00\x00\x00\xff\x007\xedK\xd9\x91\xb3 d@I\x01\x13\x02&\x00\x00\x00\x00$\x00L\x00\x9b\x00\xb6\x00\xec\x00\xd9\x00\xff\x00\xfeW\xfeWV\xff\x02\xab\x80\xa9)m|\xc6\x00\x00\x00\x00\x00\x00\x00\x00T\x00V\x00\x92\x009\x00\n\xf6\x08\xf4\x14\xec*\xdaN\xbe\xc2>\xd3?\xca>\xfd\x00\xff\x00\xfb\x00\xf5\x00\xe1\x00\xe1\x00\xf0\x00\xf9\x00\xfc\x03\xaea\xafdA\xca\x89\x9a\x91\xb2!\xe6\xa1\xee\x00\x00\x10\x00\x10\x004\x00d\x00L\x00\x18\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbcCt\x0b:\x05\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x00\x7f\x00?\x00\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x18\xff\t\xfe\x02\xfd\xe7\x19\x1e\x01\x01\x00\x00\x00\xff\x00\xff\x00\xff\x00\xfe\x00\xff\x00\x1f\x00\x01\x00\x00\x00P\xbf\xe0_\xf1\xee~\xf0\xc2\xfc\x02\xfc\xfc\x00\x00\x00\xff\x00\xbf\x00\x7f\x00~\x00\xfe\x00\xfe\x00\xfc\x00\x00\x00@\x80\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc0\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x009\xc6\x7f\x81\xf9\x079\xf7\xe4\xfb$\xfb\x06\xf9\xf9\x00\xff\x00\xff\x00\xff\x00?\x00\xff\x00\xff\x00\xff\x00\xf9\x00'), + BasePatchData(address=46567, patch_data=b'6'), + BasePatchData(address=160226, patch_data=b"u\x83\x83\x83\x91\x83\x9f\x83\xad\x83\xbb\x83\xc9\x83\xd7\x83\xe5\x83\xf3\x83\x01\x84\x0f\x84\x1d\x84+\x849\x84G\x84U\x84c\x84q\x84\x7f\x84\x8d\x84\x9b\x84\xa9\x84\xb7\x84\xc5\x84\xd3\x84\xe1\x84\xef\x84\xfd\x84\x0b\x85\x19\x85'\x855\x85C\x85Q\x85_\x85m\x85{\x85\x89\x85\x97\x85\xa5\x85\xb3\x85\xc1\x85\xcf\x85\xdd\x85\xeb\x85\xf9\x85\x07\x86\x15\x86#\x861\x86?\x86M\x86[\x86i\x86w\x86\x85\x86\x93\x86\xa1\x86\xaf\x86\xbd\x86\xcb\x86\xd9\x86\xe7\x86\xf5\x86\x03\x87\x11\x87\x1f\x87-\x87;\x87I\x87W\x87e\x87s\x87\x81\x87\x8f\x87\x9d\x87\xab\x87\xb9\x87\xc7\x87\xd5\x87\xe3\x87\xf1\x87\xff\x87\r\x88\x1b\x88)\x887\x88E\x88S\x88a\x88o\x88}\x88\x8b\x88\x99\x88\xa7\x88\xb5\x88\xc3\x88\xd1\x88\xdf\x88\xed\x88\xfb\x88\t\x89\x17\x89%\x893\x89A\x89O\x89]\x89k\x89y\x89\x87\x89\x95\x89\xa3\x89\xb1\x89\xbf\x89\xcd\x89\xdb\x89\xe9\x89\xf7\x89\x05\x8a\x13\x8a!\x8a/\x8a=\x8aK\x8aY\x8ag\x8au\x8a\x83\x8a\x91\x8a\x9f\x8a\xad\x8a\xbb\x8a\xc9\x8a\xd7\x8a\xe5\x8a\xf3\x8a\x01\x8b\x0f\x8b\x1d\x8b+\x8b9\x8bG\x8bU\x8bc\x8bq\x8b\x7f\x8b\x8d\x8b\x9b\x8b\xa9\x8b\xb7\x8b\xc5\x8b\xd3\x8b\xe1\x8b\xef\x8b\xfd\x8b\x0b\x8c\x19\x8c'\x8c5\x8cC\x8cQ\x8c_\x8cm\x8c{\x8c\x89\x8c\x97\x8c\xa5\x8c\xb3\x8c\xc1\x8c\xcf\x8c\xdd\x8c\xeb\x8c\xf9\x8c\x07\x8d\x15\x8d#\x8d1\x8d?\x8dM\x8d[\x8di\x8dw\x8d\x85\x8d\x93\x8d\xa1\x8d\xaf\x8d\xbd\x8d\xcb\x8d\xd9\x8d\xe7\x8d\xf5\x8d\x03\x8e\x11\x8e\x1f\x8e-\x8e;\x8eI\x8eW\x8ee\x8es\x8e\x81\x8e\x8f\x8e\x9d\x8e\xab\x8e\xb9\x8e\xc7\x8e\xd5\x8e\xe3\x8e\xf1\x8e\xff\x8e\r\x8f\x1b\x8f)\x8f7\x8fE\x8fS\x8fa\x8fo\x8f}\x8f\x8b\x8f\x99\x8f\xa7\x8f\xb5\x8f\xc3\x8f\xd1\x8f\xdf\x8f\xed\x8f\xfb\x8f\t\x90\x17\x90%\x903\x90A\x90O\x90]\x90k\x90y\x90\x87\x90\x95\x90\xa3\x90\xb1\x90\xbf\x90\xcd\x90\xdb\x90\xe9\x90\xf7\x90\x05\x91\x13\x91!\x91/\x91=\x91K\x91Y\x91g\x91u\x91\x83\x91\x91\x91\x9f\x91\xad\x91\xbb\x91\xc9\x91\xd7\x91\xe5\x91\xf3\x91\x01\x92\x0f\x92\x1d\x92+\x929\x92G\x92U\x92c\x92q\x92\x7f\x92\x8d\x92\x9b\x92\xa9\x92\xb7\x92\xc5\x92\xd3\x92\xe1\x92\xef\x92\xfd\x92\x0b\x93\x19\x93'\x935\x93C\x93Q\x93_\x93m\x93{\x93\x89\x93\x97\x93\xa5\x93\xb3\x93\xc1\x93\xcf\x93\xdd\x93\xeb\x93\xf9\x93\x07\x94\x15\x94#\x941\x94?\x94M\x94[\x94i\x94w\x94\x85\x94\x93\x94\xa1\x94\xaf\x94\xbd\x94\xcb\x94\xd9\x94\xe7\x94"), + BasePatchData(address=1770357, patch_data=b'A!\x13"\x07=\x00\x00\x00\x10\xc0\x00\x00\x04\xc0\x00\x00\x04\x00\x00\x00\x00\x00\x00r\x00PR\xc0\x1d\x04\x06\x00\x14\x00\x00\x00\x00\x11\x00\x18\r\xc0\x07\x06\x19\x00\x00\x00\x00\x0c\x02\x12\x00\x00\x00\x00\x18\r&\x00&\x14\x00\x00\x00\xb5\x00\x08\x08\x00\x08\x08\x14\x00%\x00 \x06\x05\x0c\x00%\x00\x00\x08\x08\x14\x00%\x00 \x06\x05\x0c\x00%\x00 \x06\x05\x0c\x00%\x00\x00\x00\x17\x17\xc0\x07\x06\xc0\x07\x06\x07\x00\x00\x00\x00\x0f\x07\x19\x00\'\x00\x00\x0f\x07\x19\x00\'\x00\x00\x00KJJ\x00\x0f\x00\x0f\x07\x19\x00\'\x00\x00\x00\t:\x01\x0f\x07\x01\x0f\x07\x19\x00\x03\x00\x00\x00j\x1b\xc0(\x0e\xc0(\x0e\x13\x00\x00\x00\x00\x00\x00k\x8c\x8c@@\x1b\x0e\x18\x058\x00\x00\x13\x0b\x1c\x00\x08\x00\x00\x13\x0b\x1c\x00\x08\x00\x00\x00\x00\x1e\x00!\x13\x00!\x13"\x00\x00\x00\x00\x01\x01\x01\x00\x00\x00\x00!\x13"\x00\x00\x00\x00\x01\x01\x01\x00\x00\x00\x00\x01\x01\x01\x00\x00\x00\x08\x00\x00\x02\xc0\x1d\x04\xc0\x1d\x04\x06\x00\x00\x00\x00\x18\r&\x00\x00\x00\x00\x18\r&\x00\x00\x00\x00\x18\r\x1e\x00\x00\x00 \x18\r&\x00\x00\x00\xc0\x18\r&\x00\x00\x00\xc0\x18\r&\x00\x00\x00\x00\x00\x00\xb6\x90\x08\x08\x90\x08\x08\x11\x03\x00\x00\x00\x00\x00f \x06\x05 \x06\x05\x19\x005\x00\x00\x00\'\x07\'\x01\x0f\x00\x07\x06\x07\x00\x00\x00\x00"\x12\x07\x00\x00\x00\x01\x0f\x07\x19\x00\x00\x00\x00\x0f\x07\x19\x00\x16\x00\x00\x0f\x07\x19\x00\x16\x00\x00\x00\x00jjh\x0fh\x0f\x07\x08\x00\x03\x1c\x00\x00\x00\x0b\x00\x1a\x0e\x00\x1a\x0e\t\x00\x04?\x00\x00\x00\x8c\x00\x1b\x0e\x00\x1b\x0e\x18\x00\x00\x00\x00\x00\x00L \x13\x0b \x13\x0b\x1c\x00\x17\x00\x00\x00>\x0e\x00\x13\x0b\x00\x13\x0b)\x00\x17\x00\x00\x00\x00? \x0c\x02 \x0c\x02\x12\x00\x15%\x01\x01\x01\x01\x00\x00\x00\x01\x01\x01\x01\x00\x00\x00\x00\x18\r&\x00\x01\x00\x01\x01\x01\x01\x00\x00\x00\x00\x18\r&\x00\x01\x00\x00\x18\r&\x00\x00\x00\x00\x18\r\x1e\x00\x00\x00\x00\x18\r&\x00\x01\x00\x00\n\x08\x11\x00\x16\x00\x00\n\x08\x11\x00\x16\x00\x00\x00\x00vvv \x00\n\x08\x11\x00\x16\x00\x00\x00\x00vvv \x06\x05\x19\x006\x00\x00\x001\x171\x80\n\x80\n\x08\x11\x002\x1b\x00\x00\x008\xcc\x0e\t\xcc\x0e\t\x1a\x02%\x00\x00\x0f\x07\x19\x00\x00\x00\x00\x0f\x07\x19\x00\x00\x00\xc0\x0f\x07+\x00\x16\x00\xc0\x0f\x07+\x00\x16\x00\x00\x00\x00;\x00\x13\x0b\x00\x07\x06\x07\x00\x00\x00\x00"\x12\x07\x00\x00\x00\x00\x13\x0b\x1c\x00*\x00\xc0\x07\x06\x19\x00\x00\x00\x00\x13\x0b\x1c\x00*\x00\xc0\x07\x06\x19\x00\x00\x00\xc0\x07\x06\x19\x00\x00\x00\x00\x0c\x02\x12\x00\x00\x00\x00\x0c\x02\x12\x00\x00\x00\x00\x00\x00@ \x06\x05 \x06\x05\x19\x007\x04"\x00w\'w\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00B\x00\x04\x05\x00\x04\x05\x0b\x00\x15%\x80\n\x08\x11\x00\x00\x00\x80\n\x08\x11\x00\x00\x00\x00\x00\x00T\x80\n\x08\x80\n\x08\x11\x00\x00\x19\x80\n\x08\x11\x00\x00\x00\x80\n\x08\x11\x00\x00\x00\x80\n\x08\x11\x00\x00\x00\x80\n\x08\x11\x00\x00\x19\x80\n\x08\x11\x00\x00\x00\x80\n\x08\x11\x00\x00\x00\x00\x00\x00( \r\t \r\t\x13\x00\x00\x00\x00\x00) \x0f\x07\x19 \x0f\x07\x19\x00\x00\x00\x00\x00\n\n\x00\x0f\x07\x00\x0f\x07\x08\x00\x00\x00\x00\x00\x00+\x00\x07\x06\x00\x07\x06\x13\x00\x00\x00 \x1a\x0e\x0c\x003\x00 \x1a\x0e\x0c\x003\x00\x00\x00\x96\x96\xcc\x13\x0b\xcc\x13\x0b)\x02\x02\x00\x00\x00\x00\x1e\x00\x13\x0b\x00\x13\x0b)\x00\'\x14\x00\x00\x00\x1f_\xc0\x00\xc0\x00\x02\'\x00\x00\x00\x00\x00\x000\xb0\x01\x00\x01\x00\x00\x02\x00\x13\x00\x00\x00\x00B\x01\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00A2h\x04h\x04\x05\n\x00\x00\x1d\x00\x17\n\x1b\x00\x01\x00\x00\x17\n\x1b\x00\x01\x00`\x17\n\x1b\x00\x01\x00`\x17\n\x1b\x00\x01\x00\x00\x00\x00\xbc\x00\n\x08\x00\n\x08\x11\x00<\x00\x00\r\t\x13\x0034\x00\r\t\x13\x0034\x00\x0f\x07\x19\x00\x17\x00\x00\r\t\x13\x0034\x00\x0f\x07\x19\x00\x17\x00\x00\r\t\x13\x0034\x00\x0f\x07\x19\x00\x17\x00\x00\x0f\x07\x19\x00\x17\x00\x00\x00\x00\t\t\x00\x0f\x00\x0f\x07\x08\x00\x01\x00\x00\x00\t\x00\x1a\x0e\x0c\x00\x1a\x0e\x0c\x00\x00\x00\x00\x00\x00\x1d \x1a\x0e \x1a\x0e\x0c\x002?\x00\x00\xa6\xa6\x00\x13\x0b\x00\x13\x0b)\x00\x17\x00\x00\x00\x00n\x00\x13\x0b\x00\x13\x0b\x1c\x00\x00\x00\x00\x00\xbe\xc0\x00\x00\x04\xc0\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01\xc0\x00\x00\x03\x00\x00\x00\x00\x00\x00a\xc0\x00\x00\xc0\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01\xc0\x04\x05\n\x00\x03\x00\x00\x00\x00c \n\x08 \n\x08\x11\x00\x00\x00\x00\x0044\x01\x01\x10\x01\x01\x10\r\x00\x00\x00\x00\r\t\x13\x00#\x00\x00\r\t\x13\x00#\x00\x00\r\t\x13\x00\x16\x00\x00\r\t\x13\x00\x16\x00\x00\r\t\x13\x00!(\x00\r\t\x13\x00!(\xc0\r\t\x13\x00\x00\x00\xc0\r\t\x13\x00\x00\x00\x00\x10\x07\x15\x00%\x00\x00\x10\x07\x15\x00%\x00\xc0\x1b\x0e\n\x00\x17\x00\xc0\x1b\x0e\n\x00\x17\x00\x00\x1b\x0e\n\x00\x00\x00\x00\x1b\x0e\n\x00\x00\x00\x00\x00\x00]\x00$\x0e\x00$\x0e#\x00\t\x00\x00\x00\x00\\ \x13\x0b \x13\x0b\x1c\x00\x00\x00\x00\x00~~\x00\x13\x0b\x00\x13\x0b\x1c\x00\'\x00\x00\x00\x00?\x7f\xc0\x00\xc0\x00\x00\x04\x00\x00\x00\xc0\x00\x00\x04\x00\x00\x00\xc0\x00\x00\x04\x00\x00\x00\x08\x00\x00Q\x00\t\x05\xc0\x00\x00\x04\x00\x00\x00\xc0\x00\x00\x04\x00\x00\x00\x00\t\x05\n\x00\r\x00\x00\x00\x00S\xe0#\n\xe0#\n!\x00\x17\x00\x00\x00\x00\xab\xe0#\n\xe0#\n!\x00\x00\x00\x00\x00\xac\xc0\n\x08\x11\xc0\n\x08\x11\x00<\x00\x00\x00\x00\x16\x00\r\t\x00\r\t\x13\x00"\x00\x00\r\t\x13\x00\x00\x00\x00\r\t\x13\x00\x00\x00\x01\x0f\x07\x19\x00\x00\x00\x01\x0f\x07\x19\x00\x00\x00\x00\x00\x00\x1a\x1a\x00\x1b\x01\x0f\x07\x19\x00\x00\x00\x00\x00\x00\x1a\x1a\x00\x1b\x00\x1b\x0e\n\x00\x08\x0b\x00\x00\x00\x0c\x00$\x0e\x00$\x0e#\x00\x03?\x00\x00\x00\xa5\x00$\x0e\x00$\x0e#\x00\x05\x00\x00\x13\x0b\x1c\x00\x02\x00\x00\x13\x0b\x1c\x00\x02\x00\x00\x00\x00N\x00\x01\x01\x00\x01\x01\x04\x00\x00\x00\x08\x00\x00q\x80\xc0\x01\x00\x01\x01\x04\x00\x00\x00\x08\x00\x00q\x80\xc0\x01\xc0\x01\x01\x04\x00\x08\x00\x00\x00\x00p\xc0\x01\x01\xc0\x01\x01\x04\x00\x00\x00\x08\x00\x00\x01\x00\t\x05\x00\t\x05\n\x00\x17\x00\x00\t\x05\n\x00\'\x00\x00\t\x05\n\x00\'\x00\x00\t\x05\n\x00\x01\x00\x00\t\x05\n\x00\x01\x00\x80\n\x08\x11\x00\x00\x18\x80\n\x08\x11\x00\x00\x18\x00\x00\x00&&&\xc0\xc0\x06\x05\x19\x00\x00\x00\x00\x00\xa71\x87\x87\x00\x00(\x0e\x13\x00\x039\x00\x00\x9d\x00(\x0e\x13\x00(\x0e\x13\x00\x039\x00\x00\x9d\x00(\x0e\x13\x00(\x0e\x13\x00\x039\x00\x00\x9d\x00(\x0e\x13\x00(\x0e\x13\x00\x039\x00\x00\x9d\x00(\x0e\x13\x00(\x0e\x13\x00 \x00\x00(\x0e\x13\x00\x04<\x00(\x0e\x13\x00\x04<\x00\x00\x9b \x13\x0b\x1c \x13\x0b\x1c\x00+\x17\x00\x00\x9e^\x00\x13\x0b\x00\x13\x0b\x1c\x00\x00\x00\x00\x00\x00_`\x01\x01`\x01\x01\x04\x00\x00\x00\x00\x00\x00p\xc0\x01\x01\xc0\x01\x01\x04\x00\x00\x00\x00\t\x05\n\x00\r\x00\xc0\x01\x01\x04\x00\x00\x00\x00\t\x05\n\x00\r\x00\x00\t\x05\n\x00\r\x00\x00\t\x05\n\x00\x00\x00\x00\t\x05\n\x00\x00\x00\x00\t\x05\n\x00\x02\x00\x00\t\x05\n\x00\x02\x00\x00\x06\x05\x19\x00>\x01\x00\x06\x05\x19\x00>\x01(\x00\x00ww\x00\x0b\x00\x06\x05\x19\x00>\x01(\x00\x00ww\x00\x0b\x00\x0b\x05\x08\x00\x00\x00\x02\x00\xa9\x00(\x0e\x13\x00\x0b\x05\x08\x00\x00\x00\x02\x00\xa9\x00(\x0e\x13\x00(\x0e\x13\x00:\x0c (\x0e\x13\x00\x16\x00\x00(\x0e\x13\x00:\x0c (\x0e\x13\x00\x16\x00 (\x0e\x13\x00\x16\x00(\x00\x1c\x0c\x0c\x1c\x00\x00(\x0e\x13\x003)\x00\x13\x0b\x1c\x00\x00\x00\x00\x13\x0b\x1c\x00\x00\x00\x00\x00\x00\xae\x80\x12\x0c\x80\x12\x0c\x16\x00%\x00\x00\x11\x0c\x1c\x00\x00\x00\x80\x12\x0c\x16\x00%\x00\x00\x11\x0c\x1c\x00\x00\x00\x00\x11\x0c\x1c\x00\x00\x00\x00\x00\x00\xa0\x01\x11\x0c\x01\x11\x0c\x1c\x00\x00\x00\x01\x11\x0c\x1c\x00\x16\x00\x01\x11\x0c\x1c\x00\x16\x00\x08\x00\x00\xa2\x00%\x0e\x00%\x0e$\x00\x00\x00\x00%\x0e$\x003\x00\x00%\x0e$\x00\x00\x00\x00%\x0e$\x003\x00\x00%\x0e$\x003\x00\x00\x00\x00=h\x11\x0ch\x11\x0c\x1d\x00\x1c\x00\x00\x00\xd1\xd1\x00\x11\x0c\x00\x11\x0c\x1c\x00\x00\x00\x00\x00\x00\xd2\x01\x0b\x05\x01\x0b\x05\x08\x00\x00\x00\x00\x00\x00\xda\x00(\x0e\x00(\x0e\x13\x00\x00\x00\x00\x00}\x00(\x0e\x13\x00(\x0e\x13\x00\x00\x00\x00\x00}\x00(\x0e\x13\x00(\x0e\x13\x06\x00\x00\x00(\x0e\x13\x06\x00;\x00(\x0e\x13\x06\x00;\x00\x00{ \x13\x0b\x1c \x13\x0b\x1c\x00\x00\x00\x00\x00\xbe\xbe\x00\x13\x0b\x00\x13\x0b\x1c\x00\x17\x00\x00\x12\x0c\x1d\x00\x00\x00\x00\x12\x0c\x1d\x00\x00\x00\x00\x00\x00\x91\x00\x11\x0c\x00\x11\x0c\x1d\x00\x00\x00\xc0\x11\x0c\x1d\x00\x00\x00\xc0\x11\x0c\x1d\x00\x00\x00\x00\x00\x00\x93`\x19\r\x00\x11\x0c\x1d\x00\x00\x00\xc0\x11\x0c\x1d\x00\x00\x00`\x19\r\x17\x04%\x00\x00%\x0e$\x00\x07\x00\x00%\x0e$\x00\x07\x00\x00\x00\x00l\x00%\x0e\x00%\x0e$\x00\x00\x00\x00\x00\x00M\x00\x06\x05\x00\x06\x05\x19\x00\x00\x00\x00\x00\x17\xc0\x0b\x05\x08\xc0\x0b\x05\x08\x00\x03\x00\xc0\x0b\x05\x08\x00\x17\x00\xc0\x0b\x05\x08\x00\x17\x00\x00\x00\x89\xc0\x0b\x05\x08\xc0\x0b\x05\x08\x00\x17\x00\x00\x17\n\x1b\x00\x00\x00\x00\x17\n\x1b\x00\x00\x00\x00\x00\x00d\xe0\x17\n\xe0\x17\n \x00%\x00\x00\x13\x0b\x1c\x00\'\x00\x00\x13\x0b\x1c\x00\'\x00\x00\x00\x00\x8e\x00\x13\x0b\x00\x13\x0b\x1c\x00\'\x00\x00\x00\x00\x8e\x00\x13\x0b\x00\x13\x0b\x1c\x00\x00\x00\x00&\x02!\x00\x05\x02\x00&\x02!\x00\x05\x02\x08\x00\x00@\xc0\x00\x11\x00\x11\x0c\x1d\x00\x00\x00\x02\x00\xb2\xc0\x11\x0c\x1d\xc0\x11\x0c\x1d\x00\x03\x0e\xc0\x11\x0c\x1d\x00\'\x00\xc0\x11\x0c\x1d\x00\'\x00\x00\x19\r\x17\x00\x00\x00\x00\x19\r\x17\x00\x00\x00\x00\x00\x00\xc4\x01\x18\r\x01\x18\r%\x00\x17\x00\x00\x00\x00\x04\x00\x18\r\x00\x18\r\x1e\x00\x04<\x00\x00\x00\x15\x00\x0b\x05\x00\x18\r\x1e\x00\x00\x00 \x18\r&\x00\x00\x00\x00\x0b\x05\x08\x00\'\x00\xc0\x0b\x05\x08\x00\x00\x00\xc0\x0b\x05\x08\x00\x00\x00\x01\x0b\x05\x08\x00\x17\x00\x01\x0b\x05\x08\x00\x17\x00@\x17\n\x1b\x00\x00\x00@\x17\n\x1b\x00\x00\x00\x00\x17\n\x1b\x00\x17\x00\x00\x17\n\x1b\x00\x17\x00\x00\x00\x00E\x00\x13\x0b\x00\x13\x0b)\x00\x16\x00\x00\x00O\x9e\x00\x13\x0b\x00\x13\x0b)\x00\x16\x00\x00\x00O\x9e\x00\x13\x0b\x00\x13\x0b)\x00\x00\x00\x01\x00\x02\'\x00\x02\x0f\x01\x00\x02\'\x00\x02\x0f\x00\x00\x00\xb0\xd0\x00\x11\x00\x11\x0c\x1d\x003\x00\xc0\x11\x0c\x1d\x00\'\x00\xc0\x11\x0c\x1d\x00\'\x00\xc0\x11\x0c\x1d\x00\x00\x00\xc0\x11\x0c\x1d\x00\x00\x00\x00\x18\r%\x00\x00\x00\x00\x18\r%\x00\x00\x00\x00\x00\x00\xb4\x00\x18\r\x00\x18\r%\x00\x00\x00\x00\x18\r\x1e\x003\x00\x00\x18\r\x1e\x00\x00\x00 \x18\r&\x00\x00\x00\x00\x18\r\x1e\x003\x00\x00\x0b\x05\t\x00\x15%\x00\x0b\x05\t\x00\x15%\x00\x0b\x05\x08\x00\x17\x00\x00\x0b\x05\x08\x00\x17\x00\xc0\x17\n\x1b\x00\x00\x00\xc0\x17\n\x1b\x00\x00\x00 \x13\x0b)\x00\x14\x00\xc0\x17\n\x1b\x00\x00\x00 \x13\x0b)\x00\x14\x00\xc0\x17\n\x1b\x00\x00\x00 \x13\x0b)\x00\x14\x00 \x13\x0b)\x00\x14\x00\x00\x00\xde\x01\x00\x02! \x13\x0b)\x00\x14\x00\x00\x00\xde\x01\x00\x02!\x01\x00\x02!\x00\x0f\x00\x00\x00\x00\xc0\xe0\x00\x11\x01\x00\x02!\x00\x0f\x00\x00\x00\x00\xc0\xe0\x00\x11\x00\x11\x0c\x1d\x00\x00\x00\x00\x00\xb1\x97\x00\x11\x0c\x00\x11\x0c\x1d\x00\n\x00\x00\x00\x00\x98\x00\x0b\x05\x00\x0b\x05\x08\x00\x06\x00\x00\x0b\x05\x08\x00\x17\x00\x00\x0b\x05\x08\x00\x06\x00\x00\x0b\x05\x08\x00\x17\x00\x00\x18\r%\x00\x00\x00\x00\x18\r\x1e\x003\x00\x00\x18\r\x1e\x00\x00\x00 \x18\r&\x00\x00\x00\x00\x0b\x05\x08\x00\x06\x00\x00\x0b\x05\x08\x00\x17\x00\x00\x0b\x05\x08\x00\x06\x00\x00\x0b\x05\x08\x00\x17\x00\x00\x0b\x05\x08\x00\x17\x00\x00\x0b\x05\x08\x00\x17\x00\x00\x0b\x05\x08\x00\x17\x00\x00\x00\x00\x99\xe0\x14\x0b\xc0\x17\n\x1b\x00\x00\x00 \x13\x0b)\x00\x14\x00\xc0\x17\n\x1b\x00\x00\x00 \x13\x0b)\x00\x14\x00\xe0\x14\x0b\x16\x00%\x00\xc0 \x06\x13\x00\x00\x00\xe0\x14\x0b\x16\x00%\x00\xc0 \x06\x13\x00\x00\x00\xc0 \x06\x13\x00\x00\x00\x00\x00\x00\xef\x00&\x02\x00&\x02!\x00\x01*\x00\x00\x00\xd0\xc0\x07\x06\xc0\x07\x06(\x00\x00\x00\x00 \x06\x13\x00\x00\x00\x00 \x06\x13\x00\x00\x00\xc0 \x06\t\x00\x00\x00\xc0 \x06\t\x00\x00\x00\x01\x07\x14\x01\x00\x00\x00\x01\x07\x14\x01\x00\x00\x00\x01\x07\x06\x01\x00\x00\x00\x01\x07\x14\x01\x00\x00\x00\x01\x07\x06\x01\x00\x00\x00\x01\x07\x06\x01\x00\x00\x00 \x07\x06\x13\x00\x00\x00\x01\x07\x06\x01\x00\x00\x00 \x07\x06\x13\x00\x00\x00 \x07\x06\x13\x00\x00\x00\x00\x00\xf8\xf8\xf8\xf8\xf8 \x06\x13\x00\x00\x00\x00\x00\xfa\xfa \x07\x06 \x06\x13\x00\x00\x00\x00\x00\xfa\xfa \x07\x06 \x07\x06\x19\x00\x00\x00\x00\x00\xfb\xfb \x06 \x06\x13\x00\x00\x00\x00\x00\xfd\xfd\xfd \x06\x13\x00\x00\x00\x00\x00\xfd\xfd\xfd \x06\x13\x00\x00\x00\x00\x00\xfe \x06\x13 \x06\x13\x00\x02\x00\x08\x00\xff\xdf\xff\x00\x02\x01\x07\x06\x01\x00\x00\x00 \x07\x06\x13\x00\x00\x00\x01\x07\x06\x01\x00\x00\x00 \x07\x06\x13\x00\x00\x00\x00\x02\x03\x05\x00\x00\x02\x03\x0f\x00\x00\x00\x00\x07\x00\x02\x03\x05\x00\x00\x02\x03\x0f\x00\x00\x00\x00\x07\x00\x02\x03\x0f\x00\x00\x00\x00\x07\x06\x13\x00\x00\x00\x00\x02\x03\x0f\x00\x00\x00\x00\x07\x06\x13\x00\x00\x00\x00\x07\x06\x13\x00\x00\x00\x00\x00\x00\xe8\xe8\xe8\xe8\x00\x07\x06\x13\x00\x00\x00\x00\x00\x00\xe8\xe8\xe8\xe8\x00\x07\x06\x13\x00\x00\x00\x00\x00\x00\xe8\xe8\xe8\xe8\x00 \x06\x13\x00\x00\x00\xc0 \x06\x13\x00\x00\x00\xc0 \x06\x13\x00\x00\x00\x00\x00\x00\xea\x00\x07\x06\x00\x07\x06\x19\x00\x00\x00\x00\x00\x00\xeb\x00 \x06\x00 \x06\x13\x00\x00\x00\x00\x00\x00\xed\xed\x00\x07\x00 \x06\x13\x00\x00\x00\x00\x00\x00\xed\xed\x00\x07\x00 \x06\x13\x00\x00\x00\xc0 \x06\x13\x00\x00\x00\x00\x07\x06\x05\x00\x00\x00\x00\x00\x00\xef\x00\x05\x03\x00\x05\x03(\x00\x00\x00\x00\x1f\x03\x05\x00\x00\x00\x00\x02\x03\x0f\x00\x00\x00\x00\x15\x03\r\x00\x00\x00\x00\x15\x03\r\x00\x00\x00\x00\x05\x03\x0f\x00\x00\x00\x00\x05\x03\x0f\x00\x00\x00\x01\x15\x03\r\x00\x00\x00\x01\x15\x03\r\x00\x00\x00\x00\x1c\x0f\x10\x00\x00\x00\x00\x1c\x0f\x10\x00\x00\x00\x00\x1f\x03\x0f\x00\x00\x00\x00\x1f\x03\x0f\x00\x00\x00\x00\x02\x03\x01\x00\x00\x00\x00\x02\x03\x01\x00\x00\x00\x00\x02\x03\x0e\x00\x00\x00\x00\x02\x03\x0e\x00\x00\x00\x01\x05\x03\x05\x00\x00\x00\x01\x05\x03\x05\x00\x00\x00\x01\x07\x06\x10\x00\x00\x00\x01\x07\x06\x10\x00\x00\x00\x80\n\x08\x08\x00\x00\x1a\x80\n\x08\x08\x00\x00\x1a\x00\'\x06\x08\x00\x03\x00\x00\'\x06\x08\x00\x03\x00\x00\n\x08\x11\x00\x00\x00\x00\n\x08\x11\x00\x00\x00\x00\x07\x14\x05\x00\x00\x00\x00\x07\x14\x05\x00\x00\x00\x00\x1e\x11\x05\x00\x00\x00\x00\x1f\x03\x05\x00\x00\x00\x00\x02\x03\x0f\x00\x00\x00\x00\x1f\x03\x05\x00\x00\x00\x00\x02\x03\x0f\x00\x00\x00\x00\x1e\x11\x05\x00\x00\x00\x00\x07\x14\x05\x00\x00\x00\x00\x07\x14\x05\x00\x00\x00\x00\x03\x10\x08\x00\x00\x00\x00\x03\x10\x08\x00\x00\x00\x00\x07\x06\x07\x00\x00\x00\x00\x07\x06\x07\x00\x00\x00\x00"\x12\x07\x00\x00\x00\x00\x07\x06\x07\x00\x00\x00\x00"\x12\x07\x00\x00\x00\x00"\x12\x07\x00\x00\x00\x00 \x14\x05\x00\x00\x00\x00 \x14\x05\x00\x00\x00\xe0#\n\x0f\x00\x00\x00\x00\x05\x03\x0f\x00\x00\x00\x01\x15\x03\r\x00\x00\x00\xe0#\n\x0f\x00\x00\x00\x00\x00\x00\x1d\x00\x1c\x0f\x00\x1c\x0f\x05\x00\x00\x00\xc0\x07\x06\x08\x00\x00\x00\xc0\x07\x06\x08\x00\x00\x00\x00#\n\x0f\x00\x00\x00\x00\x1f\x03\x05\x00\x00\x00\x00\x02\x03\x0f\x00\x00\x00\x00#\n\x0f\x00\x00\x00\x00\x00\x00\x19\x00 \x06\x00 \x06*\x00\x00\x00\x00\x05\x03\x05\x00\x00\x00\x00\x05\x03\x05\x00\x00\x00\x00\x13\x06\x13\x00\x00\x00\x00\x13\x06\x13\x00\x00\x00\x00\x07\x06(\x00\x03\x00\x00\x1e\x11\x05\x00\x00\x00\x00\x07\x14\x05\x00\x00\x00\x00\x1e\x11\x05\x00\x00\x00\x00\x07\x14\x05\x00\x00\x00\x00\x07\x06(\x00\x03\x00\x00\x07\x06(\x00\x00\x00\x00\x07\x06(\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\x00\x07\x06(\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\x00\x07\x06(\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\x00 \x06*\x00\x00\x00\x00\x05\x03\x05\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff'), +) diff --git a/worlds/alttp/enemizer_data/dungeon_sprite_addresses.py b/worlds/alttp/enemizer_data/dungeon_sprite_addresses.py new file mode 100644 index 000000000000..af739b103ae9 --- /dev/null +++ b/worlds/alttp/enemizer_data/dungeon_sprite_addresses.py @@ -0,0 +1,202 @@ +from __future__ import annotations + +from typing import NamedTuple + + +class DungeonSpriteAddressData(NamedTuple): + room_id: int + sprite_id_addresses: tuple[int, ...] + +DUNGEON_SPRITE_ADDRESSES = ( + DungeonSpriteAddressData(room_id=2, sprite_id_addresses=(317750, 317753, 317756, 317759, 317762, 317792, 317795)), + DungeonSpriteAddressData(room_id=4, sprite_id_addresses=(317803, 317806, 317809, 317812, 317827, 317839, 317842, 317845)), + DungeonSpriteAddressData(room_id=9, sprite_id_addresses=(317904, 317907, 317910)), + DungeonSpriteAddressData(room_id=10, sprite_id_addresses=(317915, 317918, 317921, 317924, 317930, 317933)), + DungeonSpriteAddressData(room_id=11, sprite_id_addresses=(317941, 317944, 317947, 317950, 317953, 317956, 317959, 317962, 317965)), + DungeonSpriteAddressData(room_id=14, sprite_id_addresses=(317978, 317981, 317984)), + DungeonSpriteAddressData(room_id=17, sprite_id_addresses=(317992, 317995, 317998, 318001, 318004, 318007, 318010, 318013)), + DungeonSpriteAddressData(room_id=19, sprite_id_addresses=(318029, 318032, 318035, 318038, 318044, 318056, 318053, 318041)), + DungeonSpriteAddressData(room_id=21, sprite_id_addresses=(318105, 318108, 318111, 318114, 318117, 318120)), + DungeonSpriteAddressData(room_id=22, sprite_id_addresses=(318125, 318128, 318131, 318134, 318137, 318140, 318143)), + DungeonSpriteAddressData(room_id=23, sprite_id_addresses=(318157, 318160, 318163, 318166, 318169, 318172)), + DungeonSpriteAddressData(room_id=25, sprite_id_addresses=(318177, 318180, 318183, 318186)), + DungeonSpriteAddressData(room_id=26, sprite_id_addresses=(318191, 318194, 318197, 318200, 318203, 318206, 318209, 318212, 318218)), + DungeonSpriteAddressData(room_id=27, sprite_id_addresses=(318232, 318235, 318238, 318241)), + DungeonSpriteAddressData(room_id=30, sprite_id_addresses=(318284, 318287, 318290, 318293, 318296, 318299)), + DungeonSpriteAddressData(room_id=31, sprite_id_addresses=(318304, 318307, 318310, 318313, 318316, 318319, 318322, 318325)), + DungeonSpriteAddressData(room_id=33, sprite_id_addresses=(318335, 318341, 318344, 318347, 318350, 318353, 318356, 318359, 318362, 318365, 318368)), + DungeonSpriteAddressData(room_id=34, sprite_id_addresses=(318373, 318376, 318379, 318382, 318385, 318388, 318391)), + DungeonSpriteAddressData(room_id=36, sprite_id_addresses=(318413, 318416, 318419, 318422, 318425, 318428, 318431)), + DungeonSpriteAddressData(room_id=38, sprite_id_addresses=(318471, 318438, 318441, 318444, 318447, 318450, 318453, 318459, 318462, 318465, 318468)), + DungeonSpriteAddressData(room_id=39, sprite_id_addresses=(318476, 318479, 318482, 318485, 318488, 318491, 318494)), + DungeonSpriteAddressData(room_id=40, sprite_id_addresses=(318511,)), + DungeonSpriteAddressData(room_id=42, sprite_id_addresses=(318530, 318533, 318536, 318539, 318542, 318545)), + DungeonSpriteAddressData(room_id=43, sprite_id_addresses=(318556, 318559, 318562, 318565, 318568, 318571)), + DungeonSpriteAddressData(room_id=46, sprite_id_addresses=(318590, 318593, 318596, 318599, 318602, 318605)), + DungeonSpriteAddressData(room_id=49, sprite_id_addresses=(318621, 318624, 318627, 318630, 318633, 318636, 318639, 318642, 318645, 318648)), + DungeonSpriteAddressData(room_id=50, sprite_id_addresses=(318653, 318656, 318659, 318662, 318665)), + DungeonSpriteAddressData(room_id=52, sprite_id_addresses=(318681, 318684, 318687, 318693, 318696, 318699, 318690)), + DungeonSpriteAddressData(room_id=53, sprite_id_addresses=(318710, 318713, 318716, 318719, 318722, 318728, 318731, 318734, 318725)), + DungeonSpriteAddressData(room_id=54, sprite_id_addresses=(318742, 318745, 318754, 318760, 318763)), + DungeonSpriteAddressData(room_id=55, sprite_id_addresses=(318777, 318780, 318783, 318786, 318792, 318795, 318798, 318801, 318789)), + DungeonSpriteAddressData(room_id=56, sprite_id_addresses=(318806, 318809, 318812, 318815, 318818, 318821, 318824)), + DungeonSpriteAddressData(room_id=57, sprite_id_addresses=(318829, 318835, 318841, 318844, 318847, 318850)), + DungeonSpriteAddressData(room_id=58, sprite_id_addresses=(318855, 318858, 318861, 318864, 318867, 318870)), + DungeonSpriteAddressData(room_id=59, sprite_id_addresses=(318875, 318878, 318881, 318884, 318887, 318890, 318893)), + DungeonSpriteAddressData(room_id=60, sprite_id_addresses=(318898, 318901, 318904)), + DungeonSpriteAddressData(room_id=61, sprite_id_addresses=(318915, 318921, 318924, 318927, 318930, 318933, 318939, 318942, 318945, 318948, 318951)), + DungeonSpriteAddressData(room_id=62, sprite_id_addresses=(318959, 318962, 318980, 318983, 318989, 318992)), + DungeonSpriteAddressData(room_id=63, sprite_id_addresses=(319000, 319006, 319009)), + DungeonSpriteAddressData(room_id=64, sprite_id_addresses=(319014, 319017, 319023, 319026, 319029)), + DungeonSpriteAddressData(room_id=65, sprite_id_addresses=(319036, 319039, 319042, 319045)), + DungeonSpriteAddressData(room_id=66, sprite_id_addresses=(319050, 319053, 319056, 319059, 319062, 319065)), + DungeonSpriteAddressData(room_id=67, sprite_id_addresses=(319070, 319073)), + DungeonSpriteAddressData(room_id=68, sprite_id_addresses=(319084, 319087, 319090, 319093, 319096, 319102)), + DungeonSpriteAddressData(room_id=69, sprite_id_addresses=(319110, 319116, 319119, 319131, 319134, 319137, 319113, 319122, 319125, 319128)), + DungeonSpriteAddressData(room_id=70, sprite_id_addresses=(319142, 319148, 319154)), + DungeonSpriteAddressData(room_id=73, sprite_id_addresses=(319161, 319164, 319167, 319170, 319173, 319176, 319182, 319185, 319188, 319191, 319194, 319197)), + DungeonSpriteAddressData(room_id=74, sprite_id_addresses=(319205, 319208)), + DungeonSpriteAddressData(room_id=75, sprite_id_addresses=(319213, 319216, 319219, 319222, 319225, 319228, 319231, 319234)), + DungeonSpriteAddressData(room_id=76, sprite_id_addresses=(319245, 319248, 319251, 319254, 319257, 319260)), + DungeonSpriteAddressData(room_id=78, sprite_id_addresses=(319270, 319273, 319276, 319279)), + DungeonSpriteAddressData(room_id=80, sprite_id_addresses=(319295, 319298, 319301)), + DungeonSpriteAddressData(room_id=81, sprite_id_addresses=(319309, 319312)), + DungeonSpriteAddressData(room_id=82, sprite_id_addresses=(319317, 319320, 319323)), + DungeonSpriteAddressData(room_id=83, sprite_id_addresses=(319328, 319331, 319334, 319337, 319340, 319343, 319346, 319349, 319352, 319355, 319358, 319361, 319364)), + DungeonSpriteAddressData(room_id=84, sprite_id_addresses=(319369, 319372, 319375, 319378, 319381, 319384, 319387, 319390)), + DungeonSpriteAddressData(room_id=85, sprite_id_addresses=(319398, 319401)), + DungeonSpriteAddressData(room_id=86, sprite_id_addresses=(319415, 319418, 319421, 319424, 319430, 319433, 319436, 319442, 319439)), + DungeonSpriteAddressData(room_id=87, sprite_id_addresses=(319447, 319450, 319453, 319456, 319459, 319462, 319468, 319471, 319474, 319477, 319480, 319483, 319486, 319489)), + DungeonSpriteAddressData(room_id=88, sprite_id_addresses=(319497, 319500, 319506, 319509, 319515, 319518, 319521)), + DungeonSpriteAddressData(room_id=89, sprite_id_addresses=(319526, 319529, 319538, 319544, 319547, 319550, 319553, 319556, 319559, 319541)), + DungeonSpriteAddressData(room_id=91, sprite_id_addresses=(319575, 319578, 319581, 319584)), + DungeonSpriteAddressData(room_id=93, sprite_id_addresses=(319615, 319618, 319621, 319624, 319627, 319633, 319636, 319639, 319651, 319642, 319645, 319648, 319630)), + DungeonSpriteAddressData(room_id=94, sprite_id_addresses=(319659, 319662, 319665, 319668)), + DungeonSpriteAddressData(room_id=95, sprite_id_addresses=(319673, 319676, 319679)), + DungeonSpriteAddressData(room_id=96, sprite_id_addresses=(319684,)), + DungeonSpriteAddressData(room_id=97, sprite_id_addresses=(319689, 319692, 319695)), + DungeonSpriteAddressData(room_id=98, sprite_id_addresses=(319700, 319703, 319706)), + DungeonSpriteAddressData(room_id=99, sprite_id_addresses=(319714, 319711)), + DungeonSpriteAddressData(room_id=100, sprite_id_addresses=(319719, 319725, 319728, 319731, 319734, 319737)), + DungeonSpriteAddressData(room_id=101, sprite_id_addresses=(319760, 319763, 319766, 319769, 319772)), + DungeonSpriteAddressData(room_id=102, sprite_id_addresses=(319777, 319783, 319786, 319795, 319798, 319801, 319804, 319810)), + DungeonSpriteAddressData(room_id=103, sprite_id_addresses=(319818, 319821, 319824, 319827, 319830, 319833, 319836, 319839, 319842)), + DungeonSpriteAddressData(room_id=104, sprite_id_addresses=(319859, 319865, 319868)), + DungeonSpriteAddressData(room_id=106, sprite_id_addresses=(319873, 319876, 319879, 319882, 319885, 319888)), + DungeonSpriteAddressData(room_id=107, sprite_id_addresses=(319899, 319902, 319905, 319911, 319914, 319917, 319920, 319923, 319926, 319929, 319932)), + DungeonSpriteAddressData(room_id=109, sprite_id_addresses=(319954, 319957, 319960, 319963, 319966, 319969, 319972, 319975, 319978)), + DungeonSpriteAddressData(room_id=110, sprite_id_addresses=(319983, 319986, 319989, 319992, 319995)), + DungeonSpriteAddressData(room_id=113, sprite_id_addresses=(320000, 320003)), + DungeonSpriteAddressData(room_id=114, sprite_id_addresses=(320011, 320017)), + DungeonSpriteAddressData(room_id=115, sprite_id_addresses=(320022, 320025, 320028, 320031, 320034, 320037)), + DungeonSpriteAddressData(room_id=116, sprite_id_addresses=(320045, 320048, 320051, 320054, 320057, 320060, 320063, 320066)), + DungeonSpriteAddressData(room_id=117, sprite_id_addresses=(320071, 320074, 320077, 320080, 320083, 320086, 320095, 320098)), + DungeonSpriteAddressData(room_id=118, sprite_id_addresses=(320106, 320109, 320112, 320115, 320121)), + DungeonSpriteAddressData(room_id=119, sprite_id_addresses=(320126, 320138, 320141)), + DungeonSpriteAddressData(room_id=123, sprite_id_addresses=(320146, 320149, 320152, 320155, 320158, 320161, 320167, 320170, 320173, 320176)), + DungeonSpriteAddressData(room_id=124, sprite_id_addresses=(320181, 320184, 320187, 320190, 320193, 320196)), + DungeonSpriteAddressData(room_id=125, sprite_id_addresses=(320216, 320219, 320225, 320228, 320234, 320222, 320231, 320204, 320207, 320210, 320213, 320222, 320231)), + DungeonSpriteAddressData(room_id=126, sprite_id_addresses=(320242, 320245, 320254, 320257)), + DungeonSpriteAddressData(room_id=128, sprite_id_addresses=(320291, 320294)), + DungeonSpriteAddressData(room_id=129, sprite_id_addresses=(320302, 320305)), + DungeonSpriteAddressData(room_id=130, sprite_id_addresses=(320310, 320313, 320316)), + DungeonSpriteAddressData(room_id=131, sprite_id_addresses=(320321, 320324, 320327, 320330, 320333, 320336, 320339, 320342, 320345, 320348)), + DungeonSpriteAddressData(room_id=132, sprite_id_addresses=(320353, 320356, 320359, 320362, 320365, 320368, 320371)), + DungeonSpriteAddressData(room_id=133, sprite_id_addresses=(320376, 320379, 320382, 320385, 320388, 320391, 320394, 320397, 320400, 320403)), + DungeonSpriteAddressData(room_id=135, sprite_id_addresses=(320410, 320413, 320416, 320419, 320434, 320437, 320440, 320446, 320422)), + DungeonSpriteAddressData(room_id=139, sprite_id_addresses=(320468, 320471, 320474, 320477, 320480)), + DungeonSpriteAddressData(room_id=140, sprite_id_addresses=(320503, 320506, 320509, 320512, 320518, 320521, 320527, 320524, 320515)), + DungeonSpriteAddressData(room_id=141, sprite_id_addresses=(320538, 320541, 320544, 320547, 320550, 320556, 320559, 320562, 320565, 320568, 320571, 320535)), + DungeonSpriteAddressData(room_id=142, sprite_id_addresses=(320579, 320582, 320585, 320588, 320591, 320594, 320597)), + DungeonSpriteAddressData(room_id=145, sprite_id_addresses=(320610, 320616, 320619, 320622, 320625, 320613)), + DungeonSpriteAddressData(room_id=146, sprite_id_addresses=(320636, 320639, 320642, 320645, 320648, 320654, 320657, 320660, 320663)), + DungeonSpriteAddressData(room_id=147, sprite_id_addresses=(320668, 320671, 320674, 320677, 320680, 320683, 320686, 320689)), + DungeonSpriteAddressData(room_id=149, sprite_id_addresses=(320694, 320697, 320700, 320703)), + DungeonSpriteAddressData(room_id=151, sprite_id_addresses=(320728,)), + DungeonSpriteAddressData(room_id=152, sprite_id_addresses=(320733, 320736, 320739, 320742, 320745)), + DungeonSpriteAddressData(room_id=153, sprite_id_addresses=(320750, 320753, 320756, 320759, 320765, 320768, 320771, 320774, 320777, 320780)), + DungeonSpriteAddressData(room_id=155, sprite_id_addresses=(320794, 320797, 320800, 320803, 320806, 320809, 320812, 320815, 320818, 320821)), + DungeonSpriteAddressData(room_id=156, sprite_id_addresses=(320826, 320829, 320832, 320835, 320838, 320841)), + DungeonSpriteAddressData(room_id=157, sprite_id_addresses=(320852, 320855, 320858, 320861, 320864, 320867, 320870, 320873)), + DungeonSpriteAddressData(room_id=158, sprite_id_addresses=(320878, 320881, 320884, 320887)), + DungeonSpriteAddressData(room_id=159, sprite_id_addresses=(320907, 320910)), + DungeonSpriteAddressData(room_id=160, sprite_id_addresses=(320915, 320918, 320921)), + DungeonSpriteAddressData(room_id=161, sprite_id_addresses=(320929, 320932, 320935, 320938, 320941, 320944, 320947, 320950)), + DungeonSpriteAddressData(room_id=165, sprite_id_addresses=(320968, 320971, 320974, 320977, 320980, 320983, 320986, 320989, 320998, 321001)), + DungeonSpriteAddressData(room_id=167, sprite_id_addresses=(321014, 321017)), + DungeonSpriteAddressData(room_id=168, sprite_id_addresses=(321022, 321025, 321028, 321031, 321034)), + DungeonSpriteAddressData(room_id=169, sprite_id_addresses=(321039, 321042, 321057, 321060, 321045, 321048, 321051, 321054)), + DungeonSpriteAddressData(room_id=170, sprite_id_addresses=(321065, 321068, 321071, 321074, 321077, 321080)), + DungeonSpriteAddressData(room_id=171, sprite_id_addresses=(321088, 321091, 321094, 321097, 321100, 321103, 321106)), + DungeonSpriteAddressData(room_id=174, sprite_id_addresses=(321116, 321119)), + DungeonSpriteAddressData(room_id=176, sprite_id_addresses=(321129, 321132, 321135, 321138, 321141, 321144, 321147, 321150, 321153, 321156, 321159, 321165, 321168)), + DungeonSpriteAddressData(room_id=177, sprite_id_addresses=(321173, 321176, 321179, 321182, 321185, 321188, 321191, 321194, 321197, 321200)), + DungeonSpriteAddressData(room_id=178, sprite_id_addresses=(321205, 321208, 321211, 321214, 321217, 321220, 321223, 321226, 321229, 321232, 321235, 321238, 321241, 321244)), + DungeonSpriteAddressData(room_id=179, sprite_id_addresses=(321249, 321252, 321255, 321258, 321261)), + DungeonSpriteAddressData(room_id=182, sprite_id_addresses=(321277, 321280, 321289, 321292, 321301, 321304)), + DungeonSpriteAddressData(room_id=183, sprite_id_addresses=(321309, 321312)), + DungeonSpriteAddressData(room_id=184, sprite_id_addresses=(321317, 321320, 321323, 321326, 321329, 321332)), + DungeonSpriteAddressData(room_id=186, sprite_id_addresses=(321342, 321345, 321348, 321351, 321354, 321357, 321360)), + DungeonSpriteAddressData(room_id=187, sprite_id_addresses=(321365, 321368, 321371, 321374, 321377, 321380, 321386, 321389, 321392, 321395, 321383)), + DungeonSpriteAddressData(room_id=188, sprite_id_addresses=(321403, 321406, 321409, 321412, 321418, 321421, 321424, 321433, 321400, 321415, 321427, 321430)), + DungeonSpriteAddressData(room_id=190, sprite_id_addresses=(321440, 321446, 321449, 321452, 321455, 321458)), + DungeonSpriteAddressData(room_id=192, sprite_id_addresses=(321471, 321474, 321477, 321480, 321486, 321489, 321492, 321495)), + DungeonSpriteAddressData(room_id=193, sprite_id_addresses=(321503, 321506, 321509, 321512, 321518, 321524, 321527, 321530, 321521, 321515, 321536)), + DungeonSpriteAddressData(room_id=194, sprite_id_addresses=(321547, 321550, 321553, 321556, 321562, 321559, 321544, 321541)), + DungeonSpriteAddressData(room_id=195, sprite_id_addresses=(321567, 321585, 321588)), + DungeonSpriteAddressData(room_id=196, sprite_id_addresses=(321605, 321608, 321611, 321614, 321617, 321620)), + DungeonSpriteAddressData(room_id=201, sprite_id_addresses=(321697, 321700, 321703)), + DungeonSpriteAddressData(room_id=203, sprite_id_addresses=(321708, 321717, 321720, 321723, 321726, 321729, 321732, 321735, 321738, 321741, 321714, 321711)), + DungeonSpriteAddressData(room_id=204, sprite_id_addresses=(321746, 321749, 321755, 321758, 321761, 321770, 321773, 321776, 321779, 321782, 321785, 321752, 321764, 321767)), + DungeonSpriteAddressData(room_id=206, sprite_id_addresses=(321790, 321793, 321799, 321802, 321805, 321808, 321811)), + DungeonSpriteAddressData(room_id=208, sprite_id_addresses=(321816, 321819, 321822, 321825, 321828, 321831, 321834, 321837, 321840, 321843, 321846)), + DungeonSpriteAddressData(room_id=209, sprite_id_addresses=(321851, 321854, 321857, 321860, 321863, 321866, 321869, 321872)), + DungeonSpriteAddressData(room_id=210, sprite_id_addresses=(321877, 321880, 321883, 321886, 321889, 321892, 321895, 321898, 321901, 321904)), + DungeonSpriteAddressData(room_id=216, sprite_id_addresses=(321937, 321940, 321943, 321946, 321949, 321952, 321955, 321958, 321961, 321964, 321967)), + DungeonSpriteAddressData(room_id=217, sprite_id_addresses=(321975, 321978, 321981, 321972)), + DungeonSpriteAddressData(room_id=218, sprite_id_addresses=(321986, 321989)), + DungeonSpriteAddressData(room_id=219, sprite_id_addresses=(321994, 321997, 322000, 322006, 322003, 322012, 322009)), + DungeonSpriteAddressData(room_id=220, sprite_id_addresses=(322020, 322023, 322026, 322029, 322032, 322035, 322047, 322017, 322038, 322041, 322044)), + DungeonSpriteAddressData(room_id=223, sprite_id_addresses=(322063, 322066)), + DungeonSpriteAddressData(room_id=224, sprite_id_addresses=(322071, 322074, 322077, 322080)), + DungeonSpriteAddressData(room_id=232, sprite_id_addresses=(322189, 322192, 322195, 322198)), + DungeonSpriteAddressData(room_id=238, sprite_id_addresses=(322213, 322216, 322219, 322222, 322225)), + DungeonSpriteAddressData(room_id=239, sprite_id_addresses=(322230, 322233, 322236)), + DungeonSpriteAddressData(room_id=249, sprite_id_addresses=(322323, 322326, 322329, 322332)), + DungeonSpriteAddressData(room_id=254, sprite_id_addresses=(322378, 322381, 322384, 322387, 322390)), + DungeonSpriteAddressData(room_id=263, sprite_id_addresses=(322444, 322447)), + DungeonSpriteAddressData(room_id=264, sprite_id_addresses=(322452, 322455, 322458, 322461)), + DungeonSpriteAddressData(room_id=267, sprite_id_addresses=(322494,)), + DungeonSpriteAddressData(room_id=269, sprite_id_addresses=(322525, 322528)), + DungeonSpriteAddressData(room_id=291, sprite_id_addresses=(322671, 322674, 322677, 322680)), +) + +KEYED_SPRITE_ID_ADDRESSES = frozenset((317984, + 318044, + 318335, + 318835, + 318915, + 318983, + 320003, + 320011, + 320294, + 320759, + 321292, + 321480, + 321530, + 320000, + 321159, + 321937, + 321940, + 321943, + 321946, + 321949, + 321952, + 321955, + 321958, + 321961, + 321964, + 321967, + 321424, + 321421, + 321418)) diff --git a/worlds/alttp/enemizer_data/enemy_room_metadata.py b/worlds/alttp/enemizer_data/enemy_room_metadata.py new file mode 100644 index 000000000000..f8d29c153884 --- /dev/null +++ b/worlds/alttp/enemizer_data/enemy_room_metadata.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +from typing import NamedTuple, Optional + + +class RoomGroupRequirementData(NamedTuple): + group_id: Optional[int] + subgroup_0: Optional[int] + subgroup_1: Optional[int] + subgroup_2: Optional[int] + subgroup_3: Optional[int] + rooms: tuple[int, ...] + +SHUTTER_ROOM_IDS = frozenset((184, + 11, + 27, + 75, + 4, + 36, + 182, + 40, + 14, + 46, + 62, + 110, + 49, + 135, + 68, + 69, + 83, + 117, + 133, + 61, + 93, + 107, + 109, + 123, + 125, + 141, + 150, + 165, + 113, + 168, + 216, + 176, + 192, + 224, + 178, + 210, + 239, + 268, + 291)) +WATER_ROOM_IDS = frozenset((22, 40, 52, 54, 56, 70, 102)) +DONT_RANDOMIZE_ROOM_IDS = frozenset((0, 1, 3, 13, 20, 32, 48, 127)) +NO_SPECIAL_ENEMIES_STANDARD_ROOM_IDS = frozenset((1, 2, 17, 33, 34, 50, 65, 66, 80, 81, 82, 85, 96, 97, 98, 112, 113, 114, 128, 129, 130)) +BOSS_ROOM_IDS = frozenset((200, 51, 108, 7, 77, 90, 6, 41, 172, 222, 144, 164, 32, 13, 0)) + +ROOM_GROUP_REQUIREMENTS = ( + RoomGroupRequirementData(group_id=1, subgroup_0=70, subgroup_1=73, subgroup_2=28, subgroup_3=82, rooms=(228, 240)), + RoomGroupRequirementData(group_id=5, subgroup_0=75, subgroup_1=77, subgroup_2=74, subgroup_3=90, rooms=(243, 265, 270, 271, 272, 273, 282, 284, 290)), + RoomGroupRequirementData(group_id=None, subgroup_0=75, subgroup_1=None, subgroup_2=None, subgroup_3=None, rooms=(255, 274, 287)), + RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=77, subgroup_2=None, subgroup_3=21, rooms=(289,)), + RoomGroupRequirementData(group_id=7, subgroup_0=75, subgroup_1=77, subgroup_2=57, subgroup_3=54, rooms=(8, 44, 276, 277)), + RoomGroupRequirementData(group_id=13, subgroup_0=81, subgroup_1=None, subgroup_2=None, subgroup_3=None, rooms=(85, 258, 260)), + RoomGroupRequirementData(group_id=14, subgroup_0=71, subgroup_1=73, subgroup_2=76, subgroup_3=80, rooms=(18, 261, 266)), + RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=80, rooms=(264,)), + RoomGroupRequirementData(group_id=15, subgroup_0=79, subgroup_1=77, subgroup_2=74, subgroup_3=80, rooms=(244, 245, 257, 259, 262, 280, 281)), + RoomGroupRequirementData(group_id=18, subgroup_0=85, subgroup_1=61, subgroup_2=66, subgroup_3=67, rooms=(32, 48)), + RoomGroupRequirementData(group_id=24, subgroup_0=85, subgroup_1=26, subgroup_2=66, subgroup_3=67, rooms=(13,)), + RoomGroupRequirementData(group_id=34, subgroup_0=33, subgroup_1=65, subgroup_2=69, subgroup_3=51, rooms=(0,)), + RoomGroupRequirementData(group_id=40, subgroup_0=14, subgroup_1=None, subgroup_2=74, subgroup_3=80, rooms=(225, 256, 293, 292, 294)), + RoomGroupRequirementData(group_id=None, subgroup_0=14, subgroup_1=30, subgroup_2=None, subgroup_3=None, rooms=(291,)), + RoomGroupRequirementData(group_id=23, subgroup_0=64, subgroup_1=None, subgroup_2=None, subgroup_3=63, rooms=()), + RoomGroupRequirementData(group_id=9, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=29, rooms=(227,)), + RoomGroupRequirementData(group_id=11, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=61, rooms=()), + RoomGroupRequirementData(group_id=22, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=49, rooms=()), + RoomGroupRequirementData(group_id=22, subgroup_0=None, subgroup_1=None, subgroup_2=60, subgroup_3=None, rooms=()), + RoomGroupRequirementData(group_id=21, subgroup_0=None, subgroup_1=None, subgroup_2=58, subgroup_3=62, rooms=()), + RoomGroupRequirementData(group_id=28, subgroup_0=None, subgroup_1=None, subgroup_2=38, subgroup_3=82, rooms=(14, 126, 142, 158, 190)), + RoomGroupRequirementData(group_id=12, subgroup_0=None, subgroup_1=None, subgroup_2=48, subgroup_3=None, rooms=()), + RoomGroupRequirementData(group_id=26, subgroup_0=None, subgroup_1=None, subgroup_2=56, subgroup_3=None, rooms=()), + RoomGroupRequirementData(group_id=20, subgroup_0=None, subgroup_1=None, subgroup_2=57, subgroup_3=None, rooms=()), + RoomGroupRequirementData(group_id=32, subgroup_0=None, subgroup_1=44, subgroup_2=59, subgroup_3=None, rooms=()), + RoomGroupRequirementData(group_id=3, subgroup_0=93, subgroup_1=None, subgroup_2=None, subgroup_3=None, rooms=(81,)), + RoomGroupRequirementData(group_id=42, subgroup_0=21, subgroup_1=None, subgroup_2=None, subgroup_3=None, rooms=(286,)), + RoomGroupRequirementData(group_id=10, subgroup_0=47, subgroup_1=None, subgroup_2=46, subgroup_3=None, rooms=(92, 117, 185, 217)), + RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=34, subgroup_3=None, rooms=(54, 70, 102, 118)), + RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=32, subgroup_2=None, subgroup_3=None, rooms=(62, 159)), + RoomGroupRequirementData(group_id=None, subgroup_0=31, subgroup_1=None, subgroup_2=None, subgroup_3=None, rooms=(127,)), + RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=35, subgroup_3=None, rooms=(57, 73, 86, 87, 104, 141)), + RoomGroupRequirementData(group_id=37, subgroup_0=31, subgroup_1=None, subgroup_2=39, subgroup_3=82, rooms=(36, 180, 181, 198, 199, 214)), + RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(23, 42, 68, 76, 86, 88, 89, 103, 104, 126, 139, 235, 251)), + RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(23, 42, 76, 89, 103, 104, 126, 139, 235, 251)), + RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(11, 19, 27, 30, 42, 43, 49, 61, 62, 91, 107, 119, 135, 139, 145, 146, 155, 157, 161, 171, 182, 191, 193, 196, 239)), + RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(11, 19, 27, 30, 42, 43, 49, 53, 62, 91, 107, 119, 135, 139, 145, 146, 155, 157, 161, 171, 182, 191, 193, 196, 239)), + RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(19, 35, 150, 165, 195, 197, 213)), + RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(19, 35, 150, 165, 197, 213)), + RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(26, 38, 43, 64, 74, 87, 107, 123)), + RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(38, 43, 64, 74, 87, 107, 123, 206)), + RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(2, 88, 100, 140, 267)), + RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(26, 61, 68, 86, 94, 124, 149, 195)), + RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(4, 63, 206)), + RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(53, 55, 118)), + RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=34, subgroup_3=None, rooms=(40,)), + RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=37, subgroup_3=None, rooms=(151,)), +) diff --git a/worlds/alttp/enemizer_data/enemy_sprite_requirements.py b/worlds/alttp/enemizer_data/enemy_sprite_requirements.py new file mode 100644 index 000000000000..472cc9d7b1d7 --- /dev/null +++ b/worlds/alttp/enemizer_data/enemy_sprite_requirements.py @@ -0,0 +1,295 @@ +from __future__ import annotations + +from typing import NamedTuple, Optional + + +class EnemySpriteRequirementData(NamedTuple): + sprite_name: str + sprite_id: int + boss: bool + overlord: bool + do_not_randomize: bool + killable: bool + npc: bool + never_use_dungeon: bool + never_use_overworld: bool + cannot_have_key: bool + is_object: bool + absorbable: bool + is_water_sprite: bool + is_enemy_sprite: bool + group_ids: tuple[int, ...] + subgroup_0: tuple[int, ...] + subgroup_1: tuple[int, ...] + subgroup_2: tuple[int, ...] + subgroup_3: tuple[int, ...] + parameters: Optional[int] + special_glitched: bool + excluded_rooms: tuple[int, ...] + dont_randomize_rooms: tuple[int, ...] + spawnable_rooms: tuple[int, ...] + +ENEMY_SPRITE_REQUIREMENTS = ( + EnemySpriteRequirementData(sprite_name='RavenSprite', sprite_id=0, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17, 25), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='VultureSprite', sprite_id=1, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(18,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='EmptySprite', sprite_id=3, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='PullSwitch_GoodSprite', sprite_id=4, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='PullSwitch_TrapSprite', sprite_id=6, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='Octorok_OneWaySprite', sprite_id=8, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12, 24), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='MoldormSprite', sprite_id=9, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(48,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='Octorok_FourWaySprite', sprite_id=10, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='ChickenSprite', sprite_id=11, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(21, 80), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='BuzzblobSprite', sprite_id=13, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='SnapdragonSprite', sprite_id=14, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(22,), subgroup_1=(), subgroup_2=(23,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='OctoballoonSprite', sprite_id=15, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='OctoballoonHatchlingsSprite', sprite_id=16, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='HinoxSprite', sprite_id=17, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(22,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='MoblinSprite', sprite_id=18, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(23,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='MiniHelmasaurSprite', sprite_id=19, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(30,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='GargoylesDomainGateSprite', sprite_id=20, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='AntifairySprite', sprite_id=21, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(64, 210, 268), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='SahasrahlaAginahSprite', sprite_id=22, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(76,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='BushHoarderSprite', sprite_id=23, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='MiniMoldormSprite', sprite_id=24, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(30,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='PoeSprite', sprite_id=25, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(14, 21), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='DwarvesSprite', sprite_id=26, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(77,), subgroup_2=(), subgroup_3=(21,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='ArrowInWall_MaybeSprite', sprite_id=27, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='StatueSprite', sprite_id=28, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268, 63), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='WeathervaneSprite', sprite_id=29, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='CrystalSwitchSprite', sprite_id=30, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='BugCatchingKidSprite', sprite_id=31, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(81,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='SluggulaSprite', sprite_id=32, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(37,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='PushSwitchSprite', sprite_id=33, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(83,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='RopaSprite', sprite_id=34, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(22,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='RedBariSprite', sprite_id=35, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(127,), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='BlueBariSprite', sprite_id=36, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(127,), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='TalkingTreeSprite', sprite_id=37, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(21,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='HardhatBeetleSprite', sprite_id=38, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(30,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='DeadrockSprite', sprite_id=39, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(16,), parameters=None, special_glitched=False, excluded_rooms=(127, 268), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='StorytellersSprite', sprite_id=40, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='BlindHideoutAttendantSprite', sprite_id=41, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(14, 79), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='SweepingLadySprite', sprite_id=42, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='MultipurposeSpriteSprite', sprite_id=43, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='LumberjacksSprite', sprite_id=44, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(74,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='TelepathicStones_NoIdeaWhatThisActuallyIsLikelyUnusedSprite', sprite_id=45, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='FluteBoysNotesSprite', sprite_id=46, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='RaceHPNPCsSprite', sprite_id=47, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='Person_MaybeSprite', sprite_id=48, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='FortuneTellerSprite', sprite_id=49, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='AngryBrothersSprite', sprite_id=50, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(79,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='PullForRupeesSpriteSprite', sprite_id=51, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='ScaredGirl2Sprite', sprite_id=52, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='InnkeeperSprite', sprite_id=53, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='WitchSprite', sprite_id=54, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(76,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='WaterfallSprite', sprite_id=55, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='ArrowTargetSprite', sprite_id=56, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='AverageMiddleAgedManSprite', sprite_id=57, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='HalfMagicBatSprite', sprite_id=58, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(29,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='DashItemSprite', sprite_id=59, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='VillageKidSprite', sprite_id=60, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='Signs_ChickenLadyAlsoShowedUp_ScaredLadiesOutsideHousesSprite', sprite_id=61, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='RockHoarderSprite', sprite_id=62, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='TutorialSoldierSprite', sprite_id=63, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='LightningLockSprite', sprite_id=64, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(63,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='BlueSwordSoldier_DetectPlayerSprite', sprite_id=65, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(13, 73), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='GreenSwordSoldierSprite', sprite_id=66, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='RedSpearSoldierSprite', sprite_id=67, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(13, 73), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='AssaultSwordSoldierSprite', sprite_id=68, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='GreenSpearSoldierSprite', sprite_id=69, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(13, 73), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='BlueArcherSprite', sprite_id=70, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(72,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='GreenArcherSprite', sprite_id=71, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(72,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='RedJavelinSoldierSprite', sprite_id=72, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='RedJavelinSoldier2Sprite', sprite_id=73, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='RedBombSoldiersSprite', sprite_id=74, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='GreenSoldierRecruits_HMKnightSprite', sprite_id=75, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(73,), subgroup_2=(19,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='GeldmanSprite', sprite_id=76, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(18,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='RabbitSprite', sprite_id=77, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='PopoSprite', sprite_id=78, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(44,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='Popo2Sprite', sprite_id=79, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(44,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='CannonBallsSprite', sprite_id=80, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(46,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='ArmosSprite', sprite_id=81, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(16,), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='GiantZoraSprite', sprite_id=82, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(68,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='ArmosKnightsSprite', sprite_id=83, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(29,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='LanmolasSprite', sprite_id=84, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(49,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='FireballZoraSprite', sprite_id=85, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12, 24), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='WalkingZoraSprite', sprite_id=86, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12,), subgroup_3=(68,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='DesertPalaceBarriersSprite', sprite_id=87, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(18,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='CrabSprite', sprite_id=88, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='BirdSprite', sprite_id=89, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(55,), subgroup_3=(54,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='SquirrelSprite', sprite_id=90, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(55,), subgroup_3=(54,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='Spark_LeftToRightSprite', sprite_id=91, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='Spark_RightToLeftSprite', sprite_id=92, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='Roller_VerticalMovingSprite', sprite_id=93, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='Roller_VerticalMoving2Sprite', sprite_id=94, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='RollerSprite', sprite_id=95, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='Roller_HorizontalMovingSprite', sprite_id=96, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='BeamosSprite', sprite_id=97, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(44,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='MasterSwordSprite', sprite_id=98, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(55,), subgroup_3=(54,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='Devalant_NonShooterSprite', sprite_id=99, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='Devalant_ShooterSprite', sprite_id=100, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='ShootingGalleryProprietorSprite', sprite_id=101, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='MovingCannonBallShooters_RightSprite', sprite_id=102, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='MovingCannonBallShooters_LeftSprite', sprite_id=103, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='MovingCannonBallShooters_DownSprite', sprite_id=104, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='MovingCannonBallShooters_UpSprite', sprite_id=105, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='BallNChainTrooperSprite', sprite_id=106, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='CannonSoldierSprite', sprite_id=107, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='MirrorPortalSprite', sprite_id=108, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='RatSprite', sprite_id=109, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(28, 36), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='RopeSprite', sprite_id=110, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(28, 36), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='KeeseSprite', sprite_id=111, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(28, 36), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='LeeverSprite', sprite_id=113, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='ActivatoForThePonds_WhereYouThrowInItemsSprite', sprite_id=114, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(54,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='UnclePriestSprite', sprite_id=115, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(71, 81), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='RunningManSprite', sprite_id=116, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='BottleSalesmanSprite', sprite_id=117, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='PrincessZeldaSprite', sprite_id=118, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='VillageElderSprite', sprite_id=120, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(77,), subgroup_2=(74,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='AgahnimSprite', sprite_id=122, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(85,), subgroup_1=(26, 61), subgroup_2=(66,), subgroup_3=(67,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='AgahnimEnergyBallSprite', sprite_id=123, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='FloatingStalfosHeadSprite', sprite_id=124, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='BigSpikeTrapSprite', sprite_id=125, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='GuruguruBar_ClockwiseSprite', sprite_id=126, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(181, 150), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='GuruguruBar_CounterClockwiseSprite', sprite_id=127, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(181, 150), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='WinderSprite', sprite_id=128, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='WaterTektiteSprite', sprite_id=129, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(40,), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='AntifairyCircleSprite', sprite_id=130, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='GreenEyegoreSprite', sprite_id=131, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(46,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='RedEyegoreSprite', sprite_id=132, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(46,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='KodongosSprite', sprite_id=134, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(42,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='MothulaSprite', sprite_id=136, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(56,), subgroup_3=(82,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='MothulasBeamSprite', sprite_id=137, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(56,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='SpikeTrapSprite', sprite_id=138, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(40, 11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='GibdoSprite', sprite_id=139, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(35,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='ArrghusSprite', sprite_id=140, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(57,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='ArrghusSpawnSprite', sprite_id=141, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(57,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='TerrorpinSprite', sprite_id=142, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(42,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='SlimeSprite_JumpsOutOfTheFloor', sprite_id=143, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='WallmasterSprite', sprite_id=144, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(35,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(1, 2, 17, 33, 34, 50, 65, 66, 80, 81, 82, 96, 97, 98, 112, 113, 114, 128, 129, 130, 137, 153, 168, 169, 170, 184, 185, 186, 200, 201, 216, 217, 218, 51, 67, 83, 99, 115, 116, 117, 131, 132, 133, 7, 23, 39, 49, 119, 135, 167, 32, 48, 64, 176, 192, 208, 224, 9, 10, 11, 25, 26, 27, 42, 43, 58, 59, 74, 75, 90, 106, 6, 22, 38, 40, 52, 53, 54, 55, 56, 70, 84, 102, 118, 41, 57, 73, 86, 87, 88, 89, 103, 104, 68, 69, 100, 101, 171, 172, 187, 188, 203, 204, 219, 220, 14, 30, 31, 46, 62, 63, 78, 79, 94, 95, 110, 126, 127, 142, 158, 159, 174, 175, 190, 191, 206, 222, 144, 145, 146, 147, 151, 152, 160, 161, 162, 163, 177, 178, 179, 193, 194, 195, 209, 210, 4, 19, 20, 21, 35, 36, 164, 180, 181, 182, 183, 196, 197, 198, 199, 213, 214, 12, 13, 28, 29, 61, 76, 77, 91, 92, 93, 107, 108, 109, 123, 124, 125, 139, 140, 141, 149, 150, 155, 156, 157, 165, 166)), + EnemySpriteRequirementData(sprite_name='StalfosKnightSprite', sprite_id=145, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='HelmasaurKingSprite', sprite_id=146, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(58,), subgroup_3=(62,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='BumperSprite', sprite_id=147, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='SwimmersEvilSprite', sprite_id=148, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='EyeLaser_RightSprite', sprite_id=149, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='EyeLaser_LeftSprite', sprite_id=150, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='EyeLaser_DownSprite', sprite_id=151, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='EyeLaser_UpSprite', sprite_id=152, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='PengatorSprite', sprite_id=153, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(38,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='KyameronWaterSplashSprite', sprite_id=154, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(40,), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='WizzrobeSprite', sprite_id=155, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(37, 41), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='VerminHorizontalSprite', sprite_id=156, boss=False, overlord=False, do_not_randomize=True, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='VerminVerticalSprite', sprite_id=157, boss=False, overlord=False, do_not_randomize=True, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='Ostrich_HauntedGroveSprite', sprite_id=158, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(78,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='FluteSprite', sprite_id=159, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='Birds_HauntedGroveSprite', sprite_id=160, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(78,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='FreezorSprite', sprite_id=161, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(38,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='KholdstareSprite', sprite_id=162, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(60,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='KholdstaresShellSprite', sprite_id=163, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='FallingIceSprite', sprite_id=164, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(60,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='BlueZazakSprite', sprite_id=165, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(40,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='RedZazakSprite', sprite_id=166, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(40,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='StalfosSprite', sprite_id=167, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='BomberFlyingCreaturesFromDarkworldSprite', sprite_id=168, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(27,), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='BomberFlyingCreaturesFromDarkworld2Sprite', sprite_id=169, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(27,), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='PikitSprite', sprite_id=170, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(27,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='MaidenSprite', sprite_id=171, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='AppleSprite', sprite_id=172, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='LostOldManSprite', sprite_id=173, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(28,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='DownPipeSprite', sprite_id=174, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='UpPipeSprite', sprite_id=175, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='RightPipeSprite', sprite_id=176, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='LeftPipeSprite', sprite_id=177, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='GoodBee_AgainMaybeSprite', sprite_id=178, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='HylianInscriptionSprite', sprite_id=179, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='ThiefsChestSprite', sprite_id=180, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(21,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='BombSalesmanSprite', sprite_id=181, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(77,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='KikiSprite', sprite_id=182, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(25,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='MaidenInBlindDungeonSprite', sprite_id=183, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='MimicSprite', sprite_id=184, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(44,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='FeudingFriendsOnDeathMountainSprite', sprite_id=185, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(20,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='WhirlpoolSprite', sprite_id=186, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(), subgroup_2=(74,), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(255, 274, 287)), + EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(77,), subgroup_2=(74,), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(271, 272)), + EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(77,), subgroup_2=(74,), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(272,)), + EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(79,), subgroup_1=(), subgroup_2=(74,), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(280,)), + EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(14,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(291, 292)), + EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(14,), subgroup_1=(), subgroup_2=(74,), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(291, 292)), + EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(14,), subgroup_1=(), subgroup_2=(74,), subgroup_3=(80,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(293,)), + EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(21,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(286,)), + EnemySpriteRequirementData(sprite_name='DrunkInTheInnSprite', sprite_id=188, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(79,), subgroup_1=(77,), subgroup_2=(74,), subgroup_3=(80,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='Vitreous_LargeEyeballSprite', sprite_id=189, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(61,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='Vitreous_SmallEyeballSprite', sprite_id=190, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(61,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='VitreousLightningSprite', sprite_id=191, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(61,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='CatFish_QuakeMedallionSprite', sprite_id=192, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(24,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='AgahnimTeleportingZeldaToDarkworldSprite', sprite_id=193, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(85,), subgroup_1=(61,), subgroup_2=(66,), subgroup_3=(67,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='BouldersSprite', sprite_id=194, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(16,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='Gibo_FloatingBlobSprite', sprite_id=195, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(40,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='ThiefSprite', sprite_id=196, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(14, 21), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='MedusaSprite', sprite_id=197, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='FourWayFireballSpittersSprite', sprite_id=198, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='HokkuBokkuSprite', sprite_id=199, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='BigFairyWhoHealsYouSprite', sprite_id=200, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(57,), subgroup_3=(54,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='TektiteSprite', sprite_id=201, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(16,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='ChainChompSprite', sprite_id=202, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='TrinexxSprite', sprite_id=203, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(64,), subgroup_1=(), subgroup_2=(), subgroup_3=(63,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='AnotherPartOfTrinexxSprite', sprite_id=204, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(64,), subgroup_1=(), subgroup_2=(), subgroup_3=(63,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='YetAnotherPartOfTrinexxSprite', sprite_id=205, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(64,), subgroup_1=(), subgroup_2=(), subgroup_3=(63,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='BlindTheThiefSprite', sprite_id=206, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(44,), subgroup_2=(59,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='SwamolaSprite', sprite_id=207, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(25,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='LynelSprite', sprite_id=208, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(20,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='BunnyBeamSprite', sprite_id=209, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='FloppingFishSprite', sprite_id=210, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='StalSprite', sprite_id=211, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='LandmineSprite', sprite_id=212, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='DiggingGameProprietorSprite', sprite_id=213, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(42,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='GanonSprite', sprite_id=214, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(33,), subgroup_1=(65,), subgroup_2=(69,), subgroup_3=(51,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='CopyOfGanon_ExceptInvincibleSprite', sprite_id=215, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='HeartSprite', sprite_id=216, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='GreenRupeeSprite', sprite_id=217, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='BlueRupeeSprite', sprite_id=218, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='RedRupeeSprite', sprite_id=219, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='BombRefill1Sprite', sprite_id=220, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='BombRefill4Sprite', sprite_id=221, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='BombRefill8Sprite', sprite_id=222, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='SmallMagicRefillSprite', sprite_id=223, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='FullMagicRefillSprite', sprite_id=224, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='ArrowRefill5Sprite', sprite_id=225, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='ArrowRefill10Sprite', sprite_id=226, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='FairySprite', sprite_id=227, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='KeySprite', sprite_id=228, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='BigKeySprite', sprite_id=229, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='ShieldEaterSprite', sprite_id=230, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(27,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='MushroomSprite', sprite_id=231, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='FakeMasterSwordSprite', sprite_id=232, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='MagicShopDude_HisItemsIncludingTheMagicPowderSprite', sprite_id=233, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(), subgroup_2=(), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='HeartContainerSprite', sprite_id=234, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='HeartPieceSprite', sprite_id=235, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='BushesSprite', sprite_id=236, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='CaneOfSomariaPlatformSprite', sprite_id=237, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='MantleSprite', sprite_id=238, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(93,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='CaneOfSomariaPlatform_Unused1Sprite', sprite_id=239, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='CaneOfSomariaPlatform_Unused2Sprite', sprite_id=240, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='CaneOfSomariaPlatform_Unused3Sprite', sprite_id=241, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='MedallionTabletSprite', sprite_id=242, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(18,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='OW_OL_FallingRocks', sprite_id=244, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(16,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='OL_CanonBalls_EP4Walls', sprite_id=258, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(46,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='OL_CanonBalls_EPEntrance', sprite_id=259, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(46,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='OL_StalfosHeadTrap', sprite_id=261, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='OL_BombDrop_RopeTrap', sprite_id=262, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(28, 36), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='OL_MovingFloor', sprite_id=263, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='OL_SlimeDropper', sprite_id=264, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='OL_Wallmaster', sprite_id=265, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(35,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='OL_FloorDrop_Square', sprite_id=266, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='OL_FloorDrop_Path', sprite_id=267, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='OL_RightEvil_PirogusuSpawner', sprite_id=272, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='OL_LeftEvil_PirogusuSpawner', sprite_id=273, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='OL_DownEvil_PirogusuSpawner', sprite_id=274, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='OL_UpEvil_PirogusuSpawner', sprite_id=275, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='OL_FlyingFloorTileTrap', sprite_id=276, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='OL_WizzrobeSpawner', sprite_id=277, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(37, 41), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='OL_BlackSpawn_Zoro_BombHole', sprite_id=278, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='OL_4Skull_Trap_Pot', sprite_id=279, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='OL_Stalfos_Spawn_Trap_EP', sprite_id=280, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='OL_ArmosKnight_Trigger', sprite_id=281, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), + EnemySpriteRequirementData(sprite_name='OL_BombDrop_BombTrap', sprite_id=282, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()), +) diff --git a/worlds/alttp/enemizer_data/overworld_enemy_metadata.py b/worlds/alttp/enemizer_data/overworld_enemy_metadata.py new file mode 100644 index 000000000000..2c4582ae4ce3 --- /dev/null +++ b/worlds/alttp/enemizer_data/overworld_enemy_metadata.py @@ -0,0 +1,131 @@ +from __future__ import annotations + +from typing import NamedTuple, Optional + + +class OverworldGroupRequirementData(NamedTuple): + group_id: Optional[int] + subgroup_0: Optional[int] + subgroup_1: Optional[int] + subgroup_2: Optional[int] + subgroup_3: Optional[int] + areas: tuple[int, ...] + +AREA_IDS = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207) +DO_NOT_RANDOMIZE_AREA_IDS = frozenset((1, + 4, + 6, + 8, + 9, + 11, + 12, + 13, + 14, + 25, + 28, + 31, + 32, + 33, + 35, + 36, + 38, + 39, + 49, + 54, + 56, + 57, + 61, + 62, + 65, + 68, + 70, + 72, + 73, + 75, + 76, + 77, + 78, + 89, + 92, + 95, + 96, + 97, + 99, + 100, + 102, + 103, + 113, + 118, + 120, + 121, + 125, + 126, + 42, + 106, + 130, + 131, + 132, + 133, + 134, + 135, + 136, + 137, + 138, + 139, + 140, + 141, + 142, + 143, + 186, + 250, + 145, + 148, + 150, + 152, + 153, + 155, + 156, + 158, + 169, + 172, + 175, + 176, + 177, + 179, + 180, + 182, + 183, + 193, + 198, + 200, + 274, + 275, + 276, + 277, + 278, + 279, + 281, + 288)) + +FORCED_GROUP_REQUIREMENTS = ( + OverworldGroupRequirementData(group_id=7, subgroup_0=None, subgroup_1=None, subgroup_2=74, subgroup_3=None, areas=(2,)), + OverworldGroupRequirementData(group_id=16, subgroup_0=None, subgroup_1=None, subgroup_2=18, subgroup_3=16, areas=(3, 147)), + OverworldGroupRequirementData(group_id=7, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=17, areas=(10, 154)), + OverworldGroupRequirementData(group_id=4, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=None, areas=(15, 159)), + OverworldGroupRequirementData(group_id=3, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=14, areas=(20, 164)), + OverworldGroupRequirementData(group_id=1, subgroup_0=None, subgroup_1=None, subgroup_2=76, subgroup_3=63, areas=(27, 171)), + OverworldGroupRequirementData(group_id=6, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=None, areas=(34, 40, 178, 184)), + OverworldGroupRequirementData(group_id=8, subgroup_0=None, subgroup_1=None, subgroup_2=18, subgroup_3=None, areas=(48, 192)), + OverworldGroupRequirementData(group_id=10, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=None, areas=(58, 202)), + OverworldGroupRequirementData(group_id=22, subgroup_0=None, subgroup_1=None, subgroup_2=24, subgroup_3=None, areas=(79, 223)), + OverworldGroupRequirementData(group_id=21, subgroup_0=21, subgroup_1=None, subgroup_2=None, subgroup_3=21, areas=(98, 242)), + OverworldGroupRequirementData(group_id=27, subgroup_0=None, subgroup_1=42, subgroup_2=None, subgroup_3=None, areas=(104, 248)), + OverworldGroupRequirementData(group_id=13, subgroup_0=None, subgroup_1=None, subgroup_2=76, subgroup_3=None, areas=(22, 166)), + OverworldGroupRequirementData(group_id=29, subgroup_0=None, subgroup_1=77, subgroup_2=None, subgroup_3=21, areas=(105, 249)), + OverworldGroupRequirementData(group_id=15, subgroup_0=None, subgroup_1=None, subgroup_2=78, subgroup_3=None, areas=(42, 186)), + OverworldGroupRequirementData(group_id=17, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=76, areas=(106, 250)), + OverworldGroupRequirementData(group_id=12, subgroup_0=None, subgroup_1=None, subgroup_2=55, subgroup_3=54, areas=(128, 272)), + OverworldGroupRequirementData(group_id=14, subgroup_0=None, subgroup_1=None, subgroup_2=12, subgroup_3=68, areas=(129, 273)), + OverworldGroupRequirementData(group_id=26, subgroup_0=15, subgroup_1=None, subgroup_2=None, subgroup_3=None, areas=(146,)), + OverworldGroupRequirementData(group_id=23, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=25, areas=(94, 238)), +) diff --git a/worlds/alttp/enemizer_data/pot_shuffle_data.py b/worlds/alttp/enemizer_data/pot_shuffle_data.py new file mode 100644 index 000000000000..5b6ae17c994f --- /dev/null +++ b/worlds/alttp/enemizer_data/pot_shuffle_data.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +from typing import NamedTuple + + +class PotDataRecord(NamedTuple): + x: int + y: int + reserved: int + + +class PotRoomDataRecord(NamedTuple): + room_id: int + pots: tuple[PotDataRecord, ...] + items: tuple[int, ...] + +POT_ROOMS = ( + PotRoomDataRecord(room_id=4, pots=(PotDataRecord(x=162, y=25, reserved=0), PotDataRecord(x=152, y=25, reserved=0), PotDataRecord(x=152, y=22, reserved=0), PotDataRecord(x=162, y=22, reserved=0), PotDataRecord(x=240, y=19, reserved=0), PotDataRecord(x=204, y=19, reserved=0),), items=(10, 10)), + PotRoomDataRecord(room_id=9, pots=(PotDataRecord(x=12, y=4, reserved=0), PotDataRecord(x=48, y=4, reserved=0), PotDataRecord(x=12, y=12, reserved=0),), items=(1, 11, 136)), + PotRoomDataRecord(room_id=10, pots=(PotDataRecord(x=204, y=11, reserved=0), PotDataRecord(x=156, y=17, reserved=0), PotDataRecord(x=96, y=8, reserved=0), PotDataRecord(x=100, y=7, reserved=0), PotDataRecord(x=160, y=17, reserved=0), PotDataRecord(x=104, y=8, reserved=0), PotDataRecord(x=100, y=9, reserved=0),), items=(11, 11, 136)), + PotRoomDataRecord(room_id=17, pots=(PotDataRecord(x=152, y=19, reserved=0), PotDataRecord(x=152, y=15, reserved=0), PotDataRecord(x=144, y=15, reserved=0), PotDataRecord(x=10, y=15, reserved=0), PotDataRecord(x=144, y=19, reserved=0), PotDataRecord(x=160, y=19, reserved=0),), items=(11, 11, 11, 11)), + PotRoomDataRecord(room_id=21, pots=(PotDataRecord(x=96, y=4, reserved=0), PotDataRecord(x=100, y=4, reserved=0), PotDataRecord(x=104, y=4, reserved=0), PotDataRecord(x=108, y=4, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=12, y=6, reserved=0), PotDataRecord(x=16, y=6, reserved=0), PotDataRecord(x=20, y=6, reserved=0), PotDataRecord(x=70, y=11, reserved=0),), items=(1, 7, 9, 9, 10, 11, 12, 12, 13)), + PotRoomDataRecord(room_id=22, pots=(PotDataRecord(x=188, y=3, reserved=0), PotDataRecord(x=192, y=3, reserved=0), PotDataRecord(x=188, y=4, reserved=0), PotDataRecord(x=192, y=4, reserved=0), PotDataRecord(x=188, y=5, reserved=0), PotDataRecord(x=192, y=5, reserved=0), PotDataRecord(x=188, y=6, reserved=0), PotDataRecord(x=192, y=6, reserved=0), PotDataRecord(x=240, y=19, reserved=0),), items=(8, 9, 9, 10, 10, 11, 11, 12, 12)), + PotRoomDataRecord(room_id=26, pots=(PotDataRecord(x=232, y=19, reserved=0), PotDataRecord(x=212, y=19, reserved=0), PotDataRecord(x=28, y=5, reserved=0), PotDataRecord(x=32, y=5, reserved=0), PotDataRecord(x=28, y=27, reserved=0), PotDataRecord(x=32, y=27, reserved=0),), items=(10, 10, 10, 10)), + PotRoomDataRecord(room_id=33, pots=(PotDataRecord(x=100, y=28, reserved=0), PotDataRecord(x=168, y=24, reserved=0), PotDataRecord(x=48, y=28, reserved=0), PotDataRecord(x=82, y=28, reserved=0), PotDataRecord(x=160, y=20, reserved=0), PotDataRecord(x=104, y=28, reserved=0),), items=(11, 12, 12)), + PotRoomDataRecord(room_id=35, pots=(PotDataRecord(x=86, y=26, reserved=0), PotDataRecord(x=90, y=26, reserved=0), PotDataRecord(x=94, y=26, reserved=0), PotDataRecord(x=98, y=26, reserved=0), PotDataRecord(x=102, y=26, reserved=0),), items=(1, 10, 11)), + PotRoomDataRecord(room_id=36, pots=(PotDataRecord(x=12, y=4, reserved=0), PotDataRecord(x=48, y=4, reserved=0), PotDataRecord(x=12, y=12, reserved=0), PotDataRecord(x=48, y=12, reserved=0),), items=(1, 11, 12, 7)), + PotRoomDataRecord(room_id=38, pots=(PotDataRecord(x=28, y=4, reserved=0), PotDataRecord(x=12, y=8, reserved=0), PotDataRecord(x=150, y=19, reserved=2), PotDataRecord(x=22, y=26, reserved=2), PotDataRecord(x=220, y=26, reserved=0),), items=(7, 9, 10, 12, 136)), + PotRoomDataRecord(room_id=39, pots=(PotDataRecord(x=214, y=19, reserved=0), PotDataRecord(x=214, y=20, reserved=0), PotDataRecord(x=166, y=20, reserved=0), PotDataRecord(x=214, y=21, reserved=0), PotDataRecord(x=40, y=28, reserved=0), PotDataRecord(x=44, y=28, reserved=0), PotDataRecord(x=80, y=28, reserved=0), PotDataRecord(x=84, y=28, reserved=0), PotDataRecord(x=102, y=17, reserved=0), PotDataRecord(x=98, y=17, reserved=0), PotDataRecord(x=106, y=17, reserved=0), PotDataRecord(x=166, y=21, reserved=0), PotDataRecord(x=166, y=19, reserved=0), PotDataRecord(x=92, y=12, reserved=0), PotDataRecord(x=160, y=12, reserved=0),), items=(1, 1, 10, 11, 7, 7)), + PotRoomDataRecord(room_id=43, pots=(PotDataRecord(x=16, y=5, reserved=2), PotDataRecord(x=44, y=5, reserved=2), PotDataRecord(x=16, y=6, reserved=2), PotDataRecord(x=44, y=6, reserved=2), PotDataRecord(x=16, y=7, reserved=2), PotDataRecord(x=44, y=7, reserved=2), PotDataRecord(x=146, y=21, reserved=0), PotDataRecord(x=170, y=21, reserved=0), PotDataRecord(x=146, y=22, reserved=0), PotDataRecord(x=170, y=22, reserved=0),), items=(9, 9, 10, 10, 10, 10, 11, 11, 11, 136)), + PotRoomDataRecord(room_id=47, pots=(PotDataRecord(x=28, y=7, reserved=0), PotDataRecord(x=32, y=7, reserved=0), PotDataRecord(x=28, y=9, reserved=0), PotDataRecord(x=32, y=9, reserved=0), PotDataRecord(x=172, y=19, reserved=0), PotDataRecord(x=180, y=19, reserved=0), PotDataRecord(x=104, y=27, reserved=0), PotDataRecord(x=104, y=28, reserved=0),), items=(7, 7, 7, 7, 11, 11, 11, 11)), + PotRoomDataRecord(room_id=53, pots=(PotDataRecord(x=60, y=6, reserved=1), PotDataRecord(x=20, y=8, reserved=0), PotDataRecord(x=24, y=8, reserved=0), PotDataRecord(x=28, y=8, reserved=0), PotDataRecord(x=32, y=8, reserved=0), PotDataRecord(x=36, y=8, reserved=0), PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=76, y=23, reserved=1), PotDataRecord(x=88, y=23, reserved=1), PotDataRecord(x=100, y=27, reserved=1), PotDataRecord(x=242, y=28, reserved=1), PotDataRecord(x=240, y=22, reserved=1), PotDataRecord(x=76, y=28, reserved=1),), items=(7, 7, 7, 7, 7, 8, 11)), + PotRoomDataRecord(room_id=54, pots=(PotDataRecord(x=108, y=4, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=10, y=16, reserved=0), PotDataRecord(x=114, y=16, reserved=0),), items=(8, 10, 11)), + PotRoomDataRecord(room_id=55, pots=(PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=60, y=6, reserved=0),), items=(8,)), + PotRoomDataRecord(room_id=56, pots=(PotDataRecord(x=164, y=12, reserved=0), PotDataRecord(x=164, y=13, reserved=0), PotDataRecord(x=164, y=18, reserved=0), PotDataRecord(x=164, y=19, reserved=0),), items=(8, 7, 10, 10)), + PotRoomDataRecord(room_id=57, pots=(PotDataRecord(x=12, y=20, reserved=0), PotDataRecord(x=100, y=22, reserved=0), PotDataRecord(x=100, y=26, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(9, 9, 11, 12)), + PotRoomDataRecord(room_id=60, pots=(PotDataRecord(x=24, y=8, reserved=0), PotDataRecord(x=64, y=12, reserved=0), PotDataRecord(x=20, y=14, reserved=0), PotDataRecord(x=68, y=18, reserved=0), PotDataRecord(x=96, y=19, reserved=0), PotDataRecord(x=64, y=20, reserved=0), PotDataRecord(x=64, y=26, reserved=0),), items=(1, 7, 7, 7, 7, 11, 12)), + PotRoomDataRecord(room_id=61, pots=(PotDataRecord(x=76, y=12, reserved=0), PotDataRecord(x=112, y=12, reserved=0), PotDataRecord(x=24, y=22, reserved=0), PotDataRecord(x=40, y=22, reserved=0), PotDataRecord(x=32, y=24, reserved=0), PotDataRecord(x=20, y=26, reserved=0), PotDataRecord(x=36, y=26, reserved=0),), items=(9, 7, 10, 10, 11, 11, 13)), + PotRoomDataRecord(room_id=62, pots=(PotDataRecord(x=96, y=6, reserved=0), PotDataRecord(x=100, y=6, reserved=0), PotDataRecord(x=88, y=10, reserved=0), PotDataRecord(x=92, y=10, reserved=0),), items=(10, 11, 12, 12)), + PotRoomDataRecord(room_id=63, pots=(PotDataRecord(x=12, y=25, reserved=0), PotDataRecord(x=20, y=25, reserved=0), PotDataRecord(x=12, y=26, reserved=0), PotDataRecord(x=20, y=26, reserved=0), PotDataRecord(x=12, y=27, reserved=0), PotDataRecord(x=20, y=27, reserved=0), PotDataRecord(x=28, y=23, reserved=0),), items=(1, 1, 8, 10, 10, 11, 136)), + PotRoomDataRecord(room_id=65, pots=(PotDataRecord(x=100, y=10, reserved=0), PotDataRecord(x=52, y=15, reserved=0), PotDataRecord(x=52, y=16, reserved=0), PotDataRecord(x=148, y=22, reserved=0),), items=(1, 11, 12, 12)), + PotRoomDataRecord(room_id=67, pots=(PotDataRecord(x=112, y=28, reserved=1), PotDataRecord(x=76, y=28, reserved=1), PotDataRecord(x=76, y=20, reserved=1), PotDataRecord(x=66, y=4, reserved=0), PotDataRecord(x=78, y=4, reserved=0), PotDataRecord(x=66, y=9, reserved=0), PotDataRecord(x=78, y=9, reserved=0), PotDataRecord(x=112, y=20, reserved=1),), items=(8, 9, 11, 11, 12)), + PotRoomDataRecord(room_id=69, pots=(PotDataRecord(x=12, y=4, reserved=0), PotDataRecord(x=108, y=11, reserved=0), PotDataRecord(x=48, y=12, reserved=0), PotDataRecord(x=220, y=16, reserved=0), PotDataRecord(x=236, y=16, reserved=0),), items=(9, 9, 11, 11, 12)), + PotRoomDataRecord(room_id=73, pots=(PotDataRecord(x=156, y=27, reserved=0), PotDataRecord(x=172, y=24, reserved=0), PotDataRecord(x=172, y=23, reserved=0), PotDataRecord(x=144, y=20, reserved=0), PotDataRecord(x=104, y=15, reserved=0), PotDataRecord(x=104, y=16, reserved=0), PotDataRecord(x=144, y=19, reserved=0), PotDataRecord(x=172, y=20, reserved=0), PotDataRecord(x=144, y=27, reserved=0), PotDataRecord(x=172, y=28, reserved=0), PotDataRecord(x=160, y=27, reserved=0),), items=(11, 11, 12, 12, 12, 12)), + PotRoomDataRecord(room_id=78, pots=(PotDataRecord(x=48, y=10, reserved=2), PotDataRecord(x=140, y=11, reserved=2), PotDataRecord(x=28, y=12, reserved=2), PotDataRecord(x=112, y=12, reserved=0),), items=(136, 11, 12)), + PotRoomDataRecord(room_id=83, pots=(PotDataRecord(x=92, y=11, reserved=0), PotDataRecord(x=96, y=11, reserved=0), PotDataRecord(x=100, y=11, reserved=0), PotDataRecord(x=104, y=11, reserved=0),), items=(8, 11, 11, 12)), + PotRoomDataRecord(room_id=84, pots=(PotDataRecord(x=186, y=25, reserved=0), PotDataRecord(x=186, y=26, reserved=0), PotDataRecord(x=186, y=27, reserved=0), PotDataRecord(x=186, y=28, reserved=0),), items=(7, 11, 11, 11)), + PotRoomDataRecord(room_id=86, pots=(PotDataRecord(x=100, y=6, reserved=1), PotDataRecord(x=96, y=10, reserved=1), PotDataRecord(x=92, y=10, reserved=1), PotDataRecord(x=48, y=20, reserved=1), PotDataRecord(x=20, y=6, reserved=0), PotDataRecord(x=40, y=6, reserved=0), PotDataRecord(x=24, y=7, reserved=0), PotDataRecord(x=36, y=7, reserved=0), PotDataRecord(x=12, y=8, reserved=0), PotDataRecord(x=48, y=8, reserved=0), PotDataRecord(x=24, y=9, reserved=0), PotDataRecord(x=36, y=9, reserved=0), PotDataRecord(x=20, y=10, reserved=0), PotDataRecord(x=40, y=10, reserved=0), PotDataRecord(x=12, y=20, reserved=1),), items=(7, 7, 11, 11, 8, 12, 12, 12, 12, 12, 12)), + PotRoomDataRecord(room_id=87, pots=(PotDataRecord(x=92, y=7, reserved=0), PotDataRecord(x=12, y=20, reserved=2), PotDataRecord(x=92, y=23, reserved=0), PotDataRecord(x=100, y=23, reserved=0), PotDataRecord(x=84, y=25, reserved=0), PotDataRecord(x=76, y=27, reserved=0), PotDataRecord(x=48, y=20, reserved=2), PotDataRecord(x=30, y=22, reserved=2),), items=(7, 10, 11, 12, 12, 12, 13, 136)), + PotRoomDataRecord(room_id=88, pots=(PotDataRecord(x=96, y=9, reserved=0), PotDataRecord(x=92, y=8, reserved=0), PotDataRecord(x=108, y=8, reserved=0), PotDataRecord(x=108, y=6, reserved=0), PotDataRecord(x=104, y=5, reserved=0), PotDataRecord(x=92, y=6, reserved=0), PotDataRecord(x=12, y=12, reserved=0), PotDataRecord(x=16, y=7, reserved=0), PotDataRecord(x=96, y=5, reserved=0), PotDataRecord(x=100, y=5, reserved=0), PotDataRecord(x=12, y=7, reserved=0), PotDataRecord(x=92, y=7, reserved=0), PotDataRecord(x=108, y=7, reserved=0), PotDataRecord(x=16, y=8, reserved=0), PotDataRecord(x=100, y=9, reserved=0), PotDataRecord(x=104, y=9, reserved=0),), items=(10, 10, 11, 11, 12, 12, 12, 12)), + PotRoomDataRecord(room_id=91, pots=(PotDataRecord(x=218, y=37, reserved=0), PotDataRecord(x=222, y=37, reserved=0), PotDataRecord(x=226, y=37, reserved=0),), items=(136,)), + PotRoomDataRecord(room_id=92, pots=(PotDataRecord(x=228, y=25, reserved=0), PotDataRecord(x=104, y=24, reserved=0), PotDataRecord(x=228, y=22, reserved=0), PotDataRecord(x=216, y=25, reserved=0), PotDataRecord(x=84, y=24, reserved=0), PotDataRecord(x=216, y=22, reserved=0), PotDataRecord(x=94, y=22, reserved=0), PotDataRecord(x=94, y=26, reserved=0),), items=(10, 13)), + PotRoomDataRecord(room_id=93, pots=(PotDataRecord(x=16, y=5, reserved=0), PotDataRecord(x=44, y=5, reserved=0), PotDataRecord(x=16, y=11, reserved=0), PotDataRecord(x=44, y=11, reserved=0), PotDataRecord(x=12, y=20, reserved=0), PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=12, y=28, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(1, 7, 9, 9, 10, 10, 10, 12)), + PotRoomDataRecord(room_id=94, pots=(PotDataRecord(x=92, y=4, reserved=0), PotDataRecord(x=96, y=4, reserved=0), PotDataRecord(x=76, y=8, reserved=0), PotDataRecord(x=112, y=8, reserved=0),), items=(11, 11, 12, 12)), + PotRoomDataRecord(room_id=99, pots=(PotDataRecord(x=48, y=4, reserved=0), PotDataRecord(x=12, y=4, reserved=0), PotDataRecord(x=12, y=8, reserved=0), PotDataRecord(x=48, y=12, reserved=0), PotDataRecord(x=48, y=8, reserved=0), PotDataRecord(x=12, y=12, reserved=0),), items=(8, 11)), + PotRoomDataRecord(room_id=100, pots=(PotDataRecord(x=12, y=22, reserved=0), PotDataRecord(x=16, y=22, reserved=0), PotDataRecord(x=20, y=22, reserved=0), PotDataRecord(x=36, y=28, reserved=0), PotDataRecord(x=40, y=28, reserved=0), PotDataRecord(x=44, y=28, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(10, 10, 10, 10, 12, 12, 136)), + PotRoomDataRecord(room_id=102, pots=(PotDataRecord(x=48, y=37, reserved=0), PotDataRecord(x=52, y=37, reserved=0), PotDataRecord(x=56, y=37, reserved=0), PotDataRecord(x=84, y=5, reserved=0), PotDataRecord(x=104, y=5, reserved=0), PotDataRecord(x=48, y=38, reserved=0), PotDataRecord(x=52, y=38, reserved=0), PotDataRecord(x=56, y=38, reserved=0), PotDataRecord(x=84, y=6, reserved=0), PotDataRecord(x=104, y=6, reserved=0),), items=(7, 7, 9, 9, 9, 10, 10, 10, 11, 11)), + PotRoomDataRecord(room_id=103, pots=(PotDataRecord(x=22, y=26, reserved=0), PotDataRecord(x=18, y=22, reserved=0), PotDataRecord(x=92, y=9, reserved=0), PotDataRecord(x=84, y=28, reserved=0), PotDataRecord(x=12, y=7, reserved=0), PotDataRecord(x=48, y=7, reserved=0), PotDataRecord(x=96, y=19, reserved=0), PotDataRecord(x=74, y=20, reserved=0), PotDataRecord(x=18, y=23, reserved=0), PotDataRecord(x=18, y=26, reserved=0), PotDataRecord(x=104, y=28, reserved=0),), items=(9, 11, 11, 11, 12, 12, 12)), + PotRoomDataRecord(room_id=104, pots=(PotDataRecord(x=84, y=14, reserved=0), PotDataRecord(x=84, y=13, reserved=0), PotDataRecord(x=88, y=12, reserved=0), PotDataRecord(x=88, y=6, reserved=0), PotDataRecord(x=88, y=5, reserved=0), PotDataRecord(x=88, y=4, reserved=0), PotDataRecord(x=64, y=17, reserved=0), PotDataRecord(x=64, y=15, reserved=0), PotDataRecord(x=64, y=7, reserved=0), PotDataRecord(x=88, y=7, reserved=0), PotDataRecord(x=64, y=16, reserved=0), PotDataRecord(x=64, y=24, reserved=0), PotDataRecord(x=64, y=25, reserved=0),), items=(11, 11, 11, 12, 12)), + PotRoomDataRecord(room_id=115, pots=(PotDataRecord(x=154, y=21, reserved=0), PotDataRecord(x=158, y=21, reserved=0), PotDataRecord(x=20, y=23, reserved=0), PotDataRecord(x=36, y=23, reserved=0), PotDataRecord(x=144, y=24, reserved=0), PotDataRecord(x=168, y=24, reserved=0), PotDataRecord(x=20, y=26, reserved=0), PotDataRecord(x=36, y=26, reserved=0), PotDataRecord(x=154, y=27, reserved=0), PotDataRecord(x=158, y=27, reserved=0),), items=(1, 1, 11, 11, 7, 7, 9, 9, 12, 136)), + PotRoomDataRecord(room_id=116, pots=(PotDataRecord(x=30, y=5, reserved=0), PotDataRecord(x=62, y=5, reserved=0), PotDataRecord(x=94, y=5, reserved=0), PotDataRecord(x=14, y=11, reserved=0), PotDataRecord(x=46, y=11, reserved=0), PotDataRecord(x=78, y=11, reserved=0), PotDataRecord(x=110, y=11, reserved=0),), items=(9, 9, 11, 11, 12, 12, 136)), + PotRoomDataRecord(room_id=117, pots=(PotDataRecord(x=148, y=22, reserved=0), PotDataRecord(x=160, y=22, reserved=0), PotDataRecord(x=172, y=22, reserved=0),), items=(9, 11, 12)), + PotRoomDataRecord(room_id=123, pots=(PotDataRecord(x=48, y=10, reserved=0), PotDataRecord(x=88, y=10, reserved=0), PotDataRecord(x=76, y=7, reserved=0), PotDataRecord(x=60, y=4, reserved=0), PotDataRecord(x=64, y=4, reserved=0),), items=(11, 8)), + PotRoomDataRecord(room_id=124, pots=(PotDataRecord(x=36, y=21, reserved=0), PotDataRecord(x=24, y=11, reserved=0), PotDataRecord(x=28, y=4, reserved=0), PotDataRecord(x=32, y=4, reserved=0),), items=(11, 11)), + PotRoomDataRecord(room_id=125, pots=(PotDataRecord(x=44, y=12, reserved=0), PotDataRecord(x=44, y=6, reserved=0), PotDataRecord(x=112, y=6, reserved=0), PotDataRecord(x=108, y=20, reserved=0), PotDataRecord(x=114, y=20, reserved=0), PotDataRecord(x=76, y=28, reserved=0),), items=(9, 10, 10, 11)), + PotRoomDataRecord(room_id=126, pots=(PotDataRecord(x=86, y=15, reserved=0), PotDataRecord(x=82, y=26, reserved=0), PotDataRecord(x=100, y=26, reserved=0), PotDataRecord(x=104, y=26, reserved=0),), items=(11, 12, 136)), + PotRoomDataRecord(room_id=130, pots=(PotDataRecord(x=50, y=5, reserved=0), PotDataRecord(x=50, y=10, reserved=0), PotDataRecord(x=76, y=50, reserved=0),), items=(11,)), + PotRoomDataRecord(room_id=131, pots=(PotDataRecord(x=76, y=4, reserved=0), PotDataRecord(x=80, y=4, reserved=0), PotDataRecord(x=76, y=28, reserved=0), PotDataRecord(x=80, y=28, reserved=0),), items=(1, 7, 9, 9)), + PotRoomDataRecord(room_id=132, pots=(PotDataRecord(x=64, y=17, reserved=0), PotDataRecord(x=60, y=17, reserved=0), PotDataRecord(x=80, y=14, reserved=0), PotDataRecord(x=44, y=14, reserved=0), PotDataRecord(x=100, y=6, reserved=0), PotDataRecord(x=24, y=6, reserved=0), PotDataRecord(x=24, y=7, reserved=0), PotDataRecord(x=100, y=7, reserved=0),), items=(9, 9)), + PotRoomDataRecord(room_id=135, pots=(PotDataRecord(x=12, y=11, reserved=0), PotDataRecord(x=76, y=20, reserved=0), PotDataRecord(x=112, y=20, reserved=0), PotDataRecord(x=16, y=12, reserved=0), PotDataRecord(x=40, y=12, reserved=0), PotDataRecord(x=32, y=12, reserved=0), PotDataRecord(x=24, y=12, reserved=0), PotDataRecord(x=16, y=11, reserved=0),), items=(12, 13)), + PotRoomDataRecord(room_id=139, pots=(PotDataRecord(x=76, y=20, reserved=0), PotDataRecord(x=76, y=12, reserved=1), PotDataRecord(x=32, y=23, reserved=1), PotDataRecord(x=28, y=23, reserved=1), PotDataRecord(x=112, y=12, reserved=1), PotDataRecord(x=32, y=9, reserved=1), PotDataRecord(x=76, y=28, reserved=0),), items=(8, 11, 12)), + PotRoomDataRecord(room_id=140, pots=(PotDataRecord(x=76, y=12, reserved=2), PotDataRecord(x=112, y=12, reserved=2), PotDataRecord(x=76, y=20, reserved=0), PotDataRecord(x=92, y=20, reserved=0), PotDataRecord(x=100, y=21, reserved=0), PotDataRecord(x=104, y=26, reserved=0), PotDataRecord(x=88, y=27, reserved=0),), items=(9, 10, 10, 10, 10, 12, 136)), + PotRoomDataRecord(room_id=145, pots=(PotDataRecord(x=84, y=4, reserved=0), PotDataRecord(x=104, y=4, reserved=0),), items=(11, 12)), + PotRoomDataRecord(room_id=150, pots=(PotDataRecord(x=14, y=18, reserved=0), PotDataRecord(x=32, y=5, reserved=0), PotDataRecord(x=32, y=17, reserved=0), PotDataRecord(x=32, y=24, reserved=0), PotDataRecord(x=76, y=21, reserved=0), PotDataRecord(x=112, y=21, reserved=0), PotDataRecord(x=14, y=24, reserved=0),), items=(11, 12, 12, 13)), + PotRoomDataRecord(room_id=155, pots=(PotDataRecord(x=48, y=4, reserved=0), PotDataRecord(x=48, y=12, reserved=0),), items=(8, 12)), + PotRoomDataRecord(room_id=157, pots=(PotDataRecord(x=32, y=7, reserved=0), PotDataRecord(x=40, y=9, reserved=0), PotDataRecord(x=76, y=4, reserved=0), PotDataRecord(x=84, y=4, reserved=0),), items=(10, 12)), + PotRoomDataRecord(room_id=159, pots=(PotDataRecord(x=138, y=20, reserved=0), PotDataRecord(x=138, y=19, reserved=0), PotDataRecord(x=178, y=19, reserved=0), PotDataRecord(x=40, y=21, reserved=0), PotDataRecord(x=138, y=21, reserved=0), PotDataRecord(x=20, y=27, reserved=0), PotDataRecord(x=138, y=27, reserved=0), PotDataRecord(x=178, y=28, reserved=0), PotDataRecord(x=178, y=21, reserved=0), PotDataRecord(x=178, y=20, reserved=0), PotDataRecord(x=40, y=27, reserved=0), PotDataRecord(x=178, y=27, reserved=0), PotDataRecord(x=178, y=26, reserved=0), PotDataRecord(x=138, y=28, reserved=0), PotDataRecord(x=138, y=26, reserved=0), PotDataRecord(x=20, y=21, reserved=0),), items=(8, 11, 11, 11, 11, 11, 136)), + PotRoomDataRecord(room_id=161, pots=(PotDataRecord(x=96, y=27, reserved=0), PotDataRecord(x=92, y=21, reserved=0), PotDataRecord(x=150, y=6, reserved=0), PotDataRecord(x=100, y=11, reserved=0), PotDataRecord(x=104, y=12, reserved=0), PotDataRecord(x=108, y=13, reserved=0), PotDataRecord(x=112, y=14, reserved=0), PotDataRecord(x=96, y=23, reserved=0), PotDataRecord(x=76, y=28, reserved=0), PotDataRecord(x=112, y=28, reserved=0),), items=(8, 11, 11, 11, 12, 12)), + PotRoomDataRecord(room_id=168, pots=(PotDataRecord(x=138, y=28, reserved=0), PotDataRecord(x=178, y=28, reserved=0), PotDataRecord(x=178, y=19, reserved=0), PotDataRecord(x=138, y=19, reserved=0), PotDataRecord(x=30, y=24, reserved=0),), items=(1, 11)), + PotRoomDataRecord(room_id=169, pots=(PotDataRecord(x=12, y=19, reserved=0), PotDataRecord(x=112, y=19, reserved=0), PotDataRecord(x=144, y=43, reserved=0), PotDataRecord(x=236, y=43, reserved=0), PotDataRecord(x=144, y=44, reserved=0), PotDataRecord(x=236, y=44, reserved=0), PotDataRecord(x=16, y=20, reserved=0), PotDataRecord(x=108, y=20, reserved=0),), items=(11, 11, 11, 9, 9, 9)), + PotRoomDataRecord(room_id=170, pots=(PotDataRecord(x=212, y=10, reserved=2), PotDataRecord(x=232, y=10, reserved=2), PotDataRecord(x=232, y=5, reserved=2), PotDataRecord(x=212, y=5, reserved=2), PotDataRecord(x=94, y=8, reserved=2), PotDataRecord(x=108, y=55, reserved=0), PotDataRecord(x=108, y=56, reserved=0), PotDataRecord(x=108, y=57, reserved=0),), items=(11, 11, 11, 11, 136)), + PotRoomDataRecord(room_id=176, pots=(PotDataRecord(x=20, y=27, reserved=0), PotDataRecord(x=24, y=24, reserved=0), PotDataRecord(x=44, y=25, reserved=0), PotDataRecord(x=20, y=21, reserved=0), PotDataRecord(x=28, y=21, reserved=0), PotDataRecord(x=32, y=21, reserved=0), PotDataRecord(x=40, y=21, reserved=0), PotDataRecord(x=16, y=23, reserved=0), PotDataRecord(x=44, y=23, reserved=0), PotDataRecord(x=36, y=24, reserved=0), PotDataRecord(x=16, y=25, reserved=0), PotDataRecord(x=28, y=27, reserved=0), PotDataRecord(x=40, y=27, reserved=0), PotDataRecord(x=32, y=27, reserved=0),), items=(1, 1, 7, 7, 9, 9, 10, 10, 11, 11)), + PotRoomDataRecord(room_id=179, pots=(PotDataRecord(x=12, y=20, reserved=0), PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(8, 12, 136)), + PotRoomDataRecord(room_id=180, pots=(PotDataRecord(x=44, y=28, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(11, 13)), + PotRoomDataRecord(room_id=181, pots=(PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=112, y=15, reserved=0), PotDataRecord(x=76, y=16, reserved=0), PotDataRecord(x=112, y=16, reserved=0), PotDataRecord(x=112, y=17, reserved=0), PotDataRecord(x=112, y=28, reserved=0),), items=(7, 10, 11, 11, 13, 136)), + PotRoomDataRecord(room_id=184, pots=(PotDataRecord(x=96, y=13, reserved=0), PotDataRecord(x=88, y=16, reserved=0), PotDataRecord(x=104, y=16, reserved=0),), items=(11, 11, 136)), + PotRoomDataRecord(room_id=185, pots=(PotDataRecord(x=92, y=18, reserved=0), PotDataRecord(x=96, y=18, reserved=0), PotDataRecord(x=104, y=18, reserved=0), PotDataRecord(x=108, y=18, reserved=0),), items=(1, 1, 7, 7)), + PotRoomDataRecord(room_id=186, pots=(PotDataRecord(x=100, y=8, reserved=0), PotDataRecord(x=88, y=8, reserved=0), PotDataRecord(x=94, y=4, reserved=0), PotDataRecord(x=76, y=6, reserved=0), PotDataRecord(x=112, y=6, reserved=0), PotDataRecord(x=76, y=10, reserved=0), PotDataRecord(x=112, y=10, reserved=0), PotDataRecord(x=94, y=12, reserved=0),), items=(1, 1, 8, 11, 11, 12)), + PotRoomDataRecord(room_id=188, pots=(PotDataRecord(x=138, y=3, reserved=2), PotDataRecord(x=178, y=3, reserved=2), PotDataRecord(x=86, y=4, reserved=1), PotDataRecord(x=102, y=4, reserved=1), PotDataRecord(x=138, y=12, reserved=2), PotDataRecord(x=178, y=12, reserved=2), PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=28, y=21, reserved=0), PotDataRecord(x=32, y=21, reserved=0), PotDataRecord(x=28, y=27, reserved=0), PotDataRecord(x=32, y=27, reserved=0), PotDataRecord(x=12, y=28, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(7, 7, 7, 7, 8, 10, 10, 10, 10, 10, 11, 11, 136)), + PotRoomDataRecord(room_id=191, pots=(PotDataRecord(x=40, y=20, reserved=0), PotDataRecord(x=44, y=20, reserved=0), PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=40, y=28, reserved=0), PotDataRecord(x=44, y=28, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(9, 10, 11, 12, 12, 12)), + PotRoomDataRecord(room_id=192, pots=(PotDataRecord(x=48, y=10, reserved=0), PotDataRecord(x=12, y=14, reserved=0), PotDataRecord(x=12, y=26, reserved=0), PotDataRecord(x=28, y=27, reserved=0),), items=(1, 7, 10, 11)), + PotRoomDataRecord(room_id=194, pots=(PotDataRecord(x=180, y=7, reserved=0), PotDataRecord(x=100, y=46, reserved=0), PotDataRecord(x=68, y=48, reserved=0), PotDataRecord(x=64, y=52, reserved=0),), items=(1, 9, 12, 136)), + PotRoomDataRecord(room_id=196, pots=(PotDataRecord(x=84, y=9, reserved=0), PotDataRecord(x=24, y=14, reserved=0), PotDataRecord(x=56, y=17, reserved=0), PotDataRecord(x=84, y=17, reserved=0), PotDataRecord(x=12, y=21, reserved=0), PotDataRecord(x=76, y=23, reserved=0), PotDataRecord(x=48, y=25, reserved=0), PotDataRecord(x=12, y=26, reserved=0),), items=(1, 9, 12, 7, 10, 10, 11, 11)), + PotRoomDataRecord(room_id=199, pots=(PotDataRecord(x=12, y=10, reserved=0), PotDataRecord(x=12, y=11, reserved=0), PotDataRecord(x=12, y=22, reserved=0), PotDataRecord(x=12, y=28, reserved=0),), items=(9, 12, 11, 13)), + PotRoomDataRecord(room_id=201, pots=(PotDataRecord(x=30, y=22, reserved=0), PotDataRecord(x=94, y=22, reserved=0), PotDataRecord(x=60, y=22, reserved=0),), items=(1, 1, 136)), + PotRoomDataRecord(room_id=203, pots=(PotDataRecord(x=88, y=16, reserved=0), PotDataRecord(x=88, y=28, reserved=0),), items=(7, 11)), + PotRoomDataRecord(room_id=204, pots=(PotDataRecord(x=36, y=4, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=36, y=28, reserved=0), PotDataRecord(x=112, y=28, reserved=0),), items=(7, 11, 7, 10)), + PotRoomDataRecord(room_id=206, pots=(PotDataRecord(x=76, y=8, reserved=0), PotDataRecord(x=80, y=8, reserved=0), PotDataRecord(x=108, y=12, reserved=0), PotDataRecord(x=112, y=12, reserved=0), PotDataRecord(x=204, y=11, reserved=3),), items=(9, 12, 12, 10, 128)), + PotRoomDataRecord(room_id=208, pots=(PotDataRecord(x=158, y=5, reserved=0), PotDataRecord(x=140, y=11, reserved=0), PotDataRecord(x=42, y=13, reserved=0), PotDataRecord(x=48, y=16, reserved=0), PotDataRecord(x=176, y=20, reserved=0), PotDataRecord(x=146, y=23, reserved=0), PotDataRecord(x=12, y=28, reserved=0),), items=(1, 1, 7, 11, 11, 12, 12)), + PotRoomDataRecord(room_id=209, pots=(PotDataRecord(x=76, y=12, reserved=0), PotDataRecord(x=48, y=4, reserved=0), PotDataRecord(x=76, y=4, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=168, y=7, reserved=0), PotDataRecord(x=112, y=12, reserved=0),), items=(9, 1, 1, 1, 13)), + PotRoomDataRecord(room_id=214, pots=(PotDataRecord(x=92, y=22, reserved=0), PotDataRecord(x=96, y=22, reserved=0),), items=(10, 13)), + PotRoomDataRecord(room_id=216, pots=(PotDataRecord(x=202, y=8, reserved=0), PotDataRecord(x=242, y=8, reserved=0), PotDataRecord(x=202, y=10, reserved=0), PotDataRecord(x=242, y=10, reserved=0), PotDataRecord(x=202, y=12, reserved=0), PotDataRecord(x=242, y=12, reserved=0), PotDataRecord(x=92, y=24, reserved=0), PotDataRecord(x=96, y=24, reserved=0),), items=(9, 9, 9, 9, 9, 11, 11, 11)), + PotRoomDataRecord(room_id=218, pots=(PotDataRecord(x=24, y=23, reserved=0), PotDataRecord(x=36, y=23, reserved=0), PotDataRecord(x=24, y=25, reserved=0), PotDataRecord(x=36, y=25, reserved=0),), items=(9, 9, 11, 136)), + PotRoomDataRecord(room_id=219, pots=(PotDataRecord(x=12, y=4, reserved=0), PotDataRecord(x=62, y=19, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=88, y=16, reserved=0),), items=(7, 11)), + PotRoomDataRecord(room_id=220, pots=(PotDataRecord(x=56, y=4, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=68, y=16, reserved=0), PotDataRecord(x=12, y=28, reserved=0),), items=(7, 9, 10, 11)), + PotRoomDataRecord(room_id=235, pots=(PotDataRecord(x=206, y=8, reserved=0), PotDataRecord(x=210, y=8, reserved=0), PotDataRecord(x=88, y=14, reserved=0), PotDataRecord(x=92, y=14, reserved=0), PotDataRecord(x=96, y=14, reserved=0),), items=(7, 7, 11, 12, 12)), +) diff --git a/worlds/alttp/enemizer_data/symbols.py b/worlds/alttp/enemizer_data/symbols.py new file mode 100644 index 000000000000..903d2ae7d5ed --- /dev/null +++ b/worlds/alttp/enemizer_data/symbols.py @@ -0,0 +1,171 @@ +from __future__ import annotations + +ENEMIZER_SYMBOLS = { + ':pos_1_0': 0x36975E, + ':pos_1_1': 0x36976D, + ':pos_1_10': 0x369819, + ':pos_1_11': 0x369828, + ':pos_1_12': 0x369837, + ':pos_1_13': 0x369845, + ':pos_1_14': 0x36984B, + ':pos_1_15': 0x369851, + ':pos_1_16': 0x369873, + ':pos_1_17': 0x369898, + ':pos_1_18': 0x36989E, + ':pos_1_19': 0x3698A4, + ':pos_1_2': 0x369787, + ':pos_1_20': 0x3698C6, + ':pos_1_21': 0x3698EB, + ':pos_1_22': 0x3698F1, + ':pos_1_23': 0x3698F7, + ':pos_1_24': 0x369919, + ':pos_1_25': 0x36993E, + ':pos_1_26': 0x369944, + ':pos_1_27': 0x36994A, + ':pos_1_28': 0x36996C, + ':pos_1_29': 0x369AA5, + ':pos_1_3': 0x369796, + ':pos_1_30': 0x36B796, + ':pos_1_4': 0x3697A5, + ':pos_1_5': 0x3697B4, + ':pos_1_6': 0x3697D5, + ':pos_1_7': 0x3697E8, + ':pos_1_8': 0x3697F7, + ':pos_1_9': 0x369806, + 'CheckIfLinkShouldDie': 0x3699B2, + 'CheckIfLinkShouldDie_dead': 0x3699BB, + 'CheckIfLinkShouldDie_done': 0x3699BD, + 'Check_for_Blind_Fight': 0x1DA085, + 'CopyShield': 0x36B713, + 'CopyShield_loop_copy': 0x36B729, + 'CopyShield_shield_positon_gfx': 0x36B74B, + 'CopySword': 0x36B6D1, + 'CopySword_loop_copy': 0x36B6E7, + 'CopySword_sword_positon_gfx': 0x36B709, + 'DMAKholdstare': 0x3695A5, + 'DMATrinexx': 0x369617, + 'Dungeon_ResetSprites': 0x09C114, + 'EnemizerCodeStart': 0x3694F5, + 'EnemizerFlags': 0x368100, + 'EnemizerFlags_agahnim_fun_balls': 0x368104, + 'EnemizerFlags_close_blind_door': 0x368101, + 'EnemizerFlags_enable_mimic_override': 0x368105, + 'EnemizerFlags_enable_terrorpin_ai_fix': 0x368106, + 'EnemizerFlags_moldorm_eye_count': 0x368102, + 'EnemizerFlags_randomize_bushes': 0x368100, + 'EnemizerFlags_randomize_sprites': 0x368103, + 'EnemizerTablesStart': 0x368000, + 'Ext_OnBossDeath': 0x29803C, + 'Ext_OnDungeonCompleted': 0x29804B, + 'Ext_OnDungeonEnter': 0x298041, + 'Ext_OnDungeonExit': 0x298046, + 'Ext_OnFairyRevive': 0x298019, + 'Ext_OnFileCreate': 0x298000, + 'Ext_OnFileLoad': 0x298005, + 'Ext_OnFileSave': 0x29800A, + 'Ext_OnIemMenuOpen': 0x298023, + 'Ext_OnItemChange': 0x29802D, + 'Ext_OnItemMenuClose': 0x298028, + 'Ext_OnMapUse': 0x298014, + 'Ext_OnPlayerAttack': 0x298037, + 'Ext_OnPlayerDamaged': 0x298032, + 'Ext_OnPlayerDeath': 0x29800F, + 'Ext_OnYItemUse': 0x29801E, + 'Ext_OnZeldaRescued': 0x298050, + 'FixTerrorpin': 0x36971A, + 'FixTerrorpin_new': 0x369728, + 'GFX_Kholdstare_Shell': 0x36C79A, + 'GFX_Trinexx_Shell': 0x36D79A, + 'GFX_Trinexx_Shell2': 0x36DF9A, + 'GetRandomInt': 0x0DBA71, + 'Initialize_Blind_Fight': 0x1DA090, + 'Kholdstare_Draw': 0x0DD97F, + 'LoadFile': 0x369A87, + 'LoadNewSoundFx': 0x369A9E, + 'LoadOverworldSprites': 0x36B753, + 'Module_LoadFile_indoors': 0x028118, + 'Moldorm_UpdateOamPosition': 0x3699E7, + 'Moldorm_UpdateOamPosition_more_eyes': 0x3699ED, + 'NMIHookAction': 0x36956C, + 'NMIHookAction_loadKholdstare': 0x369584, + 'NMIHookAction_loadTrinexx': 0x369590, + 'NMIHookAction_return': 0x36959A, + 'NMIHookReturn': 0x0080D5, + 'NewLoadSoundBank': 0x369A98, + 'NewLoadSoundBank_Intro': 0x369A92, + 'OnInitFileSelect': 0x3696FA, + 'OnInitFileSelect_continue': 0x36970F, + 'Player_Main': 0x078000, + 'Sound_LoadSongBank': 0x008888, + 'Sound_SetSfx3PanLong': 0x0DBB8A, + 'Sound_SetSfxPanWithPlayerCoords': 0x0DBB67, + 'Spawn_Bees': 0x36B75D, + 'Spawn_Bees_done': 0x36B779, + 'SpritePrep_Eyegore': 0x1EC6FA, + 'SpritePrep_EyegoreNew': 0x369A1A, + 'SpritePrep_EyegoreNew_mimic': 0x369A31, + 'SpritePrep_EyegoreNew_new': 0x369A25, + 'Sprite_ResetAll': 0x09C44E, + 'Sprite_SpawnDynamically': 0x1DF65D, + 'VitreousKeyReset': 0x36B77A, + 'boss_move': 0x369743, + 'boss_move_loop_bottom_left': 0x369935, + 'boss_move_loop_bottom_left2': 0x369963, + 'boss_move_loop_bottom_right': 0x3698E2, + 'boss_move_loop_bottom_right2': 0x369910, + 'boss_move_loop_middle': 0x36983C, + 'boss_move_loop_middle2': 0x36986A, + 'boss_move_loop_top_right': 0x36988F, + 'boss_move_loop_top_right2': 0x3698BD, + 'boss_move_move_to_bottom_left': 0x369933, + 'boss_move_move_to_bottom_right': 0x3698E0, + 'boss_move_move_to_middle': 0x36983A, + 'boss_move_move_to_top_right': 0x36988D, + 'boss_move_no_blind_door': 0x3697D2, + 'boss_move_no_change': 0x369863, + 'boss_move_no_change2': 0x3698B6, + 'boss_move_no_change3': 0x369909, + 'boss_move_no_change4': 0x36995C, + 'boss_move_no_change_ov': 0x369885, + 'boss_move_no_change_ov2': 0x3698D8, + 'boss_move_no_change_ov3': 0x36992B, + 'boss_move_no_change_ov4': 0x36997E, + 'boss_move_return': 0x369986, + 'change_heartcontainer_position': 0x3699BE, + 'change_heartcontainer_position_not_moldorm_room': 0x3699E1, + 'check_blind_boss_room': 0x36B782, + 'check_special_action': 0x369731, + 'check_special_action_no_special_action': 0x36973E, + 'enemizer_info_table': 0x368000, + 'linkIsDead': 0x0780D5, + 'linkNotDead': 0x0780F7, + 'modified_room_object_table': 0x36B79A, + 'moved_room_header_bank_value_address': 0x368374, + 'newKodongoCollision': 0x369A02, + 'newKodongoCollision_continue': 0x369A19, + 'new_kholdstare_code': 0x369987, + 'new_kholdstare_code_already_iced': 0x369997, + 'new_trinexx_code': 0x36999C, + 'new_trinexx_code_already_rocked': 0x3699AC, + 'notItemSprite_Mimic': 0x369A66, + 'notItemSprite_Mimic_changeSpriteId': 0x369A7A, + 'notItemSprite_Mimic_continue': 0x369A82, + 'notItemSprite_Mimic_reloadSpriteIdAndSkipMimic': 0x369A7F, + 'resetSprite_Mimic': 0x369A4E, + 'resetSprite_Mimic_notMimic': 0x369A60, + 'room_header_table': 0x368375, + 'shieldgfx': 0x36AAD1, + 'sprite_bush_spawn': 0x3694F5, + 'sprite_bush_spawn_continue': 0x36950F, + 'sprite_bush_spawn_dontGoPhase2': 0x369565, + 'sprite_bush_spawn_item_table': 0x369525, + 'sprite_bush_spawn_newSpriteSpawn': 0x36954C, + 'sprite_bush_spawn_not_random': 0x36953B, + 'sprite_bush_spawn_not_random_old': 0x36950B, + 'sprite_bush_spawn_return': 0x369568, + 'sprite_bush_spawn_table': 0x368120, + 'sprite_bush_spawn_table_dungeons': 0x368248, + 'sprite_bush_spawn_table_overworld': 0x368120, + 'sprite_bush_spawn_table_random_sprites': 0x368370, + 'swordgfx': 0x369AD1, +} diff --git a/worlds/alttp/test/TestEnemizerPatches.py b/worlds/alttp/test/TestEnemizerPatches.py new file mode 100644 index 000000000000..4aa09bb62e2a --- /dev/null +++ b/worlds/alttp/test/TestEnemizerPatches.py @@ -0,0 +1,308 @@ +import unittest +from types import SimpleNamespace + +from worlds.alttp.EnemizerPatches import ( + ARROW_REFILL_5_SPRITE_ID, + BOSS_GFX_SHEET_INDEXES, + BOSS_PATCH_DATA, + DAMAGE_GROUP_TABLE_ADDRESS, + DUNGEON_BOSS_PATCH_DATA, + ENEMY_DAMAGE_TABLE_ADDRESS, + ENEMY_HP_TABLE_ADDRESS, + EXCLUDED_ENEMY_TABLE_SPRITE_IDS, + HIDDEN_ENEMY_CHANCE_POOL_ADDRESS, + RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL, + RETRO_ARROW_REPLACEMENT_CHECK_ADDRESS, + RETRO_RUPEE_REPLACEMENT_SPRITE_ID, + THIEF_DEFAULT_HP, + THIEF_SPRITE_ID, + TILE_TRAP_FLOOR_TILE_ADDRESS, + TRINEXX_ICE_FLOOR_ROUTINE_ADDRESS, + TRINEXX_ICE_PROJECTILE_TILE_ADDRESS, + VANILLA_HIDDEN_ENEMY_CHANCE_POOL, + _apply_killable_thief, + _apply_randomized_tile_trap_floor_tile, + _get_enemizer_symbol, + _make_native_enemizer_rng, + _option_key, + patch_bosses, + _randomize_enemy_damage, + _randomize_enemy_health, + _set_enemizer_flag, + _shuffle_damage_groups, + _update_hidden_enemy_item_table_for_retro_mode, + apply_enemizer_base_patch, +) + + +class FakeRom: + def __init__(self, size: int = 0x400000) -> None: + self.buffer = bytearray(size) + + def read_byte(self, address: int) -> int: + return self.buffer[address] + + def read_bytes(self, startaddress: int, length: int) -> bytearray: + return self.buffer[startaddress:startaddress + length] + + def write_byte(self, address: int, value: int) -> None: + self.buffer[address] = value + + def write_bytes(self, startaddress: int, values) -> None: + self.buffer[startaddress:startaddress + len(values)] = values + + def write_int16(self, address: int, value: int) -> None: + self.write_bytes(address, (value & 0xFF, (value >> 8) & 0xFF)) + + +class TestEnemizerPatches(unittest.TestCase): + def test_enemizer_base_patch_applies_mimic_hooks(self) -> None: + rom = FakeRom() + + apply_enemizer_base_patch(rom) + + self.assertEqual(tuple(rom.read_bytes(0x307CB, 2)), (0xB6, 0x91)) + self.assertEqual(tuple(rom.read_bytes(0x311B6, 4)), (0x22, 0x1A, 0x9A, 0x36)) + self.assertEqual(tuple(rom.read_bytes(0x36C08, 5)), (0x22, 0x4E, 0x9A, 0x36, 0xEA)) + self.assertEqual(tuple(rom.read_bytes(0x36DA6, 4)), (0x22, 0x66, 0x9A, 0x36)) + self.assertEqual(tuple(rom.read_bytes(0xF0BB1, 2)), (0x95, 0xC7)) + self.assertEqual(tuple(rom.read_bytes(TRINEXX_ICE_FLOOR_ROUTINE_ADDRESS, 4)), (0xEA, 0xEA, 0xEA, 0xEA)) + self.assertEqual(tuple(rom.read_bytes(TRINEXX_ICE_PROJECTILE_TILE_ADDRESS, 2)), (0x00, 0x00)) + self.assertEqual(rom.read_byte(TILE_TRAP_FLOOR_TILE_ADDRESS), 0x00) + + def test_randomized_tile_trap_floor_tile_patch_is_separate(self) -> None: + rom = FakeRom() + + _apply_randomized_tile_trap_floor_tile(rom) + + self.assertEqual(tuple(rom.read_bytes(TRINEXX_ICE_PROJECTILE_TILE_ADDRESS, 2)), (0x88, 0x01)) + self.assertEqual(rom.read_byte(TILE_TRAP_FLOOR_TILE_ADDRESS), 0x12) + + def test_enemy_shuffle_enables_hidden_enemy_and_mimic_support(self) -> None: + rom = FakeRom() + world = self._build_world(enemy_shuffle=True, bush_shuffle=False) + + self._apply_native_enemizer_features(world, rom) + + self.assertEqual( + tuple(rom.read_bytes(HIDDEN_ENEMY_CHANCE_POOL_ADDRESS, len(VANILLA_HIDDEN_ENEMY_CHANCE_POOL))), + VANILLA_HIDDEN_ENEMY_CHANCE_POOL, + ) + self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_randomize_bushes")), 0x01) + self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_randomize_sprites")), 0x01) + self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_enable_mimic_override")), 0x01) + self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_enable_terrorpin_ai_fix")), 0x01) + self.assertEqual(tuple(rom.read_bytes(0x1F2D5, 2)), (0x54, 0x9C)) + self.assertEqual(rom.read_byte(0x1F2E5), 0xB0) + self.assertEqual(rom.read_byte(0x1F2EB), 0xD0) + + def test_bush_shuffle_and_remaining_tables_are_patched_natively(self) -> None: + rom = FakeRom() + item_table_address = _get_enemizer_symbol("sprite_bush_spawn_item_table") + not_item_sprite_address = _get_enemizer_symbol("notItemSprite_Mimic") + rom.write_byte(RETRO_ARROW_REPLACEMENT_CHECK_ADDRESS, RETRO_RUPEE_REPLACEMENT_SPRITE_ID) + rom.write_byte(item_table_address + 5, ARROW_REFILL_5_SPRITE_ID) + rom.write_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID, 0x08) + + included_hp_sprite_id = 0x01 + included_damage_sprite_id = 0x02 + excluded_sprite_id = min(EXCLUDED_ENEMY_TABLE_SPRITE_IDS) + rom.write_byte(ENEMY_HP_TABLE_ADDRESS + included_hp_sprite_id, 0x06) + rom.write_byte(ENEMY_HP_TABLE_ADDRESS + excluded_sprite_id, 0x07) + rom.write_byte(ENEMY_DAMAGE_TABLE_ADDRESS + included_damage_sprite_id, 0x06) + rom.write_byte(ENEMY_DAMAGE_TABLE_ADDRESS + excluded_sprite_id, 0x05) + + world = self._build_world( + bush_shuffle=True, + killable_thieves=True, + enemy_health="hard", + enemy_damage="chaos", + ) + + self._apply_native_enemizer_features(world, rom) + + self.assertEqual( + tuple(rom.read_bytes(HIDDEN_ENEMY_CHANCE_POOL_ADDRESS, len(RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL))), + RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL, + ) + self.assertEqual(rom.read_byte(item_table_address + 5), RETRO_RUPEE_REPLACEMENT_SPRITE_ID) + self.assertEqual(rom.read_byte(not_item_sprite_address + 4), THIEF_SPRITE_ID) + self.assertNotEqual(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID), 0x08) + self.assertGreaterEqual(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID), 2) + self.assertLess(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID), 25) + self.assertGreaterEqual(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + included_hp_sprite_id), 2) + self.assertLess(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + included_hp_sprite_id), 25) + self.assertEqual(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + excluded_sprite_id), 0x07) + self.assertIn(rom.read_byte(ENEMY_DAMAGE_TABLE_ADDRESS + included_damage_sprite_id), range(8)) + self.assertEqual(rom.read_byte(ENEMY_DAMAGE_TABLE_ADDRESS + excluded_sprite_id), 0x05) + for group_id in range(10): + group_address = DAMAGE_GROUP_TABLE_ADDRESS + (group_id * 3) + green_mail, blue_mail, red_mail = rom.read_bytes(group_address, 3) + self.assertIn(green_mail, range(64)) + self.assertIn(blue_mail, range(64)) + self.assertIn(red_mail, range(64)) + + def test_killable_thief_sets_default_hp_without_enemy_health_shuffle(self) -> None: + rom = FakeRom() + rom.write_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID, 0x08) + + world = self._build_world(killable_thieves=True) + + self._apply_native_enemizer_features(world, rom) + + self.assertEqual(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID), THIEF_DEFAULT_HP) + + def test_bush_shuffle_without_enemy_shuffle_does_not_enable_sprite_randomization_flags(self) -> None: + rom = FakeRom() + + self._apply_native_enemizer_features(self._build_world(bush_shuffle=True), rom) + + self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_randomize_bushes")), 0x01) + self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_randomize_sprites")), 0x00) + self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_enable_mimic_override")), 0x00) + self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_enable_terrorpin_ai_fix")), 0x00) + self.assertEqual(tuple(rom.read_bytes(0x1F2D5, 2)), (0x00, 0x00)) + self.assertEqual(rom.read_byte(0x1F2E5), 0x00) + self.assertEqual(rom.read_byte(0x1F2EB), 0x00) + + def test_non_chaos_enemy_damage_uses_expected_mail_scaling(self) -> None: + rom = FakeRom() + + self._apply_native_enemizer_features(self._build_world(enemy_damage="hard"), rom) + + for group_id in range(10): + group_address = DAMAGE_GROUP_TABLE_ADDRESS + (group_id * 3) + green_mail, blue_mail, red_mail = rom.read_bytes(group_address, 3) + self.assertEqual(blue_mail, green_mail * 3 // 4) + self.assertEqual(red_mail, green_mail * 3 // 8) + + def test_patch_bosses_overwrites_enemy_shuffle_boss_room_graphics(self) -> None: + rom = FakeRom() + dungeon_header_base = _get_enemizer_symbol("room_header_table") + eastern_dungeon_data = DUNGEON_BOSS_PATCH_DATA[("Eastern Palace", None)] + rom.write_byte(dungeon_header_base + (eastern_dungeon_data.room_id * 14) + 3, BOSS_PATCH_DATA["Armos"].graphics) + + for table_index in BOSS_GFX_SHEET_INDEXES.values(): + rom.write_byte(0x4FC0 + table_index, 0xAA) + rom.write_byte(0x509F + table_index, 0xBB) + rom.write_byte(0x517E + table_index, 0xCC) + + patch_bosses(self._build_boss_world({"Eastern Palace": "Vitreous"}), rom) + + eastern_boss_data = BOSS_PATCH_DATA["Vitreous"] + self.assertEqual( + tuple(rom.read_bytes(eastern_dungeon_data.sprite_pointer_address, 2)), + eastern_boss_data.pointer, + ) + self.assertEqual( + rom.read_byte(dungeon_header_base + (eastern_dungeon_data.room_id * 14) + 3), + eastern_boss_data.graphics, + ) + + for table_index in BOSS_GFX_SHEET_INDEXES.values(): + self.assertEqual(rom.read_byte(0x4FC0 + table_index), 0xAA) + self.assertEqual(rom.read_byte(0x509F + table_index), 0xBB) + self.assertEqual(rom.read_byte(0x517E + table_index), 0xCC) + + def test_native_enemizer_rng_is_deterministic_for_same_world_settings(self) -> None: + world = self._build_world(enemy_health="hard", enemy_damage="chaos", bush_shuffle=True) + + rng_a = _make_native_enemizer_rng(world) + rng_b = _make_native_enemizer_rng(world) + + self.assertEqual([rng_a.randrange(256) for _ in range(8)], [rng_b.randrange(256) for _ in range(8)]) + + @staticmethod + def _apply_native_enemizer_features(world: SimpleNamespace, rom: FakeRom) -> None: + enemy_shuffle_enabled = bool(world.options.enemy_shuffle) + bush_shuffle_enabled = bool(world.options.bush_shuffle) + enemy_health_key = _option_key(world.options.enemy_health) + enemy_damage_key = _option_key(world.options.enemy_damage) + + if enemy_shuffle_enabled or bush_shuffle_enabled: + _set_enemizer_flag(rom, "EnemizerFlags_randomize_bushes", True) + hidden_enemy_chance_pool = ( + RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL if bush_shuffle_enabled else VANILLA_HIDDEN_ENEMY_CHANCE_POOL + ) + rom.write_bytes(HIDDEN_ENEMY_CHANCE_POOL_ADDRESS, hidden_enemy_chance_pool) + _update_hidden_enemy_item_table_for_retro_mode(rom) + + if enemy_shuffle_enabled: + _set_enemizer_flag(rom, "EnemizerFlags_randomize_sprites", True) + _set_enemizer_flag(rom, "EnemizerFlags_enable_mimic_override", True) + _set_enemizer_flag(rom, "EnemizerFlags_enable_terrorpin_ai_fix", True) + rom.write_bytes(0x1F2D5, (0x54, 0x9C)) + rom.write_byte(0x1F2E5, 0xB0) + rom.write_byte(0x1F2EB, 0xD0) + + if world.options.killable_thieves: + _apply_killable_thief(rom) + + if enemy_health_key != "default" or enemy_damage_key != "default": + rng = _make_native_enemizer_rng(world) + else: + rng = None + + if enemy_health_key != "default": + assert rng is not None + _randomize_enemy_health(rom, rng, enemy_health_key) + + if enemy_damage_key != "default": + assert rng is not None + _randomize_enemy_damage(rom, rng, allow_zero_damage=True) + _shuffle_damage_groups(rom, rng, chaos_mode=enemy_damage_key == "chaos", allow_zero_damage=True) + + @staticmethod + def _build_world( + *, + enemy_shuffle: bool = False, + bush_shuffle: bool = False, + killable_thieves: bool = False, + enemy_health: str = "default", + enemy_damage: str = "default", + ) -> SimpleNamespace: + return SimpleNamespace( + player=1, + multiworld=SimpleNamespace(seed=12345, seed_name="native-enemizer-test"), + options=SimpleNamespace( + enemy_shuffle=enemy_shuffle, + bush_shuffle=bush_shuffle, + killable_thieves=killable_thieves, + enemy_health=SimpleNamespace(current_key=enemy_health), + enemy_damage=SimpleNamespace(current_key=enemy_damage), + ), + ) + + @staticmethod + def _build_boss_world(boss_overrides: dict[str, str] | None = None) -> SimpleNamespace: + boss_overrides = boss_overrides or {} + + def boss(name: str) -> SimpleNamespace: + return SimpleNamespace(enemizer_name=name) + + return SimpleNamespace( + options=SimpleNamespace(mode="open"), + dungeons={ + "Eastern Palace": SimpleNamespace(boss=boss(boss_overrides.get("Eastern Palace", "Armos"))), + "Desert Palace": SimpleNamespace(boss=boss(boss_overrides.get("Desert Palace", "Lanmola"))), + "Tower of Hera": SimpleNamespace(boss=boss(boss_overrides.get("Tower of Hera", "Moldorm"))), + "Palace of Darkness": SimpleNamespace(boss=boss(boss_overrides.get("Palace of Darkness", "Helmasaur"))), + "Swamp Palace": SimpleNamespace(boss=boss(boss_overrides.get("Swamp Palace", "Arrghus"))), + "Skull Woods": SimpleNamespace(boss=boss(boss_overrides.get("Skull Woods", "Mothula"))), + "Thieves Town": SimpleNamespace(boss=boss(boss_overrides.get("Thieves Town", "Blind"))), + "Ice Palace": SimpleNamespace(boss=boss(boss_overrides.get("Ice Palace", "Kholdstare"))), + "Misery Mire": SimpleNamespace(boss=boss(boss_overrides.get("Misery Mire", "Vitreous"))), + "Turtle Rock": SimpleNamespace(boss=boss(boss_overrides.get("Turtle Rock", "Trinexx"))), + "Ganons Tower": SimpleNamespace( + bosses={ + "bottom": boss(boss_overrides.get("Ganons Tower Bottom", "Armos")), + "middle": boss(boss_overrides.get("Ganons Tower Middle", "Lanmola")), + "top": boss(boss_overrides.get("Ganons Tower Top", "Moldorm")), + } + ), + }, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/worlds/alttp/test/TestEnemyShuffle.py b/worlds/alttp/test/TestEnemyShuffle.py new file mode 100644 index 000000000000..c16d85b4ebcb --- /dev/null +++ b/worlds/alttp/test/TestEnemyShuffle.py @@ -0,0 +1,834 @@ +import unittest +from types import SimpleNamespace +import random + +from worlds.alttp.EnemyShuffle import ( + DungeonEnemyRoom, + DungeonEnemySprite, + DungeonSpriteGroup, + EnemyShuffleState, + EnemySpriteRequirement, + OverworldEnemyArea, + OverworldEnemySprite, + RandomizedDungeonEnemyRoom, + RandomizedDungeonEnemySprite, + RandomizedOverworldEnemyArea, + RandomizedOverworldEnemySprite, + WALLMASTER_SPRITE_ID, + _load_dungeon_sprite_metadata, + _read_room_sprites, + get_possible_dungeon_sprite_groups, + _get_requirements_for_usable_dungeon_enemies, + _get_requirements_for_usable_overworld_enemies, + _get_randomizable_sprites_in_room, + _apply_selected_boss_group_requirements, + _randomize_overworld_groups, + _randomize_room_sprites, + _setup_required_overworld_groups, + can_spawn_in_room, + validate_enemy_shuffle_state, +) + + +class TestEnemyShuffleValidation(unittest.TestCase): + def test_curated_room_sprite_addresses_exclude_hera_basement_key_slot(self) -> None: + room_id = 135 + sprite_table_address = 0x4E397 + rom_bytes = bytearray(0x4E3C0) + rom_bytes[sprite_table_address] = 0 + room_135_sprite_records = ( + (0x4E398, 0x05, 0x14, 0x18), + (0x4E39B, 0x07, 0x1A, 0x18), + (0x4E39E, 0x0B, 0x13, 0x18), + (0x4E3A1, 0x19, 0x06, 0x18), + (0x4E3A4, 0x08, 0xE7, 0x14), + (0x4E3A7, 0x04, 0x17, 0x1E), + (0x4E3AA, 0x0C, 0x03, 0x1E), + (0x4E3AD, 0x15, 0x04, 0x1E), + (0x4E3B0, 0x17, 0x0B, 0xA7), + (0x4E3B3, 0x18, 0x19, 0xA7), + (0x4E3B6, 0x19, 0x04, 0xA7), + (0x4E3B9, 0x1A, 0x08, 0xE4), + (0x4E3BC, 0x1C, 0x15, 0xA7), + ) + for address, byte_0, byte_1, sprite_id in room_135_sprite_records: + rom_bytes[address] = byte_0 + rom_bytes[address + 1] = byte_1 + rom_bytes[address + 2] = sprite_id + rom_bytes[0x4E3BF] = 0xFF + + sprites = _read_room_sprites(rom_bytes, room_id, sprite_table_address, _load_dungeon_sprite_metadata()) + sprite_addresses = {sprite.address for sprite in sprites} + + self.assertNotIn(0x4E3B9, sprite_addresses) + self.assertIn(0x4E3B6, sprite_addresses) + self.assertFalse(any(sprite.has_key for sprite in sprites)) + + def test_curated_room_sprite_addresses_deduplicate_duplicate_slots(self) -> None: + room_id = 125 + sprite_table_address = 0x4E2CA + metadata = _load_dungeon_sprite_metadata() + max_sprite_id_address = max(metadata["room_sprite_id_addresses"][room_id]) + rom_bytes = bytearray(max_sprite_id_address + 2) + rom_bytes[sprite_table_address] = 0 + for offset, sprite_id_address in enumerate(metadata["room_sprite_id_addresses"][room_id]): + address = sprite_id_address - 2 + sprite_id = 0x80 if offset % 2 == 0 else 0x81 + rom_bytes[address] = 0 + rom_bytes[address + 1] = 0 + rom_bytes[address + 2] = sprite_id + + sprites = _read_room_sprites(rom_bytes, room_id, sprite_table_address, metadata) + sprite_addresses = [sprite.address for sprite in sprites] + + self.assertEqual(len(sprite_addresses), len(set(sprite_addresses))) + + def test_rejects_non_killable_shutter_room(self) -> None: + room = DungeonEnemyRoom( + room_id=1, + room_header_address=0, + sprite_table_address=0, + graphics_block_id=1, + tag_1=0, + tag_2=0, + sort_sprites_value=0, + sprites=( + DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x10, is_overlord=False, has_key=False), + ), + required_group_id=None, + required_subgroup_0=tuple(), + required_subgroup_1=tuple(), + required_subgroup_2=tuple(), + required_subgroup_3=tuple(), + is_shutter_room=True, + is_water_room=False, + do_not_randomize=False, + no_special_enemies_standard=False, + ) + state = self._build_state( + dungeon_rooms={1: room}, + randomized_dungeon_rooms={ + 1: RandomizedDungeonEnemyRoom( + room_id=1, + room_header_address=0, + sprite_table_address=0, + original_graphics_block_id=1, + graphics_block_id=1, + tag_1=0, + tag_2=0, + sort_sprites_value=0, + sprites=( + RandomizedDungeonEnemySprite( + address=0x1000, + byte_0=0, + byte_1=0, + original_sprite_id=0x10, + sprite_id=0x11, + is_overlord=False, + has_key=False, + ), + ), + skipped_randomization=False, + ) + }, + sprite_requirements=( + self._requirement(0x10, killable=True, subgroup_0=(1,)), + self._requirement(0x11, killable=False, subgroup_0=(1,)), + ), + ) + + with self.assertRaises(ValueError): + validate_enemy_shuffle_state(state, is_standard_mode=False) + + def test_rejects_water_enemy_in_non_water_room(self) -> None: + room = DungeonEnemyRoom( + room_id=165, + room_header_address=0, + sprite_table_address=0, + graphics_block_id=1, + tag_1=0, + tag_2=0, + sort_sprites_value=0, + sprites=( + DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=False), + ), + required_group_id=None, + required_subgroup_0=tuple(), + required_subgroup_1=tuple(), + required_subgroup_2=tuple(), + required_subgroup_3=tuple(), + is_shutter_room=True, + is_water_room=False, + do_not_randomize=False, + no_special_enemies_standard=False, + ) + state = self._build_state( + dungeon_rooms={165: room}, + randomized_dungeon_rooms={ + 165: RandomizedDungeonEnemyRoom( + room_id=165, + room_header_address=0, + sprite_table_address=0, + original_graphics_block_id=1, + graphics_block_id=1, + tag_1=0, + tag_2=0, + sort_sprites_value=0, + sprites=( + RandomizedDungeonEnemySprite( + address=0x1000, + byte_0=0, + byte_1=0, + original_sprite_id=0x20, + sprite_id=0x81, + is_overlord=False, + has_key=False, + ), + ), + skipped_randomization=False, + ) + }, + sprite_requirements=( + self._requirement(0x20, killable=True, subgroup_0=(1,)), + self._requirement(0x81, killable=True, subgroup_0=(1,), is_water_sprite=True), + ), + ) + + with self.assertRaisesRegex(ValueError, "water enemy"): + validate_enemy_shuffle_state(state, is_standard_mode=False) + + def test_rejects_multiple_flopping_fish(self) -> None: + area = OverworldEnemyArea( + area_id=0x10, + sprite_table_address=0, + graphics_block_address=0, + graphics_block_id=1, + bush_sprite_id=0x20, + sprites=( + OverworldEnemySprite(address=0x2000, y_coord=0, x_coord=0, sprite_id=0x20), + OverworldEnemySprite(address=0x2003, y_coord=0, x_coord=0, sprite_id=0x21), + ), + do_not_randomize=False, + ) + state = self._build_state( + overworld_areas={0x10: area}, + randomized_overworld_areas={ + 0x10: RandomizedOverworldEnemyArea( + area_id=0x10, + sprite_table_address=0, + graphics_block_address=0, + original_graphics_block_id=1, + graphics_block_id=1, + original_bush_sprite_id=0x20, + bush_sprite_id=0xD2, + sprites=( + RandomizedOverworldEnemySprite( + address=0x2000, + y_coord=0, + x_coord=0, + original_sprite_id=0x20, + sprite_id=0xD2, + ), + RandomizedOverworldEnemySprite( + address=0x2003, + y_coord=0, + x_coord=0, + original_sprite_id=0x21, + sprite_id=0xD2, + ), + ), + skipped_randomization=False, + ) + }, + sprite_requirements=( + self._requirement(0x20, group_ids=(1,)), + self._requirement(0x21, group_ids=(1,)), + self._requirement(0x22, group_ids=(1,)), + self._requirement(0xD2, group_ids=(1,)), + ), + ) + + with self.assertRaises(ValueError): + validate_enemy_shuffle_state(state, is_standard_mode=False) + + def test_allows_multiple_flopping_fish_when_no_other_sprite_is_possible(self) -> None: + area = OverworldEnemyArea( + area_id=0x10, + sprite_table_address=0, + graphics_block_address=0, + graphics_block_id=1, + bush_sprite_id=0x20, + sprites=( + OverworldEnemySprite(address=0x2000, y_coord=0, x_coord=0, sprite_id=0x20), + OverworldEnemySprite(address=0x2003, y_coord=0, x_coord=0, sprite_id=0x21), + ), + do_not_randomize=False, + ) + state = self._build_state( + overworld_areas={0x10: area}, + randomized_overworld_areas={ + 0x10: RandomizedOverworldEnemyArea( + area_id=0x10, + sprite_table_address=0, + graphics_block_address=0, + original_graphics_block_id=1, + graphics_block_id=1, + original_bush_sprite_id=0x20, + bush_sprite_id=0xD2, + sprites=( + RandomizedOverworldEnemySprite( + address=0x2000, + y_coord=0, + x_coord=0, + original_sprite_id=0x20, + sprite_id=0xD2, + ), + RandomizedOverworldEnemySprite( + address=0x2003, + y_coord=0, + x_coord=0, + original_sprite_id=0x21, + sprite_id=0xD2, + ), + ), + skipped_randomization=False, + ) + }, + sprite_requirements=( + self._requirement(0x20, group_ids=(2,)), + self._requirement(0x21, group_ids=(2,)), + self._requirement(0xD2, group_ids=(1,)), + ), + ) + + validate_enemy_shuffle_state(state, is_standard_mode=False) + + def test_excludes_absorbables_from_usable_enemy_pools(self) -> None: + state = self._build_state( + sprite_requirements=( + self._requirement(0x10, subgroup_0=(1,)), + self._requirement(0xE3, subgroup_0=(1,), absorbable=True), + self._requirement(0x20, subgroup_0=(1,), never_use_dungeon=True), + self._requirement(0x21, subgroup_0=(1,), never_use_overworld=True), + ), + ) + + self.assertEqual( + [requirement.sprite_id for requirement in _get_requirements_for_usable_dungeon_enemies(state)], + [0x10, 0x21], + ) + self.assertEqual( + [requirement.sprite_id for requirement in _get_requirements_for_usable_overworld_enemies(state)], + [0x10, 0x20], + ) + + def test_key_enemy_replacements_exclude_moblins(self) -> None: + room = DungeonEnemyRoom( + room_id=1, + room_header_address=0, + sprite_table_address=0, + graphics_block_id=1, + tag_1=0, + tag_2=0, + sort_sprites_value=0, + sprites=( + DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x12, is_overlord=False, has_key=True), + ), + required_group_id=None, + required_subgroup_0=tuple(), + required_subgroup_1=tuple(), + required_subgroup_2=tuple(), + required_subgroup_3=tuple(), + is_shutter_room=False, + is_water_room=False, + do_not_randomize=False, + no_special_enemies_standard=False, + ) + state = self._build_state( + dungeon_rooms={1: room}, + sprite_requirements=( + self._requirement(0x12, killable=True, subgroup_0=(1,), cannot_have_key=True), + self._requirement(0x13, killable=True, subgroup_0=(1,)), + ), + ) + selected_group = state.sprite_groups[0x41] + + randomized_room = _randomize_room_sprites( + SimpleNamespace(random=random.Random(0)), + state, + room, + selected_group, + False, + ) + + self.assertEqual(randomized_room.sprites[0].sprite_id, 0x13) + + def test_shutter_water_room_prefers_killable_water_enemy(self) -> None: + room = DungeonEnemyRoom( + room_id=40, + room_header_address=0, + sprite_table_address=0, + graphics_block_id=1, + tag_1=0, + tag_2=0, + sort_sprites_value=0, + sprites=( + DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x8A, is_overlord=False, has_key=False), + ), + required_group_id=None, + required_subgroup_0=tuple(), + required_subgroup_1=tuple(), + required_subgroup_2=tuple(), + required_subgroup_3=tuple(), + is_shutter_room=True, + is_water_room=True, + do_not_randomize=False, + no_special_enemies_standard=False, + ) + state = self._build_state( + dungeon_rooms={40: room}, + sprite_requirements=( + self._requirement(0x8A, killable=False, subgroup_2=(34,)), + self._requirement(0x81, killable=True, subgroup_2=(34,), is_water_sprite=True), + self._requirement(0x9A, killable=False, subgroup_2=(34,), is_water_sprite=True), + ), + ) + selected_group = state.sprite_groups[0x41] + selected_group.subgroup_2 = 34 + + randomized_room = _randomize_room_sprites( + SimpleNamespace(random=random.Random(0)), + state, + room, + selected_group, + False, + ) + + self.assertEqual(randomized_room.sprites[0].sprite_id, 0x81) + + def test_non_water_shutter_room_replacements_exclude_water_enemies(self) -> None: + room = DungeonEnemyRoom( + room_id=165, + room_header_address=0, + sprite_table_address=0, + graphics_block_id=1, + tag_1=0, + tag_2=0, + sort_sprites_value=0, + sprites=( + DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=False), + ), + required_group_id=None, + required_subgroup_0=tuple(), + required_subgroup_1=tuple(), + required_subgroup_2=tuple(), + required_subgroup_3=tuple(), + is_shutter_room=True, + is_water_room=False, + do_not_randomize=False, + no_special_enemies_standard=False, + ) + state = self._build_state( + dungeon_rooms={165: room}, + sprite_requirements=( + self._requirement(0x20, killable=False, subgroup_0=(1,)), + self._requirement(0x81, killable=True, subgroup_0=(1,), is_water_sprite=True), + self._requirement(0x22, killable=True, subgroup_0=(1,)), + ), + ) + + randomized_room = _randomize_room_sprites( + SimpleNamespace(random=random.Random(1)), + state, + room, + state.sprite_groups[0x41], + False, + ) + + self.assertEqual(randomized_room.sprites[0].sprite_id, 0x22) + + def test_non_water_shutter_group_selection_requires_non_water_killable_enemy(self) -> None: + room = DungeonEnemyRoom( + room_id=165, + room_header_address=0, + sprite_table_address=0, + graphics_block_id=1, + tag_1=0, + tag_2=0, + sort_sprites_value=0, + sprites=( + DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=False), + ), + required_group_id=None, + required_subgroup_0=tuple(), + required_subgroup_1=tuple(), + required_subgroup_2=tuple(), + required_subgroup_3=tuple(), + is_shutter_room=True, + is_water_room=False, + do_not_randomize=False, + no_special_enemies_standard=False, + ) + state = self._build_state( + dungeon_rooms={165: room}, + sprite_requirements=( + self._requirement(0x20, killable=False, subgroup_0=(1,)), + self._requirement(0x81, killable=True, subgroup_0=(1,), is_water_sprite=True), + ), + ) + + self.assertEqual(get_possible_dungeon_sprite_groups(state, room), tuple()) + + def test_wallmaster_cannot_spawn_in_high_room_ids(self) -> None: + room = DungeonEnemyRoom( + room_id=0x100, + room_header_address=0, + sprite_table_address=0, + graphics_block_id=1, + tag_1=0, + tag_2=0, + sort_sprites_value=0, + sprites=tuple(), + required_group_id=None, + required_subgroup_0=tuple(), + required_subgroup_1=tuple(), + required_subgroup_2=tuple(), + required_subgroup_3=tuple(), + is_shutter_room=False, + is_water_room=False, + do_not_randomize=False, + no_special_enemies_standard=False, + ) + + self.assertFalse(can_spawn_in_room(self._requirement(WALLMASTER_SPRITE_ID), room)) + + def test_room_specific_do_not_randomize_sprites_are_not_updated(self) -> None: + room = DungeonEnemyRoom( + room_id=7, + room_header_address=0, + sprite_table_address=0, + graphics_block_id=1, + tag_1=0, + tag_2=0, + sort_sprites_value=0, + sprites=( + DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x30, is_overlord=False, has_key=False), + DungeonEnemySprite(address=0x1003, byte_0=0, byte_1=0, sprite_id=0x31, is_overlord=False, has_key=False), + ), + required_group_id=None, + required_subgroup_0=tuple(), + required_subgroup_1=tuple(), + required_subgroup_2=tuple(), + required_subgroup_3=tuple(), + is_shutter_room=False, + is_water_room=False, + do_not_randomize=False, + no_special_enemies_standard=False, + ) + state = self._build_state( + dungeon_rooms={7: room}, + sprite_requirements=( + self._requirement(0x30, subgroup_0=(1,), dont_randomize_rooms=(7,)), + self._requirement(0x31, subgroup_0=(1,)), + ), + ) + + self.assertEqual( + [sprite.sprite_id for sprite in _get_randomizable_sprites_in_room(state, room)], + [0x31], + ) + + def test_water_rooms_only_use_water_enemies(self) -> None: + room = DungeonEnemyRoom( + room_id=1, + room_header_address=0, + sprite_table_address=0, + graphics_block_id=1, + tag_1=0, + tag_2=0, + sort_sprites_value=0, + sprites=( + DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=False), + ), + required_group_id=None, + required_subgroup_0=tuple(), + required_subgroup_1=tuple(), + required_subgroup_2=tuple(), + required_subgroup_3=tuple(), + is_shutter_room=False, + is_water_room=True, + do_not_randomize=False, + no_special_enemies_standard=False, + ) + state = self._build_state( + dungeon_rooms={1: room}, + sprite_requirements=( + self._requirement(0x20, subgroup_0=(1,)), + self._requirement(0x21, subgroup_0=(1,), is_water_sprite=True), + self._requirement(0x22, subgroup_0=(1,), is_water_sprite=True), + ), + ) + + randomized_room = _randomize_room_sprites( + SimpleNamespace(random=random.Random(0)), + state, + room, + state.sprite_groups[0x41], + False, + ) + + self.assertIn(randomized_room.sprites[0].sprite_id, {0x21, 0x22}) + + def test_dungeon_group_selection_excludes_groups_without_enemy_requirements(self) -> None: + room = DungeonEnemyRoom( + room_id=1, + room_header_address=0, + sprite_table_address=0, + graphics_block_id=1, + tag_1=0, + tag_2=0, + sort_sprites_value=0, + sprites=( + DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=False), + ), + required_group_id=None, + required_subgroup_0=tuple(), + required_subgroup_1=tuple(), + required_subgroup_2=tuple(), + required_subgroup_3=tuple(), + is_shutter_room=False, + is_water_room=False, + do_not_randomize=False, + no_special_enemies_standard=False, + ) + state = self._build_state( + dungeon_rooms={1: room}, + sprite_requirements=(self._requirement(0x20, subgroup_0=(1,)),), + ) + state.sprite_groups[0x42] = DungeonSpriteGroup( + group_id=0x42, + dungeon_group_id=2, + subgroup_0=0, + subgroup_1=0, + subgroup_2=0, + subgroup_3=0, + ) + + possible_groups = get_possible_dungeon_sprite_groups(state, room) + + self.assertEqual([group.group_id for group in possible_groups], [0x41]) + + def test_key_room_group_selection_excludes_groups_without_room_spawnable_key_enemies(self) -> None: + room = DungeonEnemyRoom( + room_id=61, + room_header_address=0, + sprite_table_address=0, + graphics_block_id=1, + tag_1=0, + tag_2=0, + sort_sprites_value=0, + sprites=( + DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=True), + ), + required_group_id=None, + required_subgroup_0=tuple(), + required_subgroup_1=tuple(), + required_subgroup_2=tuple(), + required_subgroup_3=tuple(), + is_shutter_room=False, + is_water_room=False, + do_not_randomize=False, + no_special_enemies_standard=False, + ) + state = self._build_state( + dungeon_rooms={61: room}, + sprite_requirements=( + self._requirement(0x20, subgroup_0=(1,)), + self._requirement(0x50, killable=True, subgroup_1=(32,), excluded_rooms=(61,)), + self._requirement(0x9C, killable=True, subgroup_1=(32,), cannot_have_key=True), + self._requirement(0x51, killable=True, subgroup_1=(33,)), + ), + ) + state.sprite_groups[0x41] = DungeonSpriteGroup( + group_id=0x41, + dungeon_group_id=1, + subgroup_0=1, + subgroup_1=32, + subgroup_2=1, + subgroup_3=1, + ) + state.sprite_groups[0x42] = DungeonSpriteGroup( + group_id=0x42, + dungeon_group_id=2, + subgroup_0=1, + subgroup_1=33, + subgroup_2=1, + subgroup_3=1, + ) + + possible_groups = get_possible_dungeon_sprite_groups(state, room) + + self.assertEqual([group.group_id for group in possible_groups], [0x42]) + + def test_overworld_group_randomization_preserves_forced_subgroups(self) -> None: + sprite_groups = { + 7: DungeonSpriteGroup(group_id=7, dungeon_group_id=-57, subgroup_0=1, subgroup_1=2, subgroup_2=3, subgroup_3=4), + } + + _setup_required_overworld_groups( + sprite_groups, + ( + SimpleNamespace( + group_id=7, + subgroup_0=None, + subgroup_1=None, + subgroup_2=None, + subgroup_3=17, + areas=(0x02,), + ), + ), + ) + _randomize_overworld_groups(SimpleNamespace(random=random.Random(0)), sprite_groups) + + group = sprite_groups[7] + self.assertEqual(group.subgroup_3, 17) + self.assertIn(group.subgroup_0, {22, 31, 47, 14}) + self.assertIn(group.subgroup_1, {44, 30, 32}) + self.assertIn(group.subgroup_2, {12, 18, 23, 24, 28, 46, 34, 35, 39, 40, 38, 41, 36, 37, 42}) + + def test_selected_boss_group_requirements_override_shared_boss_graphics_group(self) -> None: + sprite_groups = { + 0x56: DungeonSpriteGroup( + group_id=0x56, + dungeon_group_id=22, + subgroup_0=1, + subgroup_1=1, + subgroup_2=60, + subgroup_3=49, + ), + } + sprite_requirements = ( + self._requirement(162, subgroup_2=(60,)), + self._requirement(189, subgroup_3=(61,)), + ) + + _apply_selected_boss_group_requirements( + self._build_boss_world({"Eastern Palace": "Vitreous"}), + sprite_groups, + sprite_requirements, + ) + + group = sprite_groups[0x56] + self.assertEqual(group.subgroup_2, 60) + self.assertEqual(group.subgroup_3, 61) + self.assertTrue(group.preserve_subgroup_2) + self.assertTrue(group.preserve_subgroup_3) + + @staticmethod + def _requirement( + sprite_id: int, + *, + killable: bool = False, + subgroup_0: tuple[int, ...] = tuple(), + subgroup_1: tuple[int, ...] = tuple(), + subgroup_2: tuple[int, ...] = tuple(), + subgroup_3: tuple[int, ...] = tuple(), + group_ids: tuple[int, ...] = tuple(), + absorbable: bool = False, + never_use_dungeon: bool = False, + never_use_overworld: bool = False, + cannot_have_key: bool = False, + is_water_sprite: bool = False, + excluded_rooms: tuple[int, ...] = tuple(), + dont_randomize_rooms: tuple[int, ...] = tuple(), + ) -> EnemySpriteRequirement: + return EnemySpriteRequirement( + sprite_name=f"sprite_{sprite_id:02x}", + sprite_id=sprite_id, + boss=False, + overlord=False, + do_not_randomize=False, + killable=killable, + npc=False, + never_use_dungeon=never_use_dungeon, + never_use_overworld=never_use_overworld, + cannot_have_key=cannot_have_key, + is_object=False, + absorbable=absorbable, + is_water_sprite=is_water_sprite, + is_enemy_sprite=True, + group_ids=group_ids, + subgroup_0=subgroup_0, + subgroup_1=subgroup_1, + subgroup_2=subgroup_2, + subgroup_3=subgroup_3, + parameters=None, + special_glitched=False, + excluded_rooms=excluded_rooms, + dont_randomize_rooms=dont_randomize_rooms, + spawnable_rooms=tuple(), + ) + + @staticmethod + def _build_state( + *, + dungeon_rooms=None, + overworld_areas=None, + randomized_dungeon_rooms=None, + randomized_overworld_areas=None, + sprite_requirements=tuple(), + ) -> EnemyShuffleState: + sprite_groups = { + 1: DungeonSpriteGroup(group_id=1, dungeon_group_id=-63, subgroup_0=1, subgroup_1=1, subgroup_2=1, subgroup_3=1), + 0x41: DungeonSpriteGroup(group_id=0x41, dungeon_group_id=1, subgroup_0=1, subgroup_1=1, subgroup_2=1, subgroup_3=1), + } + return EnemyShuffleState( + dungeon_rooms=dungeon_rooms or {}, + overworld_areas=overworld_areas or {}, + sprite_groups=sprite_groups, + sprite_requirements=sprite_requirements, + room_group_requirements=tuple(), + overworld_group_requirements=tuple(), + shutter_room_ids=frozenset(), + water_room_ids=frozenset(), + dont_randomize_room_ids=frozenset(), + no_special_enemies_standard_room_ids=frozenset(), + boss_room_ids=frozenset(), + dont_randomize_overworld_area_ids=frozenset(), + randomized_dungeon_rooms=randomized_dungeon_rooms or {}, + randomized_overworld_areas=randomized_overworld_areas or {}, + ) + + @staticmethod + def _build_boss_world(boss_overrides: dict[str, str] | None = None) -> SimpleNamespace: + boss_overrides = boss_overrides or {} + + def boss(name: str) -> SimpleNamespace: + return SimpleNamespace(enemizer_name=name) + + return SimpleNamespace( + options=SimpleNamespace(mode="open"), + dungeons={ + "Eastern Palace": SimpleNamespace(boss=boss(boss_overrides.get("Eastern Palace", "Armos"))), + "Desert Palace": SimpleNamespace(boss=boss(boss_overrides.get("Desert Palace", "Lanmola"))), + "Tower of Hera": SimpleNamespace(boss=boss(boss_overrides.get("Tower of Hera", "Moldorm"))), + "Palace of Darkness": SimpleNamespace(boss=boss(boss_overrides.get("Palace of Darkness", "Helmasaur"))), + "Swamp Palace": SimpleNamespace(boss=boss(boss_overrides.get("Swamp Palace", "Arrghus"))), + "Skull Woods": SimpleNamespace(boss=boss(boss_overrides.get("Skull Woods", "Mothula"))), + "Thieves Town": SimpleNamespace(boss=boss(boss_overrides.get("Thieves Town", "Blind"))), + "Ice Palace": SimpleNamespace(boss=boss(boss_overrides.get("Ice Palace", "Kholdstare"))), + "Misery Mire": SimpleNamespace(boss=boss(boss_overrides.get("Misery Mire", "Vitreous"))), + "Turtle Rock": SimpleNamespace(boss=boss(boss_overrides.get("Turtle Rock", "Trinexx"))), + "Ganons Tower": SimpleNamespace( + bosses={ + "bottom": boss(boss_overrides.get("Ganons Tower Bottom", "Armos")), + "middle": boss(boss_overrides.get("Ganons Tower Middle", "Lanmola")), + "top": boss(boss_overrides.get("Ganons Tower Top", "Moldorm")), + } + ), + }, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/worlds/alttp/test/TestPotShuffle.py b/worlds/alttp/test/TestPotShuffle.py new file mode 100644 index 000000000000..5d63d5d182c7 --- /dev/null +++ b/worlds/alttp/test/TestPotShuffle.py @@ -0,0 +1,56 @@ +import random +import unittest +from types import SimpleNamespace + +from worlds.alttp.PotShuffle import ( + POT_KEY, + POT_HOLE, + generate_pot_shuffle, + get_unique_pot_item_position, +) + + +class TestPotShuffle(unittest.TestCase): + def test_reserved_key_rooms_only_place_actual_keys(self) -> None: + for seed in range(10): + world = SimpleNamespace( + random=random.Random(seed), + options=SimpleNamespace(retro_bow=False), + ) + shuffled_pots = generate_pot_shuffle(world) + conveyor_cross_keys = [ + pot for pot in shuffled_pots[0x8B] + if pot.item == POT_KEY + ] + self.assertEqual(len(conveyor_cross_keys), 1) + + def test_get_unique_pot_item_position_returns_single_match(self) -> None: + world = SimpleNamespace( + random=random.Random(0), + options=SimpleNamespace(retro_bow=False), + ) + shuffled_pots = generate_pot_shuffle(world) + + self.assertEqual( + get_unique_pot_item_position(shuffled_pots, 0x36, POT_KEY), + (114, 16), + ) + + def test_reserved_hole_room_keeps_hole_fixed(self) -> None: + for seed in range(25): + world = SimpleNamespace( + random=random.Random(seed), + options=SimpleNamespace(retro_bow=False), + ) + shuffled_pots = generate_pot_shuffle(world) + hole_positions = [ + (pot.x, pot.y) + for pot in shuffled_pots[206] + if pot.item == POT_HOLE + ] + + self.assertEqual(hole_positions, [(204, 11)]) + + +if __name__ == "__main__": + unittest.main() From 4ef1fb7630a924154e43d582108e24c2462f609d Mon Sep 17 00:00:00 2001 From: Justus Lind Date: Sun, 10 May 2026 05:25:34 +1000 Subject: [PATCH 50/66] Muse Dash: Convert to Rule Builder (#6166) --------- Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com> --- worlds/musedash/__init__.py | 9 +-- worlds/musedash/test/TestLocationRules.py | 69 +++++++++++++++++++++++ 2 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 worlds/musedash/test/TestLocationRules.py diff --git a/worlds/musedash/__init__.py b/worlds/musedash/__init__.py index 6e64adf02496..97dfc12b08f2 100644 --- a/worlds/musedash/__init__.py +++ b/worlds/musedash/__init__.py @@ -3,6 +3,7 @@ from typing import List, ClassVar, Type, Set from math import floor from Options import PerGameCommonOptions, OptionError +from rule_builder.rules import Has from .Options import MuseDashOptions, md_option_groups from .Items import MuseDashSongItem, MuseDashFixedItem @@ -293,17 +294,17 @@ def create_regions(self) -> None: # Adds 2 item locations per song/album to the menu region. for i in range(0, len(all_selected_locations)): name = all_selected_locations[i] + rule = Has(name) loc1 = MuseDashLocation(self.player, name + "-0", self.md_collection.song_locations[name + "-0"], menu_region) - loc1.access_rule = lambda state, place=name: state.has(place, self.player) + self.set_rule(loc1, rule) menu_region.locations.append(loc1) loc2 = MuseDashLocation(self.player, name + "-1", self.md_collection.song_locations[name + "-1"], menu_region) - loc2.access_rule = lambda state, place=name: state.has(place, self.player) + self.set_rule(loc2, rule) menu_region.locations.append(loc2) def set_rules(self) -> None: - self.multiworld.completion_condition[self.player] = lambda state: \ - state.has(self.md_collection.MUSIC_SHEET_NAME, self.player, self.get_music_sheet_win_count()) + self.set_completion_rule(Has(self.md_collection.MUSIC_SHEET_NAME, self.get_music_sheet_win_count())) def get_available_traps(self) -> List[str]: full_trap_list = self.md_collection.trap_items.keys() diff --git a/worlds/musedash/test/TestLocationRules.py b/worlds/musedash/test/TestLocationRules.py new file mode 100644 index 000000000000..103130b8cf73 --- /dev/null +++ b/worlds/musedash/test/TestLocationRules.py @@ -0,0 +1,69 @@ +from . import MuseDashTestBase +from typing import List + + +class LocationRules(MuseDashTestBase): + CHECK_SONGS: List[str] = [ + "Magical Wonderland", + "Iyaiya", + "Wonderful Pain", + "Breaking Dawn", + "One-Way Subway", + "Frost Land", + "Heart-Pounding Flight", + "Pancake is Love", + "Shiguang Tuya", + "Evolution", + "Dolphin and Broadcast", + "Yuki no Shizuku Ame no Oto", + "Best One feat.tooko", + "Candy-coloured Love Theory", + "Night Wander", + "Dohna Dohna no Uta", + "Spring Carnival", + "DISCO NIGHT", + "Koi no Moonlight" + ] + + options = { + "starting_song_count": 3, + "additional_song_count": 15, + "streamer_mode_enabled": True, + "include_songs": CHECK_SONGS + } + + + def test_rules(self): + """Due to me typoing the second rule of a location, this test exists to ensure that doesn't happen again""" + muse_dash_world = self.get_world() + + for song in self.CHECK_SONGS: + if song == muse_dash_world.victory_song_name: + continue + + if song in muse_dash_world.starting_songs: + self.assertTrue(self.can_reach_location(song + "-0"), f"Starting Location {song}-0 was not beatable.") + self.assertTrue(self.can_reach_location(song + "-1"), f"Starting Location {song}-1 was not beatable.") + continue + + + self.assertFalse(self.can_reach_location(song + "-0"), f"Location {song}-0 was unlocked without an item.") + self.assertFalse(self.can_reach_location(song + "-1"), f"Location {song}-1 was unlocked without an item.") + self.collect_by_name(song) + self.assertTrue(self.can_reach_location(song + "-0"), f"Location {song}-0 was not unlocked with its item.") + self.assertTrue(self.can_reach_location(song + "-1"), f"Location {song}-1 was not unlocked with its item.") + + + sheets = self.get_items_by_name("Music Sheet") + sheets_to_win = muse_dash_world.get_music_sheet_win_count() + + for sheet in sheets: + if sheets_to_win <= 0: + break + + self.assertBeatable(False) + self.collect(sheet) + sheets_to_win -= 1 + + self.assertBeatable(True) + \ No newline at end of file From 799e0b7b0fa3c965995a6288c651d2f4d800e4ff Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Sun, 10 May 2026 20:16:20 -0400 Subject: [PATCH 51/66] KH2: fix ice cream double counting for logic (#6068) --- worlds/kh2/Items.py | 1 - worlds/kh2/__init__.py | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/kh2/Items.py b/worlds/kh2/Items.py index cb3d7c8d85ed..41457575d208 100644 --- a/worlds/kh2/Items.py +++ b/worlds/kh2/Items.py @@ -579,7 +579,6 @@ ItemName.IceCream, ItemName.WaytotheDawn, ItemName.IdentityDisk, - ItemName.IceCream, ItemName.NamineSketches ], "AllVisitLocking": { diff --git a/worlds/kh2/__init__.py b/worlds/kh2/__init__.py index 7e10dc29b7c1..c7ed1522566d 100644 --- a/worlds/kh2/__init__.py +++ b/worlds/kh2/__init__.py @@ -184,6 +184,8 @@ def create_items(self) -> None: if self.visitlocking_dict[item] == 0: self.visitlocking_dict.pop(item) self.multiworld.push_precollected(self.create_item(item)) + # tt is 3 visits so 2nd visit locking unlocks only the third visit + self.multiworld.push_precollected(self.create_item(ItemName.IceCream)) for _ in range(self.options.RandomVisitLockingItem.value): if sum(self.visitlocking_dict.values()) <= 0: From 38e77e1b46b20552d5fdf2a0ef88558e7ec2f813 Mon Sep 17 00:00:00 2001 From: Katelyn Gigante Date: Sun, 17 May 2026 02:29:30 +1000 Subject: [PATCH 52/66] Factorio: Remove the need to have two separate installs (#4221) --- worlds/factorio/Client.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) mode change 100755 => 100644 worlds/factorio/Client.py diff --git a/worlds/factorio/Client.py b/worlds/factorio/Client.py old mode 100755 new mode 100644 index 5649375916ef..f6b28f2ceb2a --- a/worlds/factorio/Client.py +++ b/worlds/factorio/Client.py @@ -63,7 +63,7 @@ def _cmd_toggle_connection_change_filter(self): def _cmd_toggle_chat(self): """Toggle sending of chat messages from players on the Factorio server to Archipelago.""" self.ctx.toggle_bridge_chat_out() - + def _cmd_rcon_reconnect(self) -> bool: """Reconnect the RCON client if its disconnected.""" try: @@ -88,7 +88,7 @@ class FactorioContext(CommonContext): def __init__(self, server_address, password, filter_connection_changes: bool, filter_item_sends: bool, bridge_chat_out: bool, rcon_port: int, rcon_password: str, server_settings_path: str | None, - factorio_server_args: tuple[str, ...]): + config_file: str, factorio_server_args: tuple[str, ...] | list[str]): super(FactorioContext, self).__init__(server_address, password) self.send_index: int = 0 self.rcon_client = None @@ -105,6 +105,7 @@ def __init__(self, server_address, password, filter_connection_changes: bool, fi self.rcon_port: int = rcon_port self.rcon_password: str = rcon_password self.server_settings_path: str = server_settings_path + self.config_file: str = config_file self.additional_factorio_server_args = factorio_server_args @property @@ -158,9 +159,11 @@ def server_args(self) -> tuple[str, ...]: "--rcon-port", str(self.rcon_port), "--rcon-password", self.rcon_password, "--server-settings", self.server_settings_path, + "--config", self.config_file, *self.additional_factorio_server_args) else: return ("--rcon-port", str(self.rcon_port), "--rcon-password", self.rcon_password, + "--config", self.config_file, *self.additional_factorio_server_args) @property @@ -364,7 +367,7 @@ async def factorio_server_watcher(ctx: FactorioContext): if not os.path.exists(savegame_name): logger.info(f"Creating savegame {savegame_name}") subprocess.run(( - executable, "--create", savegame_name, "--preset", "archipelago" + executable, "--create", savegame_name, "--preset", "archipelago", "--config", ctx.config_file )) factorio_process = subprocess.Popen((executable, "--start-server", savegame_name, *ctx.server_args), @@ -478,7 +481,7 @@ async def factorio_spinup_server(ctx: FactorioContext) -> bool: if not os.path.exists(savegame_name): logger.info(f"Creating savegame {savegame_name}") subprocess.run(( - executable, "--create", savegame_name + executable, "--create", savegame_name, "--config", ctx.config_file )) factorio_process = subprocess.Popen( (executable, "--start-server", savegame_name, *ctx.server_args), @@ -609,6 +612,9 @@ def launch(*new_args: str): if not os.path.exists(os.path.dirname(executable)): raise FileNotFoundError(f"Path {os.path.dirname(executable)} does not exist or could not be accessed.") + if os.path.isdir(executable) and os.path.exists(os.path.join(executable, "Contents", "MacOS", "factorio")): + # user entered the .App bundle, let's find the executable + executable = os.path.join(executable, "Contents", "MacOS", "factorio") if os.path.isdir(executable): # user entered a path to a directory, let's find the executable therein executable = os.path.join(executable, "factorio") if not os.path.isfile(executable): @@ -617,9 +623,15 @@ def launch(*new_args: str): else: raise FileNotFoundError(f"Path {executable} is not an executable file.") + config_file = user_path('factorio', 'config', 'apconfig.ini') + if not os.path.exists(config_file): + os.makedirs(os.path.dirname(config_file), exist_ok=True) + with open(config_file, 'w') as f: + f.write(f"[path]\nread-data=__PATH__system-read-data__\nwrite-data={user_path('factorio')}") + asyncio.run(main(lambda: FactorioContext( args.connect, args.password, initial_filter_connection_changes, initial_filter_item_sends, initial_bridge_chat_out, - rcon_port, rcon_password, server_settings, rest + rcon_port, rcon_password, server_settings, config_file, rest ))) colorama.deinit() From 24f75ba0729912e485a0b4bea05d109812bb8303 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sun, 17 May 2026 16:40:08 -0500 Subject: [PATCH 53/66] Settings: validate FilePath hash on use (#5854) --- settings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/settings.py b/settings.py index 46377389aaa8..a23e7aed9d67 100644 --- a/settings.py +++ b/settings.py @@ -98,6 +98,8 @@ def __getattribute__(self, item: str) -> Any: self._changed = True attr = new # resolve the path immediately when accessing it + if attr.exists(): + attr.__class__.validate(attr.resolve()) return attr.__class__(attr.resolve()) return attr From d8d148ac135c5848820f22bdc9bc13aa62831cbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Bolduc?= <16137441+Jouramie@users.noreply.github.com> Date: Sun, 17 May 2026 17:41:34 -0400 Subject: [PATCH 54/66] Stardew Valley: Bypass CollectionState overhead by just calling prog_items (#6065) --- worlds/stardew_valley/stardew_rule/state.py | 4 ++-- worlds/stardew_valley/test/TestStardewRule.py | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/worlds/stardew_valley/stardew_rule/state.py b/worlds/stardew_valley/stardew_rule/state.py index 3fe294a3baf5..dcf0a2bc7919 100644 --- a/worlds/stardew_valley/stardew_rule/state.py +++ b/worlds/stardew_valley/stardew_rule/state.py @@ -27,7 +27,7 @@ def __init__(self, count: int, items: Union[str, Iterable[str]], player: int): def __call__(self, state: CollectionState) -> bool: c = 0 for item in self.items: - c += state.count(item, self.player) + c += state.prog_items[self.player][item] if c >= self.count: return True return False @@ -56,7 +56,7 @@ def value(self): return self.count def __call__(self, state: CollectionState) -> bool: - return state.has(self.item, self.player, self.count) + return state.prog_items[self.player][self.item] >= self.count def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]: return self, self(state) diff --git a/worlds/stardew_valley/test/TestStardewRule.py b/worlds/stardew_valley/test/TestStardewRule.py index 93b32b0d8ab4..842e4589c65f 100644 --- a/worlds/stardew_valley/test/TestStardewRule.py +++ b/worlds/stardew_valley/test/TestStardewRule.py @@ -1,4 +1,5 @@ import unittest +from collections import Counter from typing import cast from unittest.mock import MagicMock, Mock @@ -103,7 +104,7 @@ def test_short_circuit_when_complement_found(self): def test_short_circuit_when_combinable_rules_is_false(self): collection_state = MagicMock() - collection_state.has = Mock(return_value=False) + collection_state.prog_items = {1: Counter()} other_rule = MagicMock() rule = And(Received("Potato", 1, 10), cast(StardewRule, other_rule)) @@ -113,6 +114,7 @@ def test_short_circuit_when_combinable_rules_is_false(self): def test_identity_is_removed_from_other_rules(self): collection_state = MagicMock() + collection_state.prog_items = {1: Counter()} rule = Or(false_, Received("Potato", 1, 10)) rule.evaluate_while_simplifying(collection_state) @@ -122,6 +124,7 @@ def test_identity_is_removed_from_other_rules(self): def test_complement_replaces_combinable_rules(self): collection_state = MagicMock() + collection_state.prog_items = {1: Counter()} rule = Or(Received("Potato", 1, 10), true_) rule.evaluate_while_simplifying(collection_state) @@ -132,6 +135,7 @@ def test_simplifying_to_complement_propagates_complement(self): expected_simplified = true_ expected_result = True collection_state = MagicMock() + collection_state.prog_items = {1: Counter()} rule = Or(Or(expected_simplified), Received("Potato", 1, 10)) actual_simplified, actual_result = rule.evaluate_while_simplifying(collection_state) @@ -142,6 +146,7 @@ def test_simplifying_to_complement_propagates_complement(self): def test_already_simplified_rules_are_not_simplified_again(self): collection_state = MagicMock() + collection_state.prog_items = {1: Counter()} other_rule = MagicMock() other_rule.evaluate_while_simplifying = Mock(return_value=(other_rule, False)) rule = Or(cast(StardewRule, other_rule), Received("Potato", 1, 10)) From b575983599046a5e1ef98cbd7ce2f38b10eb6f02 Mon Sep 17 00:00:00 2001 From: Will Morrow Date: Sun, 17 May 2026 14:44:40 -0700 Subject: [PATCH 55/66] SM64: Change power star item classification to be deprioritized (#6191) --- worlds/sm64ex/Items.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/sm64ex/Items.py b/worlds/sm64ex/Items.py index 6cd9b5eb461b..ed1922157247 100644 --- a/worlds/sm64ex/Items.py +++ b/worlds/sm64ex/Items.py @@ -12,7 +12,7 @@ class SM64ItemData(NamedTuple): classification: ItemClassification = ItemClassification.progression generic_item_data_table: dict[str, SM64ItemData] = { - "Power Star": SM64ItemData(sm64ex_base_id + 0, ItemClassification.progression_skip_balancing), + "Power Star": SM64ItemData(sm64ex_base_id + 0, ItemClassification.progression_deprioritized_skip_balancing), "Basement Key": SM64ItemData(sm64ex_base_id + 178), "Second Floor Key": SM64ItemData(sm64ex_base_id + 179), "Progressive Key": SM64ItemData(sm64ex_base_id + 180), From d0abfeb88b34f6eaad6ed677b10bea9b6e17a973 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, 18 May 2026 07:53:48 -0400 Subject: [PATCH 56/66] The Messenger: Fix portal shuffle logic not using unlocked portals (#6194) --- worlds/messenger/portals.py | 6 +----- worlds/messenger/test/test_portals.py | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/worlds/messenger/portals.py b/worlds/messenger/portals.py index 11fc6101087b..8e46eee17d6b 100644 --- a/worlds/messenger/portals.py +++ b/worlds/messenger/portals.py @@ -7,7 +7,6 @@ if TYPE_CHECKING: from . import MessengerWorld - PORTALS: list[str] = [ "Autumn Hills", "Riviere Turquoise", @@ -17,7 +16,6 @@ "Glacial Peak", ] - SHOP_POINTS: dict[str, list[str]] = { "Autumn Hills": [ "Climbing Claws", @@ -112,7 +110,6 @@ ] } - CHECKPOINTS: dict[str, list[str]] = { "Autumn Hills": [ "Hope Latch", @@ -185,7 +182,6 @@ ] } - REGION_ORDER: list[str] = [ "Autumn Hills", "Forlorn Temple", @@ -306,4 +302,4 @@ def add_closed_portal_reqs(world: "MessengerWorld") -> None: closed_portals = [entrance for entrance in PORTALS if f"{entrance} Portal" not in world.starting_portals] for portal in closed_portals: tower_exit = world.multiworld.get_entrance(f"ToTHQ {portal} Portal", world.player) - tower_exit.access_rule = lambda state, portal_item=portal: state.has(portal_item, world.player) + tower_exit.access_rule = lambda state, portal_item=portal: state.has(f"{portal_item} Portal", world.player) diff --git a/worlds/messenger/test/test_portals.py b/worlds/messenger/test/test_portals.py index b1875ac0b3ab..416b1f6a6af8 100644 --- a/worlds/messenger/test/test_portals.py +++ b/worlds/messenger/test/test_portals.py @@ -35,3 +35,29 @@ def test_portal_reqs(self) -> None: test_state.collect(item) self.assertTrue(entrance.can_reach(test_state), grouping) entrance.access_rule = lambda state: True + + +class PortalUnlockTest(MessengerTestBase): + options = { + "available_portals": 3, + } + + def test_unlocking_portal(self) -> None: + """Validate that unlocking the portal event actually unlock the portal in HQ""" + + print(self.world.starting_portals) + + for portal in PORTALS: + name = f"{portal} Portal" + if name in self.world.starting_portals: + continue + + entrance_name = f"ToTHQ {name}" + with self.subTest(portal=name, entrance_name=entrance_name): + hq_portal = self.multiworld.get_entrance(entrance_name, self.player) + test_state = CollectionState(self.multiworld) + self.assertFalse(hq_portal.can_reach(test_state), "reachable with nothing") + + event = self.multiworld.get_location(name, self.player) + test_state.collect(event.item) + self.assertTrue(hq_portal.can_reach(test_state)) From 545171f3f4e42317d61391423839a138b88d1bec Mon Sep 17 00:00:00 2001 From: qwint Date: Wed, 20 May 2026 02:12:54 -0500 Subject: [PATCH 57/66] Core: reset log level when using pyximport (#6212) --- NetUtils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/NetUtils.py b/NetUtils.py index f61dbf9fcb0f..f792a27288e3 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -527,7 +527,11 @@ class MultiData(typing.TypedDict): except ImportError: pyximport = None try: + import logging + logger = logging.getLogger() + old_level = logger.level from _speedups import LocationStore + logger.setLevel(old_level) except ImportError: warnings.warn("_speedups not available. Falling back to pure python LocationStore. " "Install a matching C++ compiler for your platform to compile _speedups.") From 787ddc400caf9bd3e327083fba3cef7e97b513be Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 27 May 2026 03:26:30 +0200 Subject: [PATCH 58/66] Core: Bump version from 0.6.7 to 0.6.8 (#6114) --- Utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Utils.py b/Utils.py index b32d863cdd28..2e57c15aed7f 100644 --- a/Utils.py +++ b/Utils.py @@ -52,7 +52,7 @@ def as_simple_string(self) -> str: return ".".join(str(item) for item in self) -__version__ = "0.6.7" +__version__ = "0.6.8" version_tuple = tuplize_version(__version__) is_linux = sys.platform.startswith("linux") From b24b3d10b646dced8d0f04daa130905e5a9788df Mon Sep 17 00:00:00 2001 From: CosmicWolf <54233835+Enderdraak@users.noreply.github.com> Date: Sun, 31 May 2026 01:49:39 +0200 Subject: [PATCH 59/66] Factorio: Locale changes (#5908) --------- Co-authored-by: Joris Santen Co-authored-by: lepideble <147614625+lepideble@users.noreply.github.com> --- worlds/factorio/Mod.py | 8 ++--- worlds/factorio/data/mod/locale/en/locale.cfg | 36 +++++++++++++++++++ worlds/factorio/data/mod_template/control.lua | 18 +++++----- .../data/mod_template/data-final-fixes.lua | 7 ++++ worlds/factorio/data/mod_template/data.lua | 3 -- .../data/mod_template/locale/en/locale.cfg | 31 ---------------- .../factorio/data/mod_template/settings.lua | 2 ++ 7 files changed, 56 insertions(+), 49 deletions(-) create mode 100644 worlds/factorio/data/mod/locale/en/locale.cfg delete mode 100644 worlds/factorio/data/mod_template/locale/en/locale.cfg diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index 00ef0d866a30..b53b0fa05f87 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -23,7 +23,6 @@ data_template: Optional[jinja2.Template] = None data_final_template: Optional[jinja2.Template] = None -locale_template: Optional[jinja2.Template] = None control_template: Optional[jinja2.Template] = None settings_template: Optional[jinja2.Template] = None @@ -94,7 +93,7 @@ def generate_mod(world: "Factorio", output_directory: str): multiworld = world.multiworld random = world.random - global data_final_template, locale_template, control_template, data_template, settings_template + global data_final_template, control_template, data_template, settings_template with template_load_lock: if not data_final_template: def load_template(name: str): @@ -107,7 +106,6 @@ def load_template(name: str): data_template = template_env.get_template("data.lua") data_final_template = template_env.get_template("data-final-fixes.lua") - locale_template = template_env.get_template(r"locale/en/locale.cfg") control_template = template_env.get_template("control.lua") settings_template = template_env.get_template("settings.lua") # get data for templates @@ -195,9 +193,7 @@ def flop_random(low, high, base=None): control_template.render(**template_data))) mod.writing_tasks.append(lambda: (versioned_mod_name + "/settings.lua", settings_template.render(**template_data))) - mod.writing_tasks.append(lambda: (versioned_mod_name + "/locale/en/locale.cfg", - locale_template.render(**template_data))) - + info = base_info.copy() info["name"] = mod_name mod.writing_tasks.append(lambda: (versioned_mod_name + "/info.json", diff --git a/worlds/factorio/data/mod/locale/en/locale.cfg b/worlds/factorio/data/mod/locale/en/locale.cfg new file mode 100644 index 000000000000..7d60d90b4398 --- /dev/null +++ b/worlds/factorio/data/mod/locale/en/locale.cfg @@ -0,0 +1,36 @@ +[map-gen-preset-name] +archipelago=Archipelago + +[entity-name] +ap-energy-bridge=Archipelago EnergyLink Bridge + +[map-gen-preset-description] +archipelago=World preset created by the Archipelago Randomizer. World may or may not contain actual archipelagos. + +[technology-name] +ap-technology-full=__1__'s __2__ (__3__) +ap-technology-hidden=__1__ + +[technology-description] +ap-technology-full=Researching this technology sends __1__ to __2____3__. +ap-technology-item-advancement=, which is considered a logical advancement +ap-technology-item-useful=, which is considered useful +ap-technology-item-trap=, which is considered fun +ap-technology-hidden=Researching this technology sends something to someone__1__. + +[mod-setting-name] +archipelago-death-link=Death Link + +[mod-setting-description] +archipelago-death-link=Kill other players in the same Archipelago Multiworld that also have Death Link turned on, when you die. + +[archipelago] +receive-ap-item=Received __1__ from __2__. +receive-ap-catchup=Received __1__ as it is already checked. +receive-sample-item=Received __1__x __2__ +sample-inventory-full=Additional items will be sent when inventory space is available. +sample-error=Unable to receive __1__x [item=__2__] as this item does not exist. +fail-to-place=Failed to place __1__ in __2__ + +[traps] +new-evolution-factor=New evolution factor: __1__ diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua index 3f754d925ce9..060fe56431c4 100644 --- a/worlds/factorio/data/mod_template/control.lua +++ b/worlds/factorio/data/mod_template/control.lua @@ -420,12 +420,12 @@ function update_player(index) sent = 0 end if sent > 0 then - player.print("Received " .. sent .. "x [item=" .. name .. ",quality={{ free_sample_quality_name }}]") + player.print({"archipelago.receive-sample-item", sent, "[item=" .. name .. ",quality="..stack.quality.."]"}) data.suppress_full_inventory_message = false end if sent ~= count then -- Couldn't full send. if not data.suppress_full_inventory_message then - player.print("Additional items will be sent when inventory space is available.", {r=1, g=1, b=0.25}) + player.print({"archipelago.sample-inventory-full"}, {r=1, g=1, b=0.25}) end data.suppress_full_inventory_message = true -- Avoid spamming them with repeated full inventory messages. samples[name] = count - sent -- Buffer the remaining items @@ -434,7 +434,7 @@ function update_player(index) samples[name] = nil -- Remove from the list end else - player.print("Unable to receive " .. count .. "x [item=" .. name .. "] as this item does not exist.") + player.print({"archipelago.sample-inventory-full", count, name}) samples[name] = nil end end @@ -665,7 +665,7 @@ function spawn_entity(surface, force, name, x, y, radius, randomize, avoid_ores) end end if new_entity == nil then - force.print("Failed to place " .. args.name .. " in " .. serpent.line({x = x, y = y, radius = radius})) + force.print({"archipelago.fail-to-place", args.name, serpent.line({x = x, y = y, radius = radius})}) end end @@ -725,7 +725,7 @@ end, local new_factor = game.forces["enemy"].get_evolution_factor("nauvis") + (TRAP_EVO_FACTOR * (1 - game.forces["enemy"].get_evolution_factor("nauvis"))) game.forces["enemy"].set_evolution_factor(new_factor, "nauvis") - game.print({"", "New evolution factor:", new_factor}) + game.print({"traps.new-evolution-factor", new_factor}) end, ["Teleport Trap"] = function() for _, player in ipairs(game.forces["player"].players) do @@ -780,7 +780,7 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi elseif index == -1 then -- for coop sync and restoring from an older savegame tech = force.technologies[item_name] if tech.researched ~= true then - game.print({"", "Received [technology=" .. tech.name .. "] as it is already checked."}) + game.print({"archipelago.receive-ap-catchup", "[technology=" .. tech.name .. "]"}) game.play_sound({path="utility/research_completed"}) tech.researched = true end @@ -792,7 +792,7 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi for _, item_name in ipairs(tech_stack) do tech = force.technologies[item_name] if tech.researched ~= true then - game.print({"", "Received [technology=" .. tech.name .. "] from ", source}) + game.print({"archipelago.receive-ap-item", "[technology=" .. tech.name .. "]", source}) game.play_sound({path="utility/research_completed"}) tech.researched = true return @@ -804,7 +804,7 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi if tech ~= nil then storage.index_sync[index] = tech if tech.researched ~= true then - game.print({"", "Received [technology=" .. tech.name .. "] from ", source}) + game.print({"archipelago.receive-ap-item", "[technology=" .. tech.name .. "]", source}) game.play_sound({path="utility/research_completed"}) tech.researched = true end @@ -812,7 +812,7 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi elseif TRAP_TABLE[item_name] ~= nil then if storage.index_sync[index] ~= item_name then -- not yet received trap storage.index_sync[index] = item_name - game.print({"", "Received ", item_name, " from ", source}) + game.print({"archipelago.receive-ap-item", item_name, source}) TRAP_TABLE[item_name]() end else diff --git a/worlds/factorio/data/mod_template/data-final-fixes.lua b/worlds/factorio/data/mod_template/data-final-fixes.lua index e45952301986..bfffab6a61a8 100644 --- a/worlds/factorio/data/mod_template/data-final-fixes.lua +++ b/worlds/factorio/data/mod_template/data-final-fixes.lua @@ -154,6 +154,13 @@ technologies["{{ original_tech_name }}"].hidden_in_factoriopedia = true {#- the tech researched by the local player #} new_tree_copy = table.deepcopy(template_tech) new_tree_copy.name = "ap-{{ location.address }}-"{# use AP ID #} +{%- if location.revealed %} +new_tree_copy.localised_name = {"technology-name.ap-technology-full", "{{ player_names[item.player] }}", "{{ item.name }}", "{{ location.name }}"} +new_tree_copy.localised_description = {"technology-description.ap-technology-full", "{{ item.name }}", "{{ player_names[item.player] }}", {% if item.advancement %}{"technology-description.ap-technology-item-advancement"}{% elif item.useful %}{"technology-description.ap-technology-item-useful"}{% elif item.trap %}{"technology-description.ap-technology-item-trap"}{% else %}""{% endif %}} +{%- else %} +new_tree_copy.localised_name = {"technology-name.ap-technology-hidden", "{{location.name}}"} +new_tree_copy.localised_description = {"technology-description.ap-technology-hidden", {% if tech_tree_information == 1 and item.advancement %}{"technology-description.ap-technology-item-advancement"}{% else %}""{% endif %}} +{% endif -%} {% if location.crafted_item is not none %} new_tree_copy.research_trigger = { type = "{{ 'craft-fluid' if location.crafted_item in liquids else 'craft-item' }}", diff --git a/worlds/factorio/data/mod_template/data.lua b/worlds/factorio/data/mod_template/data.lua index 43151ff00840..562999d5df66 100644 --- a/worlds/factorio/data/mod_template/data.lua +++ b/worlds/factorio/data/mod_template/data.lua @@ -13,7 +13,6 @@ end local energy_bridge = table.deepcopy(data.raw["accumulator"]["accumulator"]) energy_bridge.name = "ap-energy-bridge" energy_bridge.minable.result = "ap-energy-bridge" -energy_bridge.localised_name = "Archipelago EnergyLink Bridge" energy_bridge.energy_source.buffer_capacity = "50MJ" energy_bridge.energy_source.input_flow_limit = "10MW" energy_bridge.energy_source.output_flow_limit = "10MW" @@ -25,7 +24,6 @@ data.raw["accumulator"]["ap-energy-bridge"] = energy_bridge local energy_bridge_item = table.deepcopy(data.raw["item"]["accumulator"]) energy_bridge_item.name = "ap-energy-bridge" -energy_bridge_item.localised_name = "Archipelago EnergyLink Bridge" energy_bridge_item.place_result = energy_bridge.name tint_icon(energy_bridge_item, energy_bridge_tint()) data.raw["item"]["ap-energy-bridge"] = energy_bridge_item @@ -35,7 +33,6 @@ energy_bridge_recipe.name = "ap-energy-bridge" energy_bridge_recipe.results = { {type = "item", name = energy_bridge_item.name, amount = 1} } energy_bridge_recipe.energy_required = 1 energy_bridge_recipe.enabled = {% if energy_link %}true{% else %}false{% endif %} -energy_bridge_recipe.localised_name = "Archipelago EnergyLink Bridge" data.raw["recipe"]["ap-energy-bridge"] = energy_bridge_recipe data.raw["map-gen-presets"].default["archipelago"] = {{ dict_to_lua({"default": False, "order": "a", "basic_settings": world_gen["basic"], "advanced_settings": world_gen["advanced"]}) }} diff --git a/worlds/factorio/data/mod_template/locale/en/locale.cfg b/worlds/factorio/data/mod_template/locale/en/locale.cfg deleted file mode 100644 index 59dcffcd6300..000000000000 --- a/worlds/factorio/data/mod_template/locale/en/locale.cfg +++ /dev/null @@ -1,31 +0,0 @@ -[map-gen-preset-name] -archipelago=Archipelago - -[map-gen-preset-description] -archipelago=World preset created by the Archipelago Randomizer. World may or may not contain actual archipelagos. - -[technology-name] -{% for location, item in locations %} -{%- if location.revealed %} -ap-{{ location.address }}-={{ player_names[item.player] }}'s {{ item.name }} ({{ location.name }}) -{%- else %} -ap-{{ location.address }}-= {{location.name}} -{%- endif -%} -{% endfor %} - -[technology-description] -{% for location, item in locations %} -{%- if location.revealed %} -ap-{{ location.address }}-=Researching this technology sends {{ item.name }} to {{ player_names[item.player] }}{% if item.advancement %}, which is considered a logical advancement{% elif item.useful %}, which is considered useful{% elif item.trap %}, which is considered fun{% endif %}. -{%- elif tech_tree_information == 1 and item.advancement %} -ap-{{ location.address }}-=Researching this technology sends something to someone, which is considered a logical advancement. -{%- else %} -ap-{{ location.address }}-=Researching this technology sends something to someone. -{%- endif -%} -{% endfor %} - -[mod-setting-name] -archipelago-death-link-{{ slot_player }}-{{ seed_name }}=Death Link - -[mod-setting-description] -archipelago-death-link-{{ slot_player }}-{{ seed_name }}=Kill other players in the same Archipelago Multiworld that also have Death Link turned on, when you die. \ No newline at end of file diff --git a/worlds/factorio/data/mod_template/settings.lua b/worlds/factorio/data/mod_template/settings.lua index 41d30e58d552..c88c4325631c 100644 --- a/worlds/factorio/data/mod_template/settings.lua +++ b/worlds/factorio/data/mod_template/settings.lua @@ -21,6 +21,8 @@ data:extend({ type = "bool-setting", name = "archipelago-death-link-{{ slot_player }}-{{ seed_name }}", setting_type = "runtime-global", + localised_name = {"mod-setting-name.archipelago-death-link"}, + localised_description = {"mod-setting-description.archipelago-death-link"}, {% if death_link %} default_value = true {% else %} From d241829585df0bbce179e7e7a97bf78fbe0ed392 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 31 May 2026 23:55:57 +0200 Subject: [PATCH 60/66] Bump mistune from 3.2.0 to 3.2.1 in /WebHostLib (#6185) Bumps [mistune](https://github.com/lepture/mistune) from 3.2.0 to 3.2.1. - [Release notes](https://github.com/lepture/mistune/releases) - [Changelog](https://github.com/lepture/mistune/blob/main/docs/changes.rst) - [Commits](https://github.com/lepture/mistune/compare/v3.2.0...v3.2.1) --- updated-dependencies: - dependency-name: mistune dependency-version: 3.2.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- WebHostLib/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt index fd194f223221..28d6760db024 100644 --- a/WebHostLib/requirements.txt +++ b/WebHostLib/requirements.txt @@ -10,5 +10,5 @@ Flask-Cors==6.0.2 bokeh==3.8.2 markupsafe==3.0.3 setproctitle==1.3.7 -mistune==3.2.0 +mistune==3.2.1 docutils==0.22.4 From 65277a50ce725a4847e3db1d47c73e96e59a7fca Mon Sep 17 00:00:00 2001 From: Justus Lind Date: Tue, 2 Jun 2026 06:38:23 +1000 Subject: [PATCH 61/66] Muse Dash: Update to Touhou Mugakudan -V- (#6234) --- worlds/musedash/MuseDashData.py | 7 +++++++ worlds/musedash/archipelago.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/worlds/musedash/MuseDashData.py b/worlds/musedash/MuseDashData.py index 70dae6b9a4d1..6b7bbbfe7b6c 100644 --- a/worlds/musedash/MuseDashData.py +++ b/worlds/musedash/MuseDashData.py @@ -719,4 +719,11 @@ "YURUSHITE": SongData(2900843, "97-3", "DASH AND SHOOT!", False, 6, 8, 11), "girls.exe": SongData(2900844, "97-4", "DASH AND SHOOT!", False, 6, 8, 11), "Baqeela": SongData(2900845, "97-5", "DASH AND SHOOT!", False, 6, 8, 10), + "Help me, ERINNNNNN!!": SongData(2900846, "43-74", "MD Plus Project", False, 4, 8, 10), + "Utakata, Ai no Mahoroba": SongData(2900847, "98-0", "Touhou Mugakudan -V-", False, 3, 5, 7), + "dynamite": SongData(2900848, "98-1", "Touhou Mugakudan -V-", False, 5, 7, 9), + "Matsuyoi Nightbug": SongData(2900849, "98-2", "Touhou Mugakudan -V-", False, 5, 7, 10), + "Coooonsultant!": SongData(2900850, "98-3", "Touhou Mugakudan -V-", False, 6, 8, 10), + "Stop at the affected part and melt quickly - Madness Udine Quarter": SongData(2900851, "98-4", "Touhou Mugakudan -V-", False, 4, 6, 10), + "Ultimate taste": SongData(2900852, "98-5", "Touhou Mugakudan -V-", False, 6, 8, 11), } \ No newline at end of file diff --git a/worlds/musedash/archipelago.json b/worlds/musedash/archipelago.json index 49114bcf9e87..4f32398442ec 100644 --- a/worlds/musedash/archipelago.json +++ b/worlds/musedash/archipelago.json @@ -1,6 +1,6 @@ { "game": "Muse Dash", "authors": ["DeamonHunter"], - "world_version": "1.5.32", + "world_version": "1.5.33", "minimum_ap_version": "0.6.3" } \ No newline at end of file From ad0af18f5173c9085cd4850d354bb82c689c3097 Mon Sep 17 00:00:00 2001 From: Ishigh1 Date: Tue, 2 Jun 2026 09:10:43 +0200 Subject: [PATCH 62/66] Rule Builder: Resolve empty and() to True_() instead of False_()(#6239) --- 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 d940eeb386ff..a156848ad668 100644 --- a/rule_builder/rules.py +++ b/rule_builder/rules.py @@ -593,7 +593,7 @@ def from_resolved(cls, world: TWorld, children_to_process: list[Rule.Resolved]) clauses.append(child) if not clauses and not items: - return true_rule or False_().resolve(world) + return true_rule or True_().resolve(world) if len(items) == 1: item, count = next(iter(items.items())) diff --git a/test/general/test_rule_builder.py b/test/general/test_rule_builder.py index 191ba3cba718..e78d62ad406f 100644 --- a/test/general/test_rule_builder.py +++ b/test/general/test_rule_builder.py @@ -159,6 +159,14 @@ def get_filler_item_name(self) -> str: @classvar_matrix( rules=( + ( + And(), + True_.Resolved(player=1) + ), + ( + Or(), + False_.Resolved(player=1) + ), ( And(Has("A", 1), Has("A", 2)), Has.Resolved("A", 2, player=1), From 2a8ebe23991d5337b09a17d0e197f32d32245e31 Mon Sep 17 00:00:00 2001 From: Ishigh1 Date: Wed, 3 Jun 2026 08:20:04 +0200 Subject: [PATCH 63/66] Core+RB: Make has_from_list, has_from_list_unique, has_group and has_group_unique return True if count <= 0 and list is empty (#6240) --- BaseClasses.py | 8 ++++++++ rule_builder/rules.py | 23 +++++++++++++++++------ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 69b900212c50..33e4fb041cf9 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1038,6 +1038,8 @@ def count(self, item: str, player: int) -> int: def has_from_list(self, items: Iterable[str], player: int, count: int) -> bool: """Returns True if the state contains at least `count` items matching any of the item names from a list.""" + if count <= 0: + return True found: int = 0 player_prog_items = self.prog_items[player] for item_name in items: @@ -1049,6 +1051,8 @@ def has_from_list(self, items: Iterable[str], player: int, count: int) -> bool: def has_from_list_unique(self, items: Iterable[str], player: int, count: int) -> bool: """Returns True if the state contains at least `count` items matching any of the item names from a list. Ignores duplicates of the same item.""" + if count <= 0: + return True found: int = 0 player_prog_items = self.prog_items[player] for item_name in items: @@ -1077,6 +1081,8 @@ def count_from_list_unique(self, items: Iterable[str], player: int) -> int: # item name group related def has_group(self, item_name_group: str, player: int, count: int = 1) -> bool: """Returns True if the state contains at least `count` items present in a specified item group.""" + if count <= 0: + return True found: int = 0 player_prog_items = self.prog_items[player] for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]: @@ -1089,6 +1095,8 @@ def has_group_unique(self, item_name_group: str, player: int, count: int = 1) -> """Returns True if the state contains at least `count` items present in a specified item group. Ignores duplicates of the same item. """ + if count <= 0: + return True found: int = 0 player_prog_items = self.prog_items[player] for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]: diff --git a/rule_builder/rules.py b/rule_builder/rules.py index a156848ad668..0606225ffa03 100644 --- a/rule_builder/rules.py +++ b/rule_builder/rules.py @@ -1403,14 +1403,16 @@ def __init__( @override def _instantiate(self, world: TWorld) -> Rule.Resolved: + count = resolve_field(self.count, world, int) + if count <= 0: + return True_().resolve(world) if len(self.item_names) == 0: - # match state.has_from_list return False_().resolve(world) if len(self.item_names) == 1: return Has(self.item_names[0], self.count).resolve(world) return self.Resolved( self.item_names, - count=resolve_field(self.count, world, int), + count=count, player=world.player, caching_enabled=getattr(world, "rule_caching_enabled", False), ) @@ -1538,8 +1540,9 @@ def __init__( @override def _instantiate(self, world: TWorld) -> Rule.Resolved: 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 + if count <= 0: + return True_().resolve(world) + if len(self.item_names) < count: return False_().resolve(world) if len(self.item_names) == 1: return Has(self.item_names[0]).resolve(world) @@ -1657,11 +1660,14 @@ class HasGroup(Rule[TWorld], game="Archipelago"): @override def _instantiate(self, world: TWorld) -> Rule.Resolved: + count = resolve_field(self.count, world, int) + if count <= 0: + return True_().resolve(world) item_names = tuple(sorted(world.item_name_groups[self.item_name_group])) return self.Resolved( self.item_name_group, item_names, - count=resolve_field(self.count, world, int), + count=count, player=world.player, caching_enabled=getattr(world, "rule_caching_enabled", False), ) @@ -1731,11 +1737,16 @@ class HasGroupUnique(Rule[TWorld], game="Archipelago"): @override def _instantiate(self, world: TWorld) -> Rule.Resolved: + count = resolve_field(self.count, world, int) + if count <= 0: + return True_().resolve(world) item_names = tuple(sorted(world.item_name_groups[self.item_name_group])) + if len(item_names) < count: + return False_().resolve(world) return self.Resolved( self.item_name_group, item_names, - count=resolve_field(self.count, world, int), + count=count, player=world.player, caching_enabled=getattr(world, "rule_caching_enabled", False), ) From 09aba95c039f2d304c15466b7cff6f0ee8f12aa8 Mon Sep 17 00:00:00 2001 From: CosmicWolf <54233835+Enderdraak@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:53:37 +0200 Subject: [PATCH 64/66] Factorio: Fixed a needlessly small bug that does not affect gameplay (#6147) --- worlds/factorio/data/mod_template/control.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua index 060fe56431c4..3146e8042de2 100644 --- a/worlds/factorio/data/mod_template/control.lua +++ b/worlds/factorio/data/mod_template/control.lua @@ -777,7 +777,7 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi if index == nil then game.print("ap-get-technology is only to be used by the Archipelago Factorio Client") return - elseif index == -1 then -- for coop sync and restoring from an older savegame + elseif index == "-1" then -- for coop sync and restoring from an older savegame tech = force.technologies[item_name] if tech.researched ~= true then game.print({"archipelago.receive-ap-catchup", "[technology=" .. tech.name .. "]"}) From 5ccef9802ada04b27b92e84be24c9ebbe4d0906d Mon Sep 17 00:00:00 2001 From: Emily <35015090+EmilyV99@users.noreply.github.com> Date: Sun, 7 Jun 2026 12:21:30 -0400 Subject: [PATCH 65/66] WebHost: fix Authors on supported game page (#6218) --- WebHostLib/static/styles/supportedGames.css | 4 ++++ WebHostLib/templates/supportedGames.html | 10 +++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/WebHostLib/static/styles/supportedGames.css b/WebHostLib/static/styles/supportedGames.css index ab12f320716b..9458437c4343 100644 --- a/WebHostLib/static/styles/supportedGames.css +++ b/WebHostLib/static/styles/supportedGames.css @@ -42,3 +42,7 @@ #games .page-controls button{ margin-left: 0.5rem; } + +#games .author-label{ + margin-top: 0.5rem; +} diff --git a/WebHostLib/templates/supportedGames.html b/WebHostLib/templates/supportedGames.html index 128278c4b277..01f7fa2cb99d 100644 --- a/WebHostLib/templates/supportedGames.html +++ b/WebHostLib/templates/supportedGames.html @@ -49,6 +49,13 @@

Currently Supported Games

{{ game_name }} {{ world.__doc__ | default("No description provided.", true) }}
+ {% if "authors" in world.manifest %} + {% if world.manifest["authors"]|length == 1 %} +

Author: {{ world.manifest["authors"][0] }}

+ {% else %} +

Authors: {{ world.manifest["authors"] | join(", ") }}

+ {% endif %} + {% endif %} Game Page {% if world.web.tutorials %} | @@ -68,9 +75,6 @@

Currently Supported Games

Report a Bug {% endif %}
- {% if "authors" in world.manifest %} -

Authors: {{ world.manifest["authors"] | join(", ") }}

- {% endif %} {% endfor %} {% endblock %} From e6e0bc30420d13085f6f86b29ffa7e1594a8c46a Mon Sep 17 00:00:00 2001 From: Uriel Date: Sat, 20 Jun 2026 11:36:18 -0400 Subject: [PATCH 66/66] WebHost: Config option for custom port ranges v2 (#6009) * Added ability to define custom port ranges the WebHost will use for game servers, instead of pure random. * - Added better fallback to default port range when a custom range fails - Updated config to be clearer * Added ability to define custom port ranges the WebHost will use for game servers, instead of pure random. * - Added better fallback to default port range when a custom range fails - Updated config to be clearer * Updated soft-fail message * Removed dead import from customserver.py * Update requirements.txt Settings requirements to main core branch * fix what reviewers said and add some improvements * remove unused argument * try fixing test with try * use yaml lists instead of string for config * fix value type bug on ephemeral type * reuse sockets with websockets api instead of opening and closing them * add used ports cache and filter used ports when looking for ports * fix port randomizer * Apply suggestions from code review Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com> * fix some reviews * use weights for random port and remove more-itertools * fix net_connections not working on macOS * rename variables and functions * lazy init `get_used_ports` * change `game_ports` to be `tuple` * fix last_used_ports not being updated locally * fix random choices and move game_port conversion into tuple * Apply suggestions from code review Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com> * use a named tuple on parse_game_ports * only use ranges * do it the duck way * this should check all usable ports before failing * fix while loop * add return type to weighted random * Update WebHostLib/customserver.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * simplify tuple conversion check * add tests * reformat file and change `create_random_port_socket` test * add more test cases for parse_game_ports * try to prevent busy-looping on create random port socket when doing test * simplify parse game port tests to one assertListEqual * make the range lesser for port test * reduce range on macOS * Apply suggestions from code review Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Update WebHostLib/customserver.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Doug Hoskisson * remove unused import * Update WebHostLib/customserver.py Co-authored-by: Doug Hoskisson * use generator expressions * check for 0-tuple * use some kind of shuffled queue * update tests * refactor new port handling into a class (#1) * change time to monotonic * Update docs/webhost configuration sample.yaml Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * add psutil 7.2.2 as requirement * Update WebHostLib/requirements.txt Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --------- Co-authored-by: Lexipherous Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com> Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: Doug Hoskisson --- WebHostLib/__init__.py | 1 + WebHostLib/autolauncher.py | 3 +- WebHostLib/customserver.py | 135 +++++++++++++++++++++---- WebHostLib/requirements.txt | 1 + docs/webhost configuration sample.yaml | 6 ++ test/webhost/test_port_allocation.py | 89 ++++++++++++++++ 6 files changed, 215 insertions(+), 20 deletions(-) create mode 100644 test/webhost/test_port_allocation.py diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index b3dc203a3de0..90bd0c2fddc6 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -42,6 +42,7 @@ app.config["SELFLAUNCHCERT"] = None # can point to a SSL Certificate to encrypt Room websocket connections app.config["SELFLAUNCHKEY"] = None # can point to a SSL Certificate Key to encrypt Room websocket connections app.config["SELFGEN"] = True # application process is in charge of scheduling Generations. +app.config["GAME_PORTS"] = ["49152-65535", 0] # at what amount of worlds should scheduling be used, instead of rolling in the web-thread 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. diff --git a/WebHostLib/autolauncher.py b/WebHostLib/autolauncher.py index 165e08a103fb..d101f149ef0b 100644 --- a/WebHostLib/autolauncher.py +++ b/WebHostLib/autolauncher.py @@ -193,6 +193,7 @@ def __init__(self, config: dict, id: int): self.cert = config["SELFLAUNCHCERT"] self.key = config["SELFLAUNCHKEY"] self.host = config["HOST_ADDRESS"] + self.game_ports = config["GAME_PORTS"] self.rooms_to_start = multiprocessing.Queue() self.rooms_shutting_down = multiprocessing.Queue() self.name = f"MultiHoster{id}" @@ -203,7 +204,7 @@ def start(self): process = multiprocessing.Process(group=None, target=run_server_process, args=(self.name, self.ponyconfig, get_static_server_data(), - self.cert, self.key, self.host, + self.cert, self.key, self.host, self.game_ports, self.rooms_to_start, self.rooms_shutting_down), name=self.name) process.start() diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index 4257c6aff3e4..85a720169e4c 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -4,6 +4,7 @@ import collections import datetime import functools +import itertools import logging import multiprocessing import pickle @@ -13,7 +14,9 @@ import time import typing import sys +from collections.abc import Iterable +import psutil import websockets from pony.orm import commit, db_session, select @@ -24,6 +27,7 @@ server_per_message_deflate_factory, ) from Utils import restricted_loads, cache_argsless + from .locker import Locker from .models import Command, GameDataPackage, Room, db @@ -76,12 +80,8 @@ def __init__(self, static_server_data: dict, logger: logging.Logger): self.tags = ["AP", "WebHost"] def __del__(self): - try: - import psutil - from Utils import format_SI_prefix - self.logger.debug(f"Context destroyed, Mem: {format_SI_prefix(psutil.Process().memory_info().rss, 1024)}iB") - except ImportError: - self.logger.debug("Context destroyed") + from Utils import format_SI_prefix + self.logger.debug(f"Context destroyed, Mem: {format_SI_prefix(psutil.Process().memory_info().rss, 1024)}iB") def _load_game_data(self): for key, value in self.static_server_data.items(): @@ -115,7 +115,7 @@ def load(self, room_id: int): if room.last_port: self.port = room.last_port else: - self.port = get_random_port() + self.port = 0 multidata = self.decompress(room.seed.multidata) game_data_packages = {} @@ -181,8 +181,97 @@ def get_save(self) -> dict: return d -def get_random_port(): - return random.randint(49152, 65535) +class GameRangePorts(typing.NamedTuple): + valid_ports: list[int] + ephemeral_allowed: bool + + +class RandomPortSocketCreator: + """ Creates server sockets on random available ports from a configured range. """ + + _next_port_index: int + _used_ports_cache: tuple[frozenset[int], int] | None + _parsed_ports: GameRangePorts + + def __init__(self, game_ports: Iterable[str | int]) -> None: + self._next_port_index = 0 + self._used_ports_cache = None + self._parsed_ports = self._parse_game_ports(game_ports) + + @staticmethod + def _parse_game_ports(game_ports: Iterable[str | int]) -> GameRangePorts: + """ Parse the game ports configuration into a structured format. """ + valid_ports: list[int] = [] + ephemeral_allowed = False + + for item in game_ports: + if isinstance(item, str) and "-" in item: + start, end = map(int, item.split("-")) + x = range(start, end + 1) + valid_ports.extend(x) + elif int(item) == 0: + ephemeral_allowed = True + else: + valid_ports.append(int(item)) + + random.shuffle(valid_ports) + return GameRangePorts(valid_ports, ephemeral_allowed) + + @staticmethod + def _try_conns_per_process(p: psutil.Process) -> Iterable[int]: + """ Get ports from a single process's connections. """ + try: + return (c.laddr.port for c in p.net_connections("tcp4") if c.laddr) + except psutil.AccessDenied: + return () + + @staticmethod + def _get_active_net_connections() -> Iterable[int]: + """ Get all active TCP4 connections on the system. """ + # Don't even try to check if system using AIX + if psutil.AIX: + return () + + try: + return (c.laddr.port for c in psutil.net_connections("tcp4") if c.laddr) + # raises AccessDenied when done on macOS + except psutil.AccessDenied: + # flatten the list of iterables + return itertools.chain.from_iterable(map( + RandomPortSocketCreator._try_conns_per_process, + psutil.process_iter(["net_connections"]) + )) + + def _get_used_ports(self) -> frozenset[int]: + """ Get currently used ports with 90-second caching. """ + t_hash = round(time.monotonic() / 90) + if self._used_ports_cache is None or self._used_ports_cache[1] != t_hash: + self._used_ports_cache = (frozenset(self._get_active_net_connections()), t_hash) + + return self._used_ports_cache[0] + + def create(self, host: str) -> socket.socket: + """ Create a server socket on an available port. """ + valid_ports, ephemeral_allowed = self._parsed_ports + used_ports = self._get_used_ports() + + next_index = self._next_port_index + for i, port in enumerate(itertools.chain(valid_ports[next_index:], valid_ports[:next_index])): + if port in used_ports: + continue + + try: + res = socket.create_server((host, port)) + next_index = (next_index + i + 1) % len(valid_ports) + self._next_port_index = next_index + return res + except OSError: + pass + + if ephemeral_allowed: + return socket.create_server((host, 0)) + + raise OSError(98, "No available ports") @cache_argsless @@ -247,7 +336,8 @@ def tear_down_logging(room_id): 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): + host: str, game_ports: Iterable[str | int], + rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue): from setproctitle import setproctitle setproctitle(name) @@ -291,6 +381,7 @@ def get_ssl_context(): gc.collect() # free intermediate objects used during setup loop = asyncio.get_event_loop() + socket_creator = RandomPortSocketCreator(game_ports) async def start_room(room_id): with Locker(f"RoomLocker {room_id}"): @@ -300,20 +391,26 @@ async def start_room(room_id): ctx.load(room_id) ctx.init_save() assert ctx.server is None - try: + if ctx.port != 0: + try: + ctx.server = websockets.serve( + functools.partial(server, ctx=ctx), + ctx.host, + ctx.port, + ssl=get_ssl_context(), + extensions=[server_per_message_deflate_factory], + ) + await ctx.server + except OSError: + ctx.port = 0 + if ctx.port == 0: ctx.server = websockets.serve( functools.partial(server, ctx=ctx), - ctx.host, - ctx.port, + sock=socket_creator.create(ctx.host), ssl=get_ssl_context(), extensions=[server_per_message_deflate_factory], ) await ctx.server - except OSError: # likely port in use - ctx.server = websockets.serve( - functools.partial(server, ctx=ctx), ctx.host, 0, ssl=get_ssl_context()) - - await ctx.server port = 0 for wssocket in ctx.server.ws_server.sockets: socketname = wssocket.getsockname() @@ -388,7 +485,7 @@ def _done(self, task: asyncio.Future): def run(self): while 1: - next_room = rooms_to_run.get(block=True, timeout=None) + next_room = rooms_to_run.get(block=True, timeout=None) gc.collect() task = asyncio.run_coroutine_threadsafe(start_room(next_room), loop) self._tasks.append(task) diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt index 28d6760db024..886d759d9c97 100644 --- a/WebHostLib/requirements.txt +++ b/WebHostLib/requirements.txt @@ -12,3 +12,4 @@ markupsafe==3.0.3 setproctitle==1.3.7 mistune==3.2.1 docutils==0.22.4 +psutil==7.2.2 diff --git a/docs/webhost configuration sample.yaml b/docs/webhost configuration sample.yaml index 93094f1ce73f..a250e8eae7f5 100644 --- a/docs/webhost configuration sample.yaml +++ b/docs/webhost configuration sample.yaml @@ -17,6 +17,12 @@ # Web hosting port #PORT: 80 +# Ports used for game hosting. Values can be specific ports, port ranges or both. Default is: [49152-65535, 0] +# Zero means it will use a random free port if there is no free port in the ranges specified +# Examples of valid values: [40000-41000, 49152-65535] +# If ports within the range(s) are already in use, the WebHost will fallback to the default [49152-65535, 0] range. +#GAME_PORTS: [49152-65535, 0] + # Place where uploads go. #UPLOAD_FOLDER: uploads diff --git a/test/webhost/test_port_allocation.py b/test/webhost/test_port_allocation.py new file mode 100644 index 000000000000..914e7b2c873d --- /dev/null +++ b/test/webhost/test_port_allocation.py @@ -0,0 +1,89 @@ +import os +import unittest +from socket import socket as Socket # noqa: N812 + +from Utils import is_macos +from WebHostLib.customserver import RandomPortSocketCreator + +ci = bool(os.environ.get("CI")) + + +class TestPortAllocating(unittest.TestCase): + def test_parse_game_ports(self) -> None: + """Ensure that game ports with ranges are parsed correctly""" + val = RandomPortSocketCreator._parse_game_ports(("1000-2000", "2000-5000", "1000-2000", 20, 40, "20", "0")) + + self.assertCountEqual(val.valid_ports, + [*range(1000, 2001), *range(2000, 5001), *range(1000, 2001), 20, 40, 20], + "The parsed game ports are not the expected length") + self.assertTrue(val.ephemeral_allowed, "The ephemeral allowed flag is not set even though it was passed") + + val = RandomPortSocketCreator._parse_game_ports(()) + self.assertListEqual(val.valid_ports, [], "Empty list of game port returned something") + self.assertFalse(val.ephemeral_allowed, "Empty list returned that ephemeral is allowed") + + val = RandomPortSocketCreator._parse_game_ports((0,)) + self.assertListEqual(val.valid_ports, [], "Empty list of ranges returned something") + self.assertTrue(val.ephemeral_allowed, "List with just 0 is not allowing ephemeral ports") + + val = RandomPortSocketCreator._parse_game_ports((1,)) + self.assertListEqual(val.valid_ports, [1], "Valid ports doesn't contain the expected values") + self.assertFalse(val.ephemeral_allowed, "List with just single port returned that ephemeral is allowed") + + def test_parse_game_port_errors(self) -> None: + """Ensure that game ports with incorrect values raise the expected error""" + with self.assertRaises(ValueError, msg="Negative numbers didn't get interpreted as an invalid range"): + RandomPortSocketCreator._parse_game_ports(tuple("-50215")) + with self.assertRaises(ValueError, msg="Text got interpreted as a valid number"): + RandomPortSocketCreator._parse_game_ports(tuple("dwafawg")) + with self.assertRaises( + ValueError, + msg="A range with an extra dash at the end didn't get interpreted as an invalid number because of it's end dash" + ): + RandomPortSocketCreator._parse_game_ports(tuple("20-21215-")) + with self.assertRaises(ValueError, msg="Text got interpreted as a valid number for the start of a range"): + RandomPortSocketCreator._parse_game_ports(tuple("f-21215")) + + def test_random_port_socket_edge_cases(self) -> None: + """Verify if edge cases on creation of random port socket is working fine""" + # Try giving an empty tuple and fail over it + creator = RandomPortSocketCreator(()) + with self.assertRaises(OSError) as err: + creator.create("127.0.0.1") + self.assertEqual(err.exception.errno, 98, "Raised an unexpected error code") + self.assertEqual(err.exception.strerror, "No available ports", "Raised an unexpected error string") + + # Try only having ephemeral ports enabled + creator = RandomPortSocketCreator(("0",)) + try: + creator.create("127.0.0.1").close() + except OSError as err: + self.assertEqual(err.errno, 98, "Raised an unexpected error code") + # If it returns our error string that means something is wrong with our code + self.assertNotEqual(err.strerror, "No available ports", + "Raised an unexpected error string") + + @unittest.skipUnless(ci, "can't guarantee free ports outside of CI") + def test_random_port_socket(self) -> None: + """Verify if returned sockets use the correct port ranges""" + creator = RandomPortSocketCreator(("8080-8085",)) + sockets: list[Socket] = [] + for _ in range(6): + socket = creator.create("127.0.0.1") + sockets.append(socket) + _, port = socket.getsockname() + self.assertIn(port, range(8080, 8086), "Port of socket was not inside the expected range") + for s in sockets: + s.close() + + sockets.clear() + creator = RandomPortSocketCreator(("30000-65535",)) + length = 5_000 if is_macos else (30_000 - len(creator._get_used_ports())) + for _ in range(length): + socket = creator.create("127.0.0.1") + sockets.append(socket) + _, port = socket.getsockname() + self.assertIn(port, range(30_000, 65536), "Port of socket was not inside the expected range") + + for s in sockets: + s.close()