From 285c5755898b74551a807f8b11471ecfae8e6b46 Mon Sep 17 00:00:00 2001 From: Faris Date: Sat, 23 May 2026 20:43:55 -0500 Subject: [PATCH 01/12] All changes needed for UT compatibility --- worlds/hk/__init__.py | 76 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 9 deletions(-) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 6125641e8502..4ec937c9e8c2 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -72,6 +72,10 @@ class HKWorld(RandomizerCoreWorld): item_name_groups = datapackage_item_groups location_name_groups = datapackage_location_groups + #UT Attributes + ut_can_gen_without_yaml = True + found_entrances_datastorage_key = "Slot:{player}:visited_transitions" #A Template string, not an f-string + rc_regions: list[dict[str, Any]] = hk_regions rc_locations: list[dict[str, Any]] = hk_locations item_class = HKItem @@ -114,10 +118,35 @@ def __init__(self, multiworld, player): self.entrance_by_term = defaultdict(list) self.entrance_pairs = {} self.entrance_state_modifier_by_term = defaultdict(list) + self.pre_defined_location_costs = {} # generate_early def generate_early(self): + options = self.options + + if hasattr(self.multiworld,"re_gen_passthrough") and self.game in getattr(self.multiworld,"re_gen_passthrough"): #UT slot_data + slot_data = getattr(self.multiworld,"re_gen_passthrough")[self.game] + remote_options = slot_data["options"] + for option_name, option_value in remote_options.items(): + if hasattr(options,option_name): + setattr(getattr(options,option_name),"value",option_value) + self.pre_defined_location_costs=slot_data["location_costs"] + self.charm_costs = slot_data["notch_costs"] + + self.grub_count = slot_data["grub_count"] + + self.entrance_pairs = slot_data["entrance_pairs"] + else: + charm_costs = options.RandomCharmCosts.get_costs(self.random) + self.charm_costs = options.PlandoCharmCosts.get_costs(charm_costs) + # defaulting so completion condition isn't incorrect before pre_fill + self.grub_count = ( + 46 if options.GrubHuntGoal == GrubHuntGoal.special_range_names["all"] + else options.GrubHuntGoal.value + ) + + weights = { data.term: getattr(options, f"CostSanity{data.option}Weight").value for data in cost_terms.values() @@ -144,8 +173,6 @@ def generate_early(self): else: self.ranges[term] = mini.value, maxi.value - charm_costs = options.RandomCharmCosts.get_costs(self.random) - self.charm_costs = options.PlandoCharmCosts.get_costs(charm_costs) self.charm_names_and_costs = {name: (self.charm_costs[index] if name != "Void_Heart" else 0) for name, index in charm_name_to_id.items()} self.soul_modes = self.options.EntranceRandoType.soul_mode @@ -169,11 +196,6 @@ def generate_early(self): ] # actually connect it later once we have regions created - # defaulting so completion condition isn't incorrect before pre_fill - self.grub_count = ( - 46 if options.GrubHuntGoal == GrubHuntGoal.special_range_names["all"] - else options.GrubHuntGoal.value - ) self.grub_player_count = {self.player: self.grub_count} @classmethod @@ -472,6 +494,8 @@ def _stateless_connect_one_way(self, source_exit: Entrance, target_entrance: Ent def connect_entrances(self): if not self.options.EntranceRandoType: return + if hasattr(self.multiworld, "generation_is_fake"): + return #In UT gen we don't want GER to re-randomize the entrances coupled = self.options.ShuffleEntrancesMode != ShuffleEntrancesMode.option_decoupled def _connect_entrances(group: str, entrances: list[HKEntrance] | None) -> None: @@ -625,6 +649,17 @@ def setup_connections(self): region2 = self.get_region(structure_transition_to_region_map[trans_data["vanilla_target"]]) region1.connect(region2, name) + if hasattr(self.multiworld,"generation_is_fake") and getattr(self.multiworld,"enforce_deferred_connections","off") == "off": #UT flag + #In UT generation, we already got connection_pairs from slot_data + for source,target in self.entrance_pairs.items(): + exit_obj = self.get_entrance(source) + exit_region = self.get_region(structure_transition_to_region_map[target]) + exits = [entrance for entrance in exit_region.entrances if entrance.name==target] + if len(exits) != 1: + raise Exception("More then one viable target somehow") + self._stateless_connect_one_way(exit_obj,exits[0]) + return + if not one_ways: return @@ -633,6 +668,27 @@ def setup_connections(self): for entrance, exit in zip(one_ways["OneWayOut"], one_ways["OneWayIn"], strict=True): self._stateless_connect_one_way(exit, entrance) + def reconnect_found_entrances(self, found_key:str,data_storage_value:dict[str,bool]): + for entrance_checked in data_storage_value.keys(): + entrance = self.get_entrance(entrance_checked) + if entrance.connected_region is None: + target = self.entrance_pairs[entrance_checked] + target_region = self.get_region(structure_transition_to_region_map[target]) + exits = [entrance for entrance in target_region.entrances if entrance.name==target] + if len(exits) != 1: + raise Exception("More then one viable target somehow") + exit_name = exits[0].name + self._stateless_connect_one_way(entrance,exits[0]) + if self.options.ShuffleEntrancesMode != ShuffleEntrancesMode.option_decoupled: + reverse_entrance = self.get_entrance(exit_name) + if reverse_entrance.connected_region is None: + assert entrance.parent_region + reverse_exits = [entrance for entrance in entrance.parent_region.entrances if entrance.name==entrance_checked] + if len(reverse_exits) != 1: + raise Exception("More then one viable target somehow") + self._stateless_connect_one_way(reverse_entrance,reverse_exits[0]) + + def add_all_events(self): location_to_region = {loc: reg["name"] for reg in structure_regions for loc in reg["locations"]} @@ -681,9 +737,12 @@ def add_shop_location(self, shop, index=None): index = len(self.created_multi_locations[shop]) lookup_shop = shop if "(Requires_Charms)" not in shop else "Salubra" # fix because Salubra_(Requires_Charms) isn't actually in the source data + location_name = f"{shop}_{index+1}" costs = None - if shop in shop_cost_types: + if location_name in self.pre_defined_location_costs: + costs = self.pre_defined_location_costs[location_name] + elif shop in shop_cost_types: costs = { term: self.random.randint(*self.ranges[term]) for term in shop_cost_types[shop] @@ -691,7 +750,6 @@ def add_shop_location(self, shop, index=None): rule = self.rule_lookup.get(lookup_shop) region = self.get_region(self.region_lookup[lookup_shop]) - location_name = f"{shop}_{index+1}" code = self.location_name_to_id[location_name] loc = self.location_class(self.player, location_name, code, region) From aa804613c5990ecde1e5e3893a0d4de5ddb200c3 Mon Sep 17 00:00:00 2001 From: Faris Date: Wed, 27 May 2026 21:14:25 -0500 Subject: [PATCH 02/12] Trying to fix shop costs... we have not done it --- worlds/hk/__init__.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 4ec937c9e8c2..5cfe1d54b8b9 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -669,6 +669,8 @@ def setup_connections(self): self._stateless_connect_one_way(exit, entrance) def reconnect_found_entrances(self, found_key:str,data_storage_value:dict[str,bool]): + if not data_storage_value: + return for entrance_checked in data_storage_value.keys(): entrance = self.get_entrance(entrance_checked) if entrance.connected_region is None: @@ -740,9 +742,9 @@ def add_shop_location(self, shop, index=None): location_name = f"{shop}_{index+1}" costs = None - if location_name in self.pre_defined_location_costs: - costs = self.pre_defined_location_costs[location_name] - elif shop in shop_cost_types: + #if location_name in self.pre_defined_location_costs: + # costs = self.pre_defined_location_costs[location_name] + if shop in shop_cost_types: costs = { term: self.random.randint(*self.ranges[term]) for term in shop_cost_types[shop] @@ -773,7 +775,8 @@ def add_shop_locations(self): def add_extra_shop_locations(self, count): # Add additional shop items, as needed. - if not count > 0: + gen_is_fake = hasattr(self.multiworld, "generation_is_fake") + if not count > 0 and not gen_is_fake: return shops = [shop for shop, locations in self.created_multi_locations.items() if len(locations) < 16] if not self.options.EggShopSlots.value: # No eggshop, so don't place items there @@ -781,6 +784,12 @@ def add_extra_shop_locations(self, count): if not shops: return + if gen_is_fake: + for shop in shops: + while len(self.created_multi_locations[shop]) < 16: + index = len(self.created_multi_locations[shop]) + self.add_shop_location(shop, index) + return #In UT gen, create all shops for _ in range(count): shop = self.random.choice(shops) index = len(self.created_multi_locations[shop]) @@ -955,6 +964,9 @@ def _compute_weights(weights: dict, desc: str) -> dict[str, int]: continue if not location.costs: continue + if location.name in self.pre_defined_location_costs: + location.costs = self.pre_defined_location_costs[location.name] + continue if location.name == "Vessel_Fragment-Basin": continue if setting == CostSanity.option_notshops and location.basename in self.created_multi_locations: @@ -983,6 +995,8 @@ def _compute_weights(weights: dict, desc: str) -> dict[str, int]: location.sort_costs() def sort_shops_by_cost(self): + if hasattr(self.multiworld, "generation_is_fake"): + return #In a UT gen we've already placed these where they should go for shop_locations in self.created_multi_locations.values(): randomized_locations = [loc for loc in shop_locations if not loc.vanilla] if not randomized_locations: From bf650b5ba7f0a50d953063f4b407f268eebce184 Mon Sep 17 00:00:00 2001 From: Faris Date: Sat, 20 Jun 2026 21:09:19 -0500 Subject: [PATCH 03/12] finish fixing shops --- worlds/hk/__init__.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 5cfe1d54b8b9..a676f1346252 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -742,9 +742,9 @@ def add_shop_location(self, shop, index=None): location_name = f"{shop}_{index+1}" costs = None - #if location_name in self.pre_defined_location_costs: - # costs = self.pre_defined_location_costs[location_name] - if shop in shop_cost_types: + if location_name in self.pre_defined_location_costs: + costs = self.pre_defined_location_costs[location_name] + elif shop in shop_cost_types: costs = { term: self.random.randint(*self.ranges[term]) for term in shop_cost_types[shop] @@ -789,15 +789,15 @@ def add_extra_shop_locations(self, count): while len(self.created_multi_locations[shop]) < 16: index = len(self.created_multi_locations[shop]) self.add_shop_location(shop, index) - return #In UT gen, create all shops - for _ in range(count): - shop = self.random.choice(shops) - index = len(self.created_multi_locations[shop]) - self.add_shop_location(shop, index) - if len(self.created_multi_locations[shop]) >= 16: - shops.remove(shop) - if not shops: - break + else: + for _ in range(count): + shop = self.random.choice(shops) + index = len(self.created_multi_locations[shop]) + self.add_shop_location(shop, index) + if len(self.created_multi_locations[shop]) >= 16: + shops.remove(shop) + if not shops: + break # create_items def create_items(self): From f30491a0b3c173fab90ca5ef07732f2a02e1403c Mon Sep 17 00:00:00 2001 From: Faris Date: Sun, 21 Jun 2026 01:13:33 -0500 Subject: [PATCH 04/12] Fixing decoupled tracking --- worlds/hk/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 7b597fb0c3ea..6f9d641866c1 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -686,9 +686,9 @@ def setup_connections(self): for source,target in self.entrance_pairs.items(): exit_obj = self.get_entrance(source) exit_region = self.get_region(structure_transition_to_region_map[target]) - exits = [entrance for entrance in exit_region.entrances if entrance.name==target] + exits = [entrance for entrance in exit_region.entrances if entrance.name==target and entrance.parent_region is None] if len(exits) != 1: - raise Exception("More then one viable target somehow") + raise Exception(f"More then one viable target somehow {exits}") self._stateless_connect_one_way(exit_obj,exits[0]) return From 83be91d44b96a5fdadd64f46199741433bb7ad91 Mon Sep 17 00:00:00 2001 From: Faris Date: Thu, 2 Jul 2026 17:30:04 -0500 Subject: [PATCH 05/12] Adding explain and get_logical_path overrides --- worlds/hk/__init__.py | 78 ++++++++++++++++++++++- worlds/hk/resource_state_vars/__init__.py | 3 + 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 6f9d641866c1..e86bfa67ee74 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -5,7 +5,7 @@ from copy import deepcopy from typing import Any, ClassVar -from BaseClasses import CollectionState, Entrance, EntranceType, ItemClassification, LocationProgressType, MultiWorld +from BaseClasses import CollectionState, Entrance, EntranceType, ItemClassification, LocationProgressType, MultiWorld, Region from entrance_rando import EntranceRandomizationError, randomize_entrances from Options import OptionError @@ -722,7 +722,81 @@ def reconnect_found_entrances(self, found_key:str,data_storage_value:dict[str,bo raise Exception("More then one viable target somehow") self._stateless_connect_one_way(reverse_entrance,reverse_exits[0]) - + def parse_clause(self, clause:HKClause, parent_region: Region, state: CollectionState) -> list: + l_return = [] + for item,count in clause.hk_item_requirements.items(): + valid = state.has(item,self.player,count) + l_return.append({"type":"color","color":"green" if valid else "red","text":item if count==1 else f"{item}:{count}"}) + l_return.append({"type":"text","text":", "}) + for region in clause.hk_region_requirements: + valid = state.can_reach_region(region,self.player) + l_return.append({"type":"color","color":"green" if valid else "red","text":region}) + l_return.append({"type":"text","text":", "}) + if clause.hk_state_requirements: + valid = state.can_reach_region(parent_region.name,self.player) and state._hk_test_fake_state(clause,parent_region) + l_return.append({"type":"color","color":"green" if valid else "red","text":str(clause.hk_state_requirements)}) + l_return.append({"type":"text","text":", "}) + return l_return[:-1] + + def explain_path(self, entrance: Entrance, state: CollectionState) -> list: + hkClause = getattr(entrance,"hk_rule",None) + if not isinstance(hkClause,list): + return [] + l_return = [{"type":"color","color":"blue","text":entrance.name}] + for index,clause in enumerate(hkClause): + if not isinstance(clause,HKClause): + continue #maybe fix later? + l_return.append({"type":"text","text":f"\nClause {index+1} - "}) + l_return.extend(self.parse_clause(clause,entrance.parent_region,state)) + return l_return + + def explain_rule(self, target_name: str, state: CollectionState) -> list: + l_return = [] + + target = None + parent_region = None + if target_name in self.multiworld.regions.region_cache[self.player]: + target = self.get_region(target_name) + parent_region = target + # Regions have to be dealt with differently, but they don't directly have rules or costs so it's fine + for ent in target.entrances: + l_return.extend(self.explain_path(ent,state)) + l_return.append({"type":"text","text":f"\n"}) + l_return.pop() + return l_return + elif target_name in self.multiworld.regions.entrance_cache[self.player]: + target = self.get_entrance(target_name) + parent_region = target.parent_region + elif target_name in self.multiworld.regions.location_cache[self.player]: + target = self.get_location(target_name) + parent_region = target.parent_region + + if target is None or parent_region is None: + return [] + hkClause = getattr(target,"hk_rule",None) + if not isinstance(hkClause,list): + l_return.append({"type":"text","text":"Default Access"}) + else: + for index,clause in enumerate(hkClause): + if not isinstance(clause,HKClause): + continue + l_return.append({"type":"text","text":f"\nClause {index+1} - "}) + l_return.extend(self.parse_clause(clause,parent_region,state)) + costs = getattr(target,"costs",None) + if isinstance(costs,dict): + l_return.append({"type":"text","text":"\nCosts - ["}) + for cost,count in costs.items(): + valid = False + if cost == "GEO": + valid = state.has("Can_Replenish_Geo", self.player) + else: + valid = state.has(cost,self.player,count) + l_return.append({"type":"color","color":"green" if valid else "red","text":f"{cost}:{count}"}) + l_return.append({"type":"text","text":", "}) + l_return.pop() #Remove the last comma + l_return.append({"type":"text","text":"]"}) #And replace with a close bracket + return l_return + def add_all_events(self): location_to_region = {loc: reg["name"] for reg in structure_regions for loc in reg["locations"]} diff --git a/worlds/hk/resource_state_vars/__init__.py b/worlds/hk/resource_state_vars/__init__.py index 9e829d123061..8e535a00e9b6 100644 --- a/worlds/hk/resource_state_vars/__init__.py +++ b/worlds/hk/resource_state_vars/__init__.py @@ -178,6 +178,9 @@ def __init__(self, term: str, player: int): else: self.parse_term() + def __repr__(self) -> str: + return self.term_name + def parse_term(self, *args) -> None: """Subclasses should use this to expect parameter counts for init""" pass From 8126719684dde219af8d9665a54ab4012e81fdf6 Mon Sep 17 00:00:00 2001 From: Faris Date: Sat, 4 Jul 2026 13:55:19 -0500 Subject: [PATCH 06/12] Adding explain_spot --- worlds/hk/__init__.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index e86bfa67ee74..750b12699660 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -5,7 +5,7 @@ from copy import deepcopy from typing import Any, ClassVar -from BaseClasses import CollectionState, Entrance, EntranceType, ItemClassification, LocationProgressType, MultiWorld, Region +from BaseClasses import CollectionState, Entrance, EntranceType, ItemClassification, LocationProgressType, MultiWorld, Region, Location from entrance_rando import EntranceRandomizationError, randomize_entrances from Options import OptionError @@ -732,7 +732,7 @@ def parse_clause(self, clause:HKClause, parent_region: Region, state: Collection valid = state.can_reach_region(region,self.player) l_return.append({"type":"color","color":"green" if valid else "red","text":region}) l_return.append({"type":"text","text":", "}) - if clause.hk_state_requirements: + if clause.hk_state_requirements and parent_region: valid = state.can_reach_region(parent_region.name,self.player) and state._hk_test_fake_state(clause,parent_region) l_return.append({"type":"color","color":"green" if valid else "red","text":str(clause.hk_state_requirements)}) l_return.append({"type":"text","text":", "}) @@ -750,6 +750,19 @@ def explain_path(self, entrance: Entrance, state: CollectionState) -> list: l_return.extend(self.parse_clause(clause,entrance.parent_region,state)) return l_return + def explain_spot(self, location: Location, state: CollectionState) -> list: + hkClause = getattr(location,"hk_rule",None) + if not isinstance(hkClause,list): + return [] + l_return = [{"type":"color","color":"green","text":f" -> {location.name}"}] + for index,clause in enumerate(hkClause): + if not isinstance(clause,HKClause): + continue #maybe fix later? + l_return.append({"type":"text","text":f"\nClause {index+1} - "}) + l_return.extend(self.parse_clause(clause,location.parent_region,state)) + return l_return + + def explain_rule(self, target_name: str, state: CollectionState) -> list: l_return = [] From c9658c0bf0fb6da216259209d54fc907e40788b4 Mon Sep 17 00:00:00 2001 From: Faris Date: Sat, 25 Jul 2026 17:26:42 -0500 Subject: [PATCH 07/12] First round of fixes from review comments --- worlds/hk/__init__.py | 129 +++++++++-------------------------------- worlds/hk/ut_things.py | 102 ++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 103 deletions(-) create mode 100644 worlds/hk/ut_things.py diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 750b12699660..e5f0ee6cf21d 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -5,7 +5,7 @@ from copy import deepcopy from typing import Any, ClassVar -from BaseClasses import CollectionState, Entrance, EntranceType, ItemClassification, LocationProgressType, MultiWorld, Region, Location +from BaseClasses import CollectionState, Entrance, EntranceType, ItemClassification, LocationProgressType, MultiWorld from entrance_rando import EntranceRandomizationError, randomize_entrances from Options import OptionError @@ -50,6 +50,7 @@ from .state_mixin import HKLogicMixin as HKLogicMixin from .state_mixin import hk_collect, hk_remove from .template_world import RandomizerCoreWorld +from .ut_things import explain_path, explain_rule, explain_spot logger = logging.getLogger("Hollow Knight") @@ -103,6 +104,11 @@ class HKWorld(RandomizerCoreWorld): collect = hk_collect remove = hk_remove + # imported UT functions + explain_rule = explain_rule + explain_spot = explain_spot + explain_path = explain_path + def __init__(self, multiworld, player): super().__init__(multiworld, player) self.created_multi_locations: dict[str, list[HKLocation]] = { @@ -116,7 +122,7 @@ def __init__(self, multiworld, player): self.entrance_by_term = defaultdict(list) self.entrance_pairs = {} self.entrance_state_modifier_by_term = defaultdict(list) - self.pre_defined_location_costs = {} + self.pre_defined_location_costs: dict[str,dict[str,int]] = {} # generate_early def generate_early(self): @@ -501,7 +507,7 @@ def _stateless_connect_one_way(self, source_exit: Entrance, target_entrance: Ent def connect_entrances(self): if not self.options.EntranceRandoType: return - if hasattr(self.multiworld, "generation_is_fake"): + if getattr(self.multiworld, "generation_is_fake",False): return #In UT gen we don't want GER to re-randomize the entrances coupled = self.options.ShuffleEntrancesMode != ShuffleEntrancesMode.option_decoupled @@ -681,14 +687,16 @@ def setup_connections(self): rule = self.create_rule(rule_data) if rule_data else None region1.connect(region2, name, rule) - if hasattr(self.multiworld,"generation_is_fake") and getattr(self.multiworld,"enforce_deferred_connections","off") == "off": #UT flag + if getattr(self.multiworld,"generation_is_fake",False) and getattr(self.multiworld,"enforce_deferred_connections","off") == "off": #UT flag #In UT generation, we already got connection_pairs from slot_data for source,target in self.entrance_pairs.items(): exit_obj = self.get_entrance(source) exit_region = self.get_region(structure_transition_to_region_map[target]) exits = [entrance for entrance in exit_region.entrances if entrance.name==target and entrance.parent_region is None] - if len(exits) != 1: - raise Exception(f"More then one viable target somehow {exits}") + if len(exits) > 1: + raise Exception(f"More then one viable for {target}, found {exits}") + elif len(exits) == 0: + raise Exception(f"Unable to find any exists to match for {target}") self._stateless_connect_one_way(exit_obj,exits[0]) return @@ -709,8 +717,10 @@ def reconnect_found_entrances(self, found_key:str,data_storage_value:dict[str,bo target = self.entrance_pairs[entrance_checked] target_region = self.get_region(structure_transition_to_region_map[target]) exits = [entrance for entrance in target_region.entrances if entrance.name==target] - if len(exits) != 1: - raise Exception("More then one viable target somehow") + if len(exits) > 1: + raise Exception(f"More then one viable for {target}, found {exits}") + elif len(exits) == 0: + raise Exception(f"Unable to find any exists to match for {target}") exit_name = exits[0].name self._stateless_connect_one_way(entrance,exits[0]) if self.options.ShuffleEntrancesMode != ShuffleEntrancesMode.option_decoupled: @@ -718,97 +728,11 @@ def reconnect_found_entrances(self, found_key:str,data_storage_value:dict[str,bo if reverse_entrance.connected_region is None: assert entrance.parent_region reverse_exits = [entrance for entrance in entrance.parent_region.entrances if entrance.name==entrance_checked] - if len(reverse_exits) != 1: - raise Exception("More then one viable target somehow") + if len(reverse_exits) > 1: + raise Exception(f"More then one viable for {entrance_checked}, found {reverse_exits}") + elif len(reverse_exits) == 0: + raise Exception(f"Unable to find any exists to match for {target}") self._stateless_connect_one_way(reverse_entrance,reverse_exits[0]) - - def parse_clause(self, clause:HKClause, parent_region: Region, state: CollectionState) -> list: - l_return = [] - for item,count in clause.hk_item_requirements.items(): - valid = state.has(item,self.player,count) - l_return.append({"type":"color","color":"green" if valid else "red","text":item if count==1 else f"{item}:{count}"}) - l_return.append({"type":"text","text":", "}) - for region in clause.hk_region_requirements: - valid = state.can_reach_region(region,self.player) - l_return.append({"type":"color","color":"green" if valid else "red","text":region}) - l_return.append({"type":"text","text":", "}) - if clause.hk_state_requirements and parent_region: - valid = state.can_reach_region(parent_region.name,self.player) and state._hk_test_fake_state(clause,parent_region) - l_return.append({"type":"color","color":"green" if valid else "red","text":str(clause.hk_state_requirements)}) - l_return.append({"type":"text","text":", "}) - return l_return[:-1] - - def explain_path(self, entrance: Entrance, state: CollectionState) -> list: - hkClause = getattr(entrance,"hk_rule",None) - if not isinstance(hkClause,list): - return [] - l_return = [{"type":"color","color":"blue","text":entrance.name}] - for index,clause in enumerate(hkClause): - if not isinstance(clause,HKClause): - continue #maybe fix later? - l_return.append({"type":"text","text":f"\nClause {index+1} - "}) - l_return.extend(self.parse_clause(clause,entrance.parent_region,state)) - return l_return - - def explain_spot(self, location: Location, state: CollectionState) -> list: - hkClause = getattr(location,"hk_rule",None) - if not isinstance(hkClause,list): - return [] - l_return = [{"type":"color","color":"green","text":f" -> {location.name}"}] - for index,clause in enumerate(hkClause): - if not isinstance(clause,HKClause): - continue #maybe fix later? - l_return.append({"type":"text","text":f"\nClause {index+1} - "}) - l_return.extend(self.parse_clause(clause,location.parent_region,state)) - return l_return - - - def explain_rule(self, target_name: str, state: CollectionState) -> list: - l_return = [] - - target = None - parent_region = None - if target_name in self.multiworld.regions.region_cache[self.player]: - target = self.get_region(target_name) - parent_region = target - # Regions have to be dealt with differently, but they don't directly have rules or costs so it's fine - for ent in target.entrances: - l_return.extend(self.explain_path(ent,state)) - l_return.append({"type":"text","text":f"\n"}) - l_return.pop() - return l_return - elif target_name in self.multiworld.regions.entrance_cache[self.player]: - target = self.get_entrance(target_name) - parent_region = target.parent_region - elif target_name in self.multiworld.regions.location_cache[self.player]: - target = self.get_location(target_name) - parent_region = target.parent_region - - if target is None or parent_region is None: - return [] - hkClause = getattr(target,"hk_rule",None) - if not isinstance(hkClause,list): - l_return.append({"type":"text","text":"Default Access"}) - else: - for index,clause in enumerate(hkClause): - if not isinstance(clause,HKClause): - continue - l_return.append({"type":"text","text":f"\nClause {index+1} - "}) - l_return.extend(self.parse_clause(clause,parent_region,state)) - costs = getattr(target,"costs",None) - if isinstance(costs,dict): - l_return.append({"type":"text","text":"\nCosts - ["}) - for cost,count in costs.items(): - valid = False - if cost == "GEO": - valid = state.has("Can_Replenish_Geo", self.player) - else: - valid = state.has(cost,self.player,count) - l_return.append({"type":"color","color":"green" if valid else "red","text":f"{cost}:{count}"}) - l_return.append({"type":"text","text":", "}) - l_return.pop() #Remove the last comma - l_return.append({"type":"text","text":"]"}) #And replace with a close bracket - return l_return def add_all_events(self): location_to_region = {loc: reg["name"] for reg in structure_regions for loc in reg["locations"]} @@ -895,7 +819,7 @@ def add_shop_locations(self): def add_extra_shop_locations(self, count): # Add additional shop items, as needed. - gen_is_fake = hasattr(self.multiworld, "generation_is_fake") + gen_is_fake = getattr(self.multiworld, "generation_is_fake", False) if not count > 0 and not gen_is_fake: return shops = [shop for shop, locations in self.created_multi_locations.items() if len(locations) < 16] @@ -906,9 +830,8 @@ def add_extra_shop_locations(self, count): return if gen_is_fake: for shop in shops: - while len(self.created_multi_locations[shop]) < 16: - index = len(self.created_multi_locations[shop]) - self.add_shop_location(shop, index) + for index in range(len(self.created_multi_locations[shop]),16): + self.add_shop_location(shop, index) # In UT excess locations are ignored, so create all shop slots and let it figure it out else: for _ in range(count): shop = self.random.choice(shops) @@ -1115,7 +1038,7 @@ def _compute_weights(weights: dict, desc: str) -> dict[str, int]: location.sort_costs() def sort_shops_by_cost(self): - if hasattr(self.multiworld, "generation_is_fake"): + if getattr(self.multiworld, "generation_is_fake", False): return #In a UT gen we've already placed these where they should go for shop_locations in self.created_multi_locations.values(): randomized_locations = [loc for loc in shop_locations if not loc.vanilla] diff --git a/worlds/hk/ut_things.py b/worlds/hk/ut_things.py new file mode 100644 index 000000000000..a0df9265af4a --- /dev/null +++ b/worlds/hk/ut_things.py @@ -0,0 +1,102 @@ +from typing import TYPE_CHECKING +from BaseClasses import CollectionState, Entrance, Location, Region +from NetUtils import JSONMessagePart +from .classes import HKClause + +if TYPE_CHECKING: + from . import HKWorld + +def parse_clause(self: "HKWorld", clause:HKClause, parent_region: Region, state: CollectionState) -> list[JSONMessagePart]: + l_return:list[JSONMessagePart] = [] + for item,count in clause.hk_item_requirements.items(): + valid = state.has(item,self.player,count) + l_return.append({"type":"color","color":"green" if valid else "red","text":item if count==1 else f"{item}:{count}"}) + l_return.append({"type":"text","text":", "}) + for region in clause.hk_region_requirements: + valid = state.can_reach_region(region,self.player) + l_return.append({"type":"color","color":"green" if valid else "red","text":region}) + l_return.append({"type":"text","text":", "}) + if clause.hk_state_requirements and parent_region: + valid = state.can_reach_region(parent_region.name,self.player) and state._hk_test_fake_state(clause,parent_region) + l_return.append({"type":"color","color":"green" if valid else "red","text":str(clause.hk_state_requirements)}) + l_return.append({"type":"text","text":", "}) + l_return.pop() # Remove the last comma + return l_return + +def explain_path(self: "HKWorld", entrance: Entrance, state: CollectionState) -> list[JSONMessagePart]: + hk_rule = getattr(entrance,"hk_rule",None) + if not isinstance(hk_rule,list): + return [] # Empty list to tell UT to use normal entrance handeling + l_return:list[JSONMessagePart] = [{"type":"color","color":"blue","text":entrance.name}] + for index,clause in enumerate(hk_rule): + if not isinstance(clause,HKClause): + continue #maybe fix later? + l_return.append({"type":"text","text":f"\nClause {index+1} - "}) + l_return.extend(parse_clause(self,clause,entrance.parent_region,state)) + return l_return + +def explain_spot(self: "HKWorld", location: Location, state: CollectionState) -> list[JSONMessagePart]: + hk_rule = getattr(location,"hk_rule",None) + if not isinstance(hk_rule,list): + return [] # Empty list to tell UT to use normal location handeling + l_return:list[JSONMessagePart] = [{"type":"color","color":"green","text":f" -> {location.name}"}] + for index,clause in enumerate(hk_rule): + if not isinstance(clause,HKClause): + continue #maybe fix later? + l_return.append({"type":"text","text":f"\nClause {index+1} - "}) + l_return.extend(parse_clause(self,clause,location.parent_region,state)) + return l_return + + +def explain_rule(self: "HKWorld", target_name: str, state: CollectionState) -> list[JSONMessagePart]: + l_return:list[JSONMessagePart] = [] + + target = None + parent_region = None + if target_name in self.multiworld.regions.region_cache[self.player]: + target = self.get_region(target_name) + parent_region = target + # Regions have to be dealt with differently, but they don't directly have rules or costs so it's fine + for ent in target.entrances: + ent_path = self.explain_path(ent,state) + if ent_path: + l_return.extend(ent_path) + l_return.append({"type":"text","text":f"\n"}) + else: # Default entrance rule + l_return.append({"type":"text","text":f"{ent.name} - "}) + passable = ent.access_rule(state) + l_return.append({"type":"color","text":"Passable" if passable else "Impassable","color":"green" if passable else "red"}) + l_return.append({"type":"text","text":f"\n"}) + l_return.pop() # Remove the last newline + return l_return + elif target_name in self.multiworld.regions.entrance_cache[self.player]: + target = self.get_entrance(target_name) + parent_region = target.parent_region + elif target_name in self.multiworld.regions.location_cache[self.player]: + target = self.get_location(target_name) + parent_region = target.parent_region + + if target is None or parent_region is None: + return [] + hk_rule = getattr(target,"hk_rule",None) + if not isinstance(hk_rule,list): + l_return.append({"type":"text","text":"Default Access"}) + else: + for index,clause in enumerate(hk_rule): + if not isinstance(clause,HKClause): + continue + l_return.append({"type":"text","text":f"\nClause {index+1} - "}) + l_return.extend(parse_clause(self, clause,parent_region,state)) + costs = getattr(target,"costs",None) + if isinstance(costs,dict): + l_return.append({"type":"text","text":"\nCosts - ["}) + for cost,count in costs.items(): + if cost == "GEO": + valid = state.has("Can_Replenish_Geo", self.player) + else: + valid = state.has(cost,self.player,count) + l_return.append({"type":"color","color":"green" if valid else "red","text":f"{cost}:{count}"}) + l_return.append({"type":"text","text":", "}) + l_return.pop() #Remove the last comma + l_return.append({"type":"text","text":"]"}) #And replace with a close bracket + return l_return \ No newline at end of file From 99b64df3db480376593b44aa6c3bcf166a862084 Mon Sep 17 00:00:00 2001 From: Faris Date: Sat, 25 Jul 2026 17:34:50 -0500 Subject: [PATCH 08/12] switching these to cheaper type asserting --- worlds/hk/ut_things.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/worlds/hk/ut_things.py b/worlds/hk/ut_things.py index a0df9265af4a..87091d4f4a60 100644 --- a/worlds/hk/ut_things.py +++ b/worlds/hk/ut_things.py @@ -25,24 +25,24 @@ def parse_clause(self: "HKWorld", clause:HKClause, parent_region: Region, state: def explain_path(self: "HKWorld", entrance: Entrance, state: CollectionState) -> list[JSONMessagePart]: hk_rule = getattr(entrance,"hk_rule",None) - if not isinstance(hk_rule,list): + if hk_rule is None: return [] # Empty list to tell UT to use normal entrance handeling + assert isinstance(hk_rule,list) l_return:list[JSONMessagePart] = [{"type":"color","color":"blue","text":entrance.name}] for index,clause in enumerate(hk_rule): - if not isinstance(clause,HKClause): - continue #maybe fix later? + assert isinstance(clause,HKClause) l_return.append({"type":"text","text":f"\nClause {index+1} - "}) l_return.extend(parse_clause(self,clause,entrance.parent_region,state)) return l_return def explain_spot(self: "HKWorld", location: Location, state: CollectionState) -> list[JSONMessagePart]: hk_rule = getattr(location,"hk_rule",None) - if not isinstance(hk_rule,list): - return [] # Empty list to tell UT to use normal location handeling + if hk_rule is None: + return [] # Empty list to tell UT to use normal entrance handeling + assert isinstance(hk_rule,list) l_return:list[JSONMessagePart] = [{"type":"color","color":"green","text":f" -> {location.name}"}] for index,clause in enumerate(hk_rule): - if not isinstance(clause,HKClause): - continue #maybe fix later? + assert isinstance(clause,HKClause) l_return.append({"type":"text","text":f"\nClause {index+1} - "}) l_return.extend(parse_clause(self,clause,location.parent_region,state)) return l_return @@ -79,16 +79,17 @@ def explain_rule(self: "HKWorld", target_name: str, state: CollectionState) -> l if target is None or parent_region is None: return [] hk_rule = getattr(target,"hk_rule",None) - if not isinstance(hk_rule,list): + if hk_rule is None: l_return.append({"type":"text","text":"Default Access"}) else: + assert isinstance(hk_rule,list) for index,clause in enumerate(hk_rule): - if not isinstance(clause,HKClause): - continue + assert isinstance(clause,HKClause) l_return.append({"type":"text","text":f"\nClause {index+1} - "}) l_return.extend(parse_clause(self, clause,parent_region,state)) costs = getattr(target,"costs",None) - if isinstance(costs,dict): + if costs is not None: + assert isinstance(costs,dict) l_return.append({"type":"text","text":"\nCosts - ["}) for cost,count in costs.items(): if cost == "GEO": From 3a4be1a699b829252615a78a8db413777c08f321 Mon Sep 17 00:00:00 2001 From: Faris Date: Sat, 25 Jul 2026 17:59:12 -0500 Subject: [PATCH 09/12] fix formatting for targets that match both region and entrance/location --- worlds/hk/ut_things.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/worlds/hk/ut_things.py b/worlds/hk/ut_things.py index 87091d4f4a60..b34ec0096ec5 100644 --- a/worlds/hk/ut_things.py +++ b/worlds/hk/ut_things.py @@ -55,7 +55,8 @@ def explain_rule(self: "HKWorld", target_name: str, state: CollectionState) -> l parent_region = None if target_name in self.multiworld.regions.region_cache[self.player]: target = self.get_region(target_name) - parent_region = target + l_return.extend([{"type":"text","text":"Region "},{"type":"color","color":"magenta","text":target_name},{"type":"text","text":"'s Entrances:\n"}]) + # Leave parent_region None so if location/entrances don't match we return normally # Regions have to be dealt with differently, but they don't directly have rules or costs so it's fine for ent in target.entrances: ent_path = self.explain_path(ent,state) @@ -63,21 +64,24 @@ def explain_rule(self: "HKWorld", target_name: str, state: CollectionState) -> l l_return.extend(ent_path) l_return.append({"type":"text","text":f"\n"}) else: # Default entrance rule - l_return.append({"type":"text","text":f"{ent.name} - "}) + l_return.append({"type":"color","color":"blue","text":ent.name}) + l_return.append({"type":"text","text":"\nDefault rule - "}) passable = ent.access_rule(state) l_return.append({"type":"color","text":"Passable" if passable else "Impassable","color":"green" if passable else "red"}) l_return.append({"type":"text","text":f"\n"}) - l_return.pop() # Remove the last newline - return l_return - elif target_name in self.multiworld.regions.entrance_cache[self.player]: + if target_name in self.multiworld.regions.entrance_cache[self.player]: + l_return.extend([{"type":"text","text":"Entrance "},{"type":"color","color":"magenta","text":target_name},{"type":"text","text":"'s Rules:"}]) target = self.get_entrance(target_name) parent_region = target.parent_region elif target_name in self.multiworld.regions.location_cache[self.player]: + l_return.extend([{"type":"text","text":"Location "},{"type":"color","color":"magenta","text":target_name},{"type":"text","text":"'s Rules:"}]) target = self.get_location(target_name) parent_region = target.parent_region if target is None or parent_region is None: - return [] + if l_return: # If there's content to return, we have a trailing newline we need to remove + l_return.pop() + return l_return hk_rule = getattr(target,"hk_rule",None) if hk_rule is None: l_return.append({"type":"text","text":"Default Access"}) From 67976e014ba4d60d4b1b67b061866bc5e430b38d Mon Sep 17 00:00:00 2001 From: Faris Date: Sat, 25 Jul 2026 19:34:33 -0500 Subject: [PATCH 10/12] because you're a bitch about it --- worlds/hk/__init__.py | 37 ++++++++-------- worlds/hk/ut_things.py | 96 +++++++++++++++++++++--------------------- 2 files changed, 67 insertions(+), 66 deletions(-) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index e5f0ee6cf21d..9601d58afcef 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -76,6 +76,11 @@ class HKWorld(RandomizerCoreWorld): ut_can_gen_without_yaml = True found_entrances_datastorage_key = "Slot:{player}:visited_transitions" #A Template string, not an f-string + # imported UT functions + explain_rule = explain_rule + explain_spot = explain_spot + explain_path = explain_path + rc_regions: list[dict[str, Any]] = hk_regions rc_locations: list[dict[str, Any]] = hk_locations item_class = HKItem @@ -90,6 +95,7 @@ class HKWorld(RandomizerCoreWorld): entrance_pairs: dict[str, str] entrance_groups: dict[str, list[HKEntrance]] entrance_state_modifier_by_term: dict[str, list[tuple[str, str]]] + pre_defined_location_costs: dict[str, dict[str, int]] cached_filler_items: list[str] grub_count: int @@ -104,11 +110,6 @@ class HKWorld(RandomizerCoreWorld): collect = hk_collect remove = hk_remove - # imported UT functions - explain_rule = explain_rule - explain_spot = explain_spot - explain_path = explain_path - def __init__(self, multiworld, player): super().__init__(multiworld, player) self.created_multi_locations: dict[str, list[HKLocation]] = { @@ -122,19 +123,19 @@ def __init__(self, multiworld, player): self.entrance_by_term = defaultdict(list) self.entrance_pairs = {} self.entrance_state_modifier_by_term = defaultdict(list) - self.pre_defined_location_costs: dict[str,dict[str,int]] = {} + self.pre_defined_location_costs = {} # generate_early def generate_early(self): options = self.options - if hasattr(self.multiworld,"re_gen_passthrough") and self.game in getattr(self.multiworld,"re_gen_passthrough"): #UT slot_data - slot_data = getattr(self.multiworld,"re_gen_passthrough")[self.game] + if hasattr(self.multiworld, "re_gen_passthrough") and self.game in getattr(self.multiworld, "re_gen_passthrough"): #UT slot_data + slot_data = getattr(self.multiworld, "re_gen_passthrough")[self.game] remote_options = slot_data["options"] for option_name, option_value in remote_options.items(): - if hasattr(options,option_name): - setattr(getattr(options,option_name),"value",option_value) + if hasattr(options, option_name): + setattr(getattr(options, option_name), "value", option_value) self.pre_defined_location_costs=slot_data["location_costs"] self.charm_costs = slot_data["notch_costs"] @@ -507,7 +508,7 @@ def _stateless_connect_one_way(self, source_exit: Entrance, target_entrance: Ent def connect_entrances(self): if not self.options.EntranceRandoType: return - if getattr(self.multiworld, "generation_is_fake",False): + if getattr(self.multiworld, "generation_is_fake", False): return #In UT gen we don't want GER to re-randomize the entrances coupled = self.options.ShuffleEntrancesMode != ShuffleEntrancesMode.option_decoupled @@ -687,9 +688,9 @@ def setup_connections(self): rule = self.create_rule(rule_data) if rule_data else None region1.connect(region2, name, rule) - if getattr(self.multiworld,"generation_is_fake",False) and getattr(self.multiworld,"enforce_deferred_connections","off") == "off": #UT flag + if getattr(self.multiworld, "generation_is_fake", False) and getattr(self.multiworld, "enforce_deferred_connections", "off") == "off": #UT flag #In UT generation, we already got connection_pairs from slot_data - for source,target in self.entrance_pairs.items(): + for source, target in self.entrance_pairs.items(): exit_obj = self.get_entrance(source) exit_region = self.get_region(structure_transition_to_region_map[target]) exits = [entrance for entrance in exit_region.entrances if entrance.name==target and entrance.parent_region is None] @@ -697,7 +698,7 @@ def setup_connections(self): raise Exception(f"More then one viable for {target}, found {exits}") elif len(exits) == 0: raise Exception(f"Unable to find any exists to match for {target}") - self._stateless_connect_one_way(exit_obj,exits[0]) + self._stateless_connect_one_way(exit_obj, exits[0]) return if not one_ways: @@ -708,7 +709,7 @@ def setup_connections(self): for entrance, exit in zip(one_ways["OneWayOut"], one_ways["OneWayIn"], strict=True): self._stateless_connect_one_way(exit, entrance) - def reconnect_found_entrances(self, found_key:str,data_storage_value:dict[str,bool]): + def reconnect_found_entrances(self, found_key:str, data_storage_value:dict[str, bool]): if not data_storage_value: return for entrance_checked in data_storage_value.keys(): @@ -722,7 +723,7 @@ def reconnect_found_entrances(self, found_key:str,data_storage_value:dict[str,bo elif len(exits) == 0: raise Exception(f"Unable to find any exists to match for {target}") exit_name = exits[0].name - self._stateless_connect_one_way(entrance,exits[0]) + self._stateless_connect_one_way(entrance, exits[0]) if self.options.ShuffleEntrancesMode != ShuffleEntrancesMode.option_decoupled: reverse_entrance = self.get_entrance(exit_name) if reverse_entrance.connected_region is None: @@ -732,7 +733,7 @@ def reconnect_found_entrances(self, found_key:str,data_storage_value:dict[str,bo raise Exception(f"More then one viable for {entrance_checked}, found {reverse_exits}") elif len(reverse_exits) == 0: raise Exception(f"Unable to find any exists to match for {target}") - self._stateless_connect_one_way(reverse_entrance,reverse_exits[0]) + self._stateless_connect_one_way(reverse_entrance, reverse_exits[0]) def add_all_events(self): location_to_region = {loc: reg["name"] for reg in structure_regions for loc in reg["locations"]} @@ -830,7 +831,7 @@ def add_extra_shop_locations(self, count): return if gen_is_fake: for shop in shops: - for index in range(len(self.created_multi_locations[shop]),16): + for index in range(len(self.created_multi_locations[shop]), 16): self.add_shop_location(shop, index) # In UT excess locations are ignored, so create all shop slots and let it figure it out else: for _ in range(count): diff --git a/worlds/hk/ut_things.py b/worlds/hk/ut_things.py index b34ec0096ec5..62ba9108e058 100644 --- a/worlds/hk/ut_things.py +++ b/worlds/hk/ut_things.py @@ -8,43 +8,43 @@ def parse_clause(self: "HKWorld", clause:HKClause, parent_region: Region, state: CollectionState) -> list[JSONMessagePart]: l_return:list[JSONMessagePart] = [] - for item,count in clause.hk_item_requirements.items(): - valid = state.has(item,self.player,count) - l_return.append({"type":"color","color":"green" if valid else "red","text":item if count==1 else f"{item}:{count}"}) - l_return.append({"type":"text","text":", "}) + for item, count in clause.hk_item_requirements.items(): + valid = state.has(item, self.player, count) + l_return.append({"type":"color", "color":"green" if valid else "red", "text":item if count==1 else f"{item}:{count}"}) + l_return.append({"type":"text", "text":", "}) for region in clause.hk_region_requirements: - valid = state.can_reach_region(region,self.player) - l_return.append({"type":"color","color":"green" if valid else "red","text":region}) - l_return.append({"type":"text","text":", "}) + valid = state.can_reach_region(region, self.player) + l_return.append({"type":"color", "color":"green" if valid else "red", "text":region}) + l_return.append({"type":"text", "text":", "}) if clause.hk_state_requirements and parent_region: - valid = state.can_reach_region(parent_region.name,self.player) and state._hk_test_fake_state(clause,parent_region) - l_return.append({"type":"color","color":"green" if valid else "red","text":str(clause.hk_state_requirements)}) - l_return.append({"type":"text","text":", "}) + valid = state.can_reach_region(parent_region.name, self.player) and state._hk_test_fake_state(clause, parent_region) + l_return.append({"type":"color", "color":"green" if valid else "red", "text":str(clause.hk_state_requirements)}) + l_return.append({"type":"text", "text":", "}) l_return.pop() # Remove the last comma return l_return def explain_path(self: "HKWorld", entrance: Entrance, state: CollectionState) -> list[JSONMessagePart]: - hk_rule = getattr(entrance,"hk_rule",None) + hk_rule = getattr(entrance, "hk_rule", None) if hk_rule is None: return [] # Empty list to tell UT to use normal entrance handeling - assert isinstance(hk_rule,list) - l_return:list[JSONMessagePart] = [{"type":"color","color":"blue","text":entrance.name}] - for index,clause in enumerate(hk_rule): - assert isinstance(clause,HKClause) - l_return.append({"type":"text","text":f"\nClause {index+1} - "}) - l_return.extend(parse_clause(self,clause,entrance.parent_region,state)) + assert isinstance(hk_rule, list) + l_return:list[JSONMessagePart] = [{"type":"color", "color":"blue", "text":entrance.name}] + for index, clause in enumerate(hk_rule): + assert isinstance(clause, HKClause) + l_return.append({"type":"text", "text":f"\nClause {index+1} - "}) + l_return.extend(parse_clause(self, clause, entrance.parent_region, state)) return l_return def explain_spot(self: "HKWorld", location: Location, state: CollectionState) -> list[JSONMessagePart]: - hk_rule = getattr(location,"hk_rule",None) + hk_rule = getattr(location, "hk_rule", None) if hk_rule is None: return [] # Empty list to tell UT to use normal entrance handeling - assert isinstance(hk_rule,list) - l_return:list[JSONMessagePart] = [{"type":"color","color":"green","text":f" -> {location.name}"}] - for index,clause in enumerate(hk_rule): - assert isinstance(clause,HKClause) - l_return.append({"type":"text","text":f"\nClause {index+1} - "}) - l_return.extend(parse_clause(self,clause,location.parent_region,state)) + assert isinstance(hk_rule, list) + l_return:list[JSONMessagePart] = [{"type":"color", "color":"green", "text":f" -> {location.name}"}] + for index, clause in enumerate(hk_rule): + assert isinstance(clause, HKClause) + l_return.append({"type":"text", "text":f"\nClause {index+1} - "}) + l_return.extend(parse_clause(self, clause, location.parent_region, state)) return l_return @@ -55,26 +55,26 @@ def explain_rule(self: "HKWorld", target_name: str, state: CollectionState) -> l parent_region = None if target_name in self.multiworld.regions.region_cache[self.player]: target = self.get_region(target_name) - l_return.extend([{"type":"text","text":"Region "},{"type":"color","color":"magenta","text":target_name},{"type":"text","text":"'s Entrances:\n"}]) + l_return.extend([{"type":"text", "text":"Region "}, {"type":"color", "color":"magenta", "text":target_name}, {"type":"text", "text":"'s Entrances:\n"}]) # Leave parent_region None so if location/entrances don't match we return normally # Regions have to be dealt with differently, but they don't directly have rules or costs so it's fine for ent in target.entrances: - ent_path = self.explain_path(ent,state) + ent_path = self.explain_path(ent, state) if ent_path: l_return.extend(ent_path) - l_return.append({"type":"text","text":f"\n"}) + l_return.append({"type":"text", "text":f"\n"}) else: # Default entrance rule - l_return.append({"type":"color","color":"blue","text":ent.name}) - l_return.append({"type":"text","text":"\nDefault rule - "}) + l_return.append({"type":"color", "color":"blue", "text":ent.name}) + l_return.append({"type":"text", "text":"\nDefault rule - "}) passable = ent.access_rule(state) - l_return.append({"type":"color","text":"Passable" if passable else "Impassable","color":"green" if passable else "red"}) - l_return.append({"type":"text","text":f"\n"}) + l_return.append({"type":"color", "text":"Passable" if passable else "Impassable", "color":"green" if passable else "red"}) + l_return.append({"type":"text", "text":f"\n"}) if target_name in self.multiworld.regions.entrance_cache[self.player]: - l_return.extend([{"type":"text","text":"Entrance "},{"type":"color","color":"magenta","text":target_name},{"type":"text","text":"'s Rules:"}]) + l_return.extend([{"type":"text", "text":"Entrance "}, {"type":"color", "color":"magenta", "text":target_name}, {"type":"text", "text":"'s Rules:"}]) target = self.get_entrance(target_name) parent_region = target.parent_region elif target_name in self.multiworld.regions.location_cache[self.player]: - l_return.extend([{"type":"text","text":"Location "},{"type":"color","color":"magenta","text":target_name},{"type":"text","text":"'s Rules:"}]) + l_return.extend([{"type":"text", "text":"Location "}, {"type":"color", "color":"magenta", "text":target_name}, {"type":"text", "text":"'s Rules:"}]) target = self.get_location(target_name) parent_region = target.parent_region @@ -82,26 +82,26 @@ def explain_rule(self: "HKWorld", target_name: str, state: CollectionState) -> l if l_return: # If there's content to return, we have a trailing newline we need to remove l_return.pop() return l_return - hk_rule = getattr(target,"hk_rule",None) + hk_rule = getattr(target, "hk_rule", None) if hk_rule is None: - l_return.append({"type":"text","text":"Default Access"}) + l_return.append({"type":"text", "text":"Default Access"}) else: - assert isinstance(hk_rule,list) - for index,clause in enumerate(hk_rule): - assert isinstance(clause,HKClause) - l_return.append({"type":"text","text":f"\nClause {index+1} - "}) - l_return.extend(parse_clause(self, clause,parent_region,state)) - costs = getattr(target,"costs",None) + assert isinstance(hk_rule, list) + for index, clause in enumerate(hk_rule): + assert isinstance(clause, HKClause) + l_return.append({"type":"text", "text":f"\nClause {index+1} - "}) + l_return.extend(parse_clause(self, clause, parent_region, state)) + costs = getattr(target, "costs", None) if costs is not None: - assert isinstance(costs,dict) - l_return.append({"type":"text","text":"\nCosts - ["}) - for cost,count in costs.items(): + assert isinstance(costs, dict) + l_return.append({"type":"text", "text":"\nCosts - ["}) + for cost, count in costs.items(): if cost == "GEO": valid = state.has("Can_Replenish_Geo", self.player) else: - valid = state.has(cost,self.player,count) - l_return.append({"type":"color","color":"green" if valid else "red","text":f"{cost}:{count}"}) - l_return.append({"type":"text","text":", "}) + valid = state.has(cost, self.player, count) + l_return.append({"type":"color", "color":"green" if valid else "red", "text":f"{cost}:{count}"}) + l_return.append({"type":"text", "text":", "}) l_return.pop() #Remove the last comma - l_return.append({"type":"text","text":"]"}) #And replace with a close bracket + l_return.append({"type":"text", "text":"]"}) #And replace with a close bracket return l_return \ No newline at end of file From af33e9a1367138ef4e5e983aef508cb92c5d3077 Mon Sep 17 00:00:00 2001 From: Faris Date: Mon, 27 Jul 2026 11:50:13 -0500 Subject: [PATCH 11/12] Your blackmail from the space bar union has cleared, give me back my freedom --- worlds/hk/__init__.py | 47 +++++++++++-------- worlds/hk/ut_things.py | 101 +++++++++++++++++++++++++++-------------- 2 files changed, 94 insertions(+), 54 deletions(-) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 9601d58afcef..a3857e17601b 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -72,9 +72,9 @@ class HKWorld(RandomizerCoreWorld): item_name_groups = datapackage_item_groups location_name_groups = datapackage_location_groups - #UT Attributes + # UT Attributes ut_can_gen_without_yaml = True - found_entrances_datastorage_key = "Slot:{player}:visited_transitions" #A Template string, not an f-string + found_entrances_datastorage_key = "Slot:{player}:visited_transitions" # A Template string, not an f-string # imported UT functions explain_rule = explain_rule @@ -130,12 +130,12 @@ def generate_early(self): options = self.options - if hasattr(self.multiworld, "re_gen_passthrough") and self.game in getattr(self.multiworld, "re_gen_passthrough"): #UT slot_data - slot_data = getattr(self.multiworld, "re_gen_passthrough")[self.game] + if hasattr(self.multiworld, "re_gen_passthrough") and self.game in self.multiworld.re_gen_passthrough: + slot_data = self.multiworld.re_gen_passthrough[self.game] # UT slot_data remote_options = slot_data["options"] for option_name, option_value in remote_options.items(): if hasattr(options, option_name): - setattr(getattr(options, option_name), "value", option_value) + getattr(options, option_name).value = option_value self.pre_defined_location_costs=slot_data["location_costs"] self.charm_costs = slot_data["notch_costs"] @@ -151,7 +151,6 @@ def generate_early(self): else options.GrubHuntGoal.value ) - weights = { data.term: getattr(options, f"CostSanity{data.option}Weight").value for data in cost_terms.values() @@ -509,7 +508,7 @@ def connect_entrances(self): if not self.options.EntranceRandoType: return if getattr(self.multiworld, "generation_is_fake", False): - return #In UT gen we don't want GER to re-randomize the entrances + return # In UT gen we don't want GER to re-randomize the entrances coupled = self.options.ShuffleEntrancesMode != ShuffleEntrancesMode.option_decoupled def _connect_entrances(group: str, entrances: list[HKEntrance] | None) -> None: @@ -688,15 +687,21 @@ def setup_connections(self): rule = self.create_rule(rule_data) if rule_data else None region1.connect(region2, name, rule) - if getattr(self.multiworld, "generation_is_fake", False) and getattr(self.multiworld, "enforce_deferred_connections", "off") == "off": #UT flag - #In UT generation, we already got connection_pairs from slot_data + if ( + getattr(self.multiworld, "generation_is_fake", False) and + getattr(self.multiworld, "enforce_deferred_connections", "off") == "off" # UT flag + ): + # In UT generation, we already got connection_pairs from slot_data for source, target in self.entrance_pairs.items(): exit_obj = self.get_entrance(source) exit_region = self.get_region(structure_transition_to_region_map[target]) - exits = [entrance for entrance in exit_region.entrances if entrance.name==target and entrance.parent_region is None] + exits = [ + entrance for entrance in exit_region.entrances + if entrance.name == target and entrance.parent_region is None + ] if len(exits) > 1: raise Exception(f"More then one viable for {target}, found {exits}") - elif len(exits) == 0: + if len(exits) == 0: raise Exception(f"Unable to find any exists to match for {target}") self._stateless_connect_one_way(exit_obj, exits[0]) return @@ -709,7 +714,7 @@ def setup_connections(self): for entrance, exit in zip(one_ways["OneWayOut"], one_ways["OneWayIn"], strict=True): self._stateless_connect_one_way(exit, entrance) - def reconnect_found_entrances(self, found_key:str, data_storage_value:dict[str, bool]): + def reconnect_found_entrances(self, found_key: str, data_storage_value: dict[str, bool]): if not data_storage_value: return for entrance_checked in data_storage_value.keys(): @@ -717,10 +722,10 @@ def reconnect_found_entrances(self, found_key:str, data_storage_value:dict[str, if entrance.connected_region is None: target = self.entrance_pairs[entrance_checked] target_region = self.get_region(structure_transition_to_region_map[target]) - exits = [entrance for entrance in target_region.entrances if entrance.name==target] + exits = [entrance for entrance in target_region.entrances if entrance.name == target] if len(exits) > 1: raise Exception(f"More then one viable for {target}, found {exits}") - elif len(exits) == 0: + if len(exits) == 0: raise Exception(f"Unable to find any exists to match for {target}") exit_name = exits[0].name self._stateless_connect_one_way(entrance, exits[0]) @@ -728,13 +733,16 @@ def reconnect_found_entrances(self, found_key:str, data_storage_value:dict[str, reverse_entrance = self.get_entrance(exit_name) if reverse_entrance.connected_region is None: assert entrance.parent_region - reverse_exits = [entrance for entrance in entrance.parent_region.entrances if entrance.name==entrance_checked] + reverse_exits = [ + entrance for entrance in entrance.parent_region.entrances + if entrance.name == entrance_checked + ] if len(reverse_exits) > 1: raise Exception(f"More then one viable for {entrance_checked}, found {reverse_exits}") - elif len(reverse_exits) == 0: + if len(reverse_exits) == 0: raise Exception(f"Unable to find any exists to match for {target}") self._stateless_connect_one_way(reverse_entrance, reverse_exits[0]) - + def add_all_events(self): location_to_region = {loc: reg["name"] for reg in structure_regions for loc in reg["locations"]} @@ -832,7 +840,8 @@ def add_extra_shop_locations(self, count): if gen_is_fake: for shop in shops: for index in range(len(self.created_multi_locations[shop]), 16): - self.add_shop_location(shop, index) # In UT excess locations are ignored, so create all shop slots and let it figure it out + self.add_shop_location(shop, index) + # In UT excess locations are ignored, so create all shop slots and let it figure it out else: for _ in range(count): shop = self.random.choice(shops) @@ -1040,7 +1049,7 @@ def _compute_weights(weights: dict, desc: str) -> dict[str, int]: def sort_shops_by_cost(self): if getattr(self.multiworld, "generation_is_fake", False): - return #In a UT gen we've already placed these where they should go + return # In a UT gen we've already placed these where they should go for shop_locations in self.created_multi_locations.values(): randomized_locations = [loc for loc in shop_locations if not loc.vanilla] if not randomized_locations: diff --git a/worlds/hk/ut_things.py b/worlds/hk/ut_things.py index 62ba9108e058..90b5ef577f3b 100644 --- a/worlds/hk/ut_things.py +++ b/worlds/hk/ut_things.py @@ -1,107 +1,138 @@ from typing import TYPE_CHECKING + from BaseClasses import CollectionState, Entrance, Location, Region from NetUtils import JSONMessagePart + from .classes import HKClause if TYPE_CHECKING: from . import HKWorld -def parse_clause(self: "HKWorld", clause:HKClause, parent_region: Region, state: CollectionState) -> list[JSONMessagePart]: - l_return:list[JSONMessagePart] = [] +def parse_clause( + self: "HKWorld", clause: HKClause, parent_region: Region, state: CollectionState + ) -> list[JSONMessagePart]: + l_return: list[JSONMessagePart] = [] for item, count in clause.hk_item_requirements.items(): valid = state.has(item, self.player, count) - l_return.append({"type":"color", "color":"green" if valid else "red", "text":item if count==1 else f"{item}:{count}"}) - l_return.append({"type":"text", "text":", "}) + l_return.append({ + "type": "color", + "color": "green" if valid else "red", + "text": item if count == 1 else f"{item}:{count}" + }) + l_return.append({"type": "text", "text": ", "}) for region in clause.hk_region_requirements: valid = state.can_reach_region(region, self.player) - l_return.append({"type":"color", "color":"green" if valid else "red", "text":region}) - l_return.append({"type":"text", "text":", "}) + l_return.append({"type": "color", "color": "green" if valid else "red", "text": region}) + l_return.append({"type": "text", "text": ", "}) if clause.hk_state_requirements and parent_region: - valid = state.can_reach_region(parent_region.name, self.player) and state._hk_test_fake_state(clause, parent_region) - l_return.append({"type":"color", "color":"green" if valid else "red", "text":str(clause.hk_state_requirements)}) - l_return.append({"type":"text", "text":", "}) - l_return.pop() # Remove the last comma + valid = ( + state.can_reach_region(parent_region.name, self.player) and + state._hk_test_fake_state(clause, parent_region) + ) + l_return.append({ + "type": "color", + "color": "green" if valid else "red", + "text": str(clause.hk_state_requirements) + }) + l_return.append({"type": "text", "text": ", "}) + l_return.pop() # Remove the last comma return l_return def explain_path(self: "HKWorld", entrance: Entrance, state: CollectionState) -> list[JSONMessagePart]: hk_rule = getattr(entrance, "hk_rule", None) if hk_rule is None: - return [] # Empty list to tell UT to use normal entrance handeling + return [] # Empty list to tell UT to use normal entrance handeling assert isinstance(hk_rule, list) - l_return:list[JSONMessagePart] = [{"type":"color", "color":"blue", "text":entrance.name}] + l_return:list[JSONMessagePart] = [{"type": "color", "color": "blue", "text": entrance.name}] for index, clause in enumerate(hk_rule): assert isinstance(clause, HKClause) - l_return.append({"type":"text", "text":f"\nClause {index+1} - "}) + l_return.append({"type": "text", "text": f"\nClause {index+1} - "}) l_return.extend(parse_clause(self, clause, entrance.parent_region, state)) return l_return def explain_spot(self: "HKWorld", location: Location, state: CollectionState) -> list[JSONMessagePart]: hk_rule = getattr(location, "hk_rule", None) if hk_rule is None: - return [] # Empty list to tell UT to use normal entrance handeling + return [] # Empty list to tell UT to use normal entrance handeling assert isinstance(hk_rule, list) - l_return:list[JSONMessagePart] = [{"type":"color", "color":"green", "text":f" -> {location.name}"}] + l_return: list[JSONMessagePart] = [{"type": "color", "color": "green", "text": f" -> {location.name}"}] for index, clause in enumerate(hk_rule): assert isinstance(clause, HKClause) - l_return.append({"type":"text", "text":f"\nClause {index+1} - "}) + l_return.append({"type": "text", "text": f"\nClause {index+1} - "}) l_return.extend(parse_clause(self, clause, location.parent_region, state)) return l_return def explain_rule(self: "HKWorld", target_name: str, state: CollectionState) -> list[JSONMessagePart]: - l_return:list[JSONMessagePart] = [] + l_return: list[JSONMessagePart] = [] target = None parent_region = None if target_name in self.multiworld.regions.region_cache[self.player]: target = self.get_region(target_name) - l_return.extend([{"type":"text", "text":"Region "}, {"type":"color", "color":"magenta", "text":target_name}, {"type":"text", "text":"'s Entrances:\n"}]) + l_return.extend([ + {"type": "text", "text": "Region "}, + {"type": "color", "color": "magenta", "text": target_name}, + {"type": "text", "text": "'s Entrances:\n"} + ]) # Leave parent_region None so if location/entrances don't match we return normally # Regions have to be dealt with differently, but they don't directly have rules or costs so it's fine for ent in target.entrances: ent_path = self.explain_path(ent, state) if ent_path: l_return.extend(ent_path) - l_return.append({"type":"text", "text":f"\n"}) - else: # Default entrance rule - l_return.append({"type":"color", "color":"blue", "text":ent.name}) - l_return.append({"type":"text", "text":"\nDefault rule - "}) + l_return.append({"type": "text", "text": "\n"}) + else: # Default entrance rule + l_return.append({"type": "color", "color": "blue", "text": ent.name}) + l_return.append({"type": "text", "text": "\nDefault rule - "}) passable = ent.access_rule(state) - l_return.append({"type":"color", "text":"Passable" if passable else "Impassable", "color":"green" if passable else "red"}) - l_return.append({"type":"text", "text":f"\n"}) + l_return.append({ + "type": "color", + "text": "Passable" if passable else "Impassable", + "color": "green" if passable else "red" + }) + l_return.append({"type": "text", "text": "\n"}) if target_name in self.multiworld.regions.entrance_cache[self.player]: - l_return.extend([{"type":"text", "text":"Entrance "}, {"type":"color", "color":"magenta", "text":target_name}, {"type":"text", "text":"'s Rules:"}]) + l_return.extend([ + {"type": "text", "text": "Entrance "}, + {"type": "color", "color": "magenta", "text": target_name}, + {"type": "text", "text": "'s Rules:"} + ]) target = self.get_entrance(target_name) parent_region = target.parent_region elif target_name in self.multiworld.regions.location_cache[self.player]: - l_return.extend([{"type":"text", "text":"Location "}, {"type":"color", "color":"magenta", "text":target_name}, {"type":"text", "text":"'s Rules:"}]) + l_return.extend([ + {"type": "text", "text": "Location "}, + {"type": "color", "color": "magenta", "text": target_name}, + {"type": "text", "text": "'s Rules:"} + ]) target = self.get_location(target_name) parent_region = target.parent_region if target is None or parent_region is None: - if l_return: # If there's content to return, we have a trailing newline we need to remove + if l_return: # If there's content to return, we have a trailing newline we need to remove l_return.pop() return l_return hk_rule = getattr(target, "hk_rule", None) if hk_rule is None: - l_return.append({"type":"text", "text":"Default Access"}) + l_return.append({"type": "text", "text": "Default Access"}) else: assert isinstance(hk_rule, list) for index, clause in enumerate(hk_rule): assert isinstance(clause, HKClause) - l_return.append({"type":"text", "text":f"\nClause {index+1} - "}) + l_return.append({"type": "text", "text": f"\nClause {index+1} - "}) l_return.extend(parse_clause(self, clause, parent_region, state)) costs = getattr(target, "costs", None) if costs is not None: assert isinstance(costs, dict) - l_return.append({"type":"text", "text":"\nCosts - ["}) + l_return.append({"type": "text", "text": "\nCosts - ["}) for cost, count in costs.items(): if cost == "GEO": valid = state.has("Can_Replenish_Geo", self.player) else: valid = state.has(cost, self.player, count) - l_return.append({"type":"color", "color":"green" if valid else "red", "text":f"{cost}:{count}"}) - l_return.append({"type":"text", "text":", "}) - l_return.pop() #Remove the last comma - l_return.append({"type":"text", "text":"]"}) #And replace with a close bracket - return l_return \ No newline at end of file + l_return.append({"type": "color", "color": "green" if valid else "red", "text": f"{cost}:{count}"}) + l_return.append({"type": "text", "text": ", "}) + l_return.pop() # Remove the last comma + l_return.append({"type": "text", "text": "]"}) # And replace with a close bracket + return l_return From 48f4bf64d15e7f178d8bc8776452923fae6547ca Mon Sep 17 00:00:00 2001 From: Faris Date: Mon, 27 Jul 2026 12:46:32 -0500 Subject: [PATCH 12/12] i hate linters --- worlds/hk/__init__.py | 6 +++--- worlds/hk/ut_things.py | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index a3857e17601b..2eccf00d948e 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -136,7 +136,7 @@ def generate_early(self): for option_name, option_value in remote_options.items(): if hasattr(options, option_name): getattr(options, option_name).value = option_value - self.pre_defined_location_costs=slot_data["location_costs"] + self.pre_defined_location_costs = slot_data["location_costs"] self.charm_costs = slot_data["notch_costs"] self.grub_count = slot_data["grub_count"] @@ -688,7 +688,7 @@ def setup_connections(self): region1.connect(region2, name, rule) if ( - getattr(self.multiworld, "generation_is_fake", False) and + getattr(self.multiworld, "generation_is_fake", False) and getattr(self.multiworld, "enforce_deferred_connections", "off") == "off" # UT flag ): # In UT generation, we already got connection_pairs from slot_data @@ -696,7 +696,7 @@ def setup_connections(self): exit_obj = self.get_entrance(source) exit_region = self.get_region(structure_transition_to_region_map[target]) exits = [ - entrance for entrance in exit_region.entrances + entrance for entrance in exit_region.entrances if entrance.name == target and entrance.parent_region is None ] if len(exits) > 1: diff --git a/worlds/hk/ut_things.py b/worlds/hk/ut_things.py index 90b5ef577f3b..ee0befa84f0d 100644 --- a/worlds/hk/ut_things.py +++ b/worlds/hk/ut_things.py @@ -8,6 +8,7 @@ if TYPE_CHECKING: from . import HKWorld + def parse_clause( self: "HKWorld", clause: HKClause, parent_region: Region, state: CollectionState ) -> list[JSONMessagePart]: @@ -38,18 +39,20 @@ def parse_clause( l_return.pop() # Remove the last comma return l_return + def explain_path(self: "HKWorld", entrance: Entrance, state: CollectionState) -> list[JSONMessagePart]: hk_rule = getattr(entrance, "hk_rule", None) if hk_rule is None: return [] # Empty list to tell UT to use normal entrance handeling assert isinstance(hk_rule, list) - l_return:list[JSONMessagePart] = [{"type": "color", "color": "blue", "text": entrance.name}] + l_return: list[JSONMessagePart] = [{"type": "color", "color": "blue", "text": entrance.name}] for index, clause in enumerate(hk_rule): assert isinstance(clause, HKClause) l_return.append({"type": "text", "text": f"\nClause {index+1} - "}) l_return.extend(parse_clause(self, clause, entrance.parent_region, state)) return l_return + def explain_spot(self: "HKWorld", location: Location, state: CollectionState) -> list[JSONMessagePart]: hk_rule = getattr(location, "hk_rule", None) if hk_rule is None: