From 4aab317665d5d2d9a72d2bc5ea9446639eaf887e Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Mon, 9 Sep 2024 08:56:15 -0500 Subject: [PATCH 01/94] ALTTP: Plando (#2904) fixes (#3834) --- Options.py | 14 ++++++++++++++ worlds/alttp/Options.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Options.py b/Options.py index ecde6275f1ea..b79714635d9e 100644 --- a/Options.py +++ b/Options.py @@ -973,7 +973,19 @@ def from_any(cls, data: PlandoTextsFromAnyType) -> Self: if random.random() < float(text.get("percentage", 100)/100): at = text.get("at", None) if at is not None: + if isinstance(at, dict): + if at: + at = random.choices(list(at.keys()), + weights=list(at.values()), k=1)[0] + else: + raise OptionError("\"at\" must be a valid string or weighted list of strings!") given_text = text.get("text", []) + if isinstance(given_text, dict): + if not given_text: + given_text = [] + else: + given_text = random.choices(list(given_text.keys()), + weights=list(given_text.values()), k=1) if isinstance(given_text, str): given_text = [given_text] texts.append(PlandoText( @@ -981,6 +993,8 @@ def from_any(cls, data: PlandoTextsFromAnyType) -> Self: given_text, text.get("percentage", 100) )) + else: + raise OptionError("\"at\" must be a valid string or weighted list of strings!") elif isinstance(text, PlandoText): if random.random() < float(text.percentage/100): texts.append(text) diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index 20dd18038a14..bd87cbf2c3ea 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -728,7 +728,7 @@ class ALttPPlandoConnections(PlandoConnections): entrances = set([connection[0] for connection in ( *default_connections, *default_dungeon_connections, *inverted_default_connections, *inverted_default_dungeon_connections)]) - exits = set([connection[1] for connection in ( + exits = set([connection[0] for connection in ( *default_connections, *default_dungeon_connections, *inverted_default_connections, *inverted_default_dungeon_connections)]) From 09c7f5f909e6ca23de5bc58f8174a1794b07f817 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Mon, 9 Sep 2024 17:36:27 +0200 Subject: [PATCH 02/94] The Witness: Bump Required Client Version (#3891) The newest release of the Witness client connects with 0.5.1 https://github.com/NewSoupVi/The-Witness-Randomizer-for-Archipelago/releases/tag/7.0.0p10 --- worlds/witness/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index cdb17a483b1e..b4b38c883e7d 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -61,7 +61,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, 4, 5) + required_client_version = (0, 5, 1) player_logic: WitnessPlayerLogic player_locations: WitnessPlayerLocations From 170aedba8fbe35765289a8628b89acf9fd1515ec Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Mon, 9 Sep 2024 17:36:47 +0200 Subject: [PATCH 03/94] The Witness: Fix hints always displaying the Witness player (#3861) * The Witness: Fix hints always displaying the Witness player Got a bit too trigger happy with changing instances of `world.multiworld.player_name` to `world.player_name` - Some of these were actually *supposed* to be other players. Alternate title: The Witness doesn't have a Silph Scope * that one i guess --- worlds/witness/hints.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index 2c5f816b2bc2..99e8eea2eb89 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -220,7 +220,7 @@ def try_getting_location_group_for_location(world: "WitnessWorld", hint_loc: Loc def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> WitnessWordedHint: location_name = hint.location.name if hint.location.player != world.player: - location_name += " (" + world.player_name + ")" + location_name += " (" + world.multiworld.get_player_name(hint.location.player) + ")" item = hint.location.item @@ -229,7 +229,7 @@ def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> Witnes item_name = item.name if item.player != world.player: - item_name += " (" + world.player_name + ")" + item_name += " (" + world.multiworld.get_player_name(item.player) + ")" hint_text = "" area: Optional[str] = None From 7ff201e32c859eeb1b3e07ee087f11da3249f833 Mon Sep 17 00:00:00 2001 From: Spineraks Date: Tue, 10 Sep 2024 17:01:36 +0200 Subject: [PATCH 04/94] Yacht Dice: add get_filler_item_name (#3916) * Add the yacht dice (from other git) world to the yacht dice fork * Update .gitignore * Removed zillion because it doesn't work * Update .gitignore * added zillion again... * Now you can have 0 extra fragments * Added alt categories, also options * Added item categories * Extra categories are now working! :dog: * changed options and added exceptions * Testing if I change the generate.py * Revert "Testing if I change the generate.py" This reverts commit 7c2b3df6170dcf8d8f36a1de9fcbc9dccdec81f8. * ignore gitignore * Delete .gitignore * Update .gitignore * Update .gitignore * Update logic, added multiplicative categories * Changed difficulties * Update offline mode so that it works again * Adjusted difficulty * New version of the apworld, with 1000 as final score, always Will still need to check difficulty and weights of adding items. Website is not ready yet, so this version is not usable yet :) * Changed yaml and small bug fixes Fix when goal and max are same Options: changed chance to weight * no changes, just whitespaces * changed how logic works Now you put an array of mults and the cpu gets a couple of tries * Changed logic, tweaked a bit too * Preparation for 2.0 * logic tweak * Logic for alt categories properly now * Update setup_en.md * Update en_YachtDice.md * Improve performance of add_distributions * Formatting style * restore gitignore to APMW * Tweaked generation parameters and methods * Version 2.0.3 manual input option max score in logic always 2.0.3 faster gen * Comments and editing * Renamed setup guide * Improved create_items code * init of locations: remove self.event line * Moved setting early items to generate_early * Add my name to CODEOWNERS * Added Yacht Dice to the readme in list of games * Improve performance of Yacht Dice * newline * Improve typing * This is actually just slower lol * Update worlds/yachtdice/Items.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update Options.py * Styling * finished text whichstory option * removed roll and rollfragments; not used * import; worlds not world :) * Option groups! * ruff styling, fix * ruff format styling! * styling and capitalization of options * small comment * Cleaned up the "state_is_a_list" a little bit * RUFF :dog: * Changed filling the itempool for efficiency Now, we start with 17 extra items in the item pool, it's quite likely you need at least 17 items (~80%?). And then afterwards, we delete items if we overshoot the target of 1000, and add items if we haven't reached an achievable score of 1000 yet. Also, no need to recompute the entire logic when adding points. * :dog: * Removed plando "fix" * Changed indent of score multiplier * faster location function * Comments to docstrings * fixed making location closest to goal_score be goal_score * options format * iterate keys and values of a dict together * small optimization ListState * faster collection of categories * return arguments instead of making a list (will :dog: later) * Instead of turning it into a tuple, you can just make a tuple literal * remove .keys() * change .random and used enumerate * some readability improvements * Remove location "0", we don't use that one * Remove lookup_id_to_name entirely I for sure don't use it, and as far as I know it's not one of the mandatory functions for AP, these are item_name_to_id and location_name_to_id. * .append instead of += for single items, percentile function changed Also an extra comment for location ids. * remove ) too many * Removed sorted from category list * Hash categories (which makes it slower :( ) Maybe I messed up or misunderstood... I'll revert this right away since it is 2x slower, probably because of sorted instead of sort? * Revert "Hash categories (which makes it slower :( )" This reverts commit 34f2c1aed8c8813b2d9c58896650b82a810d3578. * temporary push: 40% faster generation test Small changes in logic make the generation 40% faster. I'll have to think about how big the changes are. I suspect they are rather limited. If this is the way to go, I'll remove the temp file and redo the YachtWeights file, I'll remove the functions there and just put the new weights here. * Add Points item category * Reverse changes of bad idea :) * ruff :dog: * Use numpy and pmf function to speed up gen Numpy has a built-in way to sum probability mass functions (pmf). This shaves of 60% of the generation time :D * Revert "Use numpy and pmf function to speed up gen" This reverts commit 9290191cb323ae92321d6c2cfcfe8c27370f439b. * Step inbetween to change the weights * Changed the weights to make it faster 135 -> 81 seconds on 100 random yamls * Adjusted max_dist, split dice_simulation function * Removed nonlocal and pass arguments instead * Change "weight-lists" to Dict[str, float] * Removed the return from ini_locations. Also added explanations to cat_weights * Choice options; dont'use .value (will ruff later) * Only put important options in slotdata * :dog: * Add Dict import * Split the cache per player, limit size to 400. * :dog: * added , because of style * Update apworld version to 2.0.6 2.0.5 is the apworld I released on github to be tested I never separately released 2.0.4. * Multiple smaller code improvements - changed names in YachtWeights so we don't need to translate them in Rules anymore - we now remember which categories are present in the game, and also put this in slotdata. This we do because only one of two categories is present in a game. If for some reason both are present (plando/getitem/startinventory), we now know which category to ignore - * :dog: ruff * Mostly minimize_extra_items improvements - Change logic, generation is now even faster (0.6s per default yaml). - Made the option 'minimize_extra_items' do a lot more, hopefully this makes the impact of Yacht Dice a little bit less, if you want that. Here's what is also does now: - you start with 2 dice and 2 rolls - there will be less locations/items at the start of you game * ruff :dog: * Removed printing options * Reworded some option descriptions * Yacht Dice: setup: change release-link to latest On the installation page, link to the latest release, instead of the page with all releases * Several fixes and changes -change apworld version -Removed the extra roll (this was not intended) -change extra_points_added to a mutable list to that it actually does something -removed variables multipliers_added and items_added -Rules, don't order by quantity, just by mean_score -Changed the weights in general to make it faster * :dog: * Revert setup to what it was (latest, without S) * remove temp weights file, shouldn't be here * Made sure that there is not too many step score multipliers. Too many step score multipliers lead to gen fails too, probably because you need many categories for them to actually help a lot. So it's hard to use them at the start of the game. * add filler item name --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/yachtdice/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/worlds/yachtdice/__init__.py b/worlds/yachtdice/__init__.py index 3a79eff04046..d86ee3382d33 100644 --- a/worlds/yachtdice/__init__.py +++ b/worlds/yachtdice/__init__.py @@ -466,6 +466,9 @@ def create_regions(self): menu.exits.append(connection) connection.connect(board) self.multiworld.regions += [menu, board] + + def get_filler_item_name(self) -> str: + return "Good RNG" def set_rules(self): """ From 874392756b706bc07f4c1ff9429ed0b16e52abd3 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Wed, 11 Sep 2024 04:20:07 -0700 Subject: [PATCH 05/94] Pokemon Emerald: Add normalize encounter rate option to slot data (#3917) --- worlds/pokemon_emerald/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py index abdee26f572f..d281dde23cb0 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -711,6 +711,7 @@ def fill_slot_data(self) -> Dict[str, Any]: "trainersanity", "modify_118", "death_link", + "normalize_encounter_rates", ) slot_data["free_fly_location_id"] = self.free_fly_location_id slot_data["hm_requirements"] = self.hm_requirements From c9f1a21bd2b8888e5b4dc75123c19b0a016ee261 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Wed, 11 Sep 2024 04:22:04 -0700 Subject: [PATCH 06/94] BizHawkClient: Remove `run_gui` in favor of `make_gui` (#3910) --- CommonClient.py | 2 +- worlds/_bizhawk/context.py | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 911de4226dc3..6bdd8fc819da 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -662,7 +662,7 @@ def handle_connection_loss(self, msg: str) -> None: logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True}) self._messagebox_connection_loss = self.gui_error(msg, exc_info[1]) - def make_gui(self) -> type: + def make_gui(self) -> typing.Type["kvui.GameManager"]: """To return the Kivy App class needed for run_gui so it can be overridden before being built""" from kvui import GameManager diff --git a/worlds/_bizhawk/context.py b/worlds/_bizhawk/context.py index 234faf3b65cf..896c8fb7b504 100644 --- a/worlds/_bizhawk/context.py +++ b/worlds/_bizhawk/context.py @@ -59,14 +59,10 @@ def __init__(self, server_address: Optional[str], password: Optional[str]): self.bizhawk_ctx = BizHawkContext() self.watcher_timeout = 0.5 - def run_gui(self): - from kvui import GameManager - - class BizHawkManager(GameManager): - base_title = "Archipelago BizHawk Client" - - self.ui = BizHawkManager(self) - self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + def make_gui(self): + ui = super().make_gui() + ui.base_title = "Archipelago BizHawk Client" + return ui def on_package(self, cmd, args): if cmd == "Connected": From 7621889b8b626e89947d6258ddd5ade65d434ddb Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Wed, 11 Sep 2024 04:22:53 -0700 Subject: [PATCH 07/94] DS3: Add nex3 as a world maintainer (#3882) I've already discussed this with @Marechal-L and gotten his approval. --- docs/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 28dcc6736283..ee7fd7ed863b 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -46,7 +46,7 @@ /worlds/clique/ @ThePhar # Dark Souls III -/worlds/dark_souls_3/ @Marechal-L +/worlds/dark_souls_3/ @Marechal-L @nex3 # Donkey Kong Country 3 /worlds/dkc3/ @PoryGone From ed948e3e5b60ea67d126b7ef06a60dcccd71f4aa Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Fri, 13 Sep 2024 15:02:13 +0100 Subject: [PATCH 08/94] sm64ex: Add missing indirect condition for BitFS randomized entrance (#3926) The Bowser in the Fire Sea randomized entrance has an access rule that requires being able to reach "DDD: Board Bowser's Sub", but being able to reach a location also requires being able to reach the region that location is in, so an indirect condition is required. --- worlds/sm64ex/Regions.py | 4 ++-- worlds/sm64ex/Rules.py | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/worlds/sm64ex/Regions.py b/worlds/sm64ex/Regions.py index 6fc2d74b96dc..52126bcf9ff7 100644 --- a/worlds/sm64ex/Regions.py +++ b/worlds/sm64ex/Regions.py @@ -246,10 +246,10 @@ def create_regions(world: MultiWorld, options: SM64Options, player: int): regBitS.subregions = [bits_top] -def connect_regions(world: MultiWorld, player: int, source: str, target: str, rule=None): +def connect_regions(world: MultiWorld, player: int, source: str, target: str, rule=None) -> Entrance: sourceRegion = world.get_region(source, player) targetRegion = world.get_region(target, player) - sourceRegion.connect(targetRegion, rule=rule) + return sourceRegion.connect(targetRegion, rule=rule) def create_region(name: str, player: int, world: MultiWorld) -> SM64Region: diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py index 9add8d9b2932..1535f9ca1fde 100644 --- a/worlds/sm64ex/Rules.py +++ b/worlds/sm64ex/Rules.py @@ -92,9 +92,12 @@ def set_rules(world, options: SM64Options, player: int, area_connections: dict, connect_regions(world, player, "Hazy Maze Cave", randomized_entrances_s["Cavern of the Metal Cap"]) connect_regions(world, player, "Basement", randomized_entrances_s["Vanish Cap under the Moat"], rf.build_rule("GP")) - connect_regions(world, player, "Basement", randomized_entrances_s["Bowser in the Fire Sea"], - lambda state: state.has("Power Star", player, star_costs["BasementDoorCost"]) and - state.can_reach("DDD: Board Bowser's Sub", 'Location', player)) + entrance = connect_regions(world, player, "Basement", randomized_entrances_s["Bowser in the Fire Sea"], + lambda state: state.has("Power Star", player, star_costs["BasementDoorCost"]) and + state.can_reach("DDD: Board Bowser's Sub", 'Location', player)) + # Access to "DDD: Board Bowser's Sub" does not require access to other locations or regions, so the only region that + # needs to be registered is its parent region. + world.register_indirect_condition(world.get_location("DDD: Board Bowser's Sub", player).parent_region, entrance) connect_regions(world, player, "Menu", "Second Floor", lambda state: state.has("Second Floor Key", player) or state.has("Progressive Key", player, 2)) From 5530d181da643beb96abd915c259f6c22cb9dc7f Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 16 Sep 2024 06:48:13 +0200 Subject: [PATCH 09/94] Core: update version number (#3944) --- Utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Utils.py b/Utils.py index f89330cf7c65..d6709431d32c 100644 --- a/Utils.py +++ b/Utils.py @@ -46,7 +46,7 @@ def as_simple_string(self) -> str: return ".".join(str(item) for item in self) -__version__ = "0.5.0" +__version__ = "0.5.1" version_tuple = tuplize_version(__version__) is_linux = sys.platform.startswith("linux") From 84805a4e541c6dc2a0d95b8c3609b1faf9c240db Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Mon, 16 Sep 2024 08:30:47 -0400 Subject: [PATCH 10/94] HK: XBox doesn't exist #3932 --- worlds/hk/docs/setup_en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/hk/docs/setup_en.md b/worlds/hk/docs/setup_en.md index c046785038d8..21cdcb68b3a9 100644 --- a/worlds/hk/docs/setup_en.md +++ b/worlds/hk/docs/setup_en.md @@ -15,7 +15,7 @@ ### What to do if Lumafly fails to find your installation directory 1. Find the directory manually. * Xbox Game Pass: - 1. Enter the XBox app and move your mouse over "Hollow Knight" on the left sidebar. + 1. Enter the Xbox app and move your mouse over "Hollow Knight" on the left sidebar. 2. Click the three points then click "Manage". 3. Go to the "Files" tab and select "Browse...". 4. Click "Hollow Knight", then "Content", then click the path bar and copy it. From ee12dda3611cdf016d8e8a8633a32333a3f47a13 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Mon, 16 Sep 2024 12:06:20 -0400 Subject: [PATCH 11/94] Lingo: Added missing connection from The Tenacious -> Hub Room (#3947) --- worlds/lingo/data/LL1.yaml | 4 +++- worlds/lingo/data/generated.dat | Bin 149166 -> 149230 bytes 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index 16a1573b1d56..bbed1464530b 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -482,7 +482,9 @@ Crossroads: door: Crossroads Entrance The Tenacious: - door: Tenacious Entrance + - door: Tenacious Entrance + - room: The Tenacious + door: Shortcut to Hub Room Near Far Area: True Hedge Maze: door: Shortcut to Hedge Maze diff --git a/worlds/lingo/data/generated.dat b/worlds/lingo/data/generated.dat index e2d3d06bec9642cf1b782cc3751ec542c322b558..789fc0856d62853f80fa5bd332b87c6fdd63ee60 100644 GIT binary patch delta 32275 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

U5jJVz{w%7y6n9Ni@HWmP7_!i>?bhpSWv&*7)( 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 delta 32247 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 UkY1CJx8$FF&puxoG}7^Z0FSwKuK)l5 From ce42e42af78676758f6bd30c1cceb576ebf1fcb6 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Tue, 17 Sep 2024 07:36:05 -0500 Subject: [PATCH 12/94] Core: fix single player item links (#3721) * fix single player item links * Make a variable and fix weird spacing * use advancement instead of classification --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- BaseClasses.py | 2 ++ Fill.py | 20 +++++++++----------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index b40b872f0c8c..a5de1689a7fe 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -342,6 +342,8 @@ def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ region = Region("Menu", group_id, self, "ItemLink") self.regions.append(region) locations = region.locations + # ensure that progression items are linked first, then non-progression + self.itempool.sort(key=lambda item: item.advancement) for item in self.itempool: count = common_item_count.get(item.player, {}).get(item.name, 0) if count: diff --git a/Fill.py b/Fill.py index e2fcff00358e..706cca657457 100644 --- a/Fill.py +++ b/Fill.py @@ -475,28 +475,26 @@ def mark_for_locking(location: Location): nonlocal lock_later lock_later.append(location) + single_player = multiworld.players == 1 and not multiworld.groups + if prioritylocations: # "priority fill" fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool, - single_player_placement=multiworld.players == 1, swap=False, on_place=mark_for_locking, - name="Priority") + single_player_placement=single_player, swap=False, on_place=mark_for_locking, name="Priority") accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool) defaultlocations = prioritylocations + defaultlocations if progitempool: # "advancement/progression fill" if panic_method == "swap": - fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, - swap=True, - name="Progression", single_player_placement=multiworld.players == 1) + fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=True, + name="Progression", single_player_placement=single_player) elif panic_method == "raise": - fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, - swap=False, - name="Progression", single_player_placement=multiworld.players == 1) + fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=False, + name="Progression", single_player_placement=single_player) elif panic_method == "start_inventory": - fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, - swap=False, allow_partial=True, - name="Progression", single_player_placement=multiworld.players == 1) + fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=False, + allow_partial=True, name="Progression", single_player_placement=single_player) if progitempool: for item in progitempool: logging.debug(f"Moved {item} to start_inventory to prevent fill failure.") From b8d23ec5956cbf8313c328e4f3f9f9d08c9e0492 Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Tue, 17 Sep 2024 13:41:56 +0100 Subject: [PATCH 13/94] MMBN3: Add missing indirect conditions (#3931) Entrances to SciLab_Cyberworld and Yoka_Cyberworld had logic for being able to reach SciLab_Overworld, but did not register this indirect condition. Entrances to Beach_Cyberworld had logic for being able to reach Yoka_Overworld, but did not register this indirect condition. Entrances to Undernet and Secret_Area had logic for having a high enough explore score, but explore score is calculated based on the accessibility of a number of regions and no indirect conditions were being registered for these regions. --- worlds/mmbn3/__init__.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/worlds/mmbn3/__init__.py b/worlds/mmbn3/__init__.py index 97725e728bae..6d28b101c377 100644 --- a/worlds/mmbn3/__init__.py +++ b/worlds/mmbn3/__init__.py @@ -97,6 +97,28 @@ def create_regions(self) -> None: add_item_rule(loc, lambda item: not item.advancement) region.locations.append(loc) self.multiworld.regions.append(region) + + # Regions which contribute to explore score when accessible. + explore_score_region_names = ( + RegionName.WWW_Island, + RegionName.SciLab_Overworld, + RegionName.SciLab_Cyberworld, + RegionName.Yoka_Overworld, + RegionName.Yoka_Cyberworld, + RegionName.Beach_Overworld, + RegionName.Beach_Cyberworld, + RegionName.Undernet, + RegionName.Deep_Undernet, + RegionName.Secret_Area, + ) + explore_score_regions = [self.get_region(region_name) for region_name in explore_score_region_names] + + # Entrances which use explore score in their logic need to register all the explore score regions as indirect + # conditions. + def register_explore_score_indirect_conditions(entrance): + for explore_score_region in explore_score_regions: + self.multiworld.register_indirect_condition(explore_score_region, entrance) + for region_info in regions: region = name_to_region[region_info.name] for connection in region_info.connections: @@ -119,6 +141,7 @@ def create_regions(self) -> None: entrance.access_rule = lambda state: \ state.has(ItemName.CSciPas, self.player) or \ state.can_reach(RegionName.SciLab_Overworld, "Region", self.player) + self.multiworld.register_indirect_condition(self.get_region(RegionName.SciLab_Overworld), entrance) if connection == RegionName.Yoka_Cyberworld: entrance.access_rule = lambda state: \ state.has(ItemName.CYokaPas, self.player) or \ @@ -126,16 +149,19 @@ def create_regions(self) -> None: state.can_reach(RegionName.SciLab_Overworld, "Region", self.player) and state.has(ItemName.Press, self.player) ) + self.multiworld.register_indirect_condition(self.get_region(RegionName.SciLab_Overworld), entrance) if connection == RegionName.Beach_Cyberworld: entrance.access_rule = lambda state: state.has(ItemName.CBeacPas, self.player) and\ state.can_reach(RegionName.Yoka_Overworld, "Region", self.player) - + self.multiworld.register_indirect_condition(self.get_region(RegionName.Yoka_Overworld), entrance) if connection == RegionName.Undernet: entrance.access_rule = lambda state: self.explore_score(state) > 8 and\ state.has(ItemName.Press, self.player) + register_explore_score_indirect_conditions(entrance) if connection == RegionName.Secret_Area: entrance.access_rule = lambda state: self.explore_score(state) > 12 and\ state.has(ItemName.Hammer, self.player) + register_explore_score_indirect_conditions(entrance) if connection == RegionName.WWW_Island: entrance.access_rule = lambda state:\ state.has(ItemName.Progressive_Undernet_Rank, self.player, 8) From 4692e6f08aa9c7cea764be0f079e506e17c695b0 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Tue, 17 Sep 2024 07:42:19 -0500 Subject: [PATCH 14/94] MM2: fix Air Shooter minimum damage #3922 --- worlds/mm2/rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/mm2/rules.py b/worlds/mm2/rules.py index c30688f2adbe..eddd09927445 100644 --- a/worlds/mm2/rules.py +++ b/worlds/mm2/rules.py @@ -37,7 +37,7 @@ minimum_weakness_requirement: Dict[int, int] = { 0: 1, # Mega Buster is free 1: 14, # 2 shots of Atomic Fire - 2: 1, # 14 shots of Air Shooter, although you likely hit more than one shot + 2: 2, # 14 shots of Air Shooter 3: 4, # 9 uses of Leaf Shield, 3 ends up 1 damage off 4: 1, # 56 uses of Bubble Lead 5: 1, # 224 uses of Quick Boomerang From 1c0cec0de2311f818c1a19b4f0d91219e0d9c852 Mon Sep 17 00:00:00 2001 From: digiholic Date: Tue, 17 Sep 2024 06:42:48 -0600 Subject: [PATCH 15/94] [OSRS] Adds Description to OSRS World #3921 --- worlds/osrs/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/worlds/osrs/__init__.py b/worlds/osrs/__init__.py index 1b7ca9c1e0f4..49aa1666084e 100644 --- a/worlds/osrs/__init__.py +++ b/worlds/osrs/__init__.py @@ -33,6 +33,12 @@ class OSRSWeb(WebWorld): class OSRSWorld(World): + """ + The best retro fantasy MMORPG on the planet. Old School is RuneScape but… older! This is the open world you know and love, but as it was in 2007. + The Randomizer takes the form of a Chunk-Restricted f2p Ironman that takes a brand new account up through defeating + the Green Dragon of Crandor and earning a spot in the fabled Champion's Guild! + """ + game = "Old School Runescape" options_dataclass = OSRSOptions options: OSRSOptions @@ -635,7 +641,7 @@ def can_gold(state): else: return lambda state: can_tan(state) or (can_silver(state) and can_smelt_silver(state)) or \ (can_gold(state) and can_smelt_gold(state)) - if skill.lower() == "Cooking": + if skill.lower() == "cooking": if self.options.brutal_grinds or level < 15: return lambda state: state.can_reach(RegionNames.Milk, "Region", self.player) or \ state.can_reach(RegionNames.Egg, "Region", self.player) or \ From f8d3c26e3c6e4972f7845c6cd10bede41d8fd7cf Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Tue, 17 Sep 2024 05:43:22 -0700 Subject: [PATCH 16/94] Pokemon Emerald: Fix unguarded wonder trade write (#3939) --- worlds/pokemon_emerald/client.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/worlds/pokemon_emerald/client.py b/worlds/pokemon_emerald/client.py index cda829def9d9..d742b8936f14 100644 --- a/worlds/pokemon_emerald/client.py +++ b/worlds/pokemon_emerald/client.py @@ -133,6 +133,7 @@ class PokemonEmeraldClient(BizHawkClient): latest_wonder_trade_reply: dict wonder_trade_cooldown: int wonder_trade_cooldown_timer: int + queued_received_trade: Optional[str] death_counter: Optional[int] previous_death_link: float @@ -153,6 +154,7 @@ def initialize_client(self): self.previous_death_link = 0 self.ignore_next_death_link = False self.current_map = None + self.queued_received_trade = None async def validate_rom(self, ctx: "BizHawkClientContext") -> bool: from CommonClient import logger @@ -548,22 +550,29 @@ async def handle_wonder_trade(self, ctx: "BizHawkClientContext", guards: Dict[st (sb1_address + 0x37CC, [1], "System Bus"), ]) elif trade_is_sent != 0 and wonder_trade_pokemon_data[19] != 2: - # Game is waiting on receiving a trade. See if there are any available trades that were not - # sent by this player, and if so, try to receive one. - if self.wonder_trade_cooldown_timer <= 0 and f"pokemon_wonder_trades_{ctx.team}" in ctx.stored_data: + # Game is waiting on receiving a trade. + if self.queued_received_trade is not None: + # Client is holding a trade, ready to write it into the game + success = await bizhawk.guarded_write(ctx.bizhawk_ctx, [ + (sb1_address + 0x377C, json_to_pokemon_data(self.queued_received_trade), "System Bus"), + ], [guards["SAVE BLOCK 1"]]) + + # Notify the player if it was written, otherwise hold it for the next loop + if success: + logger.info("Wonder trade received!") + self.queued_received_trade = None + + elif self.wonder_trade_cooldown_timer <= 0 and f"pokemon_wonder_trades_{ctx.team}" in ctx.stored_data: + # See if there are any available trades that were not sent by this player. If so, try to receive one. if any(item[0] != ctx.slot for key, item in ctx.stored_data.get(f"pokemon_wonder_trades_{ctx.team}", {}).items() if key != "_lock" and orjson.loads(item[1])["species"] <= 386): - received_trade = await self.wonder_trade_receive(ctx) - if received_trade is None: + self.queued_received_trade = await self.wonder_trade_receive(ctx) + if self.queued_received_trade is None: self.wonder_trade_cooldown_timer = self.wonder_trade_cooldown self.wonder_trade_cooldown *= 2 self.wonder_trade_cooldown += random.randrange(0, 500) else: - await bizhawk.write(ctx.bizhawk_ctx, [ - (sb1_address + 0x377C, json_to_pokemon_data(received_trade), "System Bus"), - ]) - logger.info("Wonder trade received!") self.wonder_trade_cooldown = 5000 else: From ec50b0716aa280c7bf4ce7a13691de8dc95f8b34 Mon Sep 17 00:00:00 2001 From: qwint Date: Tue, 17 Sep 2024 07:44:32 -0500 Subject: [PATCH 17/94] Core: Add color conversions for colorama/terminal output #3940 --- NetUtils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/NetUtils.py b/NetUtils.py index c451fa3f8460..4776b228db17 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -273,7 +273,8 @@ def _handle_color(self, node: JSONMessagePart): color_codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43, - 'blue_bg': 44, 'magenta_bg': 45, 'cyan_bg': 46, 'white_bg': 47} + 'blue_bg': 44, 'magenta_bg': 45, 'cyan_bg': 46, 'white_bg': 47, + 'plum': 35, 'slateblue': 34, 'salmon': 31,} # convert ui colors to terminal colors def color_code(*args): From 96542fb2d891ff26c86b8a652907980ea3686702 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Tue, 17 Sep 2024 09:08:15 -0400 Subject: [PATCH 18/94] Blasphemous: Move pre_fill to create_items #3901 --- worlds/blasphemous/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/worlds/blasphemous/__init__.py b/worlds/blasphemous/__init__.py index b110c316da48..67031710e4eb 100644 --- a/worlds/blasphemous/__init__.py +++ b/worlds/blasphemous/__init__.py @@ -199,8 +199,6 @@ def create_items(self): self.multiworld.itempool += pool - - def pre_fill(self): self.place_items_from_dict(unrandomized_dict) if self.options.thorn_shuffle == "vanilla": @@ -335,4 +333,4 @@ class BlasphemousItem(Item): class BlasphemousLocation(Location): - game: str = "Blasphemous" \ No newline at end of file + game: str = "Blasphemous" From dae3fe188d253bfd8340a15ff5b44a8189413008 Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Tue, 17 Sep 2024 14:11:35 +0100 Subject: [PATCH 19/94] OOT: Fix incorrect region accessibility after update_reachable_regions() (#3712) `CollectionState.update_reachable_regions()` un-stales the state for all players, but when checking `OOTRegion.can_reach()`, it would only update OOT's age region accessibility when the state was stale, so if the state was always un-staled by `update_reachable_regions()` immediately before `OOTRegion.can_reach()`, OOT's age region accessibility would never update. This patch fixes the issue by replacing use of CollectionState.stale with a separate stale state dictionary specific to OOT that is only un-staled by `_oot_update_age_reachable_regions()`. OOT's collect() and remove() implementations have been updated to stale the new OOT-specific state. --- worlds/oot/Regions.py | 2 +- worlds/oot/Rules.py | 13 +++++++++---- worlds/oot/__init__.py | 9 +++++++-- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/worlds/oot/Regions.py b/worlds/oot/Regions.py index 5d5cc9b13822..4a3d7e416a15 100644 --- a/worlds/oot/Regions.py +++ b/worlds/oot/Regions.py @@ -64,7 +64,7 @@ def get_scene(self): return None def can_reach(self, state): - if state.stale[self.player]: + if state._oot_stale[self.player]: stored_age = state.age[self.player] state._oot_update_age_reachable_regions(self.player) state.age[self.player] = stored_age diff --git a/worlds/oot/Rules.py b/worlds/oot/Rules.py index 4bbf15435cfe..36563a3f9f27 100644 --- a/worlds/oot/Rules.py +++ b/worlds/oot/Rules.py @@ -8,12 +8,17 @@ from .Items import oot_is_item_of_type from .LocationList import dungeon_song_locations -from BaseClasses import CollectionState +from BaseClasses import CollectionState, MultiWorld from worlds.generic.Rules import set_rule, add_rule, add_item_rule, forbid_item from ..AutoWorld import LogicMixin class OOTLogic(LogicMixin): + def init_mixin(self, parent: MultiWorld): + # Separate stale state for OOTRegion.can_reach() to use because CollectionState.update_reachable_regions() sets + # `self.state[player] = False` for all players without updating OOT's age region accessibility. + self._oot_stale = {player: True for player, world in parent.worlds.items() + if parent.worlds[player].game == "Ocarina of Time"} def _oot_has_stones(self, count, player): return self.has_group("stones", player, count) @@ -92,9 +97,9 @@ def _oot_reach_at_time(self, regionname, tod, already_checked, player): return False # Store the age before calling this! - def _oot_update_age_reachable_regions(self, player): - self.stale[player] = False - for age in ['child', 'adult']: + def _oot_update_age_reachable_regions(self, player): + self._oot_stale[player] = False + for age in ['child', 'adult']: self.age[player] = age rrp = getattr(self, f'{age}_reachable_regions')[player] bc = getattr(self, f'{age}_blocked_connections')[player] diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index ee78958b2dbe..94587a41a0f2 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -1301,6 +1301,7 @@ def write_spoiler(self, spoiler_handle: typing.TextIO) -> None: # the appropriate number of keys in the collection state when they are # picked up. def collect(self, state: CollectionState, item: OOTItem) -> bool: + state._oot_stale[self.player] = True if item.advancement and item.special and item.special.get('alias', False): alt_item_name, count = item.special.get('alias') state.prog_items[self.player][alt_item_name] += count @@ -1313,8 +1314,12 @@ def remove(self, state: CollectionState, item: OOTItem) -> bool: state.prog_items[self.player][alt_item_name] -= count if state.prog_items[self.player][alt_item_name] < 1: del (state.prog_items[self.player][alt_item_name]) + state._oot_stale[self.player] = True return True - return super().remove(state, item) + changed = super().remove(state, item) + if changed: + state._oot_stale[self.player] = True + return changed # Helper functions @@ -1389,7 +1394,7 @@ def get_state_with_complete_itempool(self): # If free_scarecrow give Scarecrow Song if self.free_scarecrow: all_state.collect(self.create_item("Scarecrow Song"), prevent_sweep=True) - all_state.stale[self.player] = True + all_state._oot_stale[self.player] = True return all_state From 97be5f1dde63e4fbec51f8973a184a7c66dd6a37 Mon Sep 17 00:00:00 2001 From: Rensen3 <127029481+Rensen3@users.noreply.github.com> Date: Tue, 17 Sep 2024 15:13:19 +0200 Subject: [PATCH 20/94] YGO06: slotdata fix (#3953) * YGO06: fix slot data for universal tracker * YGO06: put Extremely Low Deck Bonus after Low Deck Bonus --- worlds/yugioh06/__init__.py | 2 +- worlds/yugioh06/rules.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/worlds/yugioh06/__init__.py b/worlds/yugioh06/__init__.py index 1cf44f090fed..a39b52cd09d5 100644 --- a/worlds/yugioh06/__init__.py +++ b/worlds/yugioh06/__init__.py @@ -430,7 +430,7 @@ def fill_slot_data(self) -> Dict[str, Any]: "final_campaign_boss_campaign_opponents": self.options.final_campaign_boss_campaign_opponents.value, "fourth_tier_5_campaign_boss_campaign_opponents": - self.options.fourth_tier_5_campaign_boss_unlock_condition.value, + self.options.fourth_tier_5_campaign_boss_campaign_opponents.value, "third_tier_5_campaign_boss_campaign_opponents": self.options.third_tier_5_campaign_boss_campaign_opponents.value, "number_of_challenges": self.options.number_of_challenges.value, diff --git a/worlds/yugioh06/rules.py b/worlds/yugioh06/rules.py index a804c7e7286a..0b46e0b5d0b0 100644 --- a/worlds/yugioh06/rules.py +++ b/worlds/yugioh06/rules.py @@ -39,10 +39,10 @@ def set_rules(world): "No Trap Cards Bonus": lambda state: yugioh06_difficulty(state, player, 2), "No Damage Bonus": lambda state: state.has_group("Campaign Boss Beaten", player, 3), "Low Deck Bonus": lambda state: state.has_any(["Reasoning", "Monster Gate", "Magical Merchant"], player) and - yugioh06_difficulty(state, player, 3), + yugioh06_difficulty(state, player, 2), "Extremely Low Deck Bonus": lambda state: state.has_any(["Reasoning", "Monster Gate", "Magical Merchant"], player) and - yugioh06_difficulty(state, player, 2), + yugioh06_difficulty(state, player, 3), "Opponent's Turn Finish Bonus": lambda state: yugioh06_difficulty(state, player, 2), "Exactly 0 LP Bonus": lambda state: yugioh06_difficulty(state, player, 2), "Reversal Finish Bonus": lambda state: yugioh06_difficulty(state, player, 2), From 5aea8d4ab56fcb063ce672d44bcf1d97229d705d Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Tue, 17 Sep 2024 06:14:05 -0700 Subject: [PATCH 21/94] Pokemon Emerald: Update changelog (#3952) --- worlds/pokemon_emerald/CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/worlds/pokemon_emerald/CHANGELOG.md b/worlds/pokemon_emerald/CHANGELOG.md index 0437c0dae8ff..6a1844e79fde 100644 --- a/worlds/pokemon_emerald/CHANGELOG.md +++ b/worlds/pokemon_emerald/CHANGELOG.md @@ -1,3 +1,21 @@ +# 2.3.0 + +### Features + +- Added a Swedish translation of the setup guide. +- The client communicates map transitions to any trackers connected to the slot. +- Added the player's Normalize Encounter Rates option to slot data for trackers. + +### Fixes + +- Fixed a logic issue where the "Mauville City - Coin Case from Lady in House" location only required a Harbor Mail if +the player randomized NPC gifts. +- The Dig tutor has its compatibility percentage raised to 50% if the player's TM/tutor compatibility is set lower. +- A Team Magma Grunt in the Space Center which could become unreachable while trainersanity is active by overlapping +with another NPC was moved to an unoccupied space. +- Fixed a problem where the client would crash on certain operating systems while using certain python versions if the +player tried to wonder trade. + # 2.2.0 ### Features @@ -175,6 +193,7 @@ turn to face you when you run. species equally likely to appear, but makes rare encounters less rare. - Added `Trick House` location group. - Removed `Postgame Locations` location group. +- Added a Spanish translation of the setup guide. ### QoL From 8f7e0dc441610e292cfb1ce5688a2017fe175ae3 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Tue, 17 Sep 2024 14:17:41 -0700 Subject: [PATCH 22/94] Core: Improve death link option description (#3951) --- Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Options.py b/Options.py index b79714635d9e..ac4b2b8cd8bb 100644 --- a/Options.py +++ b/Options.py @@ -1335,7 +1335,7 @@ class PriorityLocations(LocationSet): class DeathLink(Toggle): - """When you die, everyone dies. Of course the reverse is true too.""" + """When you die, everyone who enabled death link dies. Of course, the reverse is true too.""" display_name = "Death Link" rich_text_doc = True From b982e9ebb4eb3a370efb36860b8e51a80881d24a Mon Sep 17 00:00:00 2001 From: Ziktofel Date: Tue, 17 Sep 2024 23:18:43 +0200 Subject: [PATCH 23/94] SC2: Fix /received display bugs (#3949) * SC2: Fix location display in /received command * SC2: Backport broken markup fix in /received output from the dev branch * Cleanup --- worlds/sc2/Client.py | 14 +++++++------- worlds/sc2/ClientGui.py | 16 +++++++++++++++- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/worlds/sc2/Client.py b/worlds/sc2/Client.py index bb325ba1da45..813cf2884517 100644 --- a/worlds/sc2/Client.py +++ b/worlds/sc2/Client.py @@ -97,12 +97,12 @@ class ConfigurableOptionInfo(typing.NamedTuple): class ColouredMessage: - def __init__(self, text: str = '') -> None: + def __init__(self, text: str = '', *, keep_markup: bool = False) -> None: self.parts: typing.List[dict] = [] if text: - self(text) - def __call__(self, text: str) -> 'ColouredMessage': - add_json_text(self.parts, text) + self(text, keep_markup=keep_markup) + def __call__(self, text: str, *, keep_markup: bool = False) -> 'ColouredMessage': + add_json_text(self.parts, text, keep_markup=keep_markup) return self def coloured(self, text: str, colour: str) -> 'ColouredMessage': add_json_text(self.parts, text, type="color", color=colour) @@ -128,7 +128,7 @@ def formatted_print(self, text: str) -> None: # Note(mm): Bold/underline can help readability, but unfortunately the CommonClient does not filter bold tags from command-line output. # Regardless, using `on_print_json` to get formatted text in the GUI and output in the command-line and in the logs, # without having to branch code from CommonClient - self.ctx.on_print_json({"data": [{"text": text}]}) + self.ctx.on_print_json({"data": [{"text": text, "keep_markup": True}]}) def _cmd_difficulty(self, difficulty: str = "") -> bool: """Overrides the current difficulty set for the world. Takes the argument casual, normal, hard, or brutal""" @@ -257,7 +257,7 @@ def print_faction_title(): print_faction_title() has_printed_faction_title = True (ColouredMessage('* ').item(item.item, self.ctx.slot, flags=item.flags) - (" from ").location(item.location, self.ctx.slot) + (" from ").location(item.location, item.player) (" by ").player(item.player) ).send(self.ctx) @@ -278,7 +278,7 @@ def print_faction_title(): for item in received_items_of_this_type: filter_match_count += len(received_items_of_this_type) (ColouredMessage(' * ').item(item.item, self.ctx.slot, flags=item.flags) - (" from ").location(item.location, self.ctx.slot) + (" from ").location(item.location, item.player) (" by ").player(item.player) ).send(self.ctx) diff --git a/worlds/sc2/ClientGui.py b/worlds/sc2/ClientGui.py index 22e444efe7c9..fe62e6162457 100644 --- a/worlds/sc2/ClientGui.py +++ b/worlds/sc2/ClientGui.py @@ -1,7 +1,8 @@ from typing import * import asyncio -from kvui import GameManager, HoverBehavior, ServerToolTip +from NetUtils import JSONMessagePart +from kvui import GameManager, HoverBehavior, ServerToolTip, KivyJSONtoTextParser from kivy.app import App from kivy.clock import Clock from kivy.uix.tabbedpanel import TabbedPanelItem @@ -69,6 +70,18 @@ class MissionLayout(GridLayout): class MissionCategory(GridLayout): pass + +class SC2JSONtoKivyParser(KivyJSONtoTextParser): + def _handle_text(self, node: JSONMessagePart): + if node.get("keep_markup", False): + for ref in node.get("refs", []): + node["text"] = f"[ref={self.ref_count}|{ref}]{node['text']}[/ref]" + self.ref_count += 1 + return super(KivyJSONtoTextParser, self)._handle_text(node) + else: + return super()._handle_text(node) + + class SC2Manager(GameManager): logging_pairs = [ ("Client", "Archipelago"), @@ -87,6 +100,7 @@ class SC2Manager(GameManager): def __init__(self, ctx) -> None: super().__init__(ctx) + self.json_to_kivy_parser = SC2JSONtoKivyParser(ctx) def clear_tooltip(self) -> None: if self.ctx.current_tooltip: From d1a7bc66e6f217d9c024e99b8556c2e981ab208a Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Tue, 17 Sep 2024 14:49:36 -0700 Subject: [PATCH 24/94] Pokemon Emerald: Prevent client from spamming goal status update (#3900) --- worlds/pokemon_emerald/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/pokemon_emerald/client.py b/worlds/pokemon_emerald/client.py index d742b8936f14..c91b7d3e26b0 100644 --- a/worlds/pokemon_emerald/client.py +++ b/worlds/pokemon_emerald/client.py @@ -352,6 +352,7 @@ async def game_watcher(self, ctx: "BizHawkClientContext") -> None: # Send game clear if not ctx.finished_game and game_clear: + ctx.finished_game = True await ctx.send_msgs([{ "cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL, From 78c5489189596d3f3851a119efebf58e94cea788 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Tue, 17 Sep 2024 21:50:02 +0000 Subject: [PATCH 25/94] DS3: Mark the Archdeacon Set as downstream of Deacons of the Deep (#3883) This ensures that if Deacons is replaced with Yhorm, the Storm Ruler won't show up in these locations. --- worlds/dark_souls_3/Bosses.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/worlds/dark_souls_3/Bosses.py b/worlds/dark_souls_3/Bosses.py index 008a29713202..fac7d913c338 100644 --- a/worlds/dark_souls_3/Bosses.py +++ b/worlds/dark_souls_3/Bosses.py @@ -63,6 +63,9 @@ class DS3BossInfo: DS3BossInfo("Deacons of the Deep", 3500800, locations = { "CD: Soul of the Deacons of the Deep", "CD: Small Doll - boss drop", + "CD: Archdeacon White Crown - boss room after killing boss", + "CD: Archdeacon Holy Garb - boss room after killing boss", + "CD: Archdeacon Skirt - boss room after killing boss", "FS: Hawkwood's Shield - gravestone after Hawkwood leaves", }), DS3BossInfo("Abyss Watchers", 3300801, before_storm_ruler = True, locations = { From dc218b79974f5d0418b5d2e200106519256751a0 Mon Sep 17 00:00:00 2001 From: Mrks <68022469+mrkssr@users.noreply.github.com> Date: Tue, 17 Sep 2024 23:56:40 +0200 Subject: [PATCH 26/94] LADX: Adding Slot Data For Magpie Tracker (#3582) * wip: LADX slot_data * LADX: slot_data * Sending slot_data to magpie. * Moved sending slot_data from pushing to pull by Magpie request. * Adding EoF newline to tracker.py. * Update Tracker.py * Update __init__.py * Update LinksAwakeningClient.py --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- LinksAwakeningClient.py | 5 +++++ worlds/ladx/Tracker.py | 18 +++++++++++++++++- worlds/ladx/__init__.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/LinksAwakeningClient.py b/LinksAwakeningClient.py index a51645feac92..298788098d9e 100644 --- a/LinksAwakeningClient.py +++ b/LinksAwakeningClient.py @@ -467,6 +467,8 @@ class LinksAwakeningContext(CommonContext): def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str], magpie: typing.Optional[bool]) -> None: self.client = LinksAwakeningClient() + self.slot_data = {} + if magpie: self.magpie_enabled = True self.magpie = MagpieBridge() @@ -564,6 +566,8 @@ async def server_auth(self, password_requested: bool = False): def on_package(self, cmd: str, args: dict): if cmd == "Connected": self.game = self.slot_info[self.slot].game + self.slot_data = args.get("slot_data", {}) + # TODO - use watcher_event if cmd == "ReceivedItems": for index, item in enumerate(args["items"], start=args["index"]): @@ -628,6 +632,7 @@ async def deathlink(): self.magpie.set_checks(self.client.tracker.all_checks) await self.magpie.set_item_tracker(self.client.item_tracker) await self.magpie.send_gps(self.client.gps_tracker) + self.magpie.slot_data = self.slot_data except Exception: # Don't let magpie errors take out the client pass diff --git a/worlds/ladx/Tracker.py b/worlds/ladx/Tracker.py index 851fca164453..5f48b64c4f5e 100644 --- a/worlds/ladx/Tracker.py +++ b/worlds/ladx/Tracker.py @@ -149,6 +149,8 @@ class MagpieBridge: item_tracker = None ws = None features = [] + slot_data = {} + async def handler(self, websocket): self.ws = websocket while True: @@ -163,6 +165,9 @@ async def handler(self, websocket): await self.send_all_inventory() if "checks" in self.features: await self.send_all_checks() + if "slot_data" in self.features: + await self.send_slot_data(self.slot_data) + # Translate renamed IDs back to LADXR IDs @staticmethod def fixup_id(the_id): @@ -222,6 +227,18 @@ async def send_gps(self, gps): return await gps.send_location(self.ws) + async def send_slot_data(self, slot_data): + if not self.ws: + return + + logger.debug("Sending slot_data to magpie.") + message = { + "type": "slot_data", + "slot_data": slot_data + } + + await self.ws.send(json.dumps(message)) + async def serve(self): async with websockets.serve(lambda w: self.handler(w), "", 17026, logger=logger): await asyncio.Future() # run forever @@ -237,4 +254,3 @@ async def set_item_tracker(self, item_tracker): await self.send_all_inventory() else: await self.send_inventory_diffs() - diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index c958ef212fe4..79f1fe470f81 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -512,3 +512,31 @@ def remove(self, state, item: Item) -> bool: if change and item.name in self.rupees: state.prog_items[self.player]["RUPEES"] -= self.rupees[item.name] return change + + def fill_slot_data(self): + slot_data = {} + + if not self.multiworld.is_race: + # all of these option are NOT used by the LADX- or Text-Client. + # they are used by Magpie tracker (https://github.com/kbranch/Magpie/wiki/Autotracker-API) + # for convenient auto-tracking of the generated settings and adjusting the tracker accordingly + + slot_options = ["instrument_count"] + + slot_options_display_name = [ + "goal", "logic", "tradequest", "rooster", + "experimental_dungeon_shuffle", "experimental_entrance_shuffle", "trendy_game", "gfxmod", + "shuffle_nightmare_keys", "shuffle_small_keys", "shuffle_maps", + "shuffle_compasses", "shuffle_stone_beaks", "shuffle_instruments", "nag_messages" + ] + + # use the default behaviour to grab options + slot_data = self.options.as_dict(*slot_options) + + # for options which should not get the internal int value but the display name use the extra handling + slot_data.update({ + option: value.current_key + for option, value in dataclasses.asdict(self.options).items() if option in slot_options_display_name + }) + + return slot_data From 4ea1dddd2f420bca6a73e13d04228931f04a3834 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Tue, 17 Sep 2024 17:57:55 -0400 Subject: [PATCH 27/94] TUNIC: Better logic for Library Lab glass and Fortress leaf piles #3880 --- worlds/tunic/er_rules.py | 15 ++++++++++++++- worlds/tunic/rules.py | 13 ++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index 65175e41ca14..ee48f60eaca4 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -1339,13 +1339,26 @@ def set_er_location_rules(world: "TunicWorld") -> None: set_rule(world.get_location("Frog's Domain - Escape Chest"), lambda state: state.has_any({grapple, laurels}, player)) + # Library Lab + set_rule(world.get_location("Library Lab - Page 1"), + lambda state: has_stick(state, player) or state.has_any((fire_wand, gun), player)) + set_rule(world.get_location("Library Lab - Page 2"), + lambda state: has_stick(state, player) or state.has_any((fire_wand, gun), player)) + set_rule(world.get_location("Library Lab - Page 3"), + lambda state: has_stick(state, player) or state.has_any((fire_wand, gun), player)) + # Eastern Vault Fortress set_rule(world.get_location("Fortress Arena - Hexagon Red"), lambda state: state.has(vault_key, player)) + # yes, you can clear the leaves with dagger + # gun isn't included since it can only break one leaf pile at a time, and we don't check how much mana you have + # but really, I expect the player to just throw a bomb at them if they don't have melee + set_rule(world.get_location("Fortress Leaf Piles - Secret Chest"), + lambda state: has_stick(state, player) or state.has(ice_dagger, player)) # Beneath the Vault set_rule(world.get_location("Beneath the Fortress - Bridge"), - lambda state: state.has_group("Melee Weapons", player, 1) or state.has_any({laurels, fire_wand}, player)) + lambda state: has_stick(state, player) or state.has_any({laurels, fire_wand}, player)) # Quarry set_rule(world.get_location("Quarry - [Central] Above Ladder Dash Chest"), diff --git a/worlds/tunic/rules.py b/worlds/tunic/rules.py index 942bbc773aa5..14ed84d44964 100644 --- a/worlds/tunic/rules.py +++ b/worlds/tunic/rules.py @@ -296,9 +296,20 @@ def set_location_rules(world: "TunicWorld") -> None: set_rule(world.get_location("Frog's Domain - Escape Chest"), lambda state: state.has_any({grapple, laurels}, player)) + # Library Lab + set_rule(world.get_location("Library Lab - Page 1"), + lambda state: has_stick(state, player) or state.has_any((fire_wand, gun), player)) + set_rule(world.get_location("Library Lab - Page 2"), + lambda state: has_stick(state, player) or state.has_any((fire_wand, gun), player)) + set_rule(world.get_location("Library Lab - Page 3"), + lambda state: has_stick(state, player) or state.has_any((fire_wand, gun), player)) + # Eastern Vault Fortress + # yes, you can clear the leaves with dagger + # gun isn't included since it can only break one leaf pile at a time, and we don't check how much mana you have + # but really, I expect the player to just throw a bomb at them if they don't have melee set_rule(world.get_location("Fortress Leaf Piles - Secret Chest"), - lambda state: state.has(laurels, player)) + lambda state: state.has(laurels, player) and (has_stick(state, player) or state.has(ice_dagger, player))) set_rule(world.get_location("Fortress Arena - Siege Engine/Vault Key Pickup"), lambda state: has_sword(state, player) and (has_ability(prayer, state, world) From 30a0b337a2bc79407127b6b60a86c4c1793bc5be Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Tue, 17 Sep 2024 17:58:45 -0400 Subject: [PATCH 28/94] DS3: Make Red Eye Orb always require Lift Chamber Key #3857 --- worlds/dark_souls_3/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index 46c7ef1336c1..b51668539be2 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -612,9 +612,7 @@ def set_rules(self) -> None: self._add_entrance_rule("Painted World of Ariandel (Before Contraption)", "Basin of Vows") # Define the access rules to some specific locations - if self._is_location_available("FS: Lift Chamber Key - Leonhard"): - self._add_location_rule("HWL: Red Eye Orb - wall tower, miniboss", - "Lift Chamber Key") + self._add_location_rule("HWL: Red Eye Orb - wall tower, miniboss", "Lift Chamber Key") self._add_location_rule("ID: Bellowing Dragoncrest Ring - drop from B1 towards pit", "Jailbreaker's Key") self._add_location_rule("ID: Covetous Gold Serpent Ring - Siegward's cell", "Old Cell Key") From 4e60f3cc54063051393cdd7bd253464398a08146 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Tue, 17 Sep 2024 17:00:26 -0500 Subject: [PATCH 29/94] The Messenger: Fix Portal Plando Issues (#3838) * add a more clear error message for a missing exit * remove portal region from the available pool * ensure plando portals are in the correct spot in the list and it gets cleared correctly --- worlds/messenger/portals.py | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/worlds/messenger/portals.py b/worlds/messenger/portals.py index 1da210cb23ff..17152a1a1538 100644 --- a/worlds/messenger/portals.py +++ b/worlds/messenger/portals.py @@ -215,13 +215,13 @@ def create_mapping(in_portal: str, warp: str) -> str: if "Portal" in warp: exit_string += "Portal" - world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}00")) + world.portal_mapping.insert(PORTALS.index(in_portal), int(f"{REGION_ORDER.index(parent)}00")) elif warp in SHOP_POINTS[parent]: exit_string += f"{warp} Shop" - world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}1{SHOP_POINTS[parent].index(warp)}")) + world.portal_mapping.insert(PORTALS.index(in_portal), int(f"{REGION_ORDER.index(parent)}1{SHOP_POINTS[parent].index(warp)}")) else: exit_string += f"{warp} Checkpoint" - world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}2{CHECKPOINTS[parent].index(warp)}")) + world.portal_mapping.insert(PORTALS.index(in_portal), int(f"{REGION_ORDER.index(parent)}2{CHECKPOINTS[parent].index(warp)}")) world.spoiler_portal_mapping[in_portal] = exit_string connect_portal(world, in_portal, exit_string) @@ -230,12 +230,15 @@ def create_mapping(in_portal: str, warp: str) -> str: def handle_planned_portals(plando_connections: List[PlandoConnection]) -> None: """checks the provided plando connections for portals and connects them""" + nonlocal available_portals + for connection in plando_connections: - if connection.entrance not in PORTALS: - continue # let it crash here if input is invalid - create_mapping(connection.entrance, connection.exit) + available_portals.remove(connection.exit) + parent = create_mapping(connection.entrance, connection.exit) world.plando_portals.append(connection.entrance) + if shuffle_type < ShufflePortals.option_anywhere: + available_portals = [port for port in available_portals if port not in shop_points[parent]] shuffle_type = world.options.shuffle_portals shop_points = deepcopy(SHOP_POINTS) @@ -251,8 +254,13 @@ def handle_planned_portals(plando_connections: List[PlandoConnection]) -> None: plando = world.options.portal_plando.value if not plando: plando = world.options.plando_connections.value - if plando and world.multiworld.plando_options & PlandoOptions.connections: - handle_planned_portals(plando) + if plando and world.multiworld.plando_options & PlandoOptions.connections and not world.plando_portals: + try: + handle_planned_portals(plando) + # any failure i expect will trigger on available_portals.remove + except ValueError: + raise ValueError(f"Unable to complete portal plando for Player {world.player_name}. " + f"If you attempted to plando a checkpoint, checkpoints must be shuffled.") for portal in PORTALS: if portal in world.plando_portals: @@ -276,8 +284,13 @@ def disconnect_portals(world: "MessengerWorld") -> None: entrance.connected_region = None if portal in world.spoiler_portal_mapping: del world.spoiler_portal_mapping[portal] - if len(world.portal_mapping) > len(world.spoiler_portal_mapping): - world.portal_mapping = world.portal_mapping[:len(world.spoiler_portal_mapping)] + if world.plando_portals: + indexes = [PORTALS.index(portal) for portal in world.plando_portals] + planned_portals = [] + for index, portal_coord in enumerate(world.portal_mapping): + if index in indexes: + planned_portals.append(portal_coord) + world.portal_mapping = planned_portals def validate_portals(world: "MessengerWorld") -> bool: From a7c96436d9b8f51b98533141815fc4658cbb256a Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Wed, 18 Sep 2024 01:03:33 +0300 Subject: [PATCH 30/94] Stardew valley: Add Marlon bedroom entrance rule (#3735) * - Created a test for the "Mapping Cave Systems" book * - Added missing rule to marlon's bedroom * - Can kill any monster, not just green slime * - Added a compound source structure, but I ended up deciding to not use it here. Still keeping it as it will probably be useful eventually * - Use the compound source of the monster compoundium (ironic, I know) * - Add required elevators --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- .../content/vanilla/pelican_town.py | 8 +++--- worlds/stardew_valley/data/game_item.py | 5 ++++ worlds/stardew_valley/logic/source_logic.py | 12 +++++++-- worlds/stardew_valley/rules.py | 2 ++ worlds/stardew_valley/test/__init__.py | 4 +-- worlds/stardew_valley/test/rules/TestBooks.py | 26 +++++++++++++++++++ 6 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 worlds/stardew_valley/test/rules/TestBooks.py diff --git a/worlds/stardew_valley/content/vanilla/pelican_town.py b/worlds/stardew_valley/content/vanilla/pelican_town.py index 220b46eae2a4..73cc8f119a3e 100644 --- a/worlds/stardew_valley/content/vanilla/pelican_town.py +++ b/worlds/stardew_valley/content/vanilla/pelican_town.py @@ -1,6 +1,6 @@ from ..game_content import ContentPack from ...data import villagers_data, fish_data -from ...data.game_item import GenericSource, ItemTag, Tag, CustomRuleSource +from ...data.game_item import GenericSource, ItemTag, Tag, CustomRuleSource, CompoundSource from ...data.harvest import ForagingSource, SeasonalForagingSource, ArtifactSpotSource from ...data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement from ...data.shop import ShopSource, MysteryBoxSource, ArtifactTroveSource, PrizeMachineSource, FishingTreasureChestSource @@ -229,8 +229,10 @@ ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), Book.mapping_cave_systems: ( Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), - GenericSource(regions=(Region.adventurer_guild_bedroom,)), - ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), + CompoundSource(sources=( + GenericSource(regions=(Region.adventurer_guild_bedroom,)), + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3), + ))), Book.monster_compendium: ( Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), CustomRuleSource(create_rule=lambda logic: logic.monster.can_kill_many(Generic.any)), diff --git a/worlds/stardew_valley/data/game_item.py b/worlds/stardew_valley/data/game_item.py index 2107ca30d33a..6c8d30ed8e6f 100644 --- a/worlds/stardew_valley/data/game_item.py +++ b/worlds/stardew_valley/data/game_item.py @@ -59,6 +59,11 @@ class CustomRuleSource(ItemSource): create_rule: Callable[[Any], StardewRule] +@dataclass(frozen=True, **kw_only) +class CompoundSource(ItemSource): + sources: Tuple[ItemSource, ...] = () + + class Tag(ItemSource): """Not a real source, just a way to add tags to an item. Will be removed from the item sources during unpacking.""" tag: Tuple[ItemTag, ...] diff --git a/worlds/stardew_valley/logic/source_logic.py b/worlds/stardew_valley/logic/source_logic.py index 0e9b8e976f5b..9ef68a020eef 100644 --- a/worlds/stardew_valley/logic/source_logic.py +++ b/worlds/stardew_valley/logic/source_logic.py @@ -12,7 +12,7 @@ from .requirement_logic import RequirementLogicMixin from .tool_logic import ToolLogicMixin from ..data.artisan import MachineSource -from ..data.game_item import GenericSource, ItemSource, GameItem, CustomRuleSource +from ..data.game_item import GenericSource, ItemSource, GameItem, CustomRuleSource, CompoundSource from ..data.harvest import ForagingSource, FruitBatsSource, MushroomCaveSource, SeasonalForagingSource, \ HarvestCropSource, HarvestFruitTreeSource, ArtifactSpotSource from ..data.shop import ShopSource, MysteryBoxSource, ArtifactTroveSource, PrizeMachineSource, FishingTreasureChestSource @@ -25,7 +25,7 @@ def __init__(self, *args, **kwargs): class SourceLogic(BaseLogic[Union[SourceLogicMixin, HasLogicMixin, ReceivedLogicMixin, HarvestingLogicMixin, MoneyLogicMixin, RegionLogicMixin, -ArtisanLogicMixin, ToolLogicMixin, RequirementLogicMixin, GrindLogicMixin]]): + ArtisanLogicMixin, ToolLogicMixin, RequirementLogicMixin, GrindLogicMixin]]): def has_access_to_item(self, item: GameItem): rules = [] @@ -40,6 +40,10 @@ def has_access_to_any(self, sources: Iterable[ItemSource]): return self.logic.or_(*(self.logic.source.has_access_to(source) & self.logic.requirement.meet_all_requirements(source.other_requirements) for source in sources)) + def has_access_to_all(self, sources: Iterable[ItemSource]): + return self.logic.and_(*(self.logic.source.has_access_to(source) & self.logic.requirement.meet_all_requirements(source.other_requirements) + for source in sources)) + @functools.singledispatchmethod def has_access_to(self, source: Any): raise ValueError(f"Sources of type{type(source)} have no rule registered.") @@ -52,6 +56,10 @@ def _(self, source: GenericSource): def _(self, source: CustomRuleSource): return source.create_rule(self.logic) + @has_access_to.register + def _(self, source: CompoundSource): + return self.logic.source.has_access_to_all(source.sources) + @has_access_to.register def _(self, source: ForagingSource): return self.logic.harvesting.can_forage_from(source) diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index e9bdd8c25bbb..7f39ee1ac2d4 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -39,6 +39,7 @@ from .strings.entrance_names import dig_to_mines_floor, dig_to_skull_floor, Entrance, move_to_woods_depth, DeepWoodsEntrance, AlecEntrance, \ SVEEntrance, LaceyEntrance, BoardingHouseEntrance, LogicEntrance from .strings.forageable_names import Forageable +from .strings.generic_names import Generic from .strings.geode_names import Geode from .strings.material_names import Material from .strings.metal_names import MetalBar, Mineral @@ -263,6 +264,7 @@ def set_entrance_rules(logic: StardewLogic, multiworld, player, world_options: S set_entrance_rule(multiworld, player, LogicEntrance.buy_experience_books, logic.time.has_lived_months(2)) set_entrance_rule(multiworld, player, LogicEntrance.buy_year1_books, logic.time.has_year_two) set_entrance_rule(multiworld, player, LogicEntrance.buy_year3_books, logic.time.has_year_three) + set_entrance_rule(multiworld, player, Entrance.adventurer_guild_to_bedroom, logic.monster.can_kill_max(Generic.any)) def set_dangerous_mine_rules(logic, multiworld, player, world_options: StardewValleyOptions): diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index e7278cba2800..3fe05d205ce0 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -256,10 +256,10 @@ def run_default_tests(self) -> bool: return False return super().run_default_tests - def collect_lots_of_money(self): + def collect_lots_of_money(self, percent: float = 0.25): self.multiworld.state.collect(self.world.create_item("Shipping Bin"), prevent_sweep=False) real_total_prog_items = self.multiworld.worlds[self.player].total_progression_items - required_prog_items = int(round(real_total_prog_items * 0.25)) + required_prog_items = int(round(real_total_prog_items * percent)) for i in range(required_prog_items): self.multiworld.state.collect(self.world.create_item("Stardrop"), prevent_sweep=False) self.multiworld.worlds[self.player].total_progression_items = real_total_prog_items diff --git a/worlds/stardew_valley/test/rules/TestBooks.py b/worlds/stardew_valley/test/rules/TestBooks.py new file mode 100644 index 000000000000..6605e7e645e3 --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestBooks.py @@ -0,0 +1,26 @@ +from ... import options +from ...test import SVTestBase + + +class TestBooksLogic(SVTestBase): + options = { + options.Booksanity.internal_name: options.Booksanity.option_all, + } + + def test_need_weapon_for_mapping_cave_systems(self): + self.collect_lots_of_money(0.5) + + location = self.multiworld.get_location("Read Mapping Cave Systems", self.player) + + self.assert_reach_location_false(location, self.multiworld.state) + + self.collect("Progressive Mine Elevator") + self.collect("Progressive Mine Elevator") + self.collect("Progressive Mine Elevator") + self.collect("Progressive Mine Elevator") + self.assert_reach_location_false(location, self.multiworld.state) + + self.collect("Progressive Weapon") + self.assert_reach_location_true(location, self.multiworld.state) + + From 8c5b65ff26ef7042e94ae7cf211a1e41fdbd31ee Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Wed, 18 Sep 2024 01:07:40 +0300 Subject: [PATCH 31/94] Stardew Valley: Remove Accessibility and progression balancing from presets #3833 --- worlds/stardew_valley/presets.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/worlds/stardew_valley/presets.py b/worlds/stardew_valley/presets.py index cf6f87a1501c..1861a914235c 100644 --- a/worlds/stardew_valley/presets.py +++ b/worlds/stardew_valley/presets.py @@ -57,8 +57,6 @@ } easy_settings = { - "progression_balancing": ProgressionBalancing.default, - "accessibility": Accessibility.option_full, Goal.internal_name: Goal.option_community_center, FarmType.internal_name: "random", StartingMoney.internal_name: "very rich", @@ -103,8 +101,6 @@ } medium_settings = { - "progression_balancing": 25, - "accessibility": Accessibility.option_full, Goal.internal_name: Goal.option_community_center, FarmType.internal_name: "random", StartingMoney.internal_name: "rich", @@ -149,8 +145,6 @@ } hard_settings = { - "progression_balancing": 0, - "accessibility": Accessibility.option_full, Goal.internal_name: Goal.option_grandpa_evaluation, FarmType.internal_name: "random", StartingMoney.internal_name: "extra", @@ -195,8 +189,6 @@ } nightmare_settings = { - "progression_balancing": 0, - "accessibility": Accessibility.option_full, Goal.internal_name: Goal.option_community_center, FarmType.internal_name: "random", StartingMoney.internal_name: "vanilla", @@ -241,8 +233,6 @@ } short_settings = { - "progression_balancing": ProgressionBalancing.default, - "accessibility": Accessibility.option_full, Goal.internal_name: Goal.option_bottom_of_the_mines, FarmType.internal_name: "random", StartingMoney.internal_name: "filthy rich", @@ -287,8 +277,6 @@ } minsanity_settings = { - "progression_balancing": ProgressionBalancing.default, - "accessibility": Accessibility.option_minimal, Goal.internal_name: Goal.default, FarmType.internal_name: "random", StartingMoney.internal_name: StartingMoney.default, @@ -333,8 +321,6 @@ } allsanity_settings = { - "progression_balancing": ProgressionBalancing.default, - "accessibility": Accessibility.option_full, Goal.internal_name: Goal.default, FarmType.internal_name: "random", StartingMoney.internal_name: StartingMoney.default, From debb93661803dd2b114ee1975ef4dca74d7528df Mon Sep 17 00:00:00 2001 From: sgrunt Date: Tue, 17 Sep 2024 16:08:18 -0600 Subject: [PATCH 32/94] DOOM II: Fix sector 95 assignment in DOOM II MAP17 to correctly flag the BFG9000 location as in the Yellow Key area (#3705) Co-authored-by: sgrunt --- worlds/doom_ii/Locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/doom_ii/Locations.py b/worlds/doom_ii/Locations.py index 3ce87b8a6662..376f19446f21 100644 --- a/worlds/doom_ii/Locations.py +++ b/worlds/doom_ii/Locations.py @@ -1470,7 +1470,7 @@ class LocationDict(TypedDict, total=False): 'map': 6, 'index': 102, 'doom_type': 2006, - 'region': "Tenements (MAP17) Main"}, + 'region': "Tenements (MAP17) Yellow"}, 361243: {'name': 'Tenements (MAP17) - Plasma gun', 'episode': 2, 'map': 6, From 6fac83b84cab8909e8104c931781ffb9d18945b4 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 18 Sep 2024 00:18:17 +0200 Subject: [PATCH 33/94] Factorio: update API use (#3760) --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/factorio/Mod.py | 39 ++++----- worlds/factorio/Options.py | 90 +++++++++----------- worlds/factorio/Shapes.py | 10 +-- worlds/factorio/Technologies.py | 5 +- worlds/factorio/__init__.py | 141 ++++++++++++++++---------------- 5 files changed, 136 insertions(+), 149 deletions(-) diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index d7b3d4b1ebca..7eec71875829 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -1,5 +1,6 @@ """Outputs a Factorio Mod to facilitate integration with Archipelago""" +import dataclasses import json import os import shutil @@ -88,6 +89,8 @@ def write_contents(self, opened_zipfile: zipfile.ZipFile): def generate_mod(world: "Factorio", output_directory: str): player = world.player multiworld = world.multiworld + random = world.random + global data_final_template, locale_template, control_template, data_template, settings_template with template_load_lock: if not data_final_template: @@ -110,8 +113,6 @@ def load_template(name: str): mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.get_file_safe_player_name(player)}" versioned_mod_name = mod_name + "_" + Utils.__version__ - random = multiworld.per_slot_randoms[player] - def flop_random(low, high, base=None): """Guarantees 50% below base and 50% above base, uniform distribution in each direction.""" if base: @@ -129,43 +130,43 @@ def flop_random(low, high, base=None): "base_tech_table": base_tech_table, "tech_to_progressive_lookup": tech_to_progressive_lookup, "mod_name": mod_name, - "allowed_science_packs": multiworld.max_science_pack[player].get_allowed_packs(), - "custom_technologies": multiworld.worlds[player].custom_technologies, + "allowed_science_packs": world.options.max_science_pack.get_allowed_packs(), + "custom_technologies": world.custom_technologies, "tech_tree_layout_prerequisites": world.tech_tree_layout_prerequisites, - "slot_name": multiworld.player_name[player], "seed_name": multiworld.seed_name, + "slot_name": world.player_name, "seed_name": multiworld.seed_name, "slot_player": player, - "starting_items": multiworld.starting_items[player], "recipes": recipes, + "starting_items": world.options.starting_items, "recipes": recipes, "random": random, "flop_random": flop_random, - "recipe_time_scale": recipe_time_scales.get(multiworld.recipe_time[player].value, None), - "recipe_time_range": recipe_time_ranges.get(multiworld.recipe_time[player].value, None), + "recipe_time_scale": recipe_time_scales.get(world.options.recipe_time.value, None), + "recipe_time_range": recipe_time_ranges.get(world.options.recipe_time.value, None), "free_sample_blacklist": {item: 1 for item in free_sample_exclusions}, "progressive_technology_table": {tech.name: tech.progressive for tech in progressive_technology_table.values()}, "custom_recipes": world.custom_recipes, - "max_science_pack": multiworld.max_science_pack[player].value, + "max_science_pack": world.options.max_science_pack.value, "liquids": fluids, - "goal": multiworld.goal[player].value, - "energy_link": multiworld.energy_link[player].value, + "goal": world.options.goal.value, + "energy_link": world.options.energy_link.value, "useless_technologies": useless_technologies, - "chunk_shuffle": multiworld.chunk_shuffle[player].value if hasattr(multiworld, "chunk_shuffle") else 0, + "chunk_shuffle": 0, } - for factorio_option in Options.factorio_options: + for factorio_option, factorio_option_instance in dataclasses.asdict(world.options).items(): if factorio_option in ["free_sample_blacklist", "free_sample_whitelist"]: continue - template_data[factorio_option] = getattr(multiworld, factorio_option)[player].value + template_data[factorio_option] = factorio_option_instance.value - if getattr(multiworld, "silo")[player].value == Options.Silo.option_randomize_recipe: + if world.options.silo == Options.Silo.option_randomize_recipe: template_data["free_sample_blacklist"]["rocket-silo"] = 1 - if getattr(multiworld, "satellite")[player].value == Options.Satellite.option_randomize_recipe: + if world.options.satellite == Options.Satellite.option_randomize_recipe: template_data["free_sample_blacklist"]["satellite"] = 1 - template_data["free_sample_blacklist"].update({item: 1 for item in multiworld.free_sample_blacklist[player].value}) - template_data["free_sample_blacklist"].update({item: 0 for item in multiworld.free_sample_whitelist[player].value}) + template_data["free_sample_blacklist"].update({item: 1 for item in world.options.free_sample_blacklist.value}) + template_data["free_sample_blacklist"].update({item: 0 for item in world.options.free_sample_whitelist.value}) zf_path = os.path.join(output_directory, versioned_mod_name + ".zip") - mod = FactorioModFile(zf_path, player=player, player_name=multiworld.player_name[player]) + mod = FactorioModFile(zf_path, player=player, player_name=world.player_name) if world.zip_path: with zipfile.ZipFile(world.zip_path) as zf: diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index 3429ebbd4251..788d1f9e1d92 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -1,11 +1,14 @@ from __future__ import annotations -import typing + +from dataclasses import dataclass import datetime +import typing -from Options import Choice, OptionDict, OptionSet, Option, DefaultOnToggle, Range, DeathLink, Toggle, \ - StartInventoryPool from schema import Schema, Optional, And, Or +from Options import Choice, OptionDict, OptionSet, Option, DefaultOnToggle, Range, DeathLink, Toggle, \ + StartInventoryPool, PerGameCommonOptions + # schema helpers FloatRange = lambda low, high: And(Or(int, float), lambda f: low <= f <= high) LuaBool = Or(bool, And(int, lambda n: n in (0, 1))) @@ -422,50 +425,37 @@ class EnergyLink(Toggle): display_name = "EnergyLink" -factorio_options: typing.Dict[str, type(Option)] = { - "max_science_pack": MaxSciencePack, - "goal": Goal, - "tech_tree_layout": TechTreeLayout, - "min_tech_cost": MinTechCost, - "max_tech_cost": MaxTechCost, - "tech_cost_distribution": TechCostDistribution, - "tech_cost_mix": TechCostMix, - "ramping_tech_costs": RampingTechCosts, - "silo": Silo, - "satellite": Satellite, - "free_samples": FreeSamples, - "tech_tree_information": TechTreeInformation, - "starting_items": FactorioStartItems, - "free_sample_blacklist": FactorioFreeSampleBlacklist, - "free_sample_whitelist": FactorioFreeSampleWhitelist, - "recipe_time": RecipeTime, - "recipe_ingredients": RecipeIngredients, - "recipe_ingredients_offset": RecipeIngredientsOffset, - "imported_blueprints": ImportedBlueprint, - "world_gen": FactorioWorldGen, - "progressive": Progressive, - "teleport_traps": TeleportTrapCount, - "grenade_traps": GrenadeTrapCount, - "cluster_grenade_traps": ClusterGrenadeTrapCount, - "artillery_traps": ArtilleryTrapCount, - "atomic_rocket_traps": AtomicRocketTrapCount, - "attack_traps": AttackTrapCount, - "evolution_traps": EvolutionTrapCount, - "evolution_trap_increase": EvolutionTrapIncrease, - "death_link": DeathLink, - "energy_link": EnergyLink, - "start_inventory_from_pool": StartInventoryPool, -} - -# spoilers below. If you spoil it for yourself, please at least don't spoil it for anyone else. -if datetime.datetime.today().month == 4: - - class ChunkShuffle(Toggle): - """Entrance Randomizer.""" - display_name = "Chunk Shuffle" - - - if datetime.datetime.today().day > 1: - ChunkShuffle.__doc__ += """ - 2023 April Fool's option. Shuffles chunk border transitions.""" - factorio_options["chunk_shuffle"] = ChunkShuffle +@dataclass +class FactorioOptions(PerGameCommonOptions): + max_science_pack: MaxSciencePack + goal: Goal + tech_tree_layout: TechTreeLayout + min_tech_cost: MinTechCost + max_tech_cost: MaxTechCost + tech_cost_distribution: TechCostDistribution + tech_cost_mix: TechCostMix + ramping_tech_costs: RampingTechCosts + silo: Silo + satellite: Satellite + free_samples: FreeSamples + tech_tree_information: TechTreeInformation + starting_items: FactorioStartItems + free_sample_blacklist: FactorioFreeSampleBlacklist + free_sample_whitelist: FactorioFreeSampleWhitelist + recipe_time: RecipeTime + recipe_ingredients: RecipeIngredients + recipe_ingredients_offset: RecipeIngredientsOffset + imported_blueprints: ImportedBlueprint + world_gen: FactorioWorldGen + progressive: Progressive + teleport_traps: TeleportTrapCount + grenade_traps: GrenadeTrapCount + cluster_grenade_traps: ClusterGrenadeTrapCount + artillery_traps: ArtilleryTrapCount + atomic_rocket_traps: AtomicRocketTrapCount + attack_traps: AttackTrapCount + evolution_traps: EvolutionTrapCount + evolution_trap_increase: EvolutionTrapIncrease + death_link: DeathLink + energy_link: EnergyLink + start_inventory_from_pool: StartInventoryPool diff --git a/worlds/factorio/Shapes.py b/worlds/factorio/Shapes.py index d40871f7fa82..2a81cc3fb004 100644 --- a/worlds/factorio/Shapes.py +++ b/worlds/factorio/Shapes.py @@ -19,12 +19,10 @@ def _sorter(location: "FactorioScienceLocation"): return location.complexity, location.rel_cost -def get_shapes(factorio_world: "Factorio") -> Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]]: - world = factorio_world.multiworld - player = factorio_world.player +def get_shapes(world: "Factorio") -> Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]]: prerequisites: Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]] = {} - layout = world.tech_tree_layout[player].value - locations: List["FactorioScienceLocation"] = sorted(factorio_world.science_locations, key=lambda loc: loc.name) + layout = world.options.tech_tree_layout.value + locations: List["FactorioScienceLocation"] = sorted(world.science_locations, key=lambda loc: loc.name) world.random.shuffle(locations) if layout == TechTreeLayout.option_single: @@ -247,5 +245,5 @@ def get_shapes(factorio_world: "Factorio") -> Dict["FactorioScienceLocation", Se else: raise NotImplementedError(f"Layout {layout} is not implemented.") - factorio_world.tech_tree_layout_prerequisites = prerequisites + world.tech_tree_layout_prerequisites = prerequisites return prerequisites diff --git a/worlds/factorio/Technologies.py b/worlds/factorio/Technologies.py index 096396c0e774..112cc49f0920 100644 --- a/worlds/factorio/Technologies.py +++ b/worlds/factorio/Technologies.py @@ -13,12 +13,11 @@ from . import Options factorio_tech_id = factorio_base_id = 2 ** 17 -# Factorio technologies are imported from a .json document in /data -source_folder = os.path.join(os.path.dirname(__file__), "data") pool = ThreadPoolExecutor(1) +# Factorio technologies are imported from a .json document in /data def load_json_data(data_name: str) -> Union[List[str], Dict[str, Any]]: return orjson.loads(pkgutil.get_data(__name__, "data/" + data_name + ".json")) @@ -99,7 +98,7 @@ def __init__(self, origin: Technology, world, allowed_packs: Set[str], player: i and ((ingredients & {"chemical-science-pack", "production-science-pack", "utility-science-pack"}) or origin.name == "rocket-silo") self.player = player - if origin.name not in world.worlds[player].special_nodes: + if origin.name not in world.special_nodes: if military_allowed: ingredients.add("military-science-pack") ingredients = list(ingredients) diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 753c567286e0..925327655a24 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -11,7 +11,7 @@ from worlds.generic import Rules from .Locations import location_pools, location_table from .Mod import generate_mod -from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, TechCostDistribution +from .Options import FactorioOptions, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, TechCostDistribution from .Shapes import get_shapes from .Technologies import base_tech_table, recipe_sources, base_technology_table, \ all_ingredient_names, all_product_sources, required_technologies, get_rocket_requirements, \ @@ -89,13 +89,15 @@ class Factorio(World): advancement_technologies: typing.Set[str] web = FactorioWeb() + options_dataclass = FactorioOptions + options: FactorioOptions item_name_to_id = all_items location_name_to_id = location_table item_name_groups = { "Progressive": set(progressive_tech_table.keys()), } - required_client_version = (0, 4, 2) + required_client_version = (0, 5, 0) ordered_science_packs: typing.List[str] = MaxSciencePack.get_ordered_science_packs() tech_tree_layout_prerequisites: typing.Dict[FactorioScienceLocation, typing.Set[FactorioScienceLocation]] @@ -117,32 +119,32 @@ def __init__(self, world, player: int): def generate_early(self) -> None: # if max < min, then swap max and min - if self.multiworld.max_tech_cost[self.player] < self.multiworld.min_tech_cost[self.player]: - self.multiworld.min_tech_cost[self.player].value, self.multiworld.max_tech_cost[self.player].value = \ - self.multiworld.max_tech_cost[self.player].value, self.multiworld.min_tech_cost[self.player].value - self.tech_mix = self.multiworld.tech_cost_mix[self.player] - self.skip_silo = self.multiworld.silo[self.player].value == Silo.option_spawn + if self.options.max_tech_cost < self.options.min_tech_cost: + self.options.min_tech_cost.value, self.options.max_tech_cost.value = \ + self.options.max_tech_cost.value, self.options.min_tech_cost.value + self.tech_mix = self.options.tech_cost_mix.value + self.skip_silo = self.options.silo.value == Silo.option_spawn def create_regions(self): player = self.player - random = self.multiworld.random + random = self.random nauvis = Region("Nauvis", player, self.multiworld) location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \ - self.multiworld.evolution_traps[player] + \ - self.multiworld.attack_traps[player] + \ - self.multiworld.teleport_traps[player] + \ - self.multiworld.grenade_traps[player] + \ - self.multiworld.cluster_grenade_traps[player] + \ - self.multiworld.atomic_rocket_traps[player] + \ - self.multiworld.artillery_traps[player] + self.options.evolution_traps + \ + self.options.attack_traps + \ + self.options.teleport_traps + \ + self.options.grenade_traps + \ + self.options.cluster_grenade_traps + \ + self.options.atomic_rocket_traps + \ + self.options.artillery_traps location_pool = [] - for pack in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()): + for pack in sorted(self.options.max_science_pack.get_allowed_packs()): location_pool.extend(location_pools[pack]) try: - location_names = self.multiworld.random.sample(location_pool, location_count) + location_names = random.sample(location_pool, location_count) except ValueError as e: # should be "ValueError: Sample larger than population or is negative" raise Exception("Too many traps for too few locations. Either decrease the trap count, " @@ -150,9 +152,9 @@ def create_regions(self): self.science_locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis) for loc_name in location_names] - distribution: TechCostDistribution = self.multiworld.tech_cost_distribution[self.player] - min_cost = self.multiworld.min_tech_cost[self.player] - max_cost = self.multiworld.max_tech_cost[self.player] + distribution: TechCostDistribution = self.options.tech_cost_distribution + min_cost = self.options.min_tech_cost.value + max_cost = self.options.max_tech_cost.value if distribution == distribution.option_even: rand_values = (random.randint(min_cost, max_cost) for _ in self.science_locations) else: @@ -161,7 +163,7 @@ def create_regions(self): distribution.option_high: max_cost}[distribution.value] rand_values = (random.triangular(min_cost, max_cost, mode) for _ in self.science_locations) rand_values = sorted(rand_values) - if self.multiworld.ramping_tech_costs[self.player]: + if self.options.ramping_tech_costs: def sorter(loc: FactorioScienceLocation): return loc.complexity, loc.rel_cost else: @@ -176,7 +178,7 @@ def sorter(loc: FactorioScienceLocation): event = FactorioItem("Victory", ItemClassification.progression, None, player) location.place_locked_item(event) - for ingredient in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()): + for ingredient in sorted(self.options.max_science_pack.get_allowed_packs()): location = FactorioLocation(player, f"Automate {ingredient}", None, nauvis) nauvis.locations.append(location) event = FactorioItem(f"Automated {ingredient}", ItemClassification.progression, None, player) @@ -185,24 +187,23 @@ def sorter(loc: FactorioScienceLocation): self.multiworld.regions.append(nauvis) def create_items(self) -> None: - player = self.player self.custom_technologies = self.set_custom_technologies() self.set_custom_recipes() traps = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", "Atomic Rocket") for trap_name in traps: self.multiworld.itempool.extend(self.create_item(f"{trap_name} Trap") for _ in - range(getattr(self.multiworld, - f"{trap_name.lower().replace(' ', '_')}_traps")[player])) + range(getattr(self.options, + f"{trap_name.lower().replace(' ', '_')}_traps"))) - want_progressives = collections.defaultdict(lambda: self.multiworld.progressive[player]. - want_progressives(self.multiworld.random)) + want_progressives = collections.defaultdict(lambda: self.options.progressive. + want_progressives(self.random)) cost_sorted_locations = sorted(self.science_locations, key=lambda location: location.name) special_index = {"automation": 0, "logistics": 1, "rocket-silo": -1} loc: FactorioScienceLocation - if self.multiworld.tech_tree_information[player] == TechTreeInformation.option_full: + if self.options.tech_tree_information == TechTreeInformation.option_full: # mark all locations as pre-hinted for loc in self.science_locations: loc.revealed = True @@ -229,14 +230,13 @@ def create_items(self) -> None: loc.revealed = True def set_rules(self): - world = self.multiworld player = self.player shapes = get_shapes(self) - for ingredient in self.multiworld.max_science_pack[self.player].get_allowed_packs(): - location = world.get_location(f"Automate {ingredient}", player) + for ingredient in self.options.max_science_pack.get_allowed_packs(): + location = self.get_location(f"Automate {ingredient}") - if self.multiworld.recipe_ingredients[self.player]: + if self.options.recipe_ingredients: custom_recipe = self.custom_recipes[ingredient] location.access_rule = lambda state, ingredient=ingredient, custom_recipe=custom_recipe: \ @@ -257,30 +257,30 @@ def set_rules(self): prerequisites: all(state.can_reach(loc) for loc in locations)) silo_recipe = None - if self.multiworld.silo[self.player] == Silo.option_spawn: + if self.options.silo == Silo.option_spawn: silo_recipe = self.custom_recipes["rocket-silo"] if "rocket-silo" in self.custom_recipes \ else next(iter(all_product_sources.get("rocket-silo"))) part_recipe = self.custom_recipes["rocket-part"] satellite_recipe = None - if self.multiworld.goal[self.player] == Goal.option_satellite: + if self.options.goal == Goal.option_satellite: satellite_recipe = self.custom_recipes["satellite"] if "satellite" in self.custom_recipes \ else next(iter(all_product_sources.get("satellite"))) victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe, satellite_recipe) - if self.multiworld.silo[self.player] != Silo.option_spawn: + if self.options.silo != Silo.option_spawn: victory_tech_names.add("rocket-silo") - world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player) - for technology in - victory_tech_names) + self.get_location("Rocket Launch").access_rule = lambda state: all(state.has(technology, player) + for technology in + victory_tech_names) - world.completion_condition[player] = lambda state: state.has('Victory', player) + self.multiworld.completion_condition[player] = lambda state: state.has('Victory', player) def generate_basic(self): - map_basic_settings = self.multiworld.world_gen[self.player].value["basic"] + map_basic_settings = self.options.world_gen.value["basic"] if map_basic_settings.get("seed", None) is None: # allow seed 0 # 32 bit uint - map_basic_settings["seed"] = self.multiworld.per_slot_randoms[self.player].randint(0, 2 ** 32 - 1) + map_basic_settings["seed"] = self.random.randint(0, 2 ** 32 - 1) - start_location_hints: typing.Set[str] = self.multiworld.start_location_hints[self.player].value + start_location_hints: typing.Set[str] = self.options.start_location_hints.value for loc in self.science_locations: # show start_location_hints ingame @@ -304,8 +304,6 @@ def collect_item(self, state, item, remove=False): return super(Factorio, self).collect_item(state, item, remove) - option_definitions = factorio_options - @classmethod def stage_write_spoiler(cls, world, spoiler_handle): factorio_players = world.get_game_players(cls.game) @@ -345,7 +343,7 @@ def make_balanced_recipe(self, original: Recipe, pool: typing.Set[str], factor: # have to first sort for determinism, while filtering out non-stacking items pool: typing.List[str] = sorted(pool & valid_ingredients) # then sort with random data to shuffle - self.multiworld.random.shuffle(pool) + self.random.shuffle(pool) target_raw = int(sum((count for ingredient, count in original.base_cost.items())) * factor) target_energy = original.total_energy * factor target_num_ingredients = len(original.ingredients) + ingredients_offset @@ -389,7 +387,7 @@ def make_balanced_recipe(self, original: Recipe, pool: typing.Set[str], factor: if min_num > max_num: fallback_pool.append(ingredient) continue # can't use that ingredient - num = self.multiworld.random.randint(min_num, max_num) + num = self.random.randint(min_num, max_num) new_ingredients[ingredient] = num remaining_raw -= num * ingredient_raw remaining_energy -= num * ingredient_energy @@ -433,66 +431,66 @@ def make_balanced_recipe(self, original: Recipe, pool: typing.Set[str], factor: def set_custom_technologies(self): custom_technologies = {} - allowed_packs = self.multiworld.max_science_pack[self.player].get_allowed_packs() + allowed_packs = self.options.max_science_pack.get_allowed_packs() for technology_name, technology in base_technology_table.items(): - custom_technologies[technology_name] = technology.get_custom(self.multiworld, allowed_packs, self.player) + custom_technologies[technology_name] = technology.get_custom(self, allowed_packs, self.player) return custom_technologies def set_custom_recipes(self): - ingredients_offset = self.multiworld.recipe_ingredients_offset[self.player] + ingredients_offset = self.options.recipe_ingredients_offset original_rocket_part = recipes["rocket-part"] science_pack_pools = get_science_pack_pools() - valid_pool = sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_max_pack()] & valid_ingredients) - self.multiworld.random.shuffle(valid_pool) + valid_pool = sorted(science_pack_pools[self.options.max_science_pack.get_max_pack()] & valid_ingredients) + self.random.shuffle(valid_pool) self.custom_recipes = {"rocket-part": Recipe("rocket-part", original_rocket_part.category, {valid_pool[x]: 10 for x in range(3 + ingredients_offset)}, original_rocket_part.products, original_rocket_part.energy)} - if self.multiworld.recipe_ingredients[self.player]: + if self.options.recipe_ingredients: valid_pool = [] - for pack in self.multiworld.max_science_pack[self.player].get_ordered_science_packs(): + for pack in self.options.max_science_pack.get_ordered_science_packs(): valid_pool += sorted(science_pack_pools[pack]) - self.multiworld.random.shuffle(valid_pool) + self.random.shuffle(valid_pool) if pack in recipes: # skips over space science pack new_recipe = self.make_quick_recipe(recipes[pack], valid_pool, ingredients_offset= - ingredients_offset) + ingredients_offset.value) self.custom_recipes[pack] = new_recipe - if self.multiworld.silo[self.player].value == Silo.option_randomize_recipe \ - or self.multiworld.satellite[self.player].value == Satellite.option_randomize_recipe: + if self.options.silo.value == Silo.option_randomize_recipe \ + or self.options.satellite.value == Satellite.option_randomize_recipe: valid_pool = set() - for pack in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()): + for pack in sorted(self.options.max_science_pack.get_allowed_packs()): valid_pool |= science_pack_pools[pack] - if self.multiworld.silo[self.player].value == Silo.option_randomize_recipe: + if self.options.silo.value == Silo.option_randomize_recipe: new_recipe = self.make_balanced_recipe( recipes["rocket-silo"], valid_pool, - factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7, - ingredients_offset=ingredients_offset) + factor=(self.options.max_science_pack.value + 1) / 7, + ingredients_offset=ingredients_offset.value) self.custom_recipes["rocket-silo"] = new_recipe - if self.multiworld.satellite[self.player].value == Satellite.option_randomize_recipe: + if self.options.satellite.value == Satellite.option_randomize_recipe: new_recipe = self.make_balanced_recipe( recipes["satellite"], valid_pool, - factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7, - ingredients_offset=ingredients_offset) + factor=(self.options.max_science_pack.value + 1) / 7, + ingredients_offset=ingredients_offset.value) self.custom_recipes["satellite"] = new_recipe bridge = "ap-energy-bridge" new_recipe = self.make_quick_recipe( Recipe(bridge, "crafting", {"replace_1": 1, "replace_2": 1, "replace_3": 1, "replace_4": 1, "replace_5": 1, "replace_6": 1}, {bridge: 1}, 10), - sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_ordered_science_packs()[0]]), - ingredients_offset=ingredients_offset) + sorted(science_pack_pools[self.options.max_science_pack.get_ordered_science_packs()[0]]), + ingredients_offset=ingredients_offset.value) for ingredient_name in new_recipe.ingredients: - new_recipe.ingredients[ingredient_name] = self.multiworld.random.randint(50, 500) + new_recipe.ingredients[ingredient_name] = self.random.randint(50, 500) self.custom_recipes[bridge] = new_recipe - needed_recipes = self.multiworld.max_science_pack[self.player].get_allowed_packs() | {"rocket-part"} - if self.multiworld.silo[self.player] != Silo.option_spawn: + needed_recipes = self.options.max_science_pack.get_allowed_packs() | {"rocket-part"} + if self.options.silo != Silo.option_spawn: needed_recipes |= {"rocket-silo"} - if self.multiworld.goal[self.player].value == Goal.option_satellite: + if self.options.goal.value == Goal.option_satellite: needed_recipes |= {"satellite"} for recipe in needed_recipes: @@ -542,7 +540,8 @@ def __init__(self, player: int, name: str, address: int, parent: Region): self.ingredients = {Factorio.ordered_science_packs[self.complexity]: 1} for complexity in range(self.complexity): - if parent.multiworld.tech_cost_mix[self.player] > parent.multiworld.random.randint(0, 99): + if (parent.multiworld.worlds[self.player].options.tech_cost_mix > + parent.multiworld.worlds[self.player].random.randint(0, 99)): self.ingredients[Factorio.ordered_science_packs[complexity]] = 1 @property From f73c0d9894e3ecbc9baec9c9a465a5f1cdd4fd18 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Wed, 18 Sep 2024 00:47:26 +0200 Subject: [PATCH 34/94] WebHost: Better host room v2 (#3948) * WebHost: add spinner to room command and show error message if fetch fails due to NetworkError * WebHost: don't update room log while tab is inactive * WebHost: don't include log for automated requests * WebHost: refresh room also for re-spinups and do that from javascript * Test, WebHost: send fake user-agent where required * WebHost: remove wrong comment in host room --- WebHostLib/misc.py | 35 ++++-- WebHostLib/static/styles/hostRoom.css | 25 +++++ WebHostLib/templates/hostRoom.html | 151 +++++++++++++++++--------- test/webhost/test_host_room.py | 3 +- 4 files changed, 154 insertions(+), 60 deletions(-) diff --git a/WebHostLib/misc.py b/WebHostLib/misc.py index 01c1ad84a707..4784fcd9da63 100644 --- a/WebHostLib/misc.py +++ b/WebHostLib/misc.py @@ -132,26 +132,41 @@ def display_log(room: UUID) -> Union[str, Response, Tuple[str, int]]: return "Access Denied", 403 -@app.route('/room/', methods=['GET', 'POST']) +@app.post("/room/") +def host_room_command(room: UUID): + room: Room = Room.get(id=room) + if room is None: + return abort(404) + + if room.owner == session["_id"]: + cmd = request.form["cmd"] + if cmd: + Command(room=room, commandtext=cmd) + commit() + return redirect(url_for("host_room", room=room.id)) + + +@app.get("/room/") def host_room(room: UUID): room: Room = Room.get(id=room) if room is None: return abort(404) - if request.method == "POST": - if room.owner == session["_id"]: - cmd = request.form["cmd"] - if cmd: - Command(room=room, commandtext=cmd) - commit() - return redirect(url_for("host_room", room=room.id)) now = datetime.datetime.utcnow() # indicate that the page should reload to get the assigned port - should_refresh = not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3) + should_refresh = ((not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3)) + or room.last_activity < now - datetime.timedelta(seconds=room.timeout)) with db_session: room.last_activity = now # will trigger a spinup, if it's not already running - def get_log(max_size: int = 1024000) -> str: + browser_tokens = "Mozilla", "Chrome", "Safari" + automated = ("update" in request.args + or "Discordbot" in request.user_agent.string + or not any(browser_token in request.user_agent.string for browser_token in browser_tokens)) + + def get_log(max_size: int = 0 if automated else 1024000) -> str: + if max_size == 0: + return "…" try: with open(os.path.join("logs", str(room.id) + ".txt"), "rb") as log: raw_size = 0 diff --git a/WebHostLib/static/styles/hostRoom.css b/WebHostLib/static/styles/hostRoom.css index 827f74c04df7..625b78cc5d3f 100644 --- a/WebHostLib/static/styles/hostRoom.css +++ b/WebHostLib/static/styles/hostRoom.css @@ -58,3 +58,28 @@ overflow-y: auto; max-height: 400px; } + +.loader{ + display: inline-block; + visibility: hidden; + margin-left: 5px; + width: 40px; + aspect-ratio: 4; + --_g: no-repeat radial-gradient(circle closest-side,#fff 90%,#fff0); + background: + var(--_g) 0 50%, + var(--_g) 50% 50%, + var(--_g) 100% 50%; + background-size: calc(100%/3) 100%; + animation: l7 1s infinite linear; +} + +.loader.loading{ + visibility: visible; +} + +@keyframes l7{ + 33%{background-size:calc(100%/3) 0% ,calc(100%/3) 100%,calc(100%/3) 100%} + 50%{background-size:calc(100%/3) 100%,calc(100%/3) 0 ,calc(100%/3) 100%} + 66%{background-size:calc(100%/3) 100%,calc(100%/3) 100%,calc(100%/3) 0 } +} diff --git a/WebHostLib/templates/hostRoom.html b/WebHostLib/templates/hostRoom.html index fa8e26c2cbf8..8e76dafc12fa 100644 --- a/WebHostLib/templates/hostRoom.html +++ b/WebHostLib/templates/hostRoom.html @@ -19,28 +19,30 @@ {% block body %} {% include 'header/grassHeader.html' %}

- {% if room.owner == session["_id"] %} - Room created from Seed #{{ room.seed.id|suuid }} -
- {% endif %} - {% if room.tracker %} - This room has a Multiworld Tracker - and a Sphere Tracker enabled. -
- {% endif %} - The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity. - Should you wish to continue later, - anyone can simply refresh this page and the server will resume.
- {% if room.last_port == -1 %} - There was an error hosting this Room. Another attempt will be made on refreshing this page. - The most likely failure reason is that the multiworld is too old to be loaded now. - {% elif room.last_port %} - You can connect to this room by using - '/connect {{ config['HOST_ADDRESS'] }}:{{ room.last_port }}' - - in the client.
- {% endif %} + + {% if room.owner == session["_id"] %} + Room created from Seed #{{ room.seed.id|suuid }} +
+ {% endif %} + {% if room.tracker %} + This room has a Multiworld Tracker + and a Sphere Tracker enabled. +
+ {% endif %} + The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity. + Should you wish to continue later, + anyone can simply refresh this page and the server will resume.
+ {% if room.last_port == -1 %} + There was an error hosting this Room. Another attempt will be made on refreshing this page. + The most likely failure reason is that the multiworld is too old to be loaded now. + {% elif room.last_port %} + You can connect to this room by using + '/connect {{ config['HOST_ADDRESS'] }}:{{ room.last_port }}' + + in the client.
+ {% endif %} +
{{ macros.list_patches_room(room) }} {% if room.owner == session["_id"] %}
@@ -49,6 +51,7 @@ +
@@ -62,6 +65,7 @@ let url = '{{ url_for('display_log', room = room.id) }}'; let bytesReceived = {{ log_len }}; let updateLogTimeout; + let updateLogImmediately = false; let awaitingCommandResponse = false; let logger = document.getElementById("logger"); @@ -78,29 +82,36 @@ async function updateLog() { try { - let res = await fetch(url, { - headers: { - 'Range': `bytes=${bytesReceived}-`, - } - }); - if (res.ok) { - let text = await res.text(); - if (text.length > 0) { - awaitingCommandResponse = false; - if (bytesReceived === 0 || res.status !== 206) { - logger.innerHTML = ''; + if (!document.hidden) { + updateLogImmediately = false; + let res = await fetch(url, { + headers: { + 'Range': `bytes=${bytesReceived}-`, } - if (res.status !== 206) { - bytesReceived = 0; - } else { - bytesReceived += new Blob([text]).size; + }); + if (res.ok) { + let text = await res.text(); + if (text.length > 0) { + awaitingCommandResponse = false; + if (bytesReceived === 0 || res.status !== 206) { + logger.innerHTML = ''; + } + if (res.status !== 206) { + bytesReceived = 0; + } else { + bytesReceived += new Blob([text]).size; + } + if (logger.innerHTML.endsWith('…')) { + logger.innerHTML = logger.innerHTML.substring(0, logger.innerHTML.length - 1); + } + logger.appendChild(document.createTextNode(text)); + scrollToBottom(logger); + let loader = document.getElementById("command-form").getElementsByClassName("loader")[0]; + loader.classList.remove("loading"); } - if (logger.innerHTML.endsWith('…')) { - logger.innerHTML = logger.innerHTML.substring(0, logger.innerHTML.length - 1); - } - logger.appendChild(document.createTextNode(text)); - scrollToBottom(logger); } + } else { + updateLogImmediately = true; } } finally { @@ -125,20 +136,62 @@ }); ev.preventDefault(); // has to happen before first await form.reset(); - let res = await req; - if (res.ok || res.type === 'opaqueredirect') { - awaitingCommandResponse = true; - window.clearTimeout(updateLogTimeout); - updateLogTimeout = window.setTimeout(updateLog, 100); - } else { - window.alert(res.statusText); + let loader = form.getElementsByClassName("loader")[0]; + loader.classList.add("loading"); + try { + let res = await req; + if (res.ok || res.type === 'opaqueredirect') { + awaitingCommandResponse = true; + window.clearTimeout(updateLogTimeout); + updateLogTimeout = window.setTimeout(updateLog, 100); + } else { + loader.classList.remove("loading"); + window.alert(res.statusText); + } + } catch (e) { + console.error(e); + loader.classList.remove("loading"); + window.alert(e.message); } } document.getElementById("command-form").addEventListener("submit", postForm); updateLogTimeout = window.setTimeout(updateLog, 1000); logger.scrollTop = logger.scrollHeight; + document.addEventListener("visibilitychange", () => { + if (!document.hidden && updateLogImmediately) { + updateLog(); + } + }) {% endif %} +
{% endblock %} diff --git a/test/webhost/test_host_room.py b/test/webhost/test_host_room.py index e9dae41dd06f..4aa83e3b1c6c 100644 --- a/test/webhost/test_host_room.py +++ b/test/webhost/test_host_room.py @@ -131,7 +131,8 @@ def test_host_room_own(self) -> None: f.write(text) with self.app.app_context(), self.app.test_request_context(): - response = self.client.get(url_for("host_room", room=self.room_id)) + response = self.client.get(url_for("host_room", room=self.room_id), + headers={"User-Agent": "Mozilla/5.0"}) response_text = response.get_data(True) self.assertEqual(response.status_code, 200) self.assertIn("href=\"/seed/", response_text) From 69487661ddfcf3500a7ebac270ac1b843b116625 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Tue, 17 Sep 2024 18:33:03 -0500 Subject: [PATCH 35/94] Core: change yaml_output to output a full csv (#3653) * make yaml_output arg a bool instead of number * make yaml_output dump all player options as csv * it sorts by game so swap those columns * capitalize game and name headers * use a list and just add an if before adding instead of sorting * skip options that the world doesn't want displayed * check if the class is a subclass of Removed specifically instead of the none flag * don't create empty rows * add a header for every game option that isn't from the common ones even if they have the same name * add to webhost gen args so it can still gen --- Generate.py | 31 +++++------------------------ Main.py | 3 +++ Options.py | 44 ++++++++++++++++++++++++++++++++++++++++-- WebHostLib/generate.py | 1 + 4 files changed, 51 insertions(+), 28 deletions(-) diff --git a/Generate.py b/Generate.py index 4eba05cc52fe..2488504f3049 100644 --- a/Generate.py +++ b/Generate.py @@ -43,10 +43,10 @@ def mystery_argparse(): 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='info', help='Sets log level') - parser.add_argument('--yaml_output', default=0, type=lambda value: max(int(value), 0), - help='Output rolled mystery results to yaml up to specified number (made for async multiworld)') - parser.add_argument('--plando', default=defaults.plando_options, - help='List of options that can be set manually. Can be combined, for example "bosses, items"') + parser.add_argument("--yaml_output", action="store_true", + help="Output rolled player options to csv (made for async multiworld).") + parser.add_argument("--plando", default=defaults.plando_options, + help="List of options that can be set manually. Can be combined, for example \"bosses, items\"") parser.add_argument("--skip_prog_balancing", action="store_true", help="Skip progression balancing step during generation.") parser.add_argument("--skip_output", action="store_true", @@ -156,6 +156,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]: erargs.skip_prog_balancing = args.skip_prog_balancing erargs.skip_output = args.skip_output erargs.name = {} + erargs.yaml_output = args.yaml_output settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \ {fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None) @@ -216,28 +217,6 @@ def main(args=None) -> Tuple[argparse.Namespace, int]: if len(set(name.lower() for name in erargs.name.values())) != len(erargs.name): raise Exception(f"Names have to be unique. Names: {Counter(name.lower() for name in erargs.name.values())}") - if args.yaml_output: - import yaml - important = {} - for option, player_settings in vars(erargs).items(): - if type(player_settings) == dict: - if all(type(value) != list for value in player_settings.values()): - if len(player_settings.values()) > 1: - important[option] = {player: value for player, value in player_settings.items() if - player <= args.yaml_output} - else: - logging.debug(f"No player settings defined for option '{option}'") - - else: - if player_settings != "": # is not empty name - important[option] = player_settings - else: - logging.debug(f"No player settings defined for option '{option}'") - if args.outputpath: - os.makedirs(args.outputpath, exist_ok=True) - with open(os.path.join(args.outputpath if args.outputpath else ".", f"generate_{seed_name}.yaml"), "wt") as f: - yaml.dump(important, f) - return erargs, seed diff --git a/Main.py b/Main.py index c931e22145a5..c09a537b60bd 100644 --- a/Main.py +++ b/Main.py @@ -46,6 +46,9 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No multiworld.sprite_pool = args.sprite_pool.copy() multiworld.set_options(args) + if args.yaml_output: + from Options import dump_player_options + dump_player_options(multiworld) multiworld.set_item_links() multiworld.state = CollectionState(multiworld) logger.info('Archipelago Version %s - Seed: %s\n', __version__, multiworld.seed) diff --git a/Options.py b/Options.py index ac4b2b8cd8bb..aa6f175fa58d 100644 --- a/Options.py +++ b/Options.py @@ -8,16 +8,17 @@ import random import typing import enum +from collections import defaultdict from copy import deepcopy from dataclasses import dataclass from schema import And, Optional, Or, Schema from typing_extensions import Self -from Utils import get_fuzzy_results, is_iterable_except_str +from Utils import get_fuzzy_results, is_iterable_except_str, output_path if typing.TYPE_CHECKING: - from BaseClasses import PlandoOptions + from BaseClasses import MultiWorld, PlandoOptions from worlds.AutoWorld import World import pathlib @@ -1532,3 +1533,42 @@ def yaml_dump_scalar(scalar) -> str: with open(os.path.join(target_folder, game_name + ".yaml"), "w", encoding="utf-8-sig") as f: f.write(res) + + +def dump_player_options(multiworld: MultiWorld) -> None: + from csv import DictWriter + + game_players = defaultdict(list) + for player, game in multiworld.game.items(): + game_players[game].append(player) + game_players = dict(sorted(game_players.items())) + + output = [] + per_game_option_names = [ + getattr(option, "display_name", option_key) + for option_key, option in PerGameCommonOptions.type_hints.items() + ] + all_option_names = per_game_option_names.copy() + for game, players in game_players.items(): + game_option_names = per_game_option_names.copy() + for player in players: + world = multiworld.worlds[player] + player_output = { + "Game": multiworld.game[player], + "Name": multiworld.get_player_name(player), + } + output.append(player_output) + for option_key, option in world.options_dataclass.type_hints.items(): + if issubclass(Removed, option): + continue + display_name = getattr(option, "display_name", option_key) + player_output[display_name] = getattr(world.options, option_key).current_option_name + if display_name not in game_option_names: + all_option_names.append(display_name) + game_option_names.append(display_name) + + with open(output_path(f"generate_{multiworld.seed_name}.csv"), mode="w", newline="") as file: + fields = ["Game", "Name", *all_option_names] + writer = DictWriter(file, fields) + writer.writeheader() + writer.writerows(output) diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index a12dc0f4ae14..2daf212efc29 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -134,6 +134,7 @@ def task(): {"bosses", "items", "connections", "texts"})) erargs.skip_prog_balancing = False erargs.skip_output = False + erargs.yaml_output = False name_counter = Counter() for player, (playerfile, settings) in enumerate(gen_options.items(), 1): From da781bb4ac29fd45f800bf4af605a2f6fa93afa5 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 18 Sep 2024 04:37:10 +0200 Subject: [PATCH 36/94] Core: rename yaml_output to csv_output (#3955) --- Generate.py | 4 ++-- Main.py | 2 +- WebHostLib/generate.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Generate.py b/Generate.py index 2488504f3049..52babdf18839 100644 --- a/Generate.py +++ b/Generate.py @@ -43,7 +43,7 @@ def mystery_argparse(): 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='info', help='Sets log level') - parser.add_argument("--yaml_output", action="store_true", + parser.add_argument("--csv_output", action="store_true", help="Output rolled player options to csv (made for async multiworld).") parser.add_argument("--plando", default=defaults.plando_options, help="List of options that can be set manually. Can be combined, for example \"bosses, items\"") @@ -156,7 +156,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]: erargs.skip_prog_balancing = args.skip_prog_balancing erargs.skip_output = args.skip_output erargs.name = {} - erargs.yaml_output = args.yaml_output + erargs.csv_output = args.csv_output settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \ {fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None) diff --git a/Main.py b/Main.py index c09a537b60bd..5a0f5c98bcc4 100644 --- a/Main.py +++ b/Main.py @@ -46,7 +46,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No multiworld.sprite_pool = args.sprite_pool.copy() multiworld.set_options(args) - if args.yaml_output: + if args.csv_output: from Options import dump_player_options dump_player_options(multiworld) multiworld.set_item_links() diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index 2daf212efc29..dbe7dd958910 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -134,7 +134,7 @@ def task(): {"bosses", "items", "connections", "texts"})) erargs.skip_prog_balancing = False erargs.skip_output = False - erargs.yaml_output = False + erargs.csv_output = False name_counter = Counter() for player, (playerfile, settings) in enumerate(gen_options.items(), 1): From 710609fa602d3fb3cfa002caa2b1d31ce6fa6dbb Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Wed, 18 Sep 2024 10:27:53 +0200 Subject: [PATCH 37/94] WebHost: move api/room_status out of __init__.py (#3958) * WebHost: move room_status out of __init__.py The old location is unexpected and easy to miss. * WebHost: fix typing in api/room_status --- WebHostLib/api/__init__.py | 42 +++----------------------------------- WebHostLib/api/room.py | 42 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 39 deletions(-) create mode 100644 WebHostLib/api/room.py diff --git a/WebHostLib/api/__init__.py b/WebHostLib/api/__init__.py index 4003243a281d..cf05e87374ab 100644 --- a/WebHostLib/api/__init__.py +++ b/WebHostLib/api/__init__.py @@ -1,51 +1,15 @@ """API endpoints package.""" from typing import List, Tuple -from uuid import UUID -from flask import Blueprint, abort, url_for +from flask import Blueprint -import worlds.Files -from ..models import Room, Seed +from ..models import Seed api_endpoints = Blueprint('api', __name__, url_prefix="/api") -# unsorted/misc endpoints - def get_players(seed: Seed) -> List[Tuple[str, str]]: return [(slot.player_name, slot.game) for slot in seed.slots] -@api_endpoints.route('/room_status/') -def room_info(room: UUID): - room = Room.get(id=room) - if room is None: - return abort(404) - - def supports_apdeltapatch(game: str): - return game in worlds.Files.AutoPatchRegister.patch_types - downloads = [] - for slot in sorted(room.seed.slots): - if slot.data and not supports_apdeltapatch(slot.game): - slot_download = { - "slot": slot.player_id, - "download": url_for("download_slot_file", room_id=room.id, player_id=slot.player_id) - } - downloads.append(slot_download) - elif slot.data: - slot_download = { - "slot": slot.player_id, - "download": url_for("download_patch", patch_id=slot.id, room_id=room.id) - } - downloads.append(slot_download) - return { - "tracker": room.tracker, - "players": get_players(room.seed), - "last_port": room.last_port, - "last_activity": room.last_activity, - "timeout": room.timeout, - "downloads": downloads, - } - - -from . import generate, user, datapackage # trigger registration +from . import datapackage, generate, room, user # trigger registration diff --git a/WebHostLib/api/room.py b/WebHostLib/api/room.py new file mode 100644 index 000000000000..9337975695b2 --- /dev/null +++ b/WebHostLib/api/room.py @@ -0,0 +1,42 @@ +from typing import Any, Dict +from uuid import UUID + +from flask import abort, url_for + +import worlds.Files +from . import api_endpoints, get_players +from ..models import Room + + +@api_endpoints.route('/room_status/') +def room_info(room_id: UUID) -> Dict[str, Any]: + room = Room.get(id=room_id) + if room is None: + return abort(404) + + def supports_apdeltapatch(game: str) -> bool: + return game in worlds.Files.AutoPatchRegister.patch_types + + downloads = [] + for slot in sorted(room.seed.slots): + if slot.data and not supports_apdeltapatch(slot.game): + slot_download = { + "slot": slot.player_id, + "download": url_for("download_slot_file", room_id=room.id, player_id=slot.player_id) + } + downloads.append(slot_download) + elif slot.data: + slot_download = { + "slot": slot.player_id, + "download": url_for("download_patch", patch_id=slot.id, room_id=room.id) + } + downloads.append(slot_download) + + return { + "tracker": room.tracker, + "players": get_players(room.seed), + "last_port": room.last_port, + "last_activity": room.last_activity, + "timeout": room.timeout, + "downloads": downloads, + } From 51a6dc150c6d74fa80ce0cc4382b88dcadda58d5 Mon Sep 17 00:00:00 2001 From: jamesbrq Date: Wed, 18 Sep 2024 13:33:02 -0400 Subject: [PATCH 38/94] MLSS: Various bugfixes and QoL updates (#3744) * Small fixes * Update Location names + Remove redundant rule * Fix for str not being returned in get_filler_item_name() * ASM changes + various name/logic updates * Remove extra unintended change + Make beanstone/beanlets useful * Add missing timer logic to client * Update Rules.py * Fix bad capitalization * Small formatting and ASM changes * Update basepatch.bsdiff * Update seed verification to be more likely to make a correct comparison * Add Pipe 10 * Final batch of small fixes * FINAL CHANGE I SWEAR * Added victory Item for spoilers * Update worlds/mlss/Regions.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/mlss/Items.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Fix jokes end logic * Update worlds/mlss/Regions.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/mlss/Rules.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/mlss/Rules.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/mlss/Rules.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Fix jokes end logic * Item Location mismatch + Check options against rules * Change List to Set + Check options against rules * Moved Victory item to event * Update worlds/mlss/__init__.py Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> * Update worlds/mlss/__init__.py Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- worlds/mlss/Client.py | 25 ++++- worlds/mlss/Data.py | 6 +- worlds/mlss/Items.py | 30 ++--- worlds/mlss/Locations.py | 117 +++++++++---------- worlds/mlss/Names/LocationName.py | 88 +++++++-------- worlds/mlss/Options.py | 5 +- worlds/mlss/Regions.py | 71 ++++++------ worlds/mlss/Rom.py | 33 +++--- worlds/mlss/Rules.py | 181 ++++++++++++++++++++++++++---- worlds/mlss/__init__.py | 65 ++++------- worlds/mlss/data/basepatch.bsdiff | Bin 17596 -> 18482 bytes 11 files changed, 377 insertions(+), 244 deletions(-) diff --git a/worlds/mlss/Client.py b/worlds/mlss/Client.py index 1f08b85610d6..75f6ac653003 100644 --- a/worlds/mlss/Client.py +++ b/worlds/mlss/Client.py @@ -85,7 +85,7 @@ async def game_watcher(self, ctx: "BizHawkClientContext") -> None: if not self.seed_verify: seed = await bizhawk.read(ctx.bizhawk_ctx, [(0xDF00A0, len(ctx.seed_name), "ROM")]) seed = seed[0].decode("UTF-8") - if seed != ctx.seed_name: + if seed not in ctx.seed_name: logger.info( "ERROR: The ROM you loaded is for a different game of AP. " "Please make sure the host has sent you the correct patch file," @@ -143,17 +143,30 @@ async def game_watcher(self, ctx: "BizHawkClientContext") -> None: # If RAM address isn't 0x0 yet break out and try again later to give the rest of the items for i in range(len(ctx.items_received) - received_index): item_data = items_by_id[ctx.items_received[received_index + i].item] - b = await bizhawk.guarded_read(ctx.bizhawk_ctx, [(0x3057, 1, "EWRAM")], [(0x3057, [0x0], "EWRAM")]) - if b is None: + result = False + total = 0 + while not result: + await asyncio.sleep(0.05) + total += 0.05 + result = await bizhawk.guarded_write( + ctx.bizhawk_ctx, + [ + (0x3057, [id_to_RAM(item_data.itemID)], "EWRAM") + ], + [(0x3057, [0x0], "EWRAM")] + ) + if result: + total = 0 + if total >= 1: + break + if not result: break await bizhawk.write( ctx.bizhawk_ctx, [ - (0x3057, [id_to_RAM(item_data.itemID)], "EWRAM"), (0x4808, [(received_index + i + 1) // 0x100, (received_index + i + 1) % 0x100], "EWRAM"), - ], + ] ) - await asyncio.sleep(0.1) # Early return and location send if you are currently in a shop, # since other flags aren't going to change diff --git a/worlds/mlss/Data.py b/worlds/mlss/Data.py index 749e63bcf24d..add14aa008f1 100644 --- a/worlds/mlss/Data.py +++ b/worlds/mlss/Data.py @@ -1,6 +1,9 @@ flying = [ 0x14, 0x1D, + 0x32, + 0x33, + 0x40, 0x4C ] @@ -23,7 +26,6 @@ 0x5032AC, 0x5032CC, 0x5032EC, - 0x50330C, 0x50332C, 0x50334C, 0x50336C, @@ -151,7 +153,7 @@ 0x50458C, 0x5045AC, 0x50468C, - 0x5046CC, + # 0x5046CC, 6 enemy formation 0x5046EC, 0x50470C ] diff --git a/worlds/mlss/Items.py b/worlds/mlss/Items.py index b95f1a0bc0a8..717443ddfc06 100644 --- a/worlds/mlss/Items.py +++ b/worlds/mlss/Items.py @@ -78,21 +78,21 @@ class MLSSItem(Item): ItemData(77771060, "Beanstar Piece 3", ItemClassification.progression, 0x67), ItemData(77771061, "Beanstar Piece 4", ItemClassification.progression, 0x70), ItemData(77771062, "Spangle", ItemClassification.progression, 0x72), - ItemData(77771063, "Beanlet 1", ItemClassification.filler, 0x73), - ItemData(77771064, "Beanlet 2", ItemClassification.filler, 0x74), - ItemData(77771065, "Beanlet 3", ItemClassification.filler, 0x75), - ItemData(77771066, "Beanlet 4", ItemClassification.filler, 0x76), - ItemData(77771067, "Beanlet 5", ItemClassification.filler, 0x77), - ItemData(77771068, "Beanstone 1", ItemClassification.filler, 0x80), - ItemData(77771069, "Beanstone 2", ItemClassification.filler, 0x81), - ItemData(77771070, "Beanstone 3", ItemClassification.filler, 0x82), - ItemData(77771071, "Beanstone 4", ItemClassification.filler, 0x83), - ItemData(77771072, "Beanstone 5", ItemClassification.filler, 0x84), - ItemData(77771073, "Beanstone 6", ItemClassification.filler, 0x85), - ItemData(77771074, "Beanstone 7", ItemClassification.filler, 0x86), - ItemData(77771075, "Beanstone 8", ItemClassification.filler, 0x87), - ItemData(77771076, "Beanstone 9", ItemClassification.filler, 0x90), - ItemData(77771077, "Beanstone 10", ItemClassification.filler, 0x91), + ItemData(77771063, "Beanlet 1", ItemClassification.useful, 0x73), + ItemData(77771064, "Beanlet 2", ItemClassification.useful, 0x74), + ItemData(77771065, "Beanlet 3", ItemClassification.useful, 0x75), + ItemData(77771066, "Beanlet 4", ItemClassification.useful, 0x76), + ItemData(77771067, "Beanlet 5", ItemClassification.useful, 0x77), + ItemData(77771068, "Beanstone 1", ItemClassification.useful, 0x80), + ItemData(77771069, "Beanstone 2", ItemClassification.useful, 0x81), + ItemData(77771070, "Beanstone 3", ItemClassification.useful, 0x82), + ItemData(77771071, "Beanstone 4", ItemClassification.useful, 0x83), + ItemData(77771072, "Beanstone 5", ItemClassification.useful, 0x84), + ItemData(77771073, "Beanstone 6", ItemClassification.useful, 0x85), + ItemData(77771074, "Beanstone 7", ItemClassification.useful, 0x86), + ItemData(77771075, "Beanstone 8", ItemClassification.useful, 0x87), + ItemData(77771076, "Beanstone 9", ItemClassification.useful, 0x90), + ItemData(77771077, "Beanstone 10", ItemClassification.useful, 0x91), ItemData(77771078, "Secret Scroll 1", ItemClassification.useful, 0x92), ItemData(77771079, "Secret Scroll 2", ItemClassification.useful, 0x93), ItemData(77771080, "Castle Badge", ItemClassification.useful, 0x9F), diff --git a/worlds/mlss/Locations.py b/worlds/mlss/Locations.py index 8c00432a8f06..a2787ef9b1b1 100644 --- a/worlds/mlss/Locations.py +++ b/worlds/mlss/Locations.py @@ -4,9 +4,6 @@ class LocationData: - name: str = "" - id: int = 0x00 - def __init__(self, name, id_, itemType): self.name = name self.itemType = itemType @@ -93,8 +90,8 @@ class MLSSLocation(Location): LocationData("Hoohoo Mountain Below Summit Block 1", 0x39D873, 0), LocationData("Hoohoo Mountain Below Summit Block 2", 0x39D87B, 0), LocationData("Hoohoo Mountain Below Summit Block 3", 0x39D883, 0), - LocationData("Hoohoo Mountain After Hoohooros Block 1", 0x39D890, 0), - LocationData("Hoohoo Mountain After Hoohooros Block 2", 0x39D8A0, 0), + LocationData("Hoohoo Mountain Past Hoohooros Block 1", 0x39D890, 0), + LocationData("Hoohoo Mountain Past Hoohooros Block 2", 0x39D8A0, 0), LocationData("Hoohoo Mountain Hoohooros Room Block 1", 0x39D8AD, 0), LocationData("Hoohoo Mountain Hoohooros Room Block 2", 0x39D8B5, 0), LocationData("Hoohoo Mountain Before Hoohooros Block", 0x39D8D2, 0), @@ -104,7 +101,7 @@ class MLSSLocation(Location): LocationData("Hoohoo Mountain Room 1 Block 2", 0x39D924, 0), LocationData("Hoohoo Mountain Room 1 Block 3", 0x39D92C, 0), LocationData("Hoohoo Mountain Base Room 1 Block", 0x39D939, 0), - LocationData("Hoohoo Village Right Side Block", 0x39D957, 0), + LocationData("Hoohoo Village Eastside Block", 0x39D957, 0), LocationData("Hoohoo Village Bridge Room Block 1", 0x39D96F, 0), LocationData("Hoohoo Village Bridge Room Block 2", 0x39D97F, 0), LocationData("Hoohoo Village Bridge Room Block 3", 0x39D98F, 0), @@ -119,8 +116,8 @@ class MLSSLocation(Location): LocationData("Hoohoo Mountain Base Boostatue Room Digspot 2", 0x39D9E1, 0), LocationData("Hoohoo Mountain Base Grassy Area Block 1", 0x39D9FE, 0), LocationData("Hoohoo Mountain Base Grassy Area Block 2", 0x39D9F6, 0), - LocationData("Hoohoo Mountain Base After Minecart Minigame Block 1", 0x39DA35, 0), - LocationData("Hoohoo Mountain Base After Minecart Minigame Block 2", 0x39DA2D, 0), + LocationData("Hoohoo Mountain Base Past Minecart Minigame Block 1", 0x39DA35, 0), + LocationData("Hoohoo Mountain Base Past Minecart Minigame Block 2", 0x39DA2D, 0), LocationData("Cave Connecting Stardust Fields and Hoohoo Village Block 1", 0x39DA77, 0), LocationData("Cave Connecting Stardust Fields and Hoohoo Village Block 2", 0x39DA7F, 0), LocationData("Hoohoo Village South Cave Block", 0x39DACD, 0), @@ -143,14 +140,14 @@ class MLSSLocation(Location): LocationData("Shop Starting Flag 3", 0x3C05F4, 3), LocationData("Hoohoo Mountain Summit Digspot", 0x39D85E, 0), LocationData("Hoohoo Mountain Below Summit Digspot", 0x39D86B, 0), - LocationData("Hoohoo Mountain After Hoohooros Digspot", 0x39D898, 0), + LocationData("Hoohoo Mountain Past Hoohooros Digspot", 0x39D898, 0), LocationData("Hoohoo Mountain Hoohooros Room Digspot 1", 0x39D8BD, 0), LocationData("Hoohoo Mountain Hoohooros Room Digspot 2", 0x39D8C5, 0), LocationData("Hoohoo Mountain Before Hoohooros Digspot", 0x39D8E2, 0), LocationData("Hoohoo Mountain Room 2 Digspot 1", 0x39D907, 0), LocationData("Hoohoo Mountain Room 2 Digspot 2", 0x39D90F, 0), LocationData("Hoohoo Mountain Base Room 1 Digspot", 0x39D941, 0), - LocationData("Hoohoo Village Right Side Digspot", 0x39D95F, 0), + LocationData("Hoohoo Village Eastside Digspot", 0x39D95F, 0), LocationData("Hoohoo Village Super Hammer Cave Digspot", 0x39DB02, 0), LocationData("Hoohoo Village Super Hammer Cave Block", 0x39DAEA, 0), LocationData("Hoohoo Village North Cave Room 2 Digspot", 0x39DAB5, 0), @@ -267,7 +264,7 @@ class MLSSLocation(Location): LocationData("Chucklehuck Woods Cave Room 3 Coin Block", 0x39DDB4, 0), LocationData("Chucklehuck Woods Pipe 5 Room Coin Block", 0x39DDE6, 0), LocationData("Chucklehuck Woods Room 7 Coin Block", 0x39DE31, 0), - LocationData("Chucklehuck Woods After Chuckleroot Coin Block", 0x39DF14, 0), + LocationData("Chucklehuck Woods Past Chuckleroot Coin Block", 0x39DF14, 0), LocationData("Chucklehuck Woods Koopa Room Coin Block", 0x39DF53, 0), LocationData("Chucklehuck Woods Winkle Area Cave Coin Block", 0x39DF80, 0), LocationData("Sewers Prison Room Coin Block", 0x39E01E, 0), @@ -286,11 +283,12 @@ class MLSSLocation(Location): LocationData("Hoohoo Mountain Base Past Ultra Hammer Rocks Block 1", 0x39DA42, 0), LocationData("Hoohoo Mountain Base Past Ultra Hammer Rocks Block 2", 0x39DA4A, 0), LocationData("Hoohoo Mountain Base Past Ultra Hammer Rocks Block 3", 0x39DA52, 0), - LocationData("Hoohoo Mountain Base Boostatue Room Digspot 3 (Rightside)", 0x39D9E9, 0), + LocationData("Hoohoo Mountain Base Boostatue Room Digspot 3 (Right Side)", 0x39D9E9, 0), LocationData("Hoohoo Mountain Base Mole Near Teehee Valley", 0x277A45, 1), LocationData("Teehee Valley Entrance To Hoohoo Mountain Digspot", 0x39E5B5, 0), - LocationData("Teehee Valley Solo Luigi Maze Room 2 Digspot 1", 0x39E5C8, 0), - LocationData("Teehee Valley Solo Luigi Maze Room 2 Digspot 2", 0x39E5D0, 0), + LocationData("Teehee Valley Upper Maze Room 1 Block", 0x39E5E0, 0), + LocationData("Teehee Valley Upper Maze Room 2 Digspot 1", 0x39E5C8, 0), + LocationData("Teehee Valley Upper Maze Room 2 Digspot 2", 0x39E5D0, 0), LocationData("Hoohoo Mountain Base Guffawha Ruins Entrance Digspot", 0x39DA0B, 0), LocationData("Hoohoo Mountain Base Teehee Valley Entrance Digspot", 0x39DA20, 0), LocationData("Hoohoo Mountain Base Teehee Valley Entrance Block", 0x39DA18, 0), @@ -345,12 +343,12 @@ class MLSSLocation(Location): LocationData("Chucklehuck Woods Southwest of Chuckleroot Block", 0x39DEC2, 0), LocationData("Chucklehuck Woods Wiggler room Digspot 1", 0x39DECF, 0), LocationData("Chucklehuck Woods Wiggler room Digspot 2", 0x39DED7, 0), - LocationData("Chucklehuck Woods After Chuckleroot Block 1", 0x39DEE4, 0), - LocationData("Chucklehuck Woods After Chuckleroot Block 2", 0x39DEEC, 0), - LocationData("Chucklehuck Woods After Chuckleroot Block 3", 0x39DEF4, 0), - LocationData("Chucklehuck Woods After Chuckleroot Block 4", 0x39DEFC, 0), - LocationData("Chucklehuck Woods After Chuckleroot Block 5", 0x39DF04, 0), - LocationData("Chucklehuck Woods After Chuckleroot Block 6", 0x39DF0C, 0), + LocationData("Chucklehuck Woods Past Chuckleroot Block 1", 0x39DEE4, 0), + LocationData("Chucklehuck Woods Past Chuckleroot Block 2", 0x39DEEC, 0), + LocationData("Chucklehuck Woods Past Chuckleroot Block 3", 0x39DEF4, 0), + LocationData("Chucklehuck Woods Past Chuckleroot Block 4", 0x39DEFC, 0), + LocationData("Chucklehuck Woods Past Chuckleroot Block 5", 0x39DF04, 0), + LocationData("Chucklehuck Woods Past Chuckleroot Block 6", 0x39DF0C, 0), LocationData("Chucklehuck Woods Koopa Room Block 1", 0x39DF4B, 0), LocationData("Chucklehuck Woods Koopa Room Block 2", 0x39DF5B, 0), LocationData("Chucklehuck Woods Koopa Room Digspot", 0x39DF63, 0), @@ -367,14 +365,14 @@ class MLSSLocation(Location): ] castleTown: typing.List[LocationData] = [ - LocationData("Beanbean Castle Town Left Side House Block 1", 0x39D7A4, 0), - LocationData("Beanbean Castle Town Left Side House Block 2", 0x39D7AC, 0), - LocationData("Beanbean Castle Town Left Side House Block 3", 0x39D7B4, 0), - LocationData("Beanbean Castle Town Left Side House Block 4", 0x39D7BC, 0), - LocationData("Beanbean Castle Town Right Side House Block 1", 0x39D7D8, 0), - LocationData("Beanbean Castle Town Right Side House Block 2", 0x39D7E0, 0), - LocationData("Beanbean Castle Town Right Side House Block 3", 0x39D7E8, 0), - LocationData("Beanbean Castle Town Right Side House Block 4", 0x39D7F0, 0), + LocationData("Beanbean Castle Town West Side House Block 1", 0x39D7A4, 0), + LocationData("Beanbean Castle Town West Side House Block 2", 0x39D7AC, 0), + LocationData("Beanbean Castle Town West Side House Block 3", 0x39D7B4, 0), + LocationData("Beanbean Castle Town West Side House Block 4", 0x39D7BC, 0), + LocationData("Beanbean Castle Town East Side House Block 1", 0x39D7D8, 0), + LocationData("Beanbean Castle Town East Side House Block 2", 0x39D7E0, 0), + LocationData("Beanbean Castle Town East Side House Block 3", 0x39D7E8, 0), + LocationData("Beanbean Castle Town East Side House Block 4", 0x39D7F0, 0), LocationData("Beanbean Castle Peach's Extra Dress", 0x1E9433, 2), LocationData("Beanbean Castle Fake Beanstar", 0x1E9432, 2), LocationData("Beanbean Castle Town Beanlet 1", 0x251347, 1), @@ -444,14 +442,14 @@ class MLSSLocation(Location): ] kidnappedFlag: typing.List[LocationData] = [ - LocationData("Badge Shop Enter Fungitown Flag 1", 0x3C0640, 2), - LocationData("Badge Shop Enter Fungitown Flag 2", 0x3C0642, 2), - LocationData("Badge Shop Enter Fungitown Flag 3", 0x3C0644, 2), - LocationData("Pants Shop Enter Fungitown Flag 1", 0x3C0646, 2), - LocationData("Pants Shop Enter Fungitown Flag 2", 0x3C0648, 2), - LocationData("Pants Shop Enter Fungitown Flag 3", 0x3C064A, 2), - LocationData("Shop Enter Fungitown Flag 1", 0x3C0606, 3), - LocationData("Shop Enter Fungitown Flag 2", 0x3C0608, 3), + LocationData("Badge Shop Trunkle Flag 1", 0x3C0640, 2), + LocationData("Badge Shop Trunkle Flag 2", 0x3C0642, 2), + LocationData("Badge Shop Trunkle Flag 3", 0x3C0644, 2), + LocationData("Pants Shop Trunkle Flag 1", 0x3C0646, 2), + LocationData("Pants Shop Trunkle Flag 2", 0x3C0648, 2), + LocationData("Pants Shop Trunkle Flag 3", 0x3C064A, 2), + LocationData("Shop Trunkle Flag 1", 0x3C0606, 3), + LocationData("Shop Trunkle Flag 2", 0x3C0608, 3), ] beanstarFlag: typing.List[LocationData] = [ @@ -553,21 +551,21 @@ class MLSSLocation(Location): airport: typing.List[LocationData] = [ LocationData("Airport Entrance Digspot", 0x39E2DC, 0), LocationData("Airport Lobby Digspot", 0x39E2E9, 0), - LocationData("Airport Leftside Digspot 1", 0x39E2F6, 0), - LocationData("Airport Leftside Digspot 2", 0x39E2FE, 0), - LocationData("Airport Leftside Digspot 3", 0x39E306, 0), - LocationData("Airport Leftside Digspot 4", 0x39E30E, 0), - LocationData("Airport Leftside Digspot 5", 0x39E316, 0), + LocationData("Airport Westside Digspot 1", 0x39E2F6, 0), + LocationData("Airport Westside Digspot 2", 0x39E2FE, 0), + LocationData("Airport Westside Digspot 3", 0x39E306, 0), + LocationData("Airport Westside Digspot 4", 0x39E30E, 0), + LocationData("Airport Westside Digspot 5", 0x39E316, 0), LocationData("Airport Center Digspot 1", 0x39E323, 0), LocationData("Airport Center Digspot 2", 0x39E32B, 0), LocationData("Airport Center Digspot 3", 0x39E333, 0), LocationData("Airport Center Digspot 4", 0x39E33B, 0), LocationData("Airport Center Digspot 5", 0x39E343, 0), - LocationData("Airport Rightside Digspot 1", 0x39E350, 0), - LocationData("Airport Rightside Digspot 2", 0x39E358, 0), - LocationData("Airport Rightside Digspot 3", 0x39E360, 0), - LocationData("Airport Rightside Digspot 4", 0x39E368, 0), - LocationData("Airport Rightside Digspot 5", 0x39E370, 0), + LocationData("Airport Eastside Digspot 1", 0x39E350, 0), + LocationData("Airport Eastside Digspot 2", 0x39E358, 0), + LocationData("Airport Eastside Digspot 3", 0x39E360, 0), + LocationData("Airport Eastside Digspot 4", 0x39E368, 0), + LocationData("Airport Eastside Digspot 5", 0x39E370, 0), ] gwarharEntrance: typing.List[LocationData] = [ @@ -617,7 +615,6 @@ class MLSSLocation(Location): LocationData("Teehee Valley Past Ultra Hammer Rock Block 2", 0x39E590, 0), LocationData("Teehee Valley Past Ultra Hammer Rock Digspot 1", 0x39E598, 0), LocationData("Teehee Valley Past Ultra Hammer Rock Digspot 3", 0x39E5A8, 0), - LocationData("Teehee Valley Solo Luigi Maze Room 1 Block", 0x39E5E0, 0), LocationData("Teehee Valley Before Trunkle Digspot", 0x39E5F0, 0), LocationData("S.S. Chuckola Storage Room Block 1", 0x39E610, 0), LocationData("S.S. Chuckola Storage Room Block 2", 0x39E628, 0), @@ -667,7 +664,7 @@ class MLSSLocation(Location): LocationData("Bowser's Castle Iggy & Morton Hallway Block 1", 0x39E9EF, 0), LocationData("Bowser's Castle Iggy & Morton Hallway Block 2", 0x39E9F7, 0), LocationData("Bowser's Castle Iggy & Morton Hallway Digspot", 0x39E9FF, 0), - LocationData("Bowser's Castle After Morton Block", 0x39EA0C, 0), + LocationData("Bowser's Castle Past Morton Block", 0x39EA0C, 0), LocationData("Bowser's Castle Morton Room 1 Digspot", 0x39EA89, 0), LocationData("Bowser's Castle Lemmy Room 1 Block", 0x39EA9C, 0), LocationData("Bowser's Castle Lemmy Room 1 Digspot", 0x39EAA4, 0), @@ -705,16 +702,16 @@ class MLSSLocation(Location): LocationData("Joke's End Second Floor West Room Block 4", 0x39E781, 0), LocationData("Joke's End Mole Reward 1", 0x27788E, 1), LocationData("Joke's End Mole Reward 2", 0x2778D2, 1), -] - -jokesMain: typing.List[LocationData] = [ LocationData("Joke's End Furnace Room 1 Block 1", 0x39E70F, 0), LocationData("Joke's End Furnace Room 1 Block 2", 0x39E717, 0), LocationData("Joke's End Furnace Room 1 Block 3", 0x39E71F, 0), LocationData("Joke's End Northeast of Boiler Room 1 Block", 0x39E732, 0), - LocationData("Joke's End Northeast of Boiler Room 3 Digspot", 0x39E73F, 0), LocationData("Joke's End Northeast of Boiler Room 2 Block", 0x39E74C, 0), LocationData("Joke's End Northeast of Boiler Room 2 Digspot", 0x39E754, 0), + LocationData("Joke's End Northeast of Boiler Room 3 Digspot", 0x39E73F, 0), +] + +jokesMain: typing.List[LocationData] = [ LocationData("Joke's End Second Floor East Room Digspot", 0x39E794, 0), LocationData("Joke's End Final Split up Room Digspot", 0x39E7A7, 0), LocationData("Joke's End South of Bridge Room Block", 0x39E7B4, 0), @@ -740,10 +737,10 @@ class MLSSLocation(Location): postJokes: typing.List[LocationData] = [ LocationData("Teehee Valley Past Ultra Hammer Rock Digspot 2 (Post-Birdo)", 0x39E5A0, 0), - LocationData("Teehee Valley Before Popple Digspot 1", 0x39E55B, 0), - LocationData("Teehee Valley Before Popple Digspot 2", 0x39E563, 0), - LocationData("Teehee Valley Before Popple Digspot 3", 0x39E56B, 0), - LocationData("Teehee Valley Before Popple Digspot 4", 0x39E573, 0), + LocationData("Teehee Valley Before Birdo Digspot 1", 0x39E55B, 0), + LocationData("Teehee Valley Before Birdo Digspot 2", 0x39E563, 0), + LocationData("Teehee Valley Before Birdo Digspot 3", 0x39E56B, 0), + LocationData("Teehee Valley Before Birdo Digspot 4", 0x39E573, 0), ] theater: typing.List[LocationData] = [ @@ -766,6 +763,10 @@ class MLSSLocation(Location): LocationData("Oho Oasis Thunderhand", 0x1E9409, 2), ] +cacklettas_soul: typing.List[LocationData] = [ + LocationData("Cackletta's Soul", None, 0), +] + nonBlock = [ (0x434B, 0x1, 0x243844), # Farm Mole 1 (0x434B, 0x1, 0x24387D), # Farm Mole 2 @@ -1171,15 +1172,15 @@ class MLSSLocation(Location): + fungitownBeanstar + fungitownBirdo + bowsers + + bowsersMini + jokesEntrance + jokesMain + postJokes + theater + oasis + gwarharMain - + bowsersMini + baseUltraRocks + coins ) -location_table: typing.Dict[str, int] = {locData.name: locData.id for locData in all_locations} +location_table: typing.Dict[str, int] = {location.name: location.id for location in all_locations} diff --git a/worlds/mlss/Names/LocationName.py b/worlds/mlss/Names/LocationName.py index 7cbc2e4f31f8..5b38b2a10f6e 100644 --- a/worlds/mlss/Names/LocationName.py +++ b/worlds/mlss/Names/LocationName.py @@ -8,14 +8,14 @@ class LocationName: StardustFields4Block3 = "Stardust Fields Room 4 Block 3" StardustFields5Block = "Stardust Fields Room 5 Block" HoohooVillageHammerHouseBlock = "Hoohoo Village Hammer House Block" - BeanbeanCastleTownLeftSideHouseBlock1 = "Beanbean Castle Town Left Side House Block 1" - BeanbeanCastleTownLeftSideHouseBlock2 = "Beanbean Castle Town Left Side House Block 2" - BeanbeanCastleTownLeftSideHouseBlock3 = "Beanbean Castle Town Left Side House Block 3" - BeanbeanCastleTownLeftSideHouseBlock4 = "Beanbean Castle Town Left Side House Block 4" - BeanbeanCastleTownRightSideHouseBlock1 = "Beanbean Castle Town Right Side House Block 1" - BeanbeanCastleTownRightSideHouseBlock2 = "Beanbean Castle Town Right Side House Block 2" - BeanbeanCastleTownRightSideHouseBlock3 = "Beanbean Castle Town Right Side House Block 3" - BeanbeanCastleTownRightSideHouseBlock4 = "Beanbean Castle Town Right Side House Block 4" + BeanbeanCastleTownWestsideHouseBlock1 = "Beanbean Castle Town Westside House Block 1" + BeanbeanCastleTownWestsideHouseBlock2 = "Beanbean Castle Town Westside House Block 2" + BeanbeanCastleTownWestsideHouseBlock3 = "Beanbean Castle Town Westside House Block 3" + BeanbeanCastleTownWestsideHouseBlock4 = "Beanbean Castle Town Westside House Block 4" + BeanbeanCastleTownEastsideHouseBlock1 = "Beanbean Castle Town Eastside House Block 1" + BeanbeanCastleTownEastsideHouseBlock2 = "Beanbean Castle Town Eastside House Block 2" + BeanbeanCastleTownEastsideHouseBlock3 = "Beanbean Castle Town Eastside House Block 3" + BeanbeanCastleTownEastsideHouseBlock4 = "Beanbean Castle Town Eastside House Block 4" BeanbeanCastleTownMiniMarioBlock1 = "Beanbean Castle Town Mini Mario Block 1" BeanbeanCastleTownMiniMarioBlock2 = "Beanbean Castle Town Mini Mario Block 2" BeanbeanCastleTownMiniMarioBlock3 = "Beanbean Castle Town Mini Mario Block 3" @@ -26,9 +26,9 @@ class LocationName: HoohooMountainBelowSummitBlock1 = "Hoohoo Mountain Below Summit Block 1" HoohooMountainBelowSummitBlock2 = "Hoohoo Mountain Below Summit Block 2" HoohooMountainBelowSummitBlock3 = "Hoohoo Mountain Below Summit Block 3" - HoohooMountainAfterHoohoorosBlock1 = "Hoohoo Mountain After Hoohooros Block 1" - HoohooMountainAfterHoohoorosDigspot = "Hoohoo Mountain After Hoohooros Digspot" - HoohooMountainAfterHoohoorosBlock2 = "Hoohoo Mountain After Hoohooros Block 2" + HoohooMountainPastHoohoorosBlock1 = "Hoohoo Mountain Past Hoohooros Block 1" + HoohooMountainPastHoohoorosDigspot = "Hoohoo Mountain Past Hoohooros Digspot" + HoohooMountainPastHoohoorosBlock2 = "Hoohoo Mountain Past Hoohooros Block 2" HoohooMountainHoohoorosRoomBlock1 = "Hoohoo Mountain Hoohooros Room Block 1" HoohooMountainHoohoorosRoomBlock2 = "Hoohoo Mountain Hoohooros Room Block 2" HoohooMountainHoohoorosRoomDigspot1 = "Hoohoo Mountain Hoohooros Room Digspot 1" @@ -44,8 +44,8 @@ class LocationName: HoohooMountainRoom1Block3 = "Hoohoo Mountain Room 1 Block 3" HoohooMountainBaseRoom1Block = "Hoohoo Mountain Base Room 1 Block" HoohooMountainBaseRoom1Digspot = "Hoohoo Mountain Base Room 1 Digspot" - HoohooVillageRightSideBlock = "Hoohoo Village Right Side Block" - HoohooVillageRightSideDigspot = "Hoohoo Village Right Side Digspot" + HoohooVillageEastsideBlock = "Hoohoo Village Eastside Block" + HoohooVillageEastsideDigspot = "Hoohoo Village Eastside Digspot" HoohooVillageBridgeRoomBlock1 = "Hoohoo Village Bridge Room Block 1" HoohooVillageBridgeRoomBlock2 = "Hoohoo Village Bridge Room Block 2" HoohooVillageBridgeRoomBlock3 = "Hoohoo Village Bridge Room Block 3" @@ -65,8 +65,8 @@ class LocationName: HoohooMountainBaseGuffawhaRuinsEntranceDigspot = "Hoohoo Mountain Base Guffawha Ruins Entrance Digspot" HoohooMountainBaseTeeheeValleyEntranceDigspot = "Hoohoo Mountain Base Teehee Valley Entrance Digspot" HoohooMountainBaseTeeheeValleyEntranceBlock = "Hoohoo Mountain Base Teehee Valley Entrance Block" - HoohooMountainBaseAfterMinecartMinigameBlock1 = "Hoohoo Mountain Base After Minecart Minigame Block 1" - HoohooMountainBaseAfterMinecartMinigameBlock2 = "Hoohoo Mountain Base After Minecart Minigame Block 2" + HoohooMountainBasePastMinecartMinigameBlock1 = "Hoohoo Mountain Base Past Minecart Minigame Block 1" + HoohooMountainBasePastMinecartMinigameBlock2 = "Hoohoo Mountain Base Past Minecart Minigame Block 2" HoohooMountainBasePastUltraHammerRocksBlock1 = "Hoohoo Mountain Base Past Ultra Hammer Rocks Block 1" HoohooMountainBasePastUltraHammerRocksBlock2 = "Hoohoo Mountain Base Past Ultra Hammer Rocks Block 2" HoohooMountainBasePastUltraHammerRocksBlock3 = "Hoohoo Mountain Base Past Ultra Hammer Rocks Block 3" @@ -148,12 +148,12 @@ class LocationName: ChucklehuckWoodsSouthwestOfChucklerootBlock = "Chucklehuck Woods Southwest of Chuckleroot Block" ChucklehuckWoodsWigglerRoomDigspot1 = "Chucklehuck Woods Wiggler Room Digspot 1" ChucklehuckWoodsWigglerRoomDigspot2 = "Chucklehuck Woods Wiggler Room Digspot 2" - ChucklehuckWoodsAfterChucklerootBlock1 = "Chucklehuck Woods After Chuckleroot Block 1" - ChucklehuckWoodsAfterChucklerootBlock2 = "Chucklehuck Woods After Chuckleroot Block 2" - ChucklehuckWoodsAfterChucklerootBlock3 = "Chucklehuck Woods After Chuckleroot Block 3" - ChucklehuckWoodsAfterChucklerootBlock4 = "Chucklehuck Woods After Chuckleroot Block 4" - ChucklehuckWoodsAfterChucklerootBlock5 = "Chucklehuck Woods After Chuckleroot Block 5" - ChucklehuckWoodsAfterChucklerootBlock6 = "Chucklehuck Woods After Chuckleroot Block 6" + ChucklehuckWoodsPastChucklerootBlock1 = "Chucklehuck Woods Past Chuckleroot Block 1" + ChucklehuckWoodsPastChucklerootBlock2 = "Chucklehuck Woods Past Chuckleroot Block 2" + ChucklehuckWoodsPastChucklerootBlock3 = "Chucklehuck Woods Past Chuckleroot Block 3" + ChucklehuckWoodsPastChucklerootBlock4 = "Chucklehuck Woods Past Chuckleroot Block 4" + ChucklehuckWoodsPastChucklerootBlock5 = "Chucklehuck Woods Past Chuckleroot Block 5" + ChucklehuckWoodsPastChucklerootBlock6 = "Chucklehuck Woods Past Chuckleroot Block 6" WinkleAreaBeanstarRoomBlock = "Winkle Area Beanstar Room Block" WinkleAreaDigspot = "Winkle Area Digspot" WinkleAreaOutsideColosseumBlock = "Winkle Area Outside Colosseum Block" @@ -232,21 +232,21 @@ class LocationName: WoohooHooniversityPastCacklettaRoom2Digspot = "Woohoo Hooniversity Past Cackletta Room 2 Digspot" AirportEntranceDigspot = "Airport Entrance Digspot" AirportLobbyDigspot = "Airport Lobby Digspot" - AirportLeftsideDigspot1 = "Airport Leftside Digspot 1" - AirportLeftsideDigspot2 = "Airport Leftside Digspot 2" - AirportLeftsideDigspot3 = "Airport Leftside Digspot 3" - AirportLeftsideDigspot4 = "Airport Leftside Digspot 4" - AirportLeftsideDigspot5 = "Airport Leftside Digspot 5" + AirportWestsideDigspot1 = "Airport Westside Digspot 1" + AirportWestsideDigspot2 = "Airport Westside Digspot 2" + AirportWestsideDigspot3 = "Airport Westside Digspot 3" + AirportWestsideDigspot4 = "Airport Westside Digspot 4" + AirportWestsideDigspot5 = "Airport Westside Digspot 5" AirportCenterDigspot1 = "Airport Center Digspot 1" AirportCenterDigspot2 = "Airport Center Digspot 2" AirportCenterDigspot3 = "Airport Center Digspot 3" AirportCenterDigspot4 = "Airport Center Digspot 4" AirportCenterDigspot5 = "Airport Center Digspot 5" - AirportRightsideDigspot1 = "Airport Rightside Digspot 1" - AirportRightsideDigspot2 = "Airport Rightside Digspot 2" - AirportRightsideDigspot3 = "Airport Rightside Digspot 3" - AirportRightsideDigspot4 = "Airport Rightside Digspot 4" - AirportRightsideDigspot5 = "Airport Rightside Digspot 5" + AirportEastsideDigspot1 = "Airport Eastside Digspot 1" + AirportEastsideDigspot2 = "Airport Eastside Digspot 2" + AirportEastsideDigspot3 = "Airport Eastside Digspot 3" + AirportEastsideDigspot4 = "Airport Eastside Digspot 4" + AirportEastsideDigspot5 = "Airport Eastside Digspot 5" GwarharLagoonPipeRoomDigspot = "Gwarhar Lagoon Pipe Room Digspot" GwarharLagoonMassageParlorEntranceDigspot = "Gwarhar Lagoon Massage Parlor Entrance Digspot" GwarharLagoonPastHermieDigspot = "Gwarhar Lagoon Past Hermie Digspot" @@ -276,10 +276,10 @@ class LocationName: WoohooHooniversityBasementRoom4Block = "Woohoo Hooniversity Basement Room 4 Block" WoohooHooniversityPoppleRoomDigspot1 = "Woohoo Hooniversity Popple Room Digspot 1" WoohooHooniversityPoppleRoomDigspot2 = "Woohoo Hooniversity Popple Room Digspot 2" - TeeheeValleyBeforePoppleDigspot1 = "Teehee Valley Before Popple Digspot 1" - TeeheeValleyBeforePoppleDigspot2 = "Teehee Valley Before Popple Digspot 2" - TeeheeValleyBeforePoppleDigspot3 = "Teehee Valley Before Popple Digspot 3" - TeeheeValleyBeforePoppleDigspot4 = "Teehee Valley Before Popple Digspot 4" + TeeheeValleyBeforeBirdoDigspot1 = "Teehee Valley Before Birdo Digspot 1" + TeeheeValleyBeforeBirdoDigspot2 = "Teehee Valley Before Birdo Digspot 2" + TeeheeValleyBeforeBirdoDigspot3 = "Teehee Valley Before Birdo Digspot 3" + TeeheeValleyBeforeBirdoDigspot4 = "Teehee Valley Before Birdo Digspot 4" TeeheeValleyRoom1Digspot1 = "Teehee Valley Room 1 Digspot 1" TeeheeValleyRoom1Digspot2 = "Teehee Valley Room 1 Digspot 2" TeeheeValleyRoom1Digspot3 = "Teehee Valley Room 1 Digspot 3" @@ -296,9 +296,9 @@ class LocationName: TeeheeValleyPastUltraHammersDigspot2 = "Teehee Valley Past Ultra Hammer Rock Digspot 2 (Post-Birdo)" TeeheeValleyPastUltraHammersDigspot3 = "Teehee Valley Past Ultra Hammer Rock Digspot 3" TeeheeValleyEntranceToHoohooMountainDigspot = "Teehee Valley Entrance To Hoohoo Mountain Digspot" - TeeheeValleySoloLuigiMazeRoom2Digspot1 = "Teehee Valley Solo Luigi Maze Room 2 Digspot 1" - TeeheeValleySoloLuigiMazeRoom2Digspot2 = "Teehee Valley Solo Luigi Maze Room 2 Digspot 2" - TeeheeValleySoloLuigiMazeRoom1Block = "Teehee Valley Solo Luigi Maze Room 1 Block" + TeeheeValleyUpperMazeRoom2Digspot1 = "Teehee Valley Upper Maze Room 2 Digspot 1" + TeeheeValleyUpperMazeRoom2Digspot2 = "Teehee Valley Upper Maze Room 2 Digspot 2" + TeeheeValleyUpperMazeRoom1Block = "Teehee Valley Upper Maze Room 1 Block" TeeheeValleyBeforeTrunkleDigspot = "Teehee Valley Before Trunkle Digspot" TeeheeValleyTrunkleRoomDigspot = "Teehee Valley Trunkle Room Digspot" SSChuckolaStorageRoomBlock1 = "S.S. Chuckola Storage Room Block 1" @@ -314,10 +314,10 @@ class LocationName: JokesEndFurnaceRoom1Block1 = "Joke's End Furnace Room 1 Block 1" JokesEndFurnaceRoom1Block2 = "Joke's End Furnace Room 1 Block 2" JokesEndFurnaceRoom1Block3 = "Joke's End Furnace Room 1 Block 3" - JokesEndNortheastOfBoilerRoom1Block = "Joke's End Northeast Of Boiler Room 1 Block" - JokesEndNortheastOfBoilerRoom3Digspot = "Joke's End Northeast Of Boiler Room 3 Digspot" - JokesEndNortheastOfBoilerRoom2Block1 = "Joke's End Northeast Of Boiler Room 2 Block" - JokesEndNortheastOfBoilerRoom2Block2 = "Joke's End Northeast Of Boiler Room 2 Digspot" + JokesEndNortheastOfBoilerRoom1Block = "Joke's End Northeast of Boiler Room 1 Block" + JokesEndNortheastOfBoilerRoom3Digspot = "Joke's End Northeast of Boiler Room 3 Digspot" + JokesEndNortheastOfBoilerRoom2Block1 = "Joke's End Northeast of Boiler Room 2 Block" + JokesEndNortheastOfBoilerRoom2Digspot = "Joke's End Northeast of Boiler Room 2 Digspot" JokesEndSecondFloorWestRoomBlock1 = "Joke's End Second Floor West Room Block 1" JokesEndSecondFloorWestRoomBlock2 = "Joke's End Second Floor West Room Block 2" JokesEndSecondFloorWestRoomBlock3 = "Joke's End Second Floor West Room Block 3" @@ -505,7 +505,7 @@ class LocationName: BowsersCastleIggyMortonHallwayBlock1 = "Bowser's Castle Iggy & Morton Hallway Block 1" BowsersCastleIggyMortonHallwayBlock2 = "Bowser's Castle Iggy & Morton Hallway Block 2" BowsersCastleIggyMortonHallwayDigspot = "Bowser's Castle Iggy & Morton Hallway Digspot" - BowsersCastleAfterMortonBlock = "Bowser's Castle After Morton Block" + BowsersCastlePastMortonBlock = "Bowser's Castle Past Morton Block" BowsersCastleLudwigRoyHallwayBlock1 = "Bowser's Castle Ludwig & Roy Hallway Block 1" BowsersCastleLudwigRoyHallwayBlock2 = "Bowser's Castle Ludwig & Roy Hallway Block 2" BowsersCastleRoyCorridorBlock1 = "Bowser's Castle Roy Corridor Block 1" @@ -546,7 +546,7 @@ class LocationName: ChucklehuckWoodsCaveRoom3CoinBlock = "Chucklehuck Woods Cave Room 3 Coin Block" ChucklehuckWoodsPipe5RoomCoinBlock = "Chucklehuck Woods Pipe 5 Room Coin Block" ChucklehuckWoodsRoom7CoinBlock = "Chucklehuck Woods Room 7 Coin Block" - ChucklehuckWoodsAfterChucklerootCoinBlock = "Chucklehuck Woods After Chuckleroot Coin Block" + ChucklehuckWoodsPastChucklerootCoinBlock = "Chucklehuck Woods Past Chuckleroot Coin Block" ChucklehuckWoodsKoopaRoomCoinBlock = "Chucklehuck Woods Koopa Room Coin Block" ChucklehuckWoodsWinkleAreaCaveCoinBlock = "Chucklehuck Woods Winkle Area Cave Coin Block" SewersPrisonRoomCoinBlock = "Sewers Prison Room Coin Block" diff --git a/worlds/mlss/Options.py b/worlds/mlss/Options.py index 14c1ef3a7d5a..73e8ebd4015f 100644 --- a/worlds/mlss/Options.py +++ b/worlds/mlss/Options.py @@ -1,4 +1,4 @@ -from Options import Choice, Toggle, StartInventoryPool, PerGameCommonOptions, Range +from Options import Choice, Toggle, StartInventoryPool, PerGameCommonOptions, Range, Removed from dataclasses import dataclass @@ -282,7 +282,8 @@ class MLSSOptions(PerGameCommonOptions): extra_pipes: ExtraPipes skip_minecart: SkipMinecart disable_surf: DisableSurf - harhalls_pants: HarhallsPants + disable_harhalls_pants: HarhallsPants + harhalls_pants: Removed block_visibility: HiddenVisible chuckle_beans: ChuckleBeans music_options: MusicOptions diff --git a/worlds/mlss/Regions.py b/worlds/mlss/Regions.py index 992e99e2c7f7..7dd5e9451141 100644 --- a/worlds/mlss/Regions.py +++ b/worlds/mlss/Regions.py @@ -33,6 +33,7 @@ postJokes, baseUltraRocks, coins, + cacklettas_soul, ) from . import StateLogic @@ -40,44 +41,45 @@ from . import MLSSWorld -def create_regions(world: "MLSSWorld", excluded: typing.List[str]): +def create_regions(world: "MLSSWorld"): menu_region = Region("Menu", world.player, world.multiworld) world.multiworld.regions.append(menu_region) - create_region(world, "Main Area", mainArea, excluded) - create_region(world, "Chucklehuck Woods", chucklehuck, excluded) - create_region(world, "Beanbean Castle Town", castleTown, excluded) - create_region(world, "Shop Starting Flag", startingFlag, excluded) - create_region(world, "Shop Chuckolator Flag", chuckolatorFlag, excluded) - create_region(world, "Shop Mom Piranha Flag", piranhaFlag, excluded) - create_region(world, "Shop Enter Fungitown Flag", kidnappedFlag, excluded) - create_region(world, "Shop Beanstar Complete Flag", beanstarFlag, excluded) - create_region(world, "Shop Birdo Flag", birdoFlag, excluded) - create_region(world, "Surfable", surfable, excluded) - create_region(world, "Hooniversity", hooniversity, excluded) - create_region(world, "GwarharEntrance", gwarharEntrance, excluded) - create_region(world, "GwarharMain", gwarharMain, excluded) - create_region(world, "TeeheeValley", teeheeValley, excluded) - create_region(world, "Winkle", winkle, excluded) - create_region(world, "Sewers", sewers, excluded) - create_region(world, "Airport", airport, excluded) - create_region(world, "JokesEntrance", jokesEntrance, excluded) - create_region(world, "JokesMain", jokesMain, excluded) - create_region(world, "PostJokes", postJokes, excluded) - create_region(world, "Theater", theater, excluded) - create_region(world, "Fungitown", fungitown, excluded) - create_region(world, "Fungitown Shop Beanstar Complete Flag", fungitownBeanstar, excluded) - create_region(world, "Fungitown Shop Birdo Flag", fungitownBirdo, excluded) - create_region(world, "BooStatue", booStatue, excluded) - create_region(world, "Oasis", oasis, excluded) - create_region(world, "BaseUltraRocks", baseUltraRocks, excluded) + create_region(world, "Main Area", mainArea) + create_region(world, "Chucklehuck Woods", chucklehuck) + create_region(world, "Beanbean Castle Town", castleTown) + create_region(world, "Shop Starting Flag", startingFlag) + create_region(world, "Shop Chuckolator Flag", chuckolatorFlag) + create_region(world, "Shop Mom Piranha Flag", piranhaFlag) + create_region(world, "Shop Enter Fungitown Flag", kidnappedFlag) + create_region(world, "Shop Beanstar Complete Flag", beanstarFlag) + create_region(world, "Shop Birdo Flag", birdoFlag) + create_region(world, "Surfable", surfable) + create_region(world, "Hooniversity", hooniversity) + create_region(world, "GwarharEntrance", gwarharEntrance) + create_region(world, "GwarharMain", gwarharMain) + create_region(world, "TeeheeValley", teeheeValley) + create_region(world, "Winkle", winkle) + create_region(world, "Sewers", sewers) + create_region(world, "Airport", airport) + create_region(world, "JokesEntrance", jokesEntrance) + create_region(world, "JokesMain", jokesMain) + create_region(world, "PostJokes", postJokes) + create_region(world, "Theater", theater) + create_region(world, "Fungitown", fungitown) + create_region(world, "Fungitown Shop Beanstar Complete Flag", fungitownBeanstar) + create_region(world, "Fungitown Shop Birdo Flag", fungitownBirdo) + create_region(world, "BooStatue", booStatue) + create_region(world, "Oasis", oasis) + create_region(world, "BaseUltraRocks", baseUltraRocks) + create_region(world, "Cackletta's Soul", cacklettas_soul) if world.options.coins: - create_region(world, "Coins", coins, excluded) + create_region(world, "Coins", coins) if not world.options.castle_skip: - create_region(world, "Bowser's Castle", bowsers, excluded) - create_region(world, "Bowser's Castle Mini", bowsersMini, excluded) + create_region(world, "Bowser's Castle", bowsers) + create_region(world, "Bowser's Castle Mini", bowsersMini) def connect_regions(world: "MLSSWorld"): @@ -221,6 +223,9 @@ def connect_regions(world: "MLSSWorld"): "Bowser's Castle Mini", lambda state: StateLogic.canMini(state, world.player) and StateLogic.thunder(state, world.player), ) + connect(world, names, "Bowser's Castle Mini", "Cackletta's Soul") + else: + connect(world, names, "PostJokes", "Cackletta's Soul") connect(world, names, "Chucklehuck Woods", "Winkle", lambda state: StateLogic.canDash(state, world.player)) connect( world, @@ -282,11 +287,11 @@ def connect_regions(world: "MLSSWorld"): ) -def create_region(world: "MLSSWorld", name, locations, excluded): +def create_region(world: "MLSSWorld", name, locations): ret = Region(name, world.player, world.multiworld) for location in locations: loc = MLSSLocation(world.player, location.name, location.id, ret) - if location.name in excluded: + if location.name in world.disabled_locations: continue ret.locations.append(loc) world.multiworld.regions.append(ret) diff --git a/worlds/mlss/Rom.py b/worlds/mlss/Rom.py index 7cbbe8875195..03eac040efb2 100644 --- a/worlds/mlss/Rom.py +++ b/worlds/mlss/Rom.py @@ -8,7 +8,7 @@ from settings import get_settings from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes, APPatchExtension from .Items import item_table -from .Locations import shop, badge, pants, location_table, hidden, all_locations +from .Locations import shop, badge, pants, location_table, all_locations if TYPE_CHECKING: from . import MLSSWorld @@ -88,7 +88,7 @@ def hidden_visible(caller: APProcedurePatch, rom: bytes): return rom stream = io.BytesIO(rom) - for location in all_locations: + for location in [location for location in all_locations if location.itemType == 0]: stream.seek(location.id - 6) b = stream.read(1) if b[0] == 0x10 and options["block_visibility"] == 1: @@ -133,7 +133,7 @@ def enemy_randomize(caller: APProcedurePatch, rom: bytes): stream = io.BytesIO(rom) random.seed(options["seed"] + options["player"]) - if options["randomize_bosses"] == 1 or (options["randomize_bosses"] == 2) and options["randomize_enemies"] == 0: + if options["randomize_bosses"] == 1 or (options["randomize_bosses"] == 2 and options["randomize_enemies"] == 0): raw = [] for pos in bosses: stream.seek(pos + 1) @@ -164,6 +164,7 @@ def enemy_randomize(caller: APProcedurePatch, rom: bytes): enemies_raw = [] groups = [] + boss_groups = [] if options["randomize_enemies"] == 0: return stream.getvalue() @@ -171,7 +172,7 @@ def enemy_randomize(caller: APProcedurePatch, rom: bytes): if options["randomize_bosses"] == 2: for pos in bosses: stream.seek(pos + 1) - groups += [stream.read(0x1F)] + boss_groups += [stream.read(0x1F)] for pos in enemies: stream.seek(pos + 8) @@ -221,12 +222,19 @@ def enemy_randomize(caller: APProcedurePatch, rom: bytes): groups += [raw] chomp = False - random.shuffle(groups) arr = enemies if options["randomize_bosses"] == 2: arr += bosses + groups += boss_groups + + random.shuffle(groups) for pos in arr: + if arr[-1] in boss_groups: + stream.seek(pos) + temp = stream.read(1) + stream.seek(pos) + stream.write(bytes([temp[0] | 0x8])) stream.seek(pos + 1) stream.write(groups.pop()) @@ -320,20 +328,9 @@ def write_tokens(world: "MLSSWorld", patch: MLSSProcedurePatch) -> None: patch.write_token(APTokenTypes.WRITE, address + 3, bytes([world.random.randint(0x0, 0x26)])) for location_name in location_table.keys(): - if ( - (world.options.skip_minecart and "Minecart" in location_name and "After" not in location_name) - or (world.options.castle_skip and "Bowser" in location_name) - or (world.options.disable_surf and "Surf Minigame" in location_name) - or (world.options.harhalls_pants and "Harhall's" in location_name) - ): - continue - if (world.options.chuckle_beans == 0 and "Digspot" in location_name) or ( - world.options.chuckle_beans == 1 and location_table[location_name] in hidden - ): - continue - if not world.options.coins and "Coin" in location_name: + if location_name in world.disabled_locations: continue - location = world.multiworld.get_location(location_name, world.player) + location = world.get_location(location_name) item = location.item address = [address for address in all_locations if address.name == location.name] item_inject(world, patch, location.address, address[0].itemType, item) diff --git a/worlds/mlss/Rules.py b/worlds/mlss/Rules.py index 13627eafc479..b0b5a36465e2 100644 --- a/worlds/mlss/Rules.py +++ b/worlds/mlss/Rules.py @@ -13,7 +13,7 @@ def set_rules(world: "MLSSWorld", excluded): for location in all_locations: if "Digspot" in location.name: if (world.options.skip_minecart and "Minecart" in location.name) or ( - world.options.castle_skip and "Bowser" in location.name + world.options.castle_skip and "Bowser" in location.name ): continue if world.options.chuckle_beans == 0 or world.options.chuckle_beans == 1 and location.id in hidden: @@ -218,9 +218,9 @@ def set_rules(world: "MLSSWorld", excluded): add_rule( world.get_location(LocationName.BeanbeanOutskirtsUltraHammerUpgrade), lambda state: StateLogic.thunder(state, world.player) - and StateLogic.pieces(state, world.player) - and StateLogic.castleTown(state, world.player) - and StateLogic.rose(state, world.player), + and StateLogic.pieces(state, world.player) + and StateLogic.castleTown(state, world.player) + and StateLogic.rose(state, world.player), ) add_rule( world.get_location(LocationName.BeanbeanOutskirtsSoloLuigiCaveMole), @@ -235,27 +235,27 @@ def set_rules(world: "MLSSWorld", excluded): lambda state: StateLogic.canDig(state, world.player) and StateLogic.canMini(state, world.player), ) add_rule( - world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock1), + world.get_location(LocationName.ChucklehuckWoodsPastChucklerootBlock1), lambda state: StateLogic.fruits(state, world.player), ) add_rule( - world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock2), + world.get_location(LocationName.ChucklehuckWoodsPastChucklerootBlock2), lambda state: StateLogic.fruits(state, world.player), ) add_rule( - world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock3), + world.get_location(LocationName.ChucklehuckWoodsPastChucklerootBlock3), lambda state: StateLogic.fruits(state, world.player), ) add_rule( - world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock4), + world.get_location(LocationName.ChucklehuckWoodsPastChucklerootBlock4), lambda state: StateLogic.fruits(state, world.player), ) add_rule( - world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock5), + world.get_location(LocationName.ChucklehuckWoodsPastChucklerootBlock5), lambda state: StateLogic.fruits(state, world.player), ) add_rule( - world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock6), + world.get_location(LocationName.ChucklehuckWoodsPastChucklerootBlock6), lambda state: StateLogic.fruits(state, world.player), ) add_rule( @@ -350,10 +350,6 @@ def set_rules(world: "MLSSWorld", excluded): world.get_location(LocationName.TeeheeValleyPastUltraHammersBlock2), lambda state: StateLogic.ultra(state, world.player), ) - add_rule( - world.get_location(LocationName.TeeheeValleySoloLuigiMazeRoom1Block), - lambda state: StateLogic.ultra(state, world.player), - ) add_rule( world.get_location(LocationName.OhoOasisFirebrand), lambda state: StateLogic.canMini(state, world.player), @@ -462,6 +458,143 @@ def set_rules(world: "MLSSWorld", excluded): lambda state: StateLogic.canCrash(state, world.player), ) + if world.options.randomize_bosses.value != 0: + if world.options.chuckle_beans != 0: + add_rule( + world.get_location(LocationName.HoohooMountainHoohoorosRoomDigspot1), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainPastHoohoorosDigspot), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainPastHoohoorosConnectorRoomDigspot1), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainBelowSummitDigspot), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainSummitDigspot), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + if world.options.chuckle_beans == 2: + add_rule( + world.get_location(LocationName.HoohooMountainHoohoorosRoomDigspot2), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainPastHoohoorosConnectorRoomDigspot2), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooVillageHammers), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainPeasleysRose), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainHoohoorosRoomBlock1), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainHoohoorosRoomBlock2), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainBelowSummitBlock1), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainBelowSummitBlock2), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainBelowSummitBlock3), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainPastHoohoorosBlock1), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainPastHoohoorosBlock2), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainPastHoohoorosConnectorRoomBlock), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + + if not world.options.difficult_logic: + if world.options.chuckle_beans != 0: + add_rule( + world.get_location(LocationName.JokesEndNortheastOfBoilerRoom2Digspot), + lambda state: StateLogic.canCrash(state, world.player), + ) + add_rule( + world.get_location(LocationName.JokesEndNortheastOfBoilerRoom3Digspot), + lambda state: StateLogic.canCrash(state, world.player), + ) + add_rule( + world.get_location(LocationName.JokesEndNortheastOfBoilerRoom1Block), + lambda state: StateLogic.canCrash(state, world.player), + ) + add_rule( + world.get_location(LocationName.JokesEndNortheastOfBoilerRoom2Block1), + lambda state: StateLogic.canCrash(state, world.player), + ) + add_rule( + world.get_location(LocationName.JokesEndFurnaceRoom1Block1), + lambda state: StateLogic.canCrash(state, world.player), + ) + add_rule( + world.get_location(LocationName.JokesEndFurnaceRoom1Block2), + lambda state: StateLogic.canCrash(state, world.player), + ) + add_rule( + world.get_location(LocationName.JokesEndFurnaceRoom1Block3), + lambda state: StateLogic.canCrash(state, world.player), + ) + if world.options.coins: add_rule( world.get_location(LocationName.HoohooMountainBaseBooStatueCaveCoinBlock1), @@ -516,7 +649,7 @@ def set_rules(world: "MLSSWorld", excluded): lambda state: StateLogic.brooch(state, world.player) and StateLogic.hammers(state, world.player), ) add_rule( - world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootCoinBlock), + world.get_location(LocationName.ChucklehuckWoodsPastChucklerootCoinBlock), lambda state: StateLogic.brooch(state, world.player) and StateLogic.fruits(state, world.player), ) add_rule( @@ -546,23 +679,23 @@ def set_rules(world: "MLSSWorld", excluded): add_rule( world.get_location(LocationName.GwarharLagoonFirstUnderwaterAreaRoom2CoinBlock), lambda state: StateLogic.canDash(state, world.player) - and (StateLogic.membership(state, world.player) or StateLogic.surfable(state, world.player)), + and (StateLogic.membership(state, world.player) or StateLogic.surfable(state, world.player)), ) add_rule( world.get_location(LocationName.JokesEndSecondFloorWestRoomCoinBlock), lambda state: StateLogic.ultra(state, world.player) - and StateLogic.fire(state, world.player) - and ( - StateLogic.membership(state, world.player) - or (StateLogic.canDig(state, world.player) and StateLogic.canMini(state, world.player)) - ), + and StateLogic.fire(state, world.player) + and (StateLogic.membership(state, world.player) + or (StateLogic.canDig(state, world.player) + and StateLogic.canMini(state, world.player))), ) add_rule( world.get_location(LocationName.JokesEndNorthofBridgeRoomCoinBlock), lambda state: StateLogic.ultra(state, world.player) - and StateLogic.fire(state, world.player) - and StateLogic.canDig(state, world.player) - and (StateLogic.membership(state, world.player) or StateLogic.canMini(state, world.player)), + and StateLogic.fire(state, world.player) + and StateLogic.canDig(state, world.player) + and (StateLogic.membership(state, world.player) + or StateLogic.canMini(state, world.player)), ) if not world.options.difficult_logic: add_rule( diff --git a/worlds/mlss/__init__.py b/worlds/mlss/__init__.py index f44343c230d0..bb7ed0515419 100644 --- a/worlds/mlss/__init__.py +++ b/worlds/mlss/__init__.py @@ -4,7 +4,7 @@ import settings from BaseClasses import Tutorial, ItemClassification from worlds.AutoWorld import WebWorld, World -from typing import List, Dict, Any +from typing import Set, Dict, Any from .Locations import all_locations, location_table, bowsers, bowsersMini, hidden, coins from .Options import MLSSOptions from .Items import MLSSItem, itemList, item_frequencies, item_table @@ -55,29 +55,29 @@ class MLSSWorld(World): settings: typing.ClassVar[MLSSSettings] item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = {loc_data.name: loc_data.id for loc_data in all_locations} - required_client_version = (0, 4, 5) + required_client_version = (0, 5, 0) - disabled_locations: List[str] + disabled_locations: Set[str] def generate_early(self) -> None: - self.disabled_locations = [] - if self.options.chuckle_beans == 0: - self.disabled_locations += [location.name for location in all_locations if "Digspot" in location.name] - if self.options.castle_skip: - self.disabled_locations += [location.name for location in all_locations if "Bowser" in location.name] - if self.options.chuckle_beans == 1: - self.disabled_locations = [location.name for location in all_locations if location.id in hidden] + self.disabled_locations = set() if self.options.skip_minecart: - self.disabled_locations += [LocationName.HoohooMountainBaseMinecartCaveDigspot] + self.disabled_locations.update([LocationName.HoohooMountainBaseMinecartCaveDigspot]) if self.options.disable_surf: - self.disabled_locations += [LocationName.SurfMinigame] - if self.options.harhalls_pants: - self.disabled_locations += [LocationName.HarhallsPants] + self.disabled_locations.update([LocationName.SurfMinigame]) + if self.options.disable_harhalls_pants: + self.disabled_locations.update([LocationName.HarhallsPants]) + if self.options.chuckle_beans == 0: + self.disabled_locations.update([location.name for location in all_locations if "Digspot" in location.name]) + if self.options.chuckle_beans == 1: + self.disabled_locations.update([location.name for location in all_locations if location.id in hidden]) + if self.options.castle_skip: + self.disabled_locations.update([location.name for location in bowsers + bowsersMini]) if not self.options.coins: - self.disabled_locations += [location.name for location in all_locations if location in coins] + self.disabled_locations.update([location.name for location in coins]) def create_regions(self) -> None: - create_regions(self, self.disabled_locations) + create_regions(self) connect_regions(self) item = self.create_item("Mushroom") @@ -90,13 +90,15 @@ def create_regions(self) -> None: self.get_location(LocationName.PantsShopStartingFlag1).place_locked_item(item) item = self.create_item("Chuckle Bean") self.get_location(LocationName.PantsShopStartingFlag2).place_locked_item(item) + item = MLSSItem("Victory", ItemClassification.progression, None, self.player) + self.get_location("Cackletta's Soul").place_locked_item(item) def fill_slot_data(self) -> Dict[str, Any]: return { "CastleSkip": self.options.castle_skip.value, "SkipMinecart": self.options.skip_minecart.value, "DisableSurf": self.options.disable_surf.value, - "HarhallsPants": self.options.harhalls_pants.value, + "HarhallsPants": self.options.disable_harhalls_pants.value, "ChuckleBeans": self.options.chuckle_beans.value, "DifficultLogic": self.options.difficult_logic.value, "Coins": self.options.coins.value, @@ -111,7 +113,7 @@ def create_items(self) -> None: freq = item_frequencies.get(item.itemName, 1) if item in precollected: freq = max(freq - precollected.count(item), 0) - if self.options.harhalls_pants and "Harhall's" in item.itemName: + if self.options.disable_harhalls_pants and "Harhall's" in item.itemName: continue required_items += [item.itemName for _ in range(freq)] @@ -135,21 +137,7 @@ def create_items(self) -> None: filler_items += [item.itemName for _ in range(freq)] # And finally take as many fillers as we need to have the same amount of items and locations. - remaining = len(all_locations) - len(required_items) - 5 - if self.options.castle_skip: - remaining -= len(bowsers) + len(bowsersMini) - (5 if self.options.chuckle_beans == 0 else 0) - if self.options.skip_minecart and self.options.chuckle_beans == 2: - remaining -= 1 - if self.options.disable_surf: - remaining -= 1 - if self.options.harhalls_pants: - remaining -= 1 - if self.options.chuckle_beans == 0: - remaining -= 192 - if self.options.chuckle_beans == 1: - remaining -= 59 - if not self.options.coins: - remaining -= len(coins) + remaining = len(all_locations) - len(required_items) - len(self.disabled_locations) - 5 self.multiworld.itempool += [ self.create_item(filler_item_name) for filler_item_name in self.random.sample(filler_items, remaining) @@ -157,21 +145,14 @@ def create_items(self) -> None: def set_rules(self) -> None: set_rules(self, self.disabled_locations) - if self.options.castle_skip: - self.multiworld.completion_condition[self.player] = lambda state: state.can_reach( - "PostJokes", "Region", self.player - ) - else: - self.multiworld.completion_condition[self.player] = lambda state: state.can_reach( - "Bowser's Castle Mini", "Region", self.player - ) + self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) def create_item(self, name: str) -> MLSSItem: item = item_table[name] return MLSSItem(item.itemName, item.classification, item.code, self.player) def get_filler_item_name(self) -> str: - return self.random.choice(list(filter(lambda item: item.classification == ItemClassification.filler, itemList))) + return self.random.choice(list(filter(lambda item: item.classification == ItemClassification.filler, itemList))).itemName def generate_output(self, output_directory: str) -> None: patch = MLSSProcedurePatch(player=self.player, player_name=self.multiworld.player_name[self.player]) diff --git a/worlds/mlss/data/basepatch.bsdiff b/worlds/mlss/data/basepatch.bsdiff index 8f9324995ec4c9ef9992397d64b33847c207600d..7ed6c38ea9f432dfcf506156c77e4f56bdf3026a 100644 GIT binary patch literal 18482 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 literal 17596 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 From db5d9fbf70d89643bf766566c6d944e064369f5a Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Wed, 18 Sep 2024 14:37:17 -0400 Subject: [PATCH 39/94] Pokemon R/B: Version 5 Update (#3566) * Quiz updates * Enable Partial Trainersanity * Losable Key Items Still Count * New options api * Type Chart Seed * Continue switching to new options API * Level Scaling and Quiz fixes * Level Scaling and Quiz fixes * Clarify that palettes are only for Super Gameboy * Type chart seed groups use one random players' options * remove goal option again * Text updates * Trainersanity Trainers ignore Blind Trainers setting * Re-order simple connecting interiors so that directions are preserved when possible * Dexsanity exact number * Year update * Dexsanity Doc update * revert accidental file deletion * Fixes * Add world parameter to logic calls * restore correct seeded random object * missing world.options changes * Trainersanity table bug fix * delete entrances as well as exits when restarting door shuffle * Do not collect route 25 item for level scaling if trainer is trainersanity * world.options in level_scaling.py * Update worlds/pokemon_rb/level_scaling.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/pokemon_rb/encounters.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/pokemon_rb/encounters.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * world -> multiworld * Fix Cerulean Cave Hidden Item Center Rocks region * Fix Cerulean Cave Hidden Item Center Rocks region for real * Remove "self-locking" rules * Update worlds/pokemon_rb/regions.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Fossil events * Update worlds/pokemon_rb/level_scaling.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --------- Co-authored-by: alchav Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- worlds/pokemon_rb/__init__.py | 416 ++++++------ worlds/pokemon_rb/basepatch_blue.bsdiff4 | Bin 46356 -> 47245 bytes worlds/pokemon_rb/basepatch_red.bsdiff4 | Bin 46344 -> 47212 bytes .../docs/en_Pokemon Red and Blue.md | 9 +- worlds/pokemon_rb/encounters.py | 100 +-- worlds/pokemon_rb/items.py | 2 + worlds/pokemon_rb/level_scaling.py | 19 +- worlds/pokemon_rb/locations.py | 46 +- worlds/pokemon_rb/logic.py | 79 ++- worlds/pokemon_rb/options.py | 253 ++++---- worlds/pokemon_rb/pokemon.py | 217 ++++--- worlds/pokemon_rb/regions.py | 594 +++++++++--------- worlds/pokemon_rb/rom.py | 378 ++++++----- worlds/pokemon_rb/rom_addresses.py | 450 +++++++------ worlds/pokemon_rb/rules.py | 207 +++--- 15 files changed, 1435 insertions(+), 1335 deletions(-) diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index c1d843189820..2065507e0d59 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -3,6 +3,7 @@ import typing import threading import base64 +import random from copy import deepcopy from typing import TextIO @@ -14,7 +15,7 @@ from .items import item_table, item_groups from .locations import location_data, PokemonRBLocation from .regions import create_regions -from .options import pokemon_rb_options +from .options import PokemonRBOptions from .rom_addresses import rom_addresses from .text import encode_text from .rom import generate_output, get_base_rom_bytes, get_base_rom_path, RedDeltaPatch, BlueDeltaPatch @@ -71,7 +72,10 @@ class PokemonRedBlueWorld(World): Elite Four to become the champion!""" # -MuffinJets#4559 game = "Pokemon Red and Blue" - option_definitions = pokemon_rb_options + + options_dataclass = PokemonRBOptions + options: PokemonRBOptions + settings: typing.ClassVar[PokemonSettings] required_client_version = (0, 4, 2) @@ -85,8 +89,8 @@ class PokemonRedBlueWorld(World): web = PokemonWebWorld() - def __init__(self, world: MultiWorld, player: int): - super().__init__(world, player) + def __init__(self, multiworld: MultiWorld, player: int): + super().__init__(multiworld, player) self.item_pool = [] self.total_key_items = None self.fly_map = None @@ -101,11 +105,11 @@ def __init__(self, world: MultiWorld, player: int): self.learnsets = None self.trainer_name = None self.rival_name = None - self.type_chart = None self.traps = None self.trade_mons = {} self.finished_level_scaling = threading.Event() self.dexsanity_table = [] + self.trainersanity_table = [] self.local_locs = [] @classmethod @@ -113,11 +117,109 @@ def stage_assert_generate(cls, multiworld: MultiWorld): versions = set() for player in multiworld.player_ids: if multiworld.worlds[player].game == "Pokemon Red and Blue": - versions.add(multiworld.game_version[player].current_key) + versions.add(multiworld.worlds[player].options.game_version.current_key) for version in versions: if not os.path.exists(get_base_rom_path(version)): raise FileNotFoundError(get_base_rom_path(version)) + @classmethod + def stage_generate_early(cls, multiworld: MultiWorld): + + seed_groups = {} + pokemon_rb_worlds = multiworld.get_game_worlds("Pokemon Red and Blue") + + for world in pokemon_rb_worlds: + if not (world.options.type_chart_seed.value.isdigit() or world.options.type_chart_seed.value == "random"): + seed_groups[world.options.type_chart_seed.value] = seed_groups.get(world.options.type_chart_seed.value, + []) + [world] + + copy_chart_worlds = {} + + for worlds in seed_groups.values(): + chosen_world = multiworld.random.choice(worlds) + for world in worlds: + if world is not chosen_world: + copy_chart_worlds[world.player] = chosen_world + + for world in pokemon_rb_worlds: + if world.player in copy_chart_worlds: + continue + tc_random = world.random + if world.options.type_chart_seed.value.isdigit(): + tc_random = random.Random() + tc_random.seed(int(world.options.type_chart_seed.value)) + + if world.options.randomize_type_chart == "vanilla": + chart = deepcopy(poke_data.type_chart) + elif world.options.randomize_type_chart == "randomize": + types = poke_data.type_names.values() + matchups = [] + for type1 in types: + for type2 in types: + matchups.append([type1, type2]) + tc_random.shuffle(matchups) + immunities = world.options.immunity_matchups.value + super_effectives = world.options.super_effective_matchups.value + not_very_effectives = world.options.not_very_effective_matchups.value + normals = world.options.normal_matchups.value + while super_effectives + not_very_effectives + normals < 225 - immunities: + if super_effectives == not_very_effectives == normals == 0: + super_effectives = 225 + not_very_effectives = 225 + normals = 225 + else: + super_effectives += world.options.super_effective_matchups.value + not_very_effectives += world.options.not_very_effective_matchups.value + normals += world.options.normal_matchups.value + if super_effectives + not_very_effectives + normals > 225 - immunities: + total = super_effectives + not_very_effectives + normals + excess = total - (225 - immunities) + subtract_amounts = ( + int((excess / (super_effectives + not_very_effectives + normals)) * super_effectives), + int((excess / (super_effectives + not_very_effectives + normals)) * not_very_effectives), + int((excess / (super_effectives + not_very_effectives + normals)) * normals)) + super_effectives -= subtract_amounts[0] + not_very_effectives -= subtract_amounts[1] + normals -= subtract_amounts[2] + while super_effectives + not_very_effectives + normals > 225 - immunities: + r = tc_random.randint(0, 2) + if r == 0 and super_effectives: + super_effectives -= 1 + elif r == 1 and not_very_effectives: + not_very_effectives -= 1 + elif normals: + normals -= 1 + chart = [] + for matchup_list, matchup_value in zip([immunities, normals, super_effectives, not_very_effectives], + [0, 10, 20, 5]): + for _ in range(matchup_list): + matchup = matchups.pop() + matchup.append(matchup_value) + chart.append(matchup) + elif world.options.randomize_type_chart == "chaos": + types = poke_data.type_names.values() + matchups = [] + for type1 in types: + for type2 in types: + matchups.append([type1, type2]) + chart = [] + values = list(range(21)) + tc_random.shuffle(matchups) + tc_random.shuffle(values) + for matchup in matchups: + value = values.pop(0) + values.append(value) + matchup.append(value) + chart.append(matchup) + # sort so that super-effective matchups occur first, to prevent dual "not very effective" / "super effective" + # matchups from leading to damage being ultimately divided by 2 and then multiplied by 2, which can lead to + # damage being reduced by 1 which leads to a "not very effective" message appearing due to my changes + # to the way effectiveness messages are generated. + world.type_chart = sorted(chart, key=lambda matchup: -matchup[2]) + + for player in copy_chart_worlds: + multiworld.worlds[player].type_chart = copy_chart_worlds[player].type_chart + def generate_early(self): def encode_name(name, t): try: @@ -126,33 +228,33 @@ def encode_name(name, t): return encode_text(name, length=8, whitespace="@", safety=True) except KeyError as e: raise KeyError(f"Invalid character(s) in {t} name for player {self.multiworld.player_name[self.player]}") from e - if self.multiworld.trainer_name[self.player] == "choose_in_game": + if self.options.trainer_name == "choose_in_game": self.trainer_name = "choose_in_game" else: - self.trainer_name = encode_name(self.multiworld.trainer_name[self.player].value, "Player") - if self.multiworld.rival_name[self.player] == "choose_in_game": + self.trainer_name = encode_name(self.options.trainer_name.value, "Player") + if self.options.rival_name == "choose_in_game": self.rival_name = "choose_in_game" else: - self.rival_name = encode_name(self.multiworld.rival_name[self.player].value, "Rival") + self.rival_name = encode_name(self.options.rival_name.value, "Rival") - if not self.multiworld.badgesanity[self.player]: - self.multiworld.non_local_items[self.player].value -= self.item_name_groups["Badges"] + if not self.options.badgesanity: + self.options.non_local_items.value -= self.item_name_groups["Badges"] - if self.multiworld.key_items_only[self.player]: - self.multiworld.trainersanity[self.player] = self.multiworld.trainersanity[self.player].from_text("off") - self.multiworld.dexsanity[self.player].value = 0 - self.multiworld.randomize_hidden_items[self.player] = \ - self.multiworld.randomize_hidden_items[self.player].from_text("off") + if self.options.key_items_only: + self.options.trainersanity.value = 0 + self.options.dexsanity.value = 0 + self.options.randomize_hidden_items = \ + self.options.randomize_hidden_items.from_text("off") - if self.multiworld.badges_needed_for_hm_moves[self.player].value >= 2: + if self.options.badges_needed_for_hm_moves.value >= 2: badges_to_add = ["Marsh Badge", "Volcano Badge", "Earth Badge"] - if self.multiworld.badges_needed_for_hm_moves[self.player].value == 3: + if self.options.badges_needed_for_hm_moves.value == 3: badges = ["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Marsh Badge", "Soul Badge", "Volcano Badge", "Earth Badge"] - self.multiworld.random.shuffle(badges) + self.random.shuffle(badges) badges_to_add += [badges.pop(), badges.pop()] hm_moves = ["Cut", "Fly", "Surf", "Strength", "Flash"] - self.multiworld.random.shuffle(hm_moves) + self.random.shuffle(hm_moves) self.extra_badges = {} for badge in badges_to_add: self.extra_badges[hm_moves.pop()] = badge @@ -160,79 +262,17 @@ def encode_name(name, t): process_move_data(self) process_pokemon_data(self) - if self.multiworld.randomize_type_chart[self.player] == "vanilla": - chart = deepcopy(poke_data.type_chart) - elif self.multiworld.randomize_type_chart[self.player] == "randomize": - types = poke_data.type_names.values() - matchups = [] - for type1 in types: - for type2 in types: - matchups.append([type1, type2]) - self.multiworld.random.shuffle(matchups) - immunities = self.multiworld.immunity_matchups[self.player].value - super_effectives = self.multiworld.super_effective_matchups[self.player].value - not_very_effectives = self.multiworld.not_very_effective_matchups[self.player].value - normals = self.multiworld.normal_matchups[self.player].value - while super_effectives + not_very_effectives + normals < 225 - immunities: - if super_effectives == not_very_effectives == normals == 0: - super_effectives = 225 - not_very_effectives = 225 - normals = 225 - else: - super_effectives += self.multiworld.super_effective_matchups[self.player].value - not_very_effectives += self.multiworld.not_very_effective_matchups[self.player].value - normals += self.multiworld.normal_matchups[self.player].value - if super_effectives + not_very_effectives + normals > 225 - immunities: - total = super_effectives + not_very_effectives + normals - excess = total - (225 - immunities) - subtract_amounts = ( - int((excess / (super_effectives + not_very_effectives + normals)) * super_effectives), - int((excess / (super_effectives + not_very_effectives + normals)) * not_very_effectives), - int((excess / (super_effectives + not_very_effectives + normals)) * normals)) - super_effectives -= subtract_amounts[0] - not_very_effectives -= subtract_amounts[1] - normals -= subtract_amounts[2] - while super_effectives + not_very_effectives + normals > 225 - immunities: - r = self.multiworld.random.randint(0, 2) - if r == 0 and super_effectives: - super_effectives -= 1 - elif r == 1 and not_very_effectives: - not_very_effectives -= 1 - elif normals: - normals -= 1 - chart = [] - for matchup_list, matchup_value in zip([immunities, normals, super_effectives, not_very_effectives], - [0, 10, 20, 5]): - for _ in range(matchup_list): - matchup = matchups.pop() - matchup.append(matchup_value) - chart.append(matchup) - elif self.multiworld.randomize_type_chart[self.player] == "chaos": - types = poke_data.type_names.values() - matchups = [] - for type1 in types: - for type2 in types: - matchups.append([type1, type2]) - chart = [] - values = list(range(21)) - self.multiworld.random.shuffle(matchups) - self.multiworld.random.shuffle(values) - for matchup in matchups: - value = values.pop(0) - values.append(value) - matchup.append(value) - chart.append(matchup) - # sort so that super-effective matchups occur first, to prevent dual "not very effective" / "super effective" - # matchups from leading to damage being ultimately divided by 2 and then multiplied by 2, which can lead to - # damage being reduced by 1 which leads to a "not very effective" message appearing due to my changes - # to the way effectiveness messages are generated. - self.type_chart = sorted(chart, key=lambda matchup: -matchup[2]) - self.dexsanity_table = [ - *(True for _ in range(round(self.multiworld.dexsanity[self.player].value * 1.51))), - *(False for _ in range(151 - round(self.multiworld.dexsanity[self.player].value * 1.51))) + *(True for _ in range(round(self.options.dexsanity.value))), + *(False for _ in range(151 - round(self.options.dexsanity.value))) + ] + self.random.shuffle(self.dexsanity_table) + + self.trainersanity_table = [ + *(True for _ in range(self.options.trainersanity.value)), + *(False for _ in range(317 - self.options.trainersanity.value)) ] - self.multiworld.random.shuffle(self.dexsanity_table) + self.random.shuffle(self.trainersanity_table) def create_items(self): self.multiworld.itempool += self.item_pool @@ -275,9 +315,9 @@ def stage_fill_hook(cls, multiworld, progitempool, usefulitempool, filleritempoo filleritempool += [item for item in unplaced_items if (not item.advancement) and (not item.useful)] def fill_hook(self, progitempool, usefulitempool, filleritempool, fill_locations): - if not self.multiworld.badgesanity[self.player]: + if not self.options.badgesanity: # Door Shuffle options besides Simple place badges during door shuffling - if self.multiworld.door_shuffle[self.player] in ("off", "simple"): + if self.options.door_shuffle in ("off", "simple"): badges = [item for item in progitempool if "Badge" in item.name and item.player == self.player] for badge in badges: self.multiworld.itempool.remove(badge) @@ -297,8 +337,8 @@ def fill_hook(self, progitempool, usefulitempool, filleritempool, fill_locations for mon in poke_data.pokemon_data.keys(): state.collect(self.create_item(mon), True) state.sweep_for_advancements() - self.multiworld.random.shuffle(badges) - self.multiworld.random.shuffle(badgelocs) + self.random.shuffle(badges) + self.random.shuffle(badgelocs) badgelocs_copy = badgelocs.copy() # allow_partial so that unplaced badges aren't lost, for debugging purposes fill_restrictive(self.multiworld, state, badgelocs_copy, badges, True, True, allow_partial=True) @@ -318,7 +358,7 @@ def fill_hook(self, progitempool, usefulitempool, filleritempool, fill_locations raise FillError(f"Failed to place badges for player {self.player}") verify_hm_moves(self.multiworld, self, self.player) - if self.multiworld.key_items_only[self.player]: + if self.options.key_items_only: return tms = [item for item in usefulitempool + filleritempool if item.name.startswith("TM") and (item.player == @@ -340,7 +380,7 @@ def fill_hook(self, progitempool, usefulitempool, filleritempool, fill_locations int((int(tm.name[2:4]) - 1) / 8)] & 1 << ((int(tm.name[2:4]) - 1) % 8)] if not learnable_tms: learnable_tms = tms - tm = self.multiworld.random.choice(learnable_tms) + tm = self.random.choice(learnable_tms) loc.place_locked_item(tm) fill_locations.remove(loc) @@ -370,9 +410,9 @@ def pre_fill(self) -> None: if not all_state.can_reach(location, player=self.player): evolutions_region.locations.remove(location) - if self.multiworld.old_man[self.player] == "early_parcel": + if self.options.old_man == "early_parcel": self.multiworld.local_early_items[self.player]["Oak's Parcel"] = 1 - if self.multiworld.dexsanity[self.player]: + if self.options.dexsanity: for i, mon in enumerate(poke_data.pokemon_data): if self.dexsanity_table[i]: location = self.multiworld.get_location(f"Pokedex - {mon}", self.player) @@ -384,13 +424,13 @@ def pre_fill(self) -> None: locs = {self.multiworld.get_location("Fossil - Choice A", self.player), self.multiworld.get_location("Fossil - Choice B", self.player)} - if not self.multiworld.key_items_only[self.player]: + if not self.options.key_items_only: rule = None - if self.multiworld.fossil_check_item_types[self.player] == "key_items": + if self.options.fossil_check_item_types == "key_items": rule = lambda i: i.advancement - elif self.multiworld.fossil_check_item_types[self.player] == "unique_items": + elif self.options.fossil_check_item_types == "unique_items": rule = lambda i: i.name in item_groups["Unique"] - elif self.multiworld.fossil_check_item_types[self.player] == "no_key_items": + elif self.options.fossil_check_item_types == "no_key_items": rule = lambda i: not i.advancement if rule: for loc in locs: @@ -406,16 +446,16 @@ def pre_fill(self) -> None: if loc.item is None: locs.add(loc) - if not self.multiworld.key_items_only[self.player]: + if not self.options.key_items_only: loc = self.multiworld.get_location("Player's House 2F - Player's PC", self.player) if loc.item is None: locs.add(loc) for loc in sorted(locs): - if loc.name in self.multiworld.priority_locations[self.player].value: + if loc.name in self.options.priority_locations.value: add_item_rule(loc, lambda i: i.advancement) add_item_rule(loc, lambda i: i.player == self.player) - if self.multiworld.old_man[self.player] == "early_parcel" and loc.name != "Player's House 2F - Player's PC": + if self.options.old_man == "early_parcel" and loc.name != "Player's House 2F - Player's PC": add_item_rule(loc, lambda i: i.name != "Oak's Parcel") self.local_locs = locs @@ -440,10 +480,10 @@ def pre_fill(self) -> None: else: region_mons.add(location.item.name) - self.multiworld.elite_four_pokedex_condition[self.player].total = \ - int((len(reachable_mons) / 100) * self.multiworld.elite_four_pokedex_condition[self.player].value) + self.options.elite_four_pokedex_condition.total = \ + int((len(reachable_mons) / 100) * self.options.elite_four_pokedex_condition.value) - if self.multiworld.accessibility[self.player] == "full": + if self.options.accessibility == "full": balls = [self.create_item(ball) for ball in ["Poke Ball", "Great Ball", "Ultra Ball"]] traps = [self.create_item(trap) for trap in item_groups["Traps"]] locations = [location for location in self.multiworld.get_locations(self.player) if "Pokedex - " in @@ -469,7 +509,7 @@ def pre_fill(self) -> None: else: break else: - self.multiworld.random.shuffle(traps) + self.random.shuffle(traps) for trap in traps: try: self.multiworld.itempool.remove(trap) @@ -497,22 +537,22 @@ def stage_post_fill(cls, multiworld): found_mons.add(key) def create_regions(self): - if (self.multiworld.old_man[self.player] == "vanilla" or - self.multiworld.door_shuffle[self.player] in ("full", "insanity")): - fly_map_codes = self.multiworld.random.sample(range(2, 11), 2) - elif (self.multiworld.door_shuffle[self.player] == "simple" or - self.multiworld.route_3_condition[self.player] == "boulder_badge" or - (self.multiworld.route_3_condition[self.player] == "any_badge" and - self.multiworld.badgesanity[self.player])): - fly_map_codes = self.multiworld.random.sample(range(3, 11), 2) + if (self.options.old_man == "vanilla" or + self.options.door_shuffle in ("full", "insanity")): + fly_map_codes = self.random.sample(range(2, 11), 2) + elif (self.options.door_shuffle == "simple" or + self.options.route_3_condition == "boulder_badge" or + (self.options.route_3_condition == "any_badge" and + self.options.badgesanity)): + fly_map_codes = self.random.sample(range(3, 11), 2) else: - fly_map_codes = self.multiworld.random.sample([4, 6, 7, 8, 9, 10], 2) - if self.multiworld.free_fly_location[self.player]: + fly_map_codes = self.random.sample([4, 6, 7, 8, 9, 10], 2) + if self.options.free_fly_location: fly_map_code = fly_map_codes[0] else: fly_map_code = 0 - if self.multiworld.town_map_fly_location[self.player]: + if self.options.town_map_fly_location: town_map_fly_map_code = fly_map_codes[1] else: town_map_fly_map_code = 0 @@ -528,7 +568,7 @@ def create_regions(self): self.multiworld.completion_condition[self.player] = lambda state, player=self.player: state.has("Become Champion", player=player) def set_rules(self): - set_rules(self.multiworld, self.player) + set_rules(self.multiworld, self, self.player) def create_item(self, name: str) -> Item: return PokemonRBItem(name, self.player) @@ -548,19 +588,19 @@ def modify_multidata(self, multidata: dict): multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]] def write_spoiler_header(self, spoiler_handle: TextIO): - spoiler_handle.write(f"Cerulean Cave Total Key Items: {self.multiworld.cerulean_cave_key_items_condition[self.player].total}\n") - spoiler_handle.write(f"Elite Four Total Key Items: {self.multiworld.elite_four_key_items_condition[self.player].total}\n") - spoiler_handle.write(f"Elite Four Total Pokemon: {self.multiworld.elite_four_pokedex_condition[self.player].total}\n") - if self.multiworld.free_fly_location[self.player]: + spoiler_handle.write(f"Cerulean Cave Total Key Items: {self.options.cerulean_cave_key_items_condition.total}\n") + spoiler_handle.write(f"Elite Four Total Key Items: {self.options.elite_four_key_items_condition.total}\n") + spoiler_handle.write(f"Elite Four Total Pokemon: {self.options.elite_four_pokedex_condition.total}\n") + if self.options.free_fly_location: spoiler_handle.write(f"Free Fly Location: {self.fly_map}\n") - if self.multiworld.town_map_fly_location[self.player]: + if self.options.town_map_fly_location: spoiler_handle.write(f"Town Map Fly Location: {self.town_map_fly_map}\n") if self.extra_badges: for hm_move, badge in self.extra_badges.items(): spoiler_handle.write(hm_move + " enabled by: " + (" " * 20)[:20 - len(hm_move)] + badge + "\n") def write_spoiler(self, spoiler_handle): - if self.multiworld.randomize_type_chart[self.player].value: + if self.options.randomize_type_chart: spoiler_handle.write(f"\n\nType matchups ({self.multiworld.player_name[self.player]}):\n\n") for matchup in self.type_chart: spoiler_handle.write(f"{matchup[0]} deals {matchup[2] * 10}% damage to {matchup[1]}\n") @@ -571,39 +611,39 @@ def write_spoiler(self, spoiler_handle): spoiler_handle.write(location.name + ": " + location.item.name + "\n") def get_filler_item_name(self) -> str: - combined_traps = (self.multiworld.poison_trap_weight[self.player].value - + self.multiworld.fire_trap_weight[self.player].value - + self.multiworld.paralyze_trap_weight[self.player].value - + self.multiworld.ice_trap_weight[self.player].value - + self.multiworld.sleep_trap_weight[self.player].value) + combined_traps = (self.options.poison_trap_weight.value + + self.options.fire_trap_weight.value + + self.options.paralyze_trap_weight.value + + self.options.ice_trap_weight.value + + self.options.sleep_trap_weight.value) if (combined_traps > 0 and - self.multiworld.random.randint(1, 100) <= self.multiworld.trap_percentage[self.player].value): + self.random.randint(1, 100) <= self.options.trap_percentage.value): return self.select_trap() banned_items = item_groups["Unique"] - if (((not self.multiworld.tea[self.player]) or "Saffron City" not in [self.fly_map, self.town_map_fly_map]) - and (not self.multiworld.door_shuffle[self.player])): + 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 # able to reach the Celadon Department Store, so Poké Dolls would not allow early access to anything banned_items.append("Poke Doll") - if not self.multiworld.tea[self.player]: + if not self.options.tea: banned_items += item_groups["Vending Machine Drinks"] - return self.multiworld.random.choice([item for item in item_table if item_table[item].id and item_table[ + return self.random.choice([item for item in item_table if item_table[item].id and item_table[ item].classification == ItemClassification.filler and item not in banned_items]) def select_trap(self): if self.traps is None: self.traps = [] - self.traps += ["Poison Trap"] * self.multiworld.poison_trap_weight[self.player].value - self.traps += ["Fire Trap"] * self.multiworld.fire_trap_weight[self.player].value - self.traps += ["Paralyze Trap"] * self.multiworld.paralyze_trap_weight[self.player].value - self.traps += ["Ice Trap"] * self.multiworld.ice_trap_weight[self.player].value - self.traps += ["Sleep Trap"] * self.multiworld.sleep_trap_weight[self.player].value - return self.multiworld.random.choice(self.traps) + self.traps += ["Poison Trap"] * self.options.poison_trap_weight.value + self.traps += ["Fire Trap"] * self.options.fire_trap_weight.value + self.traps += ["Paralyze Trap"] * self.options.paralyze_trap_weight.value + self.traps += ["Ice Trap"] * self.options.ice_trap_weight.value + self.traps += ["Sleep Trap"] * self.options.sleep_trap_weight.value + return self.random.choice(self.traps) def extend_hint_information(self, hint_data): - if self.multiworld.dexsanity[self.player] or self.multiworld.door_shuffle[self.player]: + if self.options.dexsanity or self.options.door_shuffle: hint_data[self.player] = {} - if self.multiworld.dexsanity[self.player]: + if self.options.dexsanity: mon_locations = {mon: set() for mon in poke_data.pokemon_data.keys()} for loc in location_data: if loc.type in ["Wild Encounter", "Static Pokemon", "Legendary Pokemon", "Missable Pokemon"]: @@ -616,57 +656,59 @@ def extend_hint_information(self, hint_data): hint_data[self.player][self.multiworld.get_location(f"Pokedex - {mon}", self.player).address] =\ ", ".join(mon_locations[mon]) - if self.multiworld.door_shuffle[self.player]: + if self.options.door_shuffle: for location in self.multiworld.get_locations(self.player): if location.parent_region.entrance_hint and location.address: hint_data[self.player][location.address] = location.parent_region.entrance_hint def fill_slot_data(self) -> dict: - return { - "second_fossil_check_condition": self.multiworld.second_fossil_check_condition[self.player].value, - "require_item_finder": self.multiworld.require_item_finder[self.player].value, - "randomize_hidden_items": self.multiworld.randomize_hidden_items[self.player].value, - "badges_needed_for_hm_moves": self.multiworld.badges_needed_for_hm_moves[self.player].value, - "oaks_aide_rt_2": self.multiworld.oaks_aide_rt_2[self.player].value, - "oaks_aide_rt_11": self.multiworld.oaks_aide_rt_11[self.player].value, - "oaks_aide_rt_15": self.multiworld.oaks_aide_rt_15[self.player].value, - "extra_key_items": self.multiworld.extra_key_items[self.player].value, - "extra_strength_boulders": self.multiworld.extra_strength_boulders[self.player].value, - "tea": self.multiworld.tea[self.player].value, - "old_man": self.multiworld.old_man[self.player].value, - "elite_four_badges_condition": self.multiworld.elite_four_badges_condition[self.player].value, - "elite_four_key_items_condition": self.multiworld.elite_four_key_items_condition[self.player].total, - "elite_four_pokedex_condition": self.multiworld.elite_four_pokedex_condition[self.player].total, - "victory_road_condition": self.multiworld.victory_road_condition[self.player].value, - "route_22_gate_condition": self.multiworld.route_22_gate_condition[self.player].value, - "route_3_condition": self.multiworld.route_3_condition[self.player].value, - "robbed_house_officer": self.multiworld.robbed_house_officer[self.player].value, - "viridian_gym_condition": self.multiworld.viridian_gym_condition[self.player].value, - "cerulean_cave_badges_condition": self.multiworld.cerulean_cave_badges_condition[self.player].value, - "cerulean_cave_key_items_condition": self.multiworld.cerulean_cave_key_items_condition[self.player].total, + ret = { + "second_fossil_check_condition": self.options.second_fossil_check_condition.value, + "require_item_finder": self.options.require_item_finder.value, + "randomize_hidden_items": self.options.randomize_hidden_items.value, + "badges_needed_for_hm_moves": self.options.badges_needed_for_hm_moves.value, + "oaks_aide_rt_2": self.options.oaks_aide_rt_2.value, + "oaks_aide_rt_11": self.options.oaks_aide_rt_11.value, + "oaks_aide_rt_15": self.options.oaks_aide_rt_15.value, + "extra_key_items": self.options.extra_key_items.value, + "extra_strength_boulders": self.options.extra_strength_boulders.value, + "tea": self.options.tea.value, + "old_man": self.options.old_man.value, + "elite_four_badges_condition": self.options.elite_four_badges_condition.value, + "elite_four_key_items_condition": self.options.elite_four_key_items_condition.total, + "elite_four_pokedex_condition": self.options.elite_four_pokedex_condition.total, + "victory_road_condition": self.options.victory_road_condition.value, + "route_22_gate_condition": self.options.route_22_gate_condition.value, + "route_3_condition": self.options.route_3_condition.value, + "robbed_house_officer": self.options.robbed_house_officer.value, + "viridian_gym_condition": self.options.viridian_gym_condition.value, + "cerulean_cave_badges_condition": self.options.cerulean_cave_badges_condition.value, + "cerulean_cave_key_items_condition": self.options.cerulean_cave_key_items_condition.total, "free_fly_map": self.fly_map_code, "town_map_fly_map": self.town_map_fly_map_code, "extra_badges": self.extra_badges, - "type_chart": self.type_chart, - "randomize_pokedex": self.multiworld.randomize_pokedex[self.player].value, - "trainersanity": self.multiworld.trainersanity[self.player].value, - "death_link": self.multiworld.death_link[self.player].value, - "prizesanity": self.multiworld.prizesanity[self.player].value, - "key_items_only": self.multiworld.key_items_only[self.player].value, - "poke_doll_skip": self.multiworld.poke_doll_skip[self.player].value, - "bicycle_gate_skips": self.multiworld.bicycle_gate_skips[self.player].value, - "stonesanity": self.multiworld.stonesanity[self.player].value, - "door_shuffle": self.multiworld.door_shuffle[self.player].value, - "warp_tile_shuffle": self.multiworld.warp_tile_shuffle[self.player].value, - "dark_rock_tunnel_logic": self.multiworld.dark_rock_tunnel_logic[self.player].value, - "split_card_key": self.multiworld.split_card_key[self.player].value, - "all_elevators_locked": self.multiworld.all_elevators_locked[self.player].value, - "require_pokedex": self.multiworld.require_pokedex[self.player].value, - "area_1_to_1_mapping": self.multiworld.area_1_to_1_mapping[self.player].value, - "blind_trainers": self.multiworld.blind_trainers[self.player].value, + "randomize_pokedex": self.options.randomize_pokedex.value, + "trainersanity": self.options.trainersanity.value, + "death_link": self.options.death_link.value, + "prizesanity": self.options.prizesanity.value, + "key_items_only": self.options.key_items_only.value, + "poke_doll_skip": self.options.poke_doll_skip.value, + "bicycle_gate_skips": self.options.bicycle_gate_skips.value, + "stonesanity": self.options.stonesanity.value, + "door_shuffle": self.options.door_shuffle.value, + "warp_tile_shuffle": self.options.warp_tile_shuffle.value, + "dark_rock_tunnel_logic": self.options.dark_rock_tunnel_logic.value, + "split_card_key": self.options.split_card_key.value, + "all_elevators_locked": self.options.all_elevators_locked.value, + "require_pokedex": self.options.require_pokedex.value, + "area_1_to_1_mapping": self.options.area_1_to_1_mapping.value, + "blind_trainers": self.options.blind_trainers.value, } + if self.options.type_chart_seed == "random" or self.options.type_chart_seed.value.isdigit(): + ret["type_chart"] = self.type_chart + return ret class PokemonRBItem(Item): game = "Pokemon Red and Blue" diff --git a/worlds/pokemon_rb/basepatch_blue.bsdiff4 b/worlds/pokemon_rb/basepatch_blue.bsdiff4 index 0f65564a737be4e77c18cf94336ac9ba859345a9..bcd94c632d2cf9e0d3c44e142f1635cf6e42ac0b 100644 GIT binary patch literal 47245 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 K!i0yNB#b!4-%|wu literal 46356 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

%ff+)8u7 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 z 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

^yJTP6oc_cjvKX=i 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 diff --git a/worlds/pokemon_rb/basepatch_red.bsdiff4 b/worlds/pokemon_rb/basepatch_red.bsdiff4 index 826b7bf8b4e5ff0dd0926ca876c9930eebe4eefc..4b207108cf0c6f20611233888fc279b9ce44cb93 100644 GIT binary patch literal 47212 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 hHxq+uj|F+OgQN}lYl-uTcBrIq=semM;Zc_jN literal 46344 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 DjU7nf diff --git a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md index 1e5c14eb99f5..6811b5926078 100644 --- a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md +++ b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md @@ -60,11 +60,12 @@ and Safari Zone. Adds 4 extra item locations to Rock Tunnel B1F * Split Card Key: Splits the Card Key into 10 different Card Keys, one for each floor of Silph Co that has locked doors. Adds 9 location checks to friendly NPCs in Silph Co. You can also choose Progressive Card Keys to always obtain the keys in order from Card Key 2F to Card Key 11F. -* Trainersanity: Adds location checks to 317 trainers. Does not include scripted trainers, most of which disappear +* Trainersanity: Adds location checks to trainers. You may choose between 0 and 317 trainersanity checks. Trainers +will be randomly selected to be given checks. Does not include scripted trainers, most of which disappear after battling them, but also includes Gym Leaders. You must talk to the trainer after defeating them to receive -your prize. Adds 317 random filler items to the item pool -* Dexsanity: Location checks occur when registering Pokémon as owned in the Pokédex. You can choose a percentage -of Pokémon to have checks added to, chosen randomly. You can identify which Pokémon have location checks by an empty +your prize. Adds random filler items to the item pool. +* Dexsanity: Location checks occur when registering Pokémon as owned in the Pokédex. You can choose between 0 and 151 +Pokémon to have checks added to, chosen randomly. You can identify which Pokémon have location checks by an empty Poké Ball icon shown in battle or in the Pokédex menu. ## Which items can be in another player's world? diff --git a/worlds/pokemon_rb/encounters.py b/worlds/pokemon_rb/encounters.py index 6d1762b0ca71..fbe4abfe4466 100644 --- a/worlds/pokemon_rb/encounters.py +++ b/worlds/pokemon_rb/encounters.py @@ -8,7 +8,7 @@ def get_encounter_slots(self): for location in encounter_slots: if isinstance(location.original_item, list): - location.original_item = location.original_item[not self.multiworld.game_version[self.player].value] + location.original_item = location.original_item[not self.options.game_version.value] return encounter_slots @@ -39,16 +39,16 @@ def randomize_pokemon(self, mon, mons_list, randomize_type, random): return mon -def process_trainer_data(self): +def process_trainer_data(world): mons_list = [pokemon for pokemon in poke_data.pokemon_data.keys() if pokemon not in poke_data.legendary_pokemon - or self.multiworld.trainer_legendaries[self.player].value] + or world.options.trainer_legendaries.value] unevolved_mons = [pokemon for pokemon in poke_data.first_stage_pokemon if pokemon not in poke_data.legendary_pokemon - or self.multiworld.randomize_legendary_pokemon[self.player].value == 3] + or world.options.randomize_legendary_pokemon.value == 3] evolved_mons = [mon for mon in mons_list if mon not in unevolved_mons] rival_map = { - "Charmander": self.multiworld.get_location("Oak's Lab - Starter 1", self.player).item.name[9:], # strip the - "Squirtle": self.multiworld.get_location("Oak's Lab - Starter 2", self.player).item.name[9:], # 'Missable' - "Bulbasaur": self.multiworld.get_location("Oak's Lab - Starter 3", self.player).item.name[9:], # from the name + "Charmander": world.multiworld.get_location("Oak's Lab - Starter 1", world.player).item.name[9:], # strip the + "Squirtle": world.multiworld.get_location("Oak's Lab - Starter 2", world.player).item.name[9:], # 'Missable' + "Bulbasaur": world.multiworld.get_location("Oak's Lab - Starter 3", world.player).item.name[9:], # from the name } def add_evolutions(): @@ -60,7 +60,7 @@ def add_evolutions(): rival_map[poke_data.evolves_to[a]] = b add_evolutions() add_evolutions() - parties_objs = [location for location in self.multiworld.get_locations(self.player) + parties_objs = [location for location in world.multiworld.get_locations(world.player) if location.type == "Trainer Parties"] # Process Rival parties in order "Route 22 " is not a typo parties_objs.sort(key=lambda i: 0 if "Oak's Lab" in i.name else 1 if "Route 22 " in i.name else 2 if "Cerulean City" @@ -75,25 +75,25 @@ def add_evolutions(): for i, mon in enumerate(rival_party): if mon in ("Bulbasaur", "Ivysaur", "Venusaur", "Charmander", "Charmeleon", "Charizard", "Squirtle", "Wartortle", "Blastoise"): - if self.multiworld.randomize_starter_pokemon[self.player]: + if world.options.randomize_starter_pokemon: rival_party[i] = rival_map[mon] - elif self.multiworld.randomize_trainer_parties[self.player]: + elif world.options.randomize_trainer_parties: if mon in rival_map: rival_party[i] = rival_map[mon] else: - new_mon = randomize_pokemon(self, mon, + new_mon = randomize_pokemon(world, mon, unevolved_mons if mon in unevolved_mons else evolved_mons, - self.multiworld.randomize_trainer_parties[self.player].value, - self.multiworld.random) + world.options.randomize_trainer_parties.value, + world.random) rival_map[mon] = new_mon rival_party[i] = new_mon add_evolutions() else: - if self.multiworld.randomize_trainer_parties[self.player]: + if world.options.randomize_trainer_parties: for i, mon in enumerate(party["party"]): - party["party"][i] = randomize_pokemon(self, mon, mons_list, - self.multiworld.randomize_trainer_parties[self.player].value, - self.multiworld.random) + party["party"][i] = randomize_pokemon(world, mon, mons_list, + world.options.randomize_trainer_parties.value, + world.random) def process_pokemon_locations(self): @@ -106,21 +106,21 @@ def process_pokemon_locations(self): placed_mons = {pokemon: 0 for pokemon in poke_data.pokemon_data.keys()} mons_list = [pokemon for pokemon in poke_data.first_stage_pokemon if pokemon not in poke_data.legendary_pokemon - or self.multiworld.randomize_legendary_pokemon[self.player].value == 3] - if self.multiworld.randomize_legendary_pokemon[self.player] == "vanilla": + or self.options.randomize_legendary_pokemon.value == 3] + if self.options.randomize_legendary_pokemon == "vanilla": for slot in legendary_slots: location = self.multiworld.get_location(slot.name, self.player) location.place_locked_item(self.create_item("Static " + slot.original_item)) - elif self.multiworld.randomize_legendary_pokemon[self.player] == "shuffle": - self.multiworld.random.shuffle(legendary_mons) + elif self.options.randomize_legendary_pokemon == "shuffle": + self.random.shuffle(legendary_mons) for slot in legendary_slots: location = self.multiworld.get_location(slot.name, self.player) mon = legendary_mons.pop() location.place_locked_item(self.create_item("Static " + mon)) placed_mons[mon] += 1 - elif self.multiworld.randomize_legendary_pokemon[self.player] == "static": + elif self.options.randomize_legendary_pokemon == "static": static_slots = static_slots + legendary_slots - self.multiworld.random.shuffle(static_slots) + self.random.shuffle(static_slots) static_slots.sort(key=lambda s: s.name != "Pokemon Tower 6F - Restless Soul") while legendary_slots: swap_slot = legendary_slots.pop() @@ -131,12 +131,12 @@ def process_pokemon_locations(self): location = self.multiworld.get_location(slot.name, self.player) location.place_locked_item(self.create_item(slot_type + " " + swap_slot.original_item)) swap_slot.original_item = slot.original_item - elif self.multiworld.randomize_legendary_pokemon[self.player] == "any": + elif self.options.randomize_legendary_pokemon == "any": static_slots = static_slots + legendary_slots for slot in static_slots: location = self.multiworld.get_location(slot.name, self.player) - randomize_type = self.multiworld.randomize_static_pokemon[self.player].value + randomize_type = self.options.randomize_static_pokemon.value slot_type = slot.type.split()[0] if slot_type == "Legendary": slot_type = "Static" @@ -145,7 +145,7 @@ def process_pokemon_locations(self): else: mon = self.create_item(slot_type + " " + randomize_pokemon(self, slot.original_item, mons_list, randomize_type, - self.multiworld.random)) + self.random)) location.place_locked_item(mon) if slot_type != "Missable": placed_mons[mon.name.replace("Static ", "")] += 1 @@ -153,16 +153,16 @@ def process_pokemon_locations(self): chosen_mons = set() for slot in starter_slots: location = self.multiworld.get_location(slot.name, self.player) - randomize_type = self.multiworld.randomize_starter_pokemon[self.player].value + randomize_type = self.options.randomize_starter_pokemon.value slot_type = "Missable" if not randomize_type: location.place_locked_item(self.create_item(slot_type + " " + slot.original_item)) else: mon = self.create_item(slot_type + " " + randomize_pokemon(self, slot.original_item, mons_list, - randomize_type, self.multiworld.random)) + randomize_type, self.random)) while mon.name in chosen_mons: mon = self.create_item(slot_type + " " + randomize_pokemon(self, slot.original_item, mons_list, - randomize_type, self.multiworld.random)) + randomize_type, self.random)) chosen_mons.add(mon.name) location.place_locked_item(mon) @@ -170,10 +170,10 @@ def process_pokemon_locations(self): encounter_slots = encounter_slots_master.copy() zone_mapping = {} - if self.multiworld.randomize_wild_pokemon[self.player]: + if self.options.randomize_wild_pokemon: mons_list = [pokemon for pokemon in poke_data.pokemon_data.keys() if pokemon not in poke_data.legendary_pokemon - or self.multiworld.randomize_legendary_pokemon[self.player].value == 3] - self.multiworld.random.shuffle(encounter_slots) + or self.options.randomize_legendary_pokemon.value == 3] + self.random.shuffle(encounter_slots) locations = [] for slot in encounter_slots: location = self.multiworld.get_location(slot.name, self.player) @@ -181,11 +181,11 @@ def process_pokemon_locations(self): if zone not in zone_mapping: zone_mapping[zone] = {} original_mon = slot.original_item - if self.multiworld.area_1_to_1_mapping[self.player] and original_mon in zone_mapping[zone]: + if self.options.area_1_to_1_mapping and original_mon in zone_mapping[zone]: mon = zone_mapping[zone][original_mon] else: mon = randomize_pokemon(self, original_mon, mons_list, - self.multiworld.randomize_wild_pokemon[self.player].value, self.multiworld.random) + self.options.randomize_wild_pokemon.value, self.random) # while ("Pokemon Tower 6F" in slot.name and self.multiworld.get_location("Pokemon Tower 6F - Restless Soul", self.player).item.name @@ -194,7 +194,7 @@ def process_pokemon_locations(self): # the battle is treates as the Restless Soul battle and you cannot catch it. So, prevent any wild mons # from being the same species as the Restless Soul. # to account for the possibility that only one ground type Pokemon exists, match only stats for this fix - mon = randomize_pokemon(self, original_mon, mons_list, 2, self.multiworld.random) + mon = randomize_pokemon(self, original_mon, mons_list, 2, self.random) placed_mons[mon] += 1 location.item = self.create_item(mon) location.locked = True @@ -204,28 +204,28 @@ def process_pokemon_locations(self): mons_to_add = [] remaining_pokemon = [pokemon for pokemon in poke_data.pokemon_data.keys() if placed_mons[pokemon] == 0 and - (pokemon not in poke_data.legendary_pokemon or self.multiworld.randomize_legendary_pokemon[self.player].value == 3)] - if self.multiworld.catch_em_all[self.player] == "first_stage": + (pokemon not in poke_data.legendary_pokemon or self.options.randomize_legendary_pokemon.value == 3)] + if self.options.catch_em_all == "first_stage": mons_to_add = [pokemon for pokemon in poke_data.first_stage_pokemon if placed_mons[pokemon] == 0 and - (pokemon not in poke_data.legendary_pokemon or self.multiworld.randomize_legendary_pokemon[self.player].value == 3)] - elif self.multiworld.catch_em_all[self.player] == "all_pokemon": + (pokemon not in poke_data.legendary_pokemon or self.options.randomize_legendary_pokemon.value == 3)] + elif self.options.catch_em_all == "all_pokemon": mons_to_add = remaining_pokemon.copy() - logic_needed_mons = max(self.multiworld.oaks_aide_rt_2[self.player].value, - self.multiworld.oaks_aide_rt_11[self.player].value, - self.multiworld.oaks_aide_rt_15[self.player].value) - if self.multiworld.accessibility[self.player] == "minimal": + logic_needed_mons = max(self.options.oaks_aide_rt_2.value, + self.options.oaks_aide_rt_11.value, + self.options.oaks_aide_rt_15.value) + if self.options.accessibility == "minimal": logic_needed_mons = 0 - self.multiworld.random.shuffle(remaining_pokemon) + self.random.shuffle(remaining_pokemon) while (len([pokemon for pokemon in placed_mons if placed_mons[pokemon] > 0]) + len(mons_to_add) < logic_needed_mons): mons_to_add.append(remaining_pokemon.pop()) for mon in mons_to_add: stat_base = get_base_stat_total(mon) candidate_locations = encounter_slots_master.copy() - if self.multiworld.randomize_wild_pokemon[self.player].current_key in ["match_base_stats", "match_types_and_base_stats"]: + if self.options.randomize_wild_pokemon.current_key in ["match_base_stats", "match_types_and_base_stats"]: candidate_locations.sort(key=lambda slot: abs(get_base_stat_total(slot.original_item) - stat_base)) - if self.multiworld.randomize_wild_pokemon[self.player].current_key in ["match_types", "match_types_and_base_stats"]: + if self.options.randomize_wild_pokemon.current_key in ["match_types", "match_types_and_base_stats"]: candidate_locations.sort(key=lambda slot: not any([poke_data.pokemon_data[slot.original_item]["type1"] in [self.local_poke_data[mon]["type1"], self.local_poke_data[mon]["type2"]], poke_data.pokemon_data[slot.original_item]["type2"] in @@ -233,12 +233,12 @@ def process_pokemon_locations(self): candidate_locations = [self.multiworld.get_location(location.name, self.player) for location in candidate_locations] for location in candidate_locations: zone = " - ".join(location.name.split(" - ")[:-1]) - if self.multiworld.catch_em_all[self.player] == "all_pokemon" and self.multiworld.area_1_to_1_mapping[self.player]: + if self.options.catch_em_all == "all_pokemon" and self.options.area_1_to_1_mapping: if not [self.multiworld.get_location(l.name, self.player) for l in encounter_slots_master if (not l.name.startswith(zone)) and self.multiworld.get_location(l.name, self.player).item.name == location.item.name]: continue - if self.multiworld.catch_em_all[self.player] == "first_stage" and self.multiworld.area_1_to_1_mapping[self.player]: + if self.options.catch_em_all == "first_stage" and self.options.area_1_to_1_mapping: if not [self.multiworld.get_location(l.name, self.player) for l in encounter_slots_master if (not l.name.startswith(zone)) and self.multiworld.get_location(l.name, self.player).item.name == location.item.name and l.name @@ -246,10 +246,10 @@ def process_pokemon_locations(self): continue if placed_mons[location.item.name] < 2 and (location.item.name in poke_data.first_stage_pokemon - or self.multiworld.catch_em_all[self.player]): + or self.options.catch_em_all): continue - if self.multiworld.area_1_to_1_mapping[self.player]: + if self.options.area_1_to_1_mapping: place_locations = [place_location for place_location in candidate_locations if place_location.name.startswith(zone) and place_location.item.name == location.item.name] diff --git a/worlds/pokemon_rb/items.py b/worlds/pokemon_rb/items.py index de29f341c6df..fb439c4f80fa 100644 --- a/worlds/pokemon_rb/items.py +++ b/worlds/pokemon_rb/items.py @@ -194,6 +194,8 @@ def __init__(self, item_id, classification, groups): "Fuji Saved": ItemData(None, ItemClassification.progression, []), "Silph Co Liberated": ItemData(None, ItemClassification.progression, []), "Become Champion": ItemData(None, ItemClassification.progression, []), + "Mt Moon Fossils": ItemData(None, ItemClassification.progression, []), + "Cinnabar Lab": ItemData(None, ItemClassification.progression, []), "Trainer Parties": ItemData(None, ItemClassification.filler, []) } diff --git a/worlds/pokemon_rb/level_scaling.py b/worlds/pokemon_rb/level_scaling.py index 79cda394724a..76e00d9847c4 100644 --- a/worlds/pokemon_rb/level_scaling.py +++ b/worlds/pokemon_rb/level_scaling.py @@ -10,9 +10,9 @@ def level_scaling(multiworld): while locations: sphere = set() for world in multiworld.get_game_worlds("Pokemon Red and Blue"): - if (multiworld.level_scaling[world.player] != "by_spheres_and_distance" - and (multiworld.level_scaling[world.player] != "auto" or multiworld.door_shuffle[world.player] - in ("off", "simple"))): + if (world.options.level_scaling != "by_spheres_and_distance" + and (world.options.level_scaling != "auto" + or world.options.door_shuffle in ("off", "simple"))): continue regions = {multiworld.get_region("Menu", world.player)} checked_regions = set() @@ -41,7 +41,8 @@ def reachable(): # reach them earlier. We treat them both as reachable right away for this purpose return True if (location.name == "Route 25 - Item" and state.can_reach("Route 25", "Region", location.player) - and multiworld.blind_trainers[location.player].value < 100): + and multiworld.worlds[location.player].options.blind_trainers.value < 100 + and "Route 25 - Jr. Trainer M" not in multiworld.regions.location_cache[location.player]): # Assume they will take their one chance to get the trainer to walk out of the way to reach # the item behind them return True @@ -95,9 +96,9 @@ def reachable(): if (location.item.game == "Pokemon Red and Blue" and (location.item.name.startswith("Missable ") or location.item.name.startswith("Static ")) and location.name != "Pokemon Tower 6F - Restless Soul"): - # Normally, missable Pokemon (starters, the dojo rewards) are not considered in logic static Pokemon - # are not considered for moves or evolutions, as you could release them and potentially soft lock - # the game. However, for level scaling purposes, we will treat them as not missable or static. + # Normally, missable Pokemon (starters, the dojo rewards) are not considered in logic, and static + # Pokemon are not considered for moves or evolutions, as you could release them and potentially soft + # lock the game. However, for level scaling purposes, we will treat them as not missable or static. # We would not want someone playing a minimal accessibility Dexsanity game to get what would be # technically an "out of logic" Mansion Key from selecting Bulbasaur at the beginning of the game # and end up in the Mansion early and encountering level 67 Pokémon @@ -106,7 +107,7 @@ def reachable(): else: state.collect(location.item, True, location) for world in multiworld.get_game_worlds("Pokemon Red and Blue"): - if multiworld.level_scaling[world.player] == "off": + if world.options.level_scaling == "off": continue level_list_copy = level_list.copy() for sphere in spheres: @@ -136,4 +137,4 @@ def reachable(): else: sphere_objects[object].level = level_list_copy.pop(0) for world in multiworld.get_game_worlds("Pokemon Red and Blue"): - world.finished_level_scaling.set() + world.finished_level_scaling.set() \ No newline at end of file diff --git a/worlds/pokemon_rb/locations.py b/worlds/pokemon_rb/locations.py index 6aee25df2637..5885183baa9c 100644 --- a/worlds/pokemon_rb/locations.py +++ b/worlds/pokemon_rb/locations.py @@ -5,46 +5,48 @@ loc_id_start = 172000000 -def trainersanity(multiworld, player): - return multiworld.trainersanity[player] +def trainersanity(world, player): + include = world.trainersanity_table.pop(0) + world.trainersanity_table.append(include) + return include -def dexsanity(multiworld, player): - include = multiworld.worlds[player].dexsanity_table.pop(0) - multiworld.worlds[player].dexsanity_table.append(include) +def dexsanity(world, player): + include = world.dexsanity_table.pop(0) + world.dexsanity_table.append(include) return include -def hidden_items(multiworld, player): - return multiworld.randomize_hidden_items[player] +def hidden_items(world, player): + return world.options.randomize_hidden_items -def hidden_moon_stones(multiworld, player): - return multiworld.randomize_hidden_items[player] or multiworld.stonesanity[player] +def hidden_moon_stones(world, player): + return world.options.randomize_hidden_items or world.options.stonesanity -def tea(multiworld, player): - return multiworld.tea[player] +def tea(world, player): + return world.options.tea -def extra_key_items(multiworld, player): - return multiworld.extra_key_items[player] +def extra_key_items(world, player): + return world.options.extra_key_items -def always_on(multiworld, player): +def always_on(world, player): return True -def prizesanity(multiworld, player): - return multiworld.prizesanity[player] +def prizesanity(world, player): + return world.options.prizesanity -def split_card_key(multiworld, player): - return multiworld.split_card_key[player].value > 0 +def split_card_key(world, player): + return world.options.split_card_key.value > 0 -def not_stonesanity(multiworld, player): - return not multiworld.stonesanity[player] +def not_stonesanity(world, player): + return not world.options.stonesanity class LocationData: @@ -395,7 +397,7 @@ def __init__(self, flag): LocationData("Silph Co 5F", "Hidden Item Pot Plant", "Elixir", rom_addresses['Hidden_Item_Silph_Co_5F'], Hidden(18), inclusion=hidden_items), LocationData("Silph Co 9F-SW", "Hidden Item Nurse Bed", "Max Potion", rom_addresses['Hidden_Item_Silph_Co_9F'], Hidden(19), inclusion=hidden_items), LocationData("Saffron Copycat's House 2F", "Hidden Item Desk", "Nugget", rom_addresses['Hidden_Item_Copycats_House'], Hidden(20), inclusion=hidden_items), - LocationData("Cerulean Cave 1F-NW", "Hidden Item Center Rocks", "Rare Candy", rom_addresses['Hidden_Item_Cerulean_Cave_1F'], Hidden(21), inclusion=hidden_items), + LocationData("Cerulean Cave 1F-SW", "Hidden Item Center Rocks", "Rare Candy", rom_addresses['Hidden_Item_Cerulean_Cave_1F'], Hidden(21), inclusion=hidden_items), LocationData("Cerulean Cave B1F-E", "Hidden Item Northeast Rocks", "Ultra Ball", rom_addresses['Hidden_Item_Cerulean_Cave_B1F'], Hidden(22), inclusion=hidden_items), LocationData("Power Plant", "Hidden Item Central Dead End", "Max Elixir", rom_addresses['Hidden_Item_Power_Plant_1'], Hidden(23), inclusion=hidden_items), LocationData("Power Plant", "Hidden Item Before Zapdos", "PP Up", rom_addresses['Hidden_Item_Power_Plant_2'], Hidden(24), inclusion=hidden_items), @@ -786,6 +788,8 @@ def __init__(self, flag): LocationData("Celadon Game Corner", "", "Game Corner", event=True), LocationData("Cinnabar Island", "", "Cinnabar Island", event=True), + LocationData("Cinnabar Lab", "", "Cinnabar Lab", event=True), + LocationData("Mt Moon B2F", "Mt Moon Fossils", "Mt Moon Fossils", event=True), LocationData("Celadon Department Store 4F", "Buy Poke Doll", "Buy Poke Doll", event=True), LocationData("Celadon Department Store 4F", "Buy Fire Stone", "Fire Stone", event=True, inclusion=not_stonesanity), LocationData("Celadon Department Store 4F", "Buy Water Stone", "Water Stone", event=True, inclusion=not_stonesanity), diff --git a/worlds/pokemon_rb/logic.py b/worlds/pokemon_rb/logic.py index cbe28e0ddb47..03e3fa3dfad0 100644 --- a/worlds/pokemon_rb/logic.py +++ b/worlds/pokemon_rb/logic.py @@ -1,49 +1,47 @@ from . import poke_data -def can_surf(state, player): - return (((state.has("HM03 Surf", player) and can_learn_hm(state, "Surf", player)) - or state.has("Flippers", player)) and (state.has("Soul Badge", player) or - state.has(state.multiworld.worlds[player].extra_badges.get("Surf"), player) - or state.multiworld.badges_needed_for_hm_moves[player].value == 0)) +def can_surf(state, world, player): + return (((state.has("HM03 Surf", player) and can_learn_hm(state, world, "Surf", player))) and (state.has("Soul Badge", player) or + state.has(world.extra_badges.get("Surf"), player) + or world.options.badges_needed_for_hm_moves.value == 0)) -def can_cut(state, player): - return ((state.has("HM01 Cut", player) and can_learn_hm(state, "Cut", player) or state.has("Master Sword", player)) - and (state.has("Cascade Badge", player) or - state.has(state.multiworld.worlds[player].extra_badges.get("Cut"), player) or - state.multiworld.badges_needed_for_hm_moves[player].value == 0)) +def can_cut(state, world, player): + return ((state.has("HM01 Cut", player) and can_learn_hm(state, world, "Cut", player)) + and (state.has("Cascade Badge", player) or state.has(world.extra_badges.get("Cut"), player) or + world.options.badges_needed_for_hm_moves.value == 0)) -def can_fly(state, player): - return (((state.has("HM02 Fly", player) and can_learn_hm(state, "Fly", player)) or state.has("Flute", player)) and - (state.has("Thunder Badge", player) or state.has(state.multiworld.worlds[player].extra_badges.get("Fly"), player) - or state.multiworld.badges_needed_for_hm_moves[player].value == 0)) +def can_fly(state, world, player): + return (((state.has("HM02 Fly", player) and can_learn_hm(state, world, "Fly", player)) or state.has("Flute", player)) and + (state.has("Thunder Badge", player) or state.has(world.extra_badges.get("Fly"), player) + or world.options.badges_needed_for_hm_moves.value == 0)) -def can_strength(state, player): - return ((state.has("HM04 Strength", player) and can_learn_hm(state, "Strength", player)) or +def can_strength(state, world, player): + return ((state.has("HM04 Strength", player) and can_learn_hm(state, world, "Strength", player)) or state.has("Titan's Mitt", player)) and (state.has("Rainbow Badge", player) or - state.has(state.multiworld.worlds[player].extra_badges.get("Strength"), player) - or state.multiworld.badges_needed_for_hm_moves[player].value == 0) + state.has(world.extra_badges.get("Strength"), player) + or world.options.badges_needed_for_hm_moves.value == 0) -def can_flash(state, player): - return (((state.has("HM05 Flash", player) and can_learn_hm(state, "Flash", player)) or state.has("Lamp", player)) - and (state.has("Boulder Badge", player) or state.has(state.multiworld.worlds[player].extra_badges.get("Flash"), - player) or state.multiworld.badges_needed_for_hm_moves[player].value == 0)) +def can_flash(state, world, player): + return (((state.has("HM05 Flash", player) and can_learn_hm(state, world, "Flash", player)) or state.has("Lamp", player)) + and (state.has("Boulder Badge", player) or state.has(world.extra_badges.get("Flash"), + player) or world.options.badges_needed_for_hm_moves.value == 0)) -def can_learn_hm(state, move, player): - for pokemon, data in state.multiworld.worlds[player].local_poke_data.items(): +def can_learn_hm(state, world, move, player): + for pokemon, data in world.local_poke_data.items(): if state.has(pokemon, player) and data["tms"][6] & 1 << (["Cut", "Fly", "Surf", "Strength", "Flash"].index(move) + 2): return True return False -def can_get_hidden_items(state, player): - return state.has("Item Finder", player) or not state.multiworld.require_item_finder[player].value +def can_get_hidden_items(state, world, player): + return state.has("Item Finder", player) or not world.options.require_item_finder.value def has_key_items(state, count, player): @@ -53,13 +51,14 @@ def has_key_items(state, count, player): "Hideout Key", "Card Key 2F", "Card Key 3F", "Card Key 4F", "Card Key 5F", "Card Key 6F", "Card Key 7F", "Card Key 8F", "Card Key 9F", "Card Key 10F", "Card Key 11F", "Exp. All", "Fire Stone", "Thunder Stone", "Water Stone", - "Leaf Stone", "Moon Stone"] if state.has(item, player)]) + "Leaf Stone", "Moon Stone", "Oak's Parcel", "Helix Fossil", "Dome Fossil", + "Old Amber", "Tea", "Gold Teeth", "Bike Voucher"] if state.has(item, player)]) + min(state.count("Progressive Card Key", player), 10)) return key_items >= count -def can_pass_guards(state, player): - if state.multiworld.tea[player]: +def can_pass_guards(state, world, player): + if world.options.tea: return state.has("Tea", player) else: return state.has("Vending Machine Drinks", player) @@ -70,8 +69,8 @@ def has_badges(state, count, player): "Soul Badge", "Volcano Badge", "Earth Badge"] if state.has(item, player)]) >= count -def oaks_aide(state, count, player): - return ((not state.multiworld.require_pokedex[player] or state.has("Pokedex", player)) +def oaks_aide(state, world, count, player): + return ((not world.options.require_pokedex or state.has("Pokedex", player)) and has_pokemon(state, count, player)) @@ -85,9 +84,7 @@ def has_pokemon(state, count, player): def fossil_checks(state, count, player): - return (state.can_reach('Mt Moon B2F', 'Region', player) and - state.can_reach('Cinnabar Lab Fossil Room', 'Region', player) and - state.can_reach('Cinnabar Island', 'Region', player) and len( + return (state.has_all(["Mt Moon Fossils", "Cinnabar Lab", "Cinnabar Island"], player) and len( [item for item in ["Dome Fossil", "Helix Fossil", "Old Amber"] if state.has(item, player)]) >= count) @@ -96,19 +93,19 @@ def card_key(state, floor, player): state.has("Progressive Card Key", player, floor - 1) -def rock_tunnel(state, player): - return can_flash(state, player) or not state.multiworld.dark_rock_tunnel_logic[player] +def rock_tunnel(state, world, player): + return can_flash(state, world, player) or not world.options.dark_rock_tunnel_logic -def route_3(state, player): - if state.multiworld.route_3_condition[player] == "defeat_brock": +def route(state, world, player): + if world.options.route_3_condition == "defeat_brock": return state.has("Defeat Brock", player) - elif state.multiworld.route_3_condition[player] == "defeat_any_gym": + elif world.options.route_3_condition == "defeat_any_gym": return state.has_any(["Defeat Brock", "Defeat Misty", "Defeat Lt. Surge", "Defeat Erika", "Defeat Koga", "Defeat Blaine", "Defeat Sabrina", "Defeat Viridian Gym Giovanni"], player) - elif state.multiworld.route_3_condition[player] == "boulder_badge": + elif world.options.route_3_condition == "boulder_badge": return state.has("Boulder Badge", player) - elif state.multiworld.route_3_condition[player] == "any_badge": + elif world.options.route_3_condition == "any_badge": return state.has_any(["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Marsh Badge", "Soul Badge", "Volcano Badge", "Earth Badge"], player) # open diff --git a/worlds/pokemon_rb/options.py b/worlds/pokemon_rb/options.py index 9f217e82e646..21679bec00e9 100644 --- a/worlds/pokemon_rb/options.py +++ b/worlds/pokemon_rb/options.py @@ -1,4 +1,6 @@ -from Options import Toggle, Choice, Range, NamedRange, TextChoice, DeathLink, ItemsAccessibility +from dataclasses import dataclass +from Options import (PerGameCommonOptions, Toggle, Choice, Range, NamedRange, FreeText, TextChoice, DeathLink, + ItemsAccessibility) class GameVersion(Choice): @@ -263,12 +265,18 @@ class PrizeSanity(Toggle): default = 0 -class TrainerSanity(Toggle): - """Add a location check to every trainer in the game, which can be obtained by talking to a trainer after defeating - them. Does not affect gym leaders and some scripted event battles (including all Rival, Giovanni, and - Cinnabar Gym battles).""" +class TrainerSanity(NamedRange): + """Add location checks to trainers, which can be obtained by talking to a trainer after defeating them. Does not + affect gym leaders and some scripted event battles. You may specify a number of trainers to have checks, and in + this case they will be randomly selected. There is no in-game indication as to which trainers have checks.""" display_name = "Trainersanity" default = 0 + range_start = 0 + range_end = 317 + special_range_names = { + "disabled": 0, + "full": 317 + } class RequirePokedex(Toggle): @@ -286,19 +294,19 @@ class AllPokemonSeen(Toggle): class DexSanity(NamedRange): - """Adds location checks for Pokemon flagged "owned" on your Pokedex. You may specify a percentage of Pokemon to - have checks added. If Accessibility is set to full, this will be the percentage of all logically reachable - Pokemon that will get a location check added to it. With items or minimal Accessibility, it will be the percentage - of all 151 Pokemon. - If Pokedex is required, the items for Pokemon acquired before acquiring the Pokedex can be found by talking to - Professor Oak or evaluating the Pokedex via Oak's PC.""" + """Adds location checks for Pokemon flagged "owned" on your Pokedex. You may specify the exact number of Dexsanity + checks to add, and they will be distributed to Pokemon randomly. + If Accessibility is set to Full, Dexsanity checks for Pokemon that are not logically reachable will be removed, + so the number could be lower than you specified. + If Pokedex is required, the Dexsanity checks for Pokemon you acquired before acquiring the Pokedex can be found by + talking to Professor Oak or evaluating the Pokedex via Oak's PC.""" display_name = "Dexsanity" default = 0 range_start = 0 - range_end = 100 + range_end = 151 special_range_names = { "disabled": 0, - "full": 100 + "full": 151 } @@ -519,7 +527,8 @@ class TrainerLegendaries(Toggle): class BlindTrainers(Range): """Chance each frame that you are standing on a tile in a trainer's line of sight that they will fail to initiate a - battle. If you move into and out of their line of sight without stopping, this chance will only trigger once.""" + battle. If you move into and out of their line of sight without stopping, this chance will only trigger once. + Trainers which have Trainersanity location checks ignore the Blind Trainers setting.""" display_name = "Blind Trainers" range_start = 0 range_end = 100 @@ -704,6 +713,15 @@ class RandomizeTypeChart(Choice): default = 0 +class TypeChartSeed(FreeText): + """You can enter a number to use as a seed for the type chart. If you enter anything besides a number or "random", + it will be used as a type chart group name, and everyone using the same group name will get the same type chart, + made using the type chart options of one random player within the group. If a group name is used, the type matchup + information will not be made available for trackers.""" + display_name = "Type Chart Seed" + default = "random" + + class NormalMatchups(Range): """If 'randomize' is chosen for Randomize Type Chart, this will be the weight for neutral matchups. No effect if 'chaos' is chosen""" @@ -850,8 +868,8 @@ class BicycleGateSkips(Choice): class RandomizePokemonPalettes(Choice): - """Modify palettes of Pokemon. Primary Type will set Pokemons' palettes based on their primary type, Follow - Evolutions will randomize palettes but palettes will remain the same through evolutions (except Eeveelutions), + """Modify Super Gameboy palettes of Pokemon. Primary Type will set Pokemons' palettes based on their primary type, + Follow Evolutions will randomize palettes but they will remain the same through evolutions (except Eeveelutions), Completely Random will randomize all Pokemons' palettes individually""" display_name = "Randomize Pokemon Palettes" option_vanilla = 0 @@ -860,104 +878,105 @@ class RandomizePokemonPalettes(Choice): option_completely_random = 3 -pokemon_rb_options = { - "accessibility": ItemsAccessibility, - "game_version": GameVersion, - "trainer_name": TrainerName, - "rival_name": RivalName, - #"goal": Goal, - "elite_four_badges_condition": EliteFourBadgesCondition, - "elite_four_key_items_condition": EliteFourKeyItemsCondition, - "elite_four_pokedex_condition": EliteFourPokedexCondition, - "victory_road_condition": VictoryRoadCondition, - "route_22_gate_condition": Route22GateCondition, - "viridian_gym_condition": ViridianGymCondition, - "cerulean_cave_badges_condition": CeruleanCaveBadgesCondition, - "cerulean_cave_key_items_condition": CeruleanCaveKeyItemsCondition, - "route_3_condition": Route3Condition, - "robbed_house_officer": RobbedHouseOfficer, - "second_fossil_check_condition": SecondFossilCheckCondition, - "fossil_check_item_types": FossilCheckItemTypes, - "exp_all": ExpAll, - "old_man": OldMan, - "badgesanity": BadgeSanity, - "badges_needed_for_hm_moves": BadgesNeededForHMMoves, - "key_items_only": KeyItemsOnly, - "tea": Tea, - "extra_key_items": ExtraKeyItems, - "split_card_key": SplitCardKey, - "all_elevators_locked": AllElevatorsLocked, - "extra_strength_boulders": ExtraStrengthBoulders, - "require_item_finder": RequireItemFinder, - "randomize_hidden_items": RandomizeHiddenItems, - "prizesanity": PrizeSanity, - "trainersanity": TrainerSanity, - "dexsanity": DexSanity, - "randomize_pokedex": RandomizePokedex, - "require_pokedex": RequirePokedex, - "all_pokemon_seen": AllPokemonSeen, - "oaks_aide_rt_2": OaksAidRt2, - "oaks_aide_rt_11": OaksAidRt11, - "oaks_aide_rt_15": OaksAidRt15, - "stonesanity": Stonesanity, - "door_shuffle": DoorShuffle, - "warp_tile_shuffle": WarpTileShuffle, - "randomize_rock_tunnel": RandomizeRockTunnel, - "dark_rock_tunnel_logic": DarkRockTunnelLogic, - "free_fly_location": FreeFlyLocation, - "town_map_fly_location": TownMapFlyLocation, - "blind_trainers": BlindTrainers, - "minimum_steps_between_encounters": MinimumStepsBetweenEncounters, - "level_scaling": LevelScaling, - "exp_modifier": ExpModifier, - "randomize_wild_pokemon": RandomizeWildPokemon, - "area_1_to_1_mapping": Area1To1Mapping, - "randomize_starter_pokemon": RandomizeStarterPokemon, - "randomize_static_pokemon": RandomizeStaticPokemon, - "randomize_legendary_pokemon": RandomizeLegendaryPokemon, - "catch_em_all": CatchEmAll, - "randomize_pokemon_stats": RandomizePokemonStats, - "randomize_pokemon_catch_rates": RandomizePokemonCatchRates, - "minimum_catch_rate": MinimumCatchRate, - "randomize_trainer_parties": RandomizeTrainerParties, - "trainer_legendaries": TrainerLegendaries, - "move_balancing": MoveBalancing, - "fix_combat_bugs": FixCombatBugs, - "randomize_pokemon_movesets": RandomizePokemonMovesets, - "confine_transform_to_ditto": ConfineTranstormToDitto, - "start_with_four_moves": StartWithFourMoves, - "same_type_attack_bonus": SameTypeAttackBonus, - "randomize_tm_moves": RandomizeTMMoves, - "tm_same_type_compatibility": TMSameTypeCompatibility, - "tm_normal_type_compatibility": TMNormalTypeCompatibility, - "tm_other_type_compatibility": TMOtherTypeCompatibility, - "hm_same_type_compatibility": HMSameTypeCompatibility, - "hm_normal_type_compatibility": HMNormalTypeCompatibility, - "hm_other_type_compatibility": HMOtherTypeCompatibility, - "inherit_tm_hm_compatibility": InheritTMHMCompatibility, - "randomize_move_types": RandomizeMoveTypes, - "randomize_pokemon_types": RandomizePokemonTypes, - "secondary_type_chance": SecondaryTypeChance, - "randomize_type_chart": RandomizeTypeChart, - "normal_matchups": NormalMatchups, - "super_effective_matchups": SuperEffectiveMatchups, - "not_very_effective_matchups": NotVeryEffectiveMatchups, - "immunity_matchups": ImmunityMatchups, - "safari_zone_normal_battles": SafariZoneNormalBattles, - "normalize_encounter_chances": NormalizeEncounterChances, - "reusable_tms": ReusableTMs, - "better_shops": BetterShops, - "master_ball_price": MasterBallPrice, - "starting_money": StartingMoney, - "lose_money_on_blackout": LoseMoneyOnBlackout, - "poke_doll_skip": PokeDollSkip, - "bicycle_gate_skips": BicycleGateSkips, - "trap_percentage": TrapPercentage, - "poison_trap_weight": PoisonTrapWeight, - "fire_trap_weight": FireTrapWeight, - "paralyze_trap_weight": ParalyzeTrapWeight, - "sleep_trap_weight": SleepTrapWeight, - "ice_trap_weight": IceTrapWeight, - "randomize_pokemon_palettes": RandomizePokemonPalettes, - "death_link": DeathLink -} +@dataclass +class PokemonRBOptions(PerGameCommonOptions): + accessibility: ItemsAccessibility + game_version: GameVersion + trainer_name: TrainerName + rival_name: RivalName + # goal: Goal + elite_four_badges_condition: EliteFourBadgesCondition + elite_four_key_items_condition: EliteFourKeyItemsCondition + elite_four_pokedex_condition: EliteFourPokedexCondition + victory_road_condition: VictoryRoadCondition + route_22_gate_condition: Route22GateCondition + viridian_gym_condition: ViridianGymCondition + cerulean_cave_badges_condition: CeruleanCaveBadgesCondition + cerulean_cave_key_items_condition: CeruleanCaveKeyItemsCondition + route_3_condition: Route3Condition + robbed_house_officer: RobbedHouseOfficer + second_fossil_check_condition: SecondFossilCheckCondition + fossil_check_item_types: FossilCheckItemTypes + exp_all: ExpAll + old_man: OldMan + badgesanity: BadgeSanity + badges_needed_for_hm_moves: BadgesNeededForHMMoves + key_items_only: KeyItemsOnly + tea: Tea + extra_key_items: ExtraKeyItems + split_card_key: SplitCardKey + all_elevators_locked: AllElevatorsLocked + extra_strength_boulders: ExtraStrengthBoulders + require_item_finder: RequireItemFinder + randomize_hidden_items: RandomizeHiddenItems + prizesanity: PrizeSanity + trainersanity: TrainerSanity + dexsanity: DexSanity + randomize_pokedex: RandomizePokedex + require_pokedex: RequirePokedex + all_pokemon_seen: AllPokemonSeen + oaks_aide_rt_2: OaksAidRt2 + oaks_aide_rt_11: OaksAidRt11 + oaks_aide_rt_15: OaksAidRt15 + stonesanity: Stonesanity + door_shuffle: DoorShuffle + warp_tile_shuffle: WarpTileShuffle + randomize_rock_tunnel: RandomizeRockTunnel + dark_rock_tunnel_logic: DarkRockTunnelLogic + free_fly_location: FreeFlyLocation + town_map_fly_location: TownMapFlyLocation + blind_trainers: BlindTrainers + minimum_steps_between_encounters: MinimumStepsBetweenEncounters + level_scaling: LevelScaling + exp_modifier: ExpModifier + randomize_wild_pokemon: RandomizeWildPokemon + area_1_to_1_mapping: Area1To1Mapping + randomize_starter_pokemon: RandomizeStarterPokemon + randomize_static_pokemon: RandomizeStaticPokemon + randomize_legendary_pokemon: RandomizeLegendaryPokemon + catch_em_all: CatchEmAll + randomize_pokemon_stats: RandomizePokemonStats + randomize_pokemon_catch_rates: RandomizePokemonCatchRates + minimum_catch_rate: MinimumCatchRate + randomize_trainer_parties: RandomizeTrainerParties + trainer_legendaries: TrainerLegendaries + move_balancing: MoveBalancing + fix_combat_bugs: FixCombatBugs + randomize_pokemon_movesets: RandomizePokemonMovesets + confine_transform_to_ditto: ConfineTranstormToDitto + start_with_four_moves: StartWithFourMoves + same_type_attack_bonus: SameTypeAttackBonus + randomize_tm_moves: RandomizeTMMoves + tm_same_type_compatibility: TMSameTypeCompatibility + tm_normal_type_compatibility: TMNormalTypeCompatibility + tm_other_type_compatibility: TMOtherTypeCompatibility + hm_same_type_compatibility: HMSameTypeCompatibility + hm_normal_type_compatibility: HMNormalTypeCompatibility + hm_other_type_compatibility: HMOtherTypeCompatibility + inherit_tm_hm_compatibility: InheritTMHMCompatibility + randomize_move_types: RandomizeMoveTypes + randomize_pokemon_types: RandomizePokemonTypes + secondary_type_chance: SecondaryTypeChance + randomize_type_chart: RandomizeTypeChart + normal_matchups: NormalMatchups + super_effective_matchups: SuperEffectiveMatchups + not_very_effective_matchups: NotVeryEffectiveMatchups + immunity_matchups: ImmunityMatchups + type_chart_seed: TypeChartSeed + safari_zone_normal_battles: SafariZoneNormalBattles + normalize_encounter_chances: NormalizeEncounterChances + reusable_tms: ReusableTMs + better_shops: BetterShops + master_ball_price: MasterBallPrice + starting_money: StartingMoney + lose_money_on_blackout: LoseMoneyOnBlackout + poke_doll_skip: PokeDollSkip + bicycle_gate_skips: BicycleGateSkips + trap_percentage: TrapPercentage + poison_trap_weight: PoisonTrapWeight + fire_trap_weight: FireTrapWeight + paralyze_trap_weight: ParalyzeTrapWeight + sleep_trap_weight: SleepTrapWeight + ice_trap_weight: IceTrapWeight + randomize_pokemon_palettes: RandomizePokemonPalettes + death_link: DeathLink diff --git a/worlds/pokemon_rb/pokemon.py b/worlds/pokemon_rb/pokemon.py index 28098a2c53fe..32c0e36869da 100644 --- a/worlds/pokemon_rb/pokemon.py +++ b/worlds/pokemon_rb/pokemon.py @@ -3,8 +3,8 @@ from .rom_addresses import rom_addresses -def set_mon_palettes(self, random, data): - if self.multiworld.randomize_pokemon_palettes[self.player] == "vanilla": +def set_mon_palettes(world, random, data): + if world.options.randomize_pokemon_palettes == "vanilla": return pallet_map = { "Poison": 0x0F, @@ -25,9 +25,9 @@ def set_mon_palettes(self, random, data): } palettes = [] for mon in poke_data.pokemon_data: - if self.multiworld.randomize_pokemon_palettes[self.player] == "primary_type": - pallet = pallet_map[self.local_poke_data[mon]["type1"]] - elif (self.multiworld.randomize_pokemon_palettes[self.player] == "follow_evolutions" and mon in + if world.options.randomize_pokemon_palettes == "primary_type": + pallet = pallet_map[world.local_poke_data[mon]["type1"]] + elif (world.options.randomize_pokemon_palettes == "follow_evolutions" and mon in poke_data.evolves_from and poke_data.evolves_from[mon] != "Eevee"): pallet = palettes[-1] else: # completely_random or follow_evolutions and it is not an evolved form (except eeveelutions) @@ -93,40 +93,41 @@ def move_power(move_data): return power -def process_move_data(self): - self.local_move_data = deepcopy(poke_data.moves) +def process_move_data(world): + world.local_move_data = deepcopy(poke_data.moves) - if self.multiworld.randomize_move_types[self.player]: - for move, data in self.local_move_data.items(): + if world.options.randomize_move_types: + for move, data in world.local_move_data.items(): if move == "No Move": continue # The chance of randomized moves choosing a normal type move is high, so we want to retain having a higher # rate of normal type moves - data["type"] = self.multiworld.random.choice(list(poke_data.type_ids) + (["Normal"] * 4)) - - if self.multiworld.move_balancing[self.player]: - self.local_move_data["Sing"]["accuracy"] = 30 - self.local_move_data["Sleep Powder"]["accuracy"] = 40 - self.local_move_data["Spore"]["accuracy"] = 50 - self.local_move_data["Sonicboom"]["effect"] = 0 - self.local_move_data["Sonicboom"]["power"] = 50 - self.local_move_data["Dragon Rage"]["effect"] = 0 - self.local_move_data["Dragon Rage"]["power"] = 80 - self.local_move_data["Horn Drill"]["effect"] = 0 - self.local_move_data["Horn Drill"]["power"] = 70 - self.local_move_data["Horn Drill"]["accuracy"] = 90 - self.local_move_data["Guillotine"]["effect"] = 0 - self.local_move_data["Guillotine"]["power"] = 70 - self.local_move_data["Guillotine"]["accuracy"] = 90 - self.local_move_data["Fissure"]["effect"] = 0 - self.local_move_data["Fissure"]["power"] = 70 - self.local_move_data["Fissure"]["accuracy"] = 90 - self.local_move_data["Blizzard"]["accuracy"] = 70 - if self.multiworld.randomize_tm_moves[self.player]: - self.local_tms = self.multiworld.random.sample([move for move in poke_data.moves.keys() if move not in - ["No Move"] + poke_data.hm_moves], 50) + data["type"] = world.random.choice(list(poke_data.type_ids) + (["Normal"] * 4)) + + if world.options.move_balancing: + world.local_move_data["Sing"]["accuracy"] = 30 + world.local_move_data["Sleep Powder"]["accuracy"] = 40 + world.local_move_data["Spore"]["accuracy"] = 50 + world.local_move_data["Sonicboom"]["effect"] = 0 + world.local_move_data["Sonicboom"]["power"] = 50 + world.local_move_data["Dragon Rage"]["effect"] = 0 + world.local_move_data["Dragon Rage"]["power"] = 80 + world.local_move_data["Horn Drill"]["effect"] = 0 + world.local_move_data["Horn Drill"]["power"] = 70 + world.local_move_data["Horn Drill"]["accuracy"] = 90 + world.local_move_data["Guillotine"]["effect"] = 0 + world.local_move_data["Guillotine"]["power"] = 70 + world.local_move_data["Guillotine"]["accuracy"] = 90 + world.local_move_data["Fissure"]["effect"] = 0 + world.local_move_data["Fissure"]["power"] = 70 + world.local_move_data["Fissure"]["accuracy"] = 90 + world.local_move_data["Blizzard"]["accuracy"] = 70 + + if world.options.randomize_tm_moves: + world.local_tms = world.random.sample([move for move in poke_data.moves.keys() if move not in + ["No Move"] + poke_data.hm_moves], 50) else: - self.local_tms = poke_data.tm_moves.copy() + world.local_tms = poke_data.tm_moves.copy() def process_pokemon_data(self): @@ -138,12 +139,12 @@ def process_pokemon_data(self): compat_hms = set() for mon, mon_data in local_poke_data.items(): - if self.multiworld.randomize_pokemon_stats[self.player] == "shuffle": + if self.options.randomize_pokemon_stats == "shuffle": stats = [mon_data["hp"], mon_data["atk"], mon_data["def"], mon_data["spd"], mon_data["spc"]] if mon in poke_data.evolves_from: stat_shuffle_map = local_poke_data[poke_data.evolves_from[mon]]["stat_shuffle_map"] else: - stat_shuffle_map = self.multiworld.random.sample(range(0, 5), 5) + stat_shuffle_map = self.random.sample(range(0, 5), 5) mon_data["stat_shuffle_map"] = stat_shuffle_map mon_data["hp"] = stats[stat_shuffle_map[0]] @@ -151,7 +152,7 @@ def process_pokemon_data(self): mon_data["def"] = stats[stat_shuffle_map[2]] mon_data["spd"] = stats[stat_shuffle_map[3]] mon_data["spc"] = stats[stat_shuffle_map[4]] - elif self.multiworld.randomize_pokemon_stats[self.player] == "randomize": + elif self.options.randomize_pokemon_stats == "randomize": first_run = True while (mon_data["hp"] > 255 or mon_data["atk"] > 255 or mon_data["def"] > 255 or mon_data["spd"] > 255 or mon_data["spc"] > 255 or first_run): @@ -168,9 +169,9 @@ def process_pokemon_data(self): mon_data[stat] = 10 total_stats -= 10 assert total_stats >= 0, f"Error distributing stats for {mon} for player {self.player}" - dist = [self.multiworld.random.randint(1, 101) / 100, self.multiworld.random.randint(1, 101) / 100, - self.multiworld.random.randint(1, 101) / 100, self.multiworld.random.randint(1, 101) / 100, - self.multiworld.random.randint(1, 101) / 100] + dist = [self.random.randint(1, 101) / 100, self.random.randint(1, 101) / 100, + self.random.randint(1, 101) / 100, self.random.randint(1, 101) / 100, + self.random.randint(1, 101) / 100] total_dist = sum(dist) mon_data["hp"] += int(round(dist[0] / total_dist * total_stats)) @@ -178,30 +179,30 @@ def process_pokemon_data(self): mon_data["def"] += int(round(dist[2] / total_dist * total_stats)) mon_data["spd"] += int(round(dist[3] / total_dist * total_stats)) mon_data["spc"] += int(round(dist[4] / total_dist * total_stats)) - if self.multiworld.randomize_pokemon_types[self.player]: - if self.multiworld.randomize_pokemon_types[self.player].value == 1 and mon in poke_data.evolves_from: + if self.options.randomize_pokemon_types: + if self.options.randomize_pokemon_types.value == 1 and mon in poke_data.evolves_from: type1 = local_poke_data[poke_data.evolves_from[mon]]["type1"] type2 = local_poke_data[poke_data.evolves_from[mon]]["type2"] if type1 == type2: - if self.multiworld.secondary_type_chance[self.player].value == -1: + if self.options.secondary_type_chance.value == -1: if mon_data["type1"] != mon_data["type2"]: while type2 == type1: - type2 = self.multiworld.random.choice(list(poke_data.type_names.values())) - elif self.multiworld.random.randint(1, 100) <= self.multiworld.secondary_type_chance[self.player].value: - type2 = self.multiworld.random.choice(list(poke_data.type_names.values())) + type2 = self.random.choice(list(poke_data.type_names.values())) + elif self.random.randint(1, 100) <= self.options.secondary_type_chance.value: + type2 = self.random.choice(list(poke_data.type_names.values())) else: - type1 = self.multiworld.random.choice(list(poke_data.type_names.values())) + type1 = self.random.choice(list(poke_data.type_names.values())) type2 = type1 - if ((self.multiworld.secondary_type_chance[self.player].value == -1 and mon_data["type1"] - != mon_data["type2"]) or self.multiworld.random.randint(1, 100) - <= self.multiworld.secondary_type_chance[self.player].value): + if ((self.options.secondary_type_chance.value == -1 and mon_data["type1"] + != mon_data["type2"]) or self.random.randint(1, 100) + <= self.options.secondary_type_chance.value): while type2 == type1: - type2 = self.multiworld.random.choice(list(poke_data.type_names.values())) + type2 = self.random.choice(list(poke_data.type_names.values())) mon_data["type1"] = type1 mon_data["type2"] = type2 - if self.multiworld.randomize_pokemon_movesets[self.player]: - if self.multiworld.randomize_pokemon_movesets[self.player] == "prefer_types": + if self.options.randomize_pokemon_movesets: + if self.options.randomize_pokemon_movesets == "prefer_types": if mon_data["type1"] == "Normal" and mon_data["type2"] == "Normal": chances = [[75, "Normal"]] elif mon_data["type1"] == "Normal" or mon_data["type2"] == "Normal": @@ -219,9 +220,9 @@ def process_pokemon_data(self): moves = list(poke_data.moves.keys()) for move in ["No Move"] + poke_data.hm_moves: moves.remove(move) - if self.multiworld.confine_transform_to_ditto[self.player]: + if self.options.confine_transform_to_ditto: moves.remove("Transform") - if self.multiworld.start_with_four_moves[self.player]: + if self.options.start_with_four_moves: num_moves = 4 else: num_moves = len([i for i in [mon_data["start move 1"], mon_data["start move 2"], @@ -231,12 +232,12 @@ def process_pokemon_data(self): non_power_moves = [] learnsets[mon] = [] for i in range(num_moves): - if i == 0 and mon == "Ditto" and self.multiworld.confine_transform_to_ditto[self.player]: + if i == 0 and mon == "Ditto" and self.options.confine_transform_to_ditto: move = "Transform" else: - move = get_move(self.local_move_data, moves, chances, self.multiworld.random) - while move == "Transform" and self.multiworld.confine_transform_to_ditto[self.player]: - move = get_move(self.local_move_data, moves, chances, self.multiworld.random) + move = get_move(self.local_move_data, moves, chances, self.random) + while move == "Transform" and self.options.confine_transform_to_ditto: + move = get_move(self.local_move_data, moves, chances, self.random) if self.local_move_data[move]["power"] < 5: non_power_moves.append(move) else: @@ -244,59 +245,58 @@ def process_pokemon_data(self): learnsets[mon].sort(key=lambda move: move_power(self.local_move_data[move])) if learnsets[mon]: for move in non_power_moves: - learnsets[mon].insert(self.multiworld.random.randint(1, len(learnsets[mon])), move) + learnsets[mon].insert(self.random.randint(1, len(learnsets[mon])), move) else: learnsets[mon] = non_power_moves for i in range(1, 5): - if mon_data[f"start move {i}"] != "No Move" or self.multiworld.start_with_four_moves[self.player]: + if mon_data[f"start move {i}"] != "No Move" or self.options.start_with_four_moves: mon_data[f"start move {i}"] = learnsets[mon].pop(0) - if self.multiworld.randomize_pokemon_catch_rates[self.player]: - mon_data["catch rate"] = self.multiworld.random.randint(self.multiworld.minimum_catch_rate[self.player], - 255) + if self.options.randomize_pokemon_catch_rates: + mon_data["catch rate"] = self.random.randint(self.options.minimum_catch_rate, 255) else: - mon_data["catch rate"] = max(self.multiworld.minimum_catch_rate[self.player], mon_data["catch rate"]) + mon_data["catch rate"] = max(self.options.minimum_catch_rate, mon_data["catch rate"]) def roll_tm_compat(roll_move): if self.local_move_data[roll_move]["type"] in [mon_data["type1"], mon_data["type2"]]: if roll_move in poke_data.hm_moves: - if self.multiworld.hm_same_type_compatibility[self.player].value == -1: + if self.options.hm_same_type_compatibility.value == -1: return mon_data["tms"][int(flag / 8)] & 1 << (flag % 8) - r = self.multiworld.random.randint(1, 100) <= self.multiworld.hm_same_type_compatibility[self.player].value + r = self.random.randint(1, 100) <= self.options.hm_same_type_compatibility.value if r and mon not in poke_data.legendary_pokemon: compat_hms.add(roll_move) return r else: - if self.multiworld.tm_same_type_compatibility[self.player].value == -1: + if self.options.tm_same_type_compatibility.value == -1: return mon_data["tms"][int(flag / 8)] & 1 << (flag % 8) - return self.multiworld.random.randint(1, 100) <= self.multiworld.tm_same_type_compatibility[self.player].value + return self.random.randint(1, 100) <= self.options.tm_same_type_compatibility.value elif self.local_move_data[roll_move]["type"] == "Normal" and "Normal" not in [mon_data["type1"], mon_data["type2"]]: if roll_move in poke_data.hm_moves: - if self.multiworld.hm_normal_type_compatibility[self.player].value == -1: + if self.options.hm_normal_type_compatibility.value == -1: return mon_data["tms"][int(flag / 8)] & 1 << (flag % 8) - r = self.multiworld.random.randint(1, 100) <= self.multiworld.hm_normal_type_compatibility[self.player].value + r = self.random.randint(1, 100) <= self.options.hm_normal_type_compatibility.value if r and mon not in poke_data.legendary_pokemon: compat_hms.add(roll_move) return r else: - if self.multiworld.tm_normal_type_compatibility[self.player].value == -1: + if self.options.tm_normal_type_compatibility.value == -1: return mon_data["tms"][int(flag / 8)] & 1 << (flag % 8) - return self.multiworld.random.randint(1, 100) <= self.multiworld.tm_normal_type_compatibility[self.player].value + return self.random.randint(1, 100) <= self.options.tm_normal_type_compatibility.value else: if roll_move in poke_data.hm_moves: - if self.multiworld.hm_other_type_compatibility[self.player].value == -1: + if self.options.hm_other_type_compatibility.value == -1: return mon_data["tms"][int(flag / 8)] & 1 << (flag % 8) - r = self.multiworld.random.randint(1, 100) <= self.multiworld.hm_other_type_compatibility[self.player].value + r = self.random.randint(1, 100) <= self.options.hm_other_type_compatibility.value if r and mon not in poke_data.legendary_pokemon: compat_hms.add(roll_move) return r else: - if self.multiworld.tm_other_type_compatibility[self.player].value == -1: + if self.options.tm_other_type_compatibility.value == -1: return mon_data["tms"][int(flag / 8)] & 1 << (flag % 8) - return self.multiworld.random.randint(1, 100) <= self.multiworld.tm_other_type_compatibility[self.player].value + return self.random.randint(1, 100) <= self.options.tm_other_type_compatibility.value for flag, tm_move in enumerate(tms_hms): - if mon in poke_data.evolves_from and self.multiworld.inherit_tm_hm_compatibility[self.player]: + if mon in poke_data.evolves_from and self.options.inherit_tm_hm_compatibility: if local_poke_data[poke_data.evolves_from[mon]]["tms"][int(flag / 8)] & 1 << (flag % 8): # always inherit learnable tms/hms @@ -310,7 +310,7 @@ def roll_tm_compat(roll_move): # so this gets full chance roll bit = roll_tm_compat(tm_move) # otherwise 50% reduced chance to add compatibility over pre-evolved form - elif self.multiworld.random.randint(1, 100) > 50 and roll_tm_compat(tm_move): + elif self.random.randint(1, 100) > 50 and roll_tm_compat(tm_move): bit = 1 else: bit = 0 @@ -322,15 +322,13 @@ def roll_tm_compat(roll_move): mon_data["tms"][int(flag / 8)] &= ~(1 << (flag % 8)) hm_verify = ["Surf", "Strength"] - if self.multiworld.accessibility[self.player] != "minimal" or ((not - self.multiworld.badgesanity[self.player]) and max(self.multiworld.elite_four_badges_condition[self.player], - self.multiworld.route_22_gate_condition[self.player], self.multiworld.victory_road_condition[self.player]) - > 7) or (self.multiworld.door_shuffle[self.player] not in ("off", "simple")): + if self.options.accessibility != "minimal" or ((not + self.options.badgesanity) and max(self.options.elite_four_badges_condition, + self.options.route_22_gate_condition, self.options.victory_road_condition) + > 7) or (self.options.door_shuffle not in ("off", "simple")): hm_verify += ["Cut"] - if self.multiworld.accessibility[self.player] != "minimal" or (not - self.multiworld.dark_rock_tunnel_logic[self.player]) and ((self.multiworld.trainersanity[self.player] or - self.multiworld.extra_key_items[self.player]) - or self.multiworld.door_shuffle[self.player]): + if (self.options.accessibility != "minimal" or (not self.options.dark_rock_tunnel_logic) and + ((self.options.trainersanity or self.options.extra_key_items) or self.options.door_shuffle)): hm_verify += ["Flash"] # Fly does not need to be verified. Full/Insanity/Decoupled door shuffle connects reachable regions to unreachable # regions, so if Fly is available and can be learned, the towns you can fly to would be considered reachable for @@ -339,8 +337,7 @@ def roll_tm_compat(roll_move): for hm_move in hm_verify: if hm_move not in compat_hms: - mon = self.multiworld.random.choice([mon for mon in poke_data.pokemon_data if mon not in - poke_data.legendary_pokemon]) + mon = self.random.choice([mon for mon in poke_data.pokemon_data if mon not in poke_data.legendary_pokemon]) flag = tms_hms.index(hm_move) local_poke_data[mon]["tms"][int(flag / 8)] |= 1 << (flag % 8) @@ -352,7 +349,7 @@ def verify_hm_moves(multiworld, world, player): def intervene(move, test_state): move_bit = pow(2, poke_data.hm_moves.index(move) + 2) viable_mons = [mon for mon in world.local_poke_data if world.local_poke_data[mon]["tms"][6] & move_bit] - if multiworld.randomize_wild_pokemon[player] and viable_mons: + if world.options.randomize_wild_pokemon and viable_mons: accessible_slots = [loc for loc in multiworld.get_reachable_locations(test_state, player) if loc.type == "Wild Encounter"] @@ -364,7 +361,7 @@ def number_of_zones(mon): placed_mons = [slot.item.name for slot in accessible_slots] - if multiworld.area_1_to_1_mapping[player]: + if world.options.area_1_to_1_mapping: placed_mons.sort(key=lambda i: number_of_zones(i)) else: # this sort method doesn't work if you reference the same list being sorted in the lambda @@ -372,10 +369,10 @@ def number_of_zones(mon): placed_mons.sort(key=lambda i: placed_mons_copy.count(i)) placed_mon = placed_mons.pop() - replace_mon = multiworld.random.choice(viable_mons) - replace_slot = multiworld.random.choice([slot for slot in accessible_slots if slot.item.name + replace_mon = world.random.choice(viable_mons) + replace_slot = world.random.choice([slot for slot in accessible_slots if slot.item.name == placed_mon]) - if multiworld.area_1_to_1_mapping[player]: + if world.options.area_1_to_1_mapping: zone = " - ".join(replace_slot.name.split(" - ")[:-1]) replace_slots = [slot for slot in accessible_slots if slot.name.startswith(zone) and slot.item.name == placed_mon] @@ -387,7 +384,7 @@ def number_of_zones(mon): tms_hms = world.local_tms + poke_data.hm_moves flag = tms_hms.index(move) mon_list = [mon for mon in poke_data.pokemon_data.keys() if test_state.has(mon, player)] - multiworld.random.shuffle(mon_list) + world.random.shuffle(mon_list) mon_list.sort(key=lambda mon: world.local_move_data[move]["type"] not in [world.local_poke_data[mon]["type1"], world.local_poke_data[mon]["type2"]]) for mon in mon_list: @@ -399,31 +396,31 @@ def number_of_zones(mon): while True: intervene_move = None test_state = multiworld.get_all_state(False) - if not logic.can_learn_hm(test_state, "Surf", player): + if not logic.can_learn_hm(test_state, world, "Surf", player): intervene_move = "Surf" - elif not logic.can_learn_hm(test_state, "Strength", player): + elif not logic.can_learn_hm(test_state, world, "Strength", player): intervene_move = "Strength" # cut may not be needed if accessibility is minimal, unless you need all 8 badges and badgesanity is off, # as you will require cut to access celadon gyn - elif ((not logic.can_learn_hm(test_state, "Cut", player)) and - (multiworld.accessibility[player] != "minimal" or ((not - multiworld.badgesanity[player]) and max( - multiworld.elite_four_badges_condition[player], - multiworld.route_22_gate_condition[player], - multiworld.victory_road_condition[player]) - > 7) or (multiworld.door_shuffle[player] not in ("off", "simple")))): + elif ((not logic.can_learn_hm(test_state, world, "Cut", player)) and + (world.options.accessibility != "minimal" or ((not + world.options.badgesanity) and max( + world.options.elite_four_badges_condition, + world.options.route_22_gate_condition, + world.options.victory_road_condition) + > 7) or (world.options.door_shuffle not in ("off", "simple")))): intervene_move = "Cut" - elif ((not logic.can_learn_hm(test_state, "Flash", player)) - and multiworld.dark_rock_tunnel_logic[player] - and (multiworld.accessibility[player] != "minimal" - or multiworld.door_shuffle[player])): + elif ((not logic.can_learn_hm(test_state, world, "Flash", player)) + and world.options.dark_rock_tunnel_logic + and (world.options.accessibility != "minimal" + or world.options.door_shuffle)): intervene_move = "Flash" # If no Pokémon can learn Fly, then during door shuffle it would simply not treat the free fly maps # as reachable, and if on no door shuffle or simple, fly is simply never necessary. # We only intervene if a Pokémon is able to learn fly but none are reachable, as that would have been # considered in door shuffle. - elif ((not logic.can_learn_hm(test_state, "Fly", player)) - and multiworld.door_shuffle[player] not in + elif ((not logic.can_learn_hm(test_state, world, "Fly", player)) + and world.options.door_shuffle not in ("off", "simple") and [world.fly_map, world.town_map_fly_map] != ["Pallet Town", "Pallet Town"]): intervene_move = "Fly" if intervene_move: @@ -432,4 +429,4 @@ def number_of_zones(mon): intervene(intervene_move, test_state) last_intervene = intervene_move else: - break \ No newline at end of file + break diff --git a/worlds/pokemon_rb/regions.py b/worlds/pokemon_rb/regions.py index 938c39b32090..575f4a61ca6f 100644 --- a/worlds/pokemon_rb/regions.py +++ b/worlds/pokemon_rb/regions.py @@ -1409,21 +1409,20 @@ def pair(a, b): ['Route 2-E to Route 2 Gate', 'Route 2-SE to Route 2 Gate'], ['Cerulean City-Badge House Backyard to Cerulean Badge House', 'Cerulean City to Cerulean Badge House'], - ['Cerulean City-T to Cerulean Trashed House', - 'Cerulean City-Outskirts to Cerulean Trashed House'], - ['Fuchsia City to Fuchsia Good Rod House', - 'Fuchsia City-Good Rod House Backyard to Fuchsia Good Rod House'], - ['Route 11-E to Route 11 Gate 1F', 'Route 11-C to Route 11 Gate 1F'], - ['Route 12-N to Route 12 Gate 1F', 'Route 12-L to Route 12 Gate 1F'], - ['Route 15 to Route 15 Gate 1F', 'Route 15-W to Route 15 Gate 1F'], - ['Route 16-NE to Route 16 Gate 1F-N', 'Route 16-NW to Route 16 Gate 1F-N'], + ['Cerulean City-Outskirts to Cerulean Trashed House', + 'Cerulean City-T to Cerulean Trashed House',], + ['Fuchsia City-Good Rod House Backyard to Fuchsia Good Rod House', 'Fuchsia City to Fuchsia Good Rod House'], + ['Route 11-C to Route 11 Gate 1F', 'Route 11-E to Route 11 Gate 1F'], + ['Route 12-L to Route 12 Gate 1F', 'Route 12-N to Route 12 Gate 1F'], + ['Route 15-W to Route 15 Gate 1F', 'Route 15 to Route 15 Gate 1F'], + ['Route 16-NW to Route 16 Gate 1F-N', 'Route 16-NE to Route 16 Gate 1F-N'], ['Route 16-SW to Route 16 Gate 1F-W', 'Route 16-C to Route 16 Gate 1F-E'], ['Route 18-W to Route 18 Gate 1F-W', 'Route 18-E to Route 18 Gate 1F-E'], ['Route 5 to Route 5 Gate-N', 'Route 5-S to Route 5 Gate-S'], - ['Route 6 to Route 6 Gate-S', 'Route 6-N to Route 6 Gate-N'], + ['Route 6-N to Route 6 Gate-N', 'Route 6 to Route 6 Gate-S'], ['Route 7 to Route 7 Gate-W', 'Route 7-E to Route 7 Gate-E'], - ['Route 8 to Route 8 Gate-E', 'Route 8-W to Route 8 Gate-W'], - ['Route 22 to Route 22 Gate-S', 'Route 23-S to Route 22 Gate-N'] + ['Route 8-W to Route 8 Gate-W', 'Route 8 to Route 8 Gate-E',], + ['Route 23-S to Route 22 Gate-N', 'Route 22 to Route 22 Gate-S'] ] dungeons = [ @@ -1484,7 +1483,7 @@ def create_region(multiworld: MultiWorld, player: int, name: str, locations_per_ for location in locations_per_region.get(name, []): location.parent_region = ret ret.locations.append(location) - if multiworld.randomize_hidden_items[player] == "exclude" and "Hidden" in location.name: + if multiworld.worlds[player].options.randomize_hidden_items == "exclude" and "Hidden" in location.name: location.progress_type = LocationProgressType.EXCLUDED if exits: for exit in exits: @@ -1500,32 +1499,34 @@ def outdoor_map(name): return False -def create_regions(self): - multiworld = self.multiworld - player = self.player +def create_regions(world): + multiworld = world.multiworld + player = world.player locations_per_region = {} - start_inventory = self.multiworld.start_inventory[self.player].value.copy() - if self.multiworld.randomize_pokedex[self.player] == "start_with": + start_inventory = world.options.start_inventory.value.copy() + if world.options.randomize_pokedex == "start_with": start_inventory["Pokedex"] = 1 - self.multiworld.push_precollected(self.create_item("Pokedex")) - if self.multiworld.exp_all[self.player] == "start_with": + world.multiworld.push_precollected(world.create_item("Pokedex")) + if world.options.exp_all == "start_with": start_inventory["Exp. All"] = 1 - self.multiworld.push_precollected(self.create_item("Exp. All")) + world.multiworld.push_precollected(world.create_item("Exp. All")) + + world.item_pool = [] + combined_traps = (world.options.poison_trap_weight.value + + world.options.fire_trap_weight.value + + world.options.paralyze_trap_weight.value + + world.options.ice_trap_weight.value + + world.options.sleep_trap_weight.value) - self.item_pool = [] - combined_traps = (self.multiworld.poison_trap_weight[self.player].value - + self.multiworld.fire_trap_weight[self.player].value - + self.multiworld.paralyze_trap_weight[self.player].value - + self.multiworld.ice_trap_weight[self.player].value) stones = ["Moon Stone", "Fire Stone", "Water Stone", "Thunder Stone", "Leaf Stone"] for location in location_data: locations_per_region.setdefault(location.region, []) # The check for list is so that we don't try to check the item table with a list as a key - if location.inclusion(multiworld, player) and (isinstance(location.original_item, list) or - not (self.multiworld.key_items_only[self.player] and item_table[location.original_item].classification - not in (ItemClassification.progression, ItemClassification.progression_skip_balancing) and not + if location.inclusion(world, player) and (isinstance(location.original_item, list) or + not (world.options.key_items_only and item_table[location.original_item].classification + not in (ItemClassification.progression, ItemClassification.progression_skip_balancing) and not location.event)): location_object = PokemonRBLocation(player, location.name, location.address, location.rom_address, location.type, location.level, location.level_address) @@ -1535,51 +1536,53 @@ def create_regions(self): event = location.event if location.original_item is None: - item = self.create_filler() - elif location.original_item == "Exp. All" and self.multiworld.exp_all[self.player] == "remove": - item = self.create_filler() + item = world.create_filler() + elif location.original_item == "Exp. All" and world.options.exp_all == "remove": + item = world.create_filler() elif location.original_item == "Pokedex": - if self.multiworld.randomize_pokedex[self.player] == "vanilla": + if world.options.randomize_pokedex == "vanilla": + location_object.event = True event = True - item = self.create_item("Pokedex") - elif location.original_item == "Moon Stone" and self.multiworld.stonesanity[self.player]: + item = world.create_item("Pokedex") + elif location.original_item == "Moon Stone" and world.options.stonesanity: stone = stones.pop() - item = self.create_item(stone) + item = world.create_item(stone) elif location.original_item.startswith("TM"): - if self.multiworld.randomize_tm_moves[self.player]: - item = self.create_item(location.original_item.split(" ")[0]) + if world.options.randomize_tm_moves: + item = world.create_item(location.original_item.split(" ")[0]) else: - item = self.create_item(location.original_item) - elif location.original_item == "Card Key" and self.multiworld.split_card_key[self.player] == "on": - item = self.create_item("Card Key 3F") - elif "Card Key" in location.original_item and self.multiworld.split_card_key[self.player] == "progressive": - item = self.create_item("Progressive Card Key") + item = world.create_item(location.original_item) + elif location.original_item == "Card Key" and world.options.split_card_key == "on": + item = world.create_item("Card Key 3F") + elif "Card Key" in location.original_item and world.options.split_card_key == "progressive": + item = world.create_item("Progressive Card Key") else: - item = self.create_item(location.original_item) - if (item.classification == ItemClassification.filler and self.multiworld.random.randint(1, 100) - <= self.multiworld.trap_percentage[self.player].value and combined_traps != 0): - item = self.create_item(self.select_trap()) + item = world.create_item(location.original_item) + if (item.classification == ItemClassification.filler and world.random.randint(1, 100) + <= world.options.trap_percentage.value and combined_traps != 0): + item = world.create_item(world.select_trap()) - if self.multiworld.key_items_only[self.player] and (not location.event) and (not item.advancement) and location.original_item != "Exp. All": + if (world.options.key_items_only and (location.original_item != "Exp. All") + and not (location.event or item.advancement)): continue if item.name in start_inventory and start_inventory[item.name] > 0 and \ location.original_item in item_groups["Unique"]: start_inventory[location.original_item] -= 1 - item = self.create_filler() + item = world.create_filler() if event: location_object.place_locked_item(item) if location.type == "Trainer Parties": location_object.party_data = deepcopy(location.party_data) else: - self.item_pool.append(item) + world.item_pool.append(item) - self.multiworld.random.shuffle(self.item_pool) - advancement_items = [item.name for item in self.item_pool if item.advancement] \ - + [item.name for item in self.multiworld.precollected_items[self.player] if + world.random.shuffle(world.item_pool) + advancement_items = [item.name for item in world.item_pool if item.advancement] \ + + [item.name for item in world.multiworld.precollected_items[world.player] if item.advancement] - self.total_key_items = len( + world.total_key_items = len( # The stonesanity items are not checked for here and instead just always added as the `+ 4` # They will always exist, but if stonesanity is off, then only as events. # We don't want to just add 4 if stonesanity is off while still putting them in this list in case @@ -1589,15 +1592,16 @@ def create_regions(self): "Secret Key", "Poke Flute", "Mansion Key", "Safari Pass", "Plant Key", "Hideout Key", "Card Key 2F", "Card Key 3F", "Card Key 4F", "Card Key 5F", "Card Key 6F", "Card Key 7F", "Card Key 8F", "Card Key 9F", "Card Key 10F", - "Card Key 11F", "Exp. All", "Moon Stone"] if item in advancement_items]) + 4 + "Card Key 11F", "Exp. All", "Moon Stone", "Oak's Parcel", "Helix Fossil", "Dome Fossil", + "Old Amber", "Tea", "Gold Teeth", "Bike Voucher"] if item in advancement_items]) + 4 if "Progressive Card Key" in advancement_items: - self.total_key_items += 10 + world.total_key_items += 10 - self.multiworld.cerulean_cave_key_items_condition[self.player].total = \ - int((self.total_key_items / 100) * self.multiworld.cerulean_cave_key_items_condition[self.player].value) + world.options.cerulean_cave_key_items_condition.total = \ + int((world.total_key_items / 100) * world.options.cerulean_cave_key_items_condition.value) - self.multiworld.elite_four_key_items_condition[self.player].total = \ - int((self.total_key_items / 100) * self.multiworld.elite_four_key_items_condition[self.player].value) + world.options.elite_four_key_items_condition.total = \ + int((world.total_key_items / 100) * world.options.elite_four_key_items_condition.value) regions = [create_region(multiworld, player, region, locations_per_region) for region in warp_data] multiworld.regions += regions @@ -1609,7 +1613,7 @@ def create_regions(self): connect(multiworld, player, "Menu", "Pokedex", one_way=True) connect(multiworld, player, "Menu", "Evolution", one_way=True) connect(multiworld, player, "Menu", "Fossil", lambda state: logic.fossil_checks(state, - state.multiworld.second_fossil_check_condition[player].value, player), one_way=True) + world.options.second_fossil_check_condition.value, player), one_way=True) connect(multiworld, player, "Pallet Town", "Route 1") connect(multiworld, player, "Route 1", "Viridian City") connect(multiworld, player, "Viridian City", "Route 22") @@ -1617,24 +1621,24 @@ def create_regions(self): connect(multiworld, player, "Route 2-SW", "Route 2-Grass", one_way=True) connect(multiworld, player, "Route 2-NW", "Route 2-Grass", one_way=True) connect(multiworld, player, "Route 22 Gate-S", "Route 22 Gate-N", - lambda state: logic.has_badges(state, state.multiworld.route_22_gate_condition[player].value, player)) - connect(multiworld, player, "Route 23-Grass", "Route 23-C", lambda state: logic.has_badges(state, state.multiworld.victory_road_condition[player].value, player)) - connect(multiworld, player, "Route 23-Grass", "Route 23-S", lambda state: logic.can_surf(state, player)) + lambda state: logic.has_badges(state, world.options.route_22_gate_condition.value, player)) + connect(multiworld, player, "Route 23-Grass", "Route 23-C", lambda state: logic.has_badges(state, world.options.victory_road_condition.value, player)) + connect(multiworld, player, "Route 23-Grass", "Route 23-S", lambda state: logic.can_surf(state, world, player)) connect(multiworld, player, "Viridian City-N", "Viridian City-G", lambda state: - logic.has_badges(state, state.multiworld.viridian_gym_condition[player].value, player)) - connect(multiworld, player, "Route 2-SW", "Route 2-SE", lambda state: logic.can_cut(state, player)) - connect(multiworld, player, "Route 2-NW", "Route 2-NE", lambda state: logic.can_cut(state, player)) - connect(multiworld, player, "Route 2-E", "Route 2-NE", lambda state: logic.can_cut(state, player)) + logic.has_badges(state, world.options.viridian_gym_condition.value, player)) + connect(multiworld, player, "Route 2-SW", "Route 2-SE", lambda state: logic.can_cut(state, world, player)) + connect(multiworld, player, "Route 2-NW", "Route 2-NE", lambda state: logic.can_cut(state, world, player)) + connect(multiworld, player, "Route 2-E", "Route 2-NE", lambda state: logic.can_cut(state, world, player)) connect(multiworld, player, "Route 2-SW", "Viridian City-N") connect(multiworld, player, "Route 2-NW", "Pewter City") connect(multiworld, player, "Pewter City", "Pewter City-E") connect(multiworld, player, "Pewter City-M", "Pewter City", one_way=True) - connect(multiworld, player, "Pewter City", "Pewter City-M", lambda state: logic.can_cut(state, player), one_way=True) - connect(multiworld, player, "Pewter City-E", "Route 3", lambda state: logic.route_3(state, player), one_way=True) + connect(multiworld, player, "Pewter City", "Pewter City-M", lambda state: logic.can_cut(state, world, player), one_way=True) + connect(multiworld, player, "Pewter City-E", "Route 3", lambda state: logic.route(state, world, player), one_way=True) connect(multiworld, player, "Route 3", "Pewter City-E", one_way=True) connect(multiworld, player, "Route 4-W", "Route 3") - connect(multiworld, player, "Route 24", "Cerulean City-Water", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Cerulean City-Water", "Route 4-Lass", lambda state: logic.can_surf(state, player), one_way=True) + connect(multiworld, player, "Route 24", "Cerulean City-Water", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Cerulean City-Water", "Route 4-Lass", lambda state: logic.can_surf(state, world, player), one_way=True) connect(multiworld, player, "Mt Moon B2F", "Mt Moon B2F-Wild", one_way=True) connect(multiworld, player, "Mt Moon B2F-NE", "Mt Moon B2F-Wild", one_way=True) connect(multiworld, player, "Mt Moon B2F-C", "Mt Moon B2F-Wild", one_way=True) @@ -1644,14 +1648,14 @@ def create_regions(self): connect(multiworld, player, "Cerulean City", "Route 24") connect(multiworld, player, "Cerulean City", "Cerulean City-T", lambda state: state.has("Help Bill", player)) connect(multiworld, player, "Cerulean City-Outskirts", "Cerulean City", one_way=True) - connect(multiworld, player, "Cerulean City", "Cerulean City-Outskirts", lambda state: logic.can_cut(state, player), one_way=True) - connect(multiworld, player, "Cerulean City-Outskirts", "Route 9", lambda state: logic.can_cut(state, player)) + connect(multiworld, player, "Cerulean City", "Cerulean City-Outskirts", lambda state: logic.can_cut(state, world, player), one_way=True) + connect(multiworld, player, "Cerulean City-Outskirts", "Route 9", lambda state: logic.can_cut(state, world, player)) connect(multiworld, player, "Cerulean City-Outskirts", "Route 5") - connect(multiworld, player, "Cerulean Cave B1F", "Cerulean Cave B1F-E", lambda state: logic.can_surf(state, player), one_way=True) + connect(multiworld, player, "Cerulean Cave B1F", "Cerulean Cave B1F-E", lambda state: logic.can_surf(state, world, player), one_way=True) connect(multiworld, player, "Route 24", "Route 25") connect(multiworld, player, "Route 9", "Route 10-N") - connect(multiworld, player, "Route 10-N", "Route 10-C", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Route 10-C", "Route 10-P", lambda state: state.has("Plant Key", player) or not state.multiworld.extra_key_items[player].value) + connect(multiworld, player, "Route 10-N", "Route 10-C", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Route 10-C", "Route 10-P", lambda state: state.has("Plant Key", player) or not world.options.extra_key_items.value) connect(multiworld, player, "Pallet Town", "Pallet/Viridian Fishing", lambda state: state.has("Super Rod", player), one_way=True) connect(multiworld, player, "Viridian City", "Pallet/Viridian Fishing", lambda state: state.has("Super Rod", player), one_way=True) connect(multiworld, player, "Route 22", "Route 22 Fishing", lambda state: state.has("Super Rod", player), one_way=True) @@ -1697,10 +1701,10 @@ def create_regions(self): connect(multiworld, player, "Pallet Town", "Old Rod Fishing", lambda state: state.has("Old Rod", player), one_way=True) connect(multiworld, player, "Pallet Town", "Good Rod Fishing", lambda state: state.has("Good Rod", player), one_way=True) connect(multiworld, player, "Cinnabar Lab Fossil Room", "Fossil Level", lambda state: logic.fossil_checks(state, 1, player), one_way=True) - connect(multiworld, player, "Route 5 Gate-N", "Route 5 Gate-S", lambda state: logic.can_pass_guards(state, player)) - connect(multiworld, player, "Route 6 Gate-N", "Route 6 Gate-S", lambda state: logic.can_pass_guards(state, player)) - connect(multiworld, player, "Route 7 Gate-W", "Route 7 Gate-E", lambda state: logic.can_pass_guards(state, player)) - connect(multiworld, player, "Route 8 Gate-W", "Route 8 Gate-E", lambda state: logic.can_pass_guards(state, player)) + connect(multiworld, player, "Route 5 Gate-N", "Route 5 Gate-S", lambda state: logic.can_pass_guards(state, world, player)) + connect(multiworld, player, "Route 6 Gate-N", "Route 6 Gate-S", lambda state: logic.can_pass_guards(state, world, player)) + connect(multiworld, player, "Route 7 Gate-W", "Route 7 Gate-E", lambda state: logic.can_pass_guards(state, world, player)) + connect(multiworld, player, "Route 8 Gate-W", "Route 8 Gate-E", lambda state: logic.can_pass_guards(state, world, player)) connect(multiworld, player, "Saffron City", "Route 5-S") connect(multiworld, player, "Saffron City", "Route 6-N") connect(multiworld, player, "Saffron City", "Route 7-E") @@ -1710,59 +1714,59 @@ def create_regions(self): connect(multiworld, player, "Saffron City", "Saffron City-G", lambda state: state.has("Silph Co Liberated", player)) connect(multiworld, player, "Saffron City", "Saffron City-Silph", lambda state: state.has("Fuji Saved", player)) connect(multiworld, player, "Route 6", "Vermilion City") - connect(multiworld, player, "Vermilion City", "Vermilion City-G", lambda state: logic.can_surf(state, player) or logic.can_cut(state, player)) + connect(multiworld, player, "Vermilion City", "Vermilion City-G", lambda state: logic.can_surf(state, world, player) or logic.can_cut(state, world, player)) connect(multiworld, player, "Vermilion City", "Vermilion City-Dock", lambda state: state.has("S.S. Ticket", player)) connect(multiworld, player, "Vermilion City", "Route 11") - connect(multiworld, player, "Route 12-N", "Route 12-S", lambda state: logic.can_surf(state, player)) + connect(multiworld, player, "Route 12-N", "Route 12-S", lambda state: logic.can_surf(state, world, player)) connect(multiworld, player, "Route 12-W", "Route 11-E", lambda state: state.has("Poke Flute", player)) connect(multiworld, player, "Route 12-W", "Route 12-N", lambda state: state.has("Poke Flute", player)) connect(multiworld, player, "Route 12-W", "Route 12-S", lambda state: state.has("Poke Flute", player)) - connect(multiworld, player, "Route 12-S", "Route 12-Grass", lambda state: logic.can_cut(state, player), one_way=True) + connect(multiworld, player, "Route 12-S", "Route 12-Grass", lambda state: logic.can_cut(state, world, player), one_way=True) connect(multiworld, player, "Route 12-L", "Lavender Town") connect(multiworld, player, "Route 10-S", "Lavender Town") connect(multiworld, player, "Route 8", "Lavender Town") - connect(multiworld, player, "Pokemon Tower 6F", "Pokemon Tower 6F-S", lambda state: state.has("Silph Scope", player) or (state.has("Buy Poke Doll", player) and state.multiworld.poke_doll_skip[player])) - connect(multiworld, player, "Route 8", "Route 8-Grass", lambda state: logic.can_cut(state, player), one_way=True) + connect(multiworld, player, "Pokemon Tower 6F", "Pokemon Tower 6F-S", lambda state: state.has("Silph Scope", player) or (state.has("Buy Poke Doll", player) and world.options.poke_doll_skip)) + connect(multiworld, player, "Route 8", "Route 8-Grass", lambda state: logic.can_cut(state, world, player), one_way=True) connect(multiworld, player, "Route 7", "Celadon City") - connect(multiworld, player, "Celadon City", "Celadon City-G", lambda state: logic.can_cut(state, player)) + connect(multiworld, player, "Celadon City", "Celadon City-G", lambda state: logic.can_cut(state, world, player)) connect(multiworld, player, "Celadon City", "Route 16-E") - connect(multiworld, player, "Route 18 Gate 1F-W", "Route 18 Gate 1F-E", lambda state: state.has("Bicycle", player) or state.multiworld.bicycle_gate_skips[player] == "in_logic") - connect(multiworld, player, "Route 16 Gate 1F-W", "Route 16 Gate 1F-E", lambda state: state.has("Bicycle", player) or state.multiworld.bicycle_gate_skips[player] == "in_logic") - connect(multiworld, player, "Route 16-E", "Route 16-NE", lambda state: logic.can_cut(state, player)) + connect(multiworld, player, "Route 18 Gate 1F-W", "Route 18 Gate 1F-E", lambda state: state.has("Bicycle", player) or world.options.bicycle_gate_skips == "in_logic") + connect(multiworld, player, "Route 16 Gate 1F-W", "Route 16 Gate 1F-E", lambda state: state.has("Bicycle", player) or world.options.bicycle_gate_skips == "in_logic") + connect(multiworld, player, "Route 16-E", "Route 16-NE", lambda state: logic.can_cut(state, world, player)) connect(multiworld, player, "Route 16-E", "Route 16-C", lambda state: state.has("Poke Flute", player)) connect(multiworld, player, "Route 17", "Route 16-SW") connect(multiworld, player, "Route 17", "Route 18-W") # connect(multiworld, player, "Pokemon Mansion 2F", "Pokemon Mansion 2F-NW", one_way=True) - connect(multiworld, player, "Safari Zone Gate-S", "Safari Zone Gate-N", lambda state: state.has("Safari Pass", player) or not state.multiworld.extra_key_items[player].value, one_way=True) + connect(multiworld, player, "Safari Zone Gate-S", "Safari Zone Gate-N", lambda state: state.has("Safari Pass", player) or not world.options.extra_key_items.value, one_way=True) connect(multiworld, player, "Fuchsia City", "Route 15-W") connect(multiworld, player, "Fuchsia City", "Route 18-E") connect(multiworld, player, "Route 15", "Route 14") - connect(multiworld, player, "Route 14", "Route 15-N", lambda state: logic.can_cut(state, player), one_way=True) - connect(multiworld, player, "Route 14", "Route 14-Grass", lambda state: logic.can_cut(state, player), one_way=True) - connect(multiworld, player, "Route 13", "Route 13-Grass", lambda state: logic.can_cut(state, player), one_way=True) + connect(multiworld, player, "Route 14", "Route 15-N", lambda state: logic.can_cut(state, world, player), one_way=True) + connect(multiworld, player, "Route 14", "Route 14-Grass", lambda state: logic.can_cut(state, world, player), one_way=True) + connect(multiworld, player, "Route 13", "Route 13-Grass", lambda state: logic.can_cut(state, world, player), one_way=True) connect(multiworld, player, "Route 14", "Route 13") - connect(multiworld, player, "Route 13", "Route 13-E", lambda state: logic.can_strength(state, player) or logic.can_surf(state, player) or not state.multiworld.extra_strength_boulders[player].value) + connect(multiworld, player, "Route 13", "Route 13-E", lambda state: logic.can_strength(state, world, player) or logic.can_surf(state, world, player) or not world.options.extra_strength_boulders.value) connect(multiworld, player, "Route 12-S", "Route 13-E") connect(multiworld, player, "Fuchsia City", "Route 19-N") - connect(multiworld, player, "Route 19-N", "Route 19-S", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Route 20-E", "Route 20-IW", lambda state: logic.can_surf(state, player)) + connect(multiworld, player, "Route 19-N", "Route 19-S", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Route 20-E", "Route 20-IW", lambda state: logic.can_surf(state, world, player)) connect(multiworld, player, "Route 20-E", "Route 19-S") - connect(multiworld, player, "Route 20-W", "Cinnabar Island", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Route 20-IE", "Route 20-W", lambda state: logic.can_surf(state, player)) + connect(multiworld, player, "Route 20-W", "Cinnabar Island", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Route 20-IE", "Route 20-W", lambda state: logic.can_surf(state, world, player)) connect(multiworld, player, "Route 20-E", "Route 19/20-Water", one_way=True) connect(multiworld, player, "Route 20-W", "Route 19/20-Water", one_way=True) connect(multiworld, player, "Route 19-S", "Route 19/20-Water", one_way=True) - connect(multiworld, player, "Safari Zone West-NW", "Safari Zone West", lambda state: logic.can_surf(state, player)) + connect(multiworld, player, "Safari Zone West-NW", "Safari Zone West", lambda state: logic.can_surf(state, world, player)) connect(multiworld, player, "Safari Zone West", "Safari Zone West-Wild", one_way=True) connect(multiworld, player, "Safari Zone West-NW", "Safari Zone West-Wild", one_way=True) - connect(multiworld, player, "Safari Zone Center-NW", "Safari Zone Center-C", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Safari Zone Center-NE", "Safari Zone Center-C", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Safari Zone Center-S", "Safari Zone Center-C", lambda state: logic.can_surf(state, player)) + connect(multiworld, player, "Safari Zone Center-NW", "Safari Zone Center-C", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Safari Zone Center-NE", "Safari Zone Center-C", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Safari Zone Center-S", "Safari Zone Center-C", lambda state: logic.can_surf(state, world, player)) connect(multiworld, player, "Safari Zone Center-S", "Safari Zone Center-Wild", one_way=True) connect(multiworld, player, "Safari Zone Center-NW", "Safari Zone Center-Wild", one_way=True) connect(multiworld, player, "Safari Zone Center-NE", "Safari Zone Center-Wild", one_way=True) - connect(multiworld, player, "Victory Road 3F-S", "Victory Road 3F", lambda state: logic.can_strength(state, player)) - connect(multiworld, player, "Victory Road 3F-SE", "Victory Road 3F-S", lambda state: logic.can_strength(state, player), one_way=True) + connect(multiworld, player, "Victory Road 3F-S", "Victory Road 3F", lambda state: logic.can_strength(state, world, player)) + connect(multiworld, player, "Victory Road 3F-SE", "Victory Road 3F-S", lambda state: logic.can_strength(state, world, player), one_way=True) connect(multiworld, player, "Victory Road 3F", "Victory Road 3F-Wild", one_way=True) connect(multiworld, player, "Victory Road 3F-SE", "Victory Road 3F-Wild", one_way=True) connect(multiworld, player, "Victory Road 3F-S", "Victory Road 3F-Wild", one_way=True) @@ -1771,10 +1775,10 @@ def create_regions(self): connect(multiworld, player, "Victory Road 2F-C", "Victory Road 2F-Wild", one_way=True) connect(multiworld, player, "Victory Road 2F-E", "Victory Road 2F-Wild", one_way=True) connect(multiworld, player, "Victory Road 2F-SE", "Victory Road 2F-Wild", one_way=True) - connect(multiworld, player, "Victory Road 2F-W", "Victory Road 2F-C", lambda state: logic.can_strength(state, player), one_way=True) - connect(multiworld, player, "Victory Road 2F-NW", "Victory Road 2F-W", lambda state: logic.can_strength(state, player), one_way=True) - connect(multiworld, player, "Victory Road 2F-C", "Victory Road 2F-SE", lambda state: logic.can_strength(state, player) and state.has("Victory Road Boulder", player), one_way=True) - connect(multiworld, player, "Victory Road 1F-S", "Victory Road 1F", lambda state: logic.can_strength(state, player)) + connect(multiworld, player, "Victory Road 2F-W", "Victory Road 2F-C", lambda state: logic.can_strength(state, world, player), one_way=True) + connect(multiworld, player, "Victory Road 2F-NW", "Victory Road 2F-W", lambda state: logic.can_strength(state, world, player), one_way=True) + connect(multiworld, player, "Victory Road 2F-C", "Victory Road 2F-SE", lambda state: logic.can_strength(state, world, player) and state.has("Victory Road Boulder", player), one_way=True) + connect(multiworld, player, "Victory Road 1F-S", "Victory Road 1F", lambda state: logic.can_strength(state, world, player)) connect(multiworld, player, "Victory Road 1F", "Victory Road 1F-Wild", one_way=True) connect(multiworld, player, "Victory Road 1F-S", "Victory Road 1F-Wild", one_way=True) connect(multiworld, player, "Mt Moon B1F-W", "Mt Moon B1F-Wild", one_way=True) @@ -1796,50 +1800,50 @@ def create_regions(self): connect(multiworld, player, "Seafoam Islands B3F", "Seafoam Islands B3F-Wild", one_way=True) connect(multiworld, player, "Seafoam Islands B3F-NE", "Seafoam Islands B3F-Wild", one_way=True) connect(multiworld, player, "Seafoam Islands B3F-SE", "Seafoam Islands B3F-Wild", one_way=True) - connect(multiworld, player, "Seafoam Islands B4F", "Seafoam Islands B4F-W", lambda state: logic.can_surf(state, player), one_way=True) + connect(multiworld, player, "Seafoam Islands B4F", "Seafoam Islands B4F-W", lambda state: logic.can_surf(state, world, player), one_way=True) connect(multiworld, player, "Seafoam Islands B4F-W", "Seafoam Islands B4F", one_way=True) - connect(multiworld, player, "Seafoam Islands B3F", "Seafoam Islands B3F-SE", lambda state: logic.can_surf(state, player) and logic.can_strength(state, player) and state.has("Seafoam Exit Boulder", player, 6)) - connect(multiworld, player, "Viridian City", "Viridian City-N", lambda state: state.has("Oak's Parcel", player) or state.multiworld.old_man[player].value == 2 or logic.can_cut(state, player)) - connect(multiworld, player, "Route 11", "Route 11-C", lambda state: logic.can_strength(state, player) or not state.multiworld.extra_strength_boulders[player]) + connect(multiworld, player, "Seafoam Islands B3F", "Seafoam Islands B3F-SE", lambda state: logic.can_surf(state, world, player) and logic.can_strength(state, world, player) and state.has("Seafoam Exit Boulder", player, 6)) + connect(multiworld, player, "Viridian City", "Viridian City-N", lambda state: state.has("Oak's Parcel", player) or world.options.old_man.value == 2 or logic.can_cut(state, world, player)) + connect(multiworld, player, "Route 11", "Route 11-C", lambda state: logic.can_strength(state, world, player) or not world.options.extra_strength_boulders) connect(multiworld, player, "Cinnabar Island", "Cinnabar Island-G", lambda state: state.has("Secret Key", player)) - connect(multiworld, player, "Cinnabar Island", "Cinnabar Island-M", lambda state: state.has("Mansion Key", player) or not state.multiworld.extra_key_items[player].value) - connect(multiworld, player, "Route 21", "Cinnabar Island", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Pallet Town", "Route 21", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Celadon Gym", "Celadon Gym-C", lambda state: logic.can_cut(state, player), one_way=True) - connect(multiworld, player, "Celadon Game Corner", "Celadon Game Corner-Hidden Stairs", lambda state: (not state.multiworld.extra_key_items[player]) or state.has("Hideout Key", player), one_way=True) + connect(multiworld, player, "Cinnabar Island", "Cinnabar Island-M", lambda state: state.has("Mansion Key", player) or not world.options.extra_key_items.value) + connect(multiworld, player, "Route 21", "Cinnabar Island", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Pallet Town", "Route 21", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Celadon Gym", "Celadon Gym-C", lambda state: logic.can_cut(state, world, player), one_way=True) + connect(multiworld, player, "Celadon Game Corner", "Celadon Game Corner-Hidden Stairs", lambda state: (not world.options.extra_key_items) or state.has("Hideout Key", player), one_way=True) connect(multiworld, player, "Celadon Game Corner-Hidden Stairs", "Celadon Game Corner", one_way=True) connect(multiworld, player, "Rocket Hideout B1F-SE", "Rocket Hideout B1F", one_way=True) - connect(multiworld, player, "Indigo Plateau Lobby", "Indigo Plateau Lobby-N", lambda state: logic.has_badges(state, state.multiworld.elite_four_badges_condition[player].value, player) and logic.has_pokemon(state, state.multiworld.elite_four_pokedex_condition[player].total, player) and logic.has_key_items(state, state.multiworld.elite_four_key_items_condition[player].total, player) and (state.has("Pokedex", player, int(state.multiworld.elite_four_pokedex_condition[player].total > 1) * state.multiworld.require_pokedex[player].value))) + connect(multiworld, player, "Indigo Plateau Lobby", "Indigo Plateau Lobby-N", lambda state: logic.has_badges(state, world.options.elite_four_badges_condition.value, player) and logic.has_pokemon(state, world.options.elite_four_pokedex_condition.total, player) and logic.has_key_items(state, world.options.elite_four_key_items_condition.total, player) and (state.has("Pokedex", player, int(world.options.elite_four_pokedex_condition.total > 1) * world.options.require_pokedex.value))) connect(multiworld, player, "Pokemon Mansion 3F", "Pokemon Mansion 3F-Wild", one_way=True) connect(multiworld, player, "Pokemon Mansion 3F-SW", "Pokemon Mansion 3F-Wild", one_way=True) connect(multiworld, player, "Pokemon Mansion 3F-SE", "Pokemon Mansion 3F-Wild", one_way=True) connect(multiworld, player, "Pokemon Mansion 2F-E", "Pokemon Mansion 2F-Wild", one_way=True) connect(multiworld, player, "Pokemon Mansion 1F-SE", "Pokemon Mansion 1F-Wild", one_way=True) connect(multiworld, player, "Pokemon Mansion 1F", "Pokemon Mansion 1F-Wild", one_way=True) - connect(multiworld, player, "Rock Tunnel 1F-S 1", "Rock Tunnel 1F-S", lambda state: logic.rock_tunnel(state, player)) - connect(multiworld, player, "Rock Tunnel 1F-S 2", "Rock Tunnel 1F-S", lambda state: logic.rock_tunnel(state, player)) - connect(multiworld, player, "Rock Tunnel 1F-NW 1", "Rock Tunnel 1F-NW", lambda state: logic.rock_tunnel(state, player)) - connect(multiworld, player, "Rock Tunnel 1F-NW 2", "Rock Tunnel 1F-NW", lambda state: logic.rock_tunnel(state, player)) - connect(multiworld, player, "Rock Tunnel 1F-NE 1", "Rock Tunnel 1F-NE", lambda state: logic.rock_tunnel(state, player)) - connect(multiworld, player, "Rock Tunnel 1F-NE 2", "Rock Tunnel 1F-NE", lambda state: logic.rock_tunnel(state, player)) - connect(multiworld, player, "Rock Tunnel B1F-W 1", "Rock Tunnel B1F-W", lambda state: logic.rock_tunnel(state, player)) - connect(multiworld, player, "Rock Tunnel B1F-W 2", "Rock Tunnel B1F-W", lambda state: logic.rock_tunnel(state, player)) - connect(multiworld, player, "Rock Tunnel B1F-E 1", "Rock Tunnel B1F-E", lambda state: logic.rock_tunnel(state, player)) - connect(multiworld, player, "Rock Tunnel B1F-E 2", "Rock Tunnel B1F-E", lambda state: logic.rock_tunnel(state, player)) - connect(multiworld, player, "Rock Tunnel 1F-S", "Rock Tunnel 1F-Wild", lambda state: logic.rock_tunnel(state, player), one_way=True) - connect(multiworld, player, "Rock Tunnel 1F-NW", "Rock Tunnel 1F-Wild", lambda state: logic.rock_tunnel(state, player), one_way=True) - connect(multiworld, player, "Rock Tunnel 1F-NE", "Rock Tunnel 1F-Wild", lambda state: logic.rock_tunnel(state, player), one_way=True) - connect(multiworld, player, "Rock Tunnel B1F-W", "Rock Tunnel B1F-Wild", lambda state: logic.rock_tunnel(state, player), one_way=True) - connect(multiworld, player, "Rock Tunnel B1F-E", "Rock Tunnel B1F-Wild", lambda state: logic.rock_tunnel(state, player), one_way=True) + connect(multiworld, player, "Rock Tunnel 1F-S 1", "Rock Tunnel 1F-S", lambda state: logic.rock_tunnel(state, world, player)) + connect(multiworld, player, "Rock Tunnel 1F-S 2", "Rock Tunnel 1F-S", lambda state: logic.rock_tunnel(state, world, player)) + connect(multiworld, player, "Rock Tunnel 1F-NW 1", "Rock Tunnel 1F-NW", lambda state: logic.rock_tunnel(state, world, player)) + connect(multiworld, player, "Rock Tunnel 1F-NW 2", "Rock Tunnel 1F-NW", lambda state: logic.rock_tunnel(state, world, player)) + connect(multiworld, player, "Rock Tunnel 1F-NE 1", "Rock Tunnel 1F-NE", lambda state: logic.rock_tunnel(state, world, player)) + connect(multiworld, player, "Rock Tunnel 1F-NE 2", "Rock Tunnel 1F-NE", lambda state: logic.rock_tunnel(state, world, player)) + connect(multiworld, player, "Rock Tunnel B1F-W 1", "Rock Tunnel B1F-W", lambda state: logic.rock_tunnel(state, world, player)) + connect(multiworld, player, "Rock Tunnel B1F-W 2", "Rock Tunnel B1F-W", lambda state: logic.rock_tunnel(state, world, player)) + connect(multiworld, player, "Rock Tunnel B1F-E 1", "Rock Tunnel B1F-E", lambda state: logic.rock_tunnel(state, world, player)) + connect(multiworld, player, "Rock Tunnel B1F-E 2", "Rock Tunnel B1F-E", lambda state: logic.rock_tunnel(state, world, player)) + connect(multiworld, player, "Rock Tunnel 1F-S", "Rock Tunnel 1F-Wild", lambda state: logic.rock_tunnel(state, world, player), one_way=True) + connect(multiworld, player, "Rock Tunnel 1F-NW", "Rock Tunnel 1F-Wild", lambda state: logic.rock_tunnel(state, world, player), one_way=True) + connect(multiworld, player, "Rock Tunnel 1F-NE", "Rock Tunnel 1F-Wild", lambda state: logic.rock_tunnel(state, world, player), one_way=True) + connect(multiworld, player, "Rock Tunnel B1F-W", "Rock Tunnel B1F-Wild", lambda state: logic.rock_tunnel(state, world, player), one_way=True) + connect(multiworld, player, "Rock Tunnel B1F-E", "Rock Tunnel B1F-Wild", lambda state: logic.rock_tunnel(state, world, player), one_way=True) connect(multiworld, player, "Cerulean Cave 1F-SE", "Cerulean Cave 1F-Wild", one_way=True) connect(multiworld, player, "Cerulean Cave 1F-SW", "Cerulean Cave 1F-Wild", one_way=True) connect(multiworld, player, "Cerulean Cave 1F-NE", "Cerulean Cave 1F-Wild", one_way=True) connect(multiworld, player, "Cerulean Cave 1F-N", "Cerulean Cave 1F-Wild", one_way=True) connect(multiworld, player, "Cerulean Cave 1F-NW", "Cerulean Cave 1F-Wild", one_way=True) - connect(multiworld, player, "Cerulean Cave 1F-SE", "Cerulean Cave 1F-Water", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Cerulean Cave 1F-SW", "Cerulean Cave 1F-Water", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Cerulean Cave 1F-N", "Cerulean Cave 1F-Water", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Cerulean Cave 1F-NE", "Cerulean Cave 1F-Water", lambda state: logic.can_surf(state, player)) + connect(multiworld, player, "Cerulean Cave 1F-SE", "Cerulean Cave 1F-Water", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Cerulean Cave 1F-SW", "Cerulean Cave 1F-Water", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Cerulean Cave 1F-N", "Cerulean Cave 1F-Water", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Cerulean Cave 1F-NE", "Cerulean Cave 1F-Water", lambda state: logic.can_surf(state, world, player)) connect(multiworld, player, "Pokemon Mansion 3F", "Pokemon Mansion 3F-SE", one_way=True) connect(multiworld, player, "Silph Co 2F", "Silph Co 2F-NW", lambda state: logic.card_key(state, 2, player)) connect(multiworld, player, "Silph Co 2F", "Silph Co 2F-SW", lambda state: logic.card_key(state, 2, player)) @@ -1858,80 +1862,80 @@ def create_regions(self): connect(multiworld, player, "Silph Co 9F-NW", "Silph Co 9F-SW", lambda state: logic.card_key(state, 9, player)) connect(multiworld, player, "Silph Co 10F", "Silph Co 10F-SE", lambda state: logic.card_key(state, 10, player)) connect(multiworld, player, "Silph Co 11F-W", "Silph Co 11F-C", lambda state: logic.card_key(state, 11, player)) - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-1F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-2F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-3F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-4F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-5F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-6F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-7F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-8F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-9F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-10F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-11F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-1F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-2F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-3F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-4F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-5F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-6F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-7F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-8F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-9F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-10F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-11F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), connect(multiworld, player, "Rocket Hideout Elevator", "Rocket Hideout Elevator-B1F", lambda state: state.has("Lift Key", player)) connect(multiworld, player, "Rocket Hideout Elevator", "Rocket Hideout Elevator-B2F", lambda state: state.has("Lift Key", player)) connect(multiworld, player, "Rocket Hideout Elevator", "Rocket Hideout Elevator-B4F", lambda state: state.has("Lift Key", player)) - connect(multiworld, player, "Celadon Department Store Elevator", "Celadon Department Store Elevator-1F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Celadon Department Store Elevator", "Celadon Department Store Elevator-2F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Celadon Department Store Elevator", "Celadon Department Store Elevator-3F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Celadon Department Store Elevator", "Celadon Department Store Elevator-4F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Celadon Department Store Elevator", "Celadon Department Store Elevator-5F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), + connect(multiworld, player, "Celadon Department Store Elevator", "Celadon Department Store Elevator-1F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Celadon Department Store Elevator", "Celadon Department Store Elevator-2F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Celadon Department Store Elevator", "Celadon Department Store Elevator-3F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Celadon Department Store Elevator", "Celadon Department Store Elevator-4F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Celadon Department Store Elevator", "Celadon Department Store Elevator-5F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), connect(multiworld, player, "Route 23-N", "Indigo Plateau") connect(multiworld, player, "Cerulean City-Water", "Cerulean City-Cave", lambda state: - logic.has_badges(state, self.multiworld.cerulean_cave_badges_condition[player].value, player) and - logic.has_key_items(state, self.multiworld.cerulean_cave_key_items_condition[player].total, player) and logic.can_surf(state, player)) + logic.has_badges(state, world.options.cerulean_cave_badges_condition.value, player) and + logic.has_key_items(state, world.options.cerulean_cave_key_items_condition.total, player) and logic.can_surf(state, world, player)) # access to any part of a city will enable flying to the Pokemon Center - connect(multiworld, player, "Cerulean City-Cave", "Cerulean City", lambda state: logic.can_fly(state, player), one_way=True) - connect(multiworld, player, "Cerulean City-Badge House Backyard", "Cerulean City", lambda state: logic.can_fly(state, player), one_way=True) - connect(multiworld, player, "Cerulean City-T", "Cerulean City", lambda state: logic.can_fly(state, player), one_way=True, name="Cerulean City-T to Cerulean City (Fly)") - connect(multiworld, player, "Fuchsia City-Good Rod House Backyard", "Fuchsia City", lambda state: logic.can_fly(state, player), one_way=True) - connect(multiworld, player, "Saffron City-G", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True, name="Saffron City-G to Saffron City (Fly)") - connect(multiworld, player, "Saffron City-Pidgey", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True, name="Saffron City-Pidgey to Saffron City (Fly)") - connect(multiworld, player, "Saffron City-Silph", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True, name="Saffron City-Silph to Saffron City (Fly)") - connect(multiworld, player, "Saffron City-Copycat", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True, name="Saffron City-Copycat to Saffron City (Fly)") - connect(multiworld, player, "Celadon City-G", "Celadon City", lambda state: logic.can_fly(state, player), one_way=True, name="Celadon City-G to Celadon City (Fly)") - connect(multiworld, player, "Vermilion City-G", "Vermilion City", lambda state: logic.can_fly(state, player), one_way=True, name="Vermilion City-G to Vermilion City (Fly)") - connect(multiworld, player, "Vermilion City-Dock", "Vermilion City", lambda state: logic.can_fly(state, player), one_way=True, name="Vermilion City-Dock to Vermilion City (Fly)") - connect(multiworld, player, "Cinnabar Island-G", "Cinnabar Island", lambda state: logic.can_fly(state, player), one_way=True, name="Cinnabar Island-G to Cinnabar Island (Fly)") - connect(multiworld, player, "Cinnabar Island-M", "Cinnabar Island", lambda state: logic.can_fly(state, player), one_way=True, name="Cinnabar Island-M to Cinnabar Island (Fly)") + connect(multiworld, player, "Cerulean City-Cave", "Cerulean City", lambda state: logic.can_fly(state, world, player), one_way=True) + connect(multiworld, player, "Cerulean City-Badge House Backyard", "Cerulean City", lambda state: logic.can_fly(state, world, player), one_way=True) + connect(multiworld, player, "Cerulean City-T", "Cerulean City", lambda state: logic.can_fly(state, world, player), one_way=True, name="Cerulean City-T to Cerulean City (Fly)") + connect(multiworld, player, "Fuchsia City-Good Rod House Backyard", "Fuchsia City", lambda state: logic.can_fly(state, world, player), one_way=True) + connect(multiworld, player, "Saffron City-G", "Saffron City", lambda state: logic.can_fly(state, world, player), one_way=True, name="Saffron City-G to Saffron City (Fly)") + connect(multiworld, player, "Saffron City-Pidgey", "Saffron City", lambda state: logic.can_fly(state, world, player), one_way=True, name="Saffron City-Pidgey to Saffron City (Fly)") + connect(multiworld, player, "Saffron City-Silph", "Saffron City", lambda state: logic.can_fly(state, world, player), one_way=True, name="Saffron City-Silph to Saffron City (Fly)") + connect(multiworld, player, "Saffron City-Copycat", "Saffron City", lambda state: logic.can_fly(state, world, player), one_way=True, name="Saffron City-Copycat to Saffron City (Fly)") + connect(multiworld, player, "Celadon City-G", "Celadon City", lambda state: logic.can_fly(state, world, player), one_way=True, name="Celadon City-G to Celadon City (Fly)") + connect(multiworld, player, "Vermilion City-G", "Vermilion City", lambda state: logic.can_fly(state, world, player), one_way=True, name="Vermilion City-G to Vermilion City (Fly)") + connect(multiworld, player, "Vermilion City-Dock", "Vermilion City", lambda state: logic.can_fly(state, world, player), one_way=True, name="Vermilion City-Dock to Vermilion City (Fly)") + connect(multiworld, player, "Cinnabar Island-G", "Cinnabar Island", lambda state: logic.can_fly(state, world, player), one_way=True, name="Cinnabar Island-G to Cinnabar Island (Fly)") + connect(multiworld, player, "Cinnabar Island-M", "Cinnabar Island", lambda state: logic.can_fly(state, world, player), one_way=True, name="Cinnabar Island-M to Cinnabar Island (Fly)") # drops connect(multiworld, player, "Seafoam Islands 1F", "Seafoam Islands B1F", one_way=True, name="Seafoam Islands 1F to Seafoam Islands B1F (Drop)") connect(multiworld, player, "Seafoam Islands 1F", "Seafoam Islands B1F-NE", one_way=True, name="Seafoam Islands 1F to Seafoam Islands B1F-NE (Drop)") connect(multiworld, player, "Seafoam Islands B1F", "Seafoam Islands B2F-NW", one_way=True, name="Seafoam Islands 1F to Seafoam Islands B2F-NW (Drop)") connect(multiworld, player, "Seafoam Islands B1F-NE", "Seafoam Islands B2F-NE", one_way=True) - connect(multiworld, player, "Seafoam Islands B2F-NW", "Seafoam Islands B3F", lambda state: logic.can_strength(state, player) and state.has("Seafoam Exit Boulder", player, 6), one_way=True) - connect(multiworld, player, "Seafoam Islands B2F-NE", "Seafoam Islands B3F", lambda state: logic.can_strength(state, player) and state.has("Seafoam Exit Boulder", player, 6), one_way=True) - connect(multiworld, player, "Seafoam Islands B2F-NW", "Seafoam Islands B3F-SE", lambda state: logic.can_strength(state, player) and state.has("Seafoam Exit Boulder", player, 6), one_way=True) - connect(multiworld, player, "Seafoam Islands B2F-NE", "Seafoam Islands B3F-SE", lambda state: logic.can_strength(state, player) and state.has("Seafoam Exit Boulder", player, 6), one_way=True) + connect(multiworld, player, "Seafoam Islands B2F-NW", "Seafoam Islands B3F", lambda state: logic.can_strength(state, world, player) and state.has("Seafoam Exit Boulder", player, 6), one_way=True) + connect(multiworld, player, "Seafoam Islands B2F-NE", "Seafoam Islands B3F", lambda state: logic.can_strength(state, world, player) and state.has("Seafoam Exit Boulder", player, 6), one_way=True) + connect(multiworld, player, "Seafoam Islands B2F-NW", "Seafoam Islands B3F-SE", lambda state: logic.can_strength(state, world, player) and state.has("Seafoam Exit Boulder", player, 6), one_way=True) + connect(multiworld, player, "Seafoam Islands B2F-NE", "Seafoam Islands B3F-SE", lambda state: logic.can_strength(state, world, player) and state.has("Seafoam Exit Boulder", player, 6), one_way=True) # If you haven't dropped the boulders, you'll go straight to B4F connect(multiworld, player, "Seafoam Islands B2F-NW", "Seafoam Islands B4F-W", one_way=True) connect(multiworld, player, "Seafoam Islands B2F-NE", "Seafoam Islands B4F-W", one_way=True) connect(multiworld, player, "Seafoam Islands B3F", "Seafoam Islands B4F", one_way=True, name="Seafoam Islands B1F to Seafoam Islands B4F (Drop)") - connect(multiworld, player, "Seafoam Islands B3F", "Seafoam Islands B4F-W", lambda state: logic.can_surf(state, player), one_way=True) + connect(multiworld, player, "Seafoam Islands B3F", "Seafoam Islands B4F-W", lambda state: logic.can_surf(state, world, player), one_way=True) connect(multiworld, player, "Pokemon Mansion 3F-SE", "Pokemon Mansion 2F", one_way=True) connect(multiworld, player, "Pokemon Mansion 3F-SE", "Pokemon Mansion 1F-SE", one_way=True) connect(multiworld, player, "Victory Road 3F-S", "Victory Road 2F-C", one_way=True) - if multiworld.worlds[player].fly_map != "Pallet Town": - connect(multiworld, player, "Menu", multiworld.worlds[player].fly_map, - lambda state: logic.can_fly(state, player), one_way=True, name="Free Fly Location") + if world.fly_map != "Pallet Town": + connect(multiworld, player, "Menu", world.fly_map, + lambda state: logic.can_fly(state, world, player), one_way=True, name="Free Fly Location") - if multiworld.worlds[player].town_map_fly_map != "Pallet Town": - connect(multiworld, player, "Menu", multiworld.worlds[player].town_map_fly_map, - lambda state: logic.can_fly(state, player) and state.has("Town Map", player), one_way=True, + if world.town_map_fly_map != "Pallet Town": + connect(multiworld, player, "Menu", world.town_map_fly_map, + lambda state: logic.can_fly(state, world, player) and state.has("Town Map", player), one_way=True, name="Town Map Fly Location") - cache = multiworld.regions.entrance_cache[self.player].copy() - if multiworld.badgesanity[player] or multiworld.door_shuffle[player] in ("off", "simple"): + cache = multiworld.regions.entrance_cache[world.player].copy() + if world.options.badgesanity or world.options.door_shuffle in ("off", "simple"): badges = None badge_locs = None else: - badges = [item for item in self.item_pool if "Badge" in item.name] + badges = [item for item in world.item_pool if "Badge" in item.name] for badge in badges: - self.item_pool.remove(badge) + world.item_pool.remove(badge) badge_locs = [multiworld.get_location(loc, player) for loc in [ "Pewter Gym - Brock Prize", "Cerulean Gym - Misty Prize", "Vermilion Gym - Lt. Surge Prize", "Celadon Gym - Erika Prize", "Fuchsia Gym - Koga Prize", "Saffron Gym - Sabrina Prize", @@ -1939,15 +1943,18 @@ def create_regions(self): ]] for attempt in range(10): try: - door_shuffle(self, multiworld, player, badges, badge_locs) + door_shuffle(world, multiworld, player, badges, badge_locs) except DoorShuffleException as e: if attempt == 9: raise e - for region in self.multiworld.get_regions(player): + for region in world.multiworld.get_regions(player): for entrance in reversed(region.exits): if isinstance(entrance, PokemonRBWarp): region.exits.remove(entrance) - multiworld.regions.entrance_cache[self.player] = cache.copy() + for entrance in reversed(region.entrances): + if isinstance(entrance, PokemonRBWarp): + region.entrances.remove(entrance) + multiworld.regions.entrance_cache[world.player] = cache.copy() if badge_locs: for loc in badge_locs: loc.item = None @@ -1965,36 +1972,36 @@ def door_shuffle(world, multiworld, player, badges, badge_locs): shuffle = True interior = False if not outdoor_map(region.name) and not outdoor_map(entrance_data['to']['map']): - if multiworld.door_shuffle[player] not in ("full", "insanity", "decoupled"): + if world.options.door_shuffle not in ("full", "insanity", "decoupled"): shuffle = False interior = True - if multiworld.door_shuffle[player] == "simple": + if world.options.door_shuffle == "simple": if sorted([entrance_data['to']['map'], region.name]) == ["Celadon Game Corner-Hidden Stairs", "Rocket Hideout B1F"]: shuffle = True elif sorted([entrance_data['to']['map'], region.name]) == ["Celadon City", "Celadon Game Corner"]: shuffle = False - if (multiworld.randomize_rock_tunnel[player] and "Rock Tunnel" in region.name and "Rock Tunnel" in + if (world.options.randomize_rock_tunnel and "Rock Tunnel" in region.name and "Rock Tunnel" in entrance_data['to']['map']): shuffle = False elif (f"{region.name} to {entrance_data['to']['map']}" if "name" not in entrance_data else entrance_data["name"]) in silph_co_warps + saffron_gym_warps: - if multiworld.warp_tile_shuffle[player]: + if world.options.warp_tile_shuffle: shuffle = True - if multiworld.warp_tile_shuffle[player] == "mixed" and multiworld.door_shuffle[player] == "full": + if world.options.warp_tile_shuffle == "mixed" and world.options.door_shuffle == "full": interior = True else: interior = False else: shuffle = False - elif not multiworld.door_shuffle[player]: + elif not world.options.door_shuffle: shuffle = False if shuffle: entrance = PokemonRBWarp(player, f"{region.name} to {entrance_data['to']['map']}" if "name" not in entrance_data else entrance_data["name"], region, entrance_data["id"], entrance_data["address"], entrance_data["flags"] if "flags" in entrance_data else "") - if interior and multiworld.door_shuffle[player] == "full": + if interior and world.options.door_shuffle == "full": full_interiors.append(entrance) else: entrances.append(entrance) @@ -2006,22 +2013,22 @@ def door_shuffle(world, multiworld, player, badges, badge_locs): forced_connections = set() one_way_forced_connections = set() - if multiworld.door_shuffle[player]: - if multiworld.door_shuffle[player] in ("full", "insanity", "decoupled"): + if world.options.door_shuffle: + if world.options.door_shuffle in ("full", "insanity", "decoupled"): safari_zone_doors = [door for pair in safari_zone_connections for door in pair] safari_zone_doors.sort() order = ["Center", "East", "North", "West"] - multiworld.random.shuffle(order) + world.random.shuffle(order) usable_doors = ["Safari Zone Gate-N to Safari Zone Center-S"] for section in order: section_doors = [door for door in safari_zone_doors if door.startswith(f"Safari Zone {section}")] - connect_door_a = multiworld.random.choice(usable_doors) - connect_door_b = multiworld.random.choice(section_doors) + connect_door_a = world.random.choice(usable_doors) + connect_door_b = world.random.choice(section_doors) usable_doors.remove(connect_door_a) section_doors.remove(connect_door_b) forced_connections.add((connect_door_a, connect_door_b)) usable_doors += section_doors - multiworld.random.shuffle(usable_doors) + world.random.shuffle(usable_doors) while usable_doors: forced_connections.add((usable_doors.pop(), usable_doors.pop())) else: @@ -2029,32 +2036,32 @@ def door_shuffle(world, multiworld, player, badges, badge_locs): usable_safe_rooms = safe_rooms.copy() - if multiworld.door_shuffle[player] == "simple": + if world.options.door_shuffle == "simple": forced_connections.update(simple_mandatory_connections) else: usable_safe_rooms += pokemarts - if multiworld.key_items_only[player]: + if world.options.key_items_only: usable_safe_rooms.remove("Viridian Pokemart to Viridian City") - if multiworld.door_shuffle[player] in ("full", "insanity", "decoupled"): + if world.options.door_shuffle in ("full", "insanity", "decoupled"): forced_connections.update(full_mandatory_connections) - r = multiworld.random.randint(0, 3) + r = world.random.randint(0, 3) if r == 2: forced_connections.add(("Pokemon Mansion 1F-SE to Pokemon Mansion B1F", "Pokemon Mansion 3F-SE to Pokemon Mansion 2F-E")) forced_connections.add(("Pokemon Mansion 2F to Pokemon Mansion 3F", - multiworld.random.choice(mansion_stair_destinations + mansion_dead_ends + world.random.choice(mansion_stair_destinations + mansion_dead_ends + ["Pokemon Mansion B1F to Pokemon Mansion 1F-SE"]))) - if multiworld.door_shuffle[player] == "full": + if world.options.door_shuffle == "full": forced_connections.add(("Pokemon Mansion 1F to Pokemon Mansion 2F", "Pokemon Mansion 3F to Pokemon Mansion 2F")) elif r == 3: - dead_end = multiworld.random.randint(0, 1) + dead_end = world.random.randint(0, 1) forced_connections.add(("Pokemon Mansion 3F-SE to Pokemon Mansion 2F-E", mansion_dead_ends[dead_end])) forced_connections.add(("Pokemon Mansion 1F-SE to Pokemon Mansion B1F", "Pokemon Mansion B1F to Pokemon Mansion 1F-SE")) forced_connections.add(("Pokemon Mansion 2F to Pokemon Mansion 3F", - multiworld.random.choice(mansion_stair_destinations + world.random.choice(mansion_stair_destinations + [mansion_dead_ends[dead_end ^ 1]]))) else: forced_connections.add(("Pokemon Mansion 3F-SE to Pokemon Mansion 2F-E", @@ -2062,40 +2069,40 @@ def door_shuffle(world, multiworld, player, badges, badge_locs): forced_connections.add(("Pokemon Mansion 1F-SE to Pokemon Mansion B1F", mansion_dead_ends[r ^ 1])) forced_connections.add(("Pokemon Mansion 2F to Pokemon Mansion 3F", - multiworld.random.choice(mansion_stair_destinations + world.random.choice(mansion_stair_destinations + ["Pokemon Mansion B1F to Pokemon Mansion 1F-SE"]))) - if multiworld.door_shuffle[player] in ("insanity", "decoupled"): + if world.options.door_shuffle in ("insanity", "decoupled"): usable_safe_rooms += insanity_safe_rooms - safe_rooms_sample = multiworld.random.sample(usable_safe_rooms, 6) + safe_rooms_sample = world.random.sample(usable_safe_rooms, 6) pallet_safe_room = safe_rooms_sample[-1] - for a, b in zip(multiworld.random.sample(["Pallet Town to Player's House 1F", "Pallet Town to Oak's Lab", + for a, b in zip(world.random.sample(["Pallet Town to Player's House 1F", "Pallet Town to Oak's Lab", "Pallet Town to Rival's House"], 3), ["Oak's Lab to Pallet Town", "Player's House 1F to Pallet Town", pallet_safe_room]): one_way_forced_connections.add((a, b)) - if multiworld.door_shuffle[player] == "decoupled": + if world.options.door_shuffle == "decoupled": for a, b in zip(["Oak's Lab to Pallet Town", "Player's House 1F to Pallet Town", pallet_safe_room], - multiworld.random.sample(["Pallet Town to Player's House 1F", "Pallet Town to Oak's Lab", + world.random.sample(["Pallet Town to Player's House 1F", "Pallet Town to Oak's Lab", "Pallet Town to Rival's House"], 3)): one_way_forced_connections.add((a, b)) for a, b in zip(safari_zone_houses, safe_rooms_sample): one_way_forced_connections.add((a, b)) - if multiworld.door_shuffle[player] == "decoupled": - for a, b in zip(multiworld.random.sample(safe_rooms_sample[:-1], len(safe_rooms_sample) - 1), + if world.options.door_shuffle == "decoupled": + for a, b in zip(world.random.sample(safe_rooms_sample[:-1], len(safe_rooms_sample) - 1), safari_zone_houses): one_way_forced_connections.add((a, b)) - if multiworld.door_shuffle[player] == "simple": + if world.options.door_shuffle == "simple": # force Indigo Plateau Lobby to vanilla location on simple, otherwise shuffle with Pokemon Centers. - for a, b in zip(multiworld.random.sample(pokemon_center_entrances[0:-1], 11), pokemon_centers[0:-1]): + for a, b in zip(world.random.sample(pokemon_center_entrances[0:-1], 11), pokemon_centers[0:-1]): forced_connections.add((a, b)) forced_connections.add((pokemon_center_entrances[-1], pokemon_centers[-1])) - forced_pokemarts = multiworld.random.sample(pokemart_entrances, 8) - if multiworld.key_items_only[player]: + forced_pokemarts = world.random.sample(pokemart_entrances, 8) + if world.options.key_items_only: forced_pokemarts.sort(key=lambda i: i[0] != "Viridian Pokemart to Viridian City") for a, b in zip(forced_pokemarts, pokemarts): forced_connections.add((a, b)) @@ -2104,21 +2111,21 @@ def door_shuffle(world, multiworld, player, badges, badge_locs): # fly / blackout warps. Rather than mess with those coordinates (besides in Pallet Town) or have players # warping outside an entrance that isn't the Pokemon Center, just always put Pokemon Centers at Pokemon # Center entrances - for a, b in zip(multiworld.random.sample(pokemon_center_entrances, 12), pokemon_centers): + for a, b in zip(world.random.sample(pokemon_center_entrances, 12), pokemon_centers): one_way_forced_connections.add((a, b)) # Ensure a Pokemart is available at the beginning of the game - if multiworld.key_items_only[player]: - one_way_forced_connections.add((multiworld.random.choice(initial_doors), + if world.options.key_items_only: + one_way_forced_connections.add((world.random.choice(initial_doors), "Viridian Pokemart to Viridian City")) elif "Pokemart" not in pallet_safe_room: - one_way_forced_connections.add((multiworld.random.choice(initial_doors), multiworld.random.choice( + one_way_forced_connections.add((world.random.choice(initial_doors), world.random.choice( [mart for mart in pokemarts if mart not in safe_rooms_sample]))) - if multiworld.warp_tile_shuffle[player] == "shuffle" or (multiworld.warp_tile_shuffle[player] == "mixed" - and multiworld.door_shuffle[player] - in ("off", "simple", "interiors")): - warps = multiworld.random.sample(silph_co_warps, len(silph_co_warps)) + if world.options.warp_tile_shuffle == "shuffle" or (world.options.warp_tile_shuffle == "mixed" + and world.options.door_shuffle + in ("off", "simple", "interiors")): + warps = world.random.sample(silph_co_warps, len(silph_co_warps)) # The only warp tiles never reachable from the stairs/elevators are the two 7F-NW warps (where the rival is) # and the final 11F-W warp. As long as the two 7F-NW warps aren't connected to each other, everything should # always be reachable. @@ -2129,9 +2136,9 @@ def door_shuffle(world, multiworld, player, badges, badge_locs): # Shuffle Saffron Gym sections, then connect one warp from each section to the next. # Then connect the rest at random. - warps = multiworld.random.sample(saffron_gym_warps, len(saffron_gym_warps)) + warps = world.random.sample(saffron_gym_warps, len(saffron_gym_warps)) solution = ["SW", "W", "NW", "N", "NE", "E", "SE"] - multiworld.random.shuffle(solution) + world.random.shuffle(solution) solution = ["S"] + solution + ["C"] for i in range(len(solution) - 1): f, t = solution[i], solution[i + 1] @@ -2151,7 +2158,7 @@ def door_shuffle(world, multiworld, player, badges, badge_locs): forced_connections.add((warps.pop(), warps.pop(),)) dc_destinations = None - if multiworld.door_shuffle[player] == "decoupled": + if world.options.door_shuffle == "decoupled": dc_destinations = entrances.copy() for pair in one_way_forced_connections: entrance_a = multiworld.get_entrance(pair[0], player) @@ -2179,11 +2186,11 @@ def door_shuffle(world, multiworld, player, badges, badge_locs): full_interiors.remove(entrance_b) else: raise DoorShuffleException("Attempted to force connection with entrance not in any entrance pool, likely because it tried to force an entrance to connect twice.") - if multiworld.door_shuffle[player] == "decoupled": + if world.options.door_shuffle == "decoupled": dc_destinations.remove(entrance_a) dc_destinations.remove(entrance_b) - if multiworld.door_shuffle[player] == "simple": + if world.options.door_shuffle == "simple": def connect_connecting_interiors(interior_exits, exterior_entrances): for interior, exterior in zip(interior_exits, exterior_entrances): for a, b in zip(interior, exterior): @@ -2222,68 +2229,68 @@ def connect_interiors(interior_exits, exterior_entrances): single_entrance_dungeon_entrances = dungeon_entrances.copy() for i in range(2): - if not multiworld.random.randint(0, 2): + if not world.random.randint(0, 2): placed_connecting_interior_dungeons.append(multi_purpose_dungeons[i]) interior_dungeon_entrances.append([multi_purpose_dungeon_entrances[i], None]) else: placed_single_entrance_dungeons.append(multi_purpose_dungeons[i]) single_entrance_dungeon_entrances.append(multi_purpose_dungeon_entrances[i]) - multiworld.random.shuffle(placed_connecting_interior_dungeons) + world.random.shuffle(placed_connecting_interior_dungeons) while placed_connecting_interior_dungeons[0] in unsafe_connecting_interior_dungeons: - multiworld.random.shuffle(placed_connecting_interior_dungeons) + world.random.shuffle(placed_connecting_interior_dungeons) connect_connecting_interiors(placed_connecting_interior_dungeons, interior_dungeon_entrances) interiors = connecting_interiors.copy() - multiworld.random.shuffle(interiors) + world.random.shuffle(interiors) while ((connecting_interiors[2] in (interiors[2], interiors[10], interiors[11]) # Dept Store at Dept Store # or Rt 16 Gate S or N and (interiors[11] in connecting_interiors[13:17] # Saffron Gate at Rt 16 Gate S or interiors[12] in connecting_interiors[13:17])) # Saffron Gate at Rt 18 Gate and interiors[15] in connecting_interiors[13:17] # Saffron Gate at Rt 7 Gate and interiors[1] in connecting_interiors[13:17] # Saffron Gate at Rt 7-8 Underground Path - and (not multiworld.tea[player]) and multiworld.worlds[player].fly_map != "Celadon City" - and multiworld.worlds[player].town_map_fly_map != "Celadon City"): - multiworld.random.shuffle(interiors) + and (not world.options.tea) and world.fly_map != "Celadon City" + and world.town_map_fly_map != "Celadon City"): + world.random.shuffle(interiors) connect_connecting_interiors(interiors, connecting_interior_entrances) placed_gyms = gyms.copy() - multiworld.random.shuffle(placed_gyms) + world.random.shuffle(placed_gyms) # Celadon Gym requires Cut access to reach the Gym Leader. There are some scenarios where its placement # could make badge placement impossible def celadon_gym_problem(): # Badgesanity or no badges needed for HM moves means gyms can go anywhere - if multiworld.badgesanity[player] or not multiworld.badges_needed_for_hm_moves[player]: + if world.options.badgesanity or not world.options.badges_needed_for_hm_moves: return False # Celadon Gym in Pewter City and need one or more badges for Viridian City gym. # No gym leaders would be reachable. - if gyms[3] == placed_gyms[0] and multiworld.viridian_gym_condition[player] > 0: + if gyms[3] == placed_gyms[0] and world.options.viridian_gym_condition > 0: return True # Celadon Gym not on Cinnabar Island or can access Viridian City gym with one badge - if not gyms[3] == placed_gyms[6] and multiworld.viridian_gym_condition[player] > 1: + if not gyms[3] == placed_gyms[6] and world.options.viridian_gym_condition > 1: return False # At this point we need to see if we can get beyond Pewter/Cinnabar with just one badge # Can get Fly access from Pewter City gym and fly beyond Pewter/Cinnabar - if multiworld.worlds[player].fly_map not in ("Pallet Town", "Viridian City", "Cinnabar Island", - "Indigo Plateau") and multiworld.worlds[player].town_map_fly_map not in ("Pallet Town", + if world.fly_map not in ("Pallet Town", "Viridian City", "Cinnabar Island", + "Indigo Plateau") and world.town_map_fly_map not in ("Pallet Town", "Viridian City", "Cinnabar Island", "Indigo Plateau"): return False # Route 3 condition is boulder badge but Mt Moon entrance leads to safe dungeons or Rock Tunnel - if multiworld.route_3_condition[player] == "boulder_badge" and placed_connecting_interior_dungeons[2] not \ + if world.options.route_3_condition == "boulder_badge" and placed_connecting_interior_dungeons[2] not \ in (unsafe_connecting_interior_dungeons[0], unsafe_connecting_interior_dungeons[2]): return False # Route 3 condition is Defeat Brock and he is in Pewter City, or any other condition besides Boulder Badge. # Any badge can land in Pewter City, so the only problematic dungeon at Mt Moon is Seafoam Islands since # it requires two badges - if (((multiworld.route_3_condition[player] == "defeat_brock" and gyms[0] == placed_gyms[0]) - or multiworld.route_3_condition[player] not in ("defeat_brock", "boulder_badge")) + if (((world.options.route_3_condition == "defeat_brock" and gyms[0] == placed_gyms[0]) + or world.options.route_3_condition not in ("defeat_brock", "boulder_badge")) and placed_connecting_interior_dungeons[2] != unsafe_connecting_interior_dungeons[0]): return False @@ -2305,31 +2312,31 @@ def cerulean_city_problem(): and interiors[0] in connecting_interiors[13:17] # Saffron Gate at Underground Path North South and interiors[13] in connecting_interiors[13:17] # Saffron Gate at Route 5 Saffron Gate and multi_purpose_dungeons[0] == placed_connecting_interior_dungeons[4] # Pokémon Mansion at Rock Tunnel, which is - and (not multiworld.tea[player]) # not traversable backwards - and multiworld.route_3_condition[player] == "defeat_brock" - and multiworld.worlds[player].fly_map != "Cerulean City" - and multiworld.worlds[player].town_map_fly_map != "Cerulean City"): + and (not world.options.tea) # not traversable backwards + and world.options.route_3_condition == "defeat_brock" + and world.fly_map != "Cerulean City" + and world.town_map_fly_map != "Cerulean City"): return True while celadon_gym_problem() or cerulean_city_problem(): - multiworld.random.shuffle(placed_gyms) + world.random.shuffle(placed_gyms) connect_interiors(placed_gyms, gym_entrances) - multiworld.random.shuffle(placed_single_entrance_dungeons) + world.random.shuffle(placed_single_entrance_dungeons) while dungeons[4] == placed_single_entrance_dungeons[0]: # Pokémon Tower at Silph Co - multiworld.random.shuffle(placed_single_entrance_dungeons) + world.random.shuffle(placed_single_entrance_dungeons) connect_interiors(placed_single_entrance_dungeons, single_entrance_dungeon_entrances) remaining_entrances = [entrance for entrance in entrances if outdoor_map(entrance.parent_region.name)] - multiworld.random.shuffle(remaining_entrances) + world.random.shuffle(remaining_entrances) remaining_interiors = [entrance for entrance in entrances if entrance not in remaining_entrances] for entrance_a, entrance_b in zip(remaining_entrances, remaining_interiors): entrance_a.connect(entrance_b) entrance_b.connect(entrance_a) - elif multiworld.door_shuffle[player]: - if multiworld.door_shuffle[player] == "full": - multiworld.random.shuffle(full_interiors) + elif world.options.door_shuffle: + if world.options.door_shuffle == "full": + world.random.shuffle(full_interiors) def search_for_exit(entrance, region, checked_regions): checked_regions.add(region) @@ -2344,6 +2351,7 @@ def search_for_exit(entrance, region, checked_regions): return found_exit return None + e = multiworld.get_entrance("Underground Path Route 5 to Underground Path North South", player) while True: for entrance_a in full_interiors: if search_for_exit(entrance_a, entrance_a.parent_region, set()) is None: @@ -2363,7 +2371,7 @@ def search_for_exit(entrance, region, checked_regions): break loop_out_interiors = [] - multiworld.random.shuffle(entrances) + world.random.shuffle(entrances) for entrance in reversed(entrances): if not outdoor_map(entrance.parent_region.name): found_exit = search_for_exit(entrance, entrance.parent_region, set()) @@ -2380,26 +2388,26 @@ def search_for_exit(entrance, region, checked_regions): entrance_a.connect(entrance_b) entrance_b.connect(entrance_a) - elif multiworld.door_shuffle[player] == "interiors": + elif world.options.door_shuffle == "interiors": loop_out_interiors = [[multiworld.get_entrance(e[0], player), multiworld.get_entrance(e[1], player)] for e - in multiworld.random.sample(unsafe_connecting_interior_dungeons - + safe_connecting_interior_dungeons, 2)] + in world.random.sample(unsafe_connecting_interior_dungeons + + safe_connecting_interior_dungeons, 2)] entrances.remove(loop_out_interiors[0][1]) entrances.remove(loop_out_interiors[1][1]) - if not multiworld.badgesanity[player]: - multiworld.random.shuffle(badges) - while badges[3].name == "Cascade Badge" and multiworld.badges_needed_for_hm_moves[player]: - multiworld.random.shuffle(badges) + if not world.options.badgesanity: + world.random.shuffle(badges) + while badges[3].name == "Cascade Badge" and world.options.badges_needed_for_hm_moves: + world.random.shuffle(badges) for badge, loc in zip(badges, badge_locs): loc.place_locked_item(badge) state = multiworld.state.copy() for item, data in item_table.items(): if (data.id or item in poke_data.pokemon_data) and data.classification == ItemClassification.progression \ - and ("Badge" not in item or multiworld.badgesanity[player]): + and ("Badge" not in item or world.options.badgesanity): state.collect(world.create_item(item)) - multiworld.random.shuffle(entrances) + world.random.shuffle(entrances) reachable_entrances = [] relevant_events = [ @@ -2415,13 +2423,13 @@ def search_for_exit(entrance, region, checked_regions): "Victory Road Boulder", "Silph Co Liberated", ] - if multiworld.robbed_house_officer[player]: + if world.options.robbed_house_officer: relevant_events.append("Help Bill") - if multiworld.tea[player]: + if world.options.tea: relevant_events.append("Vending Machine Drinks") - if multiworld.route_3_condition[player] == "defeat_brock": + if world.options.route_3_condition == "defeat_brock": relevant_events.append("Defeat Brock") - elif multiworld.route_3_condition[player] == "defeat_any_gym": + elif world.options.route_3_condition == "defeat_any_gym": relevant_events += [ "Defeat Brock", "Defeat Misty", @@ -2447,7 +2455,7 @@ def adds_reachable_entrances(item): def dead_end(e): if e.can_reach(state): return True - elif multiworld.door_shuffle[player] == "decoupled": + elif world.options.door_shuffle == "decoupled": # Any unreachable exit in decoupled is not a dead end return False region = e.parent_region @@ -2482,10 +2490,10 @@ def dead_end(e): state.update_reachable_regions(player) state.sweep_for_advancements(locations=event_locations) - multiworld.random.shuffle(entrances) + world.random.shuffle(entrances) - if multiworld.door_shuffle[player] == "decoupled": - multiworld.random.shuffle(dc_destinations) + if world.options.door_shuffle == "decoupled": + world.random.shuffle(dc_destinations) else: entrances.sort(key=lambda e: e.name not in entrance_only) @@ -2502,15 +2510,15 @@ def dead_end(e): is_outdoor_map = outdoor_map(entrance_a.parent_region.name) - if multiworld.door_shuffle[player] in ("interiors", "full") or len(entrances) != len(reachable_entrances): + if world.options.door_shuffle in ("interiors", "full") or len(entrances) != len(reachable_entrances): find_dead_end = False if (len(reachable_entrances) > - (1 if multiworld.door_shuffle[player] in ("insanity", "decoupled") else 8) and len(entrances) + (1 if world.options.door_shuffle in ("insanity", "decoupled") else 8) and len(entrances) <= (starting_entrances - 3)): find_dead_end = True - if (multiworld.door_shuffle[player] in ("interiors", "full") and len(entrances) < 48 + if (world.options.door_shuffle in ("interiors", "full") and len(entrances) < 48 and not is_outdoor_map): # Try to prevent a situation where the only remaining outdoor entrances are ones that cannot be # reached except by connecting directly to it. @@ -2519,9 +2527,9 @@ def dead_end(e): in reachable_entrances if not outdoor_map(entrance.parent_region.name)]) > 1: find_dead_end = True - if multiworld.door_shuffle[player] == "decoupled": + if world.options.door_shuffle == "decoupled": destinations = dc_destinations - elif multiworld.door_shuffle[player] in ("interiors", "full"): + elif world.options.door_shuffle in ("interiors", "full"): destinations = [entrance for entrance in entrances if outdoor_map(entrance.parent_region.name) is not is_outdoor_map] if not destinations: @@ -2531,7 +2539,7 @@ def dead_end(e): destinations.sort(key=lambda e: e == entrance_a) for entrance in destinations: - if (dead_end(entrance) is find_dead_end and (multiworld.door_shuffle[player] != "decoupled" + if (dead_end(entrance) is find_dead_end and (world.options.door_shuffle != "decoupled" or entrance.parent_region.name.split("-")[0] != entrance_a.parent_region.name.split("-")[0])): entrance_b = entrance @@ -2540,28 +2548,28 @@ def dead_end(e): else: entrance_b = destinations.pop(0) - if multiworld.door_shuffle[player] in ("interiors", "full"): + if world.options.door_shuffle in ("interiors", "full"): # on Interiors/Full, the destinations variable does not point to the entrances list, so we need to # remove from that list here. entrances.remove(entrance_b) else: # Everything is reachable. Just start connecting the rest of the doors at random. - if multiworld.door_shuffle[player] == "decoupled": + if world.options.door_shuffle == "decoupled": entrance_b = dc_destinations.pop(0) else: entrance_b = entrances.pop(0) entrance_a.connect(entrance_b) - if multiworld.door_shuffle[player] != "decoupled": + if world.options.door_shuffle != "decoupled": entrance_b.connect(entrance_a) - if multiworld.door_shuffle[player] in ("interiors", "full"): + if world.options.door_shuffle in ("interiors", "full"): for pair in loop_out_interiors: pair[1].connected_region = pair[0].connected_region pair[1].parent_region.entrances.append(pair[0]) pair[1].target = pair[0].target - if multiworld.door_shuffle[player]: + if world.options.door_shuffle: for region in multiworld.get_regions(player): checked_regions = {region} @@ -2585,10 +2593,10 @@ def check_region(region_to_check): region.entrance_hint = check_region(region) -def connect(world: MultiWorld, player: int, source: str, target: str, rule: callable = lambda state: True, +def connect(multiworld: MultiWorld, player: int, source: str, target: str, rule: callable = lambda state: True, one_way=False, name=None): - source_region = world.get_region(source, player) - target_region = world.get_region(target, player) + source_region = multiworld.get_region(source, player) + target_region = multiworld.get_region(target, player) if name is None: name = source + " to " + target @@ -2604,7 +2612,7 @@ def connect(world: MultiWorld, player: int, source: str, target: str, rule: call source_region.exits.append(connection) connection.connect(target_region) if not one_way: - connect(world, player, target, source, rule, True) + connect(multiworld, player, target, source, rule, True) class PokemonRBWarp(Entrance): @@ -2621,7 +2629,7 @@ def access_rule(self, state): if self.connected_region is None: return False if "Elevator" in self.parent_region.name and ( - (state.multiworld.all_elevators_locked[self.player] + (state.multiworld.worlds[self.player].options.all_elevators_locked or "Rocket Hideout" in self.parent_region.name) and not state.has("Lift Key", self.player)): return False diff --git a/worlds/pokemon_rb/rom.py b/worlds/pokemon_rb/rom.py index b6c1221a29f4..5ebd204c9abc 100644 --- a/worlds/pokemon_rb/rom.py +++ b/worlds/pokemon_rb/rom.py @@ -13,22 +13,22 @@ from . import poke_data -def write_quizzes(self, data, random): +def write_quizzes(world, data, random): def get_quiz(q, a): if q == 0: r = random.randint(0, 3) if r == 0: - mon = self.trade_mons["Trade_Dux"] + mon = world.trade_mons["Trade_Dux"] text = "A woman inVermilion City" elif r == 1: - mon = self.trade_mons["Trade_Lola"] + mon = world.trade_mons["Trade_Lola"] text = "A man inCerulean City" elif r == 2: - mon = self.trade_mons["Trade_Marcel"] + mon = world.trade_mons["Trade_Marcel"] text = "Someone on Route 2" elif r == 3: - mon = self.trade_mons["Trade_Spot"] + mon = world.trade_mons["Trade_Spot"] text = "Someone on Route 5" if not a: answers.append(0) @@ -38,21 +38,30 @@ def get_quiz(q, a): return encode_text(f"{text}was looking for{mon}?") elif q == 1: - for location in self.multiworld.get_filled_locations(): - if location.item.name == "Secret Key" and location.item.player == self.player: + for location in world.multiworld.get_filled_locations(): + if location.item.name == "Secret Key" and location.item.player == world.player: break - player_name = self.multiworld.player_name[location.player] + player_name = world.multiworld.player_name[location.player] if not a: - if len(self.multiworld.player_name) > 1: + if len(world.multiworld.player_name) > 1: old_name = player_name while old_name == player_name: - player_name = random.choice(list(self.multiworld.player_name.values())) + player_name = random.choice(list(world.multiworld.player_name.values())) else: return encode_text("You're playingin a multiworldwith otherplayers?") - if player_name == self.multiworld.player_name[self.player]: - player_name = "yourself" - player_name = encode_text(player_name, force=True, safety=True) - return encode_text(f"The Secret Key wasfound by") + player_name + encode_text("") + if world.multiworld.get_entrance( + "Cinnabar Island-G to Cinnabar Gym", world.player).connected_region.name == "Cinnabar Gym": + if player_name == world.multiworld.player_name[world.player]: + player_name = "yourself" + player_name = encode_text(player_name, force=True, safety=True) + return encode_text(f"The Secret Key wasfound by") + player_name + encode_text("?") + else: + # Might not have found it yet + if player_name == world.multiworld.player_name[world.player]: + return encode_text(f"The Secret Key wasplaced inyour own world?") + player_name = encode_text(player_name, force=True, safety=True) + return (encode_text(f"The Secret Key wasplaced in") + player_name + + encode_text("'sworld?")) elif q == 2: if a: return encode_text(f"#mon ispronouncedPo-kay-mon?") @@ -62,8 +71,8 @@ def get_quiz(q, a): else: return encode_text(f"#mon ispronouncedPo-kuh-mon?") elif q == 3: - starters = [" ".join(self.multiworld.get_location( - f"Oak's Lab - Starter {i}", self.player).item.name.split(" ")[1:]) for i in range(1, 4)] + starters = [" ".join(world.multiworld.get_location( + f"Oak's Lab - Starter {i}", world.player).item.name.split(" ")[1:]) for i in range(1, 4)] mon = random.choice(starters) nots = random.choice(range(8, 16, 2)) if random.randint(0, 1): @@ -82,10 +91,10 @@ def get_quiz(q, a): return encode_text(text) elif q == 4: if a: - tm_text = self.local_tms[27] + tm_text = world.local_tms[27] else: - if self.multiworld.randomize_tm_moves[self.player]: - wrong_tms = self.local_tms.copy() + if world.options.randomize_tm_moves: + wrong_tms = world.local_tms.copy() wrong_tms.pop(27) tm_text = random.choice(wrong_tms) else: @@ -102,12 +111,36 @@ def get_quiz(q, a): i = random.randint(0, random.choice([9, 99])) return encode_text(f"POLIWAG evolves {i}times?") elif q == 7: - entity = "Motor Carrier" - if not a: - entity = random.choice(["Driver", "Shipper"]) - return encode_text("Title 49 of theU.S. Code ofFederalRegulations part397.67 states" - f"that the{entity}is responsiblefor planningroutes when" - "hazardousmaterials aretransported?") + q2 = random.randint(0, 2) + if q2 == 0: + entity = "Motor Carrier" + if not a: + entity = random.choice(["Driver", "Shipper"]) + return encode_text("Title 49 of theU.S. Code ofFederalRegulations part397.67 " + f"statesthat the{entity}is responsiblefor planning" + "routes whenhazardousmaterials aretransported?") + elif q2 == 1: + if a: + state = random.choice( + ['Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 'Colorado', 'Connecticut', + 'Delaware', 'Florida', 'Georgia', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas', + 'Kentucky', 'Louisiana', 'Maine', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota', + 'Mississippi', 'Missouri', 'Montana', 'Nebraska', 'Nevada', 'New Jersey', 'New Mexico', + 'New York', 'North Carolina', 'North Dakota', 'Ohio', 'Oklahoma', 'Oregon', 'Pennsylvania', + 'Rhode Island', 'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont', + 'Virginia', 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming']) + else: + state = "New Hampshire" + return encode_text( + f"As of 2024,{state}has a lawrequiring allfront seat vehicleoccupants to useseatbelts?") + elif q2 == 2: + if a: + country = random.choice(["The United States", "Mexico", "Canada", "Germany", "France", "China", + "Russia", "Spain", "Brazil", "Ukraine", "Saudi Arabia", "Egypt"]) + else: + country = random.choice(["The U.K.", "Pakistan", "India", "Japan", "Australia", + "New Zealand", "Thailand"]) + return encode_text(f"As of 2020,drivers in{country}drive on theright side ofthe road?") elif q == 8: mon = random.choice(list(poke_data.evolution_levels.keys())) level = poke_data.evolution_levels[mon] @@ -115,17 +148,17 @@ def get_quiz(q, a): level += random.choice(range(1, 6)) * random.choice((-1, 1)) return encode_text(f"{mon} evolvesat level {level}?") elif q == 9: - move = random.choice(list(self.local_move_data.keys())) - actual_type = self.local_move_data[move]["type"] + move = random.choice(list(world.local_move_data.keys())) + actual_type = world.local_move_data[move]["type"] question_type = actual_type while question_type == actual_type and not a: question_type = random.choice(list(poke_data.type_ids.keys())) return encode_text(f"{move} is{question_type} type?") elif q == 10: mon = random.choice(list(poke_data.pokemon_data.keys())) - actual_type = self.local_poke_data[mon][random.choice(("type1", "type2"))] + actual_type = world.local_poke_data[mon][random.choice(("type1", "type2"))] question_type = actual_type - while question_type in [self.local_poke_data[mon]["type1"], self.local_poke_data[mon]["type2"]] and not a: + while question_type in [world.local_poke_data[mon]["type1"], world.local_poke_data[mon]["type2"]] and not a: question_type = random.choice(list(poke_data.type_ids.keys())) return encode_text(f"{mon} is{question_type} type?") elif q == 11: @@ -147,8 +180,8 @@ def get_quiz(q, a): return encode_text(f"{equation}= {question_result}?") elif q == 12: route = random.choice((12, 16)) - actual_mon = self.multiworld.get_location(f"Route {route} - Sleeping Pokemon", - self.player).item.name.split("Static ")[1] + actual_mon = world.multiworld.get_location(f"Route {route} - Sleeping Pokemon", + world.player).item.name.split("Static ")[1] question_mon = actual_mon while question_mon == actual_mon and not a: question_mon = random.choice(list(poke_data.pokemon_data.keys())) @@ -157,7 +190,7 @@ def get_quiz(q, a): type1 = random.choice(list(poke_data.type_ids.keys())) type2 = random.choice(list(poke_data.type_ids.keys())) eff_msgs = ["super effective", "no ", "not veryeffective", "normal "] - for matchup in self.type_chart: + for matchup in world.type_chart: if matchup[0] == type1 and matchup[1] == type2: if matchup[2] > 10: eff = eff_msgs[0] @@ -175,15 +208,25 @@ def get_quiz(q, a): eff = random.choice(eff_msgs) return encode_text(f"{type1} deals{eff}damage to{type2} type?") elif q == 14: - fossil_level = self.multiworld.get_location("Fossil Level - Trainer Parties", - self.player).party_data[0]['level'] + fossil_level = world.multiworld.get_location("Fossil Level - Trainer Parties", + world.player).party_data[0]['level'] if not a: fossil_level += random.choice((-5, 5)) return encode_text(f"Fossil #MONrevive at level{fossil_level}?") + elif q == 15: + if a: + fodmap = random.choice(["garlic", "onion", "milk", "watermelon", "cherries", "wheat", "barley", + "pistachios", "cashews", "kidney beans", "apples", "honey"]) + else: + fodmap = random.choice(["carrots", "potatoes", "oranges", "pineapple", "blueberries", "parmesan", + "eggs", "beef", "chicken", "oat", "rice", "maple syrup", "peanuts"]) + are_is = "are" if fodmap[-1] == "s" else "is" + return encode_text(f"According toMonash Uni.,{fodmap} {are_is}considered highin FODMAPs?") answers = [random.randint(0, 1) for _ in range(6)] - questions = random.sample((range(0, 15)), 6) + questions = random.sample((range(0, 16)), 6) + question_texts = [] for i, question in enumerate(questions): question_texts.append(get_quiz(question, answers[i])) @@ -193,9 +236,9 @@ def get_quiz(q, a): write_bytes(data, question_texts[i], rom_addresses[f"Text_Quiz_{quiz}"]) -def generate_output(self, output_directory: str): - random = self.multiworld.per_slot_randoms[self.player] - game_version = self.multiworld.game_version[self.player].current_key +def generate_output(world, output_directory: str): + random = world.random + game_version = world.options.game_version.current_key data = bytes(get_base_rom_bytes(game_version)) base_patch = pkgutil.get_data(__name__, f'basepatch_{game_version}.bsdiff4') @@ -205,8 +248,8 @@ def generate_output(self, output_directory: str): basemd5 = hashlib.md5() basemd5.update(data) - pallet_connections = {entrance: self.multiworld.get_entrance(f"Pallet Town to {entrance}", - self.player).connected_region.name for + pallet_connections = {entrance: world.multiworld.get_entrance(f"Pallet Town to {entrance}", + world.player).connected_region.name for entrance in ["Player's House 1F", "Oak's Lab", "Rival's House"]} paths = None @@ -222,11 +265,11 @@ def generate_output(self, output_directory: str): elif pallet_connections["Oak's Lab"] == "Player's House 1F": write_bytes(data, [0x5F, 0xC7, 0x0C, 0x0C, 0x00, 0x00], rom_addresses["Pallet_Fly_Coords"]) - for region in self.multiworld.get_regions(self.player): + for region in world.multiworld.get_regions(world.player): for entrance in region.exits: if isinstance(entrance, PokemonRBWarp): - self.multiworld.spoiler.set_entrance(entrance.name, entrance.connected_region.name, "entrance", - self.player) + world.multiworld.spoiler.set_entrance(entrance.name, entrance.connected_region.name, "entrance", + world.player) warp_ids = (entrance.warp_id,) if isinstance(entrance.warp_id, int) else entrance.warp_id warp_to_ids = (entrance.target,) if isinstance(entrance.target, int) else entrance.target for i, warp_id in enumerate(warp_ids): @@ -241,32 +284,32 @@ def generate_output(self, output_directory: str): data[address] = 0 if "Elevator" in connected_map_name else warp_to_ids[i] data[address + 1] = map_ids[connected_map_name] - if self.multiworld.door_shuffle[self.player] == "simple": + if world.options.door_shuffle == "simple": for (entrance, _, _, map_coords_entries, map_name, _) in town_map_coords.values(): - destination = self.multiworld.get_entrance(entrance, self.player).connected_region.name + destination = world.multiworld.get_entrance(entrance, world.player).connected_region.name (_, x, y, _, _, map_order_entry) = town_map_coords[destination] for map_coord_entry in map_coords_entries: data[rom_addresses["Town_Map_Coords"] + (map_coord_entry * 4) + 1] = (y << 4) | x data[rom_addresses["Town_Map_Order"] + map_order_entry] = map_ids[map_name] - if not self.multiworld.key_items_only[self.player]: + if not world.options.key_items_only: for i, gym_leader in enumerate(("Pewter Gym - Brock TM", "Cerulean Gym - Misty TM", "Vermilion Gym - Lt. Surge TM", "Celadon Gym - Erika TM", "Fuchsia Gym - Koga TM", "Saffron Gym - Sabrina TM", "Cinnabar Gym - Blaine TM", "Viridian Gym - Giovanni TM")): - item_name = self.multiworld.get_location(gym_leader, self.player).item.name + item_name = world.multiworld.get_location(gym_leader, world.player).item.name if item_name.startswith("TM"): try: tm = int(item_name[2:4]) - move = poke_data.moves[self.local_tms[tm - 1]]["id"] + move = poke_data.moves[world.local_tms[tm - 1]]["id"] data[rom_addresses["Gym_Leader_Moves"] + (2 * i)] = move except KeyError: pass def set_trade_mon(address, loc): - mon = self.multiworld.get_location(loc, self.player).item.name + mon = world.multiworld.get_location(loc, world.player).item.name data[rom_addresses[address]] = poke_data.pokemon_data[mon]["id"] - self.trade_mons[address] = mon + world.trade_mons[address] = mon if game_version == "red": set_trade_mon("Trade_Terry", "Safari Zone Center - Wild Pokemon - 5") @@ -282,10 +325,10 @@ def set_trade_mon(address, loc): set_trade_mon("Trade_Doris", "Cerulean Cave 1F - Wild Pokemon - 9") set_trade_mon("Trade_Crinkles", "Route 12 - Wild Pokemon - 4") - data[rom_addresses['Fly_Location']] = self.fly_map_code - data[rom_addresses['Map_Fly_Location']] = self.town_map_fly_map_code + data[rom_addresses['Fly_Location']] = world.fly_map_code + data[rom_addresses['Map_Fly_Location']] = world.town_map_fly_map_code - if self.multiworld.fix_combat_bugs[self.player]: + if world.options.fix_combat_bugs: data[rom_addresses["Option_Fix_Combat_Bugs"]] = 1 data[rom_addresses["Option_Fix_Combat_Bugs_Focus_Energy"]] = 0x28 # jr z data[rom_addresses["Option_Fix_Combat_Bugs_HP_Drain_Dream_Eater"]] = 0x1A # ld a, (de) @@ -298,25 +341,25 @@ def set_trade_mon(address, loc): data[rom_addresses["Option_Fix_Combat_Bugs_Heal_Effect"] + 1] = 5 # 5 bytes ahead data[rom_addresses["Option_Fix_Combat_Bugs_Heal_Stat_Modifiers"]] = 1 - if self.multiworld.poke_doll_skip[self.player] == "in_logic": + if world.options.poke_doll_skip == "in_logic": data[rom_addresses["Option_Silph_Scope_Skip"]] = 0x00 # nop data[rom_addresses["Option_Silph_Scope_Skip"] + 1] = 0x00 # nop data[rom_addresses["Option_Silph_Scope_Skip"] + 2] = 0x00 # nop - if self.multiworld.bicycle_gate_skips[self.player] == "patched": + if world.options.bicycle_gate_skips == "patched": data[rom_addresses["Option_Route_16_Gate_Fix"]] = 0x00 # nop data[rom_addresses["Option_Route_16_Gate_Fix"] + 1] = 0x00 # nop data[rom_addresses["Option_Route_18_Gate_Fix"]] = 0x00 # nop data[rom_addresses["Option_Route_18_Gate_Fix"] + 1] = 0x00 # nop - if self.multiworld.door_shuffle[self.player]: + if world.options.door_shuffle: data[rom_addresses["Entrance_Shuffle_Fuji_Warp"]] = 1 # prevent warping to Fuji's House from Pokemon Tower 7F - if self.multiworld.all_elevators_locked[self.player]: + if world.options.all_elevators_locked: data[rom_addresses["Option_Locked_Elevator_Celadon"]] = 0x20 # jr nz data[rom_addresses["Option_Locked_Elevator_Silph"]] = 0x20 # jr nz - if self.multiworld.tea[self.player].value: + if world.options.tea: data[rom_addresses["Option_Tea"]] = 1 data[rom_addresses["Guard_Drink_List"]] = 0x54 data[rom_addresses["Guard_Drink_List"] + 1] = 0 @@ -325,90 +368,94 @@ def set_trade_mon(address, loc): "Oh wait there,the road's closed."), rom_addresses["Text_Saffron_Gate"]) + data[rom_addresses["Tea_Key_Item_A"]] = 0x28 # jr .z + data[rom_addresses["Tea_Key_Item_B"]] = 0x28 # jr .z + data[rom_addresses["Tea_Key_Item_C"]] = 0x28 # jr .z + data[rom_addresses["Fossils_Needed_For_Second_Item"]] = ( - self.multiworld.second_fossil_check_condition[self.player].value) + world.options.second_fossil_check_condition.value) - data[rom_addresses["Option_Lose_Money"]] = int(not self.multiworld.lose_money_on_blackout[self.player].value) + data[rom_addresses["Option_Lose_Money"]] = int(not world.options.lose_money_on_blackout.value) - if self.multiworld.extra_key_items[self.player]: + if world.options.extra_key_items: data[rom_addresses['Option_Extra_Key_Items_A']] = 1 data[rom_addresses['Option_Extra_Key_Items_B']] = 1 data[rom_addresses['Option_Extra_Key_Items_C']] = 1 data[rom_addresses['Option_Extra_Key_Items_D']] = 1 - data[rom_addresses["Option_Split_Card_Key"]] = self.multiworld.split_card_key[self.player].value - data[rom_addresses["Option_Blind_Trainers"]] = round(self.multiworld.blind_trainers[self.player].value * 2.55) - data[rom_addresses["Option_Cerulean_Cave_Badges"]] = self.multiworld.cerulean_cave_badges_condition[self.player].value - data[rom_addresses["Option_Cerulean_Cave_Key_Items"]] = self.multiworld.cerulean_cave_key_items_condition[self.player].total - write_bytes(data, encode_text(str(self.multiworld.cerulean_cave_badges_condition[self.player].value)), rom_addresses["Text_Cerulean_Cave_Badges"]) - write_bytes(data, encode_text(str(self.multiworld.cerulean_cave_key_items_condition[self.player].total) + " key items."), rom_addresses["Text_Cerulean_Cave_Key_Items"]) - data[rom_addresses['Option_Encounter_Minimum_Steps']] = self.multiworld.minimum_steps_between_encounters[self.player].value - data[rom_addresses['Option_Route23_Badges']] = self.multiworld.victory_road_condition[self.player].value - data[rom_addresses['Option_Victory_Road_Badges']] = self.multiworld.route_22_gate_condition[self.player].value - data[rom_addresses['Option_Elite_Four_Pokedex']] = self.multiworld.elite_four_pokedex_condition[self.player].total - data[rom_addresses['Option_Elite_Four_Key_Items']] = self.multiworld.elite_four_key_items_condition[self.player].total - data[rom_addresses['Option_Elite_Four_Badges']] = self.multiworld.elite_four_badges_condition[self.player].value - write_bytes(data, encode_text(str(self.multiworld.elite_four_badges_condition[self.player].value)), rom_addresses["Text_Elite_Four_Badges"]) - write_bytes(data, encode_text(str(self.multiworld.elite_four_key_items_condition[self.player].total) + " key items, and"), rom_addresses["Text_Elite_Four_Key_Items"]) - write_bytes(data, encode_text(str(self.multiworld.elite_four_pokedex_condition[self.player].total) + " #MON"), rom_addresses["Text_Elite_Four_Pokedex"]) - write_bytes(data, encode_text(str(self.total_key_items), length=2), rom_addresses["Trainer_Screen_Total_Key_Items"]) - - data[rom_addresses['Option_Viridian_Gym_Badges']] = self.multiworld.viridian_gym_condition[self.player].value - data[rom_addresses['Option_EXP_Modifier']] = self.multiworld.exp_modifier[self.player].value - if not self.multiworld.require_item_finder[self.player]: + data[rom_addresses["Option_Split_Card_Key"]] = world.options.split_card_key.value + data[rom_addresses["Option_Blind_Trainers"]] = round(world.options.blind_trainers.value * 2.55) + data[rom_addresses["Option_Cerulean_Cave_Badges"]] = world.options.cerulean_cave_badges_condition.value + data[rom_addresses["Option_Cerulean_Cave_Key_Items"]] = world.options.cerulean_cave_key_items_condition.total + write_bytes(data, encode_text(str(world.options.cerulean_cave_badges_condition.value)), rom_addresses["Text_Cerulean_Cave_Badges"]) + write_bytes(data, encode_text(str(world.options.cerulean_cave_key_items_condition.total) + " key items."), rom_addresses["Text_Cerulean_Cave_Key_Items"]) + data[rom_addresses['Option_Encounter_Minimum_Steps']] = world.options.minimum_steps_between_encounters.value + data[rom_addresses['Option_Route23_Badges']] = world.options.victory_road_condition.value + data[rom_addresses['Option_Victory_Road_Badges']] = world.options.route_22_gate_condition.value + data[rom_addresses['Option_Elite_Four_Pokedex']] = world.options.elite_four_pokedex_condition.total + data[rom_addresses['Option_Elite_Four_Key_Items']] = world.options.elite_four_key_items_condition.total + data[rom_addresses['Option_Elite_Four_Badges']] = world.options.elite_four_badges_condition.value + write_bytes(data, encode_text(str(world.options.elite_four_badges_condition.value)), rom_addresses["Text_Elite_Four_Badges"]) + write_bytes(data, encode_text(str(world.options.elite_four_key_items_condition.total) + " key items, and"), rom_addresses["Text_Elite_Four_Key_Items"]) + write_bytes(data, encode_text(str(world.options.elite_four_pokedex_condition.total) + " #MON"), rom_addresses["Text_Elite_Four_Pokedex"]) + write_bytes(data, encode_text(str(world.total_key_items), length=2), rom_addresses["Trainer_Screen_Total_Key_Items"]) + + data[rom_addresses['Option_Viridian_Gym_Badges']] = world.options.viridian_gym_condition.value + data[rom_addresses['Option_EXP_Modifier']] = world.options.exp_modifier.value + if not world.options.require_item_finder: data[rom_addresses['Option_Itemfinder']] = 0 # nop - if self.multiworld.extra_strength_boulders[self.player]: + if world.options.extra_strength_boulders: for i in range(0, 3): data[rom_addresses['Option_Boulders'] + (i * 3)] = 0x15 - if self.multiworld.extra_key_items[self.player]: + if world.options.extra_key_items: for i in range(0, 4): data[rom_addresses['Option_Rock_Tunnel_Extra_Items'] + (i * 3)] = 0x15 - if self.multiworld.old_man[self.player] == "open_viridian_city": + if world.options.old_man == "open_viridian_city": data[rom_addresses['Option_Old_Man']] = 0x11 data[rom_addresses['Option_Old_Man_Lying']] = 0x15 - data[rom_addresses['Option_Route3_Guard_B']] = self.multiworld.route_3_condition[self.player].value - if self.multiworld.route_3_condition[self.player] == "open": + data[rom_addresses['Option_Route3_Guard_B']] = world.options.route_3_condition.value + if world.options.route_3_condition == "open": data[rom_addresses['Option_Route3_Guard_A']] = 0x11 - if not self.multiworld.robbed_house_officer[self.player]: + if not world.options.robbed_house_officer: data[rom_addresses['Option_Trashed_House_Guard_A']] = 0x15 data[rom_addresses['Option_Trashed_House_Guard_B']] = 0x11 - if self.multiworld.require_pokedex[self.player]: + if world.options.require_pokedex: data[rom_addresses["Require_Pokedex_A"]] = 1 data[rom_addresses["Require_Pokedex_B"]] = 1 data[rom_addresses["Require_Pokedex_C"]] = 1 else: data[rom_addresses["Require_Pokedex_D"]] = 0x18 # jr - if self.multiworld.dexsanity[self.player]: + if world.options.dexsanity: data[rom_addresses["Option_Dexsanity_A"]] = 1 data[rom_addresses["Option_Dexsanity_B"]] = 1 - if self.multiworld.all_pokemon_seen[self.player]: + if world.options.all_pokemon_seen: data[rom_addresses["Option_Pokedex_Seen"]] = 1 - money = str(self.multiworld.starting_money[self.player].value).zfill(6) + money = str(world.options.starting_money.value).zfill(6) data[rom_addresses["Starting_Money_High"]] = int(money[:2], 16) data[rom_addresses["Starting_Money_Middle"]] = int(money[2:4], 16) data[rom_addresses["Starting_Money_Low"]] = int(money[4:], 16) data[rom_addresses["Text_Badges_Needed_Viridian_Gym"]] = encode_text( - str(self.multiworld.viridian_gym_condition[self.player].value))[0] + str(world.options.viridian_gym_condition.value))[0] data[rom_addresses["Text_Rt23_Badges_A"]] = encode_text( - str(self.multiworld.victory_road_condition[self.player].value))[0] + str(world.options.victory_road_condition.value))[0] data[rom_addresses["Text_Rt23_Badges_B"]] = encode_text( - str(self.multiworld.victory_road_condition[self.player].value))[0] + str(world.options.victory_road_condition.value))[0] data[rom_addresses["Text_Rt23_Badges_C"]] = encode_text( - str(self.multiworld.victory_road_condition[self.player].value))[0] + str(world.options.victory_road_condition.value))[0] data[rom_addresses["Text_Rt23_Badges_D"]] = encode_text( - str(self.multiworld.victory_road_condition[self.player].value))[0] + str(world.options.victory_road_condition.value))[0] data[rom_addresses["Text_Badges_Needed"]] = encode_text( - str(self.multiworld.elite_four_badges_condition[self.player].value))[0] + str(world.options.elite_four_badges_condition.value))[0] write_bytes(data, encode_text( - " ".join(self.multiworld.get_location("Route 4 Pokemon Center - Pokemon For Sale", self.player).item.name.upper().split()[1:])), + " ".join(world.multiworld.get_location("Route 4 Pokemon Center - Pokemon For Sale", world.player).item.name.upper().split()[1:])), rom_addresses["Text_Magikarp_Salesman"]) - if self.multiworld.badges_needed_for_hm_moves[self.player].value == 0: + if world.options.badges_needed_for_hm_moves.value == 0: for hm_move in poke_data.hm_moves: write_bytes(data, bytearray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), rom_addresses["HM_" + hm_move + "_Badge_a"]) - elif self.extra_badges: + elif world.extra_badges: written_badges = {} - for hm_move, badge in self.extra_badges.items(): + for hm_move, badge in world.extra_badges.items(): data[rom_addresses["HM_" + hm_move + "_Badge_b"]] = {"Boulder Badge": 0x47, "Cascade Badge": 0x4F, "Thunder Badge": 0x57, "Rainbow Badge": 0x5F, "Soul Badge": 0x67, "Marsh Badge": 0x6F, @@ -427,7 +474,7 @@ def set_trade_mon(address, loc): write_bytes(data, encode_text("Nothing"), rom_addresses["Badge_Text_" + badge.replace(" ", "_")]) type_loc = rom_addresses["Type_Chart"] - for matchup in self.type_chart: + for matchup in world.type_chart: if matchup[2] != 10: # don't needlessly divide damage by 10 and multiply by 10 data[type_loc] = poke_data.type_ids[matchup[0]] data[type_loc + 1] = poke_data.type_ids[matchup[1]] @@ -437,52 +484,49 @@ def set_trade_mon(address, loc): data[type_loc + 1] = 0xFF data[type_loc + 2] = 0xFF - if self.multiworld.normalize_encounter_chances[self.player].value: + if world.options.normalize_encounter_chances.value: chances = [25, 51, 77, 103, 129, 155, 180, 205, 230, 255] for i, chance in enumerate(chances): data[rom_addresses['Encounter_Chances'] + (i * 2)] = chance - for mon, mon_data in self.local_poke_data.items(): + for mon, mon_data in world.local_poke_data.items(): if mon == "Mew": address = rom_addresses["Base_Stats_Mew"] else: address = rom_addresses["Base_Stats"] + (28 * (mon_data["dex"] - 1)) - data[address + 1] = self.local_poke_data[mon]["hp"] - data[address + 2] = self.local_poke_data[mon]["atk"] - data[address + 3] = self.local_poke_data[mon]["def"] - data[address + 4] = self.local_poke_data[mon]["spd"] - data[address + 5] = self.local_poke_data[mon]["spc"] - data[address + 6] = poke_data.type_ids[self.local_poke_data[mon]["type1"]] - data[address + 7] = poke_data.type_ids[self.local_poke_data[mon]["type2"]] - data[address + 8] = self.local_poke_data[mon]["catch rate"] - data[address + 15] = poke_data.moves[self.local_poke_data[mon]["start move 1"]]["id"] - data[address + 16] = poke_data.moves[self.local_poke_data[mon]["start move 2"]]["id"] - data[address + 17] = poke_data.moves[self.local_poke_data[mon]["start move 3"]]["id"] - data[address + 18] = poke_data.moves[self.local_poke_data[mon]["start move 4"]]["id"] - write_bytes(data, self.local_poke_data[mon]["tms"], address + 20) - if mon in self.learnsets and self.learnsets[mon]: + data[address + 1] = world.local_poke_data[mon]["hp"] + data[address + 2] = world.local_poke_data[mon]["atk"] + data[address + 3] = world.local_poke_data[mon]["def"] + data[address + 4] = world.local_poke_data[mon]["spd"] + data[address + 5] = world.local_poke_data[mon]["spc"] + data[address + 6] = poke_data.type_ids[world.local_poke_data[mon]["type1"]] + data[address + 7] = poke_data.type_ids[world.local_poke_data[mon]["type2"]] + data[address + 8] = world.local_poke_data[mon]["catch rate"] + data[address + 15] = poke_data.moves[world.local_poke_data[mon]["start move 1"]]["id"] + data[address + 16] = poke_data.moves[world.local_poke_data[mon]["start move 2"]]["id"] + data[address + 17] = poke_data.moves[world.local_poke_data[mon]["start move 3"]]["id"] + data[address + 18] = poke_data.moves[world.local_poke_data[mon]["start move 4"]]["id"] + write_bytes(data, world.local_poke_data[mon]["tms"], address + 20) + if mon in world.learnsets and world.learnsets[mon]: address = rom_addresses["Learnset_" + mon.replace(" ", "")] - for i, move in enumerate(self.learnsets[mon]): + for i, move in enumerate(world.learnsets[mon]): data[(address + 1) + i * 2] = poke_data.moves[move]["id"] - data[rom_addresses["Option_Aide_Rt2"]] = self.multiworld.oaks_aide_rt_2[self.player].value - data[rom_addresses["Option_Aide_Rt11"]] = self.multiworld.oaks_aide_rt_11[self.player].value - data[rom_addresses["Option_Aide_Rt15"]] = self.multiworld.oaks_aide_rt_15[self.player].value + data[rom_addresses["Option_Aide_Rt2"]] = world.options.oaks_aide_rt_2.value + data[rom_addresses["Option_Aide_Rt11"]] = world.options.oaks_aide_rt_11.value + data[rom_addresses["Option_Aide_Rt15"]] = world.options.oaks_aide_rt_15.value - if self.multiworld.safari_zone_normal_battles[self.player].value == 1: + if world.options.safari_zone_normal_battles.value == 1: data[rom_addresses["Option_Safari_Zone_Battle_Type"]] = 255 - if self.multiworld.reusable_tms[self.player].value: + if world.options.reusable_tms.value: data[rom_addresses["Option_Reusable_TMs"]] = 0xC9 - for i in range(1, 10): - data[rom_addresses[f"Option_Trainersanity{i}"]] = self.multiworld.trainersanity[self.player].value - - data[rom_addresses["Option_Always_Half_STAB"]] = int(not self.multiworld.same_type_attack_bonus[self.player].value) + data[rom_addresses["Option_Always_Half_STAB"]] = int(not world.options.same_type_attack_bonus.value) - if self.multiworld.better_shops[self.player]: + if world.options.better_shops: inventory = ["Poke Ball", "Great Ball", "Ultra Ball"] - if self.multiworld.better_shops[self.player].value == 2: + if world.options.better_shops.value == 2: inventory.append("Master Ball") inventory += ["Potion", "Super Potion", "Hyper Potion", "Max Potion", "Full Restore", "Revive", "Antidote", "Awakening", "Burn Heal", "Ice Heal", "Paralyze Heal", "Full Heal", "Repel", "Super Repel", @@ -492,30 +536,30 @@ def set_trade_mon(address, loc): shop_data.append(0xFF) for shop in range(1, 11): write_bytes(data, shop_data, rom_addresses[f"Shop{shop}"]) - if self.multiworld.stonesanity[self.player]: + if world.options.stonesanity: write_bytes(data, bytearray([0xFE, 1, item_table["Poke Doll"].id - 172000000, 0xFF]), rom_addresses[f"Shop_Stones"]) - price = str(self.multiworld.master_ball_price[self.player].value).zfill(6) + price = str(world.options.master_ball_price.value).zfill(6) price = bytearray([int(price[:2], 16), int(price[2:4], 16), int(price[4:], 16)]) write_bytes(data, price, rom_addresses["Price_Master_Ball"]) # Money values in Red and Blue are weird - for item in reversed(self.multiworld.precollected_items[self.player]): + for item in reversed(world.multiworld.precollected_items[world.player]): if data[rom_addresses["Start_Inventory"] + item.code - 172000000] < 255: data[rom_addresses["Start_Inventory"] + item.code - 172000000] += 1 - set_mon_palettes(self, random, data) + set_mon_palettes(world, random, data) - for move_data in self.local_move_data.values(): + for move_data in world.local_move_data.values(): if move_data["id"] == 0: continue address = rom_addresses["Move_Data"] + ((move_data["id"] - 1) * 6) write_bytes(data, bytearray([move_data["id"], move_data["effect"], move_data["power"], poke_data.type_ids[move_data["type"]], round(move_data["accuracy"] * 2.55), move_data["pp"]]), address) - TM_IDs = bytearray([poke_data.moves[move]["id"] for move in self.local_tms]) + TM_IDs = bytearray([poke_data.moves[move]["id"] for move in world.local_tms]) write_bytes(data, TM_IDs, rom_addresses["TM_Moves"]) - if self.multiworld.randomize_rock_tunnel[self.player]: + if world.options.randomize_rock_tunnel: seed = randomize_rock_tunnel(data, random) write_bytes(data, encode_text(f"SEED: {seed}"), rom_addresses["Text_Rock_Tunnel_Sign"]) @@ -524,44 +568,44 @@ def set_trade_mon(address, loc): data[rom_addresses['Title_Mon_First']] = mons.pop() for mon in range(0, 16): data[rom_addresses['Title_Mons'] + mon] = mons.pop() - if self.multiworld.game_version[self.player].value: - mons.sort(key=lambda mon: 0 if mon == self.multiworld.get_location("Oak's Lab - Starter 1", self.player).item.name - else 1 if mon == self.multiworld.get_location("Oak's Lab - Starter 2", self.player).item.name else - 2 if mon == self.multiworld.get_location("Oak's Lab - Starter 3", self.player).item.name else 3) + if world.options.game_version.value: + mons.sort(key=lambda mon: 0 if mon == world.multiworld.get_location("Oak's Lab - Starter 1", world.player).item.name + else 1 if mon == world.multiworld.get_location("Oak's Lab - Starter 2", world.player).item.name else + 2 if mon == world.multiworld.get_location("Oak's Lab - Starter 3", world.player).item.name else 3) else: - mons.sort(key=lambda mon: 0 if mon == self.multiworld.get_location("Oak's Lab - Starter 2", self.player).item.name - else 1 if mon == self.multiworld.get_location("Oak's Lab - Starter 1", self.player).item.name else - 2 if mon == self.multiworld.get_location("Oak's Lab - Starter 3", self.player).item.name else 3) - write_bytes(data, encode_text(self.multiworld.seed_name[-20:], 20, True), rom_addresses['Title_Seed']) + mons.sort(key=lambda mon: 0 if mon == world.multiworld.get_location("Oak's Lab - Starter 2", world.player).item.name + else 1 if mon == world.multiworld.get_location("Oak's Lab - Starter 1", world.player).item.name else + 2 if mon == world.multiworld.get_location("Oak's Lab - Starter 3", world.player).item.name else 3) + write_bytes(data, encode_text(world.multiworld.seed_name[-20:], 20, True), rom_addresses['Title_Seed']) - slot_name = self.multiworld.player_name[self.player] + slot_name = world.multiworld.player_name[world.player] slot_name.replace("@", " ") slot_name.replace("<", " ") slot_name.replace(">", " ") write_bytes(data, encode_text(slot_name, 16, True, True), rom_addresses['Title_Slot_Name']) - if self.trainer_name == "choose_in_game": + if world.trainer_name == "choose_in_game": data[rom_addresses["Skip_Player_Name"]] = 0 else: - write_bytes(data, self.trainer_name, rom_addresses['Player_Name']) - if self.rival_name == "choose_in_game": + write_bytes(data, world.trainer_name, rom_addresses['Player_Name']) + if world.rival_name == "choose_in_game": data[rom_addresses["Skip_Rival_Name"]] = 0 else: - write_bytes(data, self.rival_name, rom_addresses['Rival_Name']) + write_bytes(data, world.rival_name, rom_addresses['Rival_Name']) data[0xFF00] = 2 # client compatibility version - rom_name = bytearray(f'AP{Utils.__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed:11}\0', + rom_name = bytearray(f'AP{Utils.__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0', 'utf8')[:21] rom_name.extend([0] * (21 - len(rom_name))) write_bytes(data, rom_name, 0xFFC6) - write_bytes(data, self.multiworld.seed_name.encode(), 0xFFDB) - write_bytes(data, self.multiworld.player_name[self.player].encode(), 0xFFF0) + write_bytes(data, world.multiworld.seed_name.encode(), 0xFFDB) + write_bytes(data, world.multiworld.player_name[world.player].encode(), 0xFFF0) - self.finished_level_scaling.wait() + world.finished_level_scaling.wait() - write_quizzes(self, data, random) + write_quizzes(world, data, random) - for location in self.multiworld.get_locations(self.player): + for location in world.multiworld.get_locations(world.player): if location.party_data: for party in location.party_data: if not isinstance(party["party_address"], list): @@ -588,7 +632,7 @@ def set_trade_mon(address, loc): continue elif location.rom_address is None: continue - if location.item and location.item.player == self.player: + if location.item and location.item.player == world.player: if location.rom_address: rom_address = location.rom_address if not isinstance(rom_address, list): @@ -599,7 +643,7 @@ def set_trade_mon(address, loc): elif " ".join(location.item.name.split()[1:]) in poke_data.pokemon_data.keys(): data[address] = poke_data.pokemon_data[" ".join(location.item.name.split()[1:])]["id"] else: - item_id = self.item_name_to_id[location.item.name] - 172000000 + item_id = world.item_name_to_id[location.item.name] - 172000000 if item_id > 255: item_id -= 256 data[address] = item_id @@ -613,18 +657,18 @@ def set_trade_mon(address, loc): for address in rom_address: data[address] = 0x2C # AP Item - outfilepname = f'_P{self.player}' - outfilepname += f"_{self.multiworld.get_file_safe_player_name(self.player).replace(' ', '_')}" \ - if self.multiworld.player_name[self.player] != 'Player%d' % self.player else '' - rompath = os.path.join(output_directory, f'AP_{self.multiworld.seed_name}{outfilepname}.gb') + outfilepname = f'_P{world.player}' + outfilepname += f"_{world.multiworld.get_file_safe_player_name(world.player).replace(' ', '_')}" \ + if world.multiworld.player_name[world.player] != 'Player%d' % world.player else '' + rompath = os.path.join(output_directory, f'AP_{world.multiworld.seed_name}{outfilepname}.gb') with open(rompath, 'wb') as outfile: outfile.write(data) - if self.multiworld.game_version[self.player].current_key == "red": - patch = RedDeltaPatch(os.path.splitext(rompath)[0] + RedDeltaPatch.patch_file_ending, player=self.player, - player_name=self.multiworld.player_name[self.player], patched_path=rompath) + if world.options.game_version.current_key == "red": + patch = RedDeltaPatch(os.path.splitext(rompath)[0] + RedDeltaPatch.patch_file_ending, player=world.player, + player_name=world.multiworld.player_name[world.player], patched_path=rompath) else: - patch = BlueDeltaPatch(os.path.splitext(rompath)[0] + BlueDeltaPatch.patch_file_ending, player=self.player, - player_name=self.multiworld.player_name[self.player], patched_path=rompath) + patch = BlueDeltaPatch(os.path.splitext(rompath)[0] + BlueDeltaPatch.patch_file_ending, player=world.player, + player_name=world.multiworld.player_name[world.player], patched_path=rompath) patch.write() os.unlink(rompath) diff --git a/worlds/pokemon_rb/rom_addresses.py b/worlds/pokemon_rb/rom_addresses.py index e5c073971d5d..ec233d94d44d 100644 --- a/worlds/pokemon_rb/rom_addresses.py +++ b/worlds/pokemon_rb/rom_addresses.py @@ -1,10 +1,9 @@ rom_addresses = { "Option_Encounter_Minimum_Steps": 0x3c1, "Option_Pitch_Black_Rock_Tunnel": 0x76a, - "Option_Blind_Trainers": 0x30d5, - "Option_Trainersanity1": 0x3165, - "Option_Split_Card_Key": 0x3e1e, - "Option_Fix_Combat_Bugs": 0x3e1f, + "Option_Blind_Trainers": 0x32f0, + "Option_Split_Card_Key": 0x3e19, + "Option_Fix_Combat_Bugs": 0x3e1a, "Option_Lose_Money": 0x40d4, "Base_Stats_Mew": 0x4260, "Title_Mon_First": 0x4373, @@ -115,9 +114,10 @@ "HM_Strength_Badge_b": 0x131ed, "HM_Flash_Badge_a": 0x131fc, "HM_Flash_Badge_b": 0x13201, - "Trainer_Screen_Total_Key_Items": 0x135dc, - "TM_Moves": 0x137b1, - "Encounter_Chances": 0x13950, + "Tea_Key_Item_A": 0x135ac, + "Trainer_Screen_Total_Key_Items": 0x1361b, + "TM_Moves": 0x137f0, + "Encounter_Chances": 0x1398f, "Warps_CeladonCity": 0x18026, "Warps_PalletTown": 0x182c7, "Warps_ViridianCity": 0x18388, @@ -128,52 +128,54 @@ "Option_Viridian_Gym_Badges": 0x1901d, "Event_Sleepy_Guy": 0x191d1, "Option_Route3_Guard_B": 0x1928a, - "Starter2_K": 0x19611, - "Starter3_K": 0x19619, - "Event_Rocket_Thief": 0x19733, - "Option_Cerulean_Cave_Badges": 0x19861, - "Option_Cerulean_Cave_Key_Items": 0x19868, - "Text_Cerulean_Cave_Badges": 0x198d7, - "Text_Cerulean_Cave_Key_Items": 0x198e5, - "Event_Stranded_Man": 0x19b3c, - "Event_Rivals_Sister": 0x19d0f, - "Warps_BluesHouse": 0x19d65, - "Warps_VermilionTradeHouse": 0x19dbc, - "Require_Pokedex_D": 0x19e53, - "Option_Elite_Four_Key_Items": 0x19e9d, - "Option_Elite_Four_Pokedex": 0x19ea4, - "Option_Elite_Four_Badges": 0x19eab, - "Text_Elite_Four_Badges": 0x19f47, - "Text_Elite_Four_Key_Items": 0x19f51, - "Text_Elite_Four_Pokedex": 0x19f64, - "Shop10": 0x1a018, - "Warps_IndigoPlateauLobby": 0x1a044, - "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_0_ITEM": 0x1a16c, - "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_1_ITEM": 0x1a17a, - "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_2_ITEM": 0x1a188, - "Event_SKC4F": 0x1a19b, - "Warps_SilphCo4F": 0x1a21d, - "Missable_Silph_Co_4F_Item_1": 0x1a25d, - "Missable_Silph_Co_4F_Item_2": 0x1a264, - "Missable_Silph_Co_4F_Item_3": 0x1a26b, - "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_0_ITEM": 0x1a3c3, - "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_1_ITEM": 0x1a3d1, - "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_2_ITEM": 0x1a3df, - "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_3_ITEM": 0x1a3ed, - "Event_SKC5F": 0x1a400, - "Warps_SilphCo5F": 0x1a4aa, - "Missable_Silph_Co_5F_Item_1": 0x1a4f2, - "Missable_Silph_Co_5F_Item_2": 0x1a4f9, - "Missable_Silph_Co_5F_Item_3": 0x1a500, - "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_0_ITEM": 0x1a630, - "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_1_ITEM": 0x1a63e, - "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_2_ITEM": 0x1a64c, - "Event_SKC6F": 0x1a66d, - "Warps_SilphCo6F": 0x1a74b, - "Missable_Silph_Co_6F_Item_1": 0x1a79b, - "Missable_Silph_Co_6F_Item_2": 0x1a7a2, - "Path_Pallet_Oak": 0x1a928, - "Path_Pallet_Player": 0x1a935, + "Starter2_K": 0x19618, + "Starter3_K": 0x19620, + "Event_Rocket_Thief": 0x1973a, + "Tea_Key_Item_C": 0x1988f, + "Option_Cerulean_Cave_Badges": 0x198a0, + "Option_Cerulean_Cave_Key_Items": 0x198a7, + "Text_Cerulean_Cave_Badges": 0x19916, + "Text_Cerulean_Cave_Key_Items": 0x19924, + "Event_Stranded_Man": 0x19b7b, + "Event_Rivals_Sister": 0x19d4e, + "Warps_BluesHouse": 0x19da4, + "Warps_VermilionTradeHouse": 0x19dfb, + "Require_Pokedex_D": 0x19e99, + "Tea_Key_Item_B": 0x19f13, + "Option_Elite_Four_Key_Items": 0x19f1b, + "Option_Elite_Four_Pokedex": 0x19f22, + "Option_Elite_Four_Badges": 0x19f29, + "Text_Elite_Four_Badges": 0x19fc5, + "Text_Elite_Four_Key_Items": 0x19fcf, + "Text_Elite_Four_Pokedex": 0x19fe2, + "Shop10": 0x1a096, + "Warps_IndigoPlateauLobby": 0x1a0c2, + "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_0_ITEM": 0x1a1ea, + "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_1_ITEM": 0x1a1f8, + "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_2_ITEM": 0x1a206, + "Event_SKC4F": 0x1a219, + "Warps_SilphCo4F": 0x1a29b, + "Missable_Silph_Co_4F_Item_1": 0x1a2db, + "Missable_Silph_Co_4F_Item_2": 0x1a2e2, + "Missable_Silph_Co_4F_Item_3": 0x1a2e9, + "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_0_ITEM": 0x1a441, + "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_1_ITEM": 0x1a44f, + "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_2_ITEM": 0x1a45d, + "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_3_ITEM": 0x1a46b, + "Event_SKC5F": 0x1a47e, + "Warps_SilphCo5F": 0x1a528, + "Missable_Silph_Co_5F_Item_1": 0x1a570, + "Missable_Silph_Co_5F_Item_2": 0x1a577, + "Missable_Silph_Co_5F_Item_3": 0x1a57e, + "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_0_ITEM": 0x1a6ae, + "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_1_ITEM": 0x1a6bc, + "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_2_ITEM": 0x1a6ca, + "Event_SKC6F": 0x1a6eb, + "Warps_SilphCo6F": 0x1a7c9, + "Missable_Silph_Co_6F_Item_1": 0x1a819, + "Missable_Silph_Co_6F_Item_2": 0x1a820, + "Path_Pallet_Oak": 0x1a9a6, + "Path_Pallet_Player": 0x1a9b3, "Warps_CinnabarIsland": 0x1c026, "Warps_Route1": 0x1c0e9, "Option_Extra_Key_Items_B": 0x1ca46, @@ -191,75 +193,75 @@ "Starter2_E": 0x1d2f7, "Starter3_E": 0x1d2ff, "Event_Pokedex": 0x1d363, - "Event_Oaks_Gift": 0x1d393, - "Starter2_P": 0x1d481, - "Starter3_P": 0x1d489, - "Warps_OaksLab": 0x1d6af, - "Event_Pokemart_Quest": 0x1d76b, - "Shop1": 0x1d795, - "Warps_ViridianMart": 0x1d7d8, - "Warps_ViridianSchoolHouse": 0x1d82b, - "Warps_ViridianNicknameHouse": 0x1d889, - "Warps_PewterNidoranHouse": 0x1d8e4, - "Warps_PewterSpeechHouse": 0x1d927, - "Warps_CeruleanTrashedHouse": 0x1d98d, - "Warps_CeruleanTradeHouse": 0x1d9de, - "Event_Bicycle_Shop": 0x1da2f, - "Bike_Shop_Item_Display": 0x1da8a, - "Warps_BikeShop": 0x1db45, - "Event_Fuji": 0x1dbfd, - "Warps_MrFujisHouse": 0x1dc44, - "Warps_LavenderCuboneHouse": 0x1dcc0, - "Warps_NameRatersHouse": 0x1ddae, - "Warps_VermilionPidgeyHouse": 0x1ddf8, - "Trainersanity_EVENT_BEAT_MEW_ITEM": 0x1de4e, - "Warps_VermilionDock": 0x1de70, - "Static_Encounter_Mew": 0x1de7e, - "Gift_Eevee": 0x1def7, - "Warps_CeladonMansionRoofHouse": 0x1df0e, - "Shop7": 0x1df49, - "Warps_FuchsiaMart": 0x1df74, - "Warps_SaffronPidgeyHouse": 0x1dfdd, - "Event_Mr_Psychic": 0x1e020, - "Warps_MrPsychicsHouse": 0x1e05d, - "Warps_DiglettsCaveRoute2": 0x1e092, - "Warps_Route2TradeHouse": 0x1e0da, - "Warps_Route5Gate": 0x1e1db, - "Warps_Route6Gate": 0x1e2ad, - "Warps_Route7Gate": 0x1e383, - "Warps_Route8Gate": 0x1e454, - "Warps_UndergroundPathRoute8": 0x1e4a5, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_0_ITEM": 0x1e511, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_1_ITEM": 0x1e51f, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_2_ITEM": 0x1e52d, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_3_ITEM": 0x1e53b, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_4_ITEM": 0x1e549, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_5_ITEM": 0x1e557, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_6_ITEM": 0x1e565, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_7_ITEM": 0x1e573, - "Trainersanity_EVENT_BEAT_ZAPDOS_ITEM": 0x1e581, - "Warps_PowerPlant": 0x1e5de, - "Static_Encounter_Voltorb_A": 0x1e5f0, - "Static_Encounter_Voltorb_B": 0x1e5f8, - "Static_Encounter_Voltorb_C": 0x1e600, - "Static_Encounter_Electrode_A": 0x1e608, - "Static_Encounter_Voltorb_D": 0x1e610, - "Static_Encounter_Voltorb_E": 0x1e618, - "Static_Encounter_Electrode_B": 0x1e620, - "Static_Encounter_Voltorb_F": 0x1e628, - "Static_Encounter_Zapdos": 0x1e630, - "Missable_Power_Plant_Item_1": 0x1e638, - "Missable_Power_Plant_Item_2": 0x1e63f, - "Missable_Power_Plant_Item_3": 0x1e646, - "Missable_Power_Plant_Item_4": 0x1e64d, - "Missable_Power_Plant_Item_5": 0x1e654, - "Warps_DiglettsCaveRoute11": 0x1e7e9, - "Event_Rt16_House_Woman": 0x1e827, - "Warps_Route16FlyHouse": 0x1e870, - "Option_Victory_Road_Badges": 0x1e8f3, - "Warps_Route22Gate": 0x1e9e3, - "Event_Bill": 0x1eb24, - "Warps_BillsHouse": 0x1eb83, + "Event_Oaks_Gift": 0x1d398, + "Starter2_P": 0x1d486, + "Starter3_P": 0x1d48e, + "Warps_OaksLab": 0x1d6b4, + "Event_Pokemart_Quest": 0x1d770, + "Shop1": 0x1d79a, + "Warps_ViridianMart": 0x1d7dd, + "Warps_ViridianSchoolHouse": 0x1d830, + "Warps_ViridianNicknameHouse": 0x1d88e, + "Warps_PewterNidoranHouse": 0x1d8e9, + "Warps_PewterSpeechHouse": 0x1d92c, + "Warps_CeruleanTrashedHouse": 0x1d992, + "Warps_CeruleanTradeHouse": 0x1d9e3, + "Event_Bicycle_Shop": 0x1da34, + "Bike_Shop_Item_Display": 0x1da8f, + "Warps_BikeShop": 0x1db4a, + "Event_Fuji": 0x1dc02, + "Warps_MrFujisHouse": 0x1dc49, + "Warps_LavenderCuboneHouse": 0x1dcc5, + "Warps_NameRatersHouse": 0x1ddb3, + "Warps_VermilionPidgeyHouse": 0x1ddfd, + "Trainersanity_EVENT_BEAT_MEW_ITEM": 0x1de53, + "Warps_VermilionDock": 0x1de75, + "Static_Encounter_Mew": 0x1de83, + "Gift_Eevee": 0x1defc, + "Warps_CeladonMansionRoofHouse": 0x1df13, + "Shop7": 0x1df4e, + "Warps_FuchsiaMart": 0x1df79, + "Warps_SaffronPidgeyHouse": 0x1dfe2, + "Event_Mr_Psychic": 0x1e025, + "Warps_MrPsychicsHouse": 0x1e062, + "Warps_DiglettsCaveRoute2": 0x1e097, + "Warps_Route2TradeHouse": 0x1e0df, + "Warps_Route5Gate": 0x1e1e0, + "Warps_Route6Gate": 0x1e2b2, + "Warps_Route7Gate": 0x1e388, + "Warps_Route8Gate": 0x1e459, + "Warps_UndergroundPathRoute8": 0x1e4aa, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_0_ITEM": 0x1e516, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_1_ITEM": 0x1e524, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_2_ITEM": 0x1e532, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_3_ITEM": 0x1e540, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_4_ITEM": 0x1e54e, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_5_ITEM": 0x1e55c, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_6_ITEM": 0x1e56a, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_7_ITEM": 0x1e578, + "Trainersanity_EVENT_BEAT_ZAPDOS_ITEM": 0x1e586, + "Warps_PowerPlant": 0x1e5e3, + "Static_Encounter_Voltorb_A": 0x1e5f5, + "Static_Encounter_Voltorb_B": 0x1e5fd, + "Static_Encounter_Voltorb_C": 0x1e605, + "Static_Encounter_Electrode_A": 0x1e60d, + "Static_Encounter_Voltorb_D": 0x1e615, + "Static_Encounter_Voltorb_E": 0x1e61d, + "Static_Encounter_Electrode_B": 0x1e625, + "Static_Encounter_Voltorb_F": 0x1e62d, + "Static_Encounter_Zapdos": 0x1e635, + "Missable_Power_Plant_Item_1": 0x1e63d, + "Missable_Power_Plant_Item_2": 0x1e644, + "Missable_Power_Plant_Item_3": 0x1e64b, + "Missable_Power_Plant_Item_4": 0x1e652, + "Missable_Power_Plant_Item_5": 0x1e659, + "Warps_DiglettsCaveRoute11": 0x1e7ee, + "Event_Rt16_House_Woman": 0x1e82c, + "Warps_Route16FlyHouse": 0x1e875, + "Option_Victory_Road_Badges": 0x1e8f8, + "Warps_Route22Gate": 0x1e9e8, + "Event_Bill": 0x1eb29, + "Warps_BillsHouse": 0x1eb88, "Starter1_O": 0x372b0, "Starter2_O": 0x372b4, "Starter3_O": 0x372b8, @@ -1470,74 +1472,73 @@ "Trainersanity_EVENT_BEAT_POKEMONTOWER_5_TRAINER_3_ITEM": 0x609ea, "Warps_PokemonTower5F": 0x60a5e, "Missable_Pokemon_Tower_5F_Item": 0x60a92, - "Option_Trainersanity2": 0x60b2a, - "Ghost_Battle1": 0x60b83, - "Ghost_Battle_Level": 0x60b88, - "Trainersanity_EVENT_BEAT_POKEMONTOWER_6_TRAINER_0_ITEM": 0x60c25, - "Trainersanity_EVENT_BEAT_POKEMONTOWER_6_TRAINER_1_ITEM": 0x60c33, - "Trainersanity_EVENT_BEAT_POKEMONTOWER_6_TRAINER_2_ITEM": 0x60c41, - "Ghost_Battle2": 0x60c69, - "Warps_PokemonTower6F": 0x60cbe, - "Missable_Pokemon_Tower_6F_Item_1": 0x60ce4, - "Missable_Pokemon_Tower_6F_Item_2": 0x60ceb, - "Entrance_Shuffle_Fuji_Warp": 0x60deb, - "Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_0_ITEM": 0x60edf, - "Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_1_ITEM": 0x60eed, - "Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_2_ITEM": 0x60efb, - "Warps_PokemonTower7F": 0x60f8b, - "Warps_CeladonMart1F": 0x61033, - "Gift_Aerodactyl": 0x610f5, - "Gift_Omanyte": 0x610f9, - "Gift_Kabuto": 0x610fd, - "Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_0_ITEM": 0x611de, - "Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_1_ITEM": 0x611ec, - "Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_2_ITEM": 0x611fa, - "Warps_ViridianForest": 0x61273, - "Missable_Viridian_Forest_Item_1": 0x612c1, - "Missable_Viridian_Forest_Item_2": 0x612c8, - "Missable_Viridian_Forest_Item_3": 0x612cf, - "Warps_SSAnne1F": 0x61310, - "Starter2_M": 0x614e5, - "Starter3_M": 0x614ed, - "Warps_SSAnne2F": 0x615ab, - "Warps_SSAnneB1F": 0x616c9, - "Trainersanity_EVENT_BEAT_SS_ANNE_5_TRAINER_0_ITEM": 0x61771, - "Trainersanity_EVENT_BEAT_SS_ANNE_5_TRAINER_1_ITEM": 0x6177f, - "Warps_SSAnneBow": 0x617c6, - "Warps_SSAnneKitchen": 0x618b6, - "Event_SS_Anne_Captain": 0x6194e, - "Warps_SSAnneCaptainsRoom": 0x619d5, - "Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_0_ITEM": 0x61a3d, - "Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_1_ITEM": 0x61a4b, - "Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_2_ITEM": 0x61a59, - "Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_3_ITEM": 0x61a67, - "Warps_SSAnne1FRooms": 0x61af7, - "Missable_SS_Anne_1F_Item": 0x61b53, - "Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_0_ITEM": 0x61c24, - "Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_1_ITEM": 0x61c32, - "Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_2_ITEM": 0x61c40, - "Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_3_ITEM": 0x61c4e, - "Warps_SSAnne2FRooms": 0x61d2c, - "Missable_SS_Anne_2F_Item_1": 0x61d88, - "Missable_SS_Anne_2F_Item_2": 0x61d9b, - "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_0_ITEM": 0x61e2c, - "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_1_ITEM": 0x61e3a, - "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_2_ITEM": 0x61e48, - "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_3_ITEM": 0x61e56, - "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_4_ITEM": 0x61e64, - "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_5_ITEM": 0x61e72, - "Warps_SSAnneB1FRooms": 0x61f20, - "Missable_SS_Anne_B1F_Item_1": 0x61f8a, - "Missable_SS_Anne_B1F_Item_2": 0x61f91, - "Missable_SS_Anne_B1F_Item_3": 0x61f98, - "Warps_UndergroundPathNorthSouth": 0x61fd5, - "Warps_UndergroundPathWestEast": 0x61ff9, - "Warps_DiglettsCave": 0x6201d, - "Trainersanity_EVENT_BEAT_SILPH_CO_11F_TRAINER_0_ITEM": 0x62358, - "Trainersanity_EVENT_BEAT_SILPH_CO_11F_TRAINER_1_ITEM": 0x62366, - "Event_Silph_Co_President": 0x62373, - "Event_SKC11F": 0x623bd, - "Warps_SilphCo11F": 0x62446, + "Ghost_Battle1": 0x60b93, + "Ghost_Battle_Level": 0x60b98, + "Trainersanity_EVENT_BEAT_POKEMONTOWER_6_TRAINER_0_ITEM": 0x60c35, + "Trainersanity_EVENT_BEAT_POKEMONTOWER_6_TRAINER_1_ITEM": 0x60c43, + "Trainersanity_EVENT_BEAT_POKEMONTOWER_6_TRAINER_2_ITEM": 0x60c51, + "Ghost_Battle2": 0x60c79, + "Warps_PokemonTower6F": 0x60cce, + "Missable_Pokemon_Tower_6F_Item_1": 0x60cf4, + "Missable_Pokemon_Tower_6F_Item_2": 0x60cfb, + "Entrance_Shuffle_Fuji_Warp": 0x60dfb, + "Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_0_ITEM": 0x60eef, + "Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_1_ITEM": 0x60efd, + "Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_2_ITEM": 0x60f0b, + "Warps_PokemonTower7F": 0x60f9b, + "Warps_CeladonMart1F": 0x61043, + "Gift_Aerodactyl": 0x61105, + "Gift_Omanyte": 0x61109, + "Gift_Kabuto": 0x6110d, + "Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_0_ITEM": 0x61209, + "Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_1_ITEM": 0x61217, + "Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_2_ITEM": 0x61225, + "Warps_ViridianForest": 0x6129e, + "Missable_Viridian_Forest_Item_1": 0x612ec, + "Missable_Viridian_Forest_Item_2": 0x612f3, + "Missable_Viridian_Forest_Item_3": 0x612fa, + "Warps_SSAnne1F": 0x6133b, + "Starter2_M": 0x61510, + "Starter3_M": 0x61518, + "Warps_SSAnne2F": 0x615d6, + "Warps_SSAnneB1F": 0x616f4, + "Trainersanity_EVENT_BEAT_SS_ANNE_5_TRAINER_0_ITEM": 0x6179c, + "Trainersanity_EVENT_BEAT_SS_ANNE_5_TRAINER_1_ITEM": 0x617aa, + "Warps_SSAnneBow": 0x617f1, + "Warps_SSAnneKitchen": 0x618e1, + "Event_SS_Anne_Captain": 0x61979, + "Warps_SSAnneCaptainsRoom": 0x61a00, + "Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_0_ITEM": 0x61a68, + "Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_1_ITEM": 0x61a76, + "Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_2_ITEM": 0x61a84, + "Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_3_ITEM": 0x61a92, + "Warps_SSAnne1FRooms": 0x61b22, + "Missable_SS_Anne_1F_Item": 0x61b7e, + "Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_0_ITEM": 0x61c4f, + "Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_1_ITEM": 0x61c5d, + "Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_2_ITEM": 0x61c6b, + "Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_3_ITEM": 0x61c79, + "Warps_SSAnne2FRooms": 0x61d57, + "Missable_SS_Anne_2F_Item_1": 0x61db3, + "Missable_SS_Anne_2F_Item_2": 0x61dc6, + "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_0_ITEM": 0x61e57, + "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_1_ITEM": 0x61e65, + "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_2_ITEM": 0x61e73, + "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_3_ITEM": 0x61e81, + "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_4_ITEM": 0x61e8f, + "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_5_ITEM": 0x61e9d, + "Warps_SSAnneB1FRooms": 0x61f4b, + "Missable_SS_Anne_B1F_Item_1": 0x61fb5, + "Missable_SS_Anne_B1F_Item_2": 0x61fbc, + "Missable_SS_Anne_B1F_Item_3": 0x61fc3, + "Warps_UndergroundPathNorthSouth": 0x62000, + "Warps_UndergroundPathWestEast": 0x62024, + "Warps_DiglettsCave": 0x62048, + "Trainersanity_EVENT_BEAT_SILPH_CO_11F_TRAINER_0_ITEM": 0x62383, + "Trainersanity_EVENT_BEAT_SILPH_CO_11F_TRAINER_1_ITEM": 0x62391, + "Event_Silph_Co_President": 0x6239e, + "Event_SKC11F": 0x623e8, + "Warps_SilphCo11F": 0x62471, "Ghost_Battle4": 0x708e1, "Town_Map_Order": 0x70f0f, "Town_Map_Coords": 0x71381, @@ -1589,44 +1590,37 @@ "Warps_FuchsiaMeetingRoom": 0x75879, "Badge_Cinnabar_Gym": 0x759de, "Event_Cinnabar_Gym": 0x759f2, - "Option_Trainersanity4": 0x75ace, - "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_B_ITEM": 0x75ada, - "Option_Trainersanity3": 0x75b1e, - "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_A_ITEM": 0x75b2a, - "Option_Trainersanity5": 0x75b85, - "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_2_ITEM": 0x75b91, - "Option_Trainersanity6": 0x75bd5, - "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_3_ITEM": 0x75be1, - "Option_Trainersanity7": 0x75c25, - "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_4_ITEM": 0x75c31, - "Option_Trainersanity8": 0x75c75, - "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_5_ITEM": 0x75c81, - "Option_Trainersanity9": 0x75cc5, - "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_6_ITEM": 0x75cd1, - "Warps_CinnabarGym": 0x75d1b, - "Warps_CinnabarLab": 0x75e02, - "Warps_CinnabarLabTradeRoom": 0x75e94, - "Event_Lab_Scientist": 0x75ee9, - "Warps_CinnabarLabMetronomeRoom": 0x75f35, - "Fossils_Needed_For_Second_Item": 0x75fb6, - "Fossil_Level": 0x76017, - "Event_Dome_Fossil_B": 0x76031, - "Event_Helix_Fossil_B": 0x76051, - "Warps_CinnabarLabFossilRoom": 0x760d2, - "Warps_CinnabarPokecenter": 0x76128, - "Shop8": 0x7616f, - "Warps_CinnabarMart": 0x7619b, - "Warps_CopycatsHouse1F": 0x761ed, - "Starter2_N": 0x762a2, - "Starter3_N": 0x762aa, - "Warps_ChampionsRoom": 0x764d5, - "Trainersanity_EVENT_BEAT_LORELEIS_ROOM_TRAINER_0_ITEM": 0x76604, - "Warps_LoreleisRoom": 0x76628, - "Trainersanity_EVENT_BEAT_BRUNOS_ROOM_TRAINER_0_ITEM": 0x7675d, - "Warps_BrunosRoom": 0x76781, - "Trainersanity_EVENT_BEAT_AGATHAS_ROOM_TRAINER_0_ITEM": 0x768bc, - "Warps_AgathasRoom": 0x768e0, - "Option_Itemfinder": 0x76a33, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_B_ITEM": 0x75adc, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_A_ITEM": 0x75b2e, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_2_ITEM": 0x75b97, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_3_ITEM": 0x75be9, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_4_ITEM": 0x75c3b, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_5_ITEM": 0x75c8d, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_6_ITEM": 0x75cdf, + "Warps_CinnabarGym": 0x75d29, + "Warps_CinnabarLab": 0x75e10, + "Warps_CinnabarLabTradeRoom": 0x75ea2, + "Event_Lab_Scientist": 0x75ef7, + "Warps_CinnabarLabMetronomeRoom": 0x75f43, + "Fossils_Needed_For_Second_Item": 0x75fc4, + "Fossil_Level": 0x76025, + "Event_Dome_Fossil_B": 0x7603f, + "Event_Helix_Fossil_B": 0x7605f, + "Warps_CinnabarLabFossilRoom": 0x760e0, + "Warps_CinnabarPokecenter": 0x76136, + "Shop8": 0x7617d, + "Warps_CinnabarMart": 0x761a9, + "Warps_CopycatsHouse1F": 0x761fb, + "Starter2_N": 0x762b0, + "Starter3_N": 0x762b8, + "Warps_ChampionsRoom": 0x764e3, + "Trainersanity_EVENT_BEAT_LORELEIS_ROOM_TRAINER_0_ITEM": 0x76612, + "Warps_LoreleisRoom": 0x76636, + "Trainersanity_EVENT_BEAT_BRUNOS_ROOM_TRAINER_0_ITEM": 0x7676b, + "Warps_BrunosRoom": 0x7678f, + "Trainersanity_EVENT_BEAT_AGATHAS_ROOM_TRAINER_0_ITEM": 0x768ca, + "Warps_AgathasRoom": 0x768ee, + "Option_Itemfinder": 0x76a41, "Text_Quiz_A": 0x88806, "Text_Quiz_B": 0x8893a, "Text_Quiz_C": 0x88a6e, diff --git a/worlds/pokemon_rb/rules.py b/worlds/pokemon_rb/rules.py index 1d68f3148963..ba4bfd471c52 100644 --- a/worlds/pokemon_rb/rules.py +++ b/worlds/pokemon_rb/rules.py @@ -3,7 +3,7 @@ from . import logic -def set_rules(multiworld, player): +def set_rules(multiworld, world, player): item_rules = { # Some items do special things when they are passed into the GiveItem function in the game, but @@ -15,54 +15,46 @@ def set_rules(multiworld, player): not in i.name) } - if multiworld.prizesanity[player]: + if world.options.prizesanity: def prize_rule(i): return i.player != player or i.name in item_groups["Unique"] item_rules["Celadon Prize Corner - Item Prize 1"] = prize_rule item_rules["Celadon Prize Corner - Item Prize 2"] = prize_rule item_rules["Celadon Prize Corner - Item Prize 3"] = prize_rule - if multiworld.accessibility[player] != "full": - multiworld.get_location("Cerulean Bicycle Shop", player).always_allow = (lambda state, item: - item.name == "Bike Voucher" - and item.player == player) - multiworld.get_location("Fuchsia Warden's House - Safari Zone Warden", player).always_allow = (lambda state, item: - item.name == "Gold Teeth" and - item.player == player) - access_rules = { "Rival's House - Rival's Sister": lambda state: state.has("Oak's Parcel", player), "Oak's Lab - Oak's Post-Route-22-Rival Gift": lambda state: state.has("Oak's Parcel", player), - "Viridian City - Sleepy Guy": lambda state: logic.can_cut(state, player) or logic.can_surf(state, player), - "Route 2 Gate - Oak's Aide": lambda state: logic.oaks_aide(state, state.multiworld.oaks_aide_rt_2[player].value + 5, player), + "Viridian City - Sleepy Guy": lambda state: logic.can_cut(state, world, player) or logic.can_surf(state, world, player), + "Route 2 Gate - Oak's Aide": lambda state: logic.oaks_aide(state, world, world.options.oaks_aide_rt_2.value + 5, player), "Cerulean Bicycle Shop": lambda state: state.has("Bike Voucher", player) or location_item_name(state, "Cerulean Bicycle Shop", player) == ("Bike Voucher", player), "Lavender Mr. Fuji's House - Mr. Fuji": lambda state: state.has("Fuji Saved", player), - "Route 11 Gate 2F - Oak's Aide": lambda state: logic.oaks_aide(state, state.multiworld.oaks_aide_rt_11[player].value + 5, player), - "Celadon City - Stranded Man": lambda state: logic.can_surf(state, player), + "Route 11 Gate 2F - Oak's Aide": lambda state: logic.oaks_aide(state, world, world.options.oaks_aide_rt_11.value + 5, player), + "Celadon City - Stranded Man": lambda state: logic.can_surf(state, world, player), "Fuchsia Warden's House - Safari Zone Warden": lambda state: state.has("Gold Teeth", player) or location_item_name(state, "Fuchsia Warden's House - Safari Zone Warden", player) == ("Gold Teeth", player), - "Route 12 - Island Item": lambda state: logic.can_surf(state, player), - "Route 15 Gate 2F - Oak's Aide": lambda state: logic.oaks_aide(state, state.multiworld.oaks_aide_rt_15[player].value + 5, player), - "Route 25 - Item": lambda state: logic.can_cut(state, player), - "Fuchsia Warden's House - Behind Boulder Item": lambda state: logic.can_strength(state, player), - "Safari Zone Center - Island Item": lambda state: logic.can_surf(state, player), + "Route 12 - Island Item": lambda state: logic.can_surf(state, world, player), + "Route 15 Gate 2F - Oak's Aide": lambda state: logic.oaks_aide(state, world, world.options.oaks_aide_rt_15.value + 5, player), + "Route 25 - Item": lambda state: logic.can_cut(state, world, player), + "Fuchsia Warden's House - Behind Boulder Item": lambda state: logic.can_strength(state, world, player), + "Safari Zone Center - Island Item": lambda state: logic.can_surf(state, world, player), "Saffron Copycat's House 2F - Copycat": lambda state: state.has("Buy Poke Doll", player), "Celadon Game Corner - West Gambler's Gift": lambda state: state.has("Coin Case", player), "Celadon Game Corner - Center Gambler's Gift": lambda state: state.has("Coin Case", player), "Celadon Game Corner - East Gambler's Gift": lambda state: state.has("Coin Case", player), - "Celadon Game Corner - Hidden Item Northwest By Counter": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player), - "Celadon Game Corner - Hidden Item Southwest Corner": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player), - "Celadon Game Corner - Hidden Item Near Rumor Man": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player), - "Celadon Game Corner - Hidden Item Near Speculating Woman": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player), - "Celadon Game Corner - Hidden Item Near West Gifting Gambler": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player), - "Celadon Game Corner - Hidden Item Near Wonderful Time Woman": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player), - "Celadon Game Corner - Hidden Item Near Failing Gym Information Guy": lambda state: state.has( "Coin Case", player) and logic.can_get_hidden_items(state, player), - "Celadon Game Corner - Hidden Item Near East Gifting Gambler": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player), - "Celadon Game Corner - Hidden Item Near Hooked Guy": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player), - "Celadon Game Corner - Hidden Item at End of Horizontal Machine Row": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player), - "Celadon Game Corner - Hidden Item in Front of Horizontal Machine Row": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player), + "Celadon Game Corner - Hidden Item Northwest By Counter": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player), + "Celadon Game Corner - Hidden Item Southwest Corner": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player), + "Celadon Game Corner - Hidden Item Near Rumor Man": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player), + "Celadon Game Corner - Hidden Item Near Speculating Woman": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player), + "Celadon Game Corner - Hidden Item Near West Gifting Gambler": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player), + "Celadon Game Corner - Hidden Item Near Wonderful Time Woman": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player), + "Celadon Game Corner - Hidden Item Near Failing Gym Information Guy": lambda state: state.has( "Coin Case", player) and logic.can_get_hidden_items(state, world, player), + "Celadon Game Corner - Hidden Item Near East Gifting Gambler": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player), + "Celadon Game Corner - Hidden Item Near Hooked Guy": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player), + "Celadon Game Corner - Hidden Item at End of Horizontal Machine Row": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player), + "Celadon Game Corner - Hidden Item in Front of Horizontal Machine Row": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player), "Celadon Prize Corner - Item Prize 1": lambda state: state.has("Coin Case", player) and state.has("Game Corner", player), "Celadon Prize Corner - Item Prize 2": lambda state: state.has("Coin Case", player) and state.has("Game Corner", player), @@ -79,9 +71,9 @@ def prize_rule(i): "Cinnabar Lab Fossil Room - Dome Fossil Pokemon": lambda state: state.has("Dome Fossil", player) and state.has("Cinnabar Island", player), "Route 12 - Sleeping Pokemon": lambda state: state.has("Poke Flute", player), "Route 16 - Sleeping Pokemon": lambda state: state.has("Poke Flute", player), - "Seafoam Islands B4F - Legendary Pokemon": lambda state: logic.can_strength(state, player) and state.has("Seafoam Boss Boulders", player), - "Vermilion Dock - Legendary Pokemon": lambda state: logic.can_surf(state, player), - "Cerulean Cave B1F - Legendary Pokemon": lambda state: logic.can_surf(state, player), + "Seafoam Islands B4F - Legendary Pokemon": lambda state: logic.can_strength(state, world, player) and state.has("Seafoam Boss Boulders", player), + "Vermilion Dock - Legendary Pokemon": lambda state: logic.can_surf(state, world, player), + "Cerulean Cave B1F - Legendary Pokemon": lambda state: logic.can_surf(state, world, player), **{f"Pokemon Tower {floor}F - Wild Pokemon - {slot}": lambda state: state.has("Silph Scope", player) for floor in range(3, 8) for slot in range(1, 11)}, "Pokemon Tower 6F - Restless Soul": lambda state: state.has("Silph Scope", player), # just for level scaling @@ -103,101 +95,101 @@ def prize_rule(i): "Route 22 - Trainer Parties": lambda state: state.has("Oak's Parcel", player), # # Rock Tunnel - "Rock Tunnel 1F - PokeManiac": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel 1F - Hiker 1": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel 1F - Hiker 2": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel 1F - Hiker 3": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel 1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel 1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel 1F - Jr. Trainer F 3": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - PokeManiac 1": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - PokeManiac 2": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - PokeManiac 3": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - Hiker 1": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - Hiker 2": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - Hiker 3": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - North Item": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - Northwest Item": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - Southwest Item": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - West Item": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel 1F - PokeManiac": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel 1F - Hiker 1": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel 1F - Hiker 2": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel 1F - Hiker 3": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel 1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel 1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel 1F - Jr. Trainer F 3": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - PokeManiac 1": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - PokeManiac 2": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - PokeManiac 3": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - Hiker 1": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - Hiker 2": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - Hiker 3": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - North Item": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - Northwest Item": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - Southwest Item": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - West Item": lambda state: logic.rock_tunnel(state, world, player), # Pokédex check "Oak's Lab - Oak's Parcel Reward": lambda state: state.has("Oak's Parcel", player), # Hidden items - "Viridian Forest - Hidden Item Northwest by Trainer": lambda state: logic.can_get_hidden_items(state, + "Viridian Forest - Hidden Item Northwest by Trainer": lambda state: logic.can_get_hidden_items(state, world, player), - "Viridian Forest - Hidden Item Entrance Tree": lambda state: logic.can_get_hidden_items(state, player), - "Mt Moon B2F - Hidden Item Dead End Before Fossils": lambda state: logic.can_get_hidden_items(state, + "Viridian Forest - Hidden Item Entrance Tree": lambda state: logic.can_get_hidden_items(state, world, player), + "Mt Moon B2F - Hidden Item Dead End Before Fossils": lambda state: logic.can_get_hidden_items(state, world, player), - "Route 25 - Hidden Item Fence Outside Bill's House": lambda state: logic.can_get_hidden_items(state, + "Route 25 - Hidden Item Fence Outside Bill's House": lambda state: logic.can_get_hidden_items(state, world, player), - "Route 9 - Hidden Item Bush By Grass": lambda state: logic.can_get_hidden_items(state, player), - "S.S. Anne Kitchen - Hidden Item Kitchen Trash": lambda state: logic.can_get_hidden_items(state, player), - "S.S. Anne B1F Rooms - Hidden Item Under Pillow": lambda state: logic.can_get_hidden_items(state, player), + "Route 9 - Hidden Item Bush By Grass": lambda state: logic.can_get_hidden_items(state, world, player), + "S.S. Anne Kitchen - Hidden Item Kitchen Trash": lambda state: logic.can_get_hidden_items(state, world, player), + "S.S. Anne B1F Rooms - Hidden Item Under Pillow": lambda state: logic.can_get_hidden_items(state, world, player), "Route 10 - Hidden Item Behind Rock Tunnel Entrance Cuttable Tree": lambda - state: logic.can_get_hidden_items(state, player) and logic.can_cut(state, player), - "Route 10 - Hidden Item Bush": lambda state: logic.can_get_hidden_items(state, player), - "Rocket Hideout B1F - Hidden Item Pot Plant": lambda state: logic.can_get_hidden_items(state, player), - "Rocket Hideout B3F - Hidden Item Near East Item": lambda state: logic.can_get_hidden_items(state, player), + state: logic.can_get_hidden_items(state, world, player) and logic.can_cut(state, world, player), + "Route 10 - Hidden Item Bush": lambda state: logic.can_get_hidden_items(state, world, player), + "Rocket Hideout B1F - Hidden Item Pot Plant": lambda state: logic.can_get_hidden_items(state, world, player), + "Rocket Hideout B3F - Hidden Item Near East Item": lambda state: logic.can_get_hidden_items(state, world, player), "Rocket Hideout B4F - Hidden Item Behind Giovanni": lambda state: - logic.can_get_hidden_items(state, player), - "Pokemon Tower 5F - Hidden Item Near West Staircase": lambda state: logic.can_get_hidden_items(state, + logic.can_get_hidden_items(state, world, player), + "Pokemon Tower 5F - Hidden Item Near West Staircase": lambda state: logic.can_get_hidden_items(state, world, player), - "Route 13 - Hidden Item Dead End Bush": lambda state: logic.can_get_hidden_items(state, player), - "Route 13 - Hidden Item Dead End By Water Corner": lambda state: logic.can_get_hidden_items(state, player), - "Pokemon Mansion B1F - Hidden Item Secret Key Room Corner": lambda state: logic.can_get_hidden_items(state, + "Route 13 - Hidden Item Dead End Bush": lambda state: logic.can_get_hidden_items(state, world, player), + "Route 13 - Hidden Item Dead End By Water Corner": lambda state: logic.can_get_hidden_items(state, world, player), + "Pokemon Mansion B1F - Hidden Item Secret Key Room Corner": lambda state: logic.can_get_hidden_items(state, world, player), - "Safari Zone West - Hidden Item Secret House Statue": lambda state: logic.can_get_hidden_items(state, + "Safari Zone West - Hidden Item Secret House Statue": lambda state: logic.can_get_hidden_items(state, world, player), - "Silph Co 5F - Hidden Item Pot Plant": lambda state: logic.can_get_hidden_items(state, player), - "Silph Co 9F - Hidden Item Nurse Bed": lambda state: logic.can_get_hidden_items(state, player), - "Saffron Copycat's House 2F - Hidden Item Desk": lambda state: logic.can_get_hidden_items(state, player), - "Cerulean Cave 1F - Hidden Item Center Rocks": lambda state: logic.can_get_hidden_items(state, player), - "Cerulean Cave B1F - Hidden Item Northeast Rocks": lambda state: logic.can_get_hidden_items(state, player), - "Power Plant - Hidden Item Central Dead End": lambda state: logic.can_get_hidden_items(state, player), - "Power Plant - Hidden Item Before Zapdos": lambda state: logic.can_get_hidden_items(state, player), - "Seafoam Islands B2F - Hidden Item Rock": lambda state: logic.can_get_hidden_items(state, player), - "Seafoam Islands B3F - Hidden Item Rock": lambda state: logic.can_get_hidden_items(state, player), + "Silph Co 5F - Hidden Item Pot Plant": lambda state: logic.can_get_hidden_items(state, world, player), + "Silph Co 9F - Hidden Item Nurse Bed": lambda state: logic.can_get_hidden_items(state, world, player), + "Saffron Copycat's House 2F - Hidden Item Desk": lambda state: logic.can_get_hidden_items(state, world, player), + "Cerulean Cave 1F - Hidden Item Center Rocks": lambda state: logic.can_get_hidden_items(state, world, player), + "Cerulean Cave B1F - Hidden Item Northeast Rocks": lambda state: logic.can_get_hidden_items(state, world, player), + "Power Plant - Hidden Item Central Dead End": lambda state: logic.can_get_hidden_items(state, world, player), + "Power Plant - Hidden Item Before Zapdos": lambda state: logic.can_get_hidden_items(state, world, player), + "Seafoam Islands B2F - Hidden Item Rock": lambda state: logic.can_get_hidden_items(state, world, player), + "Seafoam Islands B3F - Hidden Item Rock": lambda state: logic.can_get_hidden_items(state, world, player), # if you can reach any exit boulders, that means you can drop into the water tunnel and auto-surf - "Seafoam Islands B4F - Hidden Item Corner Island": lambda state: logic.can_get_hidden_items(state, player), + "Seafoam Islands B4F - Hidden Item Corner Island": lambda state: logic.can_get_hidden_items(state, world, player), "Pokemon Mansion 1F - Hidden Item Block Near Entrance Carpet": lambda - state: logic.can_get_hidden_items(state, player), - "Pokemon Mansion 3F - Hidden Item Behind Burglar": lambda state: logic.can_get_hidden_items(state, player), - "Route 23 - Hidden Item Rocks Before Victory Road": lambda state: logic.can_get_hidden_items(state, + state: logic.can_get_hidden_items(state, world, player), + "Pokemon Mansion 3F - Hidden Item Behind Burglar": lambda state: logic.can_get_hidden_items(state, world, player), + "Route 23 - Hidden Item Rocks Before Victory Road": lambda state: logic.can_get_hidden_items(state, world, player), - "Route 23 - Hidden Item East Bush After Water": lambda state: logic.can_get_hidden_items(state, + "Route 23 - Hidden Item East Bush After Water": lambda state: logic.can_get_hidden_items(state, world, player), - "Route 23 - Hidden Item On Island": lambda state: logic.can_get_hidden_items(state, - player) and logic.can_surf(state, player), - "Victory Road 2F - Hidden Item Rock Before Moltres": lambda state: logic.can_get_hidden_items(state, + "Route 23 - Hidden Item On Island": lambda state: logic.can_get_hidden_items(state, world, + player) and logic.can_surf(state, world, player), + "Victory Road 2F - Hidden Item Rock Before Moltres": lambda state: logic.can_get_hidden_items(state, world, player), - "Victory Road 2F - Hidden Item Rock In Final Room": lambda state: logic.can_get_hidden_items(state, player), - "Viridian City - Hidden Item Cuttable Tree": lambda state: logic.can_get_hidden_items(state, player), - "Route 11 - Hidden Item Isolated Bush Near Gate": lambda state: logic.can_get_hidden_items(state, player), - "Route 12 - Hidden Item Bush Near Gate": lambda state: logic.can_get_hidden_items(state, player), - "Route 17 - Hidden Item In Grass": lambda state: logic.can_get_hidden_items(state, player), - "Route 17 - Hidden Item Near Northernmost Sign": lambda state: logic.can_get_hidden_items(state, player), - "Route 17 - Hidden Item East Center": lambda state: logic.can_get_hidden_items(state, player), - "Route 17 - Hidden Item West Center": lambda state: logic.can_get_hidden_items(state, player), - "Route 17 - Hidden Item Before Final Bridge": lambda state: logic.can_get_hidden_items(state, player), + "Victory Road 2F - Hidden Item Rock In Final Room": lambda state: logic.can_get_hidden_items(state, world, player), + "Viridian City - Hidden Item Cuttable Tree": lambda state: logic.can_get_hidden_items(state, world, player), + "Route 11 - Hidden Item Isolated Bush Near Gate": lambda state: logic.can_get_hidden_items(state, world, player), + "Route 12 - Hidden Item Bush Near Gate": lambda state: logic.can_get_hidden_items(state, world, player), + "Route 17 - Hidden Item In Grass": lambda state: logic.can_get_hidden_items(state, world, player), + "Route 17 - Hidden Item Near Northernmost Sign": lambda state: logic.can_get_hidden_items(state, world, player), + "Route 17 - Hidden Item East Center": lambda state: logic.can_get_hidden_items(state, world, player), + "Route 17 - Hidden Item West Center": lambda state: logic.can_get_hidden_items(state, world, player), + "Route 17 - Hidden Item Before Final Bridge": lambda state: logic.can_get_hidden_items(state, world, player), "Underground Path North South - Hidden Item Near Northern Stairs": lambda - state: logic.can_get_hidden_items(state, player), + state: logic.can_get_hidden_items(state, world, player), "Underground Path North South - Hidden Item Near Southern Stairs": lambda - state: logic.can_get_hidden_items(state, player), - "Underground Path West East - Hidden Item West": lambda state: logic.can_get_hidden_items(state, player), - "Underground Path West East - Hidden Item East": lambda state: logic.can_get_hidden_items(state, player), - "Celadon City - Hidden Item Dead End Near Cuttable Tree": lambda state: logic.can_get_hidden_items(state, + state: logic.can_get_hidden_items(state, world, player), + "Underground Path West East - Hidden Item West": lambda state: logic.can_get_hidden_items(state, world, player), + "Underground Path West East - Hidden Item East": lambda state: logic.can_get_hidden_items(state, world, player), + "Celadon City - Hidden Item Dead End Near Cuttable Tree": lambda state: logic.can_get_hidden_items(state, world, player), - "Route 25 - Hidden Item Northeast Of Grass": lambda state: logic.can_get_hidden_items(state, player), - "Mt Moon B2F - Hidden Item Lone Rock": lambda state: logic.can_get_hidden_items(state, player), - "Vermilion City - Hidden Item In Water Near Fan Club": lambda state: logic.can_get_hidden_items(state, - player) and logic.can_surf(state, player), - "Cerulean City - Hidden Item Gym Badge Guy's Backyard": lambda state: logic.can_get_hidden_items(state, + "Route 25 - Hidden Item Northeast Of Grass": lambda state: logic.can_get_hidden_items(state, world, player), + "Mt Moon B2F - Hidden Item Lone Rock": lambda state: logic.can_get_hidden_items(state, world, player), + "Vermilion City - Hidden Item In Water Near Fan Club": lambda state: logic.can_get_hidden_items(state, world, + player) and logic.can_surf(state, world, player), + "Cerulean City - Hidden Item Gym Badge Guy's Backyard": lambda state: logic.can_get_hidden_items(state, world, player), - "Route 4 - Hidden Item Plateau East Of Mt Moon": lambda state: logic.can_get_hidden_items(state, player), + "Route 4 - Hidden Item Plateau East Of Mt Moon": lambda state: logic.can_get_hidden_items(state, world, player), # Evolutions "Evolution - Ivysaur": lambda state: state.has("Bulbasaur", player) and logic.evolve_level(state, 16, player), @@ -281,5 +273,4 @@ def prize_rule(i): if loc.name.startswith("Pokedex"): mon = loc.name.split(" - ")[1] add_rule(loc, lambda state, i=mon: (state.has("Pokedex", player) or not - state.multiworld.require_pokedex[player]) and (state.has(i, player) - or state.has(f"Static {i}", player))) + world.options.require_pokedex) and (state.has(i, player) or state.has(f"Static {i}", player))) From 0d35cd4679f6f267bfbdb1b91325eb1cb2c6ee39 Mon Sep 17 00:00:00 2001 From: Remy Jette Date: Wed, 18 Sep 2024 11:42:22 -0700 Subject: [PATCH 40/94] BizHawkClient: Avoid error launching BizHawkClient via Launcher CLI (#3554) * Core, BizHawkClient: Support launching BizHawkClient via Launcher command line * Revert changes to LauncherComponents.py --- BizHawkClient.py | 3 ++- worlds/_bizhawk/client.py | 2 +- worlds/_bizhawk/context.py | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/BizHawkClient.py b/BizHawkClient.py index 86c8e5197e3f..743785b25f16 100644 --- a/BizHawkClient.py +++ b/BizHawkClient.py @@ -1,9 +1,10 @@ from __future__ import annotations +import sys import ModuleUpdate ModuleUpdate.update() from worlds._bizhawk.context import launch if __name__ == "__main__": - launch() + launch(*sys.argv[1:]) diff --git a/worlds/_bizhawk/client.py b/worlds/_bizhawk/client.py index 00370c277a17..415b663e60af 100644 --- a/worlds/_bizhawk/client.py +++ b/worlds/_bizhawk/client.py @@ -15,7 +15,7 @@ def launch_client(*args) -> None: from .context import launch - launch_subprocess(launch, name="BizHawkClient") + launch_subprocess(launch, name="BizHawkClient", args=args) component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client, diff --git a/worlds/_bizhawk/context.py b/worlds/_bizhawk/context.py index 896c8fb7b504..2a3965a54fcd 100644 --- a/worlds/_bizhawk/context.py +++ b/worlds/_bizhawk/context.py @@ -239,11 +239,11 @@ async def _patch_and_run_game(patch_file: str): logger.exception(exc) -def launch() -> None: +def launch(*launch_args) -> None: async def main(): parser = get_base_parser() parser.add_argument("patch_file", default="", type=str, nargs="?", help="Path to an Archipelago patch file") - args = parser.parse_args() + args = parser.parse_args(launch_args) ctx = BizHawkClientContext(args.connect, args.password) ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") From 2ee8b7535dcf0ca22b5dfb84bbc01570fd5ed69f Mon Sep 17 00:00:00 2001 From: Faris <162540354+FarisTheAncient@users.noreply.github.com> Date: Wed, 18 Sep 2024 13:53:17 -0500 Subject: [PATCH 41/94] OSRS: UT integration for OSRS to support chunksanity (#3776) --- worlds/osrs/__init__.py | 50 ++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/worlds/osrs/__init__.py b/worlds/osrs/__init__.py index 49aa1666084e..9ed55f218d9f 100644 --- a/worlds/osrs/__init__.py +++ b/worlds/osrs/__init__.py @@ -90,16 +90,18 @@ def generate_early(self) -> None: rnd = self.random starting_area = self.options.starting_area + + #UT specific override, if we are in normal gen, resolve starting area, we will get it from slot_data in UT + if not hasattr(self.multiworld, "generation_is_fake"): + if starting_area.value == StartingArea.option_any_bank: + self.starting_area_item = rnd.choice(starting_area_dict) + elif starting_area.value < StartingArea.option_chunksanity: + self.starting_area_item = starting_area_dict[starting_area.value] + else: + self.starting_area_item = rnd.choice(chunksanity_starting_chunks) - if starting_area.value == StartingArea.option_any_bank: - self.starting_area_item = rnd.choice(starting_area_dict) - elif starting_area.value < StartingArea.option_chunksanity: - self.starting_area_item = starting_area_dict[starting_area.value] - else: - self.starting_area_item = rnd.choice(chunksanity_starting_chunks) - - # Set Starting Chunk - self.multiworld.push_precollected(self.create_item(self.starting_area_item)) + # Set Starting Chunk + self.multiworld.push_precollected(self.create_item(self.starting_area_item)) """ This function pulls from LogicCSVToPython so that it sends the correct tag of the repository to the client. @@ -109,8 +111,23 @@ def generate_early(self) -> None: def fill_slot_data(self): data = self.options.as_dict("brutal_grinds") data["data_csv_tag"] = data_csv_tag + data["starting_area"] = str(self.starting_area_item) #these aren't actually strings, they just play them on tv return data + def interpret_slot_data(self, slot_data: typing.Dict[str, typing.Any]) -> None: + if "starting_area" in slot_data: + self.starting_area_item = slot_data["starting_area"] + menu_region = self.multiworld.get_region("Menu",self.player) + menu_region.exits.clear() #prevent making extra exits if players just reconnect to a differnet slot + if self.starting_area_item in chunksanity_special_region_names: + starting_area_region = chunksanity_special_region_names[self.starting_area_item] + else: + starting_area_region = self.starting_area_item[6:] # len("Area: ") + starting_entrance = menu_region.create_exit(f"Start->{starting_area_region}") + starting_entrance.access_rule = lambda state: state.has(self.starting_area_item, self.player) + starting_entrance.connect(self.region_name_to_data[starting_area_region]) + + def create_regions(self) -> None: """ called to place player's regions into the MultiWorld's regions list. If it's hard to separate, this can be done @@ -128,13 +145,14 @@ def create_regions(self) -> None: # Removes the word "Area: " from the item name to get the region it applies to. # I figured tacking "Area: " at the beginning would make it _easier_ to tell apart. Turns out it made it worse - if self.starting_area_item in chunksanity_special_region_names: - starting_area_region = chunksanity_special_region_names[self.starting_area_item] - else: - starting_area_region = self.starting_area_item[6:] # len("Area: ") - starting_entrance = menu_region.create_exit(f"Start->{starting_area_region}") - starting_entrance.access_rule = lambda state: state.has(self.starting_area_item, self.player) - starting_entrance.connect(self.region_name_to_data[starting_area_region]) + if self.starting_area_item != "": #if area hasn't been set, then we shouldn't connect it + if self.starting_area_item in chunksanity_special_region_names: + starting_area_region = chunksanity_special_region_names[self.starting_area_item] + else: + starting_area_region = self.starting_area_item[6:] # len("Area: ") + starting_entrance = menu_region.create_exit(f"Start->{starting_area_region}") + starting_entrance.access_rule = lambda state: state.has(self.starting_area_item, self.player) + starting_entrance.connect(self.region_name_to_data[starting_area_region]) # Create entrances between regions for region_row in region_rows: From fced9050a477d0d66d0342a405b71987ec5bc3be Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Wed, 18 Sep 2024 12:09:47 -0700 Subject: [PATCH 42/94] Zillion: fix logic cache (#3719) --- worlds/zillion/__init__.py | 27 ++++-------- worlds/zillion/logic.py | 85 ++++++++++++++++++++++---------------- 2 files changed, 57 insertions(+), 55 deletions(-) diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index cf61d93ca4ce..d5e86bb33292 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -4,7 +4,7 @@ import settings import threading import typing -from typing import Any, Dict, List, Set, Tuple, Optional +from typing import Any, Dict, List, Set, Tuple, Optional, Union import os import logging @@ -12,7 +12,7 @@ MultiWorld, Item, CollectionState, Entrance, Tutorial from .gen_data import GenData -from .logic import cs_to_zz_locs +from .logic import ZillionLogicCache from .region import ZillionLocation, ZillionRegion from .options import ZillionOptions, validate, z_option_groups from .id_maps import ZillionSlotInfo, get_slot_info, item_name_to_id as _item_name_to_id, \ @@ -21,7 +21,6 @@ from .item import ZillionItem from .patch import ZillionPatch -from zilliandomizer.randomizer import Randomizer as ZzRandomizer from zilliandomizer.system import System from zilliandomizer.logic_components.items import RESCUE, items as zz_items, Item as ZzItem from zilliandomizer.logic_components.locations import Location as ZzLocation, Req @@ -121,6 +120,7 @@ def flush(self) -> None: """ This is kind of a cache to avoid iterating through all the multiworld locations in logic. """ slot_data_ready: threading.Event """ This event is set in `generate_output` when the data is ready for `fill_slot_data` """ + logic_cache: Union[ZillionLogicCache, None] = None def __init__(self, world: MultiWorld, player: int): super().__init__(world, player) @@ -134,9 +134,6 @@ def _make_item_maps(self, start_char: Chars) -> None: self.id_to_zz_item = id_to_zz_item def generate_early(self) -> None: - if not hasattr(self.multiworld, "zillion_logic_cache"): - setattr(self.multiworld, "zillion_logic_cache", {}) - zz_op, item_counts = validate(self.options) if zz_op.early_scope: @@ -163,6 +160,8 @@ def create_regions(self) -> None: assert self.zz_system.randomizer, "generate_early hasn't been called" assert self.id_to_zz_item, "generate_early hasn't been called" p = self.player + logic_cache = ZillionLogicCache(p, self.zz_system.randomizer, self.id_to_zz_item) + self.logic_cache = logic_cache w = self.multiworld self.my_locations = [] @@ -201,15 +200,12 @@ def create_regions(self) -> None: if not zz_loc.item: def access_rule_wrapped(zz_loc_local: ZzLocation, - p: int, - zz_r: ZzRandomizer, - id_to_zz_item: Dict[int, ZzItem], + lc: ZillionLogicCache, cs: CollectionState) -> bool: - accessible = cs_to_zz_locs(cs, p, zz_r, id_to_zz_item) + accessible = lc.cs_to_zz_locs(cs) return zz_loc_local in accessible - access_rule = functools.partial(access_rule_wrapped, - zz_loc, self.player, self.zz_system.randomizer, self.id_to_zz_item) + access_rule = functools.partial(access_rule_wrapped, zz_loc, logic_cache) loc_name = self.zz_system.randomizer.loc_name_2_pretty[zz_loc.name] loc = ZillionLocation(zz_loc, self.player, loc_name, here) @@ -402,13 +398,6 @@ def fill_slot_data(self) -> ZillionSlotInfo: # json of WebHostLib.models.Slot game = self.zz_system.get_game() return get_slot_info(game.regions, game.char_order[0], game.loc_name_2_pretty) - # def modify_multidata(self, multidata: Dict[str, Any]) -> None: - # """For deeper modification of server multidata.""" - # # not modifying multidata, just want to call this at the end of the generation process - # cache = getattr(self.multiworld, "zillion_logic_cache") - # import sys - # print(sys.getsizeof(cache)) - # end of ordered Main.py calls def create_item(self, name: str) -> Item: diff --git a/worlds/zillion/logic.py b/worlds/zillion/logic.py index dcbc6131f1a9..a14910a200e5 100644 --- a/worlds/zillion/logic.py +++ b/worlds/zillion/logic.py @@ -1,4 +1,4 @@ -from typing import Dict, FrozenSet, Tuple, List, Counter as _Counter +from typing import Dict, FrozenSet, Mapping, Tuple, List, Counter as _Counter from BaseClasses import CollectionState @@ -44,38 +44,51 @@ def item_counts(cs: CollectionState, p: int) -> Tuple[Tuple[str, int], ...]: return tuple((item_name, cs.count(item_name, p)) for item_name in item_name_to_id) -LogicCacheType = Dict[int, Tuple[Dict[int, _Counter[str]], FrozenSet[Location]]] -""" { hash: (cs.prog_items, accessible_locations) } """ - - -def cs_to_zz_locs(cs: CollectionState, p: int, zz_r: Randomizer, id_to_zz_item: Dict[int, Item]) -> FrozenSet[Location]: - """ - given an Archipelago `CollectionState`, - returns frozenset of accessible zilliandomizer locations - """ - # caching this function because it would be slow - logic_cache: LogicCacheType = getattr(cs.multiworld, "zillion_logic_cache", {}) - _hash = set_randomizer_locs(cs, p, zz_r) - counts = item_counts(cs, p) - _hash += hash(counts) - - if _hash in logic_cache and logic_cache[_hash][0] == cs.prog_items: - # print("cache hit") - return logic_cache[_hash][1] - - # print("cache miss") - have_items: List[Item] = [] - for name, count in counts: - have_items.extend([id_to_zz_item[item_name_to_id[name]]] * count) - # have_req is the result of converting AP CollectionState to zilliandomizer collection state - have_req = zz_r.make_ability(have_items) - - # This `get_locations` is where the core of the logic comes in. - # It takes a zilliandomizer collection state (a set of the abilities that I have) - # and returns list of all the zilliandomizer locations I can access with those abilities. - tr = frozenset(zz_r.get_locations(have_req)) - - # save result in cache - logic_cache[_hash] = (cs.prog_items.copy(), tr) - - return tr +_cache_miss: Tuple[None, FrozenSet[Location]] = (None, frozenset()) + + +class ZillionLogicCache: + _cache: Dict[int, Tuple[_Counter[str], FrozenSet[Location]]] + """ `{ hash: (counter_from_prog_items, accessible_zz_locations) }` """ + _player: int + _zz_r: Randomizer + _id_to_zz_item: Mapping[int, Item] + + def __init__(self, player: int, zz_r: Randomizer, id_to_zz_item: Mapping[int, Item]) -> None: + self._cache = {} + self._player = player + self._zz_r = zz_r + self._id_to_zz_item = id_to_zz_item + + def cs_to_zz_locs(self, cs: CollectionState) -> FrozenSet[Location]: + """ + given an Archipelago `CollectionState`, + returns frozenset of accessible zilliandomizer locations + """ + # caching this function because it would be slow + _hash = set_randomizer_locs(cs, self._player, self._zz_r) + counts = item_counts(cs, self._player) + _hash += hash(counts) + + cntr, locs = self._cache.get(_hash, _cache_miss) + if cntr == cs.prog_items[self._player]: + # print("cache hit") + return locs + + # print("cache miss") + have_items: List[Item] = [] + for name, count in counts: + have_items.extend([self._id_to_zz_item[item_name_to_id[name]]] * count) + # have_req is the result of converting AP CollectionState to zilliandomizer collection state + have_req = self._zz_r.make_ability(have_items) + # print(f"{have_req=}") + + # This `get_locations` is where the core of the logic comes in. + # It takes a zilliandomizer collection state (a set of the abilities that I have) + # and returns list of all the zilliandomizer locations I can access with those abilities. + tr = frozenset(self._zz_r.get_locations(have_req)) + + # save result in cache + self._cache[_hash] = (cs.prog_items[self._player].copy(), tr) + + return tr From 025c5509916158d19ee22ee884754c56ab8958c0 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Wed, 18 Sep 2024 14:26:59 -0500 Subject: [PATCH 43/94] Ocarina of Time: options and general cleanup (#3767) * working? * missed one * fix old start inventory usage * missed global random usage --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- worlds/oot/Cosmetics.py | 41 ++++---- worlds/oot/Entrance.py | 4 +- worlds/oot/EntranceShuffle.py | 40 ++++---- worlds/oot/HintList.py | 24 ++--- worlds/oot/Hints.py | 66 ++++++------ worlds/oot/ItemPool.py | 46 ++++----- worlds/oot/Messages.py | 5 +- worlds/oot/Music.py | 17 ++-- worlds/oot/N64Patch.py | 5 +- worlds/oot/Options.py | 185 ++++++++++++++++++++++++++++++---- worlds/oot/Patches.py | 26 ++--- worlds/oot/RuleParser.py | 28 ++--- worlds/oot/Rules.py | 18 ++-- worlds/oot/TextBox.py | 2 +- worlds/oot/__init__.py | 86 ++++++++-------- 15 files changed, 367 insertions(+), 226 deletions(-) diff --git a/worlds/oot/Cosmetics.py b/worlds/oot/Cosmetics.py index f40f8a1ebb06..4a748c60aa9e 100644 --- a/worlds/oot/Cosmetics.py +++ b/worlds/oot/Cosmetics.py @@ -1,9 +1,9 @@ from .Utils import data_path, __version__ from .Colors import * import logging -import worlds.oot.Music as music -import worlds.oot.Sounds as sfx -import worlds.oot.IconManip as icon +from . import Music as music +from . import Sounds as sfx +from . import IconManip as icon from .JSONDump import dump_obj, CollapseList, CollapseDict, AlignedDict, SortedDict import json @@ -105,7 +105,7 @@ def patch_tunic_colors(rom, ootworld, symbols): # handle random if tunic_option == 'Random Choice': - tunic_option = random.choice(tunic_color_list) + tunic_option = ootworld.random.choice(tunic_color_list) # handle completely random if tunic_option == 'Completely Random': color = generate_random_color() @@ -156,9 +156,9 @@ def patch_navi_colors(rom, ootworld, symbols): # choose a random choice for the whole group if navi_option_inner == 'Random Choice': - navi_option_inner = random.choice(navi_color_list) + navi_option_inner = ootworld.random.choice(navi_color_list) if navi_option_outer == 'Random Choice': - navi_option_outer = random.choice(navi_color_list) + navi_option_outer = ootworld.random.choice(navi_color_list) if navi_option_outer == 'Match Inner': navi_option_outer = navi_option_inner @@ -233,9 +233,9 @@ def patch_sword_trails(rom, ootworld, symbols): # handle random choice if option_inner == 'Random Choice': - option_inner = random.choice(sword_trail_color_list) + option_inner = ootworld.random.choice(sword_trail_color_list) if option_outer == 'Random Choice': - option_outer = random.choice(sword_trail_color_list) + option_outer = ootworld.random.choice(sword_trail_color_list) if option_outer == 'Match Inner': option_outer = option_inner @@ -326,9 +326,9 @@ def patch_trails(rom, ootworld, trails): # handle random choice if option_inner == 'Random Choice': - option_inner = random.choice(trail_color_list) + option_inner = ootworld.random.choice(trail_color_list) if option_outer == 'Random Choice': - option_outer = random.choice(trail_color_list) + option_outer = ootworld.random.choice(trail_color_list) if option_outer == 'Match Inner': option_outer = option_inner @@ -393,7 +393,7 @@ def patch_gauntlet_colors(rom, ootworld, symbols): # handle random if gauntlet_option == 'Random Choice': - gauntlet_option = random.choice(gauntlet_color_list) + gauntlet_option = ootworld.random.choice(gauntlet_color_list) # handle completely random if gauntlet_option == 'Completely Random': color = generate_random_color() @@ -424,10 +424,10 @@ def patch_shield_frame_colors(rom, ootworld, symbols): # handle random if shield_frame_option == 'Random Choice': - shield_frame_option = random.choice(shield_frame_color_list) + shield_frame_option = ootworld.random.choice(shield_frame_color_list) # handle completely random if shield_frame_option == 'Completely Random': - color = [random.getrandbits(8), random.getrandbits(8), random.getrandbits(8)] + color = [ootworld.random.getrandbits(8), ootworld.random.getrandbits(8), ootworld.random.getrandbits(8)] # grab the color from the list elif shield_frame_option in shield_frame_colors: color = list(shield_frame_colors[shield_frame_option]) @@ -458,7 +458,7 @@ def patch_heart_colors(rom, ootworld, symbols): # handle random if heart_option == 'Random Choice': - heart_option = random.choice(heart_color_list) + heart_option = ootworld.random.choice(heart_color_list) # handle completely random if heart_option == 'Completely Random': color = generate_random_color() @@ -495,7 +495,7 @@ def patch_magic_colors(rom, ootworld, symbols): magic_option = format_cosmetic_option_result(ootworld.__dict__[magic_setting]) if magic_option == 'Random Choice': - magic_option = random.choice(magic_color_list) + magic_option = ootworld.random.choice(magic_color_list) if magic_option == 'Completely Random': color = generate_random_color() @@ -559,7 +559,7 @@ def patch_button_colors(rom, ootworld, symbols): # handle random if button_option == 'Random Choice': - button_option = random.choice(list(button_colors.keys())) + button_option = ootworld.random.choice(list(button_colors.keys())) # handle completely random if button_option == 'Completely Random': fixed_font_color = [10, 10, 10] @@ -618,11 +618,11 @@ def patch_sfx(rom, ootworld, symbols): rom.write_int16(loc, sound_id) else: if selection == 'random-choice': - selection = random.choice(sfx.get_hook_pool(hook)).value.keyword + selection = ootworld.random.choice(sfx.get_hook_pool(hook)).value.keyword elif selection == 'random-ear-safe': - selection = random.choice(sfx.get_hook_pool(hook, "TRUE")).value.keyword + selection = ootworld.random.choice(sfx.get_hook_pool(hook, "TRUE")).value.keyword elif selection == 'completely-random': - selection = random.choice(sfx.standard).value.keyword + selection = ootworld.random.choice(sfx.standard).value.keyword sound_id = sound_dict[selection] for loc in hook.value.locations: rom.write_int16(loc, sound_id) @@ -644,7 +644,7 @@ def patch_instrument(rom, ootworld, symbols): choice = ootworld.sfx_ocarina if choice == 'random-choice': - choice = random.choice(list(instruments.keys())) + choice = ootworld.random.choice(list(instruments.keys())) rom.write_byte(0x00B53C7B, instruments[choice]) rom.write_byte(0x00B4BF6F, instruments[choice]) # For Lost Woods Skull Kids' minigame in Lost Woods @@ -769,7 +769,6 @@ def patch_instrument(rom, ootworld, symbols): def patch_cosmetics(ootworld, rom): # Use the world's slot seed for cosmetics - random.seed(ootworld.multiworld.per_slot_randoms[ootworld.player].random()) # try to detect the cosmetic patch data format versioned_patch_set = None diff --git a/worlds/oot/Entrance.py b/worlds/oot/Entrance.py index 6c4b6428f53e..8b041f045dcf 100644 --- a/worlds/oot/Entrance.py +++ b/worlds/oot/Entrance.py @@ -3,9 +3,9 @@ class OOTEntrance(Entrance): game: str = 'Ocarina of Time' - def __init__(self, player, world, name='', parent=None): + def __init__(self, player, multiworld, name='', parent=None): super(OOTEntrance, self).__init__(player, name, parent) - self.multiworld = world + self.multiworld = multiworld self.access_rules = [] self.reverse = None self.replaces = None diff --git a/worlds/oot/EntranceShuffle.py b/worlds/oot/EntranceShuffle.py index cda442ffb109..66c5df804cb4 100644 --- a/worlds/oot/EntranceShuffle.py +++ b/worlds/oot/EntranceShuffle.py @@ -440,16 +440,16 @@ class EntranceShuffleError(Exception): def shuffle_random_entrances(ootworld): - world = ootworld.multiworld + multiworld = ootworld.multiworld player = ootworld.player # Gather locations to keep reachable for validation all_state = ootworld.get_state_with_complete_itempool() all_state.sweep_for_advancements(locations=ootworld.get_locations()) - locations_to_ensure_reachable = {loc for loc in world.get_reachable_locations(all_state, player) if not (loc.type == 'Drop' or (loc.type == 'Event' and 'Subrule' in loc.name))} + locations_to_ensure_reachable = {loc for loc in multiworld.get_reachable_locations(all_state, player) if not (loc.type == 'Drop' or (loc.type == 'Event' and 'Subrule' in loc.name))} # Set entrance data for all entrances - set_all_entrances_data(world, player) + set_all_entrances_data(multiworld, player) # Determine entrance pools based on settings one_way_entrance_pools = {} @@ -547,10 +547,10 @@ def shuffle_random_entrances(ootworld): none_state = CollectionState(ootworld.multiworld) # Plando entrances - if world.plando_connections[player]: + if ootworld.options.plando_connections: rollbacks = [] all_targets = {**one_way_target_entrance_pools, **target_entrance_pools} - for conn in world.plando_connections[player]: + for conn in ootworld.options.plando_connections: try: entrance = ootworld.get_entrance(conn.entrance) exit = ootworld.get_entrance(conn.exit) @@ -628,7 +628,7 @@ def shuffle_random_entrances(ootworld): logging.getLogger('').error(f'Root has too many entrances left after shuffling entrances') # Game is beatable new_all_state = ootworld.get_state_with_complete_itempool() - if not world.has_beaten_game(new_all_state, player): + if not multiworld.has_beaten_game(new_all_state, player): raise EntranceShuffleError('Cannot beat game') # Validate world validate_world(ootworld, None, locations_to_ensure_reachable, all_state, none_state) @@ -675,7 +675,7 @@ def place_one_way_priority_entrance(ootworld, priority_name, allowed_regions, al all_state, none_state, one_way_entrance_pools, one_way_target_entrance_pools): avail_pool = list(chain.from_iterable(one_way_entrance_pools[t] for t in allowed_types if t in one_way_entrance_pools)) - ootworld.multiworld.random.shuffle(avail_pool) + ootworld.random.shuffle(avail_pool) for entrance in avail_pool: if entrance.replaces: @@ -725,11 +725,11 @@ def shuffle_entrance_pool(ootworld, pool_type, entrance_pool, target_entrances, raise EntranceShuffleError(f'Entrance placement attempt count exceeded for world {ootworld.player}') def shuffle_entrances(ootworld, pool_type, entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state): - ootworld.multiworld.random.shuffle(entrances) + ootworld.random.shuffle(entrances) for entrance in entrances: if entrance.connected_region != None: continue - ootworld.multiworld.random.shuffle(target_entrances) + ootworld.random.shuffle(target_entrances) # Here we deliberately introduce bias by prioritizing certain interiors, i.e. the ones most likely to cause problems. # success rate over randomization if pool_type in {'InteriorSoft', 'MixedSoft'}: @@ -785,7 +785,7 @@ def split_entrances_by_requirements(ootworld, entrances_to_split, assumed_entran # TODO: improve this function def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all_state_orig, none_state_orig): - world = ootworld.multiworld + multiworld = ootworld.multiworld player = ootworld.player all_state = all_state_orig.copy() @@ -828,8 +828,8 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all if ootworld.shuffle_interior_entrances and (ootworld.misc_hints or ootworld.hints != 'none') and \ (entrance_placed == None or entrance_placed.type in ['Interior', 'SpecialInterior']): # Ensure Kak Potion Shop entrances are in the same hint area so there is no ambiguity as to which entrance is used for hints - potion_front = get_entrance_replacing(world.get_region('Kak Potion Shop Front', player), 'Kakariko Village -> Kak Potion Shop Front', player) - potion_back = get_entrance_replacing(world.get_region('Kak Potion Shop Back', player), 'Kak Backyard -> Kak Potion Shop Back', player) + potion_front = get_entrance_replacing(multiworld.get_region('Kak Potion Shop Front', player), 'Kakariko Village -> Kak Potion Shop Front', player) + potion_back = get_entrance_replacing(multiworld.get_region('Kak Potion Shop Back', player), 'Kak Backyard -> Kak Potion Shop Back', player) if potion_front is not None and potion_back is not None and not same_hint_area(potion_front, potion_back): raise EntranceShuffleError('Kak Potion Shop entrances are not in the same hint area') elif (potion_front and not potion_back) or (not potion_front and potion_back): @@ -840,8 +840,8 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all # When cows are shuffled, ensure the same thing for Impa's House, since the cow is reachable from both sides if ootworld.shuffle_cows: - impas_front = get_entrance_replacing(world.get_region('Kak Impas House', player), 'Kakariko Village -> Kak Impas House', player) - impas_back = get_entrance_replacing(world.get_region('Kak Impas House Back', player), 'Kak Impas Ledge -> Kak Impas House Back', player) + impas_front = get_entrance_replacing(multiworld.get_region('Kak Impas House', player), 'Kakariko Village -> Kak Impas House', player) + impas_back = get_entrance_replacing(multiworld.get_region('Kak Impas House Back', player), 'Kak Impas Ledge -> Kak Impas House Back', player) if impas_front is not None and impas_back is not None and not same_hint_area(impas_front, impas_back): raise EntranceShuffleError('Kak Impas House entrances are not in the same hint area') elif (impas_front and not impas_back) or (not impas_front and impas_back): @@ -861,25 +861,25 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all any(region for region in time_travel_state.adult_reachable_regions[player] if region.time_passes)): raise EntranceShuffleError('Time passing is not guaranteed as both ages') - if ootworld.starting_age == 'child' and (world.get_region('Temple of Time', player) not in time_travel_state.adult_reachable_regions[player]): + if ootworld.starting_age == 'child' and (multiworld.get_region('Temple of Time', player) not in time_travel_state.adult_reachable_regions[player]): raise EntranceShuffleError('Path to ToT as adult not guaranteed') - if ootworld.starting_age == 'adult' and (world.get_region('Temple of Time', player) not in time_travel_state.child_reachable_regions[player]): + if ootworld.starting_age == 'adult' and (multiworld.get_region('Temple of Time', player) not in time_travel_state.child_reachable_regions[player]): raise EntranceShuffleError('Path to ToT as child not guaranteed') if (ootworld.shuffle_interior_entrances or ootworld.shuffle_overworld_entrances) and \ (entrance_placed == None or entrance_placed.type in ['Interior', 'SpecialInterior', 'Overworld', 'Spawn', 'WarpSong', 'OwlDrop']): # Ensure big poe shop is always reachable as adult - if world.get_region('Market Guard House', player) not in time_travel_state.adult_reachable_regions[player]: + if multiworld.get_region('Market Guard House', player) not in time_travel_state.adult_reachable_regions[player]: raise EntranceShuffleError('Big Poe Shop access not guaranteed as adult') if ootworld.shopsanity == 'off': # Ensure that Goron and Zora shops are accessible as adult - if world.get_region('GC Shop', player) not in all_state.adult_reachable_regions[player]: + if multiworld.get_region('GC Shop', player) not in all_state.adult_reachable_regions[player]: raise EntranceShuffleError('Goron City Shop not accessible as adult') - if world.get_region('ZD Shop', player) not in all_state.adult_reachable_regions[player]: + if multiworld.get_region('ZD Shop', player) not in all_state.adult_reachable_regions[player]: raise EntranceShuffleError('Zora\'s Domain Shop not accessible as adult') if ootworld.open_forest == 'closed': # Ensure that Kokiri Shop is reachable as child with no items - if world.get_region('KF Kokiri Shop', player) not in none_state.child_reachable_regions[player]: + if multiworld.get_region('KF Kokiri Shop', player) not in none_state.child_reachable_regions[player]: raise EntranceShuffleError('Kokiri Forest Shop not accessible as child in closed forest') diff --git a/worlds/oot/HintList.py b/worlds/oot/HintList.py index b0f20858e747..28a5d37a516a 100644 --- a/worlds/oot/HintList.py +++ b/worlds/oot/HintList.py @@ -1,5 +1,3 @@ -import random - from BaseClasses import LocationProgressType from .Items import OOTItem @@ -28,7 +26,7 @@ class Hint(object): text = "" type = [] - def __init__(self, name, text, type, choice=None): + def __init__(self, name, text, type, rand, choice=None): self.name = name self.type = [type] if not isinstance(type, list) else type @@ -36,31 +34,31 @@ def __init__(self, name, text, type, choice=None): self.text = text else: if choice == None: - self.text = random.choice(text) + self.text = rand.choice(text) else: self.text = text[choice] -def getHint(item, clearer_hint=False): +def getHint(item, rand, clearer_hint=False): if item in hintTable: textOptions, clearText, hintType = hintTable[item] if clearer_hint: if clearText == None: - return Hint(item, textOptions, hintType, 0) - return Hint(item, clearText, hintType) + return Hint(item, textOptions, hintType, rand, 0) + return Hint(item, clearText, hintType, rand) else: - return Hint(item, textOptions, hintType) + return Hint(item, textOptions, hintType, rand) elif isinstance(item, str): - return Hint(item, item, 'generic') + return Hint(item, item, 'generic', rand) else: # is an Item - return Hint(item.name, item.hint_text, 'item') + return Hint(item.name, item.hint_text, 'item', rand) def getHintGroup(group, world): ret = [] for name in hintTable: - hint = getHint(name, world.clearer_hints) + hint = getHint(name, world.random, world.clearer_hints) if hint.name in world.always_hints and group == 'always': hint.type = 'always' @@ -95,7 +93,7 @@ def getHintGroup(group, world): def getRequiredHints(world): ret = [] for name in hintTable: - hint = getHint(name) + hint = getHint(name, world.random) if 'always' in hint.type or hint.name in conditional_always and conditional_always[hint.name](world): ret.append(hint) return ret @@ -1689,7 +1687,7 @@ def hintExclusions(world, clear_cache=False): location_hints = [] for name in hintTable: - hint = getHint(name, world.clearer_hints) + hint = getHint(name, world.random, world.clearer_hints) if any(item in hint.type for item in ['always', 'dual_always', diff --git a/worlds/oot/Hints.py b/worlds/oot/Hints.py index e63e135e5045..c01241d04832 100644 --- a/worlds/oot/Hints.py +++ b/worlds/oot/Hints.py @@ -136,13 +136,13 @@ def getItemGenericName(item): def isRestrictedDungeonItem(dungeon, item): if not isinstance(item, OOTItem): return False - if (item.map or item.compass) and dungeon.multiworld.shuffle_mapcompass == 'dungeon': + if (item.map or item.compass) and dungeon.world.options.shuffle_mapcompass == 'dungeon': return item in dungeon.dungeon_items - if item.type == 'SmallKey' and dungeon.multiworld.shuffle_smallkeys == 'dungeon': + if item.type == 'SmallKey' and dungeon.world.options.shuffle_smallkeys == 'dungeon': return item in dungeon.small_keys - if item.type == 'BossKey' and dungeon.multiworld.shuffle_bosskeys == 'dungeon': + if item.type == 'BossKey' and dungeon.world.options.shuffle_bosskeys == 'dungeon': return item in dungeon.boss_key - if item.type == 'GanonBossKey' and dungeon.multiworld.shuffle_ganon_bosskey == 'dungeon': + if item.type == 'GanonBossKey' and dungeon.world.options.shuffle_ganon_bosskey == 'dungeon': return item in dungeon.boss_key return False @@ -261,8 +261,8 @@ def filterTrailingSpace(text): '', ] -def getSimpleHintNoPrefix(item): - hint = getHint(item.name, True).text +def getSimpleHintNoPrefix(item, rand): + hint = getHint(item.name, rand, True).text for prefix in hintPrefixes: if hint.startswith(prefix): @@ -417,9 +417,9 @@ def is_dungeon_item(self, item): # Formats the hint text for this area with proper grammar. # Dungeons are hinted differently depending on the clearer_hints setting. - def text(self, clearer_hints, preposition=False, world=None): + def text(self, rand, clearer_hints, preposition=False, world=None): if self.is_dungeon: - text = getHint(self.dungeon_name, clearer_hints).text + text = getHint(self.dungeon_name, rand, clearer_hints).text else: text = str(self) prefix, suffix = text.replace('#', '').split(' ', 1) @@ -489,7 +489,7 @@ def get_woth_hint(world, checked): if getattr(location.parent_region, "dungeon", None): world.woth_dungeon += 1 - location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text + location_text = getHint(location.parent_region.dungeon.name, world.random, world.clearer_hints).text else: location_text = get_hint_area(location) @@ -570,9 +570,9 @@ def get_good_item_hint(world, checked): location = world.hint_rng.choice(locations) checked[location.player].add(location.name) - item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text + item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text if getattr(location.parent_region, "dungeon", None): - location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text + location_text = getHint(location.parent_region.dungeon.name, world.hint_rng, world.clearer_hints).text return (GossipText('#%s# hoards #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)), ['Green', 'Red']), location) else: @@ -613,10 +613,10 @@ def get_specific_item_hint(world, checked): location = world.hint_rng.choice(locations) checked[location.player].add(location.name) - item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text + item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text if getattr(location.parent_region, "dungeon", None): - location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text + location_text = getHint(location.parent_region.dungeon.name, world.hint_rng, world.clearer_hints).text if world.hint_dist_user.get('vague_named_items', False): return (GossipText('#%s# may be on the hero\'s path.' % (location_text), ['Green']), location) else: @@ -648,9 +648,9 @@ def get_random_location_hint(world, checked): checked[location.player].add(location.name) dungeon = location.parent_region.dungeon - item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text + item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text if dungeon: - location_text = getHint(dungeon.name, world.clearer_hints).text + location_text = getHint(dungeon.name, world.hint_rng, world.clearer_hints).text return (GossipText('#%s# hoards #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)), ['Green', 'Red']), location) else: @@ -675,7 +675,7 @@ def get_specific_hint(world, checked, type): location_text = hint.text if '#' not in location_text: location_text = '#%s#' % location_text - item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text + item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text return (GossipText('%s #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)), ['Green', 'Red']), location) @@ -724,9 +724,9 @@ def get_entrance_hint(world, checked): connected_region = entrance.connected_region if connected_region.dungeon: - region_text = getHint(connected_region.dungeon.name, world.clearer_hints).text + region_text = getHint(connected_region.dungeon.name, world.hint_rng, world.clearer_hints).text else: - region_text = getHint(connected_region.name, world.clearer_hints).text + region_text = getHint(connected_region.name, world.hint_rng, world.clearer_hints).text if '#' not in region_text: region_text = '#%s#' % region_text @@ -882,10 +882,10 @@ def buildWorldGossipHints(world, checkedLocations=None): if location.name in world.hint_text_overrides: location_text = world.hint_text_overrides[location.name] else: - location_text = getHint(location.name, world.clearer_hints).text + location_text = getHint(location.name, world.hint_rng, world.clearer_hints).text if '#' not in location_text: location_text = '#%s#' % location_text - item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text + item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text add_hint(world, stoneGroups, GossipText('%s #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)), ['Green', 'Red']), hint_dist['always'][1], location, force_reachable=True) logging.getLogger('').debug('Placed always hint for %s.', location.name) @@ -1003,16 +1003,16 @@ def buildAltarHints(world, messages, include_rewards=True, include_wincons=True) ('Goron Ruby', 'Red'), ('Zora Sapphire', 'Blue'), ] - child_text += getHint('Spiritual Stone Text Start', world.clearer_hints).text + '\x04' + child_text += getHint('Spiritual Stone Text Start', world.hint_rng, world.clearer_hints).text + '\x04' for (reward, color) in bossRewardsSpiritualStones: child_text += buildBossString(reward, color, world) - child_text += getHint('Child Altar Text End', world.clearer_hints).text + child_text += getHint('Child Altar Text End', world.hint_rng, world.clearer_hints).text child_text += '\x0B' update_message_by_id(messages, 0x707A, get_raw_text(child_text), 0x20) # text that appears at altar as an adult. adult_text = '\x08' - adult_text += getHint('Adult Altar Text Start', world.clearer_hints).text + '\x04' + adult_text += getHint('Adult Altar Text Start', world.hint_rng, world.clearer_hints).text + '\x04' if include_rewards: bossRewardsMedallions = [ ('Light Medallion', 'Light Blue'), @@ -1029,7 +1029,7 @@ def buildAltarHints(world, messages, include_rewards=True, include_wincons=True) adult_text += '\x04' adult_text += buildGanonBossKeyString(world) else: - adult_text += getHint('Adult Altar Text End', world.clearer_hints).text + adult_text += getHint('Adult Altar Text End', world.hint_rng, world.clearer_hints).text adult_text += '\x0B' update_message_by_id(messages, 0x7057, get_raw_text(adult_text), 0x20) @@ -1044,7 +1044,7 @@ def buildBossString(reward, color, world): text = GossipText(f"\x08\x13{item_icon}One in #@'s pocket#...", [color], prefix='') else: location = world.hinted_dungeon_reward_locations[reward] - location_text = HintArea.at(location).text(world.clearer_hints, preposition=True) + location_text = HintArea.at(location).text(world.hint_rng, world.clearer_hints, preposition=True) text = GossipText(f"\x08\x13{item_icon}One {location_text}...", [color], prefix='') return str(text) + '\x04' @@ -1054,7 +1054,7 @@ def buildBridgeReqsString(world): if world.bridge == 'open': string += "The awakened ones will have #already created a bridge# to the castle where the evil dwells." else: - item_req_string = getHint('bridge_' + world.bridge, world.clearer_hints).text + item_req_string = getHint('bridge_' + world.bridge, world.hint_rng, world.clearer_hints).text if world.bridge == 'medallions': item_req_string = str(world.bridge_medallions) + ' ' + item_req_string elif world.bridge == 'stones': @@ -1077,7 +1077,7 @@ def buildGanonBossKeyString(world): string += "And the door to the \x05\x41evil one\x05\x40's chamber will be left #unlocked#." else: if world.shuffle_ganon_bosskey == 'on_lacs': - item_req_string = getHint('lacs_' + world.lacs_condition, world.clearer_hints).text + item_req_string = getHint('lacs_' + world.lacs_condition, world.hint_rng, world.clearer_hints).text if world.lacs_condition == 'medallions': item_req_string = str(world.lacs_medallions) + ' ' + item_req_string elif world.lacs_condition == 'stones': @@ -1092,7 +1092,7 @@ def buildGanonBossKeyString(world): item_req_string = '#%s#' % item_req_string bk_location_string = "provided by Zelda once %s are retrieved" % item_req_string elif world.shuffle_ganon_bosskey in ['stones', 'medallions', 'dungeons', 'tokens', 'hearts']: - item_req_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.clearer_hints).text + item_req_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.hint_rng, world.clearer_hints).text if world.shuffle_ganon_bosskey == 'medallions': item_req_string = str(world.ganon_bosskey_medallions) + ' ' + item_req_string elif world.shuffle_ganon_bosskey == 'stones': @@ -1107,7 +1107,7 @@ def buildGanonBossKeyString(world): item_req_string = '#%s#' % item_req_string bk_location_string = "automatically granted once %s are retrieved" % item_req_string else: - bk_location_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.clearer_hints).text + bk_location_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.hint_rng, world.clearer_hints).text string += "And the \x05\x41evil one\x05\x40's key will be %s." % bk_location_string return str(GossipText(string, ['Yellow'], prefix='')) @@ -1142,16 +1142,16 @@ def buildMiscItemHints(world, messages): if location.player != world.player: player_text = world.multiworld.get_player_name(location.player) + "'s " if location.game == 'Ocarina of Time': - area = HintArea.at(location, use_alt_hint=data['use_alt_hint']).text(world.clearer_hints, world=None) + area = HintArea.at(location, use_alt_hint=data['use_alt_hint']).text(world.hint_rng, world.clearer_hints, world=None) else: area = location.name text = data['default_item_text'].format(area=rom_safe_text(player_text + area)) elif 'default_item_fallback' in data: text = data['default_item_fallback'] else: - text = getHint('Validation Line', world.clearer_hints).text + text = getHint('Validation Line', world.hint_rng, world.clearer_hints).text location = world.get_location('Ganons Tower Boss Key Chest') - text += f"#{getHint(getItemGenericName(location.item), world.clearer_hints).text}#" + text += f"#{getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text}#" for find, replace in data.get('replace', {}).items(): text = text.replace(find, replace) @@ -1165,7 +1165,7 @@ def buildMiscLocationHints(world, messages): if hint_type in world.misc_hints: location = world.get_location(data['item_location']) item = location.item - item_text = getHint(getItemGenericName(item), world.clearer_hints).text + item_text = getHint(getItemGenericName(item), world.hint_rng, world.clearer_hints).text if item.player != world.player: item_text += f' for {world.multiworld.get_player_name(item.player)}' text = data['location_text'].format(item=rom_safe_text(item_text)) diff --git a/worlds/oot/ItemPool.py b/worlds/oot/ItemPool.py index 6ca6bc9268a9..805d1fc72dd2 100644 --- a/worlds/oot/ItemPool.py +++ b/worlds/oot/ItemPool.py @@ -295,16 +295,14 @@ def get_spec(tup, key, default): def get_junk_pool(ootworld): junk_pool[:] = list(junk_pool_base) - if ootworld.junk_ice_traps == 'on': + if ootworld.options.junk_ice_traps == 'on': junk_pool.append(('Ice Trap', 10)) - elif ootworld.junk_ice_traps in ['mayhem', 'onslaught']: + elif ootworld.options.junk_ice_traps in ['mayhem', 'onslaught']: junk_pool[:] = [('Ice Trap', 1)] return junk_pool -def get_junk_item(count=1, pool=None, plando_pool=None): - global random - +def get_junk_item(rand, count=1, pool=None, plando_pool=None): if count < 1: raise ValueError("get_junk_item argument 'count' must be greater than 0.") @@ -323,17 +321,17 @@ def get_junk_item(count=1, pool=None, plando_pool=None): raise RuntimeError("Not enough junk is available in the item pool to replace removed items.") else: junk_items, junk_weights = zip(*junk_pool) - return_pool.extend(random.choices(junk_items, weights=junk_weights, k=count)) + return_pool.extend(rand.choices(junk_items, weights=junk_weights, k=count)) return return_pool -def replace_max_item(items, item, max): +def replace_max_item(items, item, max, rand): count = 0 for i,val in enumerate(items): if val == item: if count >= max: - items[i] = get_junk_item()[0] + items[i] = get_junk_item(rand)[0] count += 1 @@ -375,7 +373,7 @@ def get_pool_core(world): pending_junk_pool.append('Kokiri Sword') if world.shuffle_ocarinas: pending_junk_pool.append('Ocarina') - if world.shuffle_beans and world.multiworld.start_inventory[world.player].value.get('Magic Bean Pack', 0): + if world.shuffle_beans and world.options.start_inventory.value.get('Magic Bean Pack', 0): pending_junk_pool.append('Magic Bean Pack') if (world.gerudo_fortress != "open" and world.shuffle_hideoutkeys in ['any_dungeon', 'overworld', 'keysanity', 'regional']): @@ -450,7 +448,7 @@ def get_pool_core(world): else: item = deku_scrubs_items[location.vanilla_item] if isinstance(item, list): - item = random.choices([i[0] for i in item], weights=[i[1] for i in item], k=1)[0] + item = world.random.choices([i[0] for i in item], weights=[i[1] for i in item], k=1)[0] shuffle_item = True # Kokiri Sword @@ -489,7 +487,7 @@ def get_pool_core(world): # Cows elif location.vanilla_item == 'Milk': if world.shuffle_cows: - item = get_junk_item()[0] + item = get_junk_item(world.random)[0] shuffle_item = world.shuffle_cows if not shuffle_item: location.show_in_spoiler = False @@ -508,13 +506,13 @@ def get_pool_core(world): item = 'Rutos Letter' ruto_bottles -= 1 else: - item = random.choice(normal_bottles) + item = world.random.choice(normal_bottles) shuffle_item = True # Magic Beans elif location.vanilla_item == 'Buy Magic Bean': if world.shuffle_beans: - item = 'Magic Bean Pack' if not world.multiworld.start_inventory[world.player].value.get('Magic Bean Pack', 0) else get_junk_item()[0] + item = 'Magic Bean Pack' if not world.options.start_inventory.value.get('Magic Bean Pack', 0) else get_junk_item(world.random)[0] shuffle_item = world.shuffle_beans if not shuffle_item: location.show_in_spoiler = False @@ -528,7 +526,7 @@ def get_pool_core(world): # Adult Trade Item elif location.vanilla_item == 'Pocket Egg': potential_trade_items = world.adult_trade_start if world.adult_trade_start else trade_items - item = random.choice(sorted(potential_trade_items)) + item = world.random.choice(sorted(potential_trade_items)) world.selected_adult_trade_item = item shuffle_item = True @@ -541,7 +539,7 @@ def get_pool_core(world): shuffle_item = False location.show_in_spoiler = False if shuffle_item and world.gerudo_fortress == 'normal' and 'Thieves Hideout' in world.key_rings: - item = get_junk_item()[0] if location.name != 'Hideout 1 Torch Jail Gerudo Key' else 'Small Key Ring (Thieves Hideout)' + item = get_junk_item(world.random)[0] if location.name != 'Hideout 1 Torch Jail Gerudo Key' else 'Small Key Ring (Thieves Hideout)' # Freestanding Rupees and Hearts elif location.type in ['ActorOverride', 'Freestanding', 'RupeeTower']: @@ -618,7 +616,7 @@ def get_pool_core(world): elif dungeon.name in world.key_rings and not dungeon.small_keys: item = dungeon.item_name("Small Key Ring") elif dungeon.name in world.key_rings: - item = get_junk_item()[0] + item = get_junk_item(world.random)[0] shuffle_item = True # Any other item in a dungeon. elif location.type in ["Chest", "NPC", "Song", "Collectable", "Cutscene", "BossHeart"]: @@ -630,7 +628,7 @@ def get_pool_core(world): if shuffle_setting in ['remove', 'startwith']: world.multiworld.push_precollected(dungeon_collection[-1]) world.remove_from_start_inventory.append(dungeon_collection[-1].name) - item = get_junk_item()[0] + item = get_junk_item(world.random)[0] shuffle_item = True elif shuffle_setting in ['any_dungeon', 'overworld', 'regional']: dungeon_collection[-1].priority = True @@ -658,9 +656,9 @@ def get_pool_core(world): shop_non_item_count = len(world.shop_prices) shop_item_count = shop_slots_count - shop_non_item_count - pool.extend(random.sample(remain_shop_items, shop_item_count)) + pool.extend(world.random.sample(remain_shop_items, shop_item_count)) if shop_non_item_count: - pool.extend(get_junk_item(shop_non_item_count)) + pool.extend(get_junk_item(world.random, shop_non_item_count)) # Extra rupees for shopsanity. if world.shopsanity not in ['off', '0']: @@ -706,19 +704,19 @@ def get_pool_core(world): if world.shuffle_ganon_bosskey in ['stones', 'medallions', 'dungeons', 'tokens', 'hearts', 'triforce']: placed_items['Gift from Sages'] = 'Boss Key (Ganons Castle)' - pool.extend(get_junk_item()) + pool.extend(get_junk_item(world.random)) else: placed_items['Gift from Sages'] = IGNORE_LOCATION world.get_location('Gift from Sages').show_in_spoiler = False if world.junk_ice_traps == 'off': - replace_max_item(pool, 'Ice Trap', 0) + replace_max_item(pool, 'Ice Trap', 0, world.random) elif world.junk_ice_traps == 'onslaught': for item in [item for item, weight in junk_pool_base] + ['Recovery Heart', 'Bombs (20)', 'Arrows (30)']: - replace_max_item(pool, item, 0) + replace_max_item(pool, item, 0, world.random) for item, maximum in item_difficulty_max[world.item_pool_value].items(): - replace_max_item(pool, item, maximum) + replace_max_item(pool, item, maximum, world.random) # world.distribution.alter_pool(world, pool) @@ -748,7 +746,7 @@ def get_pool_core(world): pending_item = pending_junk_pool.pop() if not junk_candidates: raise RuntimeError("Not enough junk exists in item pool for %s (+%d others) to be added." % (pending_item, len(pending_junk_pool) - 1)) - junk_item = random.choice(junk_candidates) + junk_item = world.random.choice(junk_candidates) junk_candidates.remove(junk_item) pool.remove(junk_item) pool.append(pending_item) diff --git a/worlds/oot/Messages.py b/worlds/oot/Messages.py index 25c2a9934dd4..5059c01f3c8d 100644 --- a/worlds/oot/Messages.py +++ b/worlds/oot/Messages.py @@ -1,6 +1,5 @@ # text details: https://wiki.cloudmodding.com/oot/Text_Format -import random from .HintList import misc_item_hint_table, misc_location_hint_table from .TextBox import line_wrap from .Utils import find_last @@ -969,7 +968,7 @@ def repack_messages(rom, messages, permutation=None, always_allow_skip=True, spe rom.write_bytes(entry_offset, [0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) # shuffles the messages in the game, making sure to keep various message types in their own group -def shuffle_messages(messages, except_hints=True, always_allow_skip=True): +def shuffle_messages(messages, rand, except_hints=True, always_allow_skip=True): permutation = [i for i, _ in enumerate(messages)] @@ -1002,7 +1001,7 @@ def is_exempt(m): def shuffle_group(group): group_permutation = [i for i, _ in enumerate(group)] - random.shuffle(group_permutation) + rand.shuffle(group_permutation) for index_from, index_to in enumerate(group_permutation): permutation[group[index_to].index] = group[index_from].index diff --git a/worlds/oot/Music.py b/worlds/oot/Music.py index 6ed1ab54ae5d..1bb3b65aac3f 100644 --- a/worlds/oot/Music.py +++ b/worlds/oot/Music.py @@ -1,6 +1,5 @@ #Much of this is heavily inspired from and/or based on az64's / Deathbasket's MM randomizer -import random import os from .Utils import compare_version, data_path @@ -175,7 +174,7 @@ def process_sequences(rom, sequences, target_sequences, disabled_source_sequence return sequences, target_sequences -def shuffle_music(sequences, target_sequences, music_mapping, log): +def shuffle_music(sequences, target_sequences, music_mapping, log, rand): sequence_dict = {} sequence_ids = [] @@ -191,7 +190,7 @@ def shuffle_music(sequences, target_sequences, music_mapping, log): # Shuffle the sequences if len(sequences) < len(target_sequences): raise Exception(f"Not enough custom music/fanfares ({len(sequences)}) to omit base Ocarina of Time sequences ({len(target_sequences)}).") - random.shuffle(sequence_ids) + rand.shuffle(sequence_ids) sequences = [] for target_sequence in target_sequences: @@ -328,7 +327,7 @@ def rebuild_sequences(rom, sequences): rom.write_byte(base, j.instrument_set) -def shuffle_pointers_table(rom, ids, music_mapping, log): +def shuffle_pointers_table(rom, ids, music_mapping, log, rand): # Read in all the Music data bgm_data = {} bgm_ids = [] @@ -341,7 +340,7 @@ def shuffle_pointers_table(rom, ids, music_mapping, log): bgm_ids.append(bgm[0]) # shuffle data - random.shuffle(bgm_ids) + rand.shuffle(bgm_ids) # Write Music data back in random ordering for bgm in ids: @@ -424,13 +423,13 @@ def randomize_music(rom, ootworld, music_mapping): # process_sequences(rom, sequences, target_sequences, disabled_source_sequences, disabled_target_sequences, bgm_ids) # if ootworld.background_music == 'random_custom_only': # sequences = [seq for seq in sequences if seq.cosmetic_name not in [x[0] for x in bgm_ids] or seq.cosmetic_name in music_mapping.values()] - # sequences, log = shuffle_music(sequences, target_sequences, music_mapping, log) + # sequences, log = shuffle_music(sequences, target_sequences, music_mapping, log, ootworld.random) # if ootworld.fanfares in ['random', 'random_custom_only'] or ff_mapped or ocarina_mapped: # process_sequences(rom, fanfare_sequences, fanfare_target_sequences, disabled_source_sequences, disabled_target_sequences, ff_ids, 'fanfare') # if ootworld.fanfares == 'random_custom_only': # fanfare_sequences = [seq for seq in fanfare_sequences if seq.cosmetic_name not in [x[0] for x in fanfare_sequence_ids] or seq.cosmetic_name in music_mapping.values()] - # fanfare_sequences, log = shuffle_music(fanfare_sequences, fanfare_target_sequences, music_mapping, log) + # fanfare_sequences, log = shuffle_music(fanfare_sequences, fanfare_target_sequences, music_mapping, log, ootworld.random) # if disabled_source_sequences: # log = disable_music(rom, disabled_source_sequences.values(), log) @@ -438,10 +437,10 @@ def randomize_music(rom, ootworld, music_mapping): # rebuild_sequences(rom, sequences + fanfare_sequences) # else: if ootworld.background_music == 'randomized' or bgm_mapped: - log = shuffle_pointers_table(rom, bgm_ids, music_mapping, log) + log = shuffle_pointers_table(rom, bgm_ids, music_mapping, log, ootworld.random) if ootworld.fanfares == 'randomized' or ff_mapped or ocarina_mapped: - log = shuffle_pointers_table(rom, ff_ids, music_mapping, log) + log = shuffle_pointers_table(rom, ff_ids, music_mapping, log, ootworld.random) # end_else if disabled_target_sequences: log = disable_music(rom, disabled_target_sequences.values(), log) diff --git a/worlds/oot/N64Patch.py b/worlds/oot/N64Patch.py index 5af3279e8077..3013a94a8e3b 100644 --- a/worlds/oot/N64Patch.py +++ b/worlds/oot/N64Patch.py @@ -1,5 +1,4 @@ import struct -import random import io import array import zlib @@ -88,7 +87,7 @@ def write_block_section(start, key_skip, in_data, patch_data, is_continue): # xor_range is the range the XOR key will read from. This range is not # too important, but I tried to choose from a section that didn't really # have big gaps of 0s which we want to avoid. -def create_patch_file(rom, xor_range=(0x00B8AD30, 0x00F029A0)): +def create_patch_file(rom, rand, xor_range=(0x00B8AD30, 0x00F029A0)): dma_start, dma_end = rom.get_dma_table_range() # add header @@ -100,7 +99,7 @@ def create_patch_file(rom, xor_range=(0x00B8AD30, 0x00F029A0)): # get random xor key. This range is chosen because it generally # doesn't have many sections of 0s - xor_address = random.Random().randint(*xor_range) + xor_address = rand.randint(*xor_range) patch_data.append_int32(xor_address) new_buffer = copy.copy(rom.original.buffer) diff --git a/worlds/oot/Options.py b/worlds/oot/Options.py index daf072adb59c..613c5d01b381 100644 --- a/worlds/oot/Options.py +++ b/worlds/oot/Options.py @@ -1,6 +1,8 @@ import typing import random -from Options import Option, DefaultOnToggle, Toggle, Range, OptionList, OptionSet, DeathLink, PlandoConnections +from dataclasses import dataclass +from Options import Option, DefaultOnToggle, Toggle, Range, OptionList, OptionSet, DeathLink, PlandoConnections, \ + PerGameCommonOptions, OptionGroup from .EntranceShuffle import entrance_shuffle_table from .LogicTricks import normalized_name_tricks from .ColorSFXOptions import * @@ -1281,21 +1283,166 @@ class LogicTricks(OptionList): valid_keys_casefold = True -# All options assembled into a single dict -oot_options: typing.Dict[str, type(Option)] = { - "plando_connections": OoTPlandoConnections, - "logic_rules": Logic, - "logic_no_night_tokens_without_suns_song": NightTokens, - **open_options, - **world_options, - **bridge_options, - **dungeon_items_options, - **shuffle_options, - **timesavers_options, - **misc_options, - **itempool_options, - **cosmetic_options, - **sfx_options, - "logic_tricks": LogicTricks, - "death_link": DeathLink, -} +@dataclass +class OoTOptions(PerGameCommonOptions): + plando_connections: OoTPlandoConnections + death_link: DeathLink + logic_rules: Logic + logic_no_night_tokens_without_suns_song: NightTokens + logic_tricks: LogicTricks + open_forest: Forest + open_kakariko: Gate + open_door_of_time: DoorOfTime + zora_fountain: Fountain + gerudo_fortress: Fortress + bridge: Bridge + trials: Trials + starting_age: StartingAge + shuffle_interior_entrances: InteriorEntrances + shuffle_grotto_entrances: GrottoEntrances + shuffle_dungeon_entrances: DungeonEntrances + shuffle_overworld_entrances: OverworldEntrances + owl_drops: OwlDrops + warp_songs: WarpSongs + spawn_positions: SpawnPositions + shuffle_bosses: BossEntrances + # mix_entrance_pools: MixEntrancePools + # decouple_entrances: DecoupleEntrances + triforce_hunt: TriforceHunt + triforce_goal: TriforceGoal + extra_triforce_percentage: ExtraTriforces + bombchus_in_logic: LogicalChus + dungeon_shortcuts: DungeonShortcuts + dungeon_shortcuts_list: DungeonShortcutsList + mq_dungeons_mode: MQDungeons + mq_dungeons_list: MQDungeonList + mq_dungeons_count: MQDungeonCount + # empty_dungeons_mode: EmptyDungeons + # empty_dungeons_list: EmptyDungeonList + # empty_dungeon_count: EmptyDungeonCount + bridge_stones: BridgeStones + bridge_medallions: BridgeMedallions + bridge_rewards: BridgeRewards + bridge_tokens: BridgeTokens + bridge_hearts: BridgeHearts + shuffle_mapcompass: ShuffleMapCompass + shuffle_smallkeys: ShuffleKeys + shuffle_hideoutkeys: ShuffleGerudoKeys + shuffle_bosskeys: ShuffleBossKeys + enhance_map_compass: EnhanceMC + shuffle_ganon_bosskey: ShuffleGanonBK + ganon_bosskey_medallions: GanonBKMedallions + ganon_bosskey_stones: GanonBKStones + ganon_bosskey_rewards: GanonBKRewards + ganon_bosskey_tokens: GanonBKTokens + ganon_bosskey_hearts: GanonBKHearts + key_rings: KeyRings + key_rings_list: KeyRingList + shuffle_song_items: SongShuffle + shopsanity: ShopShuffle + shop_slots: ShopSlots + shopsanity_prices: ShopPrices + tokensanity: TokenShuffle + shuffle_scrubs: ScrubShuffle + shuffle_child_trade: ShuffleChildTrade + shuffle_freestanding_items: ShuffleFreestanding + shuffle_pots: ShufflePots + shuffle_crates: ShuffleCrates + shuffle_cows: ShuffleCows + shuffle_beehives: ShuffleBeehives + shuffle_kokiri_sword: ShuffleSword + shuffle_ocarinas: ShuffleOcarinas + shuffle_gerudo_card: ShuffleCard + shuffle_beans: ShuffleBeans + shuffle_medigoron_carpet_salesman: ShuffleMedigoronCarpet + shuffle_frog_song_rupees: ShuffleFrogRupees + no_escape_sequence: SkipEscape + no_guard_stealth: SkipStealth + no_epona_race: SkipEponaRace + skip_some_minigame_phases: SkipMinigamePhases + complete_mask_quest: CompleteMaskQuest + useful_cutscenes: UsefulCutscenes + fast_chests: FastChests + free_scarecrow: FreeScarecrow + fast_bunny_hood: FastBunny + plant_beans: PlantBeans + chicken_count: ChickenCount + big_poe_count: BigPoeCount + fae_torch_count: FAETorchCount + correct_chest_appearances: CorrectChestAppearance + minor_items_as_major_chest: MinorInMajor + invisible_chests: InvisibleChests + correct_potcrate_appearances: CorrectPotCrateAppearance + hints: Hints + misc_hints: MiscHints + hint_dist: HintDistribution + text_shuffle: TextShuffle + damage_multiplier: DamageMultiplier + deadly_bonks: DeadlyBonks + no_collectible_hearts: HeroMode + starting_tod: StartingToD + blue_fire_arrows: BlueFireArrows + fix_broken_drops: FixBrokenDrops + start_with_consumables: ConsumableStart + start_with_rupees: RupeeStart + item_pool_value: ItemPoolValue + junk_ice_traps: IceTraps + ice_trap_appearance: IceTrapVisual + adult_trade_start: AdultTradeStart + default_targeting: Targeting + display_dpad: DisplayDpad + dpad_dungeon_menu: DpadDungeonMenu + correct_model_colors: CorrectColors + background_music: BackgroundMusic + fanfares: Fanfares + ocarina_fanfares: OcarinaFanfares + kokiri_color: kokiri_color + goron_color: goron_color + zora_color: zora_color + silver_gauntlets_color: silver_gauntlets_color + golden_gauntlets_color: golden_gauntlets_color + mirror_shield_frame_color: mirror_shield_frame_color + navi_color_default_inner: navi_color_default_inner + navi_color_default_outer: navi_color_default_outer + navi_color_enemy_inner: navi_color_enemy_inner + navi_color_enemy_outer: navi_color_enemy_outer + navi_color_npc_inner: navi_color_npc_inner + navi_color_npc_outer: navi_color_npc_outer + navi_color_prop_inner: navi_color_prop_inner + navi_color_prop_outer: navi_color_prop_outer + sword_trail_duration: SwordTrailDuration + sword_trail_color_inner: sword_trail_color_inner + sword_trail_color_outer: sword_trail_color_outer + bombchu_trail_color_inner: bombchu_trail_color_inner + bombchu_trail_color_outer: bombchu_trail_color_outer + boomerang_trail_color_inner: boomerang_trail_color_inner + boomerang_trail_color_outer: boomerang_trail_color_outer + heart_color: heart_color + magic_color: magic_color + a_button_color: a_button_color + b_button_color: b_button_color + c_button_color: c_button_color + start_button_color: start_button_color + sfx_navi_overworld: sfx_navi_overworld + sfx_navi_enemy: sfx_navi_enemy + sfx_low_hp: sfx_low_hp + sfx_menu_cursor: sfx_menu_cursor + sfx_menu_select: sfx_menu_select + sfx_nightfall: sfx_nightfall + sfx_horse_neigh: sfx_horse_neigh + sfx_hover_boots: sfx_hover_boots + sfx_ocarina: SfxOcarina + + +oot_option_groups: typing.List[OptionGroup] = [ + OptionGroup("Open", [option for option in open_options.values()]), + OptionGroup("World", [*[option for option in world_options.values()], + *[option for option in bridge_options.values()]]), + OptionGroup("Shuffle", [option for option in shuffle_options.values()]), + OptionGroup("Dungeon Items", [option for option in dungeon_items_options.values()]), + OptionGroup("Timesavers", [option for option in timesavers_options.values()]), + OptionGroup("Misc", [option for option in misc_options.values()]), + OptionGroup("Item Pool", [option for option in itempool_options.values()]), + OptionGroup("Cosmetics", [option for option in cosmetic_options.values()]), + OptionGroup("SFX", [option for option in sfx_options.values()]) +] diff --git a/worlds/oot/Patches.py b/worlds/oot/Patches.py index 2219d7bb95a8..561d7c3f7b6e 100644 --- a/worlds/oot/Patches.py +++ b/worlds/oot/Patches.py @@ -208,8 +208,8 @@ def patch_rom(world, rom): # Fix Ice Cavern Alcove Camera if not world.dungeon_mq['Ice Cavern']: - rom.write_byte(0x2BECA25,0x01); - rom.write_byte(0x2BECA2D,0x01); + rom.write_byte(0x2BECA25,0x01) + rom.write_byte(0x2BECA2D,0x01) # Fix GS rewards to be static rom.write_int32(0xEA3934, 0) @@ -944,7 +944,7 @@ def add_scene_exits(scene_start, offset = 0): scene_table = 0x00B71440 for scene in range(0x00, 0x65): - scene_start = rom.read_int32(scene_table + (scene * 0x14)); + scene_start = rom.read_int32(scene_table + (scene * 0x14)) add_scene_exits(scene_start) return exit_table @@ -1632,10 +1632,10 @@ def set_entrance_updates(entrances): reward_text = None elif getattr(location.item, 'looks_like_item', None) is not None: jabu_item = location.item.looks_like_item - reward_text = create_fake_name(getHint(getItemGenericName(location.item.looks_like_item), True).text) + reward_text = create_fake_name(getHint(getItemGenericName(location.item.looks_like_item), world.hint_rng, True).text) else: jabu_item = location.item - reward_text = getHint(getItemGenericName(location.item), True).text + reward_text = getHint(getItemGenericName(location.item), world.hint_rng, True).text # Update "Princess Ruto got the Spiritual Stone!" text before the midboss in Jabu if reward_text is None: @@ -1687,7 +1687,7 @@ def set_entrance_updates(entrances): # Sets hooks for gossip stone changes - symbol = rom.sym("GOSSIP_HINT_CONDITION"); + symbol = rom.sym("GOSSIP_HINT_CONDITION") if world.hints == 'none': rom.write_int32(symbol, 0) @@ -2264,9 +2264,9 @@ def update_scrub_text(message, text_replacement, default_price, price, item_name # text shuffle if world.text_shuffle == 'except_hints': - permutation = shuffle_messages(messages, except_hints=True) + permutation = shuffle_messages(messages, world.random, except_hints=True) elif world.text_shuffle == 'complete': - permutation = shuffle_messages(messages, except_hints=False) + permutation = shuffle_messages(messages, world.random, except_hints=False) # update warp song preview text boxes update_warp_song_text(messages, world) @@ -2358,7 +2358,7 @@ def update_scrub_text(message, text_replacement, default_price, price, item_name # Write numeric seed truncated to 32 bits for rng seeding # Overwritten with new seed every time a new rng value is generated - rng_seed = world.multiworld.per_slot_randoms[world.player].getrandbits(32) + rng_seed = world.random.getrandbits(32) rom.write_int32(rom.sym('RNG_SEED_INT'), rng_seed) # Static initial seed value for one-time random actions like the Hylian Shield discount rom.write_int32(rom.sym('RANDOMIZER_RNG_SEED'), rng_seed) @@ -2560,7 +2560,7 @@ def scene_get_actors(rom, actor_func, scene_data, scene, alternate=None, process room_count = rom.read_byte(scene_data + 1) room_list = scene_start + (rom.read_int32(scene_data + 4) & 0x00FFFFFF) for _ in range(0, room_count): - room_data = rom.read_int32(room_list); + room_data = rom.read_int32(room_list) if not room_data in processed_rooms: actors.update(room_get_actors(rom, actor_func, room_data, scene)) @@ -2591,7 +2591,7 @@ def get_actor_list(rom, actor_func): actors = {} scene_table = 0x00B71440 for scene in range(0x00, 0x65): - scene_data = rom.read_int32(scene_table + (scene * 0x14)); + scene_data = rom.read_int32(scene_table + (scene * 0x14)) actors.update(scene_get_actors(rom, actor_func, scene_data, scene)) return actors @@ -2605,7 +2605,7 @@ def get_override_itemid(override_table, scene, type, flags): def remove_entrance_blockers(rom): def remove_entrance_blockers_do(rom, actor_id, actor, scene): if actor_id == 0x014E and scene == 97: - actor_var = rom.read_int16(actor + 14); + actor_var = rom.read_int16(actor + 14) if actor_var == 0xFF01: rom.write_int16(actor + 14, 0x0700) get_actor_list(rom, remove_entrance_blockers_do) @@ -2789,7 +2789,7 @@ def place_shop_items(rom, world, shop_items, messages, locations, init_shop_id=F purchase_text = '\x08%s %d Rupees\x09\x01%s\x01\x1B\x05\x42Buy\x01Don\'t buy\x05\x40\x02' % (split_item_name[0], location.price, split_item_name[1]) else: if item_display.game == "Ocarina of Time": - shop_item_name = getSimpleHintNoPrefix(item_display) + shop_item_name = getSimpleHintNoPrefix(item_display, world.random) else: shop_item_name = item_display.name diff --git a/worlds/oot/RuleParser.py b/worlds/oot/RuleParser.py index 0791ad5d1a3f..e5390474b779 100644 --- a/worlds/oot/RuleParser.py +++ b/worlds/oot/RuleParser.py @@ -53,7 +53,7 @@ def isliteral(expr): class Rule_AST_Transformer(ast.NodeTransformer): def __init__(self, world, player): - self.multiworld = world + self.world = world self.player = player self.events = set() # map Region -> rule ast string -> item name @@ -86,9 +86,9 @@ def visit_Name(self, node): ctx=ast.Load()), args=[ast.Str(escaped_items[node.id]), ast.Constant(self.player)], keywords=[]) - elif node.id in self.multiworld.__dict__: + elif node.id in self.world.__dict__: # Settings are constant - return ast.parse('%r' % self.multiworld.__dict__[node.id], mode='eval').body + return ast.parse('%r' % self.world.__dict__[node.id], mode='eval').body elif node.id in State.__dict__: return self.make_call(node, node.id, [], []) elif node.id in self.kwarg_defaults or node.id in allowed_globals: @@ -137,7 +137,7 @@ def visit_Tuple(self, node): if isinstance(count, ast.Name): # Must be a settings constant - count = ast.parse('%r' % self.multiworld.__dict__[count.id], mode='eval').body + count = ast.parse('%r' % self.world.__dict__[count.id], mode='eval').body if iname in escaped_items: iname = escaped_items[iname] @@ -182,7 +182,7 @@ def visit_Call(self, node): new_args = [] for child in node.args: if isinstance(child, ast.Name): - if child.id in self.multiworld.__dict__: + if child.id in self.world.__dict__: # child = ast.Attribute( # value=ast.Attribute( # value=ast.Name(id='state', ctx=ast.Load()), @@ -190,7 +190,7 @@ def visit_Call(self, node): # ctx=ast.Load()), # attr=child.id, # ctx=ast.Load()) - child = ast.Constant(getattr(self.multiworld, child.id)) + child = ast.Constant(getattr(self.world, child.id)) elif child.id in rule_aliases: child = self.visit(child) elif child.id in escaped_items: @@ -242,7 +242,7 @@ def escape_or_string(n): # Fast check for json can_use if (len(node.ops) == 1 and isinstance(node.ops[0], ast.Eq) and isinstance(node.left, ast.Name) and isinstance(node.comparators[0], ast.Name) - and node.left.id not in self.multiworld.__dict__ and node.comparators[0].id not in self.multiworld.__dict__): + and node.left.id not in self.world.__dict__ and node.comparators[0].id not in self.world.__dict__): return ast.NameConstant(node.left.id == node.comparators[0].id) node.left = escape_or_string(node.left) @@ -378,7 +378,7 @@ def replace_subrule(self, target, node): # Requires the target regions have been defined in the world. def create_delayed_rules(self): for region_name, node, subrule_name in self.delayed_rules: - region = self.multiworld.multiworld.get_region(region_name, self.player) + region = self.world.multiworld.get_region(region_name, self.player) event = OOTLocation(self.player, subrule_name, type='Event', parent=region, internal=True) event.show_in_spoiler = False @@ -395,7 +395,7 @@ def create_delayed_rules(self): set_rule(event, access_rule) region.locations.append(event) - self.multiworld.make_event_item(subrule_name, event) + self.world.make_event_item(subrule_name, event) # Safeguard in case this is called multiple times per world self.delayed_rules.clear() @@ -448,7 +448,7 @@ def here(self, node): ## Handlers for compile-time optimizations (former State functions) def at_day(self, node): - if self.multiworld.ensure_tod_access: + if self.world.ensure_tod_access: # tod has DAY or (tod == NONE and (ss or find a path from a provider)) # parsing is better than constructing this expression by hand r = self.current_spot if type(self.current_spot) == OOTRegion else self.current_spot.parent_region @@ -456,7 +456,7 @@ def at_day(self, node): return ast.NameConstant(True) def at_dampe_time(self, node): - if self.multiworld.ensure_tod_access: + if self.world.ensure_tod_access: # tod has DAMPE or (tod == NONE and (find a path from a provider)) # parsing is better than constructing this expression by hand r = self.current_spot if type(self.current_spot) == OOTRegion else self.current_spot.parent_region @@ -464,10 +464,10 @@ def at_dampe_time(self, node): return ast.NameConstant(True) def at_night(self, node): - if self.current_spot.type == 'GS Token' and self.multiworld.logic_no_night_tokens_without_suns_song: + if self.current_spot.type == 'GS Token' and self.world.logic_no_night_tokens_without_suns_song: # Using visit here to resolve 'can_play' rule return self.visit(ast.parse('can_play(Suns_Song)', mode='eval').body) - if self.multiworld.ensure_tod_access: + if self.world.ensure_tod_access: # tod has DAMPE or (tod == NONE and (ss or find a path from a provider)) # parsing is better than constructing this expression by hand r = self.current_spot if type(self.current_spot) == OOTRegion else self.current_spot.parent_region @@ -501,7 +501,7 @@ def current_spot_adult_access(self, node): return ast.parse(f"state._oot_reach_as_age('{r.name}', 'adult', {self.player})", mode='eval').body def current_spot_starting_age_access(self, node): - return self.current_spot_child_access(node) if self.multiworld.starting_age == 'child' else self.current_spot_adult_access(node) + return self.current_spot_child_access(node) if self.world.starting_age == 'child' else self.current_spot_adult_access(node) def has_bottle(self, node): return ast.parse(f"state._oot_has_bottle({self.player})", mode='eval').body diff --git a/worlds/oot/Rules.py b/worlds/oot/Rules.py index 36563a3f9f27..00f4aeb4b7d5 100644 --- a/worlds/oot/Rules.py +++ b/worlds/oot/Rules.py @@ -10,7 +10,7 @@ from BaseClasses import CollectionState, MultiWorld from worlds.generic.Rules import set_rule, add_rule, add_item_rule, forbid_item -from ..AutoWorld import LogicMixin +from worlds.AutoWorld import LogicMixin class OOTLogic(LogicMixin): @@ -132,17 +132,17 @@ def _oot_update_age_reachable_regions(self, player): def set_rules(ootworld): logger = logging.getLogger('') - world = ootworld.multiworld + multiworld = ootworld.multiworld player = ootworld.player if ootworld.logic_rules != 'no_logic': if ootworld.triforce_hunt: - world.completion_condition[player] = lambda state: state.has('Triforce Piece', player, ootworld.triforce_goal) + multiworld.completion_condition[player] = lambda state: state.has('Triforce Piece', player, ootworld.triforce_goal) else: - world.completion_condition[player] = lambda state: state.has('Triforce', player) + multiworld.completion_condition[player] = lambda state: state.has('Triforce', player) # ganon can only carry triforce - world.get_location('Ganon', player).item_rule = lambda item: item.name == 'Triforce' + multiworld.get_location('Ganon', player).item_rule = lambda item: item.name == 'Triforce' # is_child = ootworld.parser.parse_rule('is_child') guarantee_hint = ootworld.parser.parse_rule('guarantee_hint') @@ -156,22 +156,22 @@ def set_rules(ootworld): if (ootworld.dungeon_mq['Forest Temple'] and ootworld.shuffle_bosskeys == 'dungeon' and ootworld.shuffle_smallkeys == 'dungeon' and ootworld.tokensanity == 'off'): # First room chest needs to be a small key. Make sure the boss key isn't placed here. - location = world.get_location('Forest Temple MQ First Room Chest', player) + location = multiworld.get_location('Forest Temple MQ First Room Chest', player) forbid_item(location, 'Boss Key (Forest Temple)', ootworld.player) if ootworld.shuffle_song_items in {'song', 'dungeon'} and not ootworld.songs_as_items: # Sheik in Ice Cavern is the only song location in a dungeon; need to ensure that it cannot be anything else. # This is required if map/compass included, or any_dungeon shuffle. - location = world.get_location('Sheik in Ice Cavern', player) + location = multiworld.get_location('Sheik in Ice Cavern', player) add_item_rule(location, lambda item: oot_is_item_of_type(item, 'Song')) if ootworld.shuffle_child_trade == 'skip_child_zelda': # Song from Impa must be local - location = world.get_location('Song from Impa', player) + location = multiworld.get_location('Song from Impa', player) add_item_rule(location, lambda item: item.player == player) for name in ootworld.always_hints: - add_rule(world.get_location(name, player), guarantee_hint) + add_rule(multiworld.get_location(name, player), guarantee_hint) # TODO: re-add hints once they are working # if location.type == 'HintStone' and ootworld.hints == 'mask': diff --git a/worlds/oot/TextBox.py b/worlds/oot/TextBox.py index a9db47996299..e502d739048f 100644 --- a/worlds/oot/TextBox.py +++ b/worlds/oot/TextBox.py @@ -1,4 +1,4 @@ -import worlds.oot.Messages as Messages +from . import Messages # Least common multiple of all possible character widths. A line wrap must occur when the combined widths of all of the # characters on a line reach this value. diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 94587a41a0f2..b93f60b2a08e 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -20,7 +20,7 @@ from .Regions import OOTRegion, TimeOfDay from .Rules import set_rules, set_shop_rules, set_entrances_based_rules from .RuleParser import Rule_AST_Transformer -from .Options import oot_options +from .Options import OoTOptions, oot_option_groups from .Utils import data_path, read_json from .LocationList import business_scrubs, set_drop_location_names, dungeon_song_locations from .DungeonList import dungeon_table, create_dungeons @@ -30,12 +30,12 @@ from .N64Patch import create_patch_file from .Cosmetics import patch_cosmetics -from Utils import get_options +from settings import get_settings from BaseClasses import MultiWorld, CollectionState, Tutorial, LocationProgressType from Options import Range, Toggle, VerifyKeys, Accessibility, PlandoConnections from Fill import fill_restrictive, fast_fill, FillError from worlds.generic.Rules import exclusion_rules, add_item_rule -from ..AutoWorld import World, AutoLogicRegister, WebWorld +from worlds.AutoWorld import World, AutoLogicRegister, WebWorld # OoT's generate_output doesn't benefit from more than 2 threads, instead it uses a lot of memory. i_o_limiter = threading.Semaphore(2) @@ -128,6 +128,7 @@ class OOTWeb(WebWorld): ) tutorials = [setup, setup_es, setup_fr, setup_de] + option_groups = oot_option_groups class OOTWorld(World): @@ -137,7 +138,8 @@ class OOTWorld(World): to rescue the Seven Sages, and then confront Ganondorf to save Hyrule! """ game: str = "Ocarina of Time" - option_definitions: dict = oot_options + options_dataclass = OoTOptions + options: OoTOptions settings: typing.ClassVar[OOTSettings] topology_present: bool = True item_name_to_id = {item_name: oot_data_to_ap_id(data, False) for item_name, data in item_table.items() if @@ -195,15 +197,15 @@ def __init__(self, world, player): @classmethod def stage_assert_generate(cls, multiworld: MultiWorld): - rom = Rom(file=get_options()['oot_options']['rom_file']) + rom = Rom(file=get_settings()['oot_options']['rom_file']) # Option parsing, handling incompatible options, building useful-item table def generate_early(self): self.parser = Rule_AST_Transformer(self, self.player) - for (option_name, option) in oot_options.items(): - result = getattr(self.multiworld, option_name)[self.player] + for option_name in self.options_dataclass.type_hints: + result = getattr(self.options, option_name) if isinstance(result, Range): option_value = int(result) elif isinstance(result, Toggle): @@ -223,8 +225,8 @@ def generate_early(self): self.remove_from_start_inventory = [] # some items will be precollected but not in the inventory self.starting_items = Counter() self.songs_as_items = False - self.file_hash = [self.multiworld.random.randint(0, 31) for i in range(5)] - self.connect_name = ''.join(self.multiworld.random.choices(printable, k=16)) + self.file_hash = [self.random.randint(0, 31) for i in range(5)] + self.connect_name = ''.join(self.random.choices(printable, k=16)) self.collectible_flag_addresses = {} # Incompatible option handling @@ -283,7 +285,7 @@ def generate_early(self): local_types.append('BossKey') if self.shuffle_ganon_bosskey != 'keysanity': local_types.append('GanonBossKey') - self.multiworld.local_items[self.player].value |= set(name for name, data in item_table.items() if data[0] in local_types) + self.options.local_items.value |= set(name for name, data in item_table.items() if data[0] in local_types) # If any songs are itemlinked, set songs_as_items for group in self.multiworld.groups.values(): @@ -297,7 +299,7 @@ def generate_early(self): # Determine skipped trials in GT # This needs to be done before the logic rules in GT are parsed trial_list = ['Forest', 'Fire', 'Water', 'Spirit', 'Shadow', 'Light'] - chosen_trials = self.multiworld.random.sample(trial_list, self.trials) # chooses a list of trials to NOT skip + chosen_trials = self.random.sample(trial_list, self.trials) # chooses a list of trials to NOT skip self.skipped_trials = {trial: (trial not in chosen_trials) for trial in trial_list} # Determine tricks in logic @@ -311,8 +313,8 @@ def generate_early(self): # No Logic forces all tricks on, prog balancing off and beatable-only elif self.logic_rules == 'no_logic': - self.multiworld.progression_balancing[self.player].value = False - self.multiworld.accessibility[self.player].value = Accessibility.option_minimal + self.options.progression_balancing.value = False + self.options.accessibility.value = Accessibility.option_minimal for trick in normalized_name_tricks.values(): setattr(self, trick['name'], True) @@ -333,8 +335,8 @@ def generate_early(self): # Set internal names used by the OoT generator self.keysanity = self.shuffle_smallkeys in ['keysanity', 'remove', 'any_dungeon', 'overworld'] - self.trials_random = self.multiworld.trials[self.player].randomized - self.mq_dungeons_random = self.multiworld.mq_dungeons_count[self.player].randomized + self.trials_random = self.options.trials.randomized + self.mq_dungeons_random = self.options.mq_dungeons_count.randomized self.easier_fire_arrow_entry = self.fae_torch_count < 24 if self.misc_hints: @@ -393,8 +395,8 @@ def generate_early(self): elif self.key_rings == 'choose': self.key_rings = self.key_rings_list elif self.key_rings == 'random_dungeons': - self.key_rings = self.multiworld.random.sample(keyring_dungeons, - self.multiworld.random.randint(0, len(keyring_dungeons))) + self.key_rings = self.random.sample(keyring_dungeons, + self.random.randint(0, len(keyring_dungeons))) # Determine which dungeons are MQ. Not compatible with glitched logic. mq_dungeons = set() @@ -405,7 +407,7 @@ def generate_early(self): elif self.mq_dungeons_mode == 'specific': mq_dungeons = self.mq_dungeons_specific elif self.mq_dungeons_mode == 'count': - mq_dungeons = self.multiworld.random.sample(all_dungeons, self.mq_dungeons_count) + mq_dungeons = self.random.sample(all_dungeons, self.mq_dungeons_count) else: self.mq_dungeons_mode = 'count' self.mq_dungeons_count = 0 @@ -425,8 +427,8 @@ def generate_early(self): elif self.dungeon_shortcuts_choice == 'all': self.dungeon_shortcuts = set(shortcut_dungeons) elif self.dungeon_shortcuts_choice == 'random': - self.dungeon_shortcuts = self.multiworld.random.sample(shortcut_dungeons, - self.multiworld.random.randint(0, len(shortcut_dungeons))) + self.dungeon_shortcuts = self.random.sample(shortcut_dungeons, + self.random.randint(0, len(shortcut_dungeons))) # == 'choice', leave as previous else: self.dungeon_shortcuts = set() @@ -576,7 +578,7 @@ def load_regions_from_json(self, file_path): new_exit = OOTEntrance(self.player, self.multiworld, '%s -> %s' % (new_region.name, exit), new_region) new_exit.vanilla_connected_region = exit new_exit.rule_string = rule - if self.multiworld.logic_rules != 'none': + if self.options.logic_rules != 'no_logic': self.parser.parse_spot_rule(new_exit) if new_exit.never: logger.debug('Dropping unreachable exit: %s', new_exit.name) @@ -607,7 +609,7 @@ def set_scrub_prices(self): elif self.shuffle_scrubs == 'random': # this is a random value between 0-99 # average value is ~33 rupees - price = int(self.multiworld.random.betavariate(1, 2) * 99) + price = int(self.random.betavariate(1, 2) * 99) # Set price in the dictionary as well as the location. self.scrub_prices[scrub_item] = price @@ -624,7 +626,7 @@ def random_shop_prices(self): self.shop_prices = {} for region in self.regions: if self.shopsanity == 'random': - shop_item_count = self.multiworld.random.randint(0, 4) + shop_item_count = self.random.randint(0, 4) else: shop_item_count = int(self.shopsanity) @@ -632,17 +634,17 @@ def random_shop_prices(self): if location.type == 'Shop': if location.name[-1:] in shop_item_indexes[:shop_item_count]: if self.shopsanity_prices == 'normal': - self.shop_prices[location.name] = int(self.multiworld.random.betavariate(1.5, 2) * 60) * 5 + self.shop_prices[location.name] = int(self.random.betavariate(1.5, 2) * 60) * 5 elif self.shopsanity_prices == 'affordable': self.shop_prices[location.name] = 10 elif self.shopsanity_prices == 'starting_wallet': - self.shop_prices[location.name] = self.multiworld.random.randrange(0,100,5) + self.shop_prices[location.name] = self.random.randrange(0,100,5) elif self.shopsanity_prices == 'adults_wallet': - self.shop_prices[location.name] = self.multiworld.random.randrange(0,201,5) + self.shop_prices[location.name] = self.random.randrange(0,201,5) elif self.shopsanity_prices == 'giants_wallet': - self.shop_prices[location.name] = self.multiworld.random.randrange(0,501,5) + self.shop_prices[location.name] = self.random.randrange(0,501,5) elif self.shopsanity_prices == 'tycoons_wallet': - self.shop_prices[location.name] = self.multiworld.random.randrange(0,1000,5) + self.shop_prices[location.name] = self.random.randrange(0,1000,5) # Fill boss prizes @@ -667,8 +669,8 @@ def fill_bosses(self, bossCount=9): while bossCount: bossCount -= 1 - self.multiworld.random.shuffle(prizepool) - self.multiworld.random.shuffle(prize_locs) + self.random.shuffle(prizepool) + self.random.shuffle(prize_locs) item = prizepool.pop() loc = prize_locs.pop() loc.place_locked_item(item) @@ -778,7 +780,7 @@ def create_items(self): # Call the junk fill and get a replacement if item in self.itempool: self.itempool.remove(item) - self.itempool.append(self.create_item(*get_junk_item(pool=junk_pool))) + self.itempool.append(self.create_item(*get_junk_item(self.random, pool=junk_pool))) if self.start_with_consumables: self.starting_items['Deku Sticks'] = 30 self.starting_items['Deku Nuts'] = 40 @@ -881,7 +883,7 @@ def prefill_state(base_state): # Prefill shops, songs, and dungeon items items = self.get_pre_fill_items() locations = list(self.multiworld.get_unfilled_locations(self.player)) - self.multiworld.random.shuffle(locations) + self.random.shuffle(locations) # Set up initial state state = CollectionState(self.multiworld) @@ -910,7 +912,7 @@ def prefill_state(base_state): if isinstance(locations, list): for item in stage_items: self.pre_fill_items.remove(item) - self.multiworld.random.shuffle(locations) + self.random.shuffle(locations) fill_restrictive(self.multiworld, prefill_state(state), locations, stage_items, single_player_placement=True, lock=True, allow_excluded=True) else: @@ -923,7 +925,7 @@ def prefill_state(base_state): if isinstance(locations, list): for item in dungeon_items: self.pre_fill_items.remove(item) - self.multiworld.random.shuffle(locations) + self.random.shuffle(locations) fill_restrictive(self.multiworld, prefill_state(state), locations, dungeon_items, single_player_placement=True, lock=True, allow_excluded=True) @@ -964,7 +966,7 @@ def prefill_state(base_state): while tries: try: - self.multiworld.random.shuffle(song_locations) + self.random.shuffle(song_locations) fill_restrictive(self.multiworld, prefill_state(state), song_locations[:], songs[:], single_player_placement=True, lock=True, allow_excluded=True) logger.debug(f"Successfully placed songs for player {self.player} after {6 - tries} attempt(s)") @@ -996,7 +998,7 @@ def prefill_state(base_state): 'Buy Goron Tunic': 1, 'Buy Zora Tunic': 1, }.get(item.name, 0)) # place Deku Shields if needed, then tunics, then other advancement - self.multiworld.random.shuffle(shop_locations) + self.random.shuffle(shop_locations) self.pre_fill_items = [] # all prefill should be done fill_restrictive(self.multiworld, prefill_state(state), shop_locations, shop_prog, single_player_placement=True, lock=True, allow_excluded=True) @@ -1028,7 +1030,7 @@ def prefill_state(base_state): ganon_junk_fill = min(1, ganon_junk_fill) gc = next(filter(lambda dungeon: dungeon.name == 'Ganons Castle', self.dungeons)) locations = [loc.name for region in gc.regions for loc in region.locations if loc.item is None] - junk_fill_locations = self.multiworld.random.sample(locations, round(len(locations) * ganon_junk_fill)) + junk_fill_locations = self.random.sample(locations, round(len(locations) * ganon_junk_fill)) exclusion_rules(self.multiworld, self.player, junk_fill_locations) # Locations which are not sendable must be converted to events @@ -1074,13 +1076,13 @@ def generate_output(self, output_directory: str): trap_location_ids = [loc.address for loc in self.get_locations() if loc.item.trap] self.trap_appearances = {} for loc_id in trap_location_ids: - self.trap_appearances[loc_id] = self.create_item(self.multiworld.per_slot_randoms[self.player].choice(self.fake_items).name) + self.trap_appearances[loc_id] = self.create_item(self.random.choice(self.fake_items).name) # Seed hint RNG, used for ganon text lines also - self.hint_rng = self.multiworld.per_slot_randoms[self.player] + self.hint_rng = self.random outfile_name = self.multiworld.get_out_file_name_base(self.player) - rom = Rom(file=get_options()['oot_options']['rom_file']) + rom = Rom(file=get_settings()['oot_options']['rom_file']) try: if self.hints != 'none': buildWorldGossipHints(self) @@ -1092,7 +1094,7 @@ def generate_output(self, output_directory: str): finally: self.collectible_flags_available.set() rom.update_header() - patch_data = create_patch_file(rom) + patch_data = create_patch_file(rom, self.random) rom.restore() apz5 = OoTContainer(patch_data, outfile_name, output_directory, @@ -1399,7 +1401,7 @@ def get_state_with_complete_itempool(self): return all_state def get_filler_item_name(self) -> str: - return get_junk_item(count=1, pool=get_junk_pool(self))[0] + return get_junk_item(self.random, count=1, pool=get_junk_pool(self))[0] def valid_dungeon_item_location(world: OOTWorld, option: str, dungeon: str, loc: OOTLocation) -> bool: From 926e08513c8b7fb2995b713d560fa165061e49a4 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Thu, 19 Sep 2024 01:57:59 +0200 Subject: [PATCH 44/94] The Witness: Remove some unused code #3852 --- worlds/witness/rules.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/worlds/witness/rules.py b/worlds/witness/rules.py index 2f3210a21467..74ea2aef5740 100644 --- a/worlds/witness/rules.py +++ b/worlds/witness/rules.py @@ -214,7 +214,7 @@ def optimize_requirement_option(requirement_option: List[Union[CollectionRule, S This optimises out a requirement like [("Progressive Dots": 1), ("Progressive Dots": 2)] to only the "2" version. """ - direct_items = [rule for rule in requirement_option if isinstance(rule, tuple)] + direct_items = [rule for rule in requirement_option if isinstance(rule, SimpleItemRepresentation)] if not direct_items: return requirement_option @@ -224,7 +224,7 @@ def optimize_requirement_option(requirement_option: List[Union[CollectionRule, S return [ rule for rule in requirement_option - if not (isinstance(rule, tuple) and rule[1] < max_per_item[rule[0]]) + if not (isinstance(rule, SimpleItemRepresentation) and rule[1] < max_per_item[rule[0]]) ] @@ -234,12 +234,6 @@ def convert_requirement_option(requirement: List[Union[CollectionRule, SimpleIte 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() """ - converted_sublist = [] - - for rule in requirement: - if not isinstance(rule, tuple): - converted_sublist.append(rule) - continue collection_rules = [rule for rule in requirement if not isinstance(rule, SimpleItemRepresentation)] item_rules = [rule for rule in requirement if isinstance(rule, SimpleItemRepresentation)] From 499d79f08954ca00e2b8b8876da01f52b24ca86f Mon Sep 17 00:00:00 2001 From: gaithern <36639398+gaithern@users.noreply.github.com> Date: Thu, 19 Sep 2024 15:32:47 -0500 Subject: [PATCH 45/94] Kingdom Hearts: Fix Hint Spam and Add Setting Queries #3899 --- worlds/kh1/Client.py | 52 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/worlds/kh1/Client.py b/worlds/kh1/Client.py index acfd5dba3825..33fba85f6c54 100644 --- a/worlds/kh1/Client.py +++ b/worlds/kh1/Client.py @@ -31,6 +31,9 @@ def check_stdin() -> None: print("WARNING: Console input is not routed reliably on Windows, use the GUI instead.") class KH1ClientCommandProcessor(ClientCommandProcessor): + def __init__(self, ctx): + super().__init__(ctx) + def _cmd_deathlink(self): """Toggles Deathlink""" global death_link @@ -40,6 +43,40 @@ def _cmd_deathlink(self): else: death_link = True self.output(f"Death Link turned on") + + def _cmd_goal(self): + """Prints goal setting""" + if "goal" in self.ctx.slot_data.keys(): + self.output(str(self.ctx.slot_data["goal"])) + else: + self.output("Unknown") + + def _cmd_eotw_unlock(self): + """Prints End of the World Unlock setting""" + if "required_reports_door" in self.ctx.slot_data.keys(): + if self.ctx.slot_data["required_reports_door"] > 13: + self.output("Item") + else: + self.output(str(self.ctx.slot_data["required_reports_eotw"]) + " reports") + else: + self.output("Unknown") + + def _cmd_door_unlock(self): + """Prints Final Rest Door Unlock setting""" + if "door" in self.ctx.slot_data.keys(): + if self.ctx.slot_data["door"] == "reports": + self.output(str(self.ctx.slot_data["required_reports_door"]) + " reports") + else: + self.output(str(self.ctx.slot_data["door"])) + else: + self.output("Unknown") + + def _cmd_advanced_logic(self): + """Prints advanced logic setting""" + if "advanced_logic" in self.ctx.slot_data.keys(): + self.output(str(self.ctx.slot_data["advanced_logic"])) + else: + self.output("Unknown") class KH1Context(CommonContext): command_processor: int = KH1ClientCommandProcessor @@ -51,6 +88,8 @@ def __init__(self, server_address, password): self.send_index: int = 0 self.syncing = False self.awaiting_bridge = False + self.hinted_synth_location_ids = False + self.slot_data = {} # self.game_communication_path: files go in this path to pass data between us and the actual game if "localappdata" in os.environ: self.game_communication_path = os.path.expandvars(r"%localappdata%/KH1FM") @@ -104,6 +143,7 @@ def on_package(self, cmd: str, args: dict): f.close() #Handle Slot Data + self.slot_data = args['slot_data'] for key in list(args['slot_data'].keys()): with open(os.path.join(self.game_communication_path, key + ".cfg"), 'w') as f: f.write(str(args['slot_data'][key])) @@ -217,11 +257,13 @@ async def game_watcher(ctx: KH1Context): if timegm(time.strptime(st, '%Y%m%d%H%M%S')) > ctx.last_death_link and int(time.time()) % int(timegm(time.strptime(st, '%Y%m%d%H%M%S'))) < 10: await ctx.send_death(death_text = "Sora was defeated!") if file.find("insynthshop") > -1: - await ctx.send_msgs([{ - "cmd": "LocationScouts", - "locations": [2656401,2656402,2656403,2656404,2656405,2656406], - "create_as_hint": 2 - }]) + if not ctx.hinted_synth_location_ids: + await ctx.send_msgs([{ + "cmd": "LocationScouts", + "locations": [2656401,2656402,2656403,2656404,2656405,2656406], + "create_as_hint": 2 + }]) + ctx.hinted_synth_location_ids = True ctx.locations_checked = sending message = [{"cmd": 'LocationChecks', "locations": sending}] await ctx.send_msgs(message) From 1b15c6920d88d333ba14dd33af75d42a59dfd826 Mon Sep 17 00:00:00 2001 From: digiholic Date: Fri, 20 Sep 2024 08:15:30 -0600 Subject: [PATCH 46/94] [OSRS] Adds display names to Options #3954 --- worlds/osrs/Options.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/worlds/osrs/Options.py b/worlds/osrs/Options.py index 520cd8e8b06b..81e017eddb34 100644 --- a/worlds/osrs/Options.py +++ b/worlds/osrs/Options.py @@ -63,6 +63,7 @@ class MaxCombatLevel(Range): The highest combat level of monster to possibly be assigned as a task. If set to 0, no combat tasks will be generated. """ + display_name = "Max Required Enemy Combat Level" range_start = 0 range_end = 1520 default = 50 @@ -74,6 +75,7 @@ class MaxCombatTasks(Range): If set to 0, no combat tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Combat Task Count" range_start = 0 range_end = MAX_COMBAT_TASKS default = MAX_COMBAT_TASKS @@ -85,6 +87,7 @@ class CombatTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Combat Task Weight" range_start = 0 range_end = 99 default = 50 @@ -95,6 +98,7 @@ class MaxPrayerLevel(Range): The highest Prayer requirement of any task generated. If set to 0, no Prayer tasks will be generated. """ + display_name = "Max Required Prayer Level" range_start = 0 range_end = 99 default = 50 @@ -106,6 +110,7 @@ class MaxPrayerTasks(Range): If set to 0, no Prayer tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Prayer Task Count" range_start = 0 range_end = MAX_PRAYER_TASKS default = MAX_PRAYER_TASKS @@ -117,6 +122,7 @@ class PrayerTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Prayer Task Weight" range_start = 0 range_end = 99 default = 50 @@ -127,6 +133,7 @@ class MaxMagicLevel(Range): The highest Magic requirement of any task generated. If set to 0, no Magic tasks will be generated. """ + display_name = "Max Required Magic Level" range_start = 0 range_end = 99 default = 50 @@ -138,6 +145,7 @@ class MaxMagicTasks(Range): If set to 0, no Magic tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Magic Task Count" range_start = 0 range_end = MAX_MAGIC_TASKS default = MAX_MAGIC_TASKS @@ -149,6 +157,7 @@ class MagicTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Magic Task Weight" range_start = 0 range_end = 99 default = 50 @@ -159,6 +168,7 @@ class MaxRunecraftLevel(Range): The highest Runecraft requirement of any task generated. If set to 0, no Runecraft tasks will be generated. """ + display_name = "Max Required Runecraft Level" range_start = 0 range_end = 99 default = 50 @@ -170,6 +180,7 @@ class MaxRunecraftTasks(Range): If set to 0, no Runecraft tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Runecraft Task Count" range_start = 0 range_end = MAX_RUNECRAFT_TASKS default = MAX_RUNECRAFT_TASKS @@ -181,6 +192,7 @@ class RunecraftTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Runecraft Task Weight" range_start = 0 range_end = 99 default = 50 @@ -191,6 +203,7 @@ class MaxCraftingLevel(Range): The highest Crafting requirement of any task generated. If set to 0, no Crafting tasks will be generated. """ + display_name = "Max Required Crafting Level" range_start = 0 range_end = 99 default = 50 @@ -202,6 +215,7 @@ class MaxCraftingTasks(Range): If set to 0, no Crafting tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Crafting Task Count" range_start = 0 range_end = MAX_CRAFTING_TASKS default = MAX_CRAFTING_TASKS @@ -213,6 +227,7 @@ class CraftingTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Crafting Task Weight" range_start = 0 range_end = 99 default = 50 @@ -223,6 +238,7 @@ class MaxMiningLevel(Range): The highest Mining requirement of any task generated. If set to 0, no Mining tasks will be generated. """ + display_name = "Max Required Mining Level" range_start = 0 range_end = 99 default = 50 @@ -234,6 +250,7 @@ class MaxMiningTasks(Range): If set to 0, no Mining tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Mining Task Count" range_start = 0 range_end = MAX_MINING_TASKS default = MAX_MINING_TASKS @@ -245,6 +262,7 @@ class MiningTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Mining Task Weight" range_start = 0 range_end = 99 default = 50 @@ -255,6 +273,7 @@ class MaxSmithingLevel(Range): The highest Smithing requirement of any task generated. If set to 0, no Smithing tasks will be generated. """ + display_name = "Max Required Smithing Level" range_start = 0 range_end = 99 default = 50 @@ -266,6 +285,7 @@ class MaxSmithingTasks(Range): If set to 0, no Smithing tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Smithing Task Count" range_start = 0 range_end = MAX_SMITHING_TASKS default = MAX_SMITHING_TASKS @@ -277,6 +297,7 @@ class SmithingTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Smithing Task Weight" range_start = 0 range_end = 99 default = 50 @@ -287,6 +308,7 @@ class MaxFishingLevel(Range): The highest Fishing requirement of any task generated. If set to 0, no Fishing tasks will be generated. """ + display_name = "Max Required Fishing Level" range_start = 0 range_end = 99 default = 50 @@ -298,6 +320,7 @@ class MaxFishingTasks(Range): If set to 0, no Fishing tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Fishing Task Count" range_start = 0 range_end = MAX_FISHING_TASKS default = MAX_FISHING_TASKS @@ -309,6 +332,7 @@ class FishingTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Fishing Task Weight" range_start = 0 range_end = 99 default = 50 @@ -319,6 +343,7 @@ class MaxCookingLevel(Range): The highest Cooking requirement of any task generated. If set to 0, no Cooking tasks will be generated. """ + display_name = "Max Required Cooking Level" range_start = 0 range_end = 99 default = 50 @@ -330,6 +355,7 @@ class MaxCookingTasks(Range): If set to 0, no Cooking tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Cooking Task Count" range_start = 0 range_end = MAX_COOKING_TASKS default = MAX_COOKING_TASKS @@ -341,6 +367,7 @@ class CookingTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Cooking Task Weight" range_start = 0 range_end = 99 default = 50 @@ -351,6 +378,7 @@ class MaxFiremakingLevel(Range): The highest Firemaking requirement of any task generated. If set to 0, no Firemaking tasks will be generated. """ + display_name = "Max Required Firemaking Level" range_start = 0 range_end = 99 default = 50 @@ -362,6 +390,7 @@ class MaxFiremakingTasks(Range): If set to 0, no Firemaking tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Firemaking Task Count" range_start = 0 range_end = MAX_FIREMAKING_TASKS default = MAX_FIREMAKING_TASKS @@ -373,6 +402,7 @@ class FiremakingTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Firemaking Task Weight" range_start = 0 range_end = 99 default = 50 @@ -383,6 +413,7 @@ class MaxWoodcuttingLevel(Range): The highest Woodcutting requirement of any task generated. If set to 0, no Woodcutting tasks will be generated. """ + display_name = "Max Required Woodcutting Level" range_start = 0 range_end = 99 default = 50 @@ -394,6 +425,7 @@ class MaxWoodcuttingTasks(Range): If set to 0, no Woodcutting tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Woodcutting Task Count" range_start = 0 range_end = MAX_WOODCUTTING_TASKS default = MAX_WOODCUTTING_TASKS @@ -405,6 +437,7 @@ class WoodcuttingTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Woodcutting Task Weight" range_start = 0 range_end = 99 default = 50 @@ -416,6 +449,7 @@ class MinimumGeneralTasks(Range): General progression tasks will be used to fill out any holes caused by having fewer possible tasks than needed, so there is no maximum. """ + display_name = "Minimum General Task Count" range_start = 0 range_end = NON_QUEST_LOCATION_COUNT default = 10 @@ -427,6 +461,7 @@ class GeneralTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "General Task Weight" range_start = 0 range_end = 99 default = 50 From 79942c09c2c082d2825af77d56369eb6fdc10b08 Mon Sep 17 00:00:00 2001 From: Alex Nordstrom Date: Fri, 20 Sep 2024 10:18:09 -0400 Subject: [PATCH 47/94] LADX: define filler item, fix for extra golden leaves (#3918) * set filler item also rename "Master Stalfos' Message" to "Nothing" as it shows up in game, and "Gel" to "Zol Attack" * fix for extra gold leaves * fix for start_inventory --- worlds/ladx/Items.py | 4 ++-- worlds/ladx/LADXR/patches/goldenLeaf.py | 3 ++- worlds/ladx/__init__.py | 5 ++++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/worlds/ladx/Items.py b/worlds/ladx/Items.py index 9f4784f74995..1f9358a4f5a6 100644 --- a/worlds/ladx/Items.py +++ b/worlds/ladx/Items.py @@ -83,8 +83,8 @@ class ItemName: RUPEES_200 = "200 Rupees" RUPEES_500 = "500 Rupees" SEASHELL = "Seashell" - MESSAGE = "Master Stalfos' Message" - GEL = "Gel" + MESSAGE = "Nothing" + GEL = "Zol Attack" BOOMERANG = "Boomerang" HEART_PIECE = "Heart Piece" BOWWOW = "BowWow" diff --git a/worlds/ladx/LADXR/patches/goldenLeaf.py b/worlds/ladx/LADXR/patches/goldenLeaf.py index 87cefae0f6d8..b35c722a4316 100644 --- a/worlds/ladx/LADXR/patches/goldenLeaf.py +++ b/worlds/ladx/LADXR/patches/goldenLeaf.py @@ -29,6 +29,7 @@ def fixGoldenLeaf(rom): rom.patch(0x03, 0x0980, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # If leaves >= 6 move richard rom.patch(0x06, 0x0059, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # If leaves >= 6 move richard rom.patch(0x06, 0x007D, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # Richard message if no leaves - rom.patch(0x06, 0x00B8, ASM("ld [$DB15], a"), ASM("ld [wGoldenLeaves], a")) # Stores FF in the leaf counter if we opened the path + rom.patch(0x06, 0x00B6, ASM("ld a, $FF"), ASM("ld a, $06")) + rom.patch(0x06, 0x00B8, ASM("ld [$DB15], a"), ASM("ld [wGoldenLeaves], a")) # Stores 6 in the leaf counter if we opened the path (instead of FF, so that nothing breaks if we get more for some reason) # 6:40EE uses leaves == 6 to check if we have collected the key, but only to change the message. # rom.patch(0x06, 0x2AEF, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # Telephone message handler diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index 79f1fe470f81..2846b40e67d9 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -216,7 +216,7 @@ def create_items(self) -> None: for _ in range(count): if item_name in exclude: exclude.remove(item_name) # this is destructive. create unique list above - self.multiworld.itempool.append(self.create_item("Master Stalfos' Message")) + self.multiworld.itempool.append(self.create_item("Nothing")) else: item = self.create_item(item_name) @@ -513,6 +513,9 @@ def remove(self, state, item: Item) -> bool: state.prog_items[self.player]["RUPEES"] -= self.rupees[item.name] return change + def get_filler_item_name(self) -> str: + return "Nothing" + def fill_slot_data(self): slot_data = {} From 0095eecf2b5c02f15e9121a238980cbd9e66ee3c Mon Sep 17 00:00:00 2001 From: Spineraks Date: Fri, 20 Sep 2024 19:07:45 +0200 Subject: [PATCH 48/94] Yacht Dice: Remove Victory item and make it an event instead (#3968) * Add the yacht dice (from other git) world to the yacht dice fork * Update .gitignore * Removed zillion because it doesn't work * Update .gitignore * added zillion again... * Now you can have 0 extra fragments * Added alt categories, also options * Added item categories * Extra categories are now working! :dog: * changed options and added exceptions * Testing if I change the generate.py * Revert "Testing if I change the generate.py" This reverts commit 7c2b3df6170dcf8d8f36a1de9fcbc9dccdec81f8. * ignore gitignore * Delete .gitignore * Update .gitignore * Update .gitignore * Update logic, added multiplicative categories * Changed difficulties * Update offline mode so that it works again * Adjusted difficulty * New version of the apworld, with 1000 as final score, always Will still need to check difficulty and weights of adding items. Website is not ready yet, so this version is not usable yet :) * Changed yaml and small bug fixes Fix when goal and max are same Options: changed chance to weight * no changes, just whitespaces * changed how logic works Now you put an array of mults and the cpu gets a couple of tries * Changed logic, tweaked a bit too * Preparation for 2.0 * logic tweak * Logic for alt categories properly now * Update setup_en.md * Update en_YachtDice.md * Improve performance of add_distributions * Formatting style * restore gitignore to APMW * Tweaked generation parameters and methods * Version 2.0.3 manual input option max score in logic always 2.0.3 faster gen * Comments and editing * Renamed setup guide * Improved create_items code * init of locations: remove self.event line * Moved setting early items to generate_early * Add my name to CODEOWNERS * Added Yacht Dice to the readme in list of games * Improve performance of Yacht Dice * newline * Improve typing * This is actually just slower lol * Update worlds/yachtdice/Items.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update Options.py * Styling * finished text whichstory option * removed roll and rollfragments; not used * import; worlds not world :) * Option groups! * ruff styling, fix * ruff format styling! * styling and capitalization of options * small comment * Cleaned up the "state_is_a_list" a little bit * RUFF :dog: * Changed filling the itempool for efficiency Now, we start with 17 extra items in the item pool, it's quite likely you need at least 17 items (~80%?). And then afterwards, we delete items if we overshoot the target of 1000, and add items if we haven't reached an achievable score of 1000 yet. Also, no need to recompute the entire logic when adding points. * :dog: * Removed plando "fix" * Changed indent of score multiplier * faster location function * Comments to docstrings * fixed making location closest to goal_score be goal_score * options format * iterate keys and values of a dict together * small optimization ListState * faster collection of categories * return arguments instead of making a list (will :dog: later) * Instead of turning it into a tuple, you can just make a tuple literal * remove .keys() * change .random and used enumerate * some readability improvements * Remove location "0", we don't use that one * Remove lookup_id_to_name entirely I for sure don't use it, and as far as I know it's not one of the mandatory functions for AP, these are item_name_to_id and location_name_to_id. * .append instead of += for single items, percentile function changed Also an extra comment for location ids. * remove ) too many * Removed sorted from category list * Hash categories (which makes it slower :( ) Maybe I messed up or misunderstood... I'll revert this right away since it is 2x slower, probably because of sorted instead of sort? * Revert "Hash categories (which makes it slower :( )" This reverts commit 34f2c1aed8c8813b2d9c58896650b82a810d3578. * temporary push: 40% faster generation test Small changes in logic make the generation 40% faster. I'll have to think about how big the changes are. I suspect they are rather limited. If this is the way to go, I'll remove the temp file and redo the YachtWeights file, I'll remove the functions there and just put the new weights here. * Add Points item category * Reverse changes of bad idea :) * ruff :dog: * Use numpy and pmf function to speed up gen Numpy has a built-in way to sum probability mass functions (pmf). This shaves of 60% of the generation time :D * Revert "Use numpy and pmf function to speed up gen" This reverts commit 9290191cb323ae92321d6c2cfcfe8c27370f439b. * Step inbetween to change the weights * Changed the weights to make it faster 135 -> 81 seconds on 100 random yamls * Adjusted max_dist, split dice_simulation function * Removed nonlocal and pass arguments instead * Change "weight-lists" to Dict[str, float] * Removed the return from ini_locations. Also added explanations to cat_weights * Choice options; dont'use .value (will ruff later) * Only put important options in slotdata * :dog: * Add Dict import * Split the cache per player, limit size to 400. * :dog: * added , because of style * Update apworld version to 2.0.6 2.0.5 is the apworld I released on github to be tested I never separately released 2.0.4. * Multiple smaller code improvements - changed names in YachtWeights so we don't need to translate them in Rules anymore - we now remember which categories are present in the game, and also put this in slotdata. This we do because only one of two categories is present in a game. If for some reason both are present (plando/getitem/startinventory), we now know which category to ignore - * :dog: ruff * Mostly minimize_extra_items improvements - Change logic, generation is now even faster (0.6s per default yaml). - Made the option 'minimize_extra_items' do a lot more, hopefully this makes the impact of Yacht Dice a little bit less, if you want that. Here's what is also does now: - you start with 2 dice and 2 rolls - there will be less locations/items at the start of you game * ruff :dog: * Removed printing options * Reworded some option descriptions * Yacht Dice: setup: change release-link to latest On the installation page, link to the latest release, instead of the page with all releases * Several fixes and changes -change apworld version -Removed the extra roll (this was not intended) -change extra_points_added to a mutable list to that it actually does something -removed variables multipliers_added and items_added -Rules, don't order by quantity, just by mean_score -Changed the weights in general to make it faster * :dog: * Revert setup to what it was (latest, without S) * remove temp weights file, shouldn't be here * Made sure that there is not too many step score multipliers. Too many step score multipliers lead to gen fails too, probably because you need many categories for them to actually help a lot. So it's hard to use them at the start of the game. * add filler item name * Textual fixes and changes * Remove Victory item and use event instead. * Revert "Remove Victory item and use event instead." This reverts commit c2f7d674d392a3acbc1db8614411164ba3b28bff. * Revert "Textual fixes and changes" This reverts commit e9432f92454979fcd5a31f8517586585362a7ab7. * Remove Victory item and make it an event instead --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/yachtdice/Items.py | 2 -- worlds/yachtdice/__init__.py | 12 +++++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/worlds/yachtdice/Items.py b/worlds/yachtdice/Items.py index fa52c93ad6f2..c76dc538146e 100644 --- a/worlds/yachtdice/Items.py +++ b/worlds/yachtdice/Items.py @@ -16,8 +16,6 @@ class YachtDiceItem(Item): item_table = { - # victory item, always placed manually at goal location - "Victory": ItemData(16871244000 - 1, ItemClassification.progression), "Dice": ItemData(16871244000, ItemClassification.progression), "Dice Fragment": ItemData(16871244001, ItemClassification.progression), "Roll": ItemData(16871244002, ItemClassification.progression), diff --git a/worlds/yachtdice/__init__.py b/worlds/yachtdice/__init__.py index d86ee3382d33..75993fd39443 100644 --- a/worlds/yachtdice/__init__.py +++ b/worlds/yachtdice/__init__.py @@ -1,7 +1,7 @@ import math from typing import Dict -from BaseClasses import CollectionState, Entrance, Item, Region, Tutorial +from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, Region, Tutorial from worlds.AutoWorld import WebWorld, World @@ -56,7 +56,7 @@ class YachtDiceWorld(World): item_name_groups = item_groups - ap_world_version = "2.1.2" + ap_world_version = "2.1.3" def _get_yachtdice_data(self): return { @@ -456,10 +456,12 @@ def create_regions(self): if loc_data.region == board.name ] - # Add the victory item to the correct location. - # The website declares that the game is complete when the victory item is obtained. + # Change the victory location to an event and place the Victory item there. victory_location_name = f"{self.goal_score} score" - self.get_location(victory_location_name).place_locked_item(self.create_item("Victory")) + self.get_location(victory_location_name).address = None + self.get_location(victory_location_name).place_locked_item( + Item("Victory", ItemClassification.progression, None, self.player) + ) # add the regions connection = Entrance(self.player, "New Board", menu) From ba8f03516e4d5453d9c148f89f0215611a4ef0db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor?= <67028894+JoaoVictor-FA@users.noreply.github.com> Date: Fri, 20 Sep 2024 14:19:48 -0300 Subject: [PATCH 49/94] Docs: added Brazilian Portuguese Translation for Hollow Knight setup guide (#3909) * add neww pt-br translation * setup file * Update setup_pt_br.md * add ` to paths * correct grammar * add space .-. * add more spaces .-. .-. .-. * capitalize HK * Update setup_pt_br.md * accent not the same as punctuation * small changes * Update setup_pt_br.md --- worlds/hk/__init__.py | 15 ++++++++-- worlds/hk/docs/setup_pt_br.md | 52 +++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 worlds/hk/docs/setup_pt_br.md diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 860243ee952e..6ecdacb1156d 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -124,14 +124,25 @@ class HKWeb(WebWorld): - tutorials = [Tutorial( + setup_en = Tutorial( "Mod Setup and Use Guide", "A guide to playing Hollow Knight with Archipelago.", "English", "setup_en.md", "setup/en", ["Ijwu"] - )] + ) + + setup_pt_br = Tutorial( + setup_en.tutorial_name, + setup_en.description, + "Português Brasileiro", + "setup_pt_br.md", + "setup/pt_br", + ["JoaoVictor-FA"] + ) + + tutorials = [setup_en, setup_pt_br] bug_report_page = "https://github.com/Ijwu/Archipelago.HollowKnight/issues/new?assignees=&labels=bug%2C+needs+investigation&template=bug_report.md&title=" diff --git a/worlds/hk/docs/setup_pt_br.md b/worlds/hk/docs/setup_pt_br.md new file mode 100644 index 000000000000..9ae1ea89d566 --- /dev/null +++ b/worlds/hk/docs/setup_pt_br.md @@ -0,0 +1,52 @@ +# Guia de configuração para Hollow Knight no Archipelago + +## Programas obrigatórios +* Baixe e extraia o Lumafly Mod Manager (gerenciador de mods Lumafly) do [Site Lumafly](https://themulhima.github.io/Lumafly/). +* Uma cópia legal de Hollow Knight. + * Versões Steam, Gog, e Xbox Game Pass do jogo são suportadas. + * Windows, Mac, e Linux (incluindo Steam Deck) são suportados. + +## Instalando o mod Archipelago Mod usando Lumafly +1. Abra o Lumafly e confirme que ele localizou sua pasta de instalação do Hollow Knight. +2. Clique em "Install (instalar)" perto da opção "Archipelago" mod. + * Se quiser, instale também o "Archipelago Map Mod (mod do mapa do archipelago)" para usá-lo como rastreador dentro do jogo. +3. Abra o jogo, tudo preparado! + +### O que fazer se o Lumafly falha em encontrar a sua pasta de instalação +1. Encontre a pasta manualmente. + * Xbox Game Pass: + 1. Entre no seu aplicativo Xbox e mova seu mouse em cima de "Hollow Knight" na sua barra da esquerda. + 2. Clique nos 3 pontos depois clique gerenciar. + 3. Vá nos arquivos e selecione procurar. + 4. Clique em "Hollow Knight", depois em "Content (Conteúdo)", depois clique na barra com o endereço e a copie. + * Steam: + 1. Você provavelmente colocou sua biblioteca Steam num local não padrão. Se esse for o caso você provavelmente sabe onde está. + . Encontre sua biblioteca Steam, depois encontre a pasta do Hollow Knight e copie seu endereço. + * Windows - `C:\Program Files (x86)\Steam\steamapps\common\Hollow Knight` + * Linux/Steam Deck - `~/.local/share/Steam/steamapps/common/Hollow Knight` + * Mac - `~/Library/Application Support/Steam/steamapps/common/Hollow Knight/hollow_knight.app` +2. Rode o Lumafly como administrador e, quando ele perguntar pelo endereço do arquivo, cole o endereço do arquivo que você copiou. + +## Configurando seu arquivo YAML +### O que é um YAML e por que eu preciso de um? +Um arquivo YAML é a forma que você informa suas configurações do jogador para o Archipelago. +Olhe o [guia de configuração básica de um multiworld](/tutorial/Archipelago/setup/en) aqui no site do Archipelago para aprender mais. + +### Onde eu consigo o YAML? +Você pode usar a [página de configurações do jogador para Hollow Knight](/games/Hollow%20Knight/player-options) aqui no site do Archipelago +para gerar o YAML usando a interface gráfica. + +### Entrando numa partida de Archipelago no Hollow Knight +1. Começe o jogo depois de instalar todos os mods necessários. +2. Crie um **novo jogo salvo.** +3. Selecione o modo de jogo **Archipelago** do menu de seleção. +4. Coloque as configurações corretas do seu servidor Archipelago. +5. Aperte em **Começar**. O jogo vai travar por uns segundos enquanto ele coloca todos itens. +6. O jogo vai te colocar imediatamente numa partida randomizada. + * Se você está esperando uma contagem então espere ele cair antes de apertar começar. + * Ou clique em começar e pause o jogo enquanto estiver nele. + +## Dicas e outros comandos +Enquanto jogar um multiworld, você pode interagir com o servidor usando vários comandos listados no +[Guia de comandos](/tutorial/Archipelago/commands/en). Você pode usar o cliente de texto do Archipelago para isso, +que está incluido na ultima versão do [Archipelago software](https://github.com/ArchipelagoMW/Archipelago/releases/latest). From 41ddb96b24cdec7886a4fa01cf42ef7e6e90e7bc Mon Sep 17 00:00:00 2001 From: qwint Date: Sat, 21 Sep 2024 09:45:22 -0500 Subject: [PATCH 50/94] HK: add race bool to slot data (#3971) --- worlds/hk/__init__.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 6ecdacb1156d..15addefef50a 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -21,6 +21,16 @@ from BaseClasses import Region, Location, MultiWorld, Item, LocationProgressType, Tutorial, ItemClassification, CollectionState from worlds.AutoWorld import World, LogicMixin, WebWorld +from settings import Group, Bool + + +class HollowKnightSettings(Group): + class DisableMapModSpoilers(Bool): + """Disallows the APMapMod from showing spoiler placements.""" + + disable_spoilers: typing.Union[DisableMapModSpoilers, bool] = False + + path_of_pain_locations = { "Soul_Totem-Path_of_Pain_Below_Thornskip", "Lore_Tablet-Path_of_Pain_Entrance", @@ -156,6 +166,7 @@ class HKWorld(World): game: str = "Hollow Knight" options_dataclass = HKOptions options: HKOptions + settings: typing.ClassVar[HollowKnightSettings] web = HKWeb() @@ -555,6 +566,8 @@ def fill_slot_data(self): slot_data["grub_count"] = self.grub_count + slot_data["is_race"] = int(self.settings.disable_spoilers or self.multiworld.is_race) + return slot_data def create_item(self, name: str) -> HKItem: From 69d3db21df580ba488f36a475d5ea98a34a3cf3b Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Sat, 21 Sep 2024 17:02:58 -0400 Subject: [PATCH 51/94] TUNIC: Deal with the boxes blocking the entrance to Beneath the Vault --- worlds/tunic/er_rules.py | 5 ++++- worlds/tunic/rules.py | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index ee48f60eaca4..2677ec409b3b 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -762,7 +762,10 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Beneath the Vault Ladder Exit"].connect( connecting_region=regions["Beneath the Vault Main"], rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world) - and has_lantern(state, world)) + and has_lantern(state, world) + # there's some boxes in the way + and (has_stick(state, player) or state.has_any((gun, grapple, fire_wand), player))) + # on the reverse trip, you can lure an enemy over to break the boxes if needed regions["Beneath the Vault Main"].connect( connecting_region=regions["Beneath the Vault Ladder Exit"], rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world)) diff --git a/worlds/tunic/rules.py b/worlds/tunic/rules.py index 14ed84d44964..aa69666daeb6 100644 --- a/worlds/tunic/rules.py +++ b/worlds/tunic/rules.py @@ -114,7 +114,9 @@ def set_region_rules(world: "TunicWorld") -> None: or can_ladder_storage(state, world) # using laurels or ls to get in is covered by the -> Eastern Vault Fortress rules world.get_entrance("Overworld -> Beneath the Vault").access_rule = \ - lambda state: has_lantern(state, world) and has_ability(prayer, state, world) + lambda state: (has_lantern(state, world) and has_ability(prayer, state, world) + # there's some boxes in the way + and (has_stick(state, player) or state.has_any((gun, grapple, fire_wand), player))) world.get_entrance("Ruined Atoll -> Library").access_rule = \ lambda state: state.has_any({grapple, laurels}, player) and has_ability(prayer, state, world) world.get_entrance("Overworld -> Quarry").access_rule = \ From 204e940f4741544eef50f12967cd177737d4023d Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Sat, 21 Sep 2024 17:05:00 -0400 Subject: [PATCH 52/94] Stardew Valley: Fix Art Of Crabbing Logic and Extract Festival Logic (#3625) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * here you go kaito kid * here you go kaito kid * move reward logic in its own method --------- Co-authored-by: Jouramie Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> --- .../content/vanilla/pelican_town.py | 7 +- worlds/stardew_valley/data/items.csv | 2 +- worlds/stardew_valley/logic/festival_logic.py | 186 ++++++++++++++++++ worlds/stardew_valley/logic/logic.py | 141 +------------ 4 files changed, 192 insertions(+), 144 deletions(-) create mode 100644 worlds/stardew_valley/logic/festival_logic.py diff --git a/worlds/stardew_valley/content/vanilla/pelican_town.py b/worlds/stardew_valley/content/vanilla/pelican_town.py index 73cc8f119a3e..913fe4b8ad96 100644 --- a/worlds/stardew_valley/content/vanilla/pelican_town.py +++ b/worlds/stardew_valley/content/vanilla/pelican_town.py @@ -2,7 +2,7 @@ from ...data import villagers_data, fish_data from ...data.game_item import GenericSource, ItemTag, Tag, CustomRuleSource, CompoundSource from ...data.harvest import ForagingSource, SeasonalForagingSource, ArtifactSpotSource -from ...data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement +from ...data.requirement import ToolRequirement, BookRequirement, SkillRequirement from ...data.shop import ShopSource, MysteryBoxSource, ArtifactTroveSource, PrizeMachineSource, FishingTreasureChestSource from ...strings.book_names import Book from ...strings.crop_names import Fruit @@ -250,10 +250,7 @@ ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), Book.the_art_o_crabbing: ( Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), - GenericSource(regions=(Region.beach,), - other_requirements=(ToolRequirement(Tool.fishing_rod, ToolMaterial.iridium), - SkillRequirement(Skill.fishing, 6), - SeasonRequirement(Season.winter))), + CustomRuleSource(create_rule=lambda logic: logic.festival.has_squidfest_day_1_iridium_reward()), ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), Book.treasure_appraisal_guide: ( Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), diff --git a/worlds/stardew_valley/data/items.csv b/worlds/stardew_valley/data/items.csv index e026090f8659..64c14e9f678a 100644 --- a/worlds/stardew_valley/data/items.csv +++ b/worlds/stardew_valley/data/items.csv @@ -474,7 +474,7 @@ id,name,classification,groups,mod_name 507,Resource Pack: 40 Calico Egg,useful,"FESTIVAL", 508,Resource Pack: 35 Calico Egg,useful,"FESTIVAL", 509,Resource Pack: 30 Calico Egg,useful,"FESTIVAL", -510,Book: The Art O' Crabbing,useful,"FESTIVAL", +510,Book: The Art O' Crabbing,progression,"FESTIVAL", 511,Mr Qi's Plane Ride,progression,, 521,Power: Price Catalogue,useful,"BOOK_POWER", 522,Power: Mapping Cave Systems,useful,"BOOK_POWER", diff --git a/worlds/stardew_valley/logic/festival_logic.py b/worlds/stardew_valley/logic/festival_logic.py new file mode 100644 index 000000000000..2b22617202d8 --- /dev/null +++ b/worlds/stardew_valley/logic/festival_logic.py @@ -0,0 +1,186 @@ +from typing import Union + +from .action_logic import ActionLogicMixin +from .animal_logic import AnimalLogicMixin +from .artisan_logic import ArtisanLogicMixin +from .base_logic import BaseLogicMixin, BaseLogic +from .fishing_logic import FishingLogicMixin +from .gift_logic import GiftLogicMixin +from .has_logic import HasLogicMixin +from .money_logic import MoneyLogicMixin +from .monster_logic import MonsterLogicMixin +from .museum_logic import MuseumLogicMixin +from .received_logic import ReceivedLogicMixin +from .region_logic import RegionLogicMixin +from .relationship_logic import RelationshipLogicMixin +from .skill_logic import SkillLogicMixin +from .time_logic import TimeLogicMixin +from ..options import FestivalLocations +from ..stardew_rule import StardewRule +from ..strings.book_names import Book +from ..strings.craftable_names import Fishing +from ..strings.crop_names import Fruit, Vegetable +from ..strings.festival_check_names import FestivalCheck +from ..strings.fish_names import Fish +from ..strings.forageable_names import Forageable +from ..strings.generic_names import Generic +from ..strings.machine_names import Machine +from ..strings.monster_names import Monster +from ..strings.region_names import Region + + +class FestivalLogicMixin(BaseLogicMixin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.festival = FestivalLogic(*args, **kwargs) + + +class FestivalLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, FestivalLogicMixin, ArtisanLogicMixin, AnimalLogicMixin, MoneyLogicMixin, TimeLogicMixin, +SkillLogicMixin, RegionLogicMixin, ActionLogicMixin, MonsterLogicMixin, RelationshipLogicMixin, FishingLogicMixin, MuseumLogicMixin, GiftLogicMixin]]): + + def initialize_rules(self): + self.registry.festival_rules.update({ + FestivalCheck.egg_hunt: self.logic.festival.can_win_egg_hunt(), + FestivalCheck.strawberry_seeds: self.logic.money.can_spend(1000), + FestivalCheck.dance: self.logic.relationship.has_hearts_with_any_bachelor(4), + FestivalCheck.tub_o_flowers: self.logic.money.can_spend(2000), + FestivalCheck.rarecrow_5: self.logic.money.can_spend(2500), + FestivalCheck.luau_soup: self.logic.festival.can_succeed_luau_soup(), + FestivalCheck.moonlight_jellies: self.logic.true_, + FestivalCheck.moonlight_jellies_banner: self.logic.money.can_spend(800), + FestivalCheck.starport_decal: self.logic.money.can_spend(1000), + FestivalCheck.smashing_stone: self.logic.true_, + FestivalCheck.grange_display: self.logic.festival.can_succeed_grange_display(), + FestivalCheck.rarecrow_1: self.logic.true_, # only cost star tokens + FestivalCheck.fair_stardrop: self.logic.true_, # only cost star tokens + FestivalCheck.spirit_eve_maze: self.logic.true_, + FestivalCheck.jack_o_lantern: self.logic.money.can_spend(2000), + FestivalCheck.rarecrow_2: self.logic.money.can_spend(5000), + FestivalCheck.fishing_competition: self.logic.festival.can_win_fishing_competition(), + FestivalCheck.rarecrow_4: self.logic.money.can_spend(5000), + FestivalCheck.mermaid_pearl: self.logic.has(Forageable.secret_note), + FestivalCheck.cone_hat: self.logic.money.can_spend(2500), + FestivalCheck.iridium_fireplace: self.logic.money.can_spend(15000), + FestivalCheck.rarecrow_7: self.logic.money.can_spend(5000) & self.logic.museum.can_donate_museum_artifacts(20), + FestivalCheck.rarecrow_8: self.logic.money.can_spend(5000) & self.logic.museum.can_donate_museum_items(40), + FestivalCheck.lupini_red_eagle: self.logic.money.can_spend(1200), + FestivalCheck.lupini_portrait_mermaid: self.logic.money.can_spend(1200), + FestivalCheck.lupini_solar_kingdom: self.logic.money.can_spend(1200), + FestivalCheck.lupini_clouds: self.logic.time.has_year_two & self.logic.money.can_spend(1200), + FestivalCheck.lupini_1000_years: self.logic.time.has_year_two & self.logic.money.can_spend(1200), + FestivalCheck.lupini_three_trees: self.logic.time.has_year_two & self.logic.money.can_spend(1200), + FestivalCheck.lupini_the_serpent: self.logic.time.has_year_three & self.logic.money.can_spend(1200), + FestivalCheck.lupini_tropical_fish: self.logic.time.has_year_three & self.logic.money.can_spend(1200), + FestivalCheck.lupini_land_of_clay: self.logic.time.has_year_three & self.logic.money.can_spend(1200), + FestivalCheck.secret_santa: self.logic.gifts.has_any_universal_love, + FestivalCheck.legend_of_the_winter_star: self.logic.true_, + FestivalCheck.rarecrow_3: self.logic.true_, + FestivalCheck.all_rarecrows: self.logic.region.can_reach(Region.farm) & self.logic.festival.has_all_rarecrows(), + FestivalCheck.calico_race: self.logic.true_, + FestivalCheck.mummy_mask: self.logic.true_, + FestivalCheck.calico_statue: self.logic.true_, + FestivalCheck.emily_outfit_service: self.logic.true_, + FestivalCheck.earthy_mousse: self.logic.true_, + FestivalCheck.sweet_bean_cake: self.logic.true_, + FestivalCheck.skull_cave_casserole: self.logic.true_, + FestivalCheck.spicy_tacos: self.logic.true_, + FestivalCheck.mountain_chili: self.logic.true_, + FestivalCheck.crystal_cake: self.logic.true_, + FestivalCheck.cave_kebab: self.logic.true_, + FestivalCheck.hot_log: self.logic.true_, + FestivalCheck.sour_salad: self.logic.true_, + FestivalCheck.superfood_cake: self.logic.true_, + FestivalCheck.warrior_smoothie: self.logic.true_, + FestivalCheck.rumpled_fruit_skin: self.logic.true_, + FestivalCheck.calico_pizza: self.logic.true_, + FestivalCheck.stuffed_mushrooms: self.logic.true_, + FestivalCheck.elf_quesadilla: self.logic.true_, + FestivalCheck.nachos_of_the_desert: self.logic.true_, + FestivalCheck.cloppino: self.logic.true_, + FestivalCheck.rainforest_shrimp: self.logic.true_, + FestivalCheck.shrimp_donut: self.logic.true_, + FestivalCheck.smell_of_the_sea: self.logic.true_, + FestivalCheck.desert_gumbo: self.logic.true_, + FestivalCheck.free_cactis: self.logic.true_, + FestivalCheck.monster_hunt: self.logic.monster.can_kill(Monster.serpent), + FestivalCheck.deep_dive: self.logic.region.can_reach(Region.skull_cavern_50), + FestivalCheck.treasure_hunt: self.logic.region.can_reach(Region.skull_cavern_25), + FestivalCheck.touch_calico_statue: self.logic.region.can_reach(Region.skull_cavern_25), + FestivalCheck.real_calico_egg_hunter: self.logic.region.can_reach(Region.skull_cavern_100), + FestivalCheck.willy_challenge: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.scorpion_carp]), + FestivalCheck.desert_scholar: self.logic.true_, + FestivalCheck.squidfest_day_1_copper: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]), + FestivalCheck.squidfest_day_1_iron: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]) & self.logic.has(Fishing.bait), + FestivalCheck.squidfest_day_1_gold: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]) & self.logic.has(Fishing.deluxe_bait), + FestivalCheck.squidfest_day_1_iridium: self.logic.festival.can_squidfest_day_1_iridium_reward(), + FestivalCheck.squidfest_day_2_copper: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]), + FestivalCheck.squidfest_day_2_iron: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]) & self.logic.has(Fishing.bait), + FestivalCheck.squidfest_day_2_gold: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]) & self.logic.has(Fishing.deluxe_bait), + FestivalCheck.squidfest_day_2_iridium: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]) & + self.logic.fishing.has_specific_bait(self.content.fishes[Fish.squid]), + }) + for i in range(1, 11): + check_name = f"{FestivalCheck.trout_derby_reward_pattern}{i}" + self.registry.festival_rules[check_name] = self.logic.fishing.can_catch_fish(self.content.fishes[Fish.rainbow_trout]) + + def can_squidfest_day_1_iridium_reward(self) -> StardewRule: + return self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]) & self.logic.fishing.has_specific_bait(self.content.fishes[Fish.squid]) + + def has_squidfest_day_1_iridium_reward(self) -> StardewRule: + if self.options.festival_locations == FestivalLocations.option_disabled: + return self.logic.festival.can_squidfest_day_1_iridium_reward() + else: + return self.logic.received(f"Book: {Book.the_art_o_crabbing}") + + def can_win_egg_hunt(self) -> StardewRule: + return self.logic.true_ + + def can_succeed_luau_soup(self) -> StardewRule: + if self.options.festival_locations != FestivalLocations.option_hard: + return self.logic.true_ + eligible_fish = (Fish.blobfish, Fish.crimsonfish, Fish.ice_pip, Fish.lava_eel, Fish.legend, Fish.angler, Fish.catfish, Fish.glacierfish, + Fish.mutant_carp, Fish.spookfish, Fish.stingray, Fish.sturgeon, Fish.super_cucumber) + fish_rule = self.logic.has_any(*(f for f in eligible_fish if f in self.content.fishes)) # To filter stingray + eligible_kegables = (Fruit.ancient_fruit, Fruit.apple, Fruit.banana, Forageable.coconut, Forageable.crystal_fruit, Fruit.mango, Fruit.melon, + Fruit.orange, Fruit.peach, Fruit.pineapple, Fruit.pomegranate, Fruit.rhubarb, Fruit.starfruit, Fruit.strawberry, + Forageable.cactus_fruit, Fruit.cherry, Fruit.cranberries, Fruit.grape, Forageable.spice_berry, Forageable.wild_plum, + Vegetable.hops, Vegetable.wheat) + keg_rules = [self.logic.artisan.can_keg(kegable) for kegable in eligible_kegables if kegable in self.content.game_items] + aged_rule = self.logic.has(Machine.cask) & self.logic.or_(*keg_rules) + # There are a few other valid items, but I don't feel like coding them all + return fish_rule | aged_rule + + def can_succeed_grange_display(self) -> StardewRule: + if self.options.festival_locations != FestivalLocations.option_hard: + return self.logic.true_ + + animal_rule = self.logic.animal.has_animal(Generic.any) + artisan_rule = self.logic.artisan.can_keg(Generic.any) | self.logic.artisan.can_preserves_jar(Generic.any) + cooking_rule = self.logic.money.can_spend_at(Region.saloon, 220) # Salads at the bar are good enough + fish_rule = self.logic.skill.can_fish(difficulty=50) + forage_rule = self.logic.region.can_reach_any((Region.forest, Region.backwoods)) # Hazelnut always available since the grange display is in fall + mineral_rule = self.logic.action.can_open_geode(Generic.any) # More than half the minerals are good enough + good_fruits = (fruit + for fruit in + (Fruit.apple, Fruit.banana, Forageable.coconut, Forageable.crystal_fruit, Fruit.mango, Fruit.orange, Fruit.peach, Fruit.pomegranate, + Fruit.strawberry, Fruit.melon, Fruit.rhubarb, Fruit.pineapple, Fruit.ancient_fruit, Fruit.starfruit) + if fruit in self.content.game_items) + fruit_rule = self.logic.has_any(*good_fruits) + good_vegetables = (vegeteable + for vegeteable in + (Vegetable.amaranth, Vegetable.artichoke, Vegetable.beet, Vegetable.cauliflower, Forageable.fiddlehead_fern, Vegetable.kale, + Vegetable.radish, Vegetable.taro_root, Vegetable.yam, Vegetable.red_cabbage, Vegetable.pumpkin) + if vegeteable in self.content.game_items) + vegetable_rule = self.logic.has_any(*good_vegetables) + + return animal_rule & artisan_rule & cooking_rule & fish_rule & \ + forage_rule & fruit_rule & mineral_rule & vegetable_rule + + def can_win_fishing_competition(self) -> StardewRule: + return self.logic.skill.can_fish(difficulty=60) + + def has_all_rarecrows(self) -> StardewRule: + rules = [] + for rarecrow_number in range(1, 9): + rules.append(self.logic.received(f"Rarecrow #{rarecrow_number}")) + return self.logic.and_(*rules) diff --git a/worlds/stardew_valley/logic/logic.py b/worlds/stardew_valley/logic/logic.py index fb0d938fbb1e..9d4447439f7b 100644 --- a/worlds/stardew_valley/logic/logic.py +++ b/worlds/stardew_valley/logic/logic.py @@ -16,6 +16,7 @@ from .cooking_logic import CookingLogicMixin from .crafting_logic import CraftingLogicMixin from .farming_logic import FarmingLogicMixin +from .festival_logic import FestivalLogicMixin from .fishing_logic import FishingLogicMixin from .gift_logic import GiftLogicMixin from .grind_logic import GrindLogicMixin @@ -62,7 +63,6 @@ from ..strings.currency_names import Currency from ..strings.decoration_names import Decoration from ..strings.fertilizer_names import Fertilizer, SpeedGro, RetainingSoil -from ..strings.festival_check_names import FestivalCheck from ..strings.fish_names import Fish, Trash, WaterItem, WaterChest from ..strings.flower_names import Flower from ..strings.food_names import Meal, Beverage @@ -94,7 +94,7 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin CombatLogicMixin, MagicLogicMixin, MonsterLogicMixin, ToolLogicMixin, PetLogicMixin, QualityLogicMixin, SkillLogicMixin, FarmingLogicMixin, BundleLogicMixin, FishingLogicMixin, MineLogicMixin, CookingLogicMixin, AbilityLogicMixin, SpecialOrderLogicMixin, QuestLogicMixin, CraftingLogicMixin, ModLogicMixin, HarvestingLogicMixin, SourceLogicMixin, - RequirementLogicMixin, BookLogicMixin, GrindLogicMixin, WalnutLogicMixin): + RequirementLogicMixin, BookLogicMixin, GrindLogicMixin, FestivalLogicMixin, WalnutLogicMixin): player: int options: StardewValleyOptions content: StardewContent @@ -363,89 +363,7 @@ def __init__(self, player: int, options: StardewValleyOptions, content: StardewC self.quest.initialize_rules() self.quest.update_rules(self.mod.quest.get_modded_quest_rules()) - self.registry.festival_rules.update({ - FestivalCheck.egg_hunt: self.can_win_egg_hunt(), - FestivalCheck.strawberry_seeds: self.money.can_spend(1000), - FestivalCheck.dance: self.relationship.has_hearts_with_any_bachelor(4), - FestivalCheck.tub_o_flowers: self.money.can_spend(2000), - FestivalCheck.rarecrow_5: self.money.can_spend(2500), - FestivalCheck.luau_soup: self.can_succeed_luau_soup(), - FestivalCheck.moonlight_jellies: True_(), - FestivalCheck.moonlight_jellies_banner: self.money.can_spend(800), - FestivalCheck.starport_decal: self.money.can_spend(1000), - FestivalCheck.smashing_stone: True_(), - FestivalCheck.grange_display: self.can_succeed_grange_display(), - FestivalCheck.rarecrow_1: True_(), # only cost star tokens - FestivalCheck.fair_stardrop: True_(), # only cost star tokens - FestivalCheck.spirit_eve_maze: True_(), - FestivalCheck.jack_o_lantern: self.money.can_spend(2000), - FestivalCheck.rarecrow_2: self.money.can_spend(5000), - FestivalCheck.fishing_competition: self.can_win_fishing_competition(), - FestivalCheck.rarecrow_4: self.money.can_spend(5000), - FestivalCheck.mermaid_pearl: self.has(Forageable.secret_note), - FestivalCheck.cone_hat: self.money.can_spend(2500), - FestivalCheck.iridium_fireplace: self.money.can_spend(15000), - FestivalCheck.rarecrow_7: self.money.can_spend(5000) & self.museum.can_donate_museum_artifacts(20), - FestivalCheck.rarecrow_8: self.money.can_spend(5000) & self.museum.can_donate_museum_items(40), - FestivalCheck.lupini_red_eagle: self.money.can_spend(1200), - FestivalCheck.lupini_portrait_mermaid: self.money.can_spend(1200), - FestivalCheck.lupini_solar_kingdom: self.money.can_spend(1200), - FestivalCheck.lupini_clouds: self.time.has_year_two & self.money.can_spend(1200), - FestivalCheck.lupini_1000_years: self.time.has_year_two & self.money.can_spend(1200), - FestivalCheck.lupini_three_trees: self.time.has_year_two & self.money.can_spend(1200), - FestivalCheck.lupini_the_serpent: self.time.has_year_three & self.money.can_spend(1200), - FestivalCheck.lupini_tropical_fish: self.time.has_year_three & self.money.can_spend(1200), - FestivalCheck.lupini_land_of_clay: self.time.has_year_three & self.money.can_spend(1200), - FestivalCheck.secret_santa: self.gifts.has_any_universal_love, - FestivalCheck.legend_of_the_winter_star: True_(), - FestivalCheck.rarecrow_3: True_(), - FestivalCheck.all_rarecrows: self.region.can_reach(Region.farm) & self.has_all_rarecrows(), - FestivalCheck.calico_race: True_(), - FestivalCheck.mummy_mask: True_(), - FestivalCheck.calico_statue: True_(), - FestivalCheck.emily_outfit_service: True_(), - FestivalCheck.earthy_mousse: True_(), - FestivalCheck.sweet_bean_cake: True_(), - FestivalCheck.skull_cave_casserole: True_(), - FestivalCheck.spicy_tacos: True_(), - FestivalCheck.mountain_chili: True_(), - FestivalCheck.crystal_cake: True_(), - FestivalCheck.cave_kebab: True_(), - FestivalCheck.hot_log: True_(), - FestivalCheck.sour_salad: True_(), - FestivalCheck.superfood_cake: True_(), - FestivalCheck.warrior_smoothie: True_(), - FestivalCheck.rumpled_fruit_skin: True_(), - FestivalCheck.calico_pizza: True_(), - FestivalCheck.stuffed_mushrooms: True_(), - FestivalCheck.elf_quesadilla: True_(), - FestivalCheck.nachos_of_the_desert: True_(), - FestivalCheck.cloppino: True_(), - FestivalCheck.rainforest_shrimp: True_(), - FestivalCheck.shrimp_donut: True_(), - FestivalCheck.smell_of_the_sea: True_(), - FestivalCheck.desert_gumbo: True_(), - FestivalCheck.free_cactis: True_(), - FestivalCheck.monster_hunt: self.monster.can_kill(Monster.serpent), - FestivalCheck.deep_dive: self.region.can_reach(Region.skull_cavern_50), - FestivalCheck.treasure_hunt: self.region.can_reach(Region.skull_cavern_25), - FestivalCheck.touch_calico_statue: self.region.can_reach(Region.skull_cavern_25), - FestivalCheck.real_calico_egg_hunter: self.region.can_reach(Region.skull_cavern_100), - FestivalCheck.willy_challenge: self.fishing.can_catch_fish(content.fishes[Fish.scorpion_carp]), - FestivalCheck.desert_scholar: True_(), - FestivalCheck.squidfest_day_1_copper: self.fishing.can_catch_fish(content.fishes[Fish.squid]), - FestivalCheck.squidfest_day_1_iron: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & self.has(Fishing.bait), - FestivalCheck.squidfest_day_1_gold: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & self.has(Fishing.deluxe_bait), - FestivalCheck.squidfest_day_1_iridium: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & - self.fishing.has_specific_bait(content.fishes[Fish.squid]), - FestivalCheck.squidfest_day_2_copper: self.fishing.can_catch_fish(content.fishes[Fish.squid]), - FestivalCheck.squidfest_day_2_iron: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & self.has(Fishing.bait), - FestivalCheck.squidfest_day_2_gold: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & self.has(Fishing.deluxe_bait), - FestivalCheck.squidfest_day_2_iridium: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & - self.fishing.has_specific_bait(content.fishes[Fish.squid]), - }) - for i in range(1, 11): - self.registry.festival_rules[f"{FestivalCheck.trout_derby_reward_pattern}{i}"] = self.fishing.can_catch_fish(content.fishes[Fish.rainbow_trout]) + self.festival.initialize_rules() self.special_order.initialize_rules() self.special_order.update_rules(self.mod.special_order.get_modded_special_orders_rules()) @@ -486,53 +404,6 @@ def can_finish_grandpa_evaluation(self) -> StardewRule: ] return self.count(12, *rules_worth_a_point) - def can_win_egg_hunt(self) -> StardewRule: - return True_() - - def can_succeed_luau_soup(self) -> StardewRule: - if self.options.festival_locations != FestivalLocations.option_hard: - return True_() - eligible_fish = (Fish.blobfish, Fish.crimsonfish, Fish.ice_pip, Fish.lava_eel, Fish.legend, Fish.angler, Fish.catfish, Fish.glacierfish, - Fish.mutant_carp, Fish.spookfish, Fish.stingray, Fish.sturgeon, Fish.super_cucumber) - fish_rule = self.has_any(*(f for f in eligible_fish if f in self.content.fishes)) # To filter stingray - eligible_kegables = (Fruit.ancient_fruit, Fruit.apple, Fruit.banana, Forageable.coconut, Forageable.crystal_fruit, Fruit.mango, Fruit.melon, - Fruit.orange, Fruit.peach, Fruit.pineapple, Fruit.pomegranate, Fruit.rhubarb, Fruit.starfruit, Fruit.strawberry, - Forageable.cactus_fruit, Fruit.cherry, Fruit.cranberries, Fruit.grape, Forageable.spice_berry, Forageable.wild_plum, - Vegetable.hops, Vegetable.wheat) - keg_rules = [self.artisan.can_keg(kegable) for kegable in eligible_kegables if kegable in self.content.game_items] - aged_rule = self.has(Machine.cask) & self.logic.or_(*keg_rules) - # There are a few other valid items, but I don't feel like coding them all - return fish_rule | aged_rule - - def can_succeed_grange_display(self) -> StardewRule: - if self.options.festival_locations != FestivalLocations.option_hard: - return True_() - - animal_rule = self.animal.has_animal(Generic.any) - artisan_rule = self.artisan.can_keg(Generic.any) | self.artisan.can_preserves_jar(Generic.any) - cooking_rule = self.money.can_spend_at(Region.saloon, 220) # Salads at the bar are good enough - fish_rule = self.skill.can_fish(difficulty=50) - forage_rule = self.region.can_reach_any((Region.forest, Region.backwoods)) # Hazelnut always available since the grange display is in fall - mineral_rule = self.action.can_open_geode(Generic.any) # More than half the minerals are good enough - good_fruits = (fruit - for fruit in - (Fruit.apple, Fruit.banana, Forageable.coconut, Forageable.crystal_fruit, Fruit.mango, Fruit.orange, Fruit.peach, Fruit.pomegranate, - Fruit.strawberry, Fruit.melon, Fruit.rhubarb, Fruit.pineapple, Fruit.ancient_fruit, Fruit.starfruit) - if fruit in self.content.game_items) - fruit_rule = self.has_any(*good_fruits) - good_vegetables = (vegeteable - for vegeteable in - (Vegetable.amaranth, Vegetable.artichoke, Vegetable.beet, Vegetable.cauliflower, Forageable.fiddlehead_fern, Vegetable.kale, - Vegetable.radish, Vegetable.taro_root, Vegetable.yam, Vegetable.red_cabbage, Vegetable.pumpkin) - if vegeteable in self.content.game_items) - vegetable_rule = self.has_any(*good_vegetables) - - return animal_rule & artisan_rule & cooking_rule & fish_rule & \ - forage_rule & fruit_rule & mineral_rule & vegetable_rule - - def can_win_fishing_competition(self) -> StardewRule: - return self.skill.can_fish(difficulty=60) - def has_island_trader(self) -> StardewRule: if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true: return False_() @@ -571,12 +442,6 @@ def has_all_stardrops(self) -> StardewRule: return self.received("Stardrop", number_of_stardrops_to_receive) & self.logic.and_(*other_rules) - def has_all_rarecrows(self) -> StardewRule: - rules = [] - for rarecrow_number in range(1, 9): - rules.append(self.received(f"Rarecrow #{rarecrow_number}")) - return self.logic.and_(*rules) - def has_abandoned_jojamart(self) -> StardewRule: return self.received(CommunityUpgrade.movie_theater, 1) From 2b88be5791ae260048850ba652f2ba0aadeaeed9 Mon Sep 17 00:00:00 2001 From: Kaito Sinclaire Date: Sat, 21 Sep 2024 14:06:31 -0700 Subject: [PATCH 53/94] Doom 1993 (auto-generated files): Update E4 logic (#3957) --- worlds/doom_1993/Locations.py | 4 ++-- worlds/doom_1993/Regions.py | 15 ++++++++++----- worlds/doom_1993/Rules.py | 5 ++--- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/worlds/doom_1993/Locations.py b/worlds/doom_1993/Locations.py index 2cbb9b9d150e..90a6916cd716 100644 --- a/worlds/doom_1993/Locations.py +++ b/worlds/doom_1993/Locations.py @@ -2214,13 +2214,13 @@ class LocationDict(TypedDict, total=False): 'map': 2, 'index': 217, 'doom_type': 2006, - 'region': "Perfect Hatred (E4M2) Blue"}, + 'region': "Perfect Hatred (E4M2) Upper"}, 351367: {'name': 'Perfect Hatred (E4M2) - Exit', 'episode': 4, 'map': 2, 'index': -1, 'doom_type': -1, - 'region': "Perfect Hatred (E4M2) Blue"}, + 'region': "Perfect Hatred (E4M2) Upper"}, 351368: {'name': 'Sever the Wicked (E4M3) - Invulnerability', 'episode': 4, 'map': 3, diff --git a/worlds/doom_1993/Regions.py b/worlds/doom_1993/Regions.py index f013bdceaf07..c32f7b470101 100644 --- a/worlds/doom_1993/Regions.py +++ b/worlds/doom_1993/Regions.py @@ -502,13 +502,12 @@ class RegionDict(TypedDict, total=False): "episode":4, "connections":[ {"target":"Perfect Hatred (E4M2) Blue","pro":False}, - {"target":"Perfect Hatred (E4M2) Yellow","pro":False}]}, + {"target":"Perfect Hatred (E4M2) Yellow","pro":False}, + {"target":"Perfect Hatred (E4M2) Upper","pro":True}]}, {"name":"Perfect Hatred (E4M2) Blue", "connects_to_hub":False, "episode":4, - "connections":[ - {"target":"Perfect Hatred (E4M2) Main","pro":False}, - {"target":"Perfect Hatred (E4M2) Cave","pro":False}]}, + "connections":[{"target":"Perfect Hatred (E4M2) Upper","pro":False}]}, {"name":"Perfect Hatred (E4M2) Yellow", "connects_to_hub":False, "episode":4, @@ -518,7 +517,13 @@ class RegionDict(TypedDict, total=False): {"name":"Perfect Hatred (E4M2) Cave", "connects_to_hub":False, "episode":4, - "connections":[]}, + "connections":[{"target":"Perfect Hatred (E4M2) Main","pro":False}]}, + {"name":"Perfect Hatred (E4M2) Upper", + "connects_to_hub":False, + "episode":4, + "connections":[ + {"target":"Perfect Hatred (E4M2) Cave","pro":False}, + {"target":"Perfect Hatred (E4M2) Main","pro":False}]}, # Sever the Wicked (E4M3) {"name":"Sever the Wicked (E4M3) Main", diff --git a/worlds/doom_1993/Rules.py b/worlds/doom_1993/Rules.py index 4faeb4a27dbd..89b09ff9f250 100644 --- a/worlds/doom_1993/Rules.py +++ b/worlds/doom_1993/Rules.py @@ -403,9 +403,8 @@ def set_episode4_rules(player, multiworld, pro): state.has("Chaingun", player, 1)) and (state.has("Plasma gun", player, 1) or state.has("BFG9000", player, 1))) set_rule(multiworld.get_entrance("Hell Beneath (E4M1) Main -> Hell Beneath (E4M1) Blue", player), lambda state: - state.has("Shotgun", player, 1) or - state.has("Chaingun", player, 1) or - state.has("Hell Beneath (E4M1) - Blue skull key", player, 1)) + (state.has("Hell Beneath (E4M1) - Blue skull key", player, 1)) and (state.has("Shotgun", player, 1) or + state.has("Chaingun", player, 1))) # Perfect Hatred (E4M2) set_rule(multiworld.get_entrance("Hub -> Perfect Hatred (E4M2) Main", player), lambda state: From 97ca2ad258de7b4ea1f477ba409d36f5a15a0101 Mon Sep 17 00:00:00 2001 From: CookieCat <81494827+CookieCat45@users.noreply.github.com> Date: Sat, 21 Sep 2024 17:10:18 -0400 Subject: [PATCH 54/94] AHIT: Fix massive lag spikes in extremely large multiworlds, add extra security to prevent loading the wrong save file for a seed (#3718) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * duh * Fuck it * Major fixes * a * b * Even more fixes * New option - NoFreeRoamFinale * a * Hat Logic Fix * Just to be safe * multiworld.random to world.random * KeyError fix * Update .gitignore * Update __init__.py * Zoinks Scoob * ffs * Ruh Roh Raggy, more r-r-r-random bugs! * 0.9b - cleanup + expanded logic difficulty * Update Rules.py * Update Regions.py * AttributeError fix * 0.10b - New Options * 1.0 Preparations * Docs * Docs 2 * Fixes * Update __init__.py * Fixes * variable capture my beloathed * Fixes * a * 10 Seconds logic fix * 1.1 * 1.2 * a * New client * More client changes * 1.3 * Final touch-ups for 1.3 * 1.3.1 * 1.3.3 * Zero Jumps gen error fix * more fixes * Formatting improvements * typo * Update __init__.py * Revert "Update __init__.py" This reverts commit e178a7c0a6904ace803241cab3021d7b97177e90. * init * Update to new options API * Missed some * Snatcher Coins fix * Missed some more * some slight touch ups * rewind * a * fix things * Revert "Merge branch 'main' of https://github.com/CookieCat45/Archipelago-ahit" This reverts commit a2360fe197e77a723bb70006c5eb5725c7ed3826, reversing changes made to b8948bc4958855c6e342e18bdb8dc81cfcf09455. * Update .gitignore * 1.3.6 * Final touch-ups * Fix client and leftover old options api * Delete setup-ahitclient.py * Update .gitignore * old python version fix * proper warnings for invalid act plandos * Update worlds/ahit/docs/en_A Hat in Time.md Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com> * Update worlds/ahit/docs/setup_en.md Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com> * 120 char per line * "settings" to "options" * Update DeathWishRules.py * Update worlds/ahit/docs/en_A Hat in Time.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * No more loading the data package * cleanup + act plando fixes * almost forgot * Update Rules.py * a * Update worlds/ahit/Options.py Co-authored-by: Ixrec * Options stuff * oop * no unnecessary type hints * warn about depot download length in setup guide * Update worlds/ahit/Options.py Co-authored-by: Ixrec * typo Co-authored-by: Ixrec * Update worlds/ahit/Rules.py Co-authored-by: Ixrec * review stuff * More stuff from review * comment * 1.5 Update * link fix? * link fix 2 * Update setup_en.md * Update setup_en.md * Update setup_en.md * Evil * Good fucking lord * Review stuff again + Logic fixes * More review stuff * Even more review stuff - we're almost done * DW review stuff * Finish up review stuff * remove leftover stuff * a * assert item * add A Hat in Time to readme/codeowners files * Fix range options not being corrected properly * 120 chars per line in docs * Update worlds/ahit/Regions.py Co-authored-by: Aaron Wagener * Update worlds/ahit/DeathWishLocations.py Co-authored-by: Aaron Wagener * Remove some unnecessary option.class.value * Remove data_version and more option.class.value * Update worlds/ahit/Items.py Co-authored-by: Aaron Wagener * Remove the rest of option.class.value * Update worlds/ahit/DeathWishLocations.py Co-authored-by: Aaron Wagener * review stuff * Replace connect_regions with Region.connect * review stuff * Remove unnecessary Optional from LocData * Remove HatType.NONE * Update worlds/ahit/test/TestActs.py Co-authored-by: Aaron Wagener * fix so default tests actually don't run * Improve performance for death wish rules * rename test file * change test imports * 1000 is probably unnecessary * a * change state.count to state.has * stuff * starting inventory hats fix * shouldn't have done this lol * make ship shape task goal equal to number of tasksanity checks if set to 0 * a * change act shuffle starting acts + logic updates * dumb * option groups + lambda capture cringe + typo * a * b * missing option in groups * c * Fix Your Contract Has Expired being placed on first level when it shouldn't * yche fix * formatting * major logic bug fix for death wish * Update Regions.py * Add missing indirect connections * Fix generation error from chapter 2 start with act shuffle off * a * Revert "a" This reverts commit df58bbcd998585760cc6ac9ea54b6fdf142b4fd1. * Revert "Fix generation error from chapter 2 start with act shuffle off" This reverts commit 0f4d441824af34bf7a7cff19f5f14161752d8661. * fix async lag * Update Client.py * shop item names need this now * fix indentation --------- Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com> Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Co-authored-by: Ixrec Co-authored-by: Aaron Wagener Co-authored-by: Fabian Dill --- worlds/ahit/Client.py | 38 ++++++++++++++++++++++++++++++++++++-- worlds/ahit/__init__.py | 3 ++- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/worlds/ahit/Client.py b/worlds/ahit/Client.py index 2cd67e468294..cbb5f2a13d1f 100644 --- a/worlds/ahit/Client.py +++ b/worlds/ahit/Client.py @@ -4,7 +4,7 @@ import functools from copy import deepcopy from typing import List, Any, Iterable -from NetUtils import decode, encode, JSONtoTextParser, JSONMessagePart, NetworkItem +from NetUtils import decode, encode, JSONtoTextParser, JSONMessagePart, NetworkItem, NetworkPlayer from MultiServer import Endpoint from CommonClient import CommonContext, gui_enabled, ClientCommandProcessor, logger, get_base_parser @@ -101,12 +101,35 @@ def update_items(self): def on_package(self, cmd: str, args: dict): if cmd == "Connected": - self.connected_msg = encode([args]) + json = args + # This data is not needed and causes the game to freeze for long periods of time in large asyncs. + if "slot_info" in json.keys(): + json["slot_info"] = {} + if "players" in json.keys(): + me: NetworkPlayer + for n in json["players"]: + if n.slot == json["slot"] and n.team == json["team"]: + me = n + break + + # Only put our player info in there as we actually need it + json["players"] = [me] + if DEBUG: + print(json) + self.connected_msg = encode([json]) if self.awaiting_info: self.server_msgs.append(self.room_info) self.update_items() self.awaiting_info = False + elif cmd == "RoomUpdate": + # Same story as above + json = args + if "players" in json.keys(): + json["players"] = [] + + self.server_msgs.append(encode(json)) + elif cmd == "ReceivedItems": if args["index"] == 0: self.full_inventory.clear() @@ -166,6 +189,17 @@ async def proxy(websocket, path: str = "/", ctx: AHITContext = None): await ctx.disconnect_proxy() break + if ctx.auth: + name = msg.get("name", "") + if name != "" and name != ctx.auth: + logger.info("Aborting proxy connection: player name mismatch from save file") + logger.info(f"Expected: {ctx.auth}, got: {name}") + text = encode([{"cmd": "PrintJSON", + "data": [{"text": "Connection aborted - player name mismatch"}]}]) + await ctx.send_msgs_proxy(text) + await ctx.disconnect_proxy() + break + if ctx.connected_msg and ctx.is_connected(): await ctx.send_msgs_proxy(ctx.connected_msg) ctx.update_items() diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index dd5e88abbc66..14cf13ec346d 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -253,7 +253,8 @@ def fill_slot_data(self) -> dict: else: item_name = loc.item.name - shop_item_names.setdefault(str(loc.address), item_name) + shop_item_names.setdefault(str(loc.address), + f"{item_name} ({self.multiworld.get_player_name(loc.item.player)})") slot_data["ShopItemNames"] = shop_item_names From 449782a4d89303ed03759a14e6b9ef92fc9ae07b Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Sun, 22 Sep 2024 10:21:10 -0400 Subject: [PATCH 55/94] TUNIC: Add forgotten Laurels rule for Beneath the Vault Boxes #3981 --- worlds/tunic/er_rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index 2677ec409b3b..bd2498a56a35 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -764,7 +764,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world) and has_lantern(state, world) # there's some boxes in the way - and (has_stick(state, player) or state.has_any((gun, grapple, fire_wand), player))) + and (has_stick(state, player) or state.has_any((gun, grapple, fire_wand, laurels), player))) # on the reverse trip, you can lure an enemy over to break the boxes if needed regions["Beneath the Vault Main"].connect( connecting_region=regions["Beneath the Vault Ladder Exit"], From 99c02a3eb3157dfc345f770197372c59313d77d0 Mon Sep 17 00:00:00 2001 From: CookieCat <81494827+CookieCat45@users.noreply.github.com> Date: Sun, 22 Sep 2024 10:22:11 -0400 Subject: [PATCH 56/94] AHIT: Fix Death Wish option check typo (#3978) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * duh * Fuck it * Major fixes * a * b * Even more fixes * New option - NoFreeRoamFinale * a * Hat Logic Fix * Just to be safe * multiworld.random to world.random * KeyError fix * Update .gitignore * Update __init__.py * Zoinks Scoob * ffs * Ruh Roh Raggy, more r-r-r-random bugs! * 0.9b - cleanup + expanded logic difficulty * Update Rules.py * Update Regions.py * AttributeError fix * 0.10b - New Options * 1.0 Preparations * Docs * Docs 2 * Fixes * Update __init__.py * Fixes * variable capture my beloathed * Fixes * a * 10 Seconds logic fix * 1.1 * 1.2 * a * New client * More client changes * 1.3 * Final touch-ups for 1.3 * 1.3.1 * 1.3.3 * Zero Jumps gen error fix * more fixes * Formatting improvements * typo * Update __init__.py * Revert "Update __init__.py" This reverts commit e178a7c0a6904ace803241cab3021d7b97177e90. * init * Update to new options API * Missed some * Snatcher Coins fix * Missed some more * some slight touch ups * rewind * a * fix things * Revert "Merge branch 'main' of https://github.com/CookieCat45/Archipelago-ahit" This reverts commit a2360fe197e77a723bb70006c5eb5725c7ed3826, reversing changes made to b8948bc4958855c6e342e18bdb8dc81cfcf09455. * Update .gitignore * 1.3.6 * Final touch-ups * Fix client and leftover old options api * Delete setup-ahitclient.py * Update .gitignore * old python version fix * proper warnings for invalid act plandos * Update worlds/ahit/docs/en_A Hat in Time.md Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com> * Update worlds/ahit/docs/setup_en.md Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com> * 120 char per line * "settings" to "options" * Update DeathWishRules.py * Update worlds/ahit/docs/en_A Hat in Time.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * No more loading the data package * cleanup + act plando fixes * almost forgot * Update Rules.py * a * Update worlds/ahit/Options.py Co-authored-by: Ixrec * Options stuff * oop * no unnecessary type hints * warn about depot download length in setup guide * Update worlds/ahit/Options.py Co-authored-by: Ixrec * typo Co-authored-by: Ixrec * Update worlds/ahit/Rules.py Co-authored-by: Ixrec * review stuff * More stuff from review * comment * 1.5 Update * link fix? * link fix 2 * Update setup_en.md * Update setup_en.md * Update setup_en.md * Evil * Good fucking lord * Review stuff again + Logic fixes * More review stuff * Even more review stuff - we're almost done * DW review stuff * Finish up review stuff * remove leftover stuff * a * assert item * add A Hat in Time to readme/codeowners files * Fix range options not being corrected properly * 120 chars per line in docs * Update worlds/ahit/Regions.py Co-authored-by: Aaron Wagener * Update worlds/ahit/DeathWishLocations.py Co-authored-by: Aaron Wagener * Remove some unnecessary option.class.value * Remove data_version and more option.class.value * Update worlds/ahit/Items.py Co-authored-by: Aaron Wagener * Remove the rest of option.class.value * Update worlds/ahit/DeathWishLocations.py Co-authored-by: Aaron Wagener * review stuff * Replace connect_regions with Region.connect * review stuff * Remove unnecessary Optional from LocData * Remove HatType.NONE * Update worlds/ahit/test/TestActs.py Co-authored-by: Aaron Wagener * fix so default tests actually don't run * Improve performance for death wish rules * rename test file * change test imports * 1000 is probably unnecessary * a * change state.count to state.has * stuff * starting inventory hats fix * shouldn't have done this lol * make ship shape task goal equal to number of tasksanity checks if set to 0 * a * change act shuffle starting acts + logic updates * dumb * option groups + lambda capture cringe + typo * a * b * missing option in groups * c * Fix Your Contract Has Expired being placed on first level when it shouldn't * yche fix * formatting * major logic bug fix for death wish * Update Regions.py * Add missing indirect connections * Fix generation error from chapter 2 start with act shuffle off * a * Revert "a" This reverts commit df58bbcd998585760cc6ac9ea54b6fdf142b4fd1. * Revert "Fix generation error from chapter 2 start with act shuffle off" This reverts commit 0f4d441824af34bf7a7cff19f5f14161752d8661. * Fix option typo * I lied, it's actually two lines --------- Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com> Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Co-authored-by: Ixrec Co-authored-by: Aaron Wagener Co-authored-by: Fabian Dill --- worlds/ahit/DeathWishLocations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/ahit/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py index ef74cadcaa53..ce339c7c19bb 100644 --- a/worlds/ahit/DeathWishLocations.py +++ b/worlds/ahit/DeathWishLocations.py @@ -152,10 +152,10 @@ def create_dw_regions(world: "HatInTimeWorld"): for name in annoying_dws: world.excluded_dws.append(name) - if not world.options.DWEnableBonus or world.options.DWAutoCompleteBonuses: + if not world.options.DWEnableBonus and world.options.DWAutoCompleteBonuses: for name in death_wishes: world.excluded_bonuses.append(name) - elif world.options.DWExcludeAnnoyingBonuses: + if world.options.DWExcludeAnnoyingBonuses and not world.options.DWAutoCompleteBonuses: for name in annoying_bonuses: world.excluded_bonuses.append(name) From f7ec3d750873324fce6d671418d89ccb7439a5e4 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sun, 22 Sep 2024 09:24:14 -0500 Subject: [PATCH 57/94] kvui: abstract away client tab additions #3950 --- WargrooveClient.py | 4 +--- kvui.py | 13 ++++++++++--- worlds/sc2/ClientGui.py | 5 +---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/WargrooveClient.py b/WargrooveClient.py index 39da044d659c..f9971f7a6c05 100644 --- a/WargrooveClient.py +++ b/WargrooveClient.py @@ -267,9 +267,7 @@ class WargrooveManager(GameManager): def build(self): container = super().build() - panel = TabbedPanelItem(text="Wargroove") - panel.content = self.build_tracker() - self.tabs.add_widget(panel) + self.add_client_tab("Wargroove", self.build_tracker()) return container def build_tracker(self) -> TrackerLayout: diff --git a/kvui.py b/kvui.py index 65cf52c7a4aa..536dce12208e 100644 --- a/kvui.py +++ b/kvui.py @@ -536,9 +536,8 @@ def connect_bar_validate(sender): # show Archipelago tab if other logging is present self.tabs.add_widget(panel) - hint_panel = TabbedPanelItem(text="Hints") - self.log_panels["Hints"] = hint_panel.content = HintLog(self.json_to_kivy_parser) - self.tabs.add_widget(hint_panel) + hint_panel = self.add_client_tab("Hints", HintLog(self.json_to_kivy_parser)) + self.log_panels["Hints"] = hint_panel.content if len(self.logging_pairs) == 1: self.tabs.default_tab_text = "Archipelago" @@ -572,6 +571,14 @@ def connect_bar_validate(sender): return self.container + def add_client_tab(self, title: str, content: Widget) -> Widget: + """Adds a new tab to the client window with a given title, and provides a given Widget as its content. + Returns the new tab widget, with the provided content being placed on the tab as content.""" + new_tab = TabbedPanelItem(text=title) + new_tab.content = content + self.tabs.add_widget(new_tab) + return new_tab + def update_texts(self, dt): if hasattr(self.tabs.content.children[0], "fix_heights"): self.tabs.content.children[0].fix_heights() # TODO: remove this when Kivy fixes this upstream diff --git a/worlds/sc2/ClientGui.py b/worlds/sc2/ClientGui.py index fe62e6162457..51c55b437d92 100644 --- a/worlds/sc2/ClientGui.py +++ b/worlds/sc2/ClientGui.py @@ -111,13 +111,10 @@ def clear_tooltip(self) -> None: def build(self): container = super().build() - panel = TabbedPanelItem(text="Starcraft 2 Launcher") - panel.content = CampaignScroll() + panel = self.add_client_tab("Starcraft 2 Launcher", CampaignScroll()) self.campaign_panel = MultiCampaignLayout() panel.content.add_widget(self.campaign_panel) - self.tabs.add_widget(panel) - Clock.schedule_interval(self.build_mission_table, 0.5) return container From d43dc6248506d3936a35063fa357352ad85f423b Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Sun, 22 Sep 2024 18:14:04 -0400 Subject: [PATCH 58/94] Stardew Valley: Improve Junimo Kart Regions #3984 --- worlds/stardew_valley/data/locations.csv | 4 ++-- worlds/stardew_valley/regions.py | 4 +++- worlds/stardew_valley/rules.py | 2 +- worlds/stardew_valley/strings/entrance_names.py | 1 + worlds/stardew_valley/strings/region_names.py | 1 + 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv index 608b6a5f576a..680ddfcbacbf 100644 --- a/worlds/stardew_valley/data/locations.csv +++ b/worlds/stardew_valley/data/locations.csv @@ -313,14 +313,14 @@ id,region,name,tags,mod_name 611,JotPK World 2,JotPK: Cowboy 2,"ARCADE_MACHINE,JOTPK", 612,Junimo Kart 1,Junimo Kart: Crumble Cavern,"ARCADE_MACHINE,JUNIMO_KART", 613,Junimo Kart 1,Junimo Kart: Slippery Slopes,"ARCADE_MACHINE,JUNIMO_KART", -614,Junimo Kart 2,Junimo Kart: Secret Level,"ARCADE_MACHINE,JUNIMO_KART", +614,Junimo Kart 4,Junimo Kart: Secret Level,"ARCADE_MACHINE,JUNIMO_KART", 615,Junimo Kart 2,Junimo Kart: The Gem Sea Giant,"ARCADE_MACHINE,JUNIMO_KART", 616,Junimo Kart 2,Junimo Kart: Slomp's Stomp,"ARCADE_MACHINE,JUNIMO_KART", 617,Junimo Kart 2,Junimo Kart: Ghastly Galleon,"ARCADE_MACHINE,JUNIMO_KART", 618,Junimo Kart 3,Junimo Kart: Glowshroom Grotto,"ARCADE_MACHINE,JUNIMO_KART", 619,Junimo Kart 3,Junimo Kart: Red Hot Rollercoaster,"ARCADE_MACHINE,JUNIMO_KART", 620,JotPK World 3,Journey of the Prairie King Victory,"ARCADE_MACHINE_VICTORY,JOTPK", -621,Junimo Kart 3,Junimo Kart: Sunset Speedway (Victory),"ARCADE_MACHINE_VICTORY,JUNIMO_KART", +621,Junimo Kart 4,Junimo Kart: Sunset Speedway (Victory),"ARCADE_MACHINE_VICTORY,JUNIMO_KART", 701,Secret Woods,Old Master Cannoli,MANDATORY, 702,Beach,Beach Bridge Repair,MANDATORY, 703,Desert,Galaxy Sword Shrine,MANDATORY, diff --git a/worlds/stardew_valley/regions.py b/worlds/stardew_valley/regions.py index b0fc7fa0ea52..5b7db5ac79d1 100644 --- a/worlds/stardew_valley/regions.py +++ b/worlds/stardew_valley/regions.py @@ -87,7 +87,8 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: RegionData(Region.jotpk_world_3), RegionData(Region.junimo_kart_1, [Entrance.reach_junimo_kart_2]), RegionData(Region.junimo_kart_2, [Entrance.reach_junimo_kart_3]), - RegionData(Region.junimo_kart_3), + RegionData(Region.junimo_kart_3, [Entrance.reach_junimo_kart_4]), + RegionData(Region.junimo_kart_4), RegionData(Region.alex_house), RegionData(Region.trailer), RegionData(Region.mayor_house), @@ -330,6 +331,7 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: ConnectionData(Entrance.play_junimo_kart, Region.junimo_kart_1), ConnectionData(Entrance.reach_junimo_kart_2, Region.junimo_kart_2), ConnectionData(Entrance.reach_junimo_kart_3, Region.junimo_kart_3), + ConnectionData(Entrance.reach_junimo_kart_4, Region.junimo_kart_4), ConnectionData(Entrance.town_to_sam_house, Region.sam_house, flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA), ConnectionData(Entrance.town_to_haley_house, Region.haley_house, diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index 7f39ee1ac2d4..eda2d4377e09 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -891,7 +891,7 @@ def set_arcade_machine_rules(logic: StardewLogic, multiworld: MultiWorld, player logic.has("Junimo Kart Medium Buff")) MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_junimo_kart_3, player), logic.has("Junimo Kart Big Buff")) - MultiWorldRules.add_rule(multiworld.get_location("Junimo Kart: Sunset Speedway (Victory)", player), + MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_junimo_kart_4, player), logic.has("Junimo Kart Max Buff")) MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.play_journey_of_the_prairie_king, player), logic.has("JotPK Small Buff")) diff --git a/worlds/stardew_valley/strings/entrance_names.py b/worlds/stardew_valley/strings/entrance_names.py index 58a919f2a8a4..b1c84004eb7a 100644 --- a/worlds/stardew_valley/strings/entrance_names.py +++ b/worlds/stardew_valley/strings/entrance_names.py @@ -94,6 +94,7 @@ class Entrance: play_junimo_kart = "Play Junimo Kart" reach_junimo_kart_2 = "Reach Junimo Kart 2" reach_junimo_kart_3 = "Reach Junimo Kart 3" + reach_junimo_kart_4 = "Reach Junimo Kart 4" enter_locker_room = "Bathhouse Entrance to Locker Room" enter_public_bath = "Locker Room to Public Bath" enter_witch_swamp = "Witch Warp Cave to Witch's Swamp" diff --git a/worlds/stardew_valley/strings/region_names.py b/worlds/stardew_valley/strings/region_names.py index 58763b6fcb80..2bbc6228ab19 100644 --- a/worlds/stardew_valley/strings/region_names.py +++ b/worlds/stardew_valley/strings/region_names.py @@ -114,6 +114,7 @@ class Region: junimo_kart_1 = "Junimo Kart 1" junimo_kart_2 = "Junimo Kart 2" junimo_kart_3 = "Junimo Kart 3" + junimo_kart_4 = "Junimo Kart 4" mines_floor_5 = "The Mines - Floor 5" mines_floor_10 = "The Mines - Floor 10" mines_floor_15 = "The Mines - Floor 15" From 8021b457b6e0193b047f90de196963ee6460eaf1 Mon Sep 17 00:00:00 2001 From: Mrks <68022469+mrkssr@users.noreply.github.com> Date: Mon, 23 Sep 2024 23:19:26 +0200 Subject: [PATCH 59/94] WebHost: Added Games Of A Seed To The User Content Page (#3585) * Added contained games of a seed to the user content page as tooltip. * Changed sort handling. * Limited amount of shown games. * Added missing dashes. Co-authored-by: Kory Dondzila * Closing a-tags. Co-authored-by: Kory Dondzila * Closing a-tags. Co-authored-by: Kory Dondzila * Moved games list to table cell level. Co-authored-by: Kory Dondzila * Moved games list to table cell level. --------- Co-authored-by: Kory Dondzila --- WebHostLib/templates/userContent.html | 31 +++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/WebHostLib/templates/userContent.html b/WebHostLib/templates/userContent.html index 71a0f6747bc3..4e3747f4f952 100644 --- a/WebHostLib/templates/userContent.html +++ b/WebHostLib/templates/userContent.html @@ -1,5 +1,21 @@ {% extends 'tablepage.html' %} +{%- macro games(slots) -%} + {%- set gameList = [] -%} + {%- set maxGamesToShow = 10 -%} + + {%- for slot in (slots|list|sort(attribute="player_id"))[:maxGamesToShow] -%} + {% set player = "#" + slot["player_id"]|string + " " + slot["player_name"] + " : " + slot["game"] -%} + {% set _ = gameList.append(player) -%} + {%- endfor -%} + + {%- if slots|length > maxGamesToShow -%} + {% set _ = gameList.append("... and " + (slots|length - maxGamesToShow)|string + " more") -%} + {%- endif -%} + + {{ gameList|join('\n') }} +{%- endmacro -%} + {% block head %} {{ super() }} User Content @@ -33,10 +49,12 @@

Your Rooms

{{ room.seed.id|suuid }} {{ room.id|suuid }} - {{ room.seed.slots|length }} + + {{ room.seed.slots|length }} + {{ room.creation_time.strftime("%Y-%m-%d %H:%M") }} {{ room.last_activity.strftime("%Y-%m-%d %H:%M") }} - Delete next maintenance. + Delete next maintenance. {% endfor %} @@ -60,10 +78,15 @@

Your Seeds

{% for seed in seeds %} {{ seed.id|suuid }} - {% if seed.multidata %}{{ seed.slots|length }}{% else %}1{% endif %} + + {% if seed.multidata %} + {{ seed.slots|length }} + {% else %} + 1 + {% endif %} {{ seed.creation_time.strftime("%Y-%m-%d %H:%M") }} - Delete next maintenance. + Delete next maintenance. {% endfor %} From f06d4503d83209b8fae6897eef500493d57826e8 Mon Sep 17 00:00:00 2001 From: Kory Dondzila Date: Mon, 23 Sep 2024 16:21:03 -0500 Subject: [PATCH 60/94] Adds link to other players' trackers in player hints. (#3569) --- WebHostLib/templates/genericTracker.html | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/WebHostLib/templates/genericTracker.html b/WebHostLib/templates/genericTracker.html index 5a533204083b..947cf2837278 100644 --- a/WebHostLib/templates/genericTracker.html +++ b/WebHostLib/templates/genericTracker.html @@ -99,14 +99,18 @@ {% if hint.finding_player == player %} {{ player_names_with_alias[(team, hint.finding_player)] }} {% else %} - {{ player_names_with_alias[(team, hint.finding_player)] }} + + {{ player_names_with_alias[(team, hint.finding_player)] }} + {% endif %} {% if hint.receiving_player == player %} {{ player_names_with_alias[(team, hint.receiving_player)] }} {% else %} - {{ player_names_with_alias[(team, hint.receiving_player)] }} + + {{ player_names_with_alias[(team, hint.receiving_player)] }} + {% endif %} {{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }} From e910a372733aee02d37cd784ca2398874bea1a04 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Wed, 25 Sep 2024 17:47:38 +0200 Subject: [PATCH 61/94] Core: Put an assert for parent region in Entrance.can_reach just like the one in Location.can_reach (#3998) * Core: Move connection.parent_region assert to can_reach This is how it already works for locations and it feels more correct to me to check in the place where the crash would happen. Also update location error to be a bit more verbose * Bring back the other assert * Update BaseClasses.py --- BaseClasses.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index a5de1689a7fe..916a5b18042d 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -720,7 +720,7 @@ def _update_reachable_regions_explicit_indirect_conditions(self, player: int, qu if new_region in reachable_regions: blocked_connections.remove(connection) elif connection.can_reach(self): - assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region" + assert new_region, f"tried to search through an Entrance \"{connection}\" with no connected Region" reachable_regions.add(new_region) blocked_connections.remove(connection) blocked_connections.update(new_region.exits) @@ -946,6 +946,7 @@ def __init__(self, player: int, name: str = "", parent: Optional[Region] = None) self.player = player def can_reach(self, state: CollectionState) -> bool: + assert self.parent_region, f"called can_reach on an Entrance \"{self}\" with no parent_region" if self.parent_region.can_reach(state) and self.access_rule(state): if not self.hide_path and not self in state.path: state.path[self] = (self.name, state.path.get(self.parent_region, (self.parent_region.name, None))) @@ -1166,7 +1167,7 @@ def can_fill(self, state: CollectionState, item: Item, check_access: bool = True def can_reach(self, state: CollectionState) -> bool: # Region.can_reach is just a cache lookup, so placing it first for faster abort on average - assert self.parent_region, "Can't reach location without region" + assert self.parent_region, f"called can_reach on a Location \"{self}\" with no parent_region" return self.parent_region.can_reach(state) and self.access_rule(state) def place_locked_item(self, item: Item): From 9a9fea0ca2d686ca350c93ae246e02da44a36b77 Mon Sep 17 00:00:00 2001 From: Felix R <50271878+FelicitusNeko@users.noreply.github.com> Date: Thu, 26 Sep 2024 15:47:03 -0300 Subject: [PATCH 62/94] bumpstik: add hazard bumpers to completion (#3991) * bumpstik: add hazard bumpers to completion * bumpstik: update to use has_all_counts for completion as suggested by ScipioWright --- worlds/bumpstik/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/bumpstik/__init__.py b/worlds/bumpstik/__init__.py index fe261dc94d30..ffe9efd2de87 100644 --- a/worlds/bumpstik/__init__.py +++ b/worlds/bumpstik/__init__.py @@ -125,6 +125,6 @@ def set_rules(self): lambda state: state.has("Hazard Bumper", self.player, 25) self.multiworld.completion_condition[self.player] = \ - lambda state: state.has("Booster Bumper", self.player, 5) and \ - state.has("Treasure Bumper", self.player, 32) + lambda state: state.has_all_counts({"Booster Bumper": 5, "Treasure Bumper": 32, "Hazard Bumper": 25}, \ + self.player) From e85a835b47b082936b8fb7233d8857d6a0c81a17 Mon Sep 17 00:00:00 2001 From: qwint Date: Thu, 26 Sep 2024 18:02:10 -0400 Subject: [PATCH 63/94] Core: use base collect/remove for item link groups (#3999) * use base collect/remove for item link groups * Update BaseClasses.py --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- BaseClasses.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index 916a5b18042d..0d4f34e51445 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -194,7 +194,9 @@ def add_group(self, name: str, game: str, players: AbstractSet[int] = frozenset( self.player_types[new_id] = NetUtils.SlotType.group world_type = AutoWorld.AutoWorldRegister.world_types[game] self.worlds[new_id] = world_type.create_group(self, new_id, players) - self.worlds[new_id].collect_item = classmethod(AutoWorld.World.collect_item).__get__(self.worlds[new_id]) + self.worlds[new_id].collect_item = AutoWorld.World.collect_item.__get__(self.worlds[new_id]) + self.worlds[new_id].collect = AutoWorld.World.collect.__get__(self.worlds[new_id]) + self.worlds[new_id].remove = AutoWorld.World.remove.__get__(self.worlds[new_id]) self.player_name[new_id] = name new_group = self.groups[new_id] = Group(name=name, game=game, players=players, From a043ed50a6af54dd1b80efb06d251bc83e6ab2ad Mon Sep 17 00:00:00 2001 From: Benny D <78334662+benny-dreamly@users.noreply.github.com> Date: Thu, 26 Sep 2024 16:56:36 -0600 Subject: [PATCH 64/94] Timespinner: Fix Typo in Download Location #3997 --- worlds/timespinner/Locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/timespinner/Locations.py b/worlds/timespinner/Locations.py index f99dd7615571..2423e06bb010 100644 --- a/worlds/timespinner/Locations.py +++ b/worlds/timespinner/Locations.py @@ -207,7 +207,7 @@ def get_location_datas(player: Optional[int], options: Optional[TimespinnerOptio LocationData('Library', 'Library: Terminal 2 (Lachiem)', 1337156, lambda state: state.has('Tablet', player)), LocationData('Library', 'Library: Terminal 1 (Windaria)', 1337157, lambda state: state.has('Tablet', player)), # 1337158 Is lost in time - LocationData('Library', 'Library: Terminal 3 (Emporer Nuvius)', 1337159, lambda state: state.has('Tablet', player)), + LocationData('Library', 'Library: Terminal 3 (Emperor Nuvius)', 1337159, lambda state: state.has('Tablet', player)), LocationData('Library', 'Library: V terminal 1 (War of the Sisters)', 1337160, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)), LocationData('Library', 'Library: V terminal 2 (Lake Desolation Map)', 1337161, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)), LocationData('Library', 'Library: V terminal 3 (Vilete)', 1337162, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)), From ab8caea8be1d8b38a1de8560c66cb66fb0e2873b Mon Sep 17 00:00:00 2001 From: Ziktofel Date: Fri, 27 Sep 2024 00:57:21 +0200 Subject: [PATCH 65/94] SC2: Fix item origins, so including/excluding NCO/BW/EXT items works properly (#3990) --- worlds/sc2/Items.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/worlds/sc2/Items.py b/worlds/sc2/Items.py index 8277d0e7e13d..ee1f34d75be9 100644 --- a/worlds/sc2/Items.py +++ b/worlds/sc2/Items.py @@ -1274,16 +1274,16 @@ def get_full_item_list(): description="Defensive structure. Slows the attack and movement speeds of all nearby Zerg units."), ItemNames.STRUCTURE_ARMOR: ItemData(620 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 9, SC2Race.TERRAN, - description="Increases armor of all Terran structures by 2."), + description="Increases armor of all Terran structures by 2.", origin={"ext"}), ItemNames.HI_SEC_AUTO_TRACKING: ItemData(621 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 10, SC2Race.TERRAN, - description="Increases attack range of all Terran structures by 1."), + description="Increases attack range of all Terran structures by 1.", origin={"ext"}), ItemNames.ADVANCED_OPTICS: ItemData(622 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 11, SC2Race.TERRAN, - description="Increases attack range of all Terran mechanical units by 1."), + description="Increases attack range of all Terran mechanical units by 1.", origin={"ext"}), ItemNames.ROGUE_FORCES: ItemData(623 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 12, SC2Race.TERRAN, - description="Mercenary calldowns are no longer limited by charges."), + description="Mercenary calldowns are no longer limited by charges.", origin={"ext"}), ItemNames.ZEALOT: ItemData(700 + SC2WOL_ITEM_ID_OFFSET, "Unit", 0, SC2Race.PROTOSS, @@ -2369,7 +2369,8 @@ def get_basic_units(world: World, race: SC2Race) -> typing.Set[str]: ItemNames.BATTLECRUISER_PROGRESSIVE_MISSILE_PODS, ItemNames.THOR_PROGRESSIVE_IMMORTALITY_PROTOCOL, ItemNames.PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM, - ItemNames.DIAMONDBACK_PROGRESSIVE_TRI_LITHIUM_POWER_CELL + ItemNames.DIAMONDBACK_PROGRESSIVE_TRI_LITHIUM_POWER_CELL, + ItemNames.PROGRESSIVE_ORBITAL_COMMAND } kerrigan_actives: typing.List[typing.Set[str]] = [ From 5ea55d77b0d2fbe5850c4b08665af64d75f75fa3 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Thu, 26 Sep 2024 18:25:41 -0500 Subject: [PATCH 66/94] The Messenger: add webhost auto connection steps to guide (#3904) * The Messenger: add webhost auto connection steps to guide and fix doc spacing * rever comments * add notes about potential steam popup * medic's feedback Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/messenger/docs/en_The Messenger.md | 18 ++++++++++-------- worlds/messenger/docs/setup_en.md | 17 +++++++++++++++-- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/worlds/messenger/docs/en_The Messenger.md b/worlds/messenger/docs/en_The Messenger.md index 8248a4755d3f..a68ee5ba4c7a 100644 --- a/worlds/messenger/docs/en_The Messenger.md +++ b/worlds/messenger/docs/en_The Messenger.md @@ -39,7 +39,9 @@ You can find items wherever items can be picked up in the original game. This in When you attempt to hint for items in Archipelago you can use either the name for the specific item, or the name of a group of items. Hinting for a group will choose a random item from the group that you do not currently have and hint -for it. The groups you can use for The Messenger are: +for it. + +The groups you can use for The Messenger are: * Notes - This covers the music notes * Keys - An alternative name for the music notes * Crest - The Sun and Moon Crests @@ -50,26 +52,26 @@ for it. The groups you can use for The Messenger are: * The player can return to the Tower of Time HQ at any point by selecting the button from the options menu * This can cause issues if used at specific times. If used in any of these known problematic areas, immediately -quit to title and reload the save. The currently known areas include: + quit to title and reload the save. The currently known areas include: * During Boss fights * After Courage Note collection (Corrupted Future chase) * After reaching ninja village a teleport option is added to the menu to reach it quickly * Toggle Windmill Shuriken button is added to option menu once the item is received * The mod option menu will also have a hint item button, as well as a release and collect button that are all placed -when the player fulfills the necessary conditions. + when the player fulfills the necessary conditions. * After running the game with the mod, a config file (APConfig.toml) will be generated in your game folder that can be -used to modify certain settings such as text size and color. This can also be used to specify a player name that can't -be entered in game. + used to modify certain settings such as text size and color. This can also be used to specify a player name that can't + be entered in game. ## Known issues * Ruxxtin Coffin cutscene will sometimes not play correctly, but will still reward the item * If you receive the Magic Firefly while in Quillshroom Marsh, The De-curse Queen cutscene will not play. You can exit -to Searing Crags and re-enter to get it to play correctly. + to Searing Crags and re-enter to get it to play correctly. * Teleporting back to HQ, then returning to the same level you just left through a Portal can cause Ninja to run left -and enter a different portal than the one entered by the player or lead to other incorrect inputs, causing a soft lock + and enter a different portal than the one entered by the player or lead to other incorrect inputs, causing a soft lock * Text entry menus don't accept controller input * In power seal hunt mode, the chest must be opened by entering the shop from a level. Teleporting to HQ and opening the -chest will not work. + chest will not work. ## What do I do if I have a problem? diff --git a/worlds/messenger/docs/setup_en.md b/worlds/messenger/docs/setup_en.md index c1770e747442..64b706c2643a 100644 --- a/worlds/messenger/docs/setup_en.md +++ b/worlds/messenger/docs/setup_en.md @@ -41,14 +41,27 @@ These steps can also be followed to launch the game and check for mod updates af ## Joining a MultiWorld Game +### Automatic Connection on archipelago.gg + +1. Go to the room page of the MultiWorld you are going to join. +2. Click on your slot name on the left side. +3. Click the "The Messenger" button in the prompt. +4. Follow the remaining prompts. This process will check that you have the mod installed and will also check for updates + before launching The Messenger. If you are using the Steam version of The Messenger you may also get a prompt from + Steam asking if the game should be launched with arguments. These arguments are the URI which the mod uses to + connect. +5. Start a new save. You will already be connected in The Messenger and do not need to go through the menus. + +### Manual Connection + 1. Launch the game 2. Navigate to `Options > Archipelago Options` 3. Enter connection info using the relevant option buttons * **The game is limited to alphanumerical characters, `.`, and `-`.** * This defaults to `archipelago.gg` and does not need to be manually changed if connecting to a game hosted on the -website. + website. * If using a name that cannot be entered in the in game menus, there is a config file (APConfig.toml) in the game -directory. When using this, all connection information must be entered in the file. + directory. When using this, all connection information must be entered in the file. 4. Select the `Connect to Archipelago` button 5. Navigate to save file selection 6. Start a new game From a2d585ba5cffd6e843e5355acb25a9be65c365b5 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Thu, 26 Sep 2024 19:26:06 -0400 Subject: [PATCH 67/94] Stardew Valley: Add Cinder Shard resource pack (#4001) * - Add Cinder Shard resource pack * - Make it ginger island exclusive --- worlds/stardew_valley/data/items.csv | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/stardew_valley/data/items.csv b/worlds/stardew_valley/data/items.csv index 64c14e9f678a..ffcae223e251 100644 --- a/worlds/stardew_valley/data/items.csv +++ b/worlds/stardew_valley/data/items.csv @@ -819,6 +819,7 @@ id,name,classification,groups,mod_name 5289,Prismatic Shard,filler,"RESOURCE_PACK", 5290,Stardrop Tea,filler,"RESOURCE_PACK", 5291,Resource Pack: 2 Artifact Trove,filler,"RESOURCE_PACK", +5292,Resource Pack: 20 Cinder Shard,filler,"GINGER_ISLAND,RESOURCE_PACK", 10001,Luck Level,progression,SKILL_LEVEL_UP,Luck Skill 10002,Magic Level,progression,SKILL_LEVEL_UP,Magic 10003,Socializing Level,progression,SKILL_LEVEL_UP,Socializing Skill From 5c4e81d04600ab4a2162bc19b11762ba055caaaa Mon Sep 17 00:00:00 2001 From: BadMagic100 Date: Thu, 26 Sep 2024 16:27:22 -0700 Subject: [PATCH 68/94] Hollow Knight: Clean outdated slot data code and comments #3988 --- worlds/hk/__init__.py | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 15addefef50a..9ec77e6bf0cd 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -534,26 +534,16 @@ def fill_slot_data(self): for option_name in hollow_knight_options: option = getattr(self.options, option_name) try: + # exclude more complex types - we only care about int, bool, enum for player options; the client + # can get them back to the necessary type. optionvalue = int(option.value) - except TypeError: - pass # C# side is currently typed as dict[str, int], drop what doesn't fit - else: options[option_name] = optionvalue + except TypeError: + pass # 32 bit int slot_data["seed"] = self.random.randint(-2147483647, 2147483646) - # Backwards compatibility for shop cost data (HKAP < 0.1.0) - if not self.options.CostSanity: - for shop, terms in shop_cost_types.items(): - unit = cost_terms[next(iter(terms))].option - if unit == "Geo": - continue - slot_data[f"{unit}_costs"] = { - loc.name: next(iter(loc.costs.values())) - for loc in self.created_multi_locations[shop] - } - # HKAP 0.1.0 and later cost data. location_costs = {} for region in self.multiworld.get_regions(self.player): @@ -566,7 +556,7 @@ def fill_slot_data(self): slot_data["grub_count"] = self.grub_count - slot_data["is_race"] = int(self.settings.disable_spoilers or self.multiworld.is_race) + slot_data["is_race"] = self.settings.disable_spoilers or self.multiworld.is_race return slot_data From 177c0fef52a7ebdde6778195ed5ed6acc1238207 Mon Sep 17 00:00:00 2001 From: soopercool101 Date: Thu, 26 Sep 2024 18:29:26 -0500 Subject: [PATCH 69/94] SM64: Remove outdated information on save bugs from setup guide (#3879) * Remove outdated information from SM64 setup guide Recent build changes have made it so that old saves no longer remove logical gates or prevent Toads from granting stars, remove info highlighting these issues. * Better line break location --- worlds/sm64ex/docs/setup_en.md | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/worlds/sm64ex/docs/setup_en.md b/worlds/sm64ex/docs/setup_en.md index 5983057f7d7a..7456bcb70b62 100644 --- a/worlds/sm64ex/docs/setup_en.md +++ b/worlds/sm64ex/docs/setup_en.md @@ -77,9 +77,6 @@ Should your name or password have spaces, enclose it in quotes: `"YourPassword"` Should the connection fail (for example when using the wrong name or IP/Port combination) the game will inform you of that. Additionally, any time the game is not connected (for example when the connection is unstable) it will attempt to reconnect and display a status text. -**Important:** You must start a new file for every new seed you play. Using `⭐x0` files is **not** sufficient. -Failing to use a new file may make some locations unavailable. However, this can be fixed without losing any progress by exiting and starting a new file. - ### Playing offline To play offline, first generate a seed on the game's options page. @@ -129,18 +126,6 @@ To use this batch file, double-click it. A window will open. Type the five-digi Once you provide those two bits of information, the game will open. - If the game only says `Connecting`, try again. Double-check the port number and slot name; even a single typo will cause your connection to fail. -### Addendum - Deleting old saves - -Loading an old Mario save alongside a new seed is a bad idea, as it can cause locked doors and castle secret stars to already be unlocked / obtained. You should avoid opening a save that says "Stars x 0" as opposed to one that simply says "New". - -You can manually delete these old saves in-game before starting a new game, but that can be tedious. With a small edit to the batch files, you can delete these old saves automatically. Just add the line `del %AppData%\sm64ex\*.bin` to the batch file, above the `start` command. For example, here is `offline.bat` with the additional line: - -`del %AppData%\sm64ex\*.bin` - -`start sm64.us.f3dex2e.exe --sm64ap_file %1` - -This extra line deletes any previous save data before opening the game. Don't worry about lost stars or checks - the AP server (or in the case of offline, the `.save` file) keeps track of your star count, unlocked keys/caps/cannons, and which locations have already been checked, so you won't have to redo them. At worst you'll have to rewatch the door unlocking animations, and catch the rabbit Mips twice for his first star again if you haven't yet collected the second one. - ## Installation Troubleshooting Start the game from the command line to view helpful messages regarding SM64EX. @@ -166,8 +151,9 @@ The Japanese Version should have no problem displaying these. ### Toad does not have an item for me. -This happens when you load an existing file that had already received an item from that toad. +This happens on older builds when you load an existing file that had already received an item from that toad. To resolve this, exit and start from a `NEW` file. The server will automatically restore your progress. +Alternatively, updating your build will prevent this issue in the future. ### What happens if I lose connection? From 05439012dcd45cefd5ad99159024fb92d1213b8b Mon Sep 17 00:00:00 2001 From: palex00 <32203971+palex00@users.noreply.github.com> Date: Fri, 27 Sep 2024 01:30:23 +0200 Subject: [PATCH 70/94] Adjusts Whitespaces in the Plando Doc to be able to be copied directly (#3902) * Update plando_en.md * Also adjusts plando_connections indentation * ughh --- worlds/generic/docs/plando_en.md | 186 +++++++++++++++---------------- 1 file changed, 93 insertions(+), 93 deletions(-) diff --git a/worlds/generic/docs/plando_en.md b/worlds/generic/docs/plando_en.md index 161b1e465b33..1980e81cbcc4 100644 --- a/worlds/generic/docs/plando_en.md +++ b/worlds/generic/docs/plando_en.md @@ -22,9 +22,9 @@ enabled (opt-in). * You can add the necessary plando modules for your settings to the `requires` section of your YAML. Doing so will throw an error if the options that you need to generate properly are not enabled to ensure you will get the results you desire. Only enter in the plando modules that you are using here but it should look like: ```yaml - requires: - version: current.version.number - plando: bosses, items, texts, connections +requires: + version: current.version.number + plando: bosses, items, texts, connections ``` ## Item Plando @@ -74,77 +74,77 @@ A list of all available items and locations can be found in the [website's datap ### Examples ```yaml -plando_items: - # example block 1 - Timespinner - - item: - Empire Orb: 1 - Radiant Orb: 1 - location: Starter Chest 1 - from_pool: true - world: true - percentage: 50 - - # example block 2 - Ocarina of Time - - items: - Kokiri Sword: 1 - Biggoron Sword: 1 - Bow: 1 - Magic Meter: 1 - Progressive Strength Upgrade: 3 - Progressive Hookshot: 2 - locations: - - Deku Tree Slingshot Chest - - Dodongos Cavern Bomb Bag Chest - - Jabu Jabus Belly Boomerang Chest - - Bottom of the Well Lens of Truth Chest - - Forest Temple Bow Chest - - Fire Temple Megaton Hammer Chest - - Water Temple Longshot Chest - - Shadow Temple Hover Boots Chest - - Spirit Temple Silver Gauntlets Chest - world: false - - # example block 3 - Slay the Spire - - items: - Boss Relic: 3 - locations: - - Boss Relic 1 - - Boss Relic 2 - - Boss Relic 3 - - # example block 4 - Factorio - - items: - progressive-electric-energy-distribution: 2 - electric-energy-accumulators: 1 - progressive-turret: 2 - locations: - - military - - gun-turret - - logistic-science-pack - - steel-processing - percentage: 80 - force: true - -# example block 5 - Secret of Evermore - - items: - Levitate: 1 - Revealer: 1 - Energize: 1 - locations: - - Master Sword Pedestal - - Boss Relic 1 - world: true - count: 2 - -# example block 6 - A Link to the Past - - items: - Progressive Sword: 4 - world: - - BobsSlaytheSpire - - BobsRogueLegacy - count: - min: 1 - max: 4 + plando_items: + # example block 1 - Timespinner + - item: + Empire Orb: 1 + Radiant Orb: 1 + location: Starter Chest 1 + from_pool: true + world: true + percentage: 50 + + # example block 2 - Ocarina of Time + - items: + Kokiri Sword: 1 + Biggoron Sword: 1 + Bow: 1 + Magic Meter: 1 + Progressive Strength Upgrade: 3 + Progressive Hookshot: 2 + locations: + - Deku Tree Slingshot Chest + - Dodongos Cavern Bomb Bag Chest + - Jabu Jabus Belly Boomerang Chest + - Bottom of the Well Lens of Truth Chest + - Forest Temple Bow Chest + - Fire Temple Megaton Hammer Chest + - Water Temple Longshot Chest + - Shadow Temple Hover Boots Chest + - Spirit Temple Silver Gauntlets Chest + world: false + + # example block 3 - Slay the Spire + - items: + Boss Relic: 3 + locations: + - Boss Relic 1 + - Boss Relic 2 + - Boss Relic 3 + + # example block 4 - Factorio + - items: + progressive-electric-energy-distribution: 2 + electric-energy-accumulators: 1 + progressive-turret: 2 + locations: + - military + - gun-turret + - logistic-science-pack + - steel-processing + percentage: 80 + force: true + + # example block 5 - Secret of Evermore + - items: + Levitate: 1 + Revealer: 1 + Energize: 1 + locations: + - Master Sword Pedestal + - Boss Relic 1 + world: true + count: 2 + + # example block 6 - A Link to the Past + - items: + Progressive Sword: 4 + world: + - BobsSlaytheSpire + - BobsRogueLegacy + count: + min: 1 + max: 4 ``` 1. This block has a 50% chance to occur, and if it does, it will place either the Empire Orb or Radiant Orb on another player's Starter Chest 1 and removes the chosen item from the item pool. @@ -221,25 +221,25 @@ its [plando guide](/tutorial/A%20Link%20to%20the%20Past/plando/en#connections). ### Examples ```yaml -plando_connections: - # example block 1 - A Link to the Past - - entrance: Cave Shop (Lake Hylia) - exit: Cave 45 - direction: entrance - - entrance: Cave 45 - exit: Cave Shop (Lake Hylia) - direction: entrance - - entrance: Agahnims Tower - exit: Old Man Cave Exit (West) - direction: exit - - # example block 2 - Minecraft - - entrance: Overworld Structure 1 - exit: Nether Fortress - direction: both - - entrance: Overworld Structure 2 - exit: Village - direction: both + plando_connections: + # example block 1 - A Link to the Past + - entrance: Cave Shop (Lake Hylia) + exit: Cave 45 + direction: entrance + - entrance: Cave 45 + exit: Cave Shop (Lake Hylia) + direction: entrance + - entrance: Agahnims Tower + exit: Old Man Cave Exit (West) + direction: exit + + # example block 2 - Minecraft + - entrance: Overworld Structure 1 + exit: Nether Fortress + direction: both + - entrance: Overworld Structure 2 + exit: Village + direction: both ``` 1. These connections are decoupled, so going into the Lake Hylia Cave Shop will take you to the inside of Cave 45, and From 3205e9b3a00763460af9481c78ac7124c19e09e0 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Thu, 26 Sep 2024 23:31:50 +0000 Subject: [PATCH 71/94] DS3: Update setup instructions (#3817) * DS3: Point the DS3 client link to my GitHub It's not clear if/when my PR will land for the upstream fork, or if we'll just start using my fork as the primary source of truth. For now, it's the only one with 3.0.0-compatible releases. * DS3: Document Proton support * DS3: Document another way to get a YAML template * DS3: Don't say that the mod will force offline mode ModEngine2 is *supposed to* do this, but in practice it does not * Code review * Update Linux instructions per user experiences --- worlds/dark_souls_3/docs/setup_en.md | 31 ++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/worlds/dark_souls_3/docs/setup_en.md b/worlds/dark_souls_3/docs/setup_en.md index ed90289a8baf..9755cce1c6a8 100644 --- a/worlds/dark_souls_3/docs/setup_en.md +++ b/worlds/dark_souls_3/docs/setup_en.md @@ -3,7 +3,7 @@ ## Required Software - [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/) -- [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases) +- [Dark Souls III AP Client](https://github.com/nex3/Dark-Souls-III-Archipelago-client/releases/latest) ## Optional Software @@ -11,8 +11,9 @@ ## Setting Up -First, download the client from the link above. It doesn't need to go into any particular directory; -it'll automatically locate _Dark Souls III_ in your Steam installation folder. +First, download the client from the link above (`DS3.Archipelago.*.zip`). It doesn't need to go +into any particular directory; it'll automatically locate _Dark Souls III_ in your Steam +installation folder. Version 3.0.0 of the randomizer _only_ supports the latest version of _Dark Souls III_, 1.15.2. This is the latest version, so you don't need to do any downpatching! However, if you've already @@ -35,8 +36,9 @@ randomized item and (optionally) enemy locations. You only need to do this once To run _Dark Souls III_ in Archipelago mode: -1. Start Steam. **Do not run in offline mode.** The mod will make sure you don't connect to the - DS3 servers, and running Steam in offline mode will make certain scripted invaders fail to spawn. +1. Start Steam. **Do not run in offline mode.** Running Steam in offline mode will make certain + scripted invaders fail to spawn. Instead, change the game itself to offline mode on the menu + screen. 2. Run `launchmod_darksouls3.bat`. This will start _Dark Souls III_ as well as a command prompt that you can use to interact with the Archipelago server. @@ -52,4 +54,21 @@ To run _Dark Souls III_ in Archipelago mode: ### Where do I get a config file? The [Player Options](/games/Dark%20Souls%20III/player-options) page on the website allows you to -configure your personal options and export them into a config file. +configure your personal options and export them into a config file. The [AP client archive] also +includes an options template. + +[AP client archive]: https://github.com/nex3/Dark-Souls-III-Archipelago-client/releases/latest + +### Does this work with Proton? + +The *Dark Souls III* Archipelago randomizer supports running on Linux under Proton. There are a few +things to keep in mind: + +* Because `DS3Randomizer.exe` relies on the .NET runtime, you'll need to install + the [.NET Runtime] under **plain [WINE]**, then run `DS3Randomizer.exe` under + plain WINE as well. It won't work as a Proton app! + +* To run the game itself, just run `launchmod_darksouls3.bat` under Proton. + +[.NET Runtime]: https://dotnet.microsoft.com/en-us/download/dotnet/8.0 +[WINE]: https://www.winehq.org/ From 7337309426a247ff824b702389df6bfc87e381a6 Mon Sep 17 00:00:00 2001 From: qwint Date: Thu, 26 Sep 2024 19:34:54 -0400 Subject: [PATCH 72/94] CommonClient: add more docstrings and comments #3821 --- CommonClient.py | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 6bdd8fc819da..1aedd518b4f8 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -45,10 +45,21 @@ def get_ssl_context(): class ClientCommandProcessor(CommandProcessor): + """ + The Command Processor will parse every method of the class that starts with "_cmd_" as a command to be called + when parsing user input, i.e. _cmd_exit will be called when the user sends the command "/exit". + + The decorator @mark_raw can be imported from MultiServer and tells the parser to only split on the first + space after the command i.e. "/exit one two three" will be passed in as method("one two three") with mark_raw + and method("one", "two", "three") without. + + In addition all docstrings for command methods will be displayed to the user on launch and when using "/help" + """ def __init__(self, ctx: CommonContext): self.ctx = ctx def output(self, text: str): + """Helper function to abstract logging to the CommonClient UI""" logger.info(text) def _cmd_exit(self) -> bool: @@ -164,13 +175,14 @@ def _cmd_ready(self): async_start(self.ctx.send_msgs([{"cmd": "StatusUpdate", "status": state}]), name="send StatusUpdate") def default(self, raw: str): + """The default message parser to be used when parsing any messages that do not match a command""" raw = self.ctx.on_user_say(raw) if raw: async_start(self.ctx.send_msgs([{"cmd": "Say", "text": raw}]), name="send Say") class CommonContext: - # Should be adjusted as needed in subclasses + # The following attributes are used to Connect and should be adjusted as needed in subclasses tags: typing.Set[str] = {"AP"} game: typing.Optional[str] = None items_handling: typing.Optional[int] = None @@ -429,7 +441,10 @@ async def get_username(self): self.auth = await self.console_input() async def send_connect(self, **kwargs: typing.Any) -> None: - """ send `Connect` packet to log in to server """ + """ + Send a `Connect` packet to log in to the server, + additional keyword args can override any value in the connection packet + """ payload = { 'cmd': 'Connect', 'password': self.password, 'name': self.auth, 'version': Utils.version_tuple, @@ -459,6 +474,7 @@ def cancel_autoreconnect(self) -> bool: return False def slot_concerns_self(self, slot) -> bool: + """Helper function to abstract player groups, should be used instead of checking slot == self.slot directly.""" if slot == self.slot: return True if slot in self.slot_info: @@ -466,6 +482,7 @@ def slot_concerns_self(self, slot) -> bool: return False def is_echoed_chat(self, print_json_packet: dict) -> bool: + """Helper function for filtering out messages sent by self.""" return print_json_packet.get("type", "") == "Chat" \ and print_json_packet.get("team", None) == self.team \ and print_json_packet.get("slot", None) == self.slot @@ -497,13 +514,14 @@ def on_user_say(self, text: str) -> typing.Optional[str]: """Gets called before sending a Say to the server from the user. Returned text is sent, or sending is aborted if None is returned.""" return text - + def on_ui_command(self, text: str) -> None: """Gets called by kivy when the user executes a command starting with `/` or `!`. The command processor is still called; this is just intended for command echoing.""" self.ui.print_json([{"text": text, "type": "color", "color": "orange"}]) def update_permissions(self, permissions: typing.Dict[str, int]): + """Internal method to parse and save server permissions from RoomInfo""" for permission_name, permission_flag in permissions.items(): try: flag = Permission(permission_flag) @@ -613,6 +631,7 @@ def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None: logger.info(f"DeathLink: Received from {data['source']}") async def send_death(self, death_text: str = ""): + """Helper function to send a deathlink using death_text as the unique death cause string.""" if self.server and self.server.socket: logger.info("DeathLink: Sending death to your friends...") self.last_death_link = time.time() @@ -626,6 +645,7 @@ async def send_death(self, death_text: str = ""): }]) async def update_death_link(self, death_link: bool): + """Helper function to set Death Link connection tag on/off and update the connection if already connected.""" old_tags = self.tags.copy() if death_link: self.tags.add("DeathLink") @@ -635,7 +655,7 @@ async def update_death_link(self, death_link: bool): await self.send_msgs([{"cmd": "ConnectUpdate", "tags": self.tags}]) def gui_error(self, title: str, text: typing.Union[Exception, str]) -> typing.Optional["kvui.MessageBox"]: - """Displays an error messagebox""" + """Displays an error messagebox in the loaded Kivy UI. Override if using a different UI framework""" if not self.ui: return None title = title or "Error" @@ -987,6 +1007,7 @@ async def console_loop(ctx: CommonContext): def get_base_parser(description: typing.Optional[str] = None): + """Base argument parser to be reused for components subclassing off of CommonClient""" import argparse parser = argparse.ArgumentParser(description=description) parser.add_argument('--connect', default=None, help='Address of the multiworld host.') @@ -1037,6 +1058,7 @@ async def main(args): parser.add_argument("url", nargs="?", help="Archipelago connection url") args = parser.parse_args(args) + # handle if text client is launched using the "archipelago://name:pass@host:port" url from webhost if args.url: url = urllib.parse.urlparse(args.url) if url.scheme == "archipelago": @@ -1048,6 +1070,7 @@ async def main(args): else: parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281") + # use colorama to display colored text highlighting on windows colorama.init() asyncio.run(main(args)) From de0c4984708cdfa7bea1f17d36a7ca15d34243d5 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 28 Sep 2024 22:37:42 +0200 Subject: [PATCH 73/94] Core: update World method comment (#3866) --- worlds/AutoWorld.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 19ec9a14a8c7..f7dae2b92750 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -342,7 +342,7 @@ def __getattr__(self, item: str) -> Any: # overridable methods that get called by Main.py, sorted by execution order # can also be implemented as a classmethod and called "stage_", - # in that case the MultiWorld object is passed as an argument, and it gets called once for the entire multiworld. + # in that case the MultiWorld object is passed as the first argument, and it gets called once for the entire multiworld. # An example of this can be found in alttp as stage_pre_fill @classmethod From 8193fa12b205f21bcfb1083961a6131962797dda Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Sat, 28 Sep 2024 13:49:11 -0700 Subject: [PATCH 74/94] BizHawkClient: Fix typing mistake (#3938) --- worlds/_bizhawk/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/worlds/_bizhawk/__init__.py b/worlds/_bizhawk/__init__.py index 74f2954b984b..3627f385c2d3 100644 --- a/worlds/_bizhawk/__init__.py +++ b/worlds/_bizhawk/__init__.py @@ -223,8 +223,8 @@ async def set_message_interval(ctx: BizHawkContext, value: float) -> None: raise SyncError(f"Expected response of type SET_MESSAGE_INTERVAL_RESPONSE but got {res['type']}") -async def guarded_read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[int, int, str]], - guard_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> typing.Optional[typing.List[bytes]]: +async def guarded_read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tuple[int, int, str]], + guard_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> typing.Optional[typing.List[bytes]]: """Reads an array of bytes at 1 or more addresses if and only if every byte in guard_list matches its expected value. @@ -266,7 +266,7 @@ async def guarded_read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[ return ret -async def read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[int, int, str]]) -> typing.List[bytes]: +async def read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tuple[int, int, str]]) -> typing.List[bytes]: """Reads data at 1 or more addresses. Items in `read_list` should be organized `(address, size, domain)` where @@ -278,8 +278,8 @@ async def read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[int, int return await guarded_read(ctx, read_list, []) -async def guarded_write(ctx: BizHawkContext, write_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]], - guard_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> bool: +async def guarded_write(ctx: BizHawkContext, write_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]], + guard_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> bool: """Writes data to 1 or more addresses if and only if every byte in guard_list matches its expected value. Items in `write_list` should be organized `(address, value, domain)` where @@ -316,7 +316,7 @@ async def guarded_write(ctx: BizHawkContext, write_list: typing.List[typing.Tupl return True -async def write(ctx: BizHawkContext, write_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> None: +async def write(ctx: BizHawkContext, write_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> None: """Writes data to 1 or more addresses. Items in write_list should be organized `(address, value, domain)` where From 67f6b458d7292c44a2a3870523d28868fbbb056c Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Tue, 1 Oct 2024 14:08:13 -0500 Subject: [PATCH 75/94] Core: add race mode to multidata and datastore (#4017) * add race mode to multidata and datastore * have commonclient check race mode on connect and add it to the tooltip ui --- CommonClient.py | 2 ++ Main.py | 1 + MultiServer.py | 2 ++ kvui.py | 2 ++ 4 files changed, 7 insertions(+) diff --git a/CommonClient.py b/CommonClient.py index 1aedd518b4f8..8325227d5e5c 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -325,6 +325,7 @@ def __init__(self, server_address: typing.Optional[str] = None, password: typing "collect": "disabled", "remaining": "disabled", } + self.race_mode: int = 0 # own state self.finished_game = False @@ -454,6 +455,7 @@ async def send_connect(self, **kwargs: typing.Any) -> None: if kwargs: payload.update(kwargs) await self.send_msgs([payload]) + await self.send_msgs([{"cmd": "Get", "keys": ["race_mode"]}]) async def console_input(self) -> str: if self.ui: diff --git a/Main.py b/Main.py index 5a0f5c98bcc4..4008ca5e9017 100644 --- a/Main.py +++ b/Main.py @@ -338,6 +338,7 @@ def precollect_hint(location): "seed_name": multiworld.seed_name, "spheres": spheres, "datapackage": data_package, + "race_mode": int(multiworld.is_race), } AutoWorld.call_all(multiworld, "modify_multidata", multidata) diff --git a/MultiServer.py b/MultiServer.py index e0b137fd68ce..91f4eec61574 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -427,6 +427,8 @@ def _load(self, decoded_obj: dict, game_data_packages: typing.Dict[str, typing.A use_embedded_server_options: bool): self.read_data = {} + # there might be a better place to put this. + self.stored_data["race_mode"] = decoded_obj.get("race_mode", 0) mdata_ver = decoded_obj["minimum_versions"]["server"] if mdata_ver > version_tuple: raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver}," diff --git a/kvui.py b/kvui.py index 536dce12208e..d58af3ed0e78 100644 --- a/kvui.py +++ b/kvui.py @@ -243,6 +243,8 @@ def get_text(self): f"\nYou currently have {ctx.hint_points} points." elif ctx.hint_cost == 0: text += "\n!hint is free to use." + if ctx.stored_data and "race_mode" in ctx.stored_data: + text += "\nRace mode is enabled." if ctx.stored_data["race_mode"] else "\nRace mode is disabled." else: text += f"\nYou are not authenticated yet." From dc1da4e88b4268ba12f6fd8b4a1ce362b4e9eebf Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Tue, 1 Oct 2024 12:08:43 -0700 Subject: [PATCH 76/94] Pokemon Emerald: Another wonder trade fix (#4014) * Pokemon Emerald: Another guarded write on wonder trades * Pokemon Emerald: Reorder sending wonder trade and erasing data In case the guarded write fails --- worlds/pokemon_emerald/client.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/worlds/pokemon_emerald/client.py b/worlds/pokemon_emerald/client.py index c91b7d3e26b0..4405b34074e0 100644 --- a/worlds/pokemon_emerald/client.py +++ b/worlds/pokemon_emerald/client.py @@ -545,11 +545,12 @@ async def handle_wonder_trade(self, ctx: "BizHawkClientContext", guards: Dict[st if trade_is_sent == 0 and wonder_trade_pokemon_data[19] == 2: # Game has wonder trade data to send. Send it to data storage, remove it from the game's memory, # and mark that the game is waiting on receiving a trade - Utils.async_start(self.wonder_trade_send(ctx, pokemon_data_to_json(wonder_trade_pokemon_data))) - await bizhawk.write(ctx.bizhawk_ctx, [ + success = await bizhawk.guarded_write(ctx.bizhawk_ctx, [ (sb1_address + 0x377C, bytes(0x50), "System Bus"), (sb1_address + 0x37CC, [1], "System Bus"), - ]) + ], [guards["SAVE BLOCK 1"]]) + if success: + Utils.async_start(self.wonder_trade_send(ctx, pokemon_data_to_json(wonder_trade_pokemon_data))) elif trade_is_sent != 0 and wonder_trade_pokemon_data[19] != 2: # Game is waiting on receiving a trade. if self.queued_received_trade is not None: From 23469fa5c3113f83d21d608c25535aa1da95370b Mon Sep 17 00:00:00 2001 From: Alex Nordstrom Date: Tue, 1 Oct 2024 15:09:23 -0400 Subject: [PATCH 77/94] LADX: ghost fills ammo to initial max (#4005) * ghost fills ammo to max * Revert "ghost fills ammo to max" This reverts commit 68804fef1403197f2192e4c7d02f8793ac1c7ca0. * fill to first max --- worlds/ladx/LADXR/patches/owl.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/worlds/ladx/LADXR/patches/owl.py b/worlds/ladx/LADXR/patches/owl.py index 47e575191a31..20b8221604c6 100644 --- a/worlds/ladx/LADXR/patches/owl.py +++ b/worlds/ladx/LADXR/patches/owl.py @@ -81,23 +81,23 @@ def removeOwlEvents(rom): ; Give powder ld a, [$DB4C] - cp $10 + cp $20 jr nc, doNotGivePowder - ld a, $10 + ld a, $20 ld [$DB4C], a doNotGivePowder: ld a, [$DB4D] - cp $10 + cp $30 jr nc, doNotGiveBombs - ld a, $10 + ld a, $30 ld [$DB4D], a doNotGiveBombs: ld a, [$DB45] - cp $10 + cp $30 jr nc, doNotGiveArrows - ld a, $10 + ld a, $30 ld [$DB45], a doNotGiveArrows: From 5a853dfccdfe138e00dff7dc55890980e71b7e05 Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Tue, 1 Oct 2024 20:30:45 +0100 Subject: [PATCH 78/94] Tests: Fix indentation in TestTwoPlayerMulti (#4010) The "filling multiworld" subtest was at the wrong indentation, so was only running for the last world_type. "games" has additionally been added to the subtest to help better identify failures. Now that the subtest is actually being run for each world type, this adds about 20 seconds to the duration of the test on my machine. --- test/multiworld/test_multiworlds.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/multiworld/test_multiworlds.py b/test/multiworld/test_multiworlds.py index 8415ac4c8429..3c1d0e4544eb 100644 --- a/test/multiworld/test_multiworlds.py +++ b/test/multiworld/test_multiworlds.py @@ -71,7 +71,7 @@ def test_two_player_single_game_fills(self) -> None: for world in self.multiworld.worlds.values(): world.options.accessibility.value = Accessibility.option_full self.assertSteps(gen_steps) - with self.subTest("filling multiworld", seed=self.multiworld.seed): - distribute_items_restrictive(self.multiworld) - call_all(self.multiworld, "post_fill") - self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game") + with self.subTest("filling multiworld", games=world_type.game, seed=self.multiworld.seed): + distribute_items_restrictive(self.multiworld) + call_all(self.multiworld, "post_fill") + self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game") From f06f95d03dd88f4a8aa35a88d2b75eedc7526a24 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Tue, 1 Oct 2024 16:55:34 -0500 Subject: [PATCH 79/94] Core: move race_mode to read_data instead of stored_data (#4020) * move race_mode to read_data * add race_mode to docs --- CommonClient.py | 3 +-- MultiServer.py | 2 +- docs/network protocol.md | 1 + kvui.py | 5 +++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 8325227d5e5c..296c10ed4b4e 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -325,7 +325,6 @@ def __init__(self, server_address: typing.Optional[str] = None, password: typing "collect": "disabled", "remaining": "disabled", } - self.race_mode: int = 0 # own state self.finished_game = False @@ -455,7 +454,7 @@ async def send_connect(self, **kwargs: typing.Any) -> None: if kwargs: payload.update(kwargs) await self.send_msgs([payload]) - await self.send_msgs([{"cmd": "Get", "keys": ["race_mode"]}]) + await self.send_msgs([{"cmd": "Get", "keys": ["_read_race_mode"]}]) async def console_input(self) -> str: if self.ui: diff --git a/MultiServer.py b/MultiServer.py index 91f4eec61574..c3e377e9a29d 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -428,7 +428,7 @@ def _load(self, decoded_obj: dict, game_data_packages: typing.Dict[str, typing.A self.read_data = {} # there might be a better place to put this. - self.stored_data["race_mode"] = decoded_obj.get("race_mode", 0) + self.read_data["race_mode"] = lambda: decoded_obj.get("race_mode", 0) mdata_ver = decoded_obj["minimum_versions"]["server"] if mdata_ver > version_tuple: raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver}," diff --git a/docs/network protocol.md b/docs/network protocol.md index f8080fecc879..1c4579c4066f 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -395,6 +395,7 @@ Some special keys exist with specific return data, all of them have the prefix ` | item_name_groups_{game_name} | dict\[str, list\[str\]\] | item_name_groups belonging to the requested game. | | location_name_groups_{game_name} | dict\[str, list\[str\]\] | location_name_groups belonging to the requested game. | | client_status_{team}_{slot} | [ClientStatus](#ClientStatus) | The current game status of the requested player. | +| race_mode | int | 0 if race mode is disabled, and 1 if it's enabled. | ### Set Used to write data to the server's data storage, that data can then be shared across worlds or just saved for later. Values for keys in the data storage can be retrieved with a [Get](#Get) package, or monitored with a [SetNotify](#SetNotify) package. diff --git a/kvui.py b/kvui.py index d58af3ed0e78..74d8ad06734a 100644 --- a/kvui.py +++ b/kvui.py @@ -243,8 +243,9 @@ def get_text(self): f"\nYou currently have {ctx.hint_points} points." elif ctx.hint_cost == 0: text += "\n!hint is free to use." - if ctx.stored_data and "race_mode" in ctx.stored_data: - text += "\nRace mode is enabled." if ctx.stored_data["race_mode"] else "\nRace mode is disabled." + 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." else: text += f"\nYou are not authenticated yet." From 0ec9039ca6955129ba0fd15b0ba5a48cc71108da Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Wed, 2 Oct 2024 00:02:17 +0200 Subject: [PATCH 80/94] The Witness: Small code refactor (cast_not_none) (#3798) * cast not none * ruff * Missed a spot --- worlds/witness/__init__.py | 6 +++--- worlds/witness/data/utils.py | 7 ++++++- worlds/witness/player_items.py | 8 ++++---- worlds/witness/test/__init__.py | 5 +++-- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index b4b38c883e7d..c9848f2ffe47 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -14,7 +14,7 @@ from .data import static_locations as static_witness_locations from .data import static_logic as static_witness_logic from .data.item_definition_classes import DoorItemDefinition, ItemData -from .data.utils import get_audio_logs +from .data.utils import cast_not_none, get_audio_logs 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 @@ -55,7 +55,7 @@ class WitnessWorld(World): item_name_to_id = { # ITEM_DATA doesn't have any event items in it - name: cast(int, data.ap_code) for name, data in static_witness_items.ITEM_DATA.items() + name: cast_not_none(data.ap_code) for name, data in static_witness_items.ITEM_DATA.items() } location_name_to_id = static_witness_locations.ALL_LOCATIONS_TO_ID item_name_groups = static_witness_items.ITEM_GROUPS @@ -336,7 +336,7 @@ def fill_slot_data(self) -> Dict[str, Any]: for item_name, hint in laser_hints.items(): item_def = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name]) self.laser_ids_to_hints[int(item_def.panel_id_hexes[0], 16)] = make_compact_hint_data(hint, self.player) - already_hinted_locations.add(cast(Location, hint.location)) + already_hinted_locations.add(cast_not_none(hint.location)) # Audio Log Hints diff --git a/worlds/witness/data/utils.py b/worlds/witness/data/utils.py index 84eca5afc43f..737daff70fae 100644 --- a/worlds/witness/data/utils.py +++ b/worlds/witness/data/utils.py @@ -1,7 +1,7 @@ from math import floor from pkgutil import get_data from random import Random -from typing import Any, Collection, Dict, FrozenSet, Iterable, List, Set, Tuple, TypeVar +from typing import Any, Collection, Dict, FrozenSet, Iterable, List, Optional, Set, Tuple, TypeVar T = TypeVar("T") @@ -13,6 +13,11 @@ WitnessRule = FrozenSet[FrozenSet[str]] +def cast_not_none(value: Optional[T]) -> T: + assert value is not None + return value + + def weighted_sample(world_random: Random, population: List[T], weights: List[float], k: int) -> List[T]: positions = range(len(population)) indices: List[int] = [] diff --git a/worlds/witness/player_items.py b/worlds/witness/player_items.py index 72dfc2b7ee54..4c98cb78495e 100644 --- a/worlds/witness/player_items.py +++ b/worlds/witness/player_items.py @@ -15,7 +15,7 @@ ProgressiveItemDefinition, WeightedItemDefinition, ) -from .data.utils import build_weighted_int_list +from .data.utils import build_weighted_int_list, cast_not_none from .locations import WitnessPlayerLocations from .player_logic import WitnessPlayerLogic @@ -200,7 +200,7 @@ def get_symbol_ids_not_in_pool(self) -> List[int]: """ return [ # data.ap_code is guaranteed for a symbol definition - cast(int, data.ap_code) for name, data in static_witness_items.ITEM_DATA.items() + cast_not_none(data.ap_code) for name, data in static_witness_items.ITEM_DATA.items() if name not in self.item_data.keys() and data.definition.category is ItemCategory.SYMBOL ] @@ -211,8 +211,8 @@ def get_progressive_item_ids_in_pool(self) -> Dict[int, List[int]]: 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(int, item.ap_code)] = [cast(int, static_witness_items.ITEM_DATA[child_item].ap_code) - for child_item in item.definition.child_item_names] + 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 diff --git a/worlds/witness/test/__init__.py b/worlds/witness/test/__init__.py index 4453609ddcdb..c3b427851af0 100644 --- a/worlds/witness/test/__init__.py +++ b/worlds/witness/test/__init__.py @@ -1,4 +1,4 @@ -from typing import Any, ClassVar, Dict, Iterable, List, Mapping, Union, cast +from typing import Any, ClassVar, Dict, Iterable, List, Mapping, Union from BaseClasses import CollectionState, Entrance, Item, Location, Region @@ -7,6 +7,7 @@ from test.multiworld.test_multiworlds import MultiworldTestBase from .. import WitnessWorld +from ..data.utils import cast_not_none class WitnessTestBase(WorldTestBase): @@ -32,7 +33,7 @@ def assert_dependency_on_event_item(self, spot: Union[Location, Region, Entrance event_items = [item for item in self.multiworld.get_items() if item.name == item_name] self.assertTrue(event_items, f"Event item {item_name} does not exist.") - event_locations = [cast(Location, event_item.location) for event_item in event_items] + event_locations = [cast_not_none(event_item.location) for event_item in event_items] # Checking for an access dependency on an event item requires a bit of extra work, # as state.remove forces a sweep, which will pick up the event item again right after we tried to remove it. From 05a67386c61c4be699b8a54067b84a72ea004126 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 2 Oct 2024 03:09:43 +0200 Subject: [PATCH 81/94] Core: use shlex splitting instead of whitespace splitting for client and server commands (#4011) --- MultiServer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MultiServer.py b/MultiServer.py index c3e377e9a29d..0fe950b5e4f3 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -15,6 +15,7 @@ import operator import pickle import random +import shlex import threading import time import typing @@ -1152,7 +1153,7 @@ def __call__(self, raw: str) -> typing.Optional[bool]: if not raw: return try: - command = raw.split() + command = shlex.split(raw, comments=False) basecommand = command[0] if basecommand[0] == self.marker: method = self.commands.get(basecommand[1:].lower(), None) From 216e0603e1bcc4fea9985338212b333e38f1d468 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Fri, 4 Oct 2024 14:27:23 -0500 Subject: [PATCH 82/94] KDL3: Fix webhost not giving a patch #4023 --- worlds/kdl3/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/kdl3/__init__.py b/worlds/kdl3/__init__.py index f01c82dd16a3..1b5acbe97a3c 100644 --- a/worlds/kdl3/__init__.py +++ b/worlds/kdl3/__init__.py @@ -325,7 +325,7 @@ def generate_basic(self) -> None: def generate_output(self, output_directory: str) -> None: try: - patch = KDL3ProcedurePatch() + patch = KDL3ProcedurePatch(player=self.player, player_name=self.player_name) patch_rom(self, patch) self.rom_name = patch.name From e5a0ef799f513d3d6231140e5a4ba561b0bfdcd8 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Fri, 4 Oct 2024 12:28:43 -0700 Subject: [PATCH 83/94] Pokemon Emerald: Update changelog (#4003) --- worlds/pokemon_emerald/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/worlds/pokemon_emerald/CHANGELOG.md b/worlds/pokemon_emerald/CHANGELOG.md index 6a1844e79fde..2d7db0dad4d5 100644 --- a/worlds/pokemon_emerald/CHANGELOG.md +++ b/worlds/pokemon_emerald/CHANGELOG.md @@ -8,6 +8,9 @@ ### Fixes +- Fixed a rare issue where receiving a wonder trade could partially corrupt the save data, preventing the player from +receiving new items. +- Fixed the client spamming the "goal complete" status update to the server instead of sending it once. - Fixed a logic issue where the "Mauville City - Coin Case from Lady in House" location only required a Harbor Mail if the player randomized NPC gifts. - The Dig tutor has its compatibility percentage raised to 50% if the player's TM/tutor compatibility is set lower. From 97f2c25924b0b75f9dcb74e9dc28e5546a22b3e9 Mon Sep 17 00:00:00 2001 From: palex00 <32203971+palex00@users.noreply.github.com> Date: Sat, 5 Oct 2024 02:13:04 +0200 Subject: [PATCH 84/94] [KH2] Adds more options to slot data #4031 --- worlds/kh2/__init__.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/worlds/kh2/__init__.py b/worlds/kh2/__init__.py index faf0bed88567..2809460aed6a 100644 --- a/worlds/kh2/__init__.py +++ b/worlds/kh2/__init__.py @@ -101,7 +101,18 @@ def fill_slot_data(self) -> dict: if ability in self.goofy_ability_dict and self.goofy_ability_dict[ability] >= 1: self.goofy_ability_dict[ability] -= 1 - slot_data = self.options.as_dict("Goal", "FinalXemnas", "LuckyEmblemsRequired", "BountyRequired") + slot_data = self.options.as_dict( + "Goal", + "FinalXemnas", + "LuckyEmblemsRequired", + "BountyRequired", + "FightLogic", + "FinalFormLogic", + "AutoFormLogic", + "LevelDepth", + "DonaldGoofyStatsanity", + "CorSkipToggle" + ) slot_data.update({ "hitlist": [], # remove this after next update "PoptrackerVersionCheck": 4.3, From 6287bc27a68ace32679fe8a41a580df7180cd9d8 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sat, 5 Oct 2024 18:14:22 +0200 Subject: [PATCH 85/94] WebHost: Fix too-many-players error not showing (#4033) * WebHost: fix 'too many players' error not showing * WebHost, Tests: add basic tests for generate endpoint * WebHost: hopefully make CodeQL happy with MAX_ROLL redirect --- WebHostLib/generate.py | 1 + test/webhost/test_generate.py | 73 +++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 test/webhost/test_generate.py diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index dbe7dd958910..b19f3d483515 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -81,6 +81,7 @@ def start_generation(options: Dict[str, Union[dict, str]], meta: Dict[str, Any]) elif len(gen_options) > app.config["MAX_ROLL"]: flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players. " f"If you have a larger group, please generate it yourself and upload it.") + return redirect(url_for(request.endpoint, **(request.view_args or {}))) elif len(gen_options) >= app.config["JOB_THRESHOLD"]: gen = Generation( options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}), diff --git a/test/webhost/test_generate.py b/test/webhost/test_generate.py new file mode 100644 index 000000000000..5440f6e02bec --- /dev/null +++ b/test/webhost/test_generate.py @@ -0,0 +1,73 @@ +import zipfile +from io import BytesIO + +from flask import url_for + +from . import TestBase + + +class TestGenerate(TestBase): + def test_valid_yaml(self) -> None: + """ + Verify that posting a valid yaml will start generating a game. + """ + with self.app.app_context(), self.app.test_request_context(): + yaml_data = """ + name: Player1 + game: Archipelago + Archipelago: {} + """ + response = self.client.post(url_for("generate"), + data={"file": (BytesIO(yaml_data.encode("utf-8")), "test.yaml")}, + follow_redirects=True) + self.assertEqual(response.status_code, 200) + self.assertTrue("/seed/" in response.request.path or + "/wait/" in response.request.path, + f"Response did not properly redirect ({response.request.path})") + + def test_empty_zip(self) -> None: + """ + Verify that posting an empty zip will give an error. + """ + with self.app.app_context(), self.app.test_request_context(): + zip_data = BytesIO() + zipfile.ZipFile(zip_data, "w").close() + zip_data.seek(0) + self.assertGreater(len(zip_data.read()), 0) + zip_data.seek(0) + response = self.client.post(url_for("generate"), + data={"file": (zip_data, "test.zip")}, + follow_redirects=True) + self.assertIn("user-message", response.text, + "Request did not call flash()") + self.assertIn("not find any valid files", response.text, + "Response shows unexpected error") + self.assertIn("generate-game-form", response.text, + "Response did not get user back to the form") + + def test_too_many_players(self) -> None: + """ + Verify that posting too many players will give an error. + """ + max_roll = self.app.config["MAX_ROLL"] + # validate that max roll has a sensible value, otherwise we probably changed how it works + self.assertIsInstance(max_roll, int) + self.assertGreater(max_roll, 1) + self.assertLess(max_roll, 100) + # create a yaml with max_roll+1 players and watch it fail + with self.app.app_context(), self.app.test_request_context(): + yaml_data = "---\n".join([ + f"name: Player{n}\n" + "game: Archipelago\n" + "Archipelago: {}\n" + for n in range(1, max_roll + 2) + ]) + response = self.client.post(url_for("generate"), + data={"file": (BytesIO(yaml_data.encode("utf-8")), "test.yaml")}, + follow_redirects=True) + self.assertIn("user-message", response.text, + "Request did not call flash()") + self.assertIn("limited to", response.text, + "Response shows unexpected error") + self.assertIn("generate-game-form", response.text, + "Response did not get user back to the form") From 2751ccdaabc7e55fdfaa68cbb1d6ea9bb1d666ce Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Thu, 10 Oct 2024 21:02:31 -0400 Subject: [PATCH 86/94] DS3: Make your own region cache (#4040) * Make your own region cache * Using a string --- worlds/dark_souls_3/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index b51668539be2..1aec6945eb8b 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -89,6 +89,7 @@ def __init__(self, multiworld: MultiWorld, player: int): self.all_excluded_locations = set() def generate_early(self) -> None: + self.created_regions = set() self.all_excluded_locations.update(self.options.exclude_locations.value) # Inform Universal Tracker where Yhorm is being randomized to. @@ -294,6 +295,7 @@ def create_region(self, region_name, location_table) -> Region: new_region.locations.append(new_location) self.multiworld.regions.append(new_region) + self.created_regions.add(region_name) return new_region def create_items(self) -> None: @@ -1305,7 +1307,7 @@ def _add_location_rule(self, location: Union[str, List[str]], rule: Union[Collec def _add_entrance_rule(self, region: str, rule: Union[CollectionRule, str]) -> None: """Sets a rule for the entrance to the given region.""" assert region in location_tables - if not any(region == reg for reg in self.multiworld.regions.region_cache[self.player]): return + if region not in self.created_regions: return if isinstance(rule, str): if " -> " not in rule: assert item_dictionary[rule].classification == ItemClassification.progression From f495bf726101e5125e73a1aabb23ee9ab6518d04 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Thu, 10 Oct 2024 20:05:21 -0500 Subject: [PATCH 87/94] The Messenger: fix missing money wrench rule (#4041) * The Messenger: fix missing money wrench rule * add a unit test for money wrench --- worlds/messenger/rules.py | 2 ++ worlds/messenger/test/test_shop.py | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/worlds/messenger/rules.py b/worlds/messenger/rules.py index 85b73dec4147..c354ad70aba6 100644 --- a/worlds/messenger/rules.py +++ b/worlds/messenger/rules.py @@ -220,6 +220,8 @@ def __init__(self, world: "MessengerWorld") -> None: } self.location_rules = { + # hq + "Money Wrench": self.can_shop, # ninja village "Ninja Village Seal - Tree House": self.has_dart, diff --git a/worlds/messenger/test/test_shop.py b/worlds/messenger/test/test_shop.py index 971ff1763b47..ce6fd19e33c8 100644 --- a/worlds/messenger/test/test_shop.py +++ b/worlds/messenger/test/test_shop.py @@ -1,5 +1,6 @@ from typing import Dict +from BaseClasses import CollectionState from . import MessengerTestBase from ..shop import SHOP_ITEMS, FIGURINES @@ -89,3 +90,15 @@ def test_costs(self) -> None: self.assertTrue(loc in FIGURINES) self.assertEqual(len(figures), len(FIGURINES)) + + max_cost_state = CollectionState(self.multiworld) + self.assertFalse(self.world.get_location("Money Wrench").can_reach(max_cost_state)) + prog_shards = [] + for item in self.multiworld.itempool: + if "Time Shard " in item.name: + value = int(item.name.strip("Time Shard ()")) + if value >= 100: + prog_shards.append(item) + for shard in prog_shards: + max_cost_state.collect(shard, True) + self.assertTrue(self.world.get_location("Money Wrench").can_reach(max_cost_state)) From ef4d1e77e3eec4ce0c7a4fe381b18487aedc8b40 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 11 Oct 2024 23:24:42 +0200 Subject: [PATCH 88/94] Core: make shlex split an attempt with regular split retry (#4046) --- MultiServer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/MultiServer.py b/MultiServer.py index 0fe950b5e4f3..8dfad5040de3 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -1153,7 +1153,10 @@ def __call__(self, raw: str) -> typing.Optional[bool]: if not raw: return try: - command = shlex.split(raw, comments=False) + try: + command = shlex.split(raw, comments=False) + except ValueError: # most likely: "ValueError: No closing quotation" + command = raw.split() basecommand = command[0] if basecommand[0] == self.marker: method = self.commands.get(basecommand[1:].lower(), None) From 2d0bdebaa9d58b5606dd60a773e45a3916658d71 Mon Sep 17 00:00:00 2001 From: gurglemurgle5 <95941332+gurglemurgle5@users.noreply.github.com> Date: Sat, 12 Oct 2024 19:01:28 -0500 Subject: [PATCH 89/94] Docs: Add ConnectUpdate to the list of client packets in the network protocol documentation #4045 --- docs/network protocol.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/network protocol.md b/docs/network protocol.md index 1c4579c4066f..4a96a43f818f 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -268,6 +268,7 @@ Additional arguments added to the [Set](#Set) package that triggered this [SetRe These packets are sent purely from client to server. They are not accepted by clients. * [Connect](#Connect) +* [ConnectUpdate](#ConnectUpdate) * [Sync](#Sync) * [LocationChecks](#LocationChecks) * [LocationScouts](#LocationScouts) From e8f3aa96dadef8651a05c0f070a1b8a361cb4eb9 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sun, 13 Oct 2024 17:21:36 -0400 Subject: [PATCH 90/94] Timespinner: Two typos #4051 --- worlds/timespinner/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index 66744cffdf85..f241d4468162 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -190,7 +190,7 @@ def write_spoiler_header(self, spoiler_handle: TextIO) -> None: if self.options.has_replaced_options: warning = \ - f"NOTICE: Timespinner options for player '{self.player_name}' where renamed from PasCalCase to snake_case, " \ + f"NOTICE: Timespinner options for player '{self.player_name}' were renamed from PascalCase to snake_case, " \ "please update your yaml" spoiler_handle.write("\n") From b772d42df56d915927919bb6d4176576c19d95c0 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 14 Oct 2024 00:15:53 +0200 Subject: [PATCH 91/94] Core: turn MultiServer item_names and location_names into instance vars (#4053) --- MultiServer.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 8dfad5040de3..bac35648cf5a 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -185,11 +185,9 @@ class Context: slot_info: typing.Dict[int, NetworkSlot] generator_version = Version(0, 0, 0) checksums: typing.Dict[str, str] - item_names: typing.Dict[str, typing.Dict[int, str]] = ( - collections.defaultdict(lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})'))) + item_names: typing.Dict[str, typing.Dict[int, str]] item_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]] - location_names: typing.Dict[str, typing.Dict[int, str]] = ( - collections.defaultdict(lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})'))) + location_names: typing.Dict[str, typing.Dict[int, str]] location_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]] all_item_and_group_names: typing.Dict[str, typing.Set[str]] all_location_and_group_names: typing.Dict[str, typing.Set[str]] @@ -198,7 +196,6 @@ class Context: """ each sphere is { player: { location_id, ... } } """ logger: logging.Logger - def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int, hint_cost: int, item_cheat: bool, release_mode: str = "disabled", collect_mode="disabled", remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2, @@ -269,6 +266,10 @@ def __init__(self, host: str, port: int, server_password: str, password: str, lo self.location_name_groups = {} self.all_item_and_group_names = {} self.all_location_and_group_names = {} + self.item_names = collections.defaultdict( + lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})')) + self.location_names = collections.defaultdict( + lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')) self.non_hintable_names = collections.defaultdict(frozenset) self._load_game_data() From d4d777b101f1759b0164181045cd7719d6b0d8ed Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Sun, 13 Oct 2024 18:17:53 -0400 Subject: [PATCH 92/94] OoT: Add aliases for Progressive Hookshot (#4052) * Add aliases for Progressive Hookshot * Update worlds/oot/__init__.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/oot/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index b93f60b2a08e..c3925bf2a8bf 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -184,6 +184,10 @@ class OOTWorld(World): "Small Key Ring (Spirit Temple)", "Small Key Ring (Thieves Hideout)", "Small Key Ring (Water Temple)", "Boss Key (Fire Temple)", "Boss Key (Forest Temple)", "Boss Key (Ganons Castle)", "Boss Key (Shadow Temple)", "Boss Key (Spirit Temple)", "Boss Key (Water Temple)"}, + + # aliases + "Longshot": {"Progressive Hookshot"}, # fuzzy hinting thought Longshot was Slingshot + "Hookshot": {"Progressive Hookshot"}, # for consistency, mostly } location_name_groups = build_location_name_groups() From f2ac937d1e6d2964ab1f5951cdb54f47bdd96e78 Mon Sep 17 00:00:00 2001 From: Seafo <92278897+Seatori@users.noreply.github.com> Date: Sun, 13 Oct 2024 18:22:37 -0400 Subject: [PATCH 93/94] Minecraft: Fix plando connections #4048 Plando connections was broken as a result of https://github.com/ArchipelagoMW/Archipelago/pull/3765 This fixes it. --- worlds/minecraft/Structures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/minecraft/Structures.py b/worlds/minecraft/Structures.py index df3d944a6c65..d4f62f3498e9 100644 --- a/worlds/minecraft/Structures.py +++ b/worlds/minecraft/Structures.py @@ -29,7 +29,7 @@ def set_pair(exit, struct): # Connect plando structures first if self.options.plando_connections: - for conn in self.plando_connections: + for conn in self.options.plando_connections: set_pair(conn.entrance, conn.exit) # The algorithm tries to place the most restrictive structures first. This algorithm always works on the From 618564c60a1ae18d26d09688e08fc7df9e61dd8c Mon Sep 17 00:00:00 2001 From: Louis M Date: Mon, 14 Oct 2024 12:53:20 -0400 Subject: [PATCH 94/94] Aquaria: Adding slot data for poptracker (#4056) * Adds neccessary slot data for Aquaria * Comma oops --------- Co-authored-by: palex00 <32203971+palex00@users.noreply.github.com> --- worlds/aquaria/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/worlds/aquaria/__init__.py b/worlds/aquaria/__init__.py index 1fb04036d81b..dd17d09d8a6a 100644 --- a/worlds/aquaria/__init__.py +++ b/worlds/aquaria/__init__.py @@ -212,4 +212,8 @@ def fill_slot_data(self) -> Dict[str, Any]: "skip_first_vision": bool(self.options.skip_first_vision.value), "unconfine_home_water_energy_door": self.options.unconfine_home_water.value in [1, 3], "unconfine_home_water_transturtle": self.options.unconfine_home_water.value in [2, 3], + "bind_song_needed_to_get_under_rock_bulb": bool(self.options.bind_song_needed_to_get_under_rock_bulb), + "no_progression_hard_or_hidden_locations": bool(self.options.no_progression_hard_or_hidden_locations), + "light_needed_to_get_to_dark_places": bool(self.options.light_needed_to_get_to_dark_places), + "turtle_randomizer": self.options.turtle_randomizer.value, }