mirror of
				https://github.com/MarioSpore/Grinch-AP.git
				synced 2025-10-21 20:21:32 -06:00 
			
		
		
		
	Ocarina of Time: long-awaited bugfixes (#2344)
- Added location name groups, so you can make your entire Water Temple priority to annoy everyone else - Significant improvement to ER generation success rate (~80% to >99%) - Changed `adult_trade_start` option to a choice option instead of a list (this shouldn't actually break any YAMLs though, due to the lesser-known property of lists parsing as a uniformly-weighted choice) - Major improvements to the option tooltips where needed. (Possibly too much text now) - Changed default hint distribution to `async` to help people's generation times. The tooltip explains that it removes WOTH hints so people hopefully don't get tripped up. - Makes stick and nut capacity upgrades useful items - Added shop prices and required trials to spoiler log - Added Cojiro to adult trade item group, because it had been forgotten previously - Fixed size-modified chests not being moved properly due to trap appearance changing the size - Fixed Thieves Hideout keyring not being allowed in start inventory - Fixed hint generation not accurately flagging barren locations on certain dungeon item shuffle settings - Fixed bug where you could plando arbitrarily-named items into the world, breaking everything
This commit is contained in:
		| @@ -10,7 +10,7 @@ from string import printable | ||||
|  | ||||
| logger = logging.getLogger("Ocarina of Time") | ||||
|  | ||||
| from .Location import OOTLocation, LocationFactory, location_name_to_id | ||||
| from .Location import OOTLocation, LocationFactory, location_name_to_id, build_location_name_groups | ||||
| from .Entrance import OOTEntrance | ||||
| from .EntranceShuffle import shuffle_random_entrances, entrance_shuffle_table, EntranceShuffleError | ||||
| from .HintList import getRequiredHints | ||||
| @@ -163,11 +163,13 @@ class OOTWorld(World): | ||||
|             "Bottle with Big Poe", "Bottle with Red Potion", "Bottle with Green Potion", | ||||
|             "Bottle with Blue Potion", "Bottle with Fairy", "Bottle with Fish", | ||||
|             "Bottle with Blue Fire", "Bottle with Bugs", "Bottle with Poe"}, | ||||
|         "Adult Trade Item": {"Pocket Egg", "Pocket Cucco", "Odd Mushroom", | ||||
|         "Adult Trade Item": {"Pocket Egg", "Pocket Cucco", "Cojiro", "Odd Mushroom", | ||||
|             "Odd Potion", "Poachers Saw", "Broken Sword", "Prescription", | ||||
|             "Eyeball Frog", "Eyedrops", "Claim Check"} | ||||
|             "Eyeball Frog", "Eyedrops", "Claim Check"}, | ||||
|     } | ||||
|  | ||||
|     location_name_groups = build_location_name_groups() | ||||
|  | ||||
|     def __init__(self, world, player): | ||||
|         self.hint_data_available = threading.Event() | ||||
|         self.collectible_flags_available = threading.Event() | ||||
| @@ -384,6 +386,7 @@ class OOTWorld(World): | ||||
|             self.mq_dungeons_mode = 'count' | ||||
|             self.mq_dungeons_count = 0 | ||||
|         self.dungeon_mq = {item['name']: (item['name'] in mq_dungeons) for item in dungeon_table} | ||||
|         self.dungeon_mq['Thieves Hideout'] = False  # fix for bug in SaveContext:287 | ||||
|  | ||||
|         # Empty dungeon placeholder for the moment | ||||
|         self.empty_dungeons = {name: False for name in self.dungeon_mq} | ||||
| @@ -409,6 +412,9 @@ class OOTWorld(World): | ||||
|         self.starting_tod = self.starting_tod.replace('_', '-') | ||||
|         self.shuffle_scrubs = self.shuffle_scrubs.replace('_prices', '') | ||||
|  | ||||
|         # Convert adult trade option to expected Set | ||||
|         self.adult_trade_start = {self.adult_trade_start.title().replace('_', ' ')} | ||||
|  | ||||
|         # Get hint distribution | ||||
|         self.hint_dist_user = read_json(data_path('Hints', f'{self.hint_dist}.json')) | ||||
|  | ||||
| @@ -446,7 +452,7 @@ class OOTWorld(World): | ||||
|         self.always_hints = [hint.name for hint in getRequiredHints(self)] | ||||
|  | ||||
|         # Determine items which are not considered advancement based on settings. They will never be excluded. | ||||
|         self.nonadvancement_items = {'Double Defense'} | ||||
|         self.nonadvancement_items = {'Double Defense', 'Deku Stick Capacity', 'Deku Nut Capacity'} | ||||
|         if (self.damage_multiplier != 'ohko' and self.damage_multiplier != 'quadruple' and | ||||
|                 self.shuffle_scrubs == 'off' and not self.shuffle_grotto_entrances): | ||||
|             # nayru's love may be required to prevent forced damage | ||||
| @@ -633,16 +639,18 @@ class OOTWorld(World): | ||||
|             self.multiworld.itempool.remove(item) | ||||
|             self.hinted_dungeon_reward_locations[item.name] = loc | ||||
|  | ||||
|     def create_item(self, name: str): | ||||
|     def create_item(self, name: str, allow_arbitrary_name: bool = False): | ||||
|         if name in item_table: | ||||
|             return OOTItem(name, self.player, item_table[name], False, | ||||
|                            (name in self.nonadvancement_items if getattr(self, 'nonadvancement_items', | ||||
|                                                                          None) else False)) | ||||
|         return OOTItem(name, self.player, ('Event', True, None, None), True, False) | ||||
|         if allow_arbitrary_name: | ||||
|             return OOTItem(name, self.player, ('Event', True, None, None), True, False) | ||||
|         raise Exception(f"Invalid item name: {name}") | ||||
|  | ||||
|     def make_event_item(self, name, location, item=None): | ||||
|         if item is None: | ||||
|             item = self.create_item(name) | ||||
|             item = self.create_item(name, allow_arbitrary_name=True) | ||||
|         self.multiworld.push_item(location, item, collect=False) | ||||
|         location.locked = True | ||||
|         location.event = True | ||||
| @@ -800,23 +808,25 @@ class OOTWorld(World): | ||||
|                         self.multiworld.itempool.remove(item) | ||||
|                     self.multiworld.random.shuffle(locations) | ||||
|                     fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, stage_items, | ||||
|                         single_player_placement=True, lock=True) | ||||
|                         single_player_placement=True, lock=True, allow_excluded=True) | ||||
|             else: | ||||
|                 for dungeon_info in dungeon_table: | ||||
|                     dungeon_name = dungeon_info['name'] | ||||
|                     locations = gather_locations(self.multiworld, fill_stage, self.player, dungeon=dungeon_name) | ||||
|                     if isinstance(locations, list): | ||||
|                         dungeon_items = list(filter(lambda item: dungeon_name in item.name, stage_items)) | ||||
|                         if not dungeon_items: | ||||
|                             continue | ||||
|                         for item in dungeon_items: | ||||
|                             self.multiworld.itempool.remove(item) | ||||
|                         self.multiworld.random.shuffle(locations) | ||||
|                         fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, dungeon_items, | ||||
|                             single_player_placement=True, lock=True) | ||||
|                             single_player_placement=True, lock=True, allow_excluded=True) | ||||
|  | ||||
|         # Place songs | ||||
|         # 5 built-in retries because this section can fail sometimes | ||||
|         if self.shuffle_song_items != 'any': | ||||
|             tries = 5 | ||||
|             tries = 10 | ||||
|             if self.shuffle_song_items == 'song': | ||||
|                 song_locations = list(filter(lambda location: location.type == 'Song', | ||||
|                                              self.multiworld.get_unfilled_locations(player=self.player))) | ||||
| @@ -852,7 +862,7 @@ class OOTWorld(World): | ||||
|                 try: | ||||
|                     self.multiworld.random.shuffle(song_locations) | ||||
|                     fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), song_locations[:], songs[:], | ||||
|                                      True, True) | ||||
|                                      single_player_placement=True, lock=True, allow_excluded=True) | ||||
|                     logger.debug(f"Successfully placed songs for player {self.player} after {6 - tries} attempt(s)") | ||||
|                 except FillError as e: | ||||
|                     tries -= 1 | ||||
| @@ -888,7 +898,8 @@ class OOTWorld(World): | ||||
|             self.multiworld.random.shuffle(shop_locations) | ||||
|             for item in shop_prog + shop_junk: | ||||
|                 self.multiworld.itempool.remove(item) | ||||
|             fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), shop_locations, shop_prog, True, True) | ||||
|             fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), shop_locations, shop_prog, | ||||
|                 single_player_placement=True, lock=True, allow_excluded=True) | ||||
|             fast_fill(self.multiworld, shop_junk, shop_locations) | ||||
|             for loc in shop_locations: | ||||
|                 loc.locked = True | ||||
| @@ -963,7 +974,7 @@ class OOTWorld(World): | ||||
|                             multiworld.itempool.remove(item) | ||||
|                         multiworld.random.shuffle(locations) | ||||
|                         fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_stage_items, | ||||
|                             single_player_placement=False, lock=True) | ||||
|                             single_player_placement=False, lock=True, allow_excluded=True) | ||||
|                         if fill_stage == 'Song': | ||||
|                             # We don't want song locations to contain progression unless it's a song | ||||
|                             # or it was marked as priority. | ||||
| @@ -984,7 +995,7 @@ class OOTWorld(World): | ||||
|                                 multiworld.itempool.remove(item) | ||||
|                             multiworld.random.shuffle(locations) | ||||
|                             fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_dungeon_items, | ||||
|                                 single_player_placement=False, lock=True) | ||||
|                                 single_player_placement=False, lock=True, allow_excluded=True) | ||||
|  | ||||
|     def generate_output(self, output_directory: str): | ||||
|         if self.hints != 'none': | ||||
| @@ -1051,7 +1062,10 @@ class OOTWorld(World): | ||||
|     def stage_generate_output(cls, multiworld: MultiWorld, output_directory: str): | ||||
|         def hint_type_players(hint_type: str) -> set: | ||||
|             return {autoworld.player for autoworld in multiworld.get_game_worlds("Ocarina of Time") | ||||
|                     if autoworld.hints != 'none' and autoworld.hint_dist_user['distribution'][hint_type]['copies'] > 0} | ||||
|                     if autoworld.hints != 'none'  | ||||
|                     and autoworld.hint_dist_user['distribution'][hint_type]['copies'] > 0 | ||||
|                     and (autoworld.hint_dist_user['distribution'][hint_type]['fixed'] > 0  | ||||
|                       or autoworld.hint_dist_user['distribution'][hint_type]['weight'] > 0)} | ||||
|  | ||||
|         try: | ||||
|             item_hint_players = hint_type_players('item') | ||||
| @@ -1078,10 +1092,10 @@ class OOTWorld(World): | ||||
|  | ||||
|                     if loc.game == "Ocarina of Time" and loc.item.code and (not loc.locked or | ||||
|                         (oot_is_item_of_type(loc.item, 'Song') or | ||||
|                             (oot_is_item_of_type(loc.item, 'SmallKey')         and multiworld.worlds[loc.player].shuffle_smallkeys     == 'any_dungeon') or | ||||
|                             (oot_is_item_of_type(loc.item, 'HideoutSmallKey')  and multiworld.worlds[loc.player].shuffle_hideoutkeys  == 'any_dungeon') or | ||||
|                             (oot_is_item_of_type(loc.item, 'BossKey')          and multiworld.worlds[loc.player].shuffle_bosskeys      == 'any_dungeon') or | ||||
|                             (oot_is_item_of_type(loc.item, 'GanonBossKey')     and multiworld.worlds[loc.player].shuffle_ganon_bosskey == 'any_dungeon'))): | ||||
|                             (oot_is_item_of_type(loc.item, 'SmallKey')         and multiworld.worlds[loc.player].shuffle_smallkeys     in ('overworld', 'any_dungeon', 'regional')) or | ||||
|                             (oot_is_item_of_type(loc.item, 'HideoutSmallKey')  and multiworld.worlds[loc.player].shuffle_hideoutkeys   in ('overworld', 'any_dungeon', 'regional')) or | ||||
|                             (oot_is_item_of_type(loc.item, 'BossKey')          and multiworld.worlds[loc.player].shuffle_bosskeys      in ('overworld', 'any_dungeon', 'regional')) or | ||||
|                             (oot_is_item_of_type(loc.item, 'GanonBossKey')     and multiworld.worlds[loc.player].shuffle_ganon_bosskey in ('overworld', 'any_dungeon', 'regional')))): | ||||
|                         if loc.player in barren_hint_players: | ||||
|                             hint_area = get_hint_area(loc) | ||||
|                             items_by_region[loc.player][hint_area]['weight'] += 1 | ||||
| @@ -1096,7 +1110,12 @@ class OOTWorld(World): | ||||
|             elif barren_hint_players or woth_hint_players:  # Check only relevant oot locations for barren/woth | ||||
|                 for player in (barren_hint_players | woth_hint_players): | ||||
|                     for loc in multiworld.worlds[player].get_locations(): | ||||
|                         if loc.item.code and (not loc.locked or oot_is_item_of_type(loc.item, 'Song')): | ||||
|                         if loc.item.code and (not loc.locked or | ||||
|                             (oot_is_item_of_type(loc.item, 'Song') or | ||||
|                                 (oot_is_item_of_type(loc.item, 'SmallKey')         and multiworld.worlds[loc.player].shuffle_smallkeys     in ('overworld', 'any_dungeon', 'regional')) or | ||||
|                                 (oot_is_item_of_type(loc.item, 'HideoutSmallKey')  and multiworld.worlds[loc.player].shuffle_hideoutkeys   in ('overworld', 'any_dungeon', 'regional')) or | ||||
|                                 (oot_is_item_of_type(loc.item, 'BossKey')          and multiworld.worlds[loc.player].shuffle_bosskeys      in ('overworld', 'any_dungeon', 'regional')) or | ||||
|                                 (oot_is_item_of_type(loc.item, 'GanonBossKey')     and multiworld.worlds[loc.player].shuffle_ganon_bosskey in ('overworld', 'any_dungeon', 'regional')))): | ||||
|                             if player in barren_hint_players: | ||||
|                                 hint_area = get_hint_area(loc) | ||||
|                                 items_by_region[player][hint_area]['weight'] += 1 | ||||
| @@ -1183,6 +1202,15 @@ class OOTWorld(World): | ||||
|                             er_hint_data[self.player][location.address] = main_entrance.name | ||||
|                             logger.debug(f"Set {location.name} hint data to {main_entrance.name}") | ||||
|  | ||||
|     def write_spoiler(self, spoiler_handle: typing.TextIO) -> None: | ||||
|         required_trials_str = ", ".join(t for t in self.skipped_trials if not self.skipped_trials[t]) | ||||
|         spoiler_handle.write(f"\n\nTrials ({self.multiworld.get_player_name(self.player)}): {required_trials_str}\n") | ||||
|  | ||||
|         if self.shopsanity != 'off': | ||||
|             spoiler_handle.write(f"\nShop Prices ({self.multiworld.get_player_name(self.player)}):\n") | ||||
|             for k, v in self.shop_prices.items(): | ||||
|                 spoiler_handle.write(f"{k}: {v} Rupees\n") | ||||
|  | ||||
|     # Key ring handling: | ||||
|     # Key rings are multiple items glued together into one, so we need to give | ||||
|     # the appropriate number of keys in the collection state when they are | ||||
| @@ -1265,25 +1293,13 @@ class OOTWorld(World): | ||||
|     # Specifically ensures that only real items are gotten, not any events. | ||||
|     # In particular, ensures that Time Travel needs to be found. | ||||
|     def get_state_with_complete_itempool(self): | ||||
|         all_state = self.multiworld.get_all_state(use_cache=False) | ||||
|         # Remove event progression items | ||||
|         for item, player in all_state.prog_items: | ||||
|             if player == self.player and (item not in item_table or item_table[item][2] is None): | ||||
|                 all_state.prog_items[(item, player)] = 0 | ||||
|         # Remove all events and checked locations | ||||
|         all_state.locations_checked = {loc for loc in all_state.locations_checked if loc.player != self.player} | ||||
|         all_state.events = {loc for loc in all_state.events if loc.player != self.player} | ||||
|         all_state = CollectionState(self.multiworld) | ||||
|         for item in self.multiworld.itempool: | ||||
|             if item.player == self.player: | ||||
|                 self.multiworld.worlds[item.player].collect(all_state, item) | ||||
|         # If free_scarecrow give Scarecrow Song | ||||
|         if self.free_scarecrow: | ||||
|             all_state.collect(self.create_item("Scarecrow Song"), event=True) | ||||
|  | ||||
|         # Invalidate caches | ||||
|         all_state.child_reachable_regions[self.player] = set() | ||||
|         all_state.adult_reachable_regions[self.player] = set() | ||||
|         all_state.child_blocked_connections[self.player] = set() | ||||
|         all_state.adult_blocked_connections[self.player] = set() | ||||
|         all_state.day_reachable_regions[self.player] = set() | ||||
|         all_state.dampe_reachable_regions[self.player] = set() | ||||
|         all_state.stale[self.player] = True | ||||
|  | ||||
|         return all_state | ||||
| @@ -1349,7 +1365,7 @@ def gather_locations(multiworld: MultiWorld, | ||||
|                 condition = lambda location: location.name in dungeon_song_locations | ||||
|             locations += filter(condition, multiworld.get_unfilled_locations(player=player)) | ||||
|     else: | ||||
|         if any(map(lambda v: v in {'keysanity'}, fill_opts.values())): | ||||
|         if any(map(lambda v: v == 'keysanity', fill_opts.values())): | ||||
|             return None | ||||
|         for player, option in fill_opts.items(): | ||||
|             condition = functools.partial(valid_dungeon_item_location, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 espeon65536
					espeon65536