Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scan for previous area value across a range of addresses #51

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
53 changes: 43 additions & 10 deletions Dolphin scripts/Entrance Randomizer/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@
from lib.graph_creation import create_graphml
from lib.shaman_shop import patch_shaman_shop, randomize_shaman_shop
from lib.utils import (
PreviousArea,
draw_text,
dump_spoiler_logs,
follow_pointer_path,
highjack_transition,
prevent_item_softlock,
prevent_transition_softlocks,
Expand Down Expand Up @@ -62,19 +62,35 @@ async def main_loop():
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)
if current_area:
current_area_name = current_area.name
elif state.current_area_new == LevelCRC.MAIN_MENU:
current_area_name = "Main Menu"
else:
current_area_name = ""
previous_area_id = PreviousArea.get()
draw_text(f"Rando version: {__version__}")
draw_text(f"Seed: {seed_string}")
draw_text(patch_shaman_shop())
draw_text(
f"Current area: {hex(state.current_area_new).upper()} "
+ (f"({current_area.name})" if current_area else ""),
+ (f"({current_area_name})" if current_area_name else ""),
)
draw_text(
f"From entrance: {hex(previous_area_id).upper()} "
+ (f"({previous_area.name})" if previous_area else ""),
f"From entrance addr: {hex(PreviousArea._previous_area_address).upper()} ", # noqa: SLF001 # pyright: ignore[reportPrivateUsage]
)
if previous_area_id != -1 or state.current_area_new in {starting_area, LevelCRC.MAIN_MENU}:
previous_area_name = PreviousArea.get_name(previous_area_id)
draw_text(
f"From entrance: {hex(previous_area_id).upper()} "
+ (f"({previous_area_name})" if previous_area_name else ""),
)
else:
draw_text(
"From entrance: NOT FOUND!!!\n"
+ "This is either an entrance using a special ID we're not aware of, or a bug!\n"
+ "PLEASE REPORT THIS TO RANDO DEVS (unless you just loaded a state)\n",
)

# Always re-enable Item Swap
if memory.read_u32(ADDRESSES.item_swap) == 1:
Expand All @@ -86,27 +102,44 @@ async def main_loop():

# Skip both Jaguar fights if configured
if CONFIGS.SKIP_JAGUAR:
if highjack_transition(LevelCRC.MAIN_MENU, LevelCRC.JAGUAR, starting_area):
if highjack_transition(
LevelCRC.MAIN_MENU,
LevelCRC.JAGUAR,
0,
starting_area,
):
return
if highjack_transition(LevelCRC.GATES_OF_EL_DORADO, LevelCRC.JAGUAR, LevelCRC.PUSCA):
if highjack_transition(
LevelCRC.GATES_OF_EL_DORADO,
LevelCRC.JAGUAR,
LevelCRC.JAGUAR,
LevelCRC.PUSCA,
):
return

# Standardize the Altar of Ages exit to remove the Altar -> BBCamp transition
highjack_transition(
LevelCRC.ALTAR_OF_AGES,
LevelCRC.BITTENBINDERS_CAMP,
LevelCRC.ALTAR_OF_AGES,
LevelCRC.MYSTERIOUS_TEMPLE,
)

# Standardize the Viracocha Monoliths cutscene
# Standardize and autoskip the Viracocha Monoliths cutscene
highjack_transition(
None,
LevelCRC.VIRACOCHA_MONOLITHS_CUTSCENE,
state.current_area_old,
LevelCRC.VIRACOCHA_MONOLITHS,
)

# Standardize St. Claire's Excavation Camp
highjack_transition(None, LevelCRC.ST_CLAIRE_NIGHT, LevelCRC.ST_CLAIRE_DAY)
highjack_transition(
None,
LevelCRC.ST_CLAIRE_NIGHT,
LevelCRC.FLOODED_COURTYARD,
LevelCRC.ST_CLAIRE_DAY,
)

# TODO: Skip swim levels (3)

