From e5c9b8ad0c8f6d6866dfb3be78952c77b666cffd Mon Sep 17 00:00:00 2001 From: CookieCat <81494827+CookieCat45@users.noreply.github.com> Date: Sat, 27 Jul 2024 13:16:52 -0400 Subject: [PATCH 01/13] AHIT: Generation error fixes and some other bug fixes (#3663) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * duh * Fuck it * Major fixes * a * b * Even more fixes * New option - NoFreeRoamFinale * a * Hat Logic Fix * Just to be safe * multiworld.random to world.random * KeyError fix * Update .gitignore * Update __init__.py * Zoinks Scoob * ffs * Ruh Roh Raggy, more r-r-r-random bugs! * 0.9b - cleanup + expanded logic difficulty * Update Rules.py * Update Regions.py * AttributeError fix * 0.10b - New Options * 1.0 Preparations * Docs * Docs 2 * Fixes * Update __init__.py * Fixes * variable capture my beloathed * Fixes * a * 10 Seconds logic fix * 1.1 * 1.2 * a * New client * More client changes * 1.3 * Final touch-ups for 1.3 * 1.3.1 * 1.3.3 * Zero Jumps gen error fix * more fixes * Formatting improvements * typo * Update __init__.py * Revert "Update __init__.py" This reverts commit e178a7c0a6904ace803241cab3021d7b97177e90. * init * Update to new options API * Missed some * Snatcher Coins fix * Missed some more * some slight touch ups * rewind * a * fix things * Revert "Merge branch 'main' of https://github.com/CookieCat45/Archipelago-ahit" This reverts commit a2360fe197e77a723bb70006c5eb5725c7ed3826, reversing changes made to b8948bc4958855c6e342e18bdb8dc81cfcf09455. * Update .gitignore * 1.3.6 * Final touch-ups * Fix client and leftover old options api * Delete setup-ahitclient.py * Update .gitignore * old python version fix * proper warnings for invalid act plandos * Update worlds/ahit/docs/en_A Hat in Time.md Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com> * Update worlds/ahit/docs/setup_en.md Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com> * 120 char per line * "settings" to "options" * Update DeathWishRules.py * Update worlds/ahit/docs/en_A Hat in Time.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * No more loading the data package * cleanup + act plando fixes * almost forgot * Update Rules.py * a * Update worlds/ahit/Options.py Co-authored-by: Ixrec * Options stuff * oop * no unnecessary type hints * warn about depot download length in setup guide * Update worlds/ahit/Options.py Co-authored-by: Ixrec * typo Co-authored-by: Ixrec * Update worlds/ahit/Rules.py Co-authored-by: Ixrec * review stuff * More stuff from review * comment * 1.5 Update * link fix? * link fix 2 * Update setup_en.md * Update setup_en.md * Update setup_en.md * Evil * Good fucking lord * Review stuff again + Logic fixes * More review stuff * Even more review stuff - we're almost done * DW review stuff * Finish up review stuff * remove leftover stuff * a * assert item * add A Hat in Time to readme/codeowners files * Fix range options not being corrected properly * 120 chars per line in docs * Update worlds/ahit/Regions.py Co-authored-by: Aaron Wagener * Update worlds/ahit/DeathWishLocations.py Co-authored-by: Aaron Wagener * Remove some unnecessary option.class.value * Remove data_version and more option.class.value * Update worlds/ahit/Items.py Co-authored-by: Aaron Wagener * Remove the rest of option.class.value * Update worlds/ahit/DeathWishLocations.py Co-authored-by: Aaron Wagener * review stuff * Replace connect_regions with Region.connect * review stuff * Remove unnecessary Optional from LocData * Remove HatType.NONE * Update worlds/ahit/test/TestActs.py Co-authored-by: Aaron Wagener * fix so default tests actually don't run * Improve performance for death wish rules * rename test file * change test imports * 1000 is probably unnecessary * a * change state.count to state.has * stuff * starting inventory hats fix * shouldn't have done this lol * make ship shape task goal equal to number of tasksanity checks if set to 0 * a * change act shuffle starting acts + logic updates * dumb * option groups + lambda capture cringe + typo * a * b * missing option in groups * c * Fix Your Contract Has Expired being placed on first level when it shouldn't * yche fix * formatting * major logic bug fix for death wish * Update Regions.py * Add missing indirect connections * Fix generation error from chapter 2 start with act shuffle off * a * Revert "a" This reverts commit df58bbcd998585760cc6ac9ea54b6fdf142b4fd1. * Revert "Fix generation error from chapter 2 start with act shuffle off" This reverts commit 0f4d441824af34bf7a7cff19f5f14161752d8661. * bunch of fixes * Update Regions.py * Update __init__.py * Update __init__.py * Update __init__.py * Update Regions.py * Update worlds/ahit/__init__.py Co-authored-by: Aaron Wagener * Update __init__.py * Update __init__.py --------- Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com> Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Co-authored-by: Ixrec Co-authored-by: Aaron Wagener Co-authored-by: Fabian Dill --- worlds/ahit/Items.py | 2 +- worlds/ahit/Regions.py | 12 +++++++++-- worlds/ahit/Rules.py | 16 ++++++--------- worlds/ahit/__init__.py | 44 ++++++++++++++++++++++++++--------------- 4 files changed, 45 insertions(+), 29 deletions(-) diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index 3ef83fe81e..54c6e6b5d3 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -39,7 +39,7 @@ def create_itempool(world: "HatInTimeWorld") -> List[Item]: continue else: if name == "Scooter Badge": - if world.options.CTRLogic is CTRLogic.option_scooter or get_difficulty(world) >= Difficulty.MODERATE: + if world.options.CTRLogic == CTRLogic.option_scooter or get_difficulty(world) >= Difficulty.MODERATE: item_type = ItemClassification.progression elif name == "No Bonk Badge" and world.is_dw(): item_type = ItemClassification.progression diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index c6aeaa3577..8cb3782bde 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -659,6 +659,10 @@ def is_valid_act_combo(world: "HatInTimeWorld", entrance_act: Region, if exit_act.name not in chapter_finales: return False + exit_chapter: str = act_chapters.get(exit_act.name) + # make sure that certain time rift combinations never happen + always_block: bool = exit_chapter != "Mafia Town" and exit_chapter != "Subcon Forest" + if not ignore_certain_rules or always_block: if entrance_act.name in rift_access_regions and exit_act.name in rift_access_regions[entrance_act.name]: return False @@ -684,9 +688,12 @@ def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool: if act.name not in guaranteed_first_acts: return False + if world.options.ActRandomizer == ActRandomizer.option_light and "Time Rift" in act.name: + return False + # If there's only a single level in the starting chapter, only allow Mafia Town or Subcon Forest levels start_chapter = world.options.StartingChapter - if start_chapter is ChapterIndex.ALPINE or start_chapter is ChapterIndex.SUBCON: + if start_chapter == ChapterIndex.ALPINE or start_chapter == ChapterIndex.SUBCON: if "Time Rift" in act.name: return False @@ -723,7 +730,8 @@ def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool: elif act.name == "Contractual Obligations" and world.options.ShuffleSubconPaintings: return False - if world.options.ShuffleSubconPaintings and act_chapters.get(act.name, "") == "Subcon Forest": + if world.options.ShuffleSubconPaintings and "Time Rift" not in act.name \ + and act_chapters.get(act.name, "") == "Subcon Forest": # Only allow Subcon levels if painting skips are allowed if diff < Difficulty.MODERATE or world.options.NoPaintingSkips: return False diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index b0513c4332..b716b793a7 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -1,7 +1,6 @@ from worlds.AutoWorld import CollectionState from worlds.generic.Rules import add_rule, set_rule -from .Locations import location_table, zipline_unlocks, is_location_valid, contract_locations, \ - shop_locations, event_locs +from .Locations import location_table, zipline_unlocks, is_location_valid, shop_locations, event_locs from .Types import HatType, ChapterIndex, hat_type_to_item, Difficulty, HitType from BaseClasses import Location, Entrance, Region from typing import TYPE_CHECKING, List, Callable, Union, Dict @@ -148,14 +147,14 @@ def set_rules(world: "HatInTimeWorld"): if world.is_dlc1(): chapter_list.append(ChapterIndex.CRUISE) - if world.is_dlc2() and final_chapter is not ChapterIndex.METRO: + if world.is_dlc2() and final_chapter != ChapterIndex.METRO: chapter_list.append(ChapterIndex.METRO) chapter_list.remove(starting_chapter) world.random.shuffle(chapter_list) # Make sure Alpine is unlocked before any DLC chapters are, as the Alpine door needs to be open to access them - if starting_chapter is not ChapterIndex.ALPINE and (world.is_dlc1() or world.is_dlc2()): + if starting_chapter != ChapterIndex.ALPINE and (world.is_dlc1() or world.is_dlc2()): index1 = 69 index2 = 69 pos: int @@ -165,7 +164,7 @@ def set_rules(world: "HatInTimeWorld"): if world.is_dlc1(): index1 = chapter_list.index(ChapterIndex.CRUISE) - if world.is_dlc2() and final_chapter is not ChapterIndex.METRO: + if world.is_dlc2() and final_chapter != ChapterIndex.METRO: index2 = chapter_list.index(ChapterIndex.METRO) lowest_index = min(index1, index2) @@ -242,9 +241,6 @@ def set_rules(world: "HatInTimeWorld"): if not is_location_valid(world, key): continue - if key in contract_locations.keys(): - continue - loc = world.multiworld.get_location(key, world.player) for hat in data.required_hats: @@ -256,7 +252,7 @@ def set_rules(world: "HatInTimeWorld"): if data.paintings > 0 and world.options.ShuffleSubconPaintings: add_rule(loc, lambda state, paintings=data.paintings: has_paintings(state, world, paintings)) - if data.hit_type is not HitType.none and world.options.UmbrellaLogic: + if data.hit_type != HitType.none and world.options.UmbrellaLogic: if data.hit_type == HitType.umbrella: add_rule(loc, lambda state: state.has("Umbrella", world.player)) @@ -518,7 +514,7 @@ def set_hard_rules(world: "HatInTimeWorld"): lambda state: can_use_hat(state, world, HatType.ICE)) # Hard: clear Rush Hour with Brewing Hat only - if world.options.NoTicketSkips is not NoTicketSkips.option_true: + if world.options.NoTicketSkips != NoTicketSkips.option_true: set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), lambda state: can_use_hat(state, world, HatType.BREWING)) else: diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 15140379b9..dd5e88abbc 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -1,15 +1,16 @@ from BaseClasses import Item, ItemClassification, Tutorial, Location, MultiWorld from .Items import item_table, create_item, relic_groups, act_contracts, create_itempool, get_shop_trap_name, \ - calculate_yarn_costs + calculate_yarn_costs, alps_hooks from .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID, \ get_total_locations -from .Rules import set_rules +from .Rules import set_rules, has_paintings from .Options import AHITOptions, slot_data_options, adjust_options, RandomizeHatOrder, EndGoal, create_option_groups -from .Types import HatType, ChapterIndex, HatInTimeItem, hat_type_to_item +from .Types import HatType, ChapterIndex, HatInTimeItem, hat_type_to_item, Difficulty from .DeathWishLocations import create_dw_regions, dw_classes, death_wishes from .DeathWishRules import set_dw_rules, create_enemy_events, hit_list, bosses from worlds.AutoWorld import World, WebWorld, CollectionState +from worlds.generic.Rules import add_rule from typing import List, Dict, TextIO from worlds.LauncherComponents import Component, components, icon_paths, launch_subprocess, Type from Utils import local_path @@ -86,19 +87,27 @@ class HatInTimeWorld(World): if self.is_dw_only(): return - # If our starting chapter is 4 and act rando isn't on, force hookshot into inventory - # If starting chapter is 3 and painting shuffle is enabled, and act rando isn't, give one free painting unlock - start_chapter: ChapterIndex = ChapterIndex(self.options.StartingChapter) + # Take care of some extremely restrictive starts in other chapters with act shuffle off + if not self.options.ActRandomizer: + start_chapter = self.options.StartingChapter + if start_chapter == ChapterIndex.ALPINE: + self.multiworld.push_precollected(self.create_item("Hookshot Badge")) + if self.options.UmbrellaLogic: + self.multiworld.push_precollected(self.create_item("Umbrella")) - if start_chapter == ChapterIndex.ALPINE or start_chapter == ChapterIndex.SUBCON: - if not self.options.ActRandomizer: - if start_chapter == ChapterIndex.ALPINE: - self.multiworld.push_precollected(self.create_item("Hookshot Badge")) - if self.options.UmbrellaLogic: - self.multiworld.push_precollected(self.create_item("Umbrella")) - - if start_chapter == ChapterIndex.SUBCON and self.options.ShuffleSubconPaintings: + if self.options.ShuffleAlpineZiplines: + ziplines = list(alps_hooks.keys()) + ziplines.remove("Zipline Unlock - The Twilight Bell Path") # not enough checks from this one + self.multiworld.push_precollected(self.create_item(self.random.choice(ziplines))) + elif start_chapter == ChapterIndex.SUBCON: + if self.options.ShuffleSubconPaintings: self.multiworld.push_precollected(self.create_item("Progressive Painting Unlock")) + elif start_chapter == ChapterIndex.BIRDS: + if self.options.UmbrellaLogic: + if self.options.LogicDifficulty < Difficulty.EXPERT: + self.multiworld.push_precollected(self.create_item("Umbrella")) + elif self.options.LogicDifficulty < Difficulty.MODERATE: + self.multiworld.push_precollected(self.create_item("Umbrella")) def create_regions(self): # noinspection PyClassVar @@ -119,7 +128,10 @@ class HatInTimeWorld(World): # place vanilla contract locations if contract shuffle is off if not self.options.ShuffleActContracts: for name in contract_locations.keys(): - self.multiworld.get_location(name, self.player).place_locked_item(create_item(self, name)) + loc = self.get_location(name) + loc.place_locked_item(create_item(self, name)) + if self.options.ShuffleSubconPaintings and loc.name != "Snatcher's Contract - The Subcon Well": + add_rule(loc, lambda state: has_paintings(state, self, 1)) def create_items(self): if self.has_yarn(): @@ -317,7 +329,7 @@ class HatInTimeWorld(World): def remove(self, state: "CollectionState", item: "Item") -> bool: old_count: int = state.count(item.name, self.player) - change = super().collect(state, item) + change = super().remove(state, item) if change and old_count == 1: if "Stamp" in item.name: if "2 Stamp" in item.name: From 35ed0d4e19791281523dcab5901ff8785823badb Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Sat, 27 Jul 2024 17:17:34 -0400 Subject: [PATCH 02/13] Lingo: Fix Rhyme Room LEAP panel logic (#3699) --- worlds/lingo/data/LL1.yaml | 2 ++ worlds/lingo/data/generated.dat | Bin 148903 -> 148927 bytes 2 files changed, 2 insertions(+) diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index 1c9f4e551d..5d10f07747 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -7649,6 +7649,8 @@ LEAP: id: Double Room/Panel_leap_leap tag: midwhite + required_door: + door: Door to Cross doors: Door to Cross: id: Double Room Area Doors/Door_room_4a diff --git a/worlds/lingo/data/generated.dat b/worlds/lingo/data/generated.dat index d221b8168d9164576b3f1c5b2fc57c604da5f100..ad412fc352541d2dee982829f5484a85b9d2b446 100644 GIT binary patch delta 3860 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 RrqfE!sB*StUxsG%`!6G{Tt5H+ From e38f5d0a6132797c49a27b1d9cce9350270ce182 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Sat, 27 Jul 2024 17:17:59 -0400 Subject: [PATCH 03/13] TUNIC: Update plando connection option call to use options API #3695 --- worlds/tunic/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index b3aa1e6a34..5253e99514 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -121,7 +121,7 @@ class TunicWorld(World): cls.seed_groups[group] = SeedGroup(logic_rules=tunic.options.logic_rules.value, laurels_at_10_fairies=tunic.options.laurels_location == 3, fixed_shop=bool(tunic.options.fixed_shop), - plando=multiworld.plando_connections[tunic.player]) + plando=tunic.options.plando_connections) continue # lower value is more restrictive @@ -134,9 +134,9 @@ class TunicWorld(World): if tunic.options.fixed_shop: cls.seed_groups[group]["fixed_shop"] = True - if multiworld.plando_connections[tunic.player]: + if tunic.options.plando_connections: # loop through the connections in the player's yaml - for cxn in multiworld.plando_connections[tunic.player]: + for cxn in tunic.options.plando_connections: new_cxn = True for group_cxn in cls.seed_groups[group]["plando"]: # if neither entrance nor exit match anything in the group, add to group From 34141f8de0d2274cc66da9c3ed8045a64bad31fa Mon Sep 17 00:00:00 2001 From: lilDavid <1337lilDavid@gmail.com> Date: Sat, 27 Jul 2024 16:19:09 -0500 Subject: [PATCH 04/13] SMZ3: Classify "nice" items as useful (#3683) --- worlds/smz3/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 6056a171d3..d78c9f7d82 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -215,7 +215,6 @@ class SMZ3World(World): niceItems = TotalSMZ3Item.Item.CreateNicePool(self.smz3World) junkItems = TotalSMZ3Item.Item.CreateJunkPool(self.smz3World) - allJunkItems = niceItems + junkItems self.junkItemsNames = [item.Type.name for item in junkItems] if (self.smz3World.Config.Keysanity): @@ -228,7 +227,8 @@ class SMZ3World(World): self.multiworld.push_precollected(SMZ3Item(item.Type.name, ItemClassification.filler, item.Type, self.item_name_to_id[item.Type.name], self.player, item)) itemPool = [SMZ3Item(item.Type.name, ItemClassification.progression, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in progressionItems] + \ - [SMZ3Item(item.Type.name, ItemClassification.filler, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in allJunkItems] + [SMZ3Item(item.Type.name, ItemClassification.useful, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in niceItems] + \ + [SMZ3Item(item.Type.name, ItemClassification.filler, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in junkItems] self.smz3DungeonItems = [SMZ3Item(item.Type.name, ItemClassification.progression, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in self.dungeon] self.multiworld.itempool += itemPool From b77805e5ee5f50bae7ded6690473a0ac66fa4e07 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 28 Jul 2024 01:32:25 +0200 Subject: [PATCH 05/13] Fill: remove sweep_for_events(key_only=True) (#2239) --- BaseClasses.py | 8 +++----- Fill.py | 1 - worlds/alttp/SubClasses.py | 4 ---- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 88857f8032..1c7dad7f3b 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -680,13 +680,13 @@ class CollectionState(): def can_reach_region(self, spot: str, player: int) -> bool: return self.multiworld.get_region(spot, player).can_reach(self) - def sweep_for_events(self, key_only: bool = False, locations: Optional[Iterable[Location]] = None) -> None: + def sweep_for_events(self, locations: Optional[Iterable[Location]] = None) -> None: if locations is None: locations = self.multiworld.get_filled_locations() reachable_events = True # since the loop has a good chance to run more than once, only filter the events once - locations = {location for location in locations if location.advancement and location not in self.events and - not key_only or getattr(location.item, "locked_dungeon_item", False)} + locations = {location for location in locations if location.advancement and location not in self.events} + while reachable_events: reachable_events = {location for location in locations if location.can_reach(self)} locations -= reachable_events @@ -1291,8 +1291,6 @@ class Spoiler: state = CollectionState(multiworld) collection_spheres = [] while required_locations: - state.sweep_for_events(key_only=True) - sphere = set(filter(state.can_reach, required_locations)) for location in sphere: diff --git a/Fill.py b/Fill.py index 4967ff0736..5185bbb60e 100644 --- a/Fill.py +++ b/Fill.py @@ -646,7 +646,6 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None: def get_sphere_locations(sphere_state: CollectionState, locations: typing.Set[Location]) -> typing.Set[Location]: - sphere_state.sweep_for_events(key_only=True, locations=locations) return {loc for loc in locations if sphere_state.can_reach(loc)} def item_percentage(player: int, num: int) -> float: diff --git a/worlds/alttp/SubClasses.py b/worlds/alttp/SubClasses.py index 769dcc1998..328e28da93 100644 --- a/worlds/alttp/SubClasses.py +++ b/worlds/alttp/SubClasses.py @@ -76,10 +76,6 @@ class ALttPItem(Item): if self.type in {"SmallKey", "BigKey", "Map", "Compass"}: return self.type - @property - def locked_dungeon_item(self): - return self.location.locked and self.dungeon_item - class LTTPRegionType(IntEnum): LightWorld = 1 From b273852512a6a3d2d81101f8618186605a840d76 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Sun, 28 Jul 2024 00:44:48 -0400 Subject: [PATCH 06/13] Fix obvious typo (#3622) --- WebHostLib/tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 8e567afc35..75b5fb0202 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -79,7 +79,7 @@ class TrackerData: # Normal lookup tables as well. self.item_name_to_id[game] = game_package["item_name_to_id"] - self.location_name_to_id[game] = game_package["item_name_to_id"] + self.location_name_to_id[game] = game_package["location_name_to_id"] def get_seed_name(self) -> str: """Retrieves the seed name.""" From 67f329b96feacf9fbc595c7e8fb2126898856290 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Sun, 28 Jul 2024 11:41:57 -0400 Subject: [PATCH 07/13] Lingo: Add warpless connection between Hedge Maze and The Incomparable (#3703) These areas are technically connected through The Observant, but the connection between The Observant and The Incomparable is marked as a warp because of the warp hallways leading up to The Observant's achievement panel. Creating separate entrances for The Incomparable is a simple workaround, and allows use of that connection during a pilgrimage. --- worlds/lingo/data/LL1.yaml | 5 +++++ worlds/lingo/data/generated.dat | Bin 148927 -> 149055 bytes 2 files changed, 5 insertions(+) diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index 5d10f07747..950fd32674 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -1556,6 +1556,8 @@ room: Owl Hallway door: Shortcut to Hedge Maze Roof: True + The Incomparable: + door: Observant Entrance panels: DOWN: id: Maze Room/Panel_down_up @@ -1967,6 +1969,9 @@ door: Eight Door Orange Tower Sixth Floor: painting: True + Hedge Maze: + room: Hedge Maze + door: Observant Entrance panels: Achievement: id: Countdown Panels/Panel_incomparable_incomparable diff --git a/worlds/lingo/data/generated.dat b/worlds/lingo/data/generated.dat index ad412fc352541d2dee982829f5484a85b9d2b446..9a49d3d9d4b9078c1d8244f5023cb1d49584241a 100644 GIT binary patch delta 31722 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 e4DKCm}D%U;7`ux+bbR@&5rE!By)3 delta 31706 zcmbt-d3;nw^0+-oCX+jX+$1nLNeK6S!z4_SA(NSqnHW(KB1DrX5Fo@`KwTAZ%H)+s_uUCCgAQLKliiHue!RrySlo%y1HuE9g5q1 zeO&ai==Jeyql=uwhqlZfIb`(U)^leM9zJA5>u3O+JErBl^IC__9x-Olm~Azla=x`P ze*3%S|FQAQN5*e27;wIwJ;hfHJDo2dmdYcy{>8@)_$^_?8>P zc*AvlVr7%b{KD%7vI?HNvJC#*cU?dDbN`*UJY`n?BLL70~Ut6}?oq?R>_HVNkVp zMGkaxnA|{n9x`?zMDfis_j<(WC-hi z4)rh4ZP$g8e61~uoQa~w%Sk*^*7j3ma`@!6Q+i4C=wPiUpm9wJzGH2% z!Y$^nti|PU+&EG#)tnnO8wc|%Zyc%V`1}p-2s$W|%YGlJ=#1kj97SR(AJaFytUZGr z7qu>OR<_K(z}a2CtNH!Bs)rtBQ={BxMg>2?PcIIWNNw&|G^f)UxCs7=5RN8($-1hZ zvVBamzj2D3CJ@-au76&bfWv)BtFyUf?p(dnvw7C~5ir#H^#ftbm#@!NQ<%?hUyqyg z+4TeagqtTcc66NQbj_aK+S#coT+CB96anV<_YHUwYp{5kW1;Zd-M_}-!U zJV&GhWS~HrK~WBUEfLwOuUq-;0(bC1kv>sQ!Qu4>g3hv`C(|FT%+G$s; z3q`vxZeOg2avv|cX+nR+MOVGjR%#nZ`vB6N9uM7Au14`V-+7alRfv=lmJ>rX_$Sw8 z!LTOXJQ0U`bAG(kMK2|vb{c>A=3@9iXc93K@Bu6FmR|5@*)4@mbC6v|W|SPHbsxXa zja$s6_PJe+G$vT7-M5s5+jbg7r)c{nkG<7nF0-pKXfE^YTl>KvSKXSe^!LxOO>Ui% zr+DFYyQ)<;$iYx_wcG9G^Kb0~Z3f;}oG2$|Pk|lVkFVXB%`dzS<#!wWNh&obzH%;# zt#uwf5WA4DD{n`=zwmb4HTT}$TQM}Bzka(5;3*q(B^B=9n68FbmO{tN(v7(8+u_e) z{ELkxY8=Cnaw$K)CV_tpXn`5S|^*2&qMJ-(pZWNX2l z>3Z8%dm2xcfQCCu)qsQiiaV>IyS;b9M8tt)R?2&Ca;nM}?%w2v%Ih})qh;lxO?s;H zIs5BKs2u&*ittob!%O+*zv7;F%8{wRdZke~53n+ncEtpxWQmXjuTBde4Advth^{SlrUS z$kd5n^X7X{tKM)=d02aQwY7u7ig%NNW8FzVrSK03J@wvQ?SPu@xAfqM%R&2Br55&X(~^>E|)#(Pn{et+MfgqG#fLc??E_`O+5qSATZeG)^e z>i+zW`{F_FdLP%SxRAemUn$YBD)Yg7>HYE6W1zbIyv1#i>o%O5Esi|xja>fj!{hsz3)t1xb;Y`}P(owt9Iy)(EoyHy&Ap9Cu9RvR4Q3vDn&18RaY|Rj*ZPCbs>Y^(I+*lD!FqDH zErVT<*xWjIcE`L{vWE2W{VtO2%Ny+3ylVSYn9-{3+3@E*+lPf^uhU46p|M4|=g-j% zKj*)0M-#aJBYDHjS#-ADhrErgZ|Pjzy66&(`vZ1j0(~>qffBgv5j+TQeI!?%PQRt{ zryp?_$gKz_&papou@0KK0&udJ3WqP8nZ?IGngU%kS@$Emx=gfu#47e1Z=1XG@HCdI%J7%=5A<%xo%fV;}?Gi8Brz)EhkKq5k2g`dMi_XQi=<+jgLG9=#(% z^}lV!xrGmTI-PIaftuxo9TBuXx8mK+b9RmboM}7Dto{9L#rZqYdTb}s`gmu*FxNT_ z2j8_zb8Q!(jRTZP7<6XO_1I_)rM_5qWo))aHt(O+H-_rQ-J!m~$ zyk`I`%iViQwN=>zHe7$+Qj@`t?!ntt#ooLUQ)WYnm29?Bh(~^ z@VsYhw9>BhHffWAls2eGAiU#Qtx6~JL(jTTh3tB;4{v;~EaJx1&6VNj8=h-Om+ZIu z8(qE{v#Z~pLp?ria~>b|d}dLYN2jnET95&Sl0(n;L?w~hKZ8H;d>T#HYCssr!Ky>A z4Jf)#-szcc*gyl~cdNxlIPy%fz03I2e%fhbS6^G6_FZ&x?+ev1B>n<;OVH>o;YVN4 z0=taIzc^ZZhnnz89O)gB{4GJNU-9^Qe#eXDKATIZ=7^X-o(8xXY&Iufo&mLE@g$>`7)X>6))>`c@PlV)uku7(GviZxwd0b`=uT8 zO>_9*%endtJkz`&@;_eA20|IHXs6B|?s~;VwwNyz^}i?fDnIlJZtv(GNA9oa;6)v#`S>X|%co{*UW?{E zUn_Md;Z3X+2XxW2pi&|SvYDbY;A4M%gpF+WAztx<5>NRxI zz5AL|9f801;3r-iIY4$B7p!e`pLG4uQ1kx5zk5BSZ`enMs-4CYQw`RvIh=%^Fr3~v zoN>+pPFV%VaG5gkHC6Kfr4vF!t$p=|0VnU%OW%tR+E=2+8;!<#_*M{d)OMY#j&N%g zzj9w6Sd&fr`U0hw_w`j$I+Xvs&jWDRe(6vw=1cc$xtPcw*pFA%!~2Klg!gAmp#9=7 zziWB_10`BHRC`W0w>NI=r3cVjkfqqT@O01!J8>YEA3ETGdim8KhSmR|R%Ubfw1dNk zDLDvu&Y<~$9B7}H$!7hDRk1&STm9 zvNz(PV%gywe#aX%_f{g5K71=5{`~bV^t$G~-5dVg_GTV$emeu!W!~HR*6=RM zb0KZ7hu+T7>+bWIH&^T_G1>|^{an6Q;ZA&H(F9Cg3c%J zl)+Hq-yNY&#i(=`%3!|a-Ao=_o65H|B=F1MEz?5^THK0{y*mP0e*3Q8J!w4YJ@ny> ze-BU4OWxCaA)nv#-sDMA=%U?cc%uU542-^A|Lm`=xW(-Of5Lc%#OQ@dzX>ukR2!cIu^n0 zuzU9&{^SSfNB;GL66m7Ohk0t72YKa(aLEwII$5r`VLpo=bBfK>W#kd{P;#K7>lDOi zA;*jO{0}p4JX8V1Hg&KpakzuU^ZG;a15FLlX_QJbJp%1>TAi*%tu4+#8w_`Lm+8Em z%da`qD94i{CSJ(WMxDI3JB^Y5lg`C_%tsHSyi9J+N(psyhNXTCGADM=XK(=*gF0B> zJlIR!S=wn-OO|rhoUToQHGJpC$i_cD&bQvln_D{kVeS>Ms8 zT(cejt)rc?qgh|1^Q2D(Bcs-yn<>U$#!?e3#IR!}C@=CGKJk&7;0Mpn<-dN?tHNB^ zF5`WeD&`%evvKXl`eU;DcUk%cCnQFgsWLqV+;j47D zRa&r7F*M5Kp|1vNo5AOn&zP1rVVfb?0lV0As9eC0ezioOnvIrIQ?x8)<@}zn(+8MS z?J}mwse()S{CVJ@ix~fQvE6zMAFK^1+z6@`Wn71P#jt(*}C?}41VLcqs~_cTy^-+ zM$Uv{D!r~L+I%C3#c!rC;B3;q5{codif9qt==Ylrm%&F-vkgAli(hmUeR^2B`zTt3 zP)dqvc3Ivnujm?siigQl~ z%RSh!z`Ows_%2(y>l`q>>0VD=#DF74K~stbYg#`vNx&4>bPeFEzst~);mu4D|4So6ZO|gPMCR;&m0DaihEr^fVcP#bjX(IWX?n@5gW;(^rnf7;)i+h? zm6MaJ_Lxhl85tfCy(!juA~_o|D%wuq=JSPTzNRA=hHl++96)^kk4eZ3C?$Llut#bdt=w9a^^oJPcq z-{8@|PE-eZbTha_v=q^bRC)bXrd%P!W^PAX<(MjelP}2k{!(P^=ap0R^96tQ*9od0 zdl1}O-P9l)S=OgJ(+j0079FT<@?k$=FUJ>EF>bIQlzAJLLVLUB~%+${*-9ZvUei;9LLbqn`T9vf!!z zKYtX%|I5b8HHva8drA#Ffesr8xd_(kWlOZpqCHxXs!zUasmY36o{cU;t%Hx&cSCCM`DPG_L{L|xPXti9?hnJr4;kz+w#XOjsCrk#+C;e`AB~}2+Q_Hu+I8x#x!A>6V3yjsaJM$~ zJnkbqgj{q3#WccJia?4FxP4Q=38p!x7#2e!rDR7ew6R{{x4BNkOT#G(CFA3xb8&|= z*s|#SR`VM95Uy^qXozN+;td;1ZpZ%ZNtEl@RlW6!XL#Fq{WP16C5ll{B?U2OAdd%5XIo67DuC)c8easF%FVr93IWQ zcAH)1pl>k(R-^^(Lg8&4>)*OLiPxtt};h+eDMb|e4wVDvC?JPGb z#_M;PvVT4L3v)N%5>Cd{g_g zeRLl3u%B2G&xSWB@%$e{k!u`m^fy&{^^y`SZc0;w*HZ3kPk zQ&@&N$KnD_H7*GBF)QT?DySJ%MNmgot=HdZF}^iT0bMk(3xx^BL@Q`49RP&isD z2~rhhF=vrbELhwDPo%M7uWsrNimnGVEmzm2viwr>)C{Fmws*|!IR6r7w}U%62e^1N zl@;M1efvpSimUYaYZ_e*k!Fm&%2gF?GCP=<#xiFpjo0XpG$tKEdL)YETugOilkyrX zEn}~D)x#ysx+u{WTKVJ%lr;!6aK22c&?_WS-`F zwm2t`q!+l3%%$q5WHr(LR+cyQhi1L=nFDb8=Ckqe7fi4~T$9gIq1>>X^%YO&!x4S1 zpl09XgY|W;DA)uqJm2kLJw$15maGBZ-n5>204OH@|?6Xet5XkIRlc@G4e4I z#vj3k*m!$0PK_9j?OBI#^GH0$1xetKy=TpfntO4 z^&P~A7L$xo->(#t#DKizieCaOG1;=;Agtk383nMvt(gPLudfHu+}6IhRr_nj!zB#D z6vX}#=$%3bL|iF!SRryt*+k1cyGluWSjLU%2gXfecd68cO3VCK%F0wTH9t%>Ft!L! zU#(S08(rO(*2O|^IO07g_V(A^^2KE#r#ZB>4SGybWE(ur&ilrP?F4JvCEKI6QH{&83N`gxrT`l z;SacYX%=2S6r@S7j9V_iHsEKf%zOYsl-i)sV=1peeiVuU!)VOF*FrFxl(9kmQ6_`# zkFpr#2X1GiZu?i1XvrY?gw`3NY&h$wMj>Vlrx6jIDd>OGbSOXvKajw2lfb^wtk(b? zl;!A_{XkZ?0-8p!m@tA3kt>z!kctz?tsOyv4P4JH(Lm551N&I_3>gmre1(C5T(N)+ z297|46MSI1Ffs2)QU(Al5UWPg$)?M8v1cTUmfaUP;8_9Uu4MZHkur+r4pSN`e4~iF zR0g<1tQkek05C=wnpl?<=MK2Q?bR$dB7z*z?j~;~5~aqwN3(cK0|DP?4nW-m`PWb} zaZI>hC_9v#1IiGwV+={-!7(iNe}!z?44ddRmX!9wXP7e~)&&oG7j=--1E4^x8B1FU zE5*)mIne=#)$JF;Q38WDn?kUf2@sRUN&5hs8fLj5(F0vz6rqWk=l~E#>zNR7Y+Sh8 z-0_+YwWSa=!kpF$hjE9PH=bmJhPZn?af-^v#?x|uq35hH##7os8D!H)kE2y-TMbuZ zyl_pRL&tR!Sg6YUHAaZ*CXk8+Q7%Z5sTK5s1hap00-Ya-KiI^F?u<1~iy(HX%m;{K zLT9bkU2C5Phr^PIWNln|E$i1KS$cP!70xDKKwdA9a&jbP@%2P9M9|K$lUcM`^qr)Q zolJ4LlO>4xlZY7+Mxb5%eG;fFFvb4Ha!~F5V-n34KjY<@y3F{|DJmwjUVw#N7e#h$ zos^0h113Nx!xMnJP4)HQ>jwatFW;T6O{Z+}$K-Iiw~JK&VHX>z1ec0ZK~qsGXef?_ zQNUjn4t(t*pfM|zje*npGB?L=Q3biA^XO@lUvDoHt+r~yXhJL#$JxN$IX&T zx}72rZr~#uM`mMC^fk-`w?S{R7ut=Mh}DUy0k??{nUYq`#sO2(9)>MFznY{C%1$Aw z!?xx`AMF=d$r1gG+h~knW4me%T}!!wV=uSA zJZ7*CIAi+RXi++i=VTMO)Iv%uE(~0@V`Yoy#HL!7k@~hG3ff;D@3?>a(f-9MFlMR3 zGK|k<3pmAT9dWHCK30SXY{C9n(GN8E!Mg`TSli0-1`>U&!TBce9gF=H0uJA=S%GSH)x6EI=EI=h`c&5 zEh~hpj+N01f^hxBG84nx1EU@4dh#O^9pZl3gua(ej_YA^2FEE$4=S}~iT_Bv1TQPa z_xbwDY*FWBeX^(19LgXK&F!u7gD(M{7dR1Ai1Fd$M=HpjUQ$40Qhn?tm5WOTr&?A$ zxay?>?ymdKtEX!a>GeD6=}M!^`Qk)9?QQ~S2Z%#V@{!#Q(Ba^9p&S$#($zk?zV7sq za!?&-_{k7bUjaWWP>LftzPbtS|E8~P{=@KyvCmHzcbGtd_>bST+C{$xIAFS>pIg)-$@xIAGUSCS0^KMe;P31-(PLZ#3OBpoF)UPo4q?^BvP@ zP5>%$KtK<;s$oh8EeL%YH9}#$-31SGJ`bWm0m;dv6a)M4C(GA&I`h^Ng@1V4HdQLrzm6n*(snRpx1 zfq1nGo|K^{_#j8*nXHfa;!N6#z!hg4oLO$kzJ-5>K~6l2WC5!B#Ki=f0`My|ILxz1 zcM!%7iSfx+=1c@Di7qI_eymLZac}UtAngEJA&QuAHq8eKijEfI49o_dhcvuempn6n z_&d~pbT%1#Qo`Wn1Wa4_6G@?US@>V9OG=LdylK+VZ<`@C)6=l|ADqGByFCg#JA)1B z24d&15#2yl=dixrKo^~JQo~2jVS`{Yq(zUPL)VO)nV^@IUF(}k{JeE0D8~+Q*-X;M z_$6I3?}0OC+ANl#?r%aLYHy6_3UY3cGWL^>(v2~bijcNKT?A$kCF?R=wiomEWHq_3h>*S z4rGmJkCC>yL{_gBRJrP0QsPuT+CN!EN$u&m3|~9+sH4 zR+3ERje;M*35(R+R&qMwVYie_=&y{0eXZaV^GcMy0k`FOhAZ8s(>= zLcXYzs6^A##J*}4CZc#{Uyg1HF(r zmkhPhbD0~ymdqvV5sc?_@zh*8t@hpn=g`r)`kf^^4fFHCAMBitHh}q#DaOy!C|O{T zeJeH1&{J3{(cPcHE!k9Pm!Bd@2($h$7*gg6L3$LWBc)tiK z6}by&70|p;H;-?{nG49B01m`a-A<3a3s@aUEj;R_3*TavnWB@%w4V{=oNSB?z5lvx z)CRXY%uNzSGBd0!1WA+_)uo(85t@VM33E#nGZ!&uFB9`*9TS0x{#@Npy4bpi#aW+} zaM=|962zB_SV4+RE-Qn(l60DylhHaNax&2nghNMrxQ|dLb97_brcO3EU#6RZhapf8 zlZ@CzQXC?G>~jI-^tmBFTVb7*c0I$vG~Bb1E#+Wj$9*1jiJQs~&z4gN|T$`k|LP zkn6tUsS8;~ip*hycK|>9a2Jq)JC!r7(~ucwaA|9j@B46dD#-5C$zcf|A4ItN;3tu! zwFyGoJJn18({2MeNc#Y(c$udZO)-kl1CkOiUPQ_l8;ajAB2|HKK$jU@u=bdw1?Y58 z&%Btd25^kkq_ik{`aX#xhR#*mR#d=6%~a<%c0)!m%tPgAB|yNayQ4J<-L*;5Y8eCN#h-M;bP*o}$%i;C7o!;=}JFAD8ZWV*~H@d&z zAWF=Pssz{UwsuIW!c?OOfy8G$_*H-+8Gk5K_0m9~AQ7xIhKvCY$z6$ZjnutO=ccI? zKn$<+UB*gCMa|3-q03kTmWRyDhExU1uR`dQuXvGQtyJ9k4U8?OhI?`(lo56uzT z8RY-GEdi1c-L#b8EUCy#4#W-U{55C{M@yOb&vrUyfI=y_&?s+B_Ku&FwI(t9CdJWK!$2b3O3)aulF)7x zcDfSWTKkq0t5R2j>Q7lgH)$-#qYo96)}WQf>w)jilpkgR_Kc+jF@Rt*=$_KUEC|p_ z<%dZebUJ7^TY93S5rPw40T~Jh9SxP^>MxG0Btuza;scN_XF&Kqglo$_`o~gMnEa@S zLytZrU*V^A<@M|`k|@Il&Ud)^NlyH5JvlJ&bcxlE7kH+`LWnXcaC#@sPS8FOaE@n+ ziQCZ!QtOcpJ7`-Zz6;S^5z7$JFa=`ivF~m0>2Sdh2^0UIvM?#mOA&Q$NHLTMC3gIk z6OHVL2R7;#kwJRYTRL{(Tt$nG6^>}|RuBRC6%5GZdD|nb#Eya2njE<9t4wl^XmG%o zBN4q(X+>>uZ60g*d#HmE$2eBel0EyAeFV*huV2v}KFt z8yV%}A@mg2sF@T9+Zy>fqE;>2ndoc}2`vPf`#b4&obM zS%*Veb*fnc^&@e_k@XZ65@yE9ZM$^3T}*Q={5q^2u4KmB;*1Tf*p5FPrO3;xMfb3$I3Zy>J_tUE?IfsFXzFWN$XY}YQ^9J_MZqtIk^89&Qi6WfAL{2g6;=*)Pv62C!M%`rD_E8?bq^H0%+G^bM0S8%fSzjUu^GQ) zKoGqUZMl^WJ8%m-lV#dnSqKyARNL4n%%)QxV$f}-+XgEL3-pvT&5&txu(c>PPUCO6 zO*?OjD8$0l*~!*qIKVcS3zkCW!&!l1=IwOD(O5}oIxSA6PXRJHi}rv7fHY9~*zM#r zfe7AypnM?V5odv4@b)3I<5Vl~g{ssz6QJ|rG~JIk>UrSlAXCopbiiy7z=3rh*xMBU z2=5#G_OkSDQY7(`JLo-8m($~Q!V(bCz>HY@DKj~LOaMo{*e8XL}r zQrGlYx87-vReX3S9T{@gDn(6pHBz%lJ^Uc)1T$p~aO86vrmIQ91rUv~Qe_Z<%u|w? z>N44yVJ2b4v6Nf}9|xzg(io-r@o8_zU-j^c95S^_QAq$(S0+;_fS7a_9c(bP0x|C{ znh6dQZ4-U(1a>sYq$t%0Lz%-bgnc%X7am)Qrpg_ER*eC}~nr(xjrK3B`dxlOU2N&12HNdLB*z=ES!);}FCVk&=pWKZQ!!MTI5B(07F=G0A!~HZvn5?Qd z`K7*1rqMU;Y!i1xB&`^ehpMf~NcAnp74Bk^Xyo7k5SD~2NI8N?r*1c9xLF>5 zfR)N4^9j-KL6+a`N*H{Qm5iW+3x&)$Hq>g|L|1~n51T7&2P*q4+UuGwwp|F}5TsHJ z*;X8QkW4}F@{C5mLg7Q?na2PQe27`{5Sa_`xL>52U5$29k$CeVdQO3g-Xe1g*#*$1 zKzO&1N}*_grLs)$`?k=cVHr*w7sW{+QoOy&-`F4(6I9}pKY|_6)77yAU&8^24IQ8v zYv^s6NFIHd_OYx9!qJ2(1-_i%%>Kqa$-!o&=T*Goz%yupcyKF?klsI0VZ%O`9wLBL z01_Fw{kPFeCY8_HCg0rPC=;u<(TPFuC%4gQOy#e_<+zwC$W4%u*Z6?z4g2fhNjB)I zk;Wdpw&G;ruTr&fV06y-8c(AOqOx%+cy`+U&zy+9BVm{X9_?`YsHYG4<*unQ&q49p zb~>fD;SEP!X924yjuPoK8qSt5mOWzLSdC=2Wkpxx{P0yTL16)LxiSsj7(>Qj`asFfHhI*aKW#)k#5F&qZs!Xsa(2( zwm$}0YtkMOuRca+068!J{TMAElnca?$3aQRZAVYq^evTw{zfGLOW_pvKTcCX8gK;4 zEKC>$(LC`u(MKPFL#AFr4Se+#vz{OZp(w(!uTn0d5<>3_<5apOeB!&LI3jV0;^8OF zk>N-a!`YL7jmHaY51BfN-{_mW{z+O`GUF(B5-MomN1ik#Gfw48$~Mk8l`9FK)Qs{Z z^#}bT3(_FrQ(2MN5IRpVQz8KX2OOSH=oA6#RHZ=z905u2dwn_y5gVX>_{L^J{WT@0fD+RG&Cn<9iOKjXp+ZR|}7^gBGp#h3t0wtcRd_t;7-=MKFB>_S@ocOaPRUX>O04&JRDU=Xi zszSbSo6-ej;x5ufX1ov3A-~S@U2Li@f3=IQFC;Jnc1zuX7R0RG(saS)U9p=6D0Ny* zV-rP+6D4S!F23EZCtm<+29k}W-vP`7F?kPFp;*Pm3?4da#26ghqCFJ%g=2)rRmj*! zAc~s!c8@&0kuAA&+a*tW`r%*G zI=|H^{{^Ckmieu7JJ3LBdwBX$k11dV?cKr$lNI75Q2 zeifb?WC*S?M}l5PEE!{KERdko*VyPJ8AxkfEmHeT4? zfiPU}J7Bl0eg^^;_PW6dTkf{?68Y7(T(RgKP$(b24G#Hd-(fXPQ%9e6S{=h(9*P?p z{%6;M05+)3ChIK!{9Cv#O4eEa;Wt?)X1>cZJz=~UiD&s|-NJQttkcPJp>F@=5TR9z zUGK6nJ!Bb&OG8rsu8p0*wPOE=Aa1IB^g~dWs+@Qz3}5zMxV-T_*lG%A*L!TZEc?V2 zAF*OFsKk~MYf!CEJp2(Hp=VnmAAba?KC9n{!$YB@eGJy|Q&yD89|OwQ07+D2aNKs$ z@jlDo4U-&V+y|^@f^3a7v7+H4c>J1FY6I_pBD(5>@Td|FLkHh{5blwgQ>+ut58*_h zMs=bd%93s!(uLHzBP^pQ+{__0K>gT}!o250I69ABzj_Ys^=RHijB z%fNj&+<~&;6TtTYx(qBg;lS(x7G_%5{M#g3qICtre2y2deasx18t}SU9RHZbY5*`? z4bvwk9cBZT(h$5jL%CVtF^s&dVYu8VD#|kqmU|@nOm`!MXNSRYB=)3t@%Ife5iwjD zP5VVg>PToC_AIxNjt2~*(-sQp9}5V>V~LFNH-^Cu>RP?GgzQz2N`+xNgh0~d6mM7B zdMH68nR1E;kFd0p1WSB$1S}QR@aH3J{Df?9AGB?2Yo)iawxw{3#7fwqgZLADb1h!{1N56-?*`fC z&wK_TSFAIh!-{mM=ro>}kQWh>L6DaSQf9n@piHFmDweXa^ct43vGh8YavcX?d#}RszSTxkOq<0Kelo_#D zQ-!S@SaMUVIFUTeHljWOA+-caBy=MgAybJ?iiD&gq>dmx2onBhrKZ*sID^3SFEWYE z!)$p=vt-L0Y}r64xe}6xkZA>6$>JKcql7JwL)SUML=Ralyhr72jNgZS2e^=9f{0n%l-p%O%` z260*e(rMHZq|BHq#*eTS=G0+L8{v9YjSoTXVs8PMN~=fMdS&<#Fc)x)1}x3P(loJq zge^0(5djMj5WvzxA`}$gjIi|^e>y@IV_h?ry0CNxmM+B7bSzzjr8BW~F_gNDv!IB^ z_Y5&@q|LeX9Qrh7A;M*V&}pVGNysG#xrQK@O3391xt1VTh#{kF&Nd}Q zS7OC-s<=v`{skcw1i3~+u0zNQf-IMil?Yi$kn1Jn283KskW~aJGu9v|Bnq6i46$#N z&Cb?}w@2BU3f3X~MnLT{))T66ll<#e`PW8KH`AGfgtw~ zq|CSvK>|R={aCsQOAlb_W+-(T4-%TO1tGUk!-pkg8$xa+$loR85ro_ZkfKiGQ3-k+ WLAMjq6B6>Y{A>5)@ndWwlKvm@gojE1 From ab0903679c6de5407de01a4ba139b495be013ba5 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Sun, 28 Jul 2024 11:57:10 -0700 Subject: [PATCH 08/13] Factorio: Fix ap-get-technology nil value crashes (#3517) --- worlds/factorio/data/mod_template/control.lua | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua index 8ce0b45a5f..ace231e12b 100644 --- a/worlds/factorio/data/mod_template/control.lua +++ b/worlds/factorio/data/mod_template/control.lua @@ -660,11 +660,18 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi end local tech local force = game.forces["player"] + if call.parameter == nil then + game.print("ap-get-technology is only to be used by the Archipelago Factorio Client") + return + end chunks = split(call.parameter, "\t") local item_name = chunks[1] local index = chunks[2] local source = chunks[3] or "Archipelago" - if index == -1 then -- for coop sync and restoring from an older savegame + if index == nil then + game.print("ap-get-technology is only to be used by the Archipelago Factorio Client") + return + elseif index == -1 then -- for coop sync and restoring from an older savegame tech = force.technologies[item_name] if tech.researched ~= true then game.print({"", "Received [technology=" .. tech.name .. "] as it is already checked."}) From e764da3dc6c476be7bd69267eebb8ad511bc3fbb Mon Sep 17 00:00:00 2001 From: qwint Date: Sun, 28 Jul 2024 16:27:39 -0500 Subject: [PATCH 09/13] HK: Options API updates, et al. (#3428) * updates HK to consistently use world.random, use world.options, don't use world = self.multiworld, and remove some things from the logicMixin * Update HK to new options dataclass * Move completion condition helpers to Rules.py * updates from review --- worlds/hk/Options.py | 6 +- worlds/hk/Rules.py | 39 ++++++++++ worlds/hk/__init__.py | 162 +++++++++++++++++------------------------- 3 files changed, 108 insertions(+), 99 deletions(-) diff --git a/worlds/hk/Options.py b/worlds/hk/Options.py index 38be2cd794..e2602036a2 100644 --- a/worlds/hk/Options.py +++ b/worlds/hk/Options.py @@ -1,10 +1,12 @@ import typing import re +from dataclasses import dataclass, make_dataclass + from .ExtractedData import logic_options, starts, pool_options from .Rules import cost_terms from schema import And, Schema, Optional -from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange, DeathLink +from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange, DeathLink, PerGameCommonOptions from .Charms import vanilla_costs, names as charm_names if typing.TYPE_CHECKING: @@ -538,3 +540,5 @@ hollow_knight_options: typing.Dict[str, type(Option)] = { }, **cost_sanity_weights } + +HKOptions = make_dataclass("HKOptions", [(name, option) for name, option in hollow_knight_options.items()], bases=(PerGameCommonOptions,)) diff --git a/worlds/hk/Rules.py b/worlds/hk/Rules.py index a3c7e13cf0..e162e1dfa8 100644 --- a/worlds/hk/Rules.py +++ b/worlds/hk/Rules.py @@ -49,3 +49,42 @@ def set_rules(hk_world: World): if term == "GEO": # No geo logic! continue add_rule(location, lambda state, term=term, amount=amount: state.count(term, player) >= amount) + + +def _hk_nail_combat(state, player) -> bool: + return state.has_any({'LEFTSLASH', 'RIGHTSLASH', 'UPSLASH'}, player) + + +def _hk_can_beat_thk(state, player) -> bool: + return ( + state.has('Opened_Black_Egg_Temple', player) + and (state.count('FIREBALL', player) + state.count('SCREAM', player) + state.count('QUAKE', player)) > 1 + and _hk_nail_combat(state, player) + and ( + state.has_any({'LEFTDASH', 'RIGHTDASH'}, player) + or state._hk_option(player, 'ProficientCombat') + ) + and state.has('FOCUS', player) + ) + + +def _hk_siblings_ending(state, player) -> bool: + return _hk_can_beat_thk(state, player) and state.has('WHITEFRAGMENT', player, 3) + + +def _hk_can_beat_radiance(state, player) -> bool: + return ( + state.has('Opened_Black_Egg_Temple', player) + and _hk_nail_combat(state, player) + and state.has('WHITEFRAGMENT', player, 3) + and state.has('DREAMNAIL', player) + and ( + (state.has('LEFTCLAW', player) and state.has('RIGHTCLAW', player)) + or state.has('WINGS', player) + ) + and (state.count('FIREBALL', player) + state.count('SCREAM', player) + state.count('QUAKE', player)) > 1 + and ( + (state.has('LEFTDASH', player, 2) and state.has('RIGHTDASH', player, 2)) # Both Shade Cloaks + or (state._hk_option(player, 'ProficientCombat') and state.has('QUAKE', player)) # or Dive + ) + ) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index fbc6461f6a..e5065876dd 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -10,9 +10,9 @@ logger = logging.getLogger("Hollow Knight") from .Items import item_table, lookup_type_to_names, item_name_groups from .Regions import create_regions -from .Rules import set_rules, cost_terms +from .Rules import set_rules, cost_terms, _hk_can_beat_thk, _hk_siblings_ending, _hk_can_beat_radiance from .Options import hollow_knight_options, hollow_knight_randomize_options, Goal, WhitePalace, CostSanity, \ - shop_to_option + shop_to_option, HKOptions from .ExtractedData import locations, starts, multi_locations, location_to_region_lookup, \ event_names, item_effects, connectors, one_ways, vanilla_shop_costs, vanilla_location_costs from .Charms import names as charm_names @@ -142,7 +142,8 @@ class HKWorld(World): As the enigmatic Knight, you’ll traverse the depths, unravel its mysteries and conquer its evils. """ # from https://www.hollowknight.com game: str = "Hollow Knight" - option_definitions = hollow_knight_options + options_dataclass = HKOptions + options: HKOptions web = HKWeb() @@ -155,8 +156,8 @@ class HKWorld(World): charm_costs: typing.List[int] cached_filler_items = {} - def __init__(self, world, player): - super(HKWorld, self).__init__(world, player) + def __init__(self, multiworld, player): + super(HKWorld, self).__init__(multiworld, player) self.created_multi_locations: typing.Dict[str, typing.List[HKLocation]] = { location: list() for location in multi_locations } @@ -165,29 +166,29 @@ class HKWorld(World): self.vanilla_shop_costs = deepcopy(vanilla_shop_costs) def generate_early(self): - world = self.multiworld - charm_costs = world.RandomCharmCosts[self.player].get_costs(world.random) - self.charm_costs = world.PlandoCharmCosts[self.player].get_costs(charm_costs) - # world.exclude_locations[self.player].value.update(white_palace_locations) + options = self.options + charm_costs = options.RandomCharmCosts.get_costs(self.random) + self.charm_costs = options.PlandoCharmCosts.get_costs(charm_costs) + # options.exclude_locations.value.update(white_palace_locations) for term, data in cost_terms.items(): - mini = getattr(world, f"Minimum{data.option}Price")[self.player] - maxi = getattr(world, f"Maximum{data.option}Price")[self.player] + mini = getattr(options, f"Minimum{data.option}Price") + maxi = getattr(options, f"Maximum{data.option}Price") # if minimum > maximum, set minimum to maximum mini.value = min(mini.value, maxi.value) self.ranges[term] = mini.value, maxi.value - world.push_precollected(HKItem(starts[world.StartLocation[self.player].current_key], + self.multiworld.push_precollected(HKItem(starts[options.StartLocation.current_key], True, None, "Event", self.player)) def white_palace_exclusions(self): exclusions = set() - wp = self.multiworld.WhitePalace[self.player] + wp = self.options.WhitePalace if wp <= WhitePalace.option_nopathofpain: exclusions.update(path_of_pain_locations) if wp <= WhitePalace.option_kingfragment: exclusions.update(white_palace_checks) if wp == WhitePalace.option_exclude: exclusions.add("King_Fragment") - if self.multiworld.RandomizeCharms[self.player]: + if self.options.RandomizeCharms: # If charms are randomized, this will be junk-filled -- so transitions and events are not progression exclusions.update(white_palace_transitions) exclusions.update(white_palace_events) @@ -200,7 +201,7 @@ class HKWorld(World): # check for any goal that godhome events are relevant to all_event_names = event_names.copy() - if self.multiworld.Goal[self.player] in [Goal.option_godhome, Goal.option_godhome_flower]: + if self.options.Goal in [Goal.option_godhome, Goal.option_godhome_flower]: from .GodhomeData import godhome_event_names all_event_names.update(set(godhome_event_names)) @@ -230,12 +231,12 @@ class HKWorld(World): pool: typing.List[HKItem] = [] wp_exclusions = self.white_palace_exclusions() junk_replace: typing.Set[str] = set() - if self.multiworld.RemoveSpellUpgrades[self.player]: + if self.options.RemoveSpellUpgrades: junk_replace.update(("Abyss_Shriek", "Shade_Soul", "Descending_Dark")) randomized_starting_items = set() for attr, items in randomizable_starting_items.items(): - if getattr(self.multiworld, attr)[self.player]: + if getattr(self.options, attr): randomized_starting_items.update(items) # noinspection PyShadowingNames @@ -257,7 +258,7 @@ class HKWorld(World): if item_name in junk_replace: item_name = self.get_filler_item_name() - item = self.create_item(item_name) if not vanilla or location_name == "Start" or self.multiworld.AddUnshuffledLocations[self.player] else self.create_event(item_name) + item = self.create_item(item_name) if not vanilla or location_name == "Start" or self.options.AddUnshuffledLocations else self.create_event(item_name) if location_name == "Start": if item_name in randomized_starting_items: @@ -281,55 +282,55 @@ class HKWorld(World): location.progress_type = LocationProgressType.EXCLUDED for option_key, option in hollow_knight_randomize_options.items(): - randomized = getattr(self.multiworld, option_key)[self.player] - if all([not randomized, option_key in logicless_options, not self.multiworld.AddUnshuffledLocations[self.player]]): + randomized = getattr(self.options, option_key) + if all([not randomized, option_key in logicless_options, not self.options.AddUnshuffledLocations]): continue for item_name, location_name in zip(option.items, option.locations): if item_name in junk_replace: item_name = self.get_filler_item_name() - if (item_name == "Crystal_Heart" and self.multiworld.SplitCrystalHeart[self.player]) or \ - (item_name == "Mothwing_Cloak" and self.multiworld.SplitMothwingCloak[self.player]): + if (item_name == "Crystal_Heart" and self.options.SplitCrystalHeart) or \ + (item_name == "Mothwing_Cloak" and self.options.SplitMothwingCloak): _add("Left_" + item_name, location_name, randomized) _add("Right_" + item_name, "Split_" + location_name, randomized) continue - if item_name == "Mantis_Claw" and self.multiworld.SplitMantisClaw[self.player]: + if item_name == "Mantis_Claw" and self.options.SplitMantisClaw: _add("Left_" + item_name, "Left_" + location_name, randomized) _add("Right_" + item_name, "Right_" + location_name, randomized) continue - if item_name == "Shade_Cloak" and self.multiworld.SplitMothwingCloak[self.player]: - if self.multiworld.random.randint(0, 1): + if item_name == "Shade_Cloak" and self.options.SplitMothwingCloak: + if self.random.randint(0, 1): item_name = "Left_Mothwing_Cloak" else: item_name = "Right_Mothwing_Cloak" - if item_name == "Grimmchild2" and self.multiworld.RandomizeGrimmkinFlames[self.player] and self.multiworld.RandomizeCharms[self.player]: + if item_name == "Grimmchild2" and self.options.RandomizeGrimmkinFlames and self.options.RandomizeCharms: _add("Grimmchild1", location_name, randomized) continue _add(item_name, location_name, randomized) - if self.multiworld.RandomizeElevatorPass[self.player]: + if self.options.RandomizeElevatorPass: randomized = True _add("Elevator_Pass", "Elevator_Pass", randomized) for shop, locations in self.created_multi_locations.items(): - for _ in range(len(locations), getattr(self.multiworld, shop_to_option[shop])[self.player].value): + for _ in range(len(locations), getattr(self.options, shop_to_option[shop]).value): loc = self.create_location(shop) unfilled_locations += 1 # Balance the pool item_count = len(pool) - additional_shop_items = max(item_count - unfilled_locations, self.multiworld.ExtraShopSlots[self.player].value) + additional_shop_items = max(item_count - unfilled_locations, self.options.ExtraShopSlots.value) # Add additional shop items, as needed. if additional_shop_items > 0: shops = list(shop for shop, locations in self.created_multi_locations.items() if len(locations) < 16) - if not self.multiworld.EggShopSlots[self.player].value: # No eggshop, so don't place items there + if not self.options.EggShopSlots: # No eggshop, so don't place items there shops.remove('Egg_Shop') if shops: for _ in range(additional_shop_items): - shop = self.multiworld.random.choice(shops) + shop = self.random.choice(shops) loc = self.create_location(shop) unfilled_locations += 1 if len(self.created_multi_locations[shop]) >= 16: @@ -355,7 +356,7 @@ class HKWorld(World): loc.costs = costs def apply_costsanity(self): - setting = self.multiworld.CostSanity[self.player].value + setting = self.options.CostSanity.value if not setting: return # noop @@ -369,10 +370,10 @@ class HKWorld(World): return {k: v for k, v in weights.items() if v} - random = self.multiworld.random - hybrid_chance = getattr(self.multiworld, f"CostSanityHybridChance")[self.player].value + random = self.random + hybrid_chance = getattr(self.options, f"CostSanityHybridChance").value weights = { - data.term: getattr(self.multiworld, f"CostSanity{data.option}Weight")[self.player].value + data.term: getattr(self.options, f"CostSanity{data.option}Weight").value for data in cost_terms.values() } weights_geoless = dict(weights) @@ -427,22 +428,22 @@ class HKWorld(World): location.sort_costs() def set_rules(self): - world = self.multiworld + multiworld = self.multiworld player = self.player - goal = world.Goal[player] + goal = self.options.Goal if goal == Goal.option_hollowknight: - world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) + multiworld.completion_condition[player] = lambda state: _hk_can_beat_thk(state, player) elif goal == Goal.option_siblings: - world.completion_condition[player] = lambda state: state._hk_siblings_ending(player) + multiworld.completion_condition[player] = lambda state: _hk_siblings_ending(state, player) elif goal == Goal.option_radiance: - world.completion_condition[player] = lambda state: state._hk_can_beat_radiance(player) + multiworld.completion_condition[player] = lambda state: _hk_can_beat_radiance(state, player) elif goal == Goal.option_godhome: - world.completion_condition[player] = lambda state: state.count("Defeated_Pantheon_5", player) + multiworld.completion_condition[player] = lambda state: state.count("Defeated_Pantheon_5", player) elif goal == Goal.option_godhome_flower: - world.completion_condition[player] = lambda state: state.count("Godhome_Flower_Quest", player) + multiworld.completion_condition[player] = lambda state: state.count("Godhome_Flower_Quest", player) else: # Any goal - world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) or state._hk_can_beat_radiance(player) + multiworld.completion_condition[player] = lambda state: _hk_can_beat_thk(state, player) or _hk_can_beat_radiance(state, player) set_rules(self) @@ -450,8 +451,8 @@ class HKWorld(World): slot_data = {} options = slot_data["options"] = {} - for option_name in self.option_definitions: - option = getattr(self.multiworld, option_name)[self.player] + for option_name in hollow_knight_options: + option = getattr(self.options, option_name) try: optionvalue = int(option.value) except TypeError: @@ -460,10 +461,10 @@ class HKWorld(World): options[option_name] = optionvalue # 32 bit int - slot_data["seed"] = self.multiworld.per_slot_randoms[self.player].randint(-2147483647, 2147483646) + slot_data["seed"] = self.random.randint(-2147483647, 2147483646) # Backwards compatibility for shop cost data (HKAP < 0.1.0) - if not self.multiworld.CostSanity[self.player]: + if not self.options.CostSanity: for shop, terms in shop_cost_types.items(): unit = cost_terms[next(iter(terms))].option if unit == "Geo": @@ -498,7 +499,7 @@ class HKWorld(World): basename = name if name in shop_cost_types: costs = { - term: self.multiworld.random.randint(*self.ranges[term]) + term: self.random.randint(*self.ranges[term]) for term in shop_cost_types[name] } elif name in vanilla_location_costs: @@ -512,7 +513,7 @@ class HKWorld(World): region = self.multiworld.get_region("Menu", self.player) - if vanilla and not self.multiworld.AddUnshuffledLocations[self.player]: + if vanilla and not self.options.AddUnshuffledLocations: loc = HKLocation(self.player, name, None, region, costs=costs, vanilla=vanilla, basename=basename) @@ -560,26 +561,26 @@ class HKWorld(World): return change @classmethod - def stage_write_spoiler(cls, world: MultiWorld, spoiler_handle): - hk_players = world.get_game_players(cls.game) + def stage_write_spoiler(cls, multiworld: MultiWorld, spoiler_handle): + hk_players = multiworld.get_game_players(cls.game) spoiler_handle.write('\n\nCharm Notches:') for player in hk_players: - name = world.get_player_name(player) + name = multiworld.get_player_name(player) spoiler_handle.write(f'\n{name}\n') - hk_world: HKWorld = world.worlds[player] + hk_world: HKWorld = multiworld.worlds[player] for charm_number, cost in enumerate(hk_world.charm_costs): spoiler_handle.write(f"\n{charm_names[charm_number]}: {cost}") spoiler_handle.write('\n\nShop Prices:') for player in hk_players: - name = world.get_player_name(player) + name = multiworld.get_player_name(player) spoiler_handle.write(f'\n{name}\n') - hk_world: HKWorld = world.worlds[player] + hk_world: HKWorld = multiworld.worlds[player] - if world.CostSanity[player].value: + if hk_world.options.CostSanity: for loc in sorted( ( - loc for loc in itertools.chain(*(region.locations for region in world.get_regions(player))) + loc for loc in itertools.chain(*(region.locations for region in multiworld.get_regions(player))) if loc.costs ), key=operator.attrgetter('name') ): @@ -603,15 +604,15 @@ class HKWorld(World): 'RandomizeGeoRocks', 'RandomizeSoulTotems', 'RandomizeLoreTablets', 'RandomizeJunkPitChests', 'RandomizeRancidEggs' ): - if getattr(self.multiworld, group): + if getattr(self.options, group): fillers.extend(item for item in hollow_knight_randomize_options[group].items if item not in exclusions) self.cached_filler_items[self.player] = fillers - return self.multiworld.random.choice(self.cached_filler_items[self.player]) + return self.random.choice(self.cached_filler_items[self.player]) -def create_region(world: MultiWorld, player: int, name: str, location_names=None) -> Region: - ret = Region(name, player, world) +def create_region(multiworld: MultiWorld, player: int, name: str, location_names=None) -> Region: + ret = Region(name, player, multiworld) if location_names: for location in location_names: loc_id = HKWorld.location_name_to_id.get(location, None) @@ -684,42 +685,7 @@ class HKLogicMixin(LogicMixin): return sum(self.multiworld.worlds[player].charm_costs[notch] for notch in notches) def _hk_option(self, player: int, option_name: str) -> int: - return getattr(self.multiworld, option_name)[player].value + return getattr(self.multiworld.worlds[player].options, option_name).value def _hk_start(self, player, start_location: str) -> bool: - return self.multiworld.StartLocation[player] == start_location - - def _hk_nail_combat(self, player: int) -> bool: - return self.has_any({'LEFTSLASH', 'RIGHTSLASH', 'UPSLASH'}, player) - - def _hk_can_beat_thk(self, player: int) -> bool: - return ( - self.has('Opened_Black_Egg_Temple', player) - and (self.count('FIREBALL', player) + self.count('SCREAM', player) + self.count('QUAKE', player)) > 1 - and self._hk_nail_combat(player) - and ( - self.has_any({'LEFTDASH', 'RIGHTDASH'}, player) - or self._hk_option(player, 'ProficientCombat') - ) - and self.has('FOCUS', player) - ) - - def _hk_siblings_ending(self, player: int) -> bool: - return self._hk_can_beat_thk(player) and self.has('WHITEFRAGMENT', player, 3) - - def _hk_can_beat_radiance(self, player: int) -> bool: - return ( - self.has('Opened_Black_Egg_Temple', player) - and self._hk_nail_combat(player) - and self.has('WHITEFRAGMENT', player, 3) - and self.has('DREAMNAIL', player) - and ( - (self.has('LEFTCLAW', player) and self.has('RIGHTCLAW', player)) - or self.has('WINGS', player) - ) - and (self.count('FIREBALL', player) + self.count('SCREAM', player) + self.count('QUAKE', player)) > 1 - and ( - (self.has('LEFTDASH', player, 2) and self.has('RIGHTDASH', player, 2)) # Both Shade Cloaks - or (self._hk_option(player, 'ProficientCombat') and self.has('QUAKE', player)) # or Dive - ) - ) + return self.multiworld.worlds[player].options.StartLocation == start_location From fac72dbc207f698f8c61f37953189f1f8a55de66 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Mon, 29 Jul 2024 13:40:58 -0400 Subject: [PATCH 10/13] FFMQ: Fix reset protection (#3710) * Revert reset protection * Fix reset protection --------- Co-authored-by: alchav --- worlds/ffmq/Client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ffmq/Client.py b/worlds/ffmq/Client.py index 6cb35dd3b4..93688a6116 100644 --- a/worlds/ffmq/Client.py +++ b/worlds/ffmq/Client.py @@ -71,7 +71,7 @@ class FFMQClient(SNIClient): received = await snes_read(ctx, RECEIVED_DATA[0], RECEIVED_DATA[1]) data = await snes_read(ctx, READ_DATA_START, READ_DATA_END - READ_DATA_START) check_2 = await snes_read(ctx, 0xF53749, 1) - if check_1 != b'01' or check_2 != b'01': + if check_1 != b'\x01' or check_2 != b'\x01': return def get_range(data_range): From 80daa092a7782c941411cd1568f232e7781cc316 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Mon, 29 Jul 2024 13:42:16 -0400 Subject: [PATCH 11/13] - Take shipsanity moss out of shipsanity crops (#3709) --- worlds/stardew_valley/data/locations.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv index 0d7a10f954..608b6a5f57 100644 --- a/worlds/stardew_valley/data/locations.csv +++ b/worlds/stardew_valley/data/locations.csv @@ -2212,7 +2212,7 @@ id,region,name,tags,mod_name 3808,Shipping,Shipsanity: Mystery Box,"SHIPSANITY", 3809,Shipping,Shipsanity: Golden Tag,"SHIPSANITY", 3810,Shipping,Shipsanity: Deluxe Bait,"SHIPSANITY", -3811,Shipping,Shipsanity: Moss,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", +3811,Shipping,Shipsanity: Moss,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", 3812,Shipping,Shipsanity: Mossy Seed,"SHIPSANITY", 3813,Shipping,Shipsanity: Sonar Bobber,"SHIPSANITY", 3814,Shipping,Shipsanity: Tent Kit,"SHIPSANITY", From 954d72800541c8bc6e515f9e046d50a203ac9ab5 Mon Sep 17 00:00:00 2001 From: Phaneros <31861583+MatthewMarinets@users.noreply.github.com> Date: Mon, 29 Jul 2024 14:09:51 -0700 Subject: [PATCH 12/13] sc2: Removing unused dependency in requirements.txt (#3697) * sc2: Removing unused dependency in requirements.txt * sc2: Add missing newline in requirements.txt Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/sc2/requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/worlds/sc2/requirements.txt b/worlds/sc2/requirements.txt index 9b84863c45..5bc808b639 100644 --- a/worlds/sc2/requirements.txt +++ b/worlds/sc2/requirements.txt @@ -1,2 +1 @@ nest-asyncio >= 1.5.5 -six >= 1.16.0 \ No newline at end of file From 77e3f9fbefa32a51c6c6ce1c6a0aee584c0a72f1 Mon Sep 17 00:00:00 2001 From: Remy Jette Date: Mon, 29 Jul 2024 20:13:44 -0400 Subject: [PATCH 13/13] WebHost: Fix NamedRange values clamping to the range (#3613) If a NamedRange has a `special_range_names` entry outside the `range_start` and `range_end`, the HTML5 range input will clamp the submitted value to the closest value in the range. These means that, for example, Pokemon RB's "HM Compatibility" option's "Vanilla (-1)" option would instead get posted as "0" rather than "-1". This change updates NamedRange to behave like TextChoice, where the select element has a `name` attribute matching the option, and there is an additional element to be able to provide an option other than the select element's choices. This uses a different suffix of `-range` rather than `-custom` that TextChoice uses. The reason is we need some way to decide whether to use the custom value or the select value, and that method needs to work without JavaScript. For TextChoice this is easy, if the custom field is empty use the select element. For NamedRange this is more difficult as the browser will always submit *something*. My choice was to only use the value from the range if the select box is set to "custom". Since this only happens with JS as "custom' is hidden, I made the range hidden under no-JS. If it's preferred, I could make the select box hidden instead. Let me know. This PR also makes the `js-required` class set `display: none` with `!important` as otherwise the class wouldn't work on any rule that had `display: flex` with more specificity than a single class. --- WebHostLib/options.py | 7 +++++++ WebHostLib/templates/playerOptions/macros.html | 8 ++++---- WebHostLib/templates/playerOptions/playerOptions.html | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 33339daa19..15b7bd61ce 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -231,6 +231,13 @@ def generate_yaml(game: str): del options[key] + # Detect keys which end with -range, indicating a NamedRange with a possible custom value + elif key_parts[-1].endswith("-range"): + if options[key_parts[-1][:-6]] == "custom": + options[key_parts[-1][:-6]] = val + + del options[key] + # Detect random-* keys and set their options accordingly for key, val in options.copy().items(): if key.startswith("random-"): diff --git a/WebHostLib/templates/playerOptions/macros.html b/WebHostLib/templates/playerOptions/macros.html index 415739b861..30a4fc78df 100644 --- a/WebHostLib/templates/playerOptions/macros.html +++ b/WebHostLib/templates/playerOptions/macros.html @@ -54,7 +54,7 @@ {% macro NamedRange(option_name, option) %} {{ OptionTitle(option_name, option) }}
- {% for key, val in option.special_range_names.items() %} {% if option.default == val %} @@ -64,17 +64,17 @@ {% endfor %} -
+
- + {{ option.default | default(option.range_start) if option.default != "random" else option.range_start }} {{ RandomizeButton(option_name, option) }} diff --git a/WebHostLib/templates/playerOptions/playerOptions.html b/WebHostLib/templates/playerOptions/playerOptions.html index aeb6e864a5..73de5d56eb 100644 --- a/WebHostLib/templates/playerOptions/playerOptions.html +++ b/WebHostLib/templates/playerOptions/playerOptions.html @@ -11,7 +11,7 @@