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/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] 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() 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: 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) 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}' " 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]: 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 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"), 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 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, diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index fe1c7c16a19f..ad7fc1a0de86 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -85,6 +85,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 @@ -415,6 +420,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), @@ -441,12 +458,35 @@ 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) + # 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") + # 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: @@ -465,7 +505,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