Expand Down
14 changes: 7 additions & 7 deletions Dolphin scripts/Entrance Randomizer/lib/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
@dataclass(frozen=True)
class Addresses:
version_string: str
prev_area: tuple[int, ...]
previous_area_blocks_ptr: int
current_area: int
area_load_state: int
player_ptr: int
Expand Down Expand Up @@ -97,7 +97,7 @@ class PlayerPtrOffset(IntEnum):
"GPH": {
"D": Addresses(
version_string="GC DE 0-00",
prev_area=(0x80747648,),
previous_area_blocks_ptr=TODO,
current_area=0x80417F50,
area_load_state=TODO,
player_ptr=TODO,
Expand All @@ -107,7 +107,7 @@ class PlayerPtrOffset(IntEnum):
),
"E": Addresses(
version_string="GC US 0-00",
prev_area=(0x8072B648,),
previous_area_blocks_ptr=0x80425788,
current_area=0x8041BEB4,
area_load_state=0x8041BEC8,
player_ptr=0x8041BE4C,
Expand All @@ -117,7 +117,7 @@ class PlayerPtrOffset(IntEnum):
),
"F": Addresses(
version_string="GC FR 0-00",
prev_area=(0x80747648,),
previous_area_blocks_ptr=TODO,
current_area=0x80417F30,
area_load_state=TODO,
player_ptr=0x80417EC8,
Expand All @@ -127,7 +127,7 @@ class PlayerPtrOffset(IntEnum):
),
"P": Addresses(
version_string="GC EU 0-00",
prev_area=(0x80747648,),
previous_area_blocks_ptr=TODO,
current_area=0x80417F10,
area_load_state=TODO,
player_ptr=TODO,
Expand All @@ -139,7 +139,7 @@ class PlayerPtrOffset(IntEnum):
"RPF": {
"E": Addresses(
version_string="Wii US 0-00",
prev_area=(0x804542DC, 0x8),
previous_area_blocks_ptr=0x804542DC,
current_area=0x80448D04,
area_load_state=TODO,
player_ptr=TODO,
Expand All @@ -149,7 +149,7 @@ class PlayerPtrOffset(IntEnum):
),
"P": Addresses(
version_string="Wii EU 0-00",
prev_area=(0x804546DC, 0x18),
previous_area_blocks_ptr=0x804546DC,
current_area=0x80449104,
area_load_state=TODO,
player_ptr=TODO,
Expand Down
11 changes: 3 additions & 8 deletions Dolphin scripts/Entrance Randomizer/lib/entrance_rando.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,11 @@
from copy import copy
from enum import IntEnum
from itertools import starmap
from typing import NamedTuple

import CONFIGS
from lib.constants import * # noqa: F403
from lib.transition_infos import Area
from lib.utils import follow_pointer_path, state


class Transition(NamedTuple):
from_: int
to: int
from lib.utils import PreviousArea, Transition, state


class Choice(IntEnum):
Expand Down Expand Up @@ -157,6 +151,7 @@ class Choice(IntEnum):


def highjack_transition_rando():
return None
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reminder to revert this line before merging

Suggested change
return None

# Early return, faster check. Detect the start of a transition
if state.current_area_old == state.current_area_new:
return False
Expand Down Expand Up @@ -191,7 +186,7 @@ def highjack_transition_rando():
f"Redirecting to: {hex(redirect.to)}",
f"({hex(redirect.from_)} entrance)\n",
)
memory.write_u32(follow_pointer_path(ADDRESSES.prev_area), redirect.from_)
PreviousArea.set(redirect)
memory.write_u32(ADDRESSES.current_area, redirect.to)
state.current_area_new = redirect.to
return redirect
Expand Down
155 changes: 142 additions & 13 deletions Dolphin scripts/Entrance Randomizer/lib/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from collections.abc import Mapping, Sequence
from pathlib import Path
from typing import ClassVar
from typing import ClassVar, NamedTuple

from dolphin import gui # pyright: ignore[reportMissingModuleSource]
from lib.constants import * # noqa: F403
Expand Down Expand Up @@ -58,27 +58,36 @@ def follow_pointer_path(ppath: Sequence[int]):


def highjack_transition(
from_: int | None,
to: int | None,
redirect: int,
original_from: int | None,
original_to: int | None,
redirect_from: int,
redirect_to: int,
):
if from_ is None:
from_ = state.current_area_old
if to is None:
to = state.current_area_new
"""
Highjack a transition to transport the player elsewhere.

`original_from=None` means that any entrance will match.
`original_from=None` means that any exit will match.
"""
if original_from is None:
original_from = state.current_area_old
if original_to is None:
original_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:
if original_from == state.current_area_old and original_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)}",
f"Redirecting to: {hex(redirect_to)} ",
f"from {hex(redirect_from)} entrance",
)
memory.write_u32(ADDRESSES.current_area, redirect)
PreviousArea.set(Transition(redirect_from, redirect_to))
memory.write_u32(ADDRESSES.current_area, redirect_to)
return True
return False

Expand All @@ -94,8 +103,6 @@ def prevent_transition_softlocks():
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)


Expand Down Expand Up @@ -182,6 +189,128 @@ def prevent_item_softlock():
return


class Transition(NamedTuple):
from_: int
to: int


