From 285c5755898b74551a807f8b11471ecfae8e6b46 Mon Sep 17 00:00:00 2001 From: Faris Date: Sat, 23 May 2026 20:43:55 -0500 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] 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 6/6] 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 = []