diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index c75d9cf9e501..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 +from BaseClasses import CollectionState, Entrance, EntranceType, ItemClassification, LocationProgressType, MultiWorld, Region, Location from entrance_rando import EntranceRandomizationError, randomize_entrances from Options import OptionError @@ -71,6 +71,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 @@ -112,10 +116,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() @@ -142,8 +171,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 @@ -167,11 +194,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 @@ -479,6 +501,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: @@ -657,6 +681,17 @@ 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 + #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}") + self._stateless_connect_one_way(exit_obj,exits[0]) + return + if not one_ways: return @@ -665,6 +700,116 @@ 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]): + 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: + 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 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"]} @@ -714,9 +859,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] @@ -724,7 +872,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) @@ -748,7 +895,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 @@ -756,14 +904,20 @@ def add_extra_shop_locations(self, count): if not shops: return - 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 + 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) + 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): @@ -930,6 +1084,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: @@ -958,6 +1115,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: 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