class PreviousArea:
_previous_area_address = 0
# TODO: This information is going to be extremely important for the transition rando,
# we're gonna have to update out data structure to be able to make use of this.
CORRECTED_TRANSITION_FROM: ClassVar[dict[Transition, int]] = {
Transition(from_=LevelCRC.APU_ILLAPU_SHRINE, to=LevelCRC.WHITE_VALLEY): 0xF3ACDE92,
# Probably a leftover from when plane crash + cockpit was a single map
Transition(from_=LevelCRC.CRASH_SITE, to=LevelCRC.JUNGLE_CANYON): 0xD33711E2,
# Scorpion/Explorer entrance
# Transition(from_=LevelCRC.NATIVE_JUNGLE, to=LevelCRC.FLOODED_COURTYARD): 0x83A6748F,
# HACK for CORRECTED_PREV_ID
Transition(from_=LevelCRC.NATIVE_JUNGLE, to=0): 0x83A6748F,
# Dark cave entrance
Transition(from_=LevelCRC.NATIVE_JUNGLE, to=LevelCRC.FLOODED_COURTYARD): 0x1AAF2535,
# Dark cave entrance
Transition(from_=LevelCRC.FLOODED_COURTYARD, to=LevelCRC.NATIVE_JUNGLE): 0x402D3708,
# Jungle Outpost well
# Transition(from_=LevelCRC.TWIN_OUTPOSTS, to=LevelCRC.TWIN_OUTPOSTS_UNDERWATER): 0x9D1A6D4A,
# HACK for CORRECTED_PREV_ID
Transition(from_=LevelCRC.TWIN_OUTPOSTS, to=0): 0x9D1A6D4A,
# Burning Outpost well
Transition(from_=LevelCRC.TWIN_OUTPOSTS, to=LevelCRC.TWIN_OUTPOSTS_UNDERWATER): 0x7C65128A,
# Jungle Outpost side
# Transition(from_=LevelCRC.TWIN_OUTPOSTS_UNDERWATER, to=LevelCRC.TWIN_OUTPOSTS): 0x00D15464,
Avasam marked this conversation as resolved.
Show resolved Hide resolved
# HACK for CORRECTED_PREV_ID
Transition(from_=LevelCRC.TWIN_OUTPOSTS_UNDERWATER, to=0): 0x00D15464,
# Burning Outpost side
Transition(from_=LevelCRC.TWIN_OUTPOSTS_UNDERWATER, to=LevelCRC.TWIN_OUTPOSTS): 0xE1AE2BA4,
# Native Village uses its own ID to spawn at the Native Games gate
Transition(from_=LevelCRC.KABOOM, to=LevelCRC.NATIVE_VILLAGE): LevelCRC.NATIVE_VILLAGE,
Transition(from_=LevelCRC.PICKAXE_RACE, to=LevelCRC.NATIVE_VILLAGE): LevelCRC.NATIVE_VILLAGE, # noqa: E501
Transition(from_=LevelCRC.RAFT_BOWLING, to=LevelCRC.NATIVE_VILLAGE): LevelCRC.NATIVE_VILLAGE, # noqa: E501
Transition(from_=LevelCRC.TUCO_SHOOT, to=LevelCRC.NATIVE_VILLAGE): LevelCRC.NATIVE_VILLAGE,
Transition(from_=LevelCRC.WHACK_A_TUCO, to=LevelCRC.NATIVE_VILLAGE): LevelCRC.NATIVE_VILLAGE, # noqa: E501
}
"""
Some entrances are mapped to a different ID than the level the player actually comes from.
This maps the transition to the fake ID.
"""

CORRECTED_PREV_ID: ClassVar[dict[int, int]] = {
area_id: transition.from_
for transition, area_id
in CORRECTED_TRANSITION_FROM.items()
}
"""
Some entrances are mapped to a different ID than the level the player actually comes from.
This maps the fake ID to the real ID.
"""
__ALL_LEVELS = (
ALL_TRANSITION_AREAS
| set(CORRECTED_PREV_ID)
| set(LevelCRC)
- {LevelCRC.MAIN_MENU}
)

@classmethod
def get(cls) -> int | LevelCRC:
return cls.__update_previous_area_address()

@classmethod
def set(cls, value: Transition):
"""
Sets the "previous area id" in memory.

`value` is a `Transition` because this method tries to the fake ID
to spawn the player on the proper entrance.

If no mapping is found (ie: either value is incorrect),
then `value.from_` is used directly,
which at worst causes use the default entrance.
"""
cls.__update_previous_area_address()
memory.write_u32(
cls._previous_area_address,
cls.CORRECTED_TRANSITION_FROM.get(value, value.from_),
)

@classmethod
def get_name(cls, area_id: int):
"""
Gets the name of an area.

If a "fake ID" is passed, it'll be mapped to the real "from level".
"""
area = TRANSITION_INFOS_DICT.get(cls.CORRECTED_PREV_ID.get(area_id, area_id))
return area.name if area else ""

@classmethod
def __update_previous_area_address(cls):
# First check that the current value is a sensible known level
previous_area = memory.read_u32(cls._previous_area_address)
if previous_area in cls.__ALL_LEVELS:
return previous_area

# If not, start iterating over 16-bit blocks,
# where the first half is consistent (a pointer?)
# and the second half is maybe the value we're looking for
block_address = memory.read_u32(ADDRESSES.previous_area_blocks_ptr)
prefix = memory.read_u32(block_address)
for _ in range(32): # Limited iteration as extra safety for infinite loops
# Check if the current block is a valid level id
block_address += 8
previous_area = memory.read_u32(block_address)
if previous_area in cls.__ALL_LEVELS:
# Valid id. Assume this is our address
cls._previous_area_address = block_address
return previous_area

# Check if the next block is still part of the dynamic data
block_address += 8
next_prefix = memory.read_u32(block_address)
if next_prefix != prefix:
return -1 # We went the entire dynamic data structure w/o finding a valid ID !
return -1


def dump_spoiler_logs(
starting_area_name: str,
transitions_map: Mapping[tuple[int, int], tuple[int, int]],
Expand Down
Loading