From 3ae8992fb61d24104faa502cff275ba6a771cdbe Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:59:17 +0100 Subject: [PATCH 01/12] Clients: fix high CPU usage when launched via MultiProcessing (#4209) * Core: make Utils.stream_input not consume all CPU for non-blocking streams * Clients: ignore MultiProcessing pipe as input console --- CommonClient.py | 5 +++++ Utils.py | 3 +++ 2 files changed, 8 insertions(+) diff --git a/CommonClient.py b/CommonClient.py index 77ed85b5c652..47100a7383ab 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -710,6 +710,11 @@ def run_gui(self): def run_cli(self): if sys.stdin: + if sys.stdin.fileno() != 0: + from multiprocessing import parent_process + if parent_process(): + return # ignore MultiProcessing pipe + # steam overlay breaks when starting console_loop if 'gameoverlayrenderer' in os.environ.get('LD_PRELOAD', ''): logger.info("Skipping terminal input, due to conflicting Steam Overlay detected. Please use GUI only.") diff --git a/Utils.py b/Utils.py index 412011200f8a..2dfcd9d3e19a 100644 --- a/Utils.py +++ b/Utils.py @@ -18,6 +18,7 @@ from argparse import Namespace from settings import Settings, get_settings +from time import sleep from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union from typing_extensions import TypeGuard from yaml import load, load_all, dump @@ -568,6 +569,8 @@ def queuer(): else: if text: queue.put_nowait(text) + else: + sleep(0.01) # non-blocking stream from threading import Thread thread = Thread(target=queuer, name=f"Stream handler for {stream.name}", daemon=True) From 85a713771b98aca84c900f809b0ccf41d1002d6d Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Mon, 18 Nov 2024 11:09:27 -0600 Subject: [PATCH 02/12] Tests: have option preset validation test do full validation (#4208) * Tests: have option preset validation test do full validation * sum on an IntFlag is a thing apparently --- test/webhost/test_option_presets.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/webhost/test_option_presets.py b/test/webhost/test_option_presets.py index b0af8a871183..7105c7f80593 100644 --- a/test/webhost/test_option_presets.py +++ b/test/webhost/test_option_presets.py @@ -1,5 +1,6 @@ import unittest +from BaseClasses import PlandoOptions from worlds import AutoWorldRegister from Options import ItemDict, NamedRange, NumericOption, OptionList, OptionSet @@ -14,6 +15,10 @@ def test_option_presets_have_valid_options(self): with self.subTest(game=game_name, preset=preset_name, option=option_name): try: option = world_type.options_dataclass.type_hints[option_name].from_any(option_value) + # some options may need verification to ensure the provided option is actually valid + # pass in all plando options in case a preset wants to require certain plando options + # for some reason + option.verify(world_type, "Test Player", PlandoOptions(sum(PlandoOptions))) supported_types = [NumericOption, OptionSet, OptionList, ItemDict] if not any([issubclass(option.__class__, t) for t in supported_types]): self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' " From 48ea274655223ea50bffaa21d395a8b5fef543cd Mon Sep 17 00:00:00 2001 From: qwint Date: Tue, 19 Nov 2024 15:16:10 -0500 Subject: [PATCH 03/12] MultiServer: persist hints even if previously found (#4214) * change to persist all hints to ctx.hints regardless of found status * remove if not found entirely as it seems like it was added to not double charge hint points https://github.com/ArchipelagoMW/Archipelago/commit/9842399d8be082b757a140137706c12d84cda8c1 --- MultiServer.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 764b56362ecc..847a0b281c40 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -727,15 +727,15 @@ def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: b if not hint.local and data not in concerns[hint.finding_player]: concerns[hint.finding_player].append(data) # remember hints in all cases - if not hint.found: - # since hints are bidirectional, finding player and receiving player, - # we can check once if hint already exists - if hint not in self.hints[team, hint.finding_player]: - self.hints[team, hint.finding_player].add(hint) - new_hint_events.add(hint.finding_player) - for player in self.slot_set(hint.receiving_player): - self.hints[team, player].add(hint) - new_hint_events.add(player) + + # since hints are bidirectional, finding player and receiving player, + # we can check once if hint already exists + if hint not in self.hints[team, hint.finding_player]: + self.hints[team, hint.finding_player].add(hint) + new_hint_events.add(hint.finding_player) + for player in self.slot_set(hint.receiving_player): + self.hints[team, player].add(hint) + new_hint_events.add(player) self.logger.info("Notice (Team #%d): %s" % (team + 1, format_hint(self, team, hint))) for slot in new_hint_events: From 124ce13da75747be31da09358208d851ad8da3fb Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Wed, 20 Nov 2024 00:45:41 -0800 Subject: [PATCH 04/12] Core: improve error message for missing "game" entry in yaml (#4185) --- Generate.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Generate.py b/Generate.py index bc359a203da7..8aba72abafe9 100644 --- a/Generate.py +++ b/Generate.py @@ -453,6 +453,10 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b raise Exception(f"Option {option_key} has to be in a game's section, not on its own.") ret.game = get_choice("game", weights) + if not isinstance(ret.game, str): + if ret.game is None: + raise Exception('"game" not specified') + raise Exception(f"Invalid game: {ret.game}") if ret.game not in AutoWorldRegister.world_types: from worlds import failed_world_loads picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + failed_world_loads, limit=1)[0] From 859ae87ec911c26c439767a65be0efe098f08357 Mon Sep 17 00:00:00 2001 From: qwint Date: Thu, 21 Nov 2024 11:43:01 -0500 Subject: [PATCH 05/12] Launcher: ports the _stop fix in the Launcher kivy App to handle_url Popup App (#4213) * fixes url launched popup so it can close cleanly after spawning another kivy app like text client * whoops --- Launcher.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Launcher.py b/Launcher.py index ea59e8beb500..f04d67a5aa0d 100644 --- a/Launcher.py +++ b/Launcher.py @@ -181,6 +181,11 @@ def update_label(self, dt): App.get_running_app().stop() Window.close() + def _stop(self, *largs): + # see run_gui Launcher _stop comment for details + self.root_window.close() + super()._stop(*largs) + Popup().run() From d4b1351c99f44f4d0444e5d8055572317f52218b Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Thu, 21 Nov 2024 19:43:37 +0000 Subject: [PATCH 06/12] Aquaria: Remove BaseException handling from create_item (#4218) * Aquaria: Remove BaseException handling from create_item Catching `BaseException` without re-raising the exception should almost never be done because `BaseException` includes exit exceptions, such as `SystemExit` and `KeyboardInterrupt`. Ideally, the caught exception types should be as narrow as possible to not mask bugs from catching unexpected exceptions. Having narrow exception types can also help indicate to other developers what exceptions are expected to be raisable by the code within the `try` clause. Similarly, the `try` clause should ideally contain the minimum code necessary, to avoid masking bugs in the case that code within the `try` clause that is not expected to raise an exception does so. In this case, the only expected exception that can occur appears to be `item_table[name]` that can raise a `KeyError` when `create_item()` is passed an unexpected `name` argument. So this patch moves the other code out of the `try` clause and changes the caught exception types to only `KeyError`. * Remove try-except The KeyError that would be raised will be propagated as-is rather than raising a new exception in its place. * Remove extra newline The original code did not have this newline, so it has been removed. --- worlds/aquaria/__init__.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/worlds/aquaria/__init__.py b/worlds/aquaria/__init__.py index f79978f25fc4..f620bf6d7306 100644 --- a/worlds/aquaria/__init__.py +++ b/worlds/aquaria/__init__.py @@ -117,16 +117,13 @@ def create_item(self, name: str) -> AquariaItem: Create an AquariaItem using 'name' as item name. """ result: AquariaItem - try: - data = item_table[name] - classification: ItemClassification = ItemClassification.useful - if data.type == ItemType.JUNK: - classification = ItemClassification.filler - elif data.type == ItemType.PROGRESSION: - classification = ItemClassification.progression - result = AquariaItem(name, classification, data.id, self.player) - except BaseException: - raise Exception('The item ' + name + ' is not valid.') + data = item_table[name] + classification: ItemClassification = ItemClassification.useful + if data.type == ItemType.JUNK: + classification = ItemClassification.filler + elif data.type == ItemType.PROGRESSION: + classification = ItemClassification.progression + result = AquariaItem(name, classification, data.id, self.player) return result From 2424b79626ee4e227ce3579c6c2d4a8cee7dca86 Mon Sep 17 00:00:00 2001 From: digiholic Date: Fri, 22 Nov 2024 08:33:27 -0700 Subject: [PATCH 07/12] OSRS: Fixes to Logic errors related to Max Skill Level determining when Regions are accessible (#4188) * Removes explicit indirect conditions * Changes special rules function add rule instead of setting, and call it unconditionally * Fixes issues in rule generation that have been around but unused the whole time * Finally moves rules out into a separate file. Fixes level-related logic * Removes redundant max skill level checks on canoes, since they're in the skill training rules now * For some reason, canoe logic assumed you could always walk from lumbridge to south varrock without farms. This has been fixed * Apply suggestions from code review Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Quests now respect skill limits and can be excluded. Tasks that take multiple skills how actually check all skills * Adds alternative route for cooking that doesn't require fishing * Remove debug code --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- worlds/osrs/LogicCSV/locations_generated.py | 6 +- worlds/osrs/Names.py | 2 +- worlds/osrs/Rules.py | 337 +++++++++++++ worlds/osrs/__init__.py | 503 +++----------------- 4 files changed, 401 insertions(+), 447 deletions(-) create mode 100644 worlds/osrs/Rules.py diff --git a/worlds/osrs/LogicCSV/locations_generated.py b/worlds/osrs/LogicCSV/locations_generated.py index 073e505ad8f4..2d617a7038fe 100644 --- a/worlds/osrs/LogicCSV/locations_generated.py +++ b/worlds/osrs/LogicCSV/locations_generated.py @@ -57,11 +57,11 @@ LocationRow('Catch a Swordfish', 'fishing', ['Lobster Spot', ], [SkillRequirement('Fishing', 50), ], [], 12), LocationRow('Bake a Redberry Pie', 'cooking', ['Redberry Bush', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 10), ], [], 0), LocationRow('Cook some Stew', 'cooking', ['Bowl', 'Meat', 'Potato', ], [SkillRequirement('Cooking', 25), ], [], 0), - LocationRow('Bake an Apple Pie', 'cooking', ['Cooking Apple', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 30), ], [], 2), + LocationRow('Bake an Apple Pie', 'cooking', ['Cooking Apple', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 32), ], [], 2), LocationRow('Bake a Cake', 'cooking', ['Wheat', 'Windmill', 'Egg', 'Milk', 'Cake Tin', ], [SkillRequirement('Cooking', 40), ], [], 6), LocationRow('Bake a Meat Pizza', 'cooking', ['Wheat', 'Windmill', 'Cheese', 'Tomato', 'Meat', ], [SkillRequirement('Cooking', 45), ], [], 8), - LocationRow('Burn some Oak Logs', 'firemaking', ['Oak Tree', ], [SkillRequirement('Firemaking', 15), ], [], 0), - LocationRow('Burn some Willow Logs', 'firemaking', ['Willow Tree', ], [SkillRequirement('Firemaking', 30), ], [], 0), + LocationRow('Burn some Oak Logs', 'firemaking', ['Oak Tree', ], [SkillRequirement('Firemaking', 15), SkillRequirement('Woodcutting', 15), ], [], 0), + LocationRow('Burn some Willow Logs', 'firemaking', ['Willow Tree', ], [SkillRequirement('Firemaking', 30), SkillRequirement('Woodcutting', 30), ], [], 0), LocationRow('Travel on a Canoe', 'woodcutting', ['Canoe Tree', ], [SkillRequirement('Woodcutting', 12), ], [], 0), LocationRow('Cut an Oak Log', 'woodcutting', ['Oak Tree', ], [SkillRequirement('Woodcutting', 15), ], [], 0), LocationRow('Cut a Willow Log', 'woodcutting', ['Willow Tree', ], [SkillRequirement('Woodcutting', 30), ], [], 0), diff --git a/worlds/osrs/Names.py b/worlds/osrs/Names.py index cc92439ef859..1a44aa389c6a 100644 --- a/worlds/osrs/Names.py +++ b/worlds/osrs/Names.py @@ -31,7 +31,7 @@ class RegionNames(str, Enum): Mudskipper_Point = "Mudskipper Point" Karamja = "Karamja" Corsair_Cove = "Corsair Cove" - Wilderness = "The Wilderness" + Wilderness = "Wilderness" Crandor = "Crandor" # Resource Regions Egg = "Egg" diff --git a/worlds/osrs/Rules.py b/worlds/osrs/Rules.py new file mode 100644 index 000000000000..22a19934c8e1 --- /dev/null +++ b/worlds/osrs/Rules.py @@ -0,0 +1,337 @@ +""" + Ensures a target level can be reached with available resources + """ +from worlds.generic.Rules import CollectionRule, add_rule +from .Names import RegionNames, ItemNames + + +def get_fishing_skill_rule(level, player, options) -> CollectionRule: + if options.max_fishing_level < level: + return lambda state: False + + if options.brutal_grinds or level < 5: + return lambda state: state.can_reach_region(RegionNames.Shrimp, player) + if level < 20: + return lambda state: state.can_reach_region(RegionNames.Shrimp, player) and \ + state.can_reach_region(RegionNames.Port_Sarim, player) + else: + return lambda state: state.can_reach_region(RegionNames.Shrimp, player) and \ + state.can_reach_region(RegionNames.Port_Sarim, player) and \ + state.can_reach_region(RegionNames.Fly_Fish, player) + + +def get_mining_skill_rule(level, player, options) -> CollectionRule: + if options.max_mining_level < level: + return lambda state: False + + if options.brutal_grinds or level < 15: + return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) or \ + state.can_reach_region(RegionNames.Clay_Rock, player) + else: + # Iron is the best way to train all the way to 99, so having access to iron is all you need to check for + return lambda state: (state.can_reach_region(RegionNames.Bronze_Ores, player) or + state.can_reach_region(RegionNames.Clay_Rock, player)) and \ + state.can_reach_region(RegionNames.Iron_Rock, player) + + +def get_woodcutting_skill_rule(level, player, options) -> CollectionRule: + if options.max_woodcutting_level < level: + return lambda state: False + + if options.brutal_grinds or level < 15: + # I've checked. There is not a single chunk in the f2p that does not have at least one normal tree. + # Even the desert. + return lambda state: True + if level < 30: + return lambda state: state.can_reach_region(RegionNames.Oak_Tree, player) + else: + return lambda state: state.can_reach_region(RegionNames.Oak_Tree, player) and \ + state.can_reach_region(RegionNames.Willow_Tree, player) + + +def get_smithing_skill_rule(level, player, options) -> CollectionRule: + if options.max_smithing_level < level: + return lambda state: False + + if options.brutal_grinds: + return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) and \ + state.can_reach_region(RegionNames.Furnace, player) + if level < 15: + # Lumbridge has a special bronze-only anvil. This is the only anvil of its type so it's not included + # in the "Anvil" resource region. We still need to check for it though. + return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) and \ + state.can_reach_region(RegionNames.Furnace, player) and \ + (state.can_reach_region(RegionNames.Anvil, player) or + state.can_reach_region(RegionNames.Lumbridge, player)) + if level < 30: + # For levels between 15 and 30, the lumbridge anvil won't cut it. Only a real one will do + return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) and \ + state.can_reach_region(RegionNames.Iron_Rock, player) and \ + state.can_reach_region(RegionNames.Furnace, player) and \ + state.can_reach_region(RegionNames.Anvil, player) + else: + return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) and \ + state.can_reach_region(RegionNames.Iron_Rock, player) and \ + state.can_reach_region(RegionNames.Coal_Rock, player) and \ + state.can_reach_region(RegionNames.Furnace, player) and \ + state.can_reach_region(RegionNames.Anvil, player) + + +def get_crafting_skill_rule(level, player, options): + if options.max_crafting_level < level: + return lambda state: False + + # Crafting is really complex. Need a lot of sub-rules to make this even remotely readable + def can_spin(state): + return state.can_reach_region(RegionNames.Sheep, player) and \ + state.can_reach_region(RegionNames.Spinning_Wheel, player) + + def can_pot(state): + return state.can_reach_region(RegionNames.Clay_Rock, player) and \ + state.can_reach_region(RegionNames.Barbarian_Village, player) + + def can_tan(state): + return state.can_reach_region(RegionNames.Milk, player) and \ + state.can_reach_region(RegionNames.Al_Kharid, player) + + def mould_access(state): + return state.can_reach_region(RegionNames.Al_Kharid, player) or \ + state.can_reach_region(RegionNames.Rimmington, player) + + def can_silver(state): + return state.can_reach_region(RegionNames.Silver_Rock, player) and \ + state.can_reach_region(RegionNames.Furnace, player) and mould_access(state) + + def can_gold(state): + return state.can_reach_region(RegionNames.Gold_Rock, player) and \ + state.can_reach_region(RegionNames.Furnace, player) and mould_access(state) + + if options.brutal_grinds or level < 5: + return lambda state: can_spin(state) or can_pot(state) or can_tan(state) + + can_smelt_gold = get_smithing_skill_rule(40, player, options) + can_smelt_silver = get_smithing_skill_rule(20, player, options) + if level < 16: + return lambda state: can_pot(state) or can_tan(state) or (can_gold(state) and can_smelt_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)) + + +def get_cooking_skill_rule(level, player, options) -> CollectionRule: + if options.max_cooking_level < level: + return lambda state: False + + if options.brutal_grinds or level < 15: + return lambda state: state.can_reach_region(RegionNames.Milk, player) or \ + state.can_reach_region(RegionNames.Egg, player) or \ + state.can_reach_region(RegionNames.Shrimp, player) or \ + (state.can_reach_region(RegionNames.Wheat, player) and + state.can_reach_region(RegionNames.Windmill, player)) + else: + can_catch_fly_fish = get_fishing_skill_rule(20, player, options) + + return lambda state: ( + (state.can_reach_region(RegionNames.Fly_Fish, player) and can_catch_fly_fish(state)) or + (state.can_reach_region(RegionNames.Port_Sarim, player)) + ) and ( + state.can_reach_region(RegionNames.Milk, player) or + state.can_reach_region(RegionNames.Egg, player) or + state.can_reach_region(RegionNames.Shrimp, player) or + (state.can_reach_region(RegionNames.Wheat, player) and + state.can_reach_region(RegionNames.Windmill, player)) + ) + + +def get_runecraft_skill_rule(level, player, options) -> CollectionRule: + if options.max_runecraft_level < level: + return lambda state: False + if not options.brutal_grinds: + # Ensure access to the relevant altars + if level >= 5: + return lambda state: state.has(ItemNames.QP_Rune_Mysteries, player) and \ + state.can_reach_region(RegionNames.Falador_Farm, player) and \ + state.can_reach_region(RegionNames.Lumbridge_Swamp, player) + if level >= 9: + return lambda state: state.has(ItemNames.QP_Rune_Mysteries, player) and \ + state.can_reach_region(RegionNames.Falador_Farm, player) and \ + state.can_reach_region(RegionNames.Lumbridge_Swamp, player) and \ + state.can_reach_region(RegionNames.East_Of_Varrock, player) + if level >= 14: + return lambda state: state.has(ItemNames.QP_Rune_Mysteries, player) and \ + state.can_reach_region(RegionNames.Falador_Farm, player) and \ + state.can_reach_region(RegionNames.Lumbridge_Swamp, player) and \ + state.can_reach_region(RegionNames.East_Of_Varrock, player) and \ + state.can_reach_region(RegionNames.Al_Kharid, player) + + return lambda state: state.has(ItemNames.QP_Rune_Mysteries, player) and \ + state.can_reach_region(RegionNames.Falador_Farm, player) + + +def get_magic_skill_rule(level, player, options) -> CollectionRule: + if options.max_magic_level < level: + return lambda state: False + + return lambda state: state.can_reach_region(RegionNames.Mind_Runes, player) + + +def get_firemaking_skill_rule(level, player, options) -> CollectionRule: + if options.max_firemaking_level < level: + return lambda state: False + if not options.brutal_grinds: + if level >= 30: + can_chop_willows = get_woodcutting_skill_rule(30, player, options) + return lambda state: state.can_reach_region(RegionNames.Willow_Tree, player) and can_chop_willows(state) + if level >= 15: + can_chop_oaks = get_woodcutting_skill_rule(15, player, options) + return lambda state: state.can_reach_region(RegionNames.Oak_Tree, player) and can_chop_oaks(state) + # If brutal grinds are on, or if the level is less than 15, you can train it. + return lambda state: True + + +def get_skill_rule(skill, level, player, options) -> CollectionRule: + if skill.lower() == "fishing": + return get_fishing_skill_rule(level, player, options) + if skill.lower() == "mining": + return get_mining_skill_rule(level, player, options) + if skill.lower() == "woodcutting": + return get_woodcutting_skill_rule(level, player, options) + if skill.lower() == "smithing": + return get_smithing_skill_rule(level, player, options) + if skill.lower() == "crafting": + return get_crafting_skill_rule(level, player, options) + if skill.lower() == "cooking": + return get_cooking_skill_rule(level, player, options) + if skill.lower() == "runecraft": + return get_runecraft_skill_rule(level, player, options) + if skill.lower() == "magic": + return get_magic_skill_rule(level, player, options) + if skill.lower() == "firemaking": + return get_firemaking_skill_rule(level, player, options) + + return lambda state: True + + +def generate_special_rules_for(entrance, region_row, outbound_region_name, player, options): + if outbound_region_name == RegionNames.Cooks_Guild: + add_rule(entrance, get_cooking_skill_rule(32, player, options)) + elif outbound_region_name == RegionNames.Crafting_Guild: + add_rule(entrance, get_crafting_skill_rule(40, player, options)) + elif outbound_region_name == RegionNames.Corsair_Cove: + # Need to be able to start Corsair Curse in addition to having the item + add_rule(entrance, lambda state: state.can_reach(RegionNames.Falador_Farm, "Region", player)) + elif outbound_region_name == "Camdozaal*": + add_rule(entrance, lambda state: state.has(ItemNames.QP_Below_Ice_Mountain, player)) + elif region_row.name == "Dwarven Mountain Pass" and outbound_region_name == "Anvil*": + add_rule(entrance, lambda state: state.has(ItemNames.QP_Dorics_Quest, player)) + + # Special logic for canoes + canoe_regions = [RegionNames.Lumbridge, RegionNames.South_Of_Varrock, RegionNames.Barbarian_Village, + RegionNames.Edgeville, RegionNames.Wilderness] + if region_row.name in canoe_regions: + # Skill rules for greater distances + woodcutting_rule_d1 = get_woodcutting_skill_rule(12, player, options) + woodcutting_rule_d2 = get_woodcutting_skill_rule(27, player, options) + woodcutting_rule_d3 = get_woodcutting_skill_rule(42, player, options) + woodcutting_rule_all = get_woodcutting_skill_rule(57, player, options) + + if region_row.name == RegionNames.Lumbridge: + # Canoe Tree access for the Location + if outbound_region_name == RegionNames.Canoe_Tree: + add_rule(entrance, + lambda state: (state.can_reach_region(RegionNames.South_Of_Varrock, player) + and woodcutting_rule_d1(state)) or + (state.can_reach_region(RegionNames.Barbarian_Village, player) + and woodcutting_rule_d2(state)) or + (state.can_reach_region(RegionNames.Edgeville, player) + and woodcutting_rule_d3(state)) or + (state.can_reach_region(RegionNames.Wilderness, player) + and woodcutting_rule_all(state))) + + # Access to other chunks based on woodcutting settings + elif outbound_region_name == RegionNames.South_Of_Varrock: + add_rule(entrance, woodcutting_rule_d1) + elif outbound_region_name == RegionNames.Barbarian_Village: + add_rule(entrance, woodcutting_rule_d2) + elif outbound_region_name == RegionNames.Edgeville: + add_rule(entrance, woodcutting_rule_d3) + elif outbound_region_name == RegionNames.Wilderness: + add_rule(entrance, woodcutting_rule_all) + + elif region_row.name == RegionNames.South_Of_Varrock: + if outbound_region_name == RegionNames.Canoe_Tree: + add_rule(entrance, + lambda state: (state.can_reach_region(RegionNames.Lumbridge, player) + and woodcutting_rule_d1(state)) or + (state.can_reach_region(RegionNames.Barbarian_Village, player) + and woodcutting_rule_d1(state)) or + (state.can_reach_region(RegionNames.Edgeville, player) + and woodcutting_rule_d2(state)) or + (state.can_reach_region(RegionNames.Wilderness, player) + and woodcutting_rule_d3(state))) + + # Access to other chunks based on woodcutting settings + elif outbound_region_name == RegionNames.Lumbridge: + add_rule(entrance, woodcutting_rule_d1) + elif outbound_region_name == RegionNames.Barbarian_Village: + add_rule(entrance, woodcutting_rule_d1) + elif outbound_region_name == RegionNames.Edgeville: + add_rule(entrance, woodcutting_rule_d3) + elif outbound_region_name == RegionNames.Wilderness: + add_rule(entrance, woodcutting_rule_all) + elif region_row.name == RegionNames.Barbarian_Village: + if outbound_region_name == RegionNames.Canoe_Tree: + add_rule(entrance, + lambda state: (state.can_reach_region(RegionNames.Lumbridge, player) + and woodcutting_rule_d2(state)) or (state.can_reach_region(RegionNames.South_Of_Varrock, player) + and woodcutting_rule_d1(state)) or (state.can_reach_region(RegionNames.Edgeville, player) + and woodcutting_rule_d1(state)) or (state.can_reach_region(RegionNames.Wilderness, player) + and woodcutting_rule_d2(state))) + + # Access to other chunks based on woodcutting settings + elif outbound_region_name == RegionNames.Lumbridge: + add_rule(entrance, woodcutting_rule_d2) + elif outbound_region_name == RegionNames.South_Of_Varrock: + add_rule(entrance, woodcutting_rule_d1) + # Edgeville does not need to be checked, because it's already adjacent + elif outbound_region_name == RegionNames.Wilderness: + add_rule(entrance, woodcutting_rule_d3) + elif region_row.name == RegionNames.Edgeville: + if outbound_region_name == RegionNames.Canoe_Tree: + add_rule(entrance, + lambda state: (state.can_reach_region(RegionNames.Lumbridge, player) + and woodcutting_rule_d3(state)) or + (state.can_reach_region(RegionNames.South_Of_Varrock, player) + and woodcutting_rule_d2(state)) or + (state.can_reach_region(RegionNames.Barbarian_Village, player) + and woodcutting_rule_d1(state)) or + (state.can_reach_region(RegionNames.Wilderness, player) + and woodcutting_rule_d1(state))) + + # Access to other chunks based on woodcutting settings + elif outbound_region_name == RegionNames.Lumbridge: + add_rule(entrance, woodcutting_rule_d3) + elif outbound_region_name == RegionNames.South_Of_Varrock: + add_rule(entrance, woodcutting_rule_d2) + # Barbarian Village does not need to be checked, because it's already adjacent + # Wilderness does not need to be checked, because it's already adjacent + elif region_row.name == RegionNames.Wilderness: + if outbound_region_name == RegionNames.Canoe_Tree: + add_rule(entrance, + lambda state: (state.can_reach_region(RegionNames.Lumbridge, player) + and woodcutting_rule_all(state)) or + (state.can_reach_region(RegionNames.South_Of_Varrock, player) + and woodcutting_rule_d3(state)) or + (state.can_reach_region(RegionNames.Barbarian_Village, player) + and woodcutting_rule_d2(state)) or + (state.can_reach_region(RegionNames.Edgeville, player) + and woodcutting_rule_d1(state))) + + # Access to other chunks based on woodcutting settings + elif outbound_region_name == RegionNames.Lumbridge: + add_rule(entrance, woodcutting_rule_all) + elif outbound_region_name == RegionNames.South_Of_Varrock: + add_rule(entrance, woodcutting_rule_d3) + elif outbound_region_name == RegionNames.Barbarian_Village: + add_rule(entrance, woodcutting_rule_d2) + # Edgeville does not need to be checked, because it's already adjacent diff --git a/worlds/osrs/__init__.py b/worlds/osrs/__init__.py index 58f23a2bc1d9..d6ddd63875f4 100644 --- a/worlds/osrs/__init__.py +++ b/worlds/osrs/__init__.py @@ -1,12 +1,12 @@ import typing -from BaseClasses import Item, Tutorial, ItemClassification, Region, MultiWorld +from BaseClasses import Item, Tutorial, ItemClassification, Region, MultiWorld, CollectionState +from Fill import fill_restrictive, FillError from worlds.AutoWorld import WebWorld, World -from worlds.generic.Rules import add_rule, CollectionRule from .Items import OSRSItem, starting_area_dict, chunksanity_starting_chunks, QP_Items, ItemRow, \ chunksanity_special_region_names from .Locations import OSRSLocation, LocationRow - +from .Rules import * from .Options import OSRSOptions, StartingArea from .Names import LocationNames, ItemNames, RegionNames @@ -46,6 +46,7 @@ class OSRSWorld(World): web = OSRSWeb() base_id = 0x070000 data_version = 1 + explicit_indirect_conditions = False item_name_to_id = {item_rows[i].name: 0x070000 + i for i in range(len(item_rows))} location_name_to_id = {location_rows[i].name: 0x070000 + i for i in range(len(location_rows))} @@ -61,6 +62,7 @@ class OSRSWorld(World): starting_area_item: str locations_by_category: typing.Dict[str, typing.List[LocationRow]] + available_QP_locations: typing.List[str] def __init__(self, multiworld: MultiWorld, player: int): super().__init__(multiworld, player) @@ -75,6 +77,7 @@ def __init__(self, multiworld: MultiWorld, player: int): self.starting_area_item = "" self.locations_by_category = {} + self.available_QP_locations = [] def generate_early(self) -> None: location_categories = [location_row.category for location_row in location_rows] @@ -90,9 +93,9 @@ 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 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: @@ -127,7 +130,6 @@ def interpret_slot_data(self, slot_data: typing.Dict[str, typing.Any]) -> None: 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 @@ -145,7 +147,8 @@ 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 != "": #if area hasn't been set, then we shouldn't connect it + # if area hasn't been set, then we shouldn't connect it + if self.starting_area_item != "": if self.starting_area_item in chunksanity_special_region_names: starting_area_region = chunksanity_special_region_names[self.starting_area_item] else: @@ -164,11 +167,8 @@ def create_regions(self) -> None: entrance.connect(self.region_name_to_data[parsed_outbound]) item_name = self.region_rows_by_name[parsed_outbound].itemReq - if "*" not in outbound_region_name and "*" not in item_name: - entrance.access_rule = lambda state, item_name=item_name: state.has(item_name, self.player) - continue - - self.generate_special_rules_for(entrance, region_row, outbound_region_name) + entrance.access_rule = lambda state, item_name=item_name.replace("*",""): state.has(item_name, self.player) + generate_special_rules_for(entrance, region_row, outbound_region_name, self.player, self.options) for resource_region in region_row.resources: if not resource_region: @@ -178,321 +178,34 @@ def create_regions(self) -> None: if "*" not in resource_region: entrance.connect(self.region_name_to_data[resource_region]) else: - self.generate_special_rules_for(entrance, region_row, resource_region) entrance.connect(self.region_name_to_data[resource_region.replace('*', '')]) + generate_special_rules_for(entrance, region_row, resource_region, self.player, self.options) self.roll_locations() - def generate_special_rules_for(self, entrance, region_row, outbound_region_name): - # print(f"Special rules required to access region {outbound_region_name} from {region_row.name}") - if outbound_region_name == RegionNames.Cooks_Guild: - item_name = self.region_rows_by_name[outbound_region_name].itemReq.replace('*', '') - cooking_level_rule = self.get_skill_rule("cooking", 32) - entrance.access_rule = lambda state: state.has(item_name, self.player) and \ - cooking_level_rule(state) - if self.options.brutal_grinds: - cooking_level_32_regions = { - RegionNames.Milk, - RegionNames.Egg, - RegionNames.Shrimp, - RegionNames.Wheat, - RegionNames.Windmill, - } - else: - # Level 15 cooking and higher requires level 20 fishing. - fishing_level_20_regions = { - RegionNames.Shrimp, - RegionNames.Port_Sarim, - } - cooking_level_32_regions = { - RegionNames.Milk, - RegionNames.Egg, - RegionNames.Shrimp, - RegionNames.Wheat, - RegionNames.Windmill, - RegionNames.Fly_Fish, - *fishing_level_20_regions, - } - for region_name in cooking_level_32_regions: - self.multiworld.register_indirect_condition(self.get_region(region_name), entrance) - return - if outbound_region_name == RegionNames.Crafting_Guild: - item_name = self.region_rows_by_name[outbound_region_name].itemReq.replace('*', '') - crafting_level_rule = self.get_skill_rule("crafting", 40) - entrance.access_rule = lambda state: state.has(item_name, self.player) and \ - crafting_level_rule(state) - if self.options.brutal_grinds: - crafting_level_40_regions = { - # can_spin - RegionNames.Sheep, - RegionNames.Spinning_Wheel, - # can_pot - RegionNames.Clay_Rock, - RegionNames.Barbarian_Village, - # can_tan - RegionNames.Milk, - RegionNames.Al_Kharid, - } - else: - mould_access_regions = { - RegionNames.Al_Kharid, - RegionNames.Rimmington, - } - smithing_level_20_regions = { - RegionNames.Bronze_Ores, - RegionNames.Iron_Rock, - RegionNames.Furnace, - RegionNames.Anvil, - } - smithing_level_40_regions = { - *smithing_level_20_regions, - RegionNames.Coal_Rock, - } - crafting_level_40_regions = { - # can_tan - RegionNames.Milk, - RegionNames.Al_Kharid, - # can_silver - RegionNames.Silver_Rock, - RegionNames.Furnace, - *mould_access_regions, - # can_smelt_silver - *smithing_level_20_regions, - # can_gold - RegionNames.Gold_Rock, - RegionNames.Furnace, - *mould_access_regions, - # can_smelt_gold - *smithing_level_40_regions, - } - for region_name in crafting_level_40_regions: - self.multiworld.register_indirect_condition(self.get_region(region_name), entrance) - return - if outbound_region_name == RegionNames.Corsair_Cove: - item_name = self.region_rows_by_name[outbound_region_name].itemReq.replace('*', '') - # Need to be able to start Corsair Curse in addition to having the item - entrance.access_rule = lambda state: state.has(item_name, self.player) and \ - state.can_reach(RegionNames.Falador_Farm, "Region", self.player) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Falador_Farm, self.player), entrance) - - return - if outbound_region_name == "Camdozaal*": - item_name = self.region_rows_by_name[outbound_region_name.replace('*', '')].itemReq - entrance.access_rule = lambda state: state.has(item_name, self.player) and \ - state.has(ItemNames.QP_Below_Ice_Mountain, self.player) - return - if region_row.name == "Dwarven Mountain Pass" and outbound_region_name == "Anvil*": - entrance.access_rule = lambda state: state.has(ItemNames.QP_Dorics_Quest, self.player) - return - # Special logic for canoes - canoe_regions = [RegionNames.Lumbridge, RegionNames.South_Of_Varrock, RegionNames.Barbarian_Village, - RegionNames.Edgeville, RegionNames.Wilderness] - if region_row.name in canoe_regions: - # Skill rules for greater distances - woodcutting_rule_d1 = self.get_skill_rule("woodcutting", 12) - woodcutting_rule_d2 = self.get_skill_rule("woodcutting", 27) - woodcutting_rule_d3 = self.get_skill_rule("woodcutting", 42) - woodcutting_rule_all = self.get_skill_rule("woodcutting", 57) - - def add_indirect_conditions_for_woodcutting_levels(entrance, *levels: int): - if self.options.brutal_grinds: - # No access to specific regions required. - return - # Currently, each level requirement requires everything from the previous level requirements, so the - # maximum level requirement can be taken. - max_level = max(levels, default=0) - max_level = min(max_level, self.options.max_woodcutting_level.value) - if 15 <= max_level < 30: - self.multiworld.register_indirect_condition(self.get_region(RegionNames.Oak_Tree), entrance) - elif 30 <= max_level: - self.multiworld.register_indirect_condition(self.get_region(RegionNames.Oak_Tree), entrance) - self.multiworld.register_indirect_condition(self.get_region(RegionNames.Willow_Tree), entrance) - - if region_row.name == RegionNames.Lumbridge: - # Canoe Tree access for the Location - if outbound_region_name == RegionNames.Canoe_Tree: - entrance.access_rule = \ - lambda state: (state.can_reach_region(RegionNames.South_Of_Varrock, self.player) - and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ - (state.can_reach_region(RegionNames.Barbarian_Village) - and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \ - (state.can_reach_region(RegionNames.Edgeville) - and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) or \ - (state.can_reach_region(RegionNames.Wilderness) - and woodcutting_rule_all(state) and self.options.max_woodcutting_level >= 57) - add_indirect_conditions_for_woodcutting_levels(entrance, 12, 27, 42, 57) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance) - # Access to other chunks based on woodcutting settings - # South of Varrock does not need to be checked, because it's already adjacent - if outbound_region_name == RegionNames.Barbarian_Village: - entrance.access_rule = lambda state: woodcutting_rule_d2(state) \ - and self.options.max_woodcutting_level >= 27 - add_indirect_conditions_for_woodcutting_levels(entrance, 27) - if outbound_region_name == RegionNames.Edgeville: - entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ - and self.options.max_woodcutting_level >= 42 - add_indirect_conditions_for_woodcutting_levels(entrance, 42) - if outbound_region_name == RegionNames.Wilderness: - entrance.access_rule = lambda state: woodcutting_rule_all(state) \ - and self.options.max_woodcutting_level >= 57 - add_indirect_conditions_for_woodcutting_levels(entrance, 57) - - if region_row.name == RegionNames.South_Of_Varrock: - if outbound_region_name == RegionNames.Canoe_Tree: - entrance.access_rule = \ - lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player) - and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ - (state.can_reach_region(RegionNames.Barbarian_Village) - and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ - (state.can_reach_region(RegionNames.Edgeville) - and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \ - (state.can_reach_region(RegionNames.Wilderness) - and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) - add_indirect_conditions_for_woodcutting_levels(entrance, 12, 27, 42) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance) - # Access to other chunks based on woodcutting settings - # Lumbridge does not need to be checked, because it's already adjacent - if outbound_region_name == RegionNames.Barbarian_Village: - entrance.access_rule = lambda state: woodcutting_rule_d1(state) \ - and self.options.max_woodcutting_level >= 12 - add_indirect_conditions_for_woodcutting_levels(entrance, 12) - if outbound_region_name == RegionNames.Edgeville: - entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ - and self.options.max_woodcutting_level >= 27 - add_indirect_conditions_for_woodcutting_levels(entrance, 27) - if outbound_region_name == RegionNames.Wilderness: - entrance.access_rule = lambda state: woodcutting_rule_all(state) \ - and self.options.max_woodcutting_level >= 42 - add_indirect_conditions_for_woodcutting_levels(entrance, 42) - if region_row.name == RegionNames.Barbarian_Village: - if outbound_region_name == RegionNames.Canoe_Tree: - entrance.access_rule = \ - lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player) - and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \ - (state.can_reach_region(RegionNames.South_Of_Varrock) - and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ - (state.can_reach_region(RegionNames.Edgeville) - and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ - (state.can_reach_region(RegionNames.Wilderness) - and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) - add_indirect_conditions_for_woodcutting_levels(entrance, 12, 27) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance) - # Access to other chunks based on woodcutting settings - if outbound_region_name == RegionNames.Lumbridge: - entrance.access_rule = lambda state: woodcutting_rule_d2(state) \ - and self.options.max_woodcutting_level >= 27 - add_indirect_conditions_for_woodcutting_levels(entrance, 27) - if outbound_region_name == RegionNames.South_Of_Varrock: - entrance.access_rule = lambda state: woodcutting_rule_d1(state) \ - and self.options.max_woodcutting_level >= 12 - add_indirect_conditions_for_woodcutting_levels(entrance, 12) - # Edgeville does not need to be checked, because it's already adjacent - if outbound_region_name == RegionNames.Wilderness: - entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ - and self.options.max_woodcutting_level >= 42 - add_indirect_conditions_for_woodcutting_levels(entrance, 42) - if region_row.name == RegionNames.Edgeville: - if outbound_region_name == RegionNames.Canoe_Tree: - entrance.access_rule = \ - lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player) - and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) or \ - (state.can_reach_region(RegionNames.South_Of_Varrock) - and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \ - (state.can_reach_region(RegionNames.Barbarian_Village) - and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ - (state.can_reach_region(RegionNames.Wilderness) - and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) - add_indirect_conditions_for_woodcutting_levels(entrance, 12, 27, 42) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance) - # Access to other chunks based on woodcutting settings - if outbound_region_name == RegionNames.Lumbridge: - entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ - and self.options.max_woodcutting_level >= 42 - add_indirect_conditions_for_woodcutting_levels(entrance, 42) - if outbound_region_name == RegionNames.South_Of_Varrock: - entrance.access_rule = lambda state: woodcutting_rule_d2(state) \ - and self.options.max_woodcutting_level >= 27 - add_indirect_conditions_for_woodcutting_levels(entrance, 27) - # Barbarian Village does not need to be checked, because it's already adjacent - # Wilderness does not need to be checked, because it's already adjacent - if region_row.name == RegionNames.Wilderness: - if outbound_region_name == RegionNames.Canoe_Tree: - entrance.access_rule = \ - lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player) - and woodcutting_rule_all(state) and self.options.max_woodcutting_level >= 57) or \ - (state.can_reach_region(RegionNames.South_Of_Varrock) - and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) or \ - (state.can_reach_region(RegionNames.Barbarian_Village) - and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \ - (state.can_reach_region(RegionNames.Edgeville) - and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) - add_indirect_conditions_for_woodcutting_levels(entrance, 12, 27, 42, 57) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance) - # Access to other chunks based on woodcutting settings - if outbound_region_name == RegionNames.Lumbridge: - entrance.access_rule = lambda state: woodcutting_rule_all(state) \ - and self.options.max_woodcutting_level >= 57 - add_indirect_conditions_for_woodcutting_levels(entrance, 57) - if outbound_region_name == RegionNames.South_Of_Varrock: - entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ - and self.options.max_woodcutting_level >= 42 - add_indirect_conditions_for_woodcutting_levels(entrance, 42) - if outbound_region_name == RegionNames.Barbarian_Village: - entrance.access_rule = lambda state: woodcutting_rule_d2(state) \ - and self.options.max_woodcutting_level >= 27 - add_indirect_conditions_for_woodcutting_levels(entrance, 27) - # Edgeville does not need to be checked, because it's already adjacent + def task_within_skill_levels(self, skills_required): + # Loop through each required skill. If any of its requirements are out of the defined limit, return false + for skill in skills_required: + max_level_for_skill = getattr(self.options, f"max_{skill.skill.lower()}_level") + if skill.level > max_level_for_skill: + return False + return True def roll_locations(self): - locations_required = 0 generation_is_fake = hasattr(self.multiworld, "generation_is_fake") # UT specific override + locations_required = 0 for item_row in item_rows: locations_required += item_row.amount locations_added = 1 # At this point we've already added the starting area, so we start at 1 instead of 0 - # Quests are always added + # Quests are always added first, before anything else is rolled for i, location_row in enumerate(location_rows): if location_row.category in {"quest", "points", "goal"}: - self.create_and_add_location(i) - if location_row.category == "quest": - locations_added += 1 + if self.task_within_skill_levels(location_row.skills): + self.create_and_add_location(i) + if location_row.category == "quest": + locations_added += 1 # Build up the weighted Task Pool rnd = self.random @@ -516,10 +229,9 @@ def roll_locations(self): task_types = ["prayer", "magic", "runecraft", "mining", "crafting", "smithing", "fishing", "cooking", "firemaking", "woodcutting", "combat"] for task_type in task_types: - max_level_for_task_type = getattr(self.options, f"max_{task_type}_level") max_amount_for_task_type = getattr(self.options, f"max_{task_type}_tasks") tasks_for_this_type = [task for task in self.locations_by_category[task_type] - if task.skills[0].level <= max_level_for_task_type] + if self.task_within_skill_levels(task.skills)] if not self.options.progressive_tasks: rnd.shuffle(tasks_for_this_type) else: @@ -568,6 +280,7 @@ def roll_locations(self): self.add_location(task) locations_added += 1 + def add_location(self, location): index = [i for i in range(len(location_rows)) if location_rows[i].name == location.name][0] self.create_and_add_location(index) @@ -586,11 +299,15 @@ def get_filler_item_name(self) -> str: def create_and_add_location(self, row_index) -> None: location_row = location_rows[row_index] - # print(f"Adding task {location_row.name}") + + # Quest Points are handled differently now, but in case this gets fed an older version of the data sheet, + # the points might still be listed in a different row + if location_row.category == "points": + return # Create Location location_id = self.base_id + row_index - if location_row.category == "points" or location_row.category == "goal": + if location_row.category == "goal": location_id = None location = OSRSLocation(self.player, location_row.name, location_id) self.location_name_to_data[location_row.name] = location @@ -602,6 +319,14 @@ def create_and_add_location(self, row_index) -> None: location.parent_region = region region.locations.append(location) + # If it's a quest, generate a "Points" location we'll add an event to + if location_row.category == "quest": + points_name = location_row.name.replace("Quest:", "Points:") + points_location = OSRSLocation(self.player, points_name) + self.location_name_to_data[points_name] = points_location + points_location.parent_region = region + region.locations.append(points_location) + def set_rules(self) -> None: """ called to set access and item rules on locations and entrances. @@ -612,18 +337,26 @@ def set_rules(self) -> None: "Witchs_Potion", "Knights_Sword", "Goblin_Diplomacy", "Pirates_Treasure", "Rune_Mysteries", "Misthalin_Mystery", "Corsair_Curse", "X_Marks_the_Spot", "Below_Ice_Mountain"] - for qp_attr_name in quest_attr_names: - loc_name = getattr(LocationNames, f"QP_{qp_attr_name}") - item_name = getattr(ItemNames, f"QP_{qp_attr_name}") - self.multiworld.get_location(loc_name, self.player) \ - .place_locked_item(self.create_event(item_name)) for quest_attr_name in quest_attr_names: qp_loc_name = getattr(LocationNames, f"QP_{quest_attr_name}") + qp_loc = self.location_name_to_data.get(qp_loc_name) + q_loc_name = getattr(LocationNames, f"Q_{quest_attr_name}") - add_rule(self.multiworld.get_location(qp_loc_name, self.player), lambda state, q_loc_name=q_loc_name: ( - self.multiworld.get_location(q_loc_name, self.player).can_reach(state) - )) + q_loc = self.location_name_to_data.get(q_loc_name) + + # Checks to make sure the task is actually in the list before trying to create its rules + if qp_loc and q_loc: + # Create the QP Event Item + item_name = getattr(ItemNames, f"QP_{quest_attr_name}") + qp_loc.place_locked_item(self.create_event(item_name)) + + # If a quest is excluded, don't actually consider it for quest point progression + if q_loc_name not in self.options.exclude_locations: + self.available_QP_locations.append(item_name) + + # Set the access rule for the QP Location + add_rule(qp_loc, lambda state, loc=q_loc: (loc.can_reach(state))) # place "Victory" at "Dragon Slayer" and set collection as win condition self.multiworld.get_location(LocationNames.Q_Dragon_Slayer, self.player) \ @@ -639,7 +372,7 @@ def set_rules(self) -> None: lambda state, region_required=region_required: state.can_reach(region_required, "Region", self.player)) for skill_req in location_row.skills: - add_rule(location, self.get_skill_rule(skill_req.skill, skill_req.level)) + add_rule(location, get_skill_rule(skill_req.skill, skill_req.level, self.player, self.options)) for item_req in location_row.items: add_rule(location, lambda state, item_req=item_req: state.has(item_req, self.player)) if location_row.qp: @@ -664,124 +397,8 @@ def create_event(self, event: str): def quest_points(self, state): qp = 0 - for qp_event in QP_Items: + for qp_event in self.available_QP_locations: if state.has(qp_event, self.player): qp += int(qp_event[0]) return qp - """ - Ensures a target level can be reached with available resources - """ - - def get_skill_rule(self, skill, level) -> CollectionRule: - if skill.lower() == "fishing": - if self.options.brutal_grinds or level < 5: - return lambda state: state.can_reach(RegionNames.Shrimp, "Region", self.player) - if level < 20: - return lambda state: state.can_reach(RegionNames.Shrimp, "Region", self.player) and \ - state.can_reach(RegionNames.Port_Sarim, "Region", self.player) - else: - return lambda state: state.can_reach(RegionNames.Shrimp, "Region", self.player) and \ - state.can_reach(RegionNames.Port_Sarim, "Region", self.player) and \ - state.can_reach(RegionNames.Fly_Fish, "Region", self.player) - if skill.lower() == "mining": - if self.options.brutal_grinds or level < 15: - return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) or \ - state.can_reach(RegionNames.Clay_Rock, "Region", self.player) - else: - # Iron is the best way to train all the way to 99, so having access to iron is all you need to check for - return lambda state: (state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) or - state.can_reach(RegionNames.Clay_Rock, "Region", self.player)) and \ - state.can_reach(RegionNames.Iron_Rock, "Region", self.player) - if skill.lower() == "woodcutting": - if self.options.brutal_grinds or level < 15: - # I've checked. There is not a single chunk in the f2p that does not have at least one normal tree. - # Even the desert. - return lambda state: True - if level < 30: - return lambda state: state.can_reach(RegionNames.Oak_Tree, "Region", self.player) - else: - return lambda state: state.can_reach(RegionNames.Oak_Tree, "Region", self.player) and \ - state.can_reach(RegionNames.Willow_Tree, "Region", self.player) - if skill.lower() == "smithing": - if self.options.brutal_grinds: - return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \ - state.can_reach(RegionNames.Furnace, "Region", self.player) - if level < 15: - # Lumbridge has a special bronze-only anvil. This is the only anvil of its type so it's not included - # in the "Anvil" resource region. We still need to check for it though. - return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \ - state.can_reach(RegionNames.Furnace, "Region", self.player) and \ - (state.can_reach(RegionNames.Anvil, "Region", self.player) or - state.can_reach(RegionNames.Lumbridge, "Region", self.player)) - if level < 30: - # For levels between 15 and 30, the lumbridge anvil won't cut it. Only a real one will do - return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \ - state.can_reach(RegionNames.Iron_Rock, "Region", self.player) and \ - state.can_reach(RegionNames.Furnace, "Region", self.player) and \ - state.can_reach(RegionNames.Anvil, "Region", self.player) - else: - return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \ - state.can_reach(RegionNames.Iron_Rock, "Region", self.player) and \ - state.can_reach(RegionNames.Coal_Rock, "Region", self.player) and \ - state.can_reach(RegionNames.Furnace, "Region", self.player) and \ - state.can_reach(RegionNames.Anvil, "Region", self.player) - if skill.lower() == "crafting": - # Crafting is really complex. Need a lot of sub-rules to make this even remotely readable - def can_spin(state): - return state.can_reach(RegionNames.Sheep, "Region", self.player) and \ - state.can_reach(RegionNames.Spinning_Wheel, "Region", self.player) - - def can_pot(state): - return state.can_reach(RegionNames.Clay_Rock, "Region", self.player) and \ - state.can_reach(RegionNames.Barbarian_Village, "Region", self.player) - - def can_tan(state): - return state.can_reach(RegionNames.Milk, "Region", self.player) and \ - state.can_reach(RegionNames.Al_Kharid, "Region", self.player) - - def mould_access(state): - return state.can_reach(RegionNames.Al_Kharid, "Region", self.player) or \ - state.can_reach(RegionNames.Rimmington, "Region", self.player) - - def can_silver(state): - - return state.can_reach(RegionNames.Silver_Rock, "Region", self.player) and \ - state.can_reach(RegionNames.Furnace, "Region", self.player) and mould_access(state) - - def can_gold(state): - return state.can_reach(RegionNames.Gold_Rock, "Region", self.player) and \ - state.can_reach(RegionNames.Furnace, "Region", self.player) and mould_access(state) - - if self.options.brutal_grinds or level < 5: - return lambda state: can_spin(state) or can_pot(state) or can_tan(state) - - can_smelt_gold = self.get_skill_rule("smithing", 40) - can_smelt_silver = self.get_skill_rule("smithing", 20) - if level < 16: - return lambda state: can_pot(state) or can_tan(state) or (can_gold(state) and can_smelt_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 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 \ - state.can_reach(RegionNames.Shrimp, "Region", self.player) or \ - (state.can_reach(RegionNames.Wheat, "Region", self.player) and - state.can_reach(RegionNames.Windmill, "Region", self.player)) - else: - can_catch_fly_fish = self.get_skill_rule("fishing", 20) - return lambda state: state.can_reach(RegionNames.Fly_Fish, "Region", self.player) and \ - can_catch_fly_fish(state) and \ - (state.can_reach(RegionNames.Milk, "Region", self.player) or - state.can_reach(RegionNames.Egg, "Region", self.player) or - state.can_reach(RegionNames.Shrimp, "Region", self.player) or - (state.can_reach(RegionNames.Wheat, "Region", self.player) and - state.can_reach(RegionNames.Windmill, "Region", self.player))) - if skill.lower() == "runecraft": - return lambda state: state.has(ItemNames.QP_Rune_Mysteries, self.player) - if skill.lower() == "magic": - return lambda state: state.can_reach(RegionNames.Mind_Runes, "Region", self.player) - - return lambda state: True From ba50c947bade7ec04a597a1b73b232a7af89da65 Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Fri, 22 Nov 2024 23:13:57 +0000 Subject: [PATCH 08/12] AHiT: Fix reconnecting rift access regions for starting and plando acts (#4200) Reconnecting an act in a telescope to a time rift removes the entrances to the time rift from its access regions because it will be accessible from the telescope instead. By doing so early on, as a starting act with insanity act randomizer or as a plando-ed act, this can happen before the time rift itself has been reconnected to an act or other time rift. In which case, when later attempting to connect that time rift to an act or other time rift, the entrances from the rift access regions will no longer exist, so must be re-created. The original code was mistakenly re-creating the entrances from the time rift being reconnected, instead of from the rift access regions. --- worlds/ahit/Regions.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index c70f08b475eb..31edf1d0b057 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -740,17 +740,20 @@ def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool: def connect_time_rift(world: "HatInTimeWorld", time_rift: Region, exit_region: Region): - i = 1 - while i <= len(rift_access_regions[time_rift.name]): + for i, access_region in enumerate(rift_access_regions[time_rift.name], start=1): + # Matches the naming convention and iteration order in `create_rift_connections()`. name = f"{time_rift.name} Portal - Entrance {i}" entrance: Entrance try: - entrance = world.multiworld.get_entrance(name, world.player) + entrance = world.get_entrance(name) + # Reconnect the rift access region to the new exit region. reconnect_regions(entrance, entrance.parent_region, exit_region) except KeyError: - time_rift.connect(exit_region, name) - - i += 1 + # The original entrance to the time rift has been deleted by already reconnecting a telescope act to the + # time rift, so create a new entrance from the original rift access region to the new exit region. + # Normally, acts and time rifts are sorted such that time rifts are reconnected to acts/rifts first, but + # starting acts/rifts and act-plando can reconnect acts to time rifts before this happens. + world.get_region(access_region).connect(exit_region, name) def get_shuffleable_act_regions(world: "HatInTimeWorld") -> List[Region]: From 5729b785040e3b6f3d12444c934d988dfbbfcc71 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Fri, 22 Nov 2024 19:42:44 -0500 Subject: [PATCH 09/12] TUNIC: Fix it so item linked locations are correct in slot data (#4105) * Fix it so item linked locations are correct in slot data * List -> Set * Cache the locations instead * Just loop the multiworld once * Move it all to fill slot data and pretend we're doing a stage * Move groups up so it doesn't loop over the multiworld locations if no item links are present * Update worlds/tunic/__init__.py Co-authored-by: Mysteryem --------- Co-authored-by: Mysteryem --- worlds/tunic/__init__.py | 43 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 6360ba493853..5a2847278bd5 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -83,6 +83,11 @@ class TunicWorld(World): shop_num: int = 1 # need to make it so that you can walk out of shops, but also that they aren't all connected er_regions: Dict[str, RegionInfo] # absolutely needed so outlet regions work + # so we only loop the multiworld locations once + # if these are locations instead of their info, it gives a memory leak error + item_link_locations: Dict[int, Dict[str, List[Tuple[int, str]]]] = {} + player_item_link_locations: Dict[str, List[Location]] + def generate_early(self) -> None: if self.options.logic_rules >= LogicRules.option_no_major_glitches: self.options.laurels_zips.value = LaurelsZips.option_true @@ -387,6 +392,18 @@ def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None: if hint_text: hint_data[self.player][location.address] = hint_text + def get_real_location(self, location: Location) -> Tuple[str, int]: + # if it's not in a group, it's not in an item link + if location.player not in self.multiworld.groups or not location.item: + return location.name, location.player + try: + loc = self.player_item_link_locations[location.item.name].pop() + return loc.name, loc.player + except IndexError: + warning(f"TUNIC: Failed to parse item location for in-game hints for {self.player_name}. " + f"Using a potentially incorrect location name instead.") + return location.name, location.player + def fill_slot_data(self) -> Dict[str, Any]: slot_data: Dict[str, Any] = { "seed": self.random.randint(0, 2147483647), @@ -412,12 +429,34 @@ def fill_slot_data(self) -> Dict[str, Any]: "disable_local_spoiler": int(self.settings.disable_local_spoiler or self.multiworld.is_race), } + # this would be in a stage if there was an appropriate stage for it + self.player_item_link_locations = {} + groups = self.multiworld.get_player_groups(self.player) + if groups: + if not self.item_link_locations: + tunic_worlds: Tuple[TunicWorld] = self.multiworld.get_game_worlds("TUNIC") + # figure out our groups and the items in them + for tunic in tunic_worlds: + for group in self.multiworld.get_player_groups(tunic.player): + self.item_link_locations.setdefault(group, {}) + for location in self.multiworld.get_locations(): + if location.item and location.item.player in self.item_link_locations.keys(): + (self.item_link_locations[location.item.player].setdefault(location.item.name, []) + .append((location.player, location.name))) + + # if item links are on, set up the player's personal item link locations, so we can pop them as needed + for group, item_links in self.item_link_locations.items(): + if group in groups: + for item_name, locs in item_links.items(): + self.player_item_link_locations[item_name] = \ + [self.multiworld.get_location(location_name, player) for player, location_name in locs] + for tunic_item in filter(lambda item: item.location is not None and item.code is not None, self.slot_data_items): if tunic_item.name not in slot_data: slot_data[tunic_item.name] = [] if tunic_item.name == gold_hexagon and len(slot_data[gold_hexagon]) >= 6: continue - slot_data[tunic_item.name].extend([tunic_item.location.name, tunic_item.location.player]) + slot_data[tunic_item.name].extend(self.get_real_location(tunic_item.location)) for start_item in self.options.start_inventory_from_pool: if start_item in slot_data_item_names: @@ -436,7 +475,7 @@ def fill_slot_data(self) -> Dict[str, Any]: if item in slot_data_item_names: slot_data[item] = [] for item_location in self.multiworld.find_item_locations(item, self.player): - slot_data[item].extend([item_location.name, item_location.player]) + slot_data[item].extend(self.get_real_location(item_location)) return slot_data From 03b90cf39ba1d10952545c44fee91fd874837124 Mon Sep 17 00:00:00 2001 From: Jarno Date: Sun, 24 Nov 2024 15:57:39 +0100 Subject: [PATCH 10/12] =?UTF-8?q?Timespinner:=20Re-added=20missing=20enmem?= =?UTF-8?q?y=20rando=20option=C2=A0#4235?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- worlds/timespinner/Options.py | 6 ++++++ worlds/timespinner/__init__.py | 1 + 2 files changed, 7 insertions(+) diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py index f6a3dba3e311..c06dd36797fd 100644 --- a/worlds/timespinner/Options.py +++ b/worlds/timespinner/Options.py @@ -379,6 +379,7 @@ class TimespinnerOptions(PerGameCommonOptions, DeathLinkMixin): cantoran: Cantoran lore_checks: LoreChecks boss_rando: BossRando + enemy_rando: EnemyRando damage_rando: DamageRando damage_rando_overrides: DamageRandoOverrides hp_cap: HpCap @@ -445,6 +446,7 @@ class BackwardsCompatiableTimespinnerOptions(TimespinnerOptions): Cantoran: hidden(Cantoran) # type: ignore LoreChecks: hidden(LoreChecks) # type: ignore BossRando: hidden(BossRando) # type: ignore + EnemyRando: hidden(EnemyRando) # type: ignore DamageRando: hidden(DamageRando) # type: ignore DamageRandoOverrides: HiddenDamageRandoOverrides HpCap: hidden(HpCap) # type: ignore @@ -516,6 +518,10 @@ def handle_backward_compatibility(self) -> None: self.boss_rando == BossRando.default: self.boss_rando.value = self.BossRando.value self.has_replaced_options.value = Toggle.option_true + if self.EnemyRando != EnemyRando.default and \ + self.enemy_rando == EnemyRando.default: + self.enemy_rando.value = self.EnemyRando.value + self.has_replaced_options.value = Toggle.option_true if self.DamageRando != DamageRando.default and \ self.damage_rando == DamageRando.default: self.damage_rando.value = self.DamageRando.value diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index f241d4468162..72903bd5ffea 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -98,6 +98,7 @@ def fill_slot_data(self) -> Dict[str, object]: "Cantoran": self.options.cantoran.value, "LoreChecks": self.options.lore_checks.value, "BossRando": self.options.boss_rando.value, + "EnemyRando": self.options.enemy_rando.value, "DamageRando": self.options.damage_rando.value, "DamageRandoOverrides": self.options.damage_rando_overrides.value, "HpCap": self.options.hp_cap.value, From 36f17111bf5a9cd1027065422098e7b677640860 Mon Sep 17 00:00:00 2001 From: gaithern <36639398+gaithern@users.noreply.github.com> Date: Sun, 24 Nov 2024 11:42:21 -0600 Subject: [PATCH 11/12] Kingdom Hearts: Minor Logic Fixes (#4236) * Update Rules.py * Update worlds/kh1/Rules.py Co-authored-by: Scipio Wright * Update worlds/kh1/Rules.py Co-authored-by: Scipio Wright --------- Co-authored-by: Scipio Wright --- worlds/kh1/Rules.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/worlds/kh1/Rules.py b/worlds/kh1/Rules.py index e1f72f5b3e54..130238e5048e 100644 --- a/worlds/kh1/Rules.py +++ b/worlds/kh1/Rules.py @@ -235,6 +235,11 @@ def set_rules(kh1world): lambda state: ( state.has("Progressive Glide", player) or + ( + state.has("High Jump", player, 2) + and state.has("Footprints", player) + ) + or ( options.advanced_logic and state.has_all({ @@ -246,6 +251,11 @@ def set_rules(kh1world): lambda state: ( state.has("Progressive Glide", player) or + ( + state.has("High Jump", player, 2) + and state.has("Footprints", player) + ) + or ( options.advanced_logic and state.has_all({ @@ -258,7 +268,6 @@ def set_rules(kh1world): state.has("Footprints", player) or (options.advanced_logic and state.has("Progressive Glide", player)) - or state.has("High Jump", player, 2) )) add_rule(kh1world.get_location("Wonderland Tea Party Garden Across From Bizarre Room Entrance Chest"), lambda state: ( @@ -376,7 +385,7 @@ def set_rules(kh1world): lambda state: state.has("White Trinity", player)) add_rule(kh1world.get_location("Monstro Chamber 6 Other Platform Chest"), lambda state: ( - state.has("High Jump", player) + state.has_all(("High Jump", "Progressive Glide"), player) or (options.advanced_logic and state.has("Combo Master", player)) )) add_rule(kh1world.get_location("Monstro Chamber 6 Platform Near Chamber 5 Entrance Chest"), @@ -386,7 +395,7 @@ def set_rules(kh1world): )) add_rule(kh1world.get_location("Monstro Chamber 6 Raised Area Near Chamber 1 Entrance Chest"), lambda state: ( - state.has("High Jump", player) + state.has_all(("High Jump", "Progressive Glide"), player) or (options.advanced_logic and state.has("Combo Master", player)) )) add_rule(kh1world.get_location("Halloween Town Moonlight Hill White Trinity Chest"), @@ -595,6 +604,7 @@ def set_rules(kh1world): lambda state: ( state.has("Green Trinity", player) and has_all_magic_lvx(state, player, 2) + and has_defensive_tools(state, player) )) add_rule(kh1world.get_location("Neverland Hold Flight 2nd Chest"), lambda state: ( @@ -710,8 +720,7 @@ def set_rules(kh1world): lambda state: state.has("White Trinity", player)) add_rule(kh1world.get_location("End of the World Giant Crevasse 5th Chest"), lambda state: ( - state.has("High Jump", player) - or state.has("Progressive Glide", player) + state.has("Progressive Glide", player) )) add_rule(kh1world.get_location("End of the World Giant Crevasse 1st Chest"), lambda state: ( @@ -1441,10 +1450,11 @@ def set_rules(kh1world): has_emblems(state, player, options.keyblades_unlock_chests) and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) and has_defensive_tools(state, player) + and state.has("Progressive Blizzard", player, 3) )) add_rule(kh1world.get_location("Agrabah Defeat Kurt Zisa Zantetsuken Event"), lambda state: ( - has_emblems(state, player, options.keyblades_unlock_chests) and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) and has_defensive_tools(state, player) + has_emblems(state, player, options.keyblades_unlock_chests) and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) and has_defensive_tools(state, player) and state.has("Progressive Blizzard", player, 3) )) if options.super_bosses or options.goal.current_key == "sephiroth": add_rule(kh1world.get_location("Olympus Coliseum Defeat Sephiroth Ansem's Report 12"), From a650e90b576bb5ad791e9b6fb7c036cfe747ec97 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Sun, 24 Nov 2024 12:43:28 -0500 Subject: [PATCH 12/12] =?UTF-8?q?TUNIC:=20Add=20clarifying=20comment=20to?= =?UTF-8?q?=20item=20links=20handling=C2=A0#4233?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- worlds/tunic/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 5a2847278bd5..d1430aac1895 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -432,6 +432,7 @@ def fill_slot_data(self) -> Dict[str, Any]: # this would be in a stage if there was an appropriate stage for it self.player_item_link_locations = {} groups = self.multiworld.get_player_groups(self.player) + # checking if groups so that this doesn't run if the player isn't in a group if groups: if not self.item_link_locations: tunic_worlds: Tuple[TunicWorld] = self.multiworld.get_game_worlds("TUNIC")