diff --git a/Dolphin scripts/Entrance Randomizer/__main__.py b/Dolphin scripts/Entrance Randomizer/__main__.py index 992d779..2d97bd9 100644 --- a/Dolphin scripts/Entrance Randomizer/__main__.py +++ b/Dolphin scripts/Entrance Randomizer/__main__.py @@ -20,10 +20,10 @@ from lib.constants import * # noqa: F403 from lib.constants import __version__ from lib.entrance_rando import ( - highjack_transition, highjack_transition_rando, set_transitions_map, starting_area, + temp_disabled_exits, transitions_map, ) from lib.graph_creation import create_graphml @@ -32,6 +32,7 @@ draw_text, dump_spoiler_logs, follow_pointer_path, + highjack_transition, prevent_item_softlock, prevent_transition_softlocks, reset_draw_text_index, @@ -49,7 +50,7 @@ # Dump spoiler logs and graph dump_spoiler_logs(starting_area_name, transitions_map, seed_string) -create_graphml(transitions_map, seed_string, starting_area) +create_graphml(transitions_map, temp_disabled_exits, seed_string, starting_area) async def main_loop(): diff --git a/Dolphin scripts/Entrance Randomizer/lib/constants.py b/Dolphin scripts/Entrance Randomizer/lib/constants.py index c35500a..b0613af 100644 --- a/Dolphin scripts/Entrance Randomizer/lib/constants.py +++ b/Dolphin scripts/Entrance Randomizer/lib/constants.py @@ -239,7 +239,7 @@ class LevelCRC(IntEnum): TWIN_OUTPOSTS = 0xE6B9138A TWIN_OUTPOSTS_UNDERWATER = 0xDE524DA6 UNDERGROUND_DAM = 0x9D6149E1 - VALLEY_OF_THE_SPIRITS = 0x08E3C641 + VALLEY_OF_SPIRITS = 0x08E3C641 VIRACOCHA_MONOLITHS = 0x6F498BBD VIRACOCHA_MONOLITHS_CUTSCENE = 0xE8362F5F WHACK_A_TUCO = 0x0A1F2526 @@ -255,7 +255,7 @@ class LevelCRC(IntEnum): SOFTLOCKABLE_ENTRANCES = { int(LevelCRC.FLOODED_COURTYARD): 8, # From st claire: 7 int(LevelCRC.EYES_OF_DOOM): 9, - int(LevelCRC.VALLEY_OF_THE_SPIRITS): 8, + int(LevelCRC.VALLEY_OF_SPIRITS): 8, int(LevelCRC.COPACANTI_LAKE): 8, } """Entrances that can softlock by infinitely running into a door. diff --git a/Dolphin scripts/Entrance Randomizer/lib/entrance_rando.py b/Dolphin scripts/Entrance Randomizer/lib/entrance_rando.py index 535e395..0d69492 100644 --- a/Dolphin scripts/Entrance Randomizer/lib/entrance_rando.py +++ b/Dolphin scripts/Entrance Randomizer/lib/entrance_rando.py @@ -22,6 +22,23 @@ class Choice(IntEnum): INBETWEEN = 2 +temples = ( + LevelCRC.MONKEY_TEMPLE, + LevelCRC.SCORPION_TEMPLE, + LevelCRC.PENGUIN_TEMPLE, +) + +one_way_exits = ( + # the White Valley geyser + Transition(LevelCRC.WHITE_VALLEY, LevelCRC.MOUNTAIN_SLED_RUN), + # the Apu Illapu Shrine geyser + Transition(LevelCRC.APU_ILLAPU_SHRINE, LevelCRC.WHITE_VALLEY), + # the Apu Illapu Shrine one-way door + Transition(LevelCRC.MOUNTAIN_SLED_RUN, LevelCRC.APU_ILLAPU_SHRINE), + # the Jungle Canyon waterfall + Transition(LevelCRC.CAVERN_LAKE, LevelCRC.JUNGLE_CANYON), +) + _possible_starting_areas = [ area for area in ALL_TRANSITION_AREAS # Remove unwanted starting areas from the list of possibilities @@ -57,33 +74,7 @@ class Choice(IntEnum): } ] -# Call RNG even if this is unused to not impact randomization of other things for the same seed -starting_area = random.choice(_possible_starting_areas) -if CONFIGS.STARTING_AREA is not None: - starting_area = CONFIGS.STARTING_AREA - -transitions_map: dict[tuple[int, int], Transition] = {} -"""```python -{ - (og_from_id, og_to_id): (og_from_id, og_to_id) -} -```""" - -__connections_left: dict[int, int] = {} -"""Used in randomization process to track per Area how many exits aren't connected yet.""" - -one_way_exits = ( - # the White Valley geyser - Transition(LevelCRC.WHITE_VALLEY, LevelCRC.MOUNTAIN_SLED_RUN), - # the Apu Illapu Shrine geyser - Transition(LevelCRC.APU_ILLAPU_SHRINE, LevelCRC.WHITE_VALLEY), - # the Apu Illapu Shrine one-way door - Transition(LevelCRC.MOUNTAIN_SLED_RUN, LevelCRC.APU_ILLAPU_SHRINE), - # the Jungle Canyon waterfall - Transition(LevelCRC.CAVERN_LAKE, LevelCRC.JUNGLE_CANYON), -) - -disabled_exits = ( +temp_disabled_exits = [ # Mouth of Inti has 2 connections with Altar of Huitaca, which causes problems, # basically it's very easy to get softlocked by the spider web when entering Altar of Huitaca # So for now just don't randomize it. That way runs don't just end out of nowhere @@ -95,6 +86,10 @@ class Choice(IntEnum): # So for now just don't randomize it. That way we won't have to worry about that yet (LevelCRC.TWIN_OUTPOSTS, LevelCRC.TWIN_OUTPOSTS_UNDERWATER), (LevelCRC.TWIN_OUTPOSTS_UNDERWATER, LevelCRC.TWIN_OUTPOSTS), +] + +disabled_exits = ( + *temp_disabled_exits, # The 3 Spirit Fights are not randomized, # because that will cause issues with the transformation cutscene trigger. # Plus it wouldn't really improve anything, given that the Temples are randomized anyway. @@ -141,64 +136,23 @@ class Choice(IntEnum): (LevelCRC.BETA_VOLCANO, LevelCRC.PLANE_COCKPIT), ) +# Call RNG even if this is unused to not impact randomization of other things for the same seed +starting_area = random.choice(_possible_starting_areas) +if CONFIGS.STARTING_AREA is not None: + starting_area = CONFIGS.STARTING_AREA + TRANSITION_INFOS_DICT_RANDO = TRANSITION_INFOS_DICT.copy() ALL_POSSIBLE_TRANSITIONS_RANDO = ALL_POSSIBLE_TRANSITIONS +transitions_map: dict[tuple[int, int], Transition] = {} +"""```python +{ + (og_from_id, og_to_id): (og_from_id, og_to_id) +} +```""" -def initialize_connections_left(): - for area in TRANSITION_INFOS_DICT.values(): - __connections_left[area.area_id] = len(area.exits) - - -def remove_disabled_exits(): - # remove exits from TRANSITION_INFOS_DICT_RANDO - for area in TRANSITION_INFOS_DICT.values(): - for ex in area.exits: - current = (area.area_id, ex.area_id) - if current in one_way_exits or current in disabled_exits: - TRANSITION_INFOS_DICT_RANDO[area.area_id] = Area( - area.area_id, - area.name, - area.default_entrance, - tuple([ - x for x in TRANSITION_INFOS_DICT_RANDO[area.area_id].exits if x != ex - ]), - ) - __connections_left[area.area_id] -= 1 - - # remove exits from ALL_POSSIBLE_TRANSITIONS_RANDO - global ALL_POSSIBLE_TRANSITIONS_RANDO - for trans in ALL_POSSIBLE_TRANSITIONS: - if trans in one_way_exits or trans in disabled_exits: - ALL_POSSIBLE_TRANSITIONS_RANDO = [ # pyright: ignore[reportConstantRedefinition] - x for x in ALL_POSSIBLE_TRANSITIONS_RANDO if x != trans - ] - - -def highjack_transition( - from_: int | None, - to: int | None, - redirect: int, -): - if from_ is None: - from_ = state.current_area_old - if to is None: - to = state.current_area_new - - # Early return. Detect the start of a transition - if state.current_area_old == state.current_area_new: - return False - - if from_ == state.current_area_old and to == state.current_area_new: - print( - "highjack_transition |", - f"From: {hex(state.current_area_old)},", - f"To: {hex(state.current_area_new)}.", - f"Redirecting to: {hex(redirect)}", - ) - memory.write_u32(ADDRESSES.current_area, redirect) - return True - return False +__connections_left: dict[int, int] = {} +"""Used in randomization process to track per Area how many exits aren't connected yet.""" def highjack_transition_rando(): @@ -242,6 +196,36 @@ def highjack_transition_rando(): return redirect +def initialize_connections_left(): + for area in TRANSITION_INFOS_DICT.values(): + __connections_left[area.area_id] = len(area.exits) + + +def remove_disabled_exits(): + # remove exits from TRANSITION_INFOS_DICT_RANDO + for area in TRANSITION_INFOS_DICT.values(): + for ex in area.exits: + current = (area.area_id, ex.area_id) + if current in one_way_exits or current in disabled_exits: + TRANSITION_INFOS_DICT_RANDO[area.area_id] = Area( + area.area_id, + area.name, + area.default_entrance, + tuple([ + x for x in TRANSITION_INFOS_DICT_RANDO[area.area_id].exits if x != ex + ]), + ) + __connections_left[area.area_id] -= 1 + + # remove exits from ALL_POSSIBLE_TRANSITIONS_RANDO + global ALL_POSSIBLE_TRANSITIONS_RANDO + for trans in ALL_POSSIBLE_TRANSITIONS: + if trans in one_way_exits or trans in disabled_exits: + ALL_POSSIBLE_TRANSITIONS_RANDO = [ # pyright: ignore[reportConstantRedefinition] + x for x in ALL_POSSIBLE_TRANSITIONS_RANDO if x != trans + ] + + def link_two_levels(first: Area, second: Area): __connections_left[first.area_id] -= 1 __connections_left[second.area_id] -= 1 diff --git a/Dolphin scripts/Entrance Randomizer/lib/graph_creation.py b/Dolphin scripts/Entrance Randomizer/lib/graph_creation.py index 3482ffa..2b7136a 100644 --- a/Dolphin scripts/Entrance Randomizer/lib/graph_creation.py +++ b/Dolphin scripts/Entrance Randomizer/lib/graph_creation.py @@ -1,7 +1,8 @@ from __future__ import annotations -from collections.abc import Mapping +from collections.abc import Sequence from pathlib import Path +from typing import Any from lib.constants import * # noqa: F403 from lib.constants import __version__ @@ -33,7 +34,7 @@ def create_vertices( - transitions_map: Mapping[tuple[int, int], tuple[int, int]], + transitions_map: dict[tuple[int, int], tuple[int, int]], starting_area: int, ): output_text = "" @@ -63,25 +64,25 @@ def create_vertices( output_text += ( f'\n' + + f'id="{counter}">\n' ) counter += 1 for pairing in connections_one_way: output_text += ( f'\n' + + f'id="{counter}">\n' ) counter += 1 return output_text def create_graphml( - transitions_map: Mapping[tuple[int, int], tuple[int, int]], + # NOTE: dict is invariant, but Mapping doesn't implement copy + transitions_map: dict[tuple[int, int], tuple[int, int]] | dict[tuple[int, int], Any], + temp_disabled_exits: Sequence[tuple[int, int]], seed_string: SeedType, starting_area: int, ): - graphml_text = f"""\ - - - - {create_vertices(transitions_map, starting_area)} - {create_edges(transitions_map)} - -""" + all_transitions = transitions_map.copy() + for item in temp_disabled_exits: + all_transitions[item] = item + + graphml_text = ( + '' + + '\n' + + create_vertices(all_transitions, starting_area) + + create_edges(all_transitions) + + "" + ) # TODO (Avasam): Get actual user folder based whether Dolphin Emulator is in AppData/Roaming # and if the current installation is portable. diff --git a/Dolphin scripts/Entrance Randomizer/lib/utils.py b/Dolphin scripts/Entrance Randomizer/lib/utils.py index d7ddd23..5a3c7a3 100644 --- a/Dolphin scripts/Entrance Randomizer/lib/utils.py +++ b/Dolphin scripts/Entrance Randomizer/lib/utils.py @@ -45,49 +45,6 @@ def draw_text(text: str): _draw_text_index += 1 -def dump_spoiler_logs( - starting_area_name: str, - transitions_map: Mapping[tuple[int, int], tuple[int, int]], - seed_string: SeedType, -): - spoiler_logs = f"Starting area: {starting_area_name}\n" - red_string_list = [ - f"{TRANSITION_INFOS_DICT[original[0]].name} " - + f"({TRANSITION_INFOS_DICT[original[1]].name} exit) " - + f"will redirect to: {TRANSITION_INFOS_DICT[redirect[1]].name} " - + f"({TRANSITION_INFOS_DICT[redirect[0]].name} entrance)\n" - for original, redirect in transitions_map.items() - ] - red_string_list.sort() - for string in red_string_list: - spoiler_logs += string - - unrandomized_transitions = ALL_POSSIBLE_TRANSITIONS - transitions_map.keys() - if len(unrandomized_transitions) > 0: - spoiler_logs += "\nUnrandomized transitions:\n" - non_random_string_list = [ - f"From: {TRANSITION_INFOS_DICT[transition[0]].name}, " - + f"To: {TRANSITION_INFOS_DICT[transition[1]].name}.\n" - for transition in unrandomized_transitions - ] - non_random_string_list.sort() - for string in non_random_string_list: - spoiler_logs += string - - # TODO (Avasam): Get actual user folder based whether Dolphin Emulator is in AppData/Roaming - # and if the current installation is portable. - dolphin_path = Path().absolute() - spoiler_logs_file = ( - dolphin_path - / "User" - / "Logs" - / f"SPOILER_LOGS_v{__version__}_{seed_string}.txt" - ) - Path.mkdir(spoiler_logs_file.parent, parents=True, exist_ok=True) - Path.write_text(spoiler_logs_file, spoiler_logs) - print("Spoiler logs written to", spoiler_logs_file) - - def follow_pointer_path(ppath: Sequence[int]): addr = ppath[0] for i in range(len(ppath) - 1): @@ -100,6 +57,48 @@ def follow_pointer_path(ppath: Sequence[int]): return addr +def highjack_transition( + from_: int | None, + to: int | None, + redirect: int, +): + if from_ is None: + from_ = state.current_area_old + if to is None: + to = state.current_area_new + + # Early return. Detect the start of a transition + if state.current_area_old == state.current_area_new: + return False + + if from_ == state.current_area_old and to == state.current_area_new: + print( + "highjack_transition |", + f"From: {hex(state.current_area_old)},", + f"To: {hex(state.current_area_new)}.", + f"Redirecting to: {hex(redirect)}", + ) + memory.write_u32(ADDRESSES.current_area, redirect) + return True + return False + + +def prevent_transition_softlocks(): + """Prevents softlocking on closed doors by making Harry land.""" + height_offset = SOFTLOCKABLE_ENTRANCES.get(state.current_area_new) + if ( + # As far as we're concerned, these are indeed magic numbers. + # We haven't identified a name for these states yet. + state.area_load_state_old == 5 and state.area_load_state_new == 6 # noqa: PLR2004 + # TODO: Include "from" transition to only bump player up when needed + and height_offset + ): + player_z_addr = follow_pointer_path((ADDRESSES.player_ptr, PlayerPtrOffset.PositionZ)) + # memory.write_f32(player_x_addr, memory.read_f32(player_x_addr) + 30) + # memory.write_f32(player_y_addr, memory.read_f32(player_y_addr) + 30) + memory.write_f32(player_z_addr, memory.read_f32(player_z_addr) + height_offset) + + def prevent_item_softlock(): """ Prevent softlocking by missing the right items. @@ -183,17 +182,44 @@ def prevent_item_softlock(): return -def prevent_transition_softlocks(): - """Prevents softlocking on closed doors by making Harry land.""" - height_offset = SOFTLOCKABLE_ENTRANCES.get(state.current_area_new) - if ( - # As far as we're concerned, these are indeed magic numbers. - # We haven't identified a name for these states yet. - state.area_load_state_old == 5 and state.area_load_state_new == 6 # noqa: PLR2004 - # TODO: Include "from" transition to only bump player up when needed - and height_offset - ): - player_z_addr = follow_pointer_path((ADDRESSES.player_ptr, PlayerPtrOffset.PositionZ)) - # memory.write_f32(player_x_addr, memory.read_f32(player_x_addr) + 30) - # memory.write_f32(player_y_addr, memory.read_f32(player_y_addr) + 30) - memory.write_f32(player_z_addr, memory.read_f32(player_z_addr) + height_offset) +def dump_spoiler_logs( + starting_area_name: str, + transitions_map: Mapping[tuple[int, int], tuple[int, int]], + seed_string: SeedType, +): + spoiler_logs = f"Starting area: {starting_area_name}\n" + red_string_list = [ + f"{TRANSITION_INFOS_DICT[original[0]].name} " + + f"({TRANSITION_INFOS_DICT[original[1]].name} exit) " + + f"will redirect to: {TRANSITION_INFOS_DICT[redirect[1]].name} " + + f"({TRANSITION_INFOS_DICT[redirect[0]].name} entrance)\n" + for original, redirect in transitions_map.items() + ] + red_string_list.sort() + for string in red_string_list: + spoiler_logs += string + + unrandomized_transitions = ALL_POSSIBLE_TRANSITIONS - transitions_map.keys() + if len(unrandomized_transitions) > 0: + spoiler_logs += "\nUnrandomized transitions:\n" + non_random_string_list = [ + f"From: {TRANSITION_INFOS_DICT[transition[0]].name}, " + + f"To: {TRANSITION_INFOS_DICT[transition[1]].name}.\n" + for transition in unrandomized_transitions + ] + non_random_string_list.sort() + for string in non_random_string_list: + spoiler_logs += string + + # TODO (Avasam): Get actual user folder based whether Dolphin Emulator is in AppData/Roaming + # and if the current installation is portable. + dolphin_path = Path().absolute() + spoiler_logs_file = ( + dolphin_path + / "User" + / "Logs" + / f"SPOILER_LOGS_v{__version__}_{seed_string}.txt" + ) + Path.mkdir(spoiler_logs_file.parent, parents=True, exist_ok=True) + Path.write_text(spoiler_logs_file, spoiler_logs) + print("Spoiler logs written to", spoiler_logs_file)