From 3fa0bc8d66a72b49d8c4f684422888de1e460187 Mon Sep 17 00:00:00 2001 From: Avasam Date: Sat, 25 May 2024 12:58:24 -0400 Subject: [PATCH 1/7] WIP link to proper entrance, anti door softlock, add back crash site, fix teleporters, more area info, fix _shaman_shop_prices error --- .../Entrance Randomizer/CONFIGS.py | 12 +- .../Entrance Randomizer/__main__.py | 55 ++++--- .../Entrance Randomizer/lib/constants.py | 138 ++++++++++++++---- .../Entrance Randomizer/lib/entrance_rando.py | 120 +++++++-------- .../Entrance Randomizer/lib/shaman_shop.py | 2 +- .../Entrance Randomizer/lib/utils.py | 58 +++++++- Dolphin scripts/README.md | 8 +- Various technical notes/transition_infos.json | 72 ++++++++- 8 files changed, 331 insertions(+), 134 deletions(-) diff --git a/Dolphin scripts/Entrance Randomizer/CONFIGS.py b/Dolphin scripts/Entrance Randomizer/CONFIGS.py index 152d35c..2303d6c 100644 --- a/Dolphin scripts/Entrance Randomizer/CONFIGS.py +++ b/Dolphin scripts/Entrance Randomizer/CONFIGS.py @@ -7,6 +7,8 @@ Set your own seed, can be an `int`, `float`, `str`, `bytes` or `bytearray`. Use `None` or any Falsy value to generate a random seed. + +default = None """ STARTING_AREA: int | None = None @@ -14,12 +16,16 @@ The ID of the Area to start in. `None` for random, `0xEE8F6900` for Crash Site. See `transition_infos.json` for all available IDs + +default = None """ -LINKED_TRANSITIONS: bool = False +LINKED_TRANSITIONS: bool = True """ Whether the new destination will contain an exit back to the area you came from. Assuming both areas have as many entrances as they have exits. + +default = True """ DISABLE_MAPS_IN_SHOP: bool = True @@ -28,6 +34,8 @@ When maps are disabled, and using original shop prices, the 4 lowest prices (0, 1, 2, 2) are also removed form the pool. + +default = True """ SHOP_PRICES_RANGE: PriceRange = False # (0, 32) @@ -45,4 +53,6 @@ So it's possible for your "minimum" price to not be respected based on RNG. Use `False` or `()` to shuffle around original shop prices. + +default = False # (0, 32) """ diff --git a/Dolphin scripts/Entrance Randomizer/__main__.py b/Dolphin scripts/Entrance Randomizer/__main__.py index ffbb812..12d5982 100644 --- a/Dolphin scripts/Entrance Randomizer/__main__.py +++ b/Dolphin scripts/Entrance Randomizer/__main__.py @@ -24,11 +24,17 @@ highjack_transition_rando, set_transitions_map, starting_area, - state, transitions_map, ) from lib.shaman_shop import patch_shaman_shop, randomize_shaman_shop -from lib.utils import draw_text, dump_spoiler_logs, reset_draw_text_index +from lib.utils import ( + draw_text, + dump_spoiler_logs, + follow_pointer_path, + prevent_transition_softlocks, + reset_draw_text_index, + state, +) set_transitions_map() randomize_shaman_shop() @@ -37,12 +43,7 @@ try: starting_area_name = TRANSITION_INFOS_DICT[starting_area].name except KeyError: - if starting_area == CRASH_SITE: - starting_area_name = "Crash Site" - elif starting_area == TELEPORTERS: - starting_area_name = "Teleport" - else: - starting_area_name = str(starting_area) + starting_area_name = hex(starting_area).upper() + " (not in randomization)" # Dump spoiler logs dump_spoiler_logs(starting_area_name, transitions_map, seed_string) @@ -52,19 +53,27 @@ async def main_loop(): # Read memory, setup loop values, print debug to screen reset_draw_text_index() state.current_area_old = state.current_area_new + state.area_load_state_old = state.area_load_state_new await event.frameadvance() state.current_area_new = memory.read_u32(ADDRESSES.current_area) + state.area_load_state_new = memory.read_u32(ADDRESSES.area_load_state) current_area = TRANSITION_INFOS_DICT.get(state.current_area_new) + previous_area_id = memory.read_u32(follow_pointer_path(ADDRESSES.prev_area)) + previous_area = TRANSITION_INFOS_DICT.get(previous_area_id) draw_text(f"Rando version: {__version__}") draw_text(f"Seed: {seed_string}") draw_text(patch_shaman_shop()) draw_text( - f"Starting area: {hex(starting_area)}" + f"Starting area: {hex(starting_area).upper()}" + " (Random)" if CONFIGS.STARTING_AREA is None else f"{starting_area_name}", ) draw_text( - f"Current area: {hex(state.current_area_new).upper()}" - + f"({current_area.name})" if current_area else "", + f"Current area: {hex(state.current_area_new).upper()} " + + (f"({current_area.name})" if current_area else ""), + ) + draw_text( + f"From entrance: {hex(previous_area_id).upper()} " + + (f"({previous_area.name})" if previous_area else ""), ) # Always re-enable Item Swap. @@ -72,27 +81,35 @@ async def main_loop(): memory.write_u32(ADDRESSES.item_swap, 0) # Skip the intro fight and cutscene - if highjack_transition(0x0, JAGUAR, starting_area): + if highjack_transition(0x0, LevelCRC.JAGUAR, starting_area): return # Standardize the Altar of Ages exit - if highjack_transition(ALTAR_OF_AGES, None, MYSTERIOUS_TEMPLE): + if highjack_transition(LevelCRC.ALTAR_OF_AGES, None, LevelCRC. MYSTERIOUS_TEMPLE): # Even if the cutscene isn't actually watched. # Just leaving the Altar is good enough for the rando. state.visited_altar_of_ages = True - state.current_area_new = MYSTERIOUS_TEMPLE + state.current_area_new = LevelCRC.MYSTERIOUS_TEMPLE # Standardize the Viracocha Monoliths cutscene - if highjack_transition(None, VIRACOCHA_MONOLITHS_CUTSCENE, VIRACOCHA_MONOLITHS): - state.current_area_new = VIRACOCHA_MONOLITHS + if highjack_transition( + None, + LevelCRC.VIRACOCHA_MONOLITHS_CUTSCENE, + LevelCRC.VIRACOCHA_MONOLITHS, + ): + state.current_area_new = LevelCRC.VIRACOCHA_MONOLITHS # Standardize St. Claire's Excavation Camp - if highjack_transition(None, ST_CLAIRE_NIGHT, ST_CLAIRE_DAY): - state.current_area_new = ST_CLAIRE_DAY + if highjack_transition(None, LevelCRC.ST_CLAIRE_NIGHT, LevelCRC.ST_CLAIRE_DAY): + state.current_area_new = LevelCRC.ST_CLAIRE_DAY + + # TODO: Skip swim levels (3) redirect = highjack_transition_rando() if redirect: - state.current_area_new = redirect + state.current_area_new = redirect[1] + + prevent_transition_softlocks() while True: diff --git a/Dolphin scripts/Entrance Randomizer/lib/constants.py b/Dolphin scripts/Entrance Randomizer/lib/constants.py index ceefefc..f0dae46 100644 --- a/Dolphin scripts/Entrance Randomizer/lib/constants.py +++ b/Dolphin scripts/Entrance Randomizer/lib/constants.py @@ -3,13 +3,14 @@ import random import sys from dataclasses import dataclass +from enum import IntEnum from itertools import chain import CONFIGS from dolphin import memory # pyright: ignore[reportMissingModuleSource] from lib.transition_infos import transition_infos -__version__ = "0.3.3" +__version__ = "0.4.0" """ Major: New major feature or functionality @@ -32,6 +33,10 @@ class Addresses: version_string: str prev_area: list[int] current_area: int + area_load_state: int + player_x: list[int] + player_y: list[int] + player_z: list[int] item_swap: int shaman_shop_struct: int @@ -40,12 +45,13 @@ class Addresses: area.area_id: area for area in chain(*transition_infos) } ALL_TRANSITION_AREAS = {area.area_id for area in chain(*transition_infos)} -ALL_POSSIBLE_EXITS = [ - exit_.area_id for exit_ in chain( - *(area.exits for area in TRANSITION_INFOS_DICT.values()), - ) +ALL_POSSIBLE_TRANSITIONS = [ + (area.area_id, exit_.area_id) + for area in TRANSITION_INFOS_DICT.values() + for exit_ in area.exits ] + _game_id_base = "".join([ chr(memory.read_u8(0x80000000 + i)) for i in range(3) @@ -67,14 +73,74 @@ class Addresses: raise Exception(f"Unknown game version {GAME_VERSION}!") _addresses_map = { "GPH": { - "D": Addresses("GC DE 0-00", [0x80747648], 0x80417F50, 0x804C7734, TODO), - "E": Addresses("GC US 0-00", [0x8072B648], 0x8041BEB4, 0x804CB694, 0x7E00955C), - "F": Addresses("GC FR 0-00", [0x80747648], 0x80417F30, 0x804C7714, TODO), - "P": Addresses("GC EU 0-00", [0x80747648], 0x80417F10, 0x804C76F4, TODO), + "D": Addresses( + version_string="GC DE 0-00", + prev_area=[0x80747648], + current_area=0x80417F50, + area_load_state=TODO, + player_x=[], + player_y=[], + player_z=[], + item_swap=0x804C7734, + shaman_shop_struct=TODO, + ), + "E": Addresses( + version_string="GC US 0-00", + prev_area=[0x8072B648], + current_area=0x8041BEB4, + area_load_state=0x8041BEC8, + player_x=[0x8041BE4C, 0x338], + player_y=[0x8041BE4C, 0x33C], + player_z=[0x8041BE4C, 0x340], + item_swap=0x804CB694, + shaman_shop_struct=0x7E00955C, + ), + "F": Addresses( + version_string="GC FR 0-00", + prev_area=[0x80747648], + current_area=0x80417F30, + area_load_state=TODO, + player_x=[], + player_y=[], + player_z=[], + item_swap=0x804C7714, + shaman_shop_struct=TODO, + ), + "P": Addresses( + version_string="GC EU 0-00", + prev_area=[0x80747648], + current_area=0x80417F10, + area_load_state=TODO, + player_x=[], + player_y=[], + player_z=[], + item_swap=0x804C76F4, + shaman_shop_struct=TODO, + ), }, "RPF": { - "E": Addresses("Wii US 0-00", [0x804542DC, 0x8], 0x80448D04, 0x80446608, TODO), - "P": Addresses("Wii EU 0-00", [0x804546DC, 0x18], 0x80449104, 0x80446A08, TODO), + "E": Addresses( + version_string="Wii US 0-00", + prev_area=[0x804542DC, 0x8], + current_area=0x80448D04, + area_load_state=TODO, + player_x=[], + player_y=[], + player_z=[], + item_swap=0x80446608, + shaman_shop_struct=TODO, + ), + "P": Addresses( + version_string="Wii EU 0-00", + prev_area=[0x804546DC, 0x18], + current_area=0x80449104, + area_load_state=TODO, + player_x=[], + player_y=[], + player_z=[], + item_swap=0x80446A08, + shaman_shop_struct=TODO, + ), }, } @@ -88,23 +154,39 @@ class Addresses: ADDRESSES = _addresses print(f"Detected {ADDRESSES.version_string} version!") -# Level CRCs -JAGUAR = 0x99885996 -CRASH_SITE = 0xEE8F6900 -PLANE_COCKPIT = 0x4A3E4058 -CHAMELEON_TEMPLE = 0x0081082C -JUNGLE_CANYON = 0xDEDA69BC -MAMA_OULLO_TOWER = 0x07ECCC35 -VIRACOCHA_MONOLITHS = 0x6F498BBD -VIRACOCHA_MONOLITHS_CUTSCENE = 0xE8362F5F -ALTAR_OF_AGES = 0xABD7CCD8 -BITTENBINDERS_CAMP = 0x0EF63551 -MYSTERIOUS_TEMPLE = 0x099BF148 -APU_ILLAPU_SHRINE = 0x5511C46C -SCORPION_TEMPLE = 0x4B08BBEB -ST_CLAIRE_NIGHT = 0x72AD42FA -ST_CLAIRE_DAY = 0x72AD42FA -TELEPORTERS = 0xE97CB47C + +class LevelCRC(IntEnum): + JAGUAR = 0x99885996 + CRASH_SITE = 0xEE8F6900 + PLANE_COCKPIT = 0x4A3E4058 + FLOODED_COURTYARD = 0xECC9D759 + CHAMELEON_TEMPLE = 0x0081082C + JUNGLE_CANYON = 0xDEDA69BC + MAMA_OULLO_TOWER = 0x07ECCC35 + VIRACOCHA_MONOLITHS = 0x6F498BBD + VIRACOCHA_MONOLITHS_CUTSCENE = 0xE8362F5F + ALTAR_OF_AGES = 0xABD7CCD8 + BITTENBINDERS_CAMP = 0x0EF63551 + MYSTERIOUS_TEMPLE = 0x099BF148 + EYES_OF_DOOM = 0x9A0C8DF8 + SCORPION_TEMPLE = 0x4B08BBEB + PENGUIN_TEMPLE = 0x1B11EC74 + VALLEY_OF_THE_SPIRITS = 0x08E3C641 + COPACANTI_LAKE = 0x8147EA91 + APU_ILLAPU_SHRINE = 0x5511C46C + ST_CLAIRE_NIGHT = 0x72AD42FA + ST_CLAIRE_DAY = 0x72AD42FA + TELEPORTS = 0xE97CB47C + + +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.COPACANTI_LAKE): 8, +} +"""Entrances that can softlock by infinitly running into a door. +Value is the minimum height boost needed to regain control.""" GC_MIN_ADDRESS = 0x80000000 GC_MEM_SIZE = 0x1800000 diff --git a/Dolphin scripts/Entrance Randomizer/lib/entrance_rando.py b/Dolphin scripts/Entrance Randomizer/lib/entrance_rando.py index 3439f88..edd0c80 100644 --- a/Dolphin scripts/Entrance Randomizer/lib/entrance_rando.py +++ b/Dolphin scripts/Entrance Randomizer/lib/entrance_rando.py @@ -2,64 +2,57 @@ import random from collections.abc import Iterable +from itertools import starmap +from typing import Literal, NamedTuple import CONFIGS from lib.constants import * # noqa: F403 +from lib.utils import follow_pointer_path, state _possible_starting_areas = [ area for area in ALL_TRANSITION_AREAS # Remove impossible start areas + Don't immediately give TNT - if area not in {APU_ILLAPU_SHRINE, SCORPION_TEMPLE, ST_CLAIRE_DAY} - # Add back areas removed from transitions because of issues -] + [CRASH_SITE, TELEPORTERS] + if area not in { + LevelCRC.APU_ILLAPU_SHRINE, + LevelCRC.SCORPION_TEMPLE, + LevelCRC.ST_CLAIRE_DAY, + } +] starting_area = CONFIGS.STARTING_AREA or random.choice(_possible_starting_areas) -@dataclass -class State: - # Initialize a few values to be used in loop - current_area_old = 0 - """Area ID of the previous frame""" - current_area_new = 0 - """Area ID of the current frame""" - visited_altar_of_ages = False - - -state = State() - - -def get_prev_area_addr(): - """Used to set where you come from when you enter a new area.""" - addr = ADDRESSES.prev_area[0] - for i in range(len(ADDRESSES.prev_area) - 1): - addr = memory.read_u32(addr + ADDRESSES.prev_area[i + 1]) - if addr < GC_MIN_ADDRESS or addr > GC_MAX_ADDRESS: - raise Exception(f"Invalid address {addr}") - return addr +class Transition(NamedTuple): + from_: int + to: int # -> int: pyright doesn't narrow `int | False` to just `int` after truthy check -def highjack_transition_rando() -> int: - # Early return, faster check +def highjack_transition_rando() -> tuple[int, int] | Literal[False]: + # Early return, faster check. Detect teh start of a transition if state.current_area_old == state.current_area_new: return False - redirect = transitions_map.get(state.current_area_old, {}).get(state.current_area_new) + redirect = transitions_map.get((state.current_area_old, state.current_area_new)) if not redirect: return False # Apply Altar of Ages logic to St. Claire's Excavation Camp - if redirect in {ST_CLAIRE_DAY, ST_CLAIRE_NIGHT}: - redirect = ST_CLAIRE_NIGHT if state.visited_altar_of_ages else ST_CLAIRE_DAY + if redirect.to in {LevelCRC.ST_CLAIRE_DAY, LevelCRC.ST_CLAIRE_NIGHT}: + redirect = Transition( + redirect.from_, + to=LevelCRC.ST_CLAIRE_NIGHT if state.visited_altar_of_ages else LevelCRC.ST_CLAIRE_DAY, + ) print( "highjack_transition_rando |", f"From: {hex(state.current_area_old)},", f"To: {hex(state.current_area_new)}.", - f"Redirecting to: {hex(redirect)}", + f"Redirecting to: {hex(redirect.from_)}", + f"({hex(redirect.to)} entrance)\n", ) - memory.write_u32(ADDRESSES.current_area, redirect) + memory.write_u32(follow_pointer_path(ADDRESSES.prev_area), redirect.from_) + memory.write_u32(ADDRESSES.current_area, redirect.to) return redirect @@ -80,20 +73,11 @@ def highjack_transition(from_: int | None, to: int | None, redirect: int): return False -def get_random_redirection(from_: int, _original_to: int, possible_redirections: Iterable[int]): +def get_random_redirection(original: Transition, possible_redirections: Iterable[Transition]): possible_redirections = [ - area for area in possible_redirections + redirect for redirect in possible_redirections # Prevent looping on itself - if area != from_ - # Prevent unintended entrances to Crash Site (resets most progression!) - # and ( - # area != CRASH_SITE - # # Going from Cockpit or Canyon to Crash Site is OK. - # or ( - # from_ in {PLANE_COCKPIT, JUNGLE_CANYON} - # and original_to == CRASH_SITE - # ) - # ) + if original.from_ != redirect.to ] # Investigate and explain why that can happen return random.choice(possible_redirections) \ @@ -101,51 +85,45 @@ def get_random_redirection(from_: int, _original_to: int, possible_redirections: else None -transitions_map: dict[int, dict[int, int]] = {} +transitions_map: dict[tuple[int, int], Transition] = {} """```python { - from_id: { - og_to_id: remapped_to_id - } + (og_from_id, og_to_id): (og_from_id, og_to_id) } ```""" def set_transitions_map(): - def _transition_map_set(from_: int, to: int, redirect: int): - if transitions_map.get(from_): - transitions_map[from_][to] = redirect - else: - transitions_map[from_] = {to: redirect} - _possible_exits_bucket = ALL_POSSIBLE_EXITS.copy() + _possible_transitions_bucket = list(starmap(Transition, ALL_POSSIBLE_TRANSITIONS)) """A temporary container of transitions to pick from until it is empty.""" transitions_map.clear() for area in TRANSITION_INFOS_DICT.values(): - from_ = area.area_id for to_og in (exit_.area_id for exit_ in area.exits): - redirect = get_random_redirection(from_, to_og, _possible_exits_bucket) - if redirect is None or transitions_map.get(from_, {}).get(to_og): + original = Transition(from_=area.area_id, to=to_og) + redirect = get_random_redirection(original, _possible_transitions_bucket) + if redirect is None or original in transitions_map: # Don't override something set in a previous iteration, # like from linked two-way entrances. continue - _transition_map_set(from_, to_og, redirect) - _possible_exits_bucket.remove(redirect) + transitions_map[original] = redirect + _possible_transitions_bucket.remove(redirect) if CONFIGS.LINKED_TRANSITIONS: # Ensure we haven't already expanded all transitions back to the "from" area. # (I think that means that area had more exits than entrances) - if from_ not in _possible_exits_bucket: - continue - try: - # Get a still-available exit from the area we're redirecting to - entrance = next( - exit_.area_id for exit_ - in TRANSITION_INFOS_DICT[redirect].exits - if exit_.area_id in _possible_exits_bucket - ) - except IndexError: - # That area had more entrances than exits + # if original.from_ not in ( + # transition.to for transition in _possible_transitions_bucket + # ): + # continue + flipped_redirect = Transition(from_=redirect.to, to=redirect.from_) + flipped_original = Transition(from_=original.to, to=original.from_) + # Ensure that the transition is even possible to reverse + # (neither entrance nor exists should be one-way) + if ( + flipped_redirect not in ALL_POSSIBLE_TRANSITIONS + or flipped_original not in _possible_transitions_bucket + ): continue - _transition_map_set(redirect, entrance, from_) - _possible_exits_bucket.remove(from_) + transitions_map[flipped_redirect] = flipped_original + _possible_transitions_bucket.remove(flipped_original) diff --git a/Dolphin scripts/Entrance Randomizer/lib/shaman_shop.py b/Dolphin scripts/Entrance Randomizer/lib/shaman_shop.py index 222bc58..c273f1c 100644 --- a/Dolphin scripts/Entrance Randomizer/lib/shaman_shop.py +++ b/Dolphin scripts/Entrance Randomizer/lib/shaman_shop.py @@ -107,7 +107,7 @@ def randomize_shaman_shop(): minimum_max_price = equal_price_per_item + 1 max_price = max(minimum_max_price, CONFIGS.SHOP_PRICES_RANGE[1]) min_price = min(max_price, equal_price_per_item, CONFIGS.SHOP_PRICES_RANGE[0]) - _shaman_shop_prices: list[int] = [] + _shaman_shop_prices.clear() for items_left in range(shop_size, 0, -1): # Ensure we don't bust the total of idols max_price = min(idols_left, max_price) diff --git a/Dolphin scripts/Entrance Randomizer/lib/utils.py b/Dolphin scripts/Entrance Randomizer/lib/utils.py index 42edf01..c06cf98 100644 --- a/Dolphin scripts/Entrance Randomizer/lib/utils.py +++ b/Dolphin scripts/Entrance Randomizer/lib/utils.py @@ -1,5 +1,6 @@ from __future__ import annotations +from collections.abc import Mapping, Sequence from pathlib import Path from dolphin import gui # pyright: ignore[reportMissingModuleSource] @@ -13,6 +14,21 @@ """Count how many times draw_text has been called this frame""" +@dataclass +class State: + # Initialize a few values to be used in loop + area_load_state_old = 0 + area_load_state_new = 0 + current_area_old = 0 + """Area ID of the previous frame""" + current_area_new = 0 + """Area ID of the current frame""" + visited_altar_of_ages = False + + +state = State() + + def reset_draw_text_index(): global _draw_text_index _draw_text_index = 0 @@ -30,15 +46,17 @@ def draw_text(text: str): def dump_spoiler_logs( starting_area_name: str, - transitions_map: dict[int, dict[int, int]], + transitions_map: Mapping[tuple[int, int], tuple[int, int]], seed_string: SeedType, ): spoiler_logs = f"Starting area: {starting_area_name}\n" - for from_, to_old_and_new in transitions_map.items(): - for to_old, to_new in to_old_and_new.items(): - spoiler_logs += f"From: {TRANSITION_INFOS_DICT[from_].name}, " + \ - f"To: {TRANSITION_INFOS_DICT[to_old].name}. " + \ - f"Redirecting to: {TRANSITION_INFOS_DICT[to_new].name}\n" + for original, redirect in transitions_map.items(): + spoiler_logs += ( + f"From: {TRANSITION_INFOS_DICT[original[0]].name}, " + + f"To: {TRANSITION_INFOS_DICT[original[1]].name}. " + + f"Redirecting to: {TRANSITION_INFOS_DICT[redirect[1]].name} " + + f"({TRANSITION_INFOS_DICT[redirect[0]].name} entrance)\n" + ) # TODO (Avasam): Get actual user folder based whether Dolphin Emulator is in AppData/Roaming # and if the current installation is portable. @@ -52,3 +70,31 @@ def dump_spoiler_logs( 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): + addr = memory.read_u32(addr) + ppath[i + 1] + if addr < GC_MIN_ADDRESS or addr > GC_MAX_ADDRESS: + raise ValueError( + f"Invalid address {hex(addr).upper()} for pointer path " + + str([hex(level).upper() for level in ppath]), + ) + return addr + + +def prevent_transition_softlocks(): + """Prevents softlocking on closed door by making Harry land.""" + # As far as we're concerned, these are indeed magic numbers. + # We haven't identified a name for these states yet. + height_offset = SOFTLOCKABLE_ENTRANCES.get(state.current_area_new) + if ( + 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_z) + # 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) diff --git a/Dolphin scripts/README.md b/Dolphin scripts/README.md index 9ca03ab..cbf8d64 100644 --- a/Dolphin scripts/README.md +++ b/Dolphin scripts/README.md @@ -19,10 +19,10 @@ ### Known issues and limitations - To generate a new seed, simply reload the script. -- Non-vanilla transitions will always spawn Harry at the default entrance. This can be a bit confusing when using linked transitions. -- Some areas are not randomized because they are broken with default transition. These can be fixed eventually: - - Crash Site: Removes all abilities and items - - Teleport: The teleporter pads only activate based on which (vanilla) transition was taken. +- Some linked transitions are not spawning at the right entrance and use the default entrance instead. Known cases: + - Jungle Canyon from Punchau Shrine + - Bittenbinder's Camp from Mysterious Temple +- One-way transitions are not linked together. This can be a bit confusing when sometimes going back leads to a different area. ### Developing diff --git a/Various technical notes/transition_infos.json b/Various technical notes/transition_infos.json index 9c6f636..2a5cac5 100644 --- a/Various technical notes/transition_infos.json +++ b/Various technical notes/transition_infos.json @@ -37,12 +37,29 @@ }, { "Altar of Ages": "Not including the BBCamp exit as it only happens once from a cutscene and is cut out in entrance rando.", - "St. Claire's Excavation Camp": "Only including the Day ID, rando logic takes care of standardizing the night ID.", - "teleports": "teleports are broken if not comming from the expected areas", - "Crash Site": "Crash Site resets progression if not comming from the expected areas" + "St. Claire's Excavation Camp": "Only including the Day ID, rando logic takes care of standardizing the night ID." } ], "Jungle": [ + { + "area_id": "0xEE8F6900", + "area_name": "Crash Site", + "default_entrance": "0x53257119", + "exits": [ + { + "area_id": "0xDEDA69BC", + "area_name": "Jungle Canyon", + "requires": null + }, + { + "area_id": "0x4A3E4058", + "area_name": "Plane Cockpit", + "requires": [ + "SPEEDRUN" + ] + } + ] + }, { "area_id": "0x0081082C", "area_name": "Chameleon Temple", @@ -212,6 +229,11 @@ "area_name": "Plane Cockpit", "default_entrance": "0x38C7AE7D", "exits": [ + { + "area_id": "0xEE8F6900", + "area_name": "Crash Site", + "requires": null + }, { "area_id": "0x38C7AE7D", "area_name": "Punchau Shrine", @@ -370,6 +392,11 @@ "area_name": "Jungle Canyon", "default_entrance": "0xEE8F6900", "exits": [ + { + "area_id": "0xEE8F6900", + "area_name": "Crash Site", + "requires": null + }, { "area_id": "0x9D6149E1", "area_name": "Underground Dam", @@ -403,6 +430,28 @@ "requires": [] } ] + }, + { + "area_id": "0xE97CB47C", + "area_name": "Teleports", + "default_entrance": "0x239A2165", + "exits": [ + { + "area_id": "0x239A2165", + "area_name": "Turtle Monument", + "requires": null + }, + { + "area_id": "0x9D6149E1", + "area_name": "Underground Dam", + "requires": null + }, + { + "area_id": "0x62548B77", + "area_name": "White Valley", + "requires": null + } + ] } ], "Native Territory": [ @@ -457,6 +506,11 @@ "area_name": "Turtle Monument", "default_entrance": "0xE6B9138A", "exits": [ + { + "area_id": "0xE97CB47C", + "area_name": "Jungle Teleport", + "requires": [] + }, { "area_id": "0xE6B9138A", "area_name": "Twin Outposts (2 exits)", @@ -653,6 +707,11 @@ "area_name": "Underground Dam", "default_entrance": "0xDEDA69BC", "exits": [ + { + "area_id": "0xE97CB47C", + "area_name": "Cavern Teleport", + "requires": [] + }, { "area_id": "0xDEDA69BC", "area_name": "Jungle Canyon", @@ -738,7 +797,7 @@ { "area_id": "0x1B8833D3", "area_name": "Mountain Sled Run", - "default_entrance": "0x08E3C641", + "default_entrance": "0x8147EA91", "exits": [ { "area_id": "0x8147EA91", @@ -786,6 +845,11 @@ "area_name": "White Valley", "default_entrance": "0x1553BBE1", "exits": [ + { + "area_id": "0xE97CB47C", + "area_name": "Mountain Teleport", + "requires": [] + }, { "area_id": "0x1553BBE1", "area_name": "Penguin Den", From b69b9718dc3e3bc4372c270cf4968662cdaf7c78 Mon Sep 17 00:00:00 2001 From: Avasam Date: Sat, 25 May 2024 13:12:32 -0400 Subject: [PATCH 2/7] Linters updates and fixes --- .pre-commit-config.yaml | 29 ++++++++++++++--------------- .vscode/settings.json | 1 + pyproject.toml | 2 ++ 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 43e6bad..7254b6b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,17 +9,16 @@ repos: - id: mixed-line-ending args: [--fix=lf] - id: check-case-conflict - # - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks - # rev: v2.8.0 - # hooks: - # # Moves entire sections, I don't like that >:( - # # - id: pretty-format-toml - # # args: [--autofix] - # # I don't like it, it really messes up a lot a stuff - # # - id: pretty-format-yaml - # # args: [--autofix, --indent, "2", --offset, "0", --preserve-quotes, --line-width, "100"], - # - id: pretty-format-ini - # args: [--autofix] + - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks + rev: v2.13.0 + hooks: + # Moves entire sections, I don't like that >:( + # - id: pretty-format-toml + # args: [--autofix] + - id: pretty-format-yaml + args: [--autofix, --indent, "2", --offset, "2", --preserve-quotes, --line-width, "100"] + - id: pretty-format-ini + args: [--autofix] - repo: local # https://github.com/dprint/dprint hooks: - id: dprint @@ -27,15 +26,15 @@ repos: entry: dprint fmt language: node types: [text] - additional_dependencies: ["dprint@~0.45.1"] + additional_dependencies: ["dprint@~0.46.0"] pass_filenames: false # https://github.com/adamchainz/pre-commit-dprint/issues/3#issuecomment-1483410008 - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.4.2" # Must match requirements-dev.txt + rev: "v0.4.5" # Must match requirements-dev.txt hooks: - id: ruff args: [--fix] - repo: https://github.com/hhatto/autopep8 - rev: "v2.1.0" # Must match requirements-dev.txt + rev: "v2.1.1" # Must match requirements-dev.txt hooks: - id: autopep8 - repo: https://github.com/asottile/add-trailing-comma @@ -43,7 +42,7 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/RobertCraigie/pyright-python - rev: "v1.1.360" + rev: "v1.1.364" hooks: - id: pyright diff --git a/.vscode/settings.json b/.vscode/settings.json index 2be908b..e965622 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -34,6 +34,7 @@ "[yaml]": { "editor.defaultFormatter": "redhat.vscode-yaml" }, + "yaml.format.printWidth": 100, "[markdown]": { "editor.defaultFormatter": "DavidAnson.vscode-markdownlint" }, diff --git a/pyproject.toml b/pyproject.toml index d963930..56c9427 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,6 +82,8 @@ ignore = [ # Specific to this project ### "CPY001", # missing-copyright-notice: Assume license from root + # This is a relatively small, low contributors project. Git blame suffice. + "TD002", # missing-todo-author ### FIXME/TODO: I'd normally set them as temporarily warnings, but no warnings in Ruff yet: ### https://github.com/astral-sh/ruff/issues/1256 & https://github.com/astral-sh/ruff/issues/1774): From e91603ab3e5810f32f1239ad416ced64876df50a Mon Sep 17 00:00:00 2001 From: Avasam Date: Sat, 25 May 2024 13:18:51 -0400 Subject: [PATCH 3/7] typo --- Dolphin scripts/Entrance Randomizer/lib/entrance_rando.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dolphin scripts/Entrance Randomizer/lib/entrance_rando.py b/Dolphin scripts/Entrance Randomizer/lib/entrance_rando.py index edd0c80..a110608 100644 --- a/Dolphin scripts/Entrance Randomizer/lib/entrance_rando.py +++ b/Dolphin scripts/Entrance Randomizer/lib/entrance_rando.py @@ -29,7 +29,7 @@ class Transition(NamedTuple): # -> int: pyright doesn't narrow `int | False` to just `int` after truthy check def highjack_transition_rando() -> tuple[int, int] | Literal[False]: - # Early return, faster check. Detect teh start of a transition + # Early return, faster check. Detect the start of a transition if state.current_area_old == state.current_area_new: return False From d9e5f661e7498efd128cd88290f071703353b120 Mon Sep 17 00:00:00 2001 From: Avasam Date: Sat, 25 May 2024 13:20:23 -0400 Subject: [PATCH 4/7] redundant return --- Dolphin scripts/Entrance Randomizer/lib/entrance_rando.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Dolphin scripts/Entrance Randomizer/lib/entrance_rando.py b/Dolphin scripts/Entrance Randomizer/lib/entrance_rando.py index a110608..a663e91 100644 --- a/Dolphin scripts/Entrance Randomizer/lib/entrance_rando.py +++ b/Dolphin scripts/Entrance Randomizer/lib/entrance_rando.py @@ -3,7 +3,7 @@ import random from collections.abc import Iterable from itertools import starmap -from typing import Literal, NamedTuple +from typing import NamedTuple import CONFIGS from lib.constants import * # noqa: F403 @@ -27,8 +27,7 @@ class Transition(NamedTuple): to: int -# -> int: pyright doesn't narrow `int | False` to just `int` after truthy check -def highjack_transition_rando() -> tuple[int, int] | Literal[False]: +def highjack_transition_rando(): # Early return, faster check. Detect the start of a transition if state.current_area_old == state.current_area_new: return False From c8217216199ff5cd1da45d4b19c756794c4fb2f5 Mon Sep 17 00:00:00 2001 From: Avasam Date: Sat, 25 May 2024 13:22:19 -0400 Subject: [PATCH 5/7] Forgot to update requirements --- Dolphin scripts/requirements-dev.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dolphin scripts/requirements-dev.txt b/Dolphin scripts/requirements-dev.txt index 4fef1a0..ec6e9d8 100644 --- a/Dolphin scripts/requirements-dev.txt +++ b/Dolphin scripts/requirements-dev.txt @@ -1,4 +1,4 @@ add-trailing-comma>=3.1.0 # Must match .pre-commit-config.yaml -autopep8>=2.1.0 # Must match .pre-commit-config.yaml +autopep8>=2.1.1 # Must match .pre-commit-config.yaml pre-commit -ruff>=0.4.2 # Must match .pre-commit-config.yaml +ruff>=0.4.5 # Must match .pre-commit-config.yaml From cd950192e2998ddb51242604d7fa4d139655c865 Mon Sep 17 00:00:00 2001 From: Avasam Date: Sun, 26 May 2024 11:07:02 -0400 Subject: [PATCH 6/7] Fix altar of ages not being accessible and two non-existant transitions in list --- Dolphin scripts/Entrance Randomizer/__main__.py | 10 +++++++--- .../Entrance Randomizer/lib/entrance_rando.py | 9 +++++++-- Various technical notes/transition_infos.json | 11 +++-------- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/Dolphin scripts/Entrance Randomizer/__main__.py b/Dolphin scripts/Entrance Randomizer/__main__.py index 12d5982..1bf2699 100644 --- a/Dolphin scripts/Entrance Randomizer/__main__.py +++ b/Dolphin scripts/Entrance Randomizer/__main__.py @@ -84,12 +84,16 @@ async def main_loop(): if highjack_transition(0x0, LevelCRC.JAGUAR, starting_area): return - # Standardize the Altar of Ages exit - if highjack_transition(LevelCRC.ALTAR_OF_AGES, None, LevelCRC. MYSTERIOUS_TEMPLE): + # Standardize the Altar of Ages exit to remove the Altar -> BBCamp transition + if highjack_transition( + LevelCRC.ALTAR_OF_AGES, + LevelCRC.BITTENBINDERS_CAMP, + LevelCRC.MYSTERIOUS_TEMPLE, + ): + state.current_area_new = LevelCRC.MYSTERIOUS_TEMPLE # Even if the cutscene isn't actually watched. # Just leaving the Altar is good enough for the rando. state.visited_altar_of_ages = True - state.current_area_new = LevelCRC.MYSTERIOUS_TEMPLE # Standardize the Viracocha Monoliths cutscene if highjack_transition( diff --git a/Dolphin scripts/Entrance Randomizer/lib/entrance_rando.py b/Dolphin scripts/Entrance Randomizer/lib/entrance_rando.py index a663e91..deaedb2 100644 --- a/Dolphin scripts/Entrance Randomizer/lib/entrance_rando.py +++ b/Dolphin scripts/Entrance Randomizer/lib/entrance_rando.py @@ -47,8 +47,8 @@ def highjack_transition_rando(): "highjack_transition_rando |", f"From: {hex(state.current_area_old)},", f"To: {hex(state.current_area_new)}.", - f"Redirecting to: {hex(redirect.from_)}", - f"({hex(redirect.to)} entrance)\n", + f"Redirecting to: {hex(redirect.to)}", + f"({hex(redirect.from_)} entrance)\n", ) memory.write_u32(follow_pointer_path(ADDRESSES.prev_area), redirect.from_) memory.write_u32(ADDRESSES.current_area, redirect.to) @@ -60,6 +60,11 @@ def highjack_transition(from_: int | None, to: int | None, redirect: int): 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 |", diff --git a/Various technical notes/transition_infos.json b/Various technical notes/transition_infos.json index 2a5cac5..c161c0b 100644 --- a/Various technical notes/transition_infos.json +++ b/Various technical notes/transition_infos.json @@ -357,8 +357,8 @@ "requires": [] }, { - "area_id": "0x70EBFCA3", - "area_name": "Altar of Huitaca", + "area_id": "0x3292B6C9", + "area_name": "The Great Tree", "requires": [] } ] @@ -513,7 +513,7 @@ }, { "area_id": "0xE6B9138A", - "area_name": "Twin Outposts (2 exits)", + "area_name": "Twin Outposts", "requires": [] }, { @@ -828,11 +828,6 @@ "area_name": "Apu Illapu Shrine (Spinjas)", "default_entrance": "0x1B8833D3", "exits": [ - { - "area_id": "0x1B8833D3", - "area_name": "Mountain Sled Run", - "requires": [] - }, { "area_id": "0x62548B77", "area_name": "White Valley", From 7935db1e13efe30dc937d83f98f405640fda25a6 Mon Sep 17 00:00:00 2001 From: Avasam Date: Sun, 26 May 2024 16:50:36 -0400 Subject: [PATCH 7/7] Add unrandomized log and indicate that typings is vendored --- Dolphin scripts/Entrance Randomizer/lib/utils.py | 8 ++++++++ Dolphin scripts/typings/README.md | 1 + 2 files changed, 9 insertions(+) create mode 100644 Dolphin scripts/typings/README.md diff --git a/Dolphin scripts/Entrance Randomizer/lib/utils.py b/Dolphin scripts/Entrance Randomizer/lib/utils.py index c06cf98..148c897 100644 --- a/Dolphin scripts/Entrance Randomizer/lib/utils.py +++ b/Dolphin scripts/Entrance Randomizer/lib/utils.py @@ -58,6 +58,14 @@ def dump_spoiler_logs( + f"({TRANSITION_INFOS_DICT[redirect[0]].name} entrance)\n" ) + unrandomized_transitions = ALL_POSSIBLE_TRANSITIONS - transitions_map.keys() + spoiler_logs += "\nUnrandomized transitions:\n" + for transition in unrandomized_transitions: + spoiler_logs += ( + f"From: {TRANSITION_INFOS_DICT[transition[0]].name}, " + + f"To: {TRANSITION_INFOS_DICT[transition[1]].name}.\n" + ) + # 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() diff --git a/Dolphin scripts/typings/README.md b/Dolphin scripts/typings/README.md new file mode 100644 index 0000000..752fd58 --- /dev/null +++ b/Dolphin scripts/typings/README.md @@ -0,0 +1 @@ +Vendor of