Files
Archipelago/worlds/alttp/EnemyShuffle.py
T

1708 lines
68 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from typing import Optional, TYPE_CHECKING
from Utils import snes_to_pc
from .EnemizerPatches import apply_enemizer_base_patch
from .Rom import LocalRom, get_base_rom_path
from .enemizer_data.dungeon_sprite_addresses import DUNGEON_SPRITE_ADDRESSES, KEYED_SPRITE_ID_ADDRESSES
from .enemizer_data.enemy_room_metadata import (
BOSS_ROOM_IDS,
DONT_RANDOMIZE_ROOM_IDS,
NO_SPECIAL_ENEMIES_STANDARD_ROOM_IDS,
ROOM_GROUP_REQUIREMENTS,
SHUTTER_ROOM_IDS,
WATER_ROOM_IDS,
)
from .enemizer_data.enemy_sprite_requirements import ENEMY_SPRITE_REQUIREMENTS
from .enemizer_data.overworld_enemy_metadata import (
AREA_IDS,
DO_NOT_RANDOMIZE_AREA_IDS,
FORCED_GROUP_REQUIREMENTS,
)
from .enemizer_data.symbols import ENEMIZER_SYMBOLS
if TYPE_CHECKING:
from . import ALTTPWorld
from .Rom import LocalRom
DUNGEON_HEADER_POINTER_TABLE_BASE = 0x271E2
DUNGEON_SPRITE_POINTER_TABLE_BASE = 0x4D62E
OVERWORLD_SPRITE_POINTER_TABLE_BASE = 0x4C901
OVERWORLD_AREA_GRAPHICS_BLOCK_BASE = 0x7A81
ROOM_HEADER_BANK_LOCATION = 0xB5E7
SPRITE_GROUP_BASE_ADDRESS = 0x5B97
TOTAL_SPRITE_GROUPS = 144
TOTAL_DUNGEON_ROOMS = 0x128
SPRITE_OVERLORD_MASK = 0xE0
SPRITE_OVERLORD_REMOVE_MASK = 0x1F
SPRITE_SUBTYPE_BYTE_0_MASK = 0x60
KEY_SPRITE_ID = 0xE4
BIG_KEY_SPRITE_ID = 0xE5
WALLMASTER_SPRITE_ID = 0x90
STAL_SPRITE_ID = 0xD3
FLOPPING_FISH_SPRITE_ID = 0xD2
OW_FALLING_ROCKS_SPRITE_ID = 0xF4
OW_WALLMASTER_TO_HOULIHAN_SPRITE_ID = 0xFB
WATER_TEKTITE_SPRITE_ID = 0x81
POTENTIAL_SUBGROUP_0 = (22, 31, 47, 14)
POTENTIAL_SUBGROUP_1 = (44, 30, 32)
POTENTIAL_SUBGROUP_2 = (12, 18, 23, 24, 28, 46, 34, 35, 39, 40, 38, 41, 36, 37, 42)
POTENTIAL_SUBGROUP_3 = (17, 16, 27, 20, 82, 83)
GUARD_SUBGROUP_1_DUNGEON_GROUP_IDS = frozenset((1, 2, 3, 4))
SELECTED_BOSS_GROUP_REQUIREMENTS = {
"Armos": (9, 83),
"Lanmola": (11, 84),
"Moldorm": (12, 9),
"Arrghus": (20, 140),
"Helmasaur": (21, 146),
"Kholdstare": (22, 162),
"Vitreous": (22, 189),
"Trinexx": (23, 203),
"Mothula": (26, 136),
"Blind": (32, 206),
}
@dataclass(frozen=True)
class RoomGroupRequirement:
group_id: Optional[int]
subgroup_0: Optional[int]
subgroup_1: Optional[int]
subgroup_2: Optional[int]
subgroup_3: Optional[int]
rooms: tuple[int, ...]
@dataclass(frozen=True)
class OverworldGroupRequirement:
group_id: Optional[int]
subgroup_0: Optional[int]
subgroup_1: Optional[int]
subgroup_2: Optional[int]
subgroup_3: Optional[int]
areas: tuple[int, ...]
@dataclass
class DungeonSpriteGroup:
group_id: int
dungeon_group_id: int
subgroup_0: int
subgroup_1: int
subgroup_2: int
subgroup_3: int
preserve_subgroup_0: bool = False
preserve_subgroup_1: bool = False
preserve_subgroup_2: bool = False
preserve_subgroup_3: bool = False
@dataclass(frozen=True)
class EnemySpriteRequirement:
sprite_name: str
sprite_id: int
boss: bool
overlord: bool
do_not_randomize: bool
killable: bool
npc: bool
never_use_dungeon: bool
never_use_overworld: bool
cannot_have_key: bool
is_object: bool
absorbable: bool
is_water_sprite: bool
is_enemy_sprite: bool
group_ids: tuple[int, ...]
subgroup_0: tuple[int, ...]
subgroup_1: tuple[int, ...]
subgroup_2: tuple[int, ...]
subgroup_3: tuple[int, ...]
parameters: Optional[int]
special_glitched: bool
excluded_rooms: tuple[int, ...]
dont_randomize_rooms: tuple[int, ...]
spawnable_rooms: tuple[int, ...]
@dataclass(frozen=True)
class DungeonEnemySprite:
address: int
byte_0: int
byte_1: int
sprite_id: int
is_overlord: bool
has_key: bool
@property
def is_on_bg2(self) -> bool:
return bool(self.byte_0 & 0x80)
@property
def hm_param(self) -> int:
return ((self.byte_0 & 0x60) >> 2) | ((self.byte_1 & 0xE0) >> 5)
@property
def y_coord_pixels(self) -> int:
return (self.byte_0 & 0x1F) * 16
@property
def x_coord_pixels(self) -> int:
return (self.byte_1 & 0x1F) * 16
@dataclass(frozen=True)
class DungeonEnemyRoom:
room_id: int
room_header_address: int
sprite_table_address: int
graphics_block_id: int
tag_1: int
tag_2: int
sort_sprites_value: int
sprites: tuple[DungeonEnemySprite, ...]
required_group_id: Optional[int]
required_subgroup_0: tuple[int, ...]
required_subgroup_1: tuple[int, ...]
required_subgroup_2: tuple[int, ...]
required_subgroup_3: tuple[int, ...]
is_shutter_room: bool
is_water_room: bool
do_not_randomize: bool
no_special_enemies_standard: bool
@dataclass(frozen=True)
class RandomizedDungeonEnemySprite:
address: int
byte_0: int
byte_1: int
original_sprite_id: int
sprite_id: int
is_overlord: bool
has_key: bool
@dataclass(frozen=True)
class RandomizedDungeonEnemyRoom:
room_id: int
room_header_address: int
sprite_table_address: int
original_graphics_block_id: int
graphics_block_id: int
tag_1: int
tag_2: int
sort_sprites_value: int
sprites: tuple[RandomizedDungeonEnemySprite, ...]
skipped_randomization: bool
@dataclass(frozen=True)
class OverworldEnemySprite:
address: int
y_coord: int
x_coord: int
sprite_id: int
@dataclass(frozen=True)
class OverworldEnemyArea:
area_id: int
sprite_table_address: int
graphics_block_address: int
graphics_block_id: int
bush_sprite_id: int
sprites: tuple[OverworldEnemySprite, ...]
do_not_randomize: bool
@dataclass(frozen=True)
class RandomizedOverworldEnemySprite:
address: int
y_coord: int
x_coord: int
original_sprite_id: int
sprite_id: int
@dataclass(frozen=True)
class RandomizedOverworldEnemyArea:
area_id: int
sprite_table_address: int
graphics_block_address: int
original_graphics_block_id: int
graphics_block_id: int
original_bush_sprite_id: int
bush_sprite_id: int
sprites: tuple[RandomizedOverworldEnemySprite, ...]
skipped_randomization: bool
@dataclass(frozen=True)
class EnemyShuffleState:
dungeon_rooms: dict[int, DungeonEnemyRoom]
overworld_areas: dict[int, OverworldEnemyArea]
sprite_groups: dict[int, DungeonSpriteGroup]
sprite_requirements: tuple[EnemySpriteRequirement, ...]
room_group_requirements: tuple[RoomGroupRequirement, ...]
overworld_group_requirements: tuple[OverworldGroupRequirement, ...]
shutter_room_ids: frozenset[int]
water_room_ids: frozenset[int]
dont_randomize_room_ids: frozenset[int]
no_special_enemies_standard_room_ids: frozenset[int]
boss_room_ids: frozenset[int]
dont_randomize_overworld_area_ids: frozenset[int]
randomized_dungeon_rooms: dict[int, RandomizedDungeonEnemyRoom]
randomized_overworld_areas: dict[int, RandomizedOverworldEnemyArea]
def generate_enemy_shuffle_state(world: "ALTTPWorld") -> EnemyShuffleState:
rom_bytes = _get_base_patched_rom_bytes()
moved_header_bank = _get_enemizer_symbol("moved_room_header_bank_value_address")
bush_spawn_table_address = _get_enemizer_symbol("sprite_bush_spawn_table_overworld")
metadata = _load_enemy_room_metadata()
overworld_metadata = _load_overworld_enemy_metadata()
sprite_requirements = _load_enemy_sprite_requirements()
dungeon_rooms = {
room.room_id: room
for room in _read_dungeon_rooms(rom_bytes, moved_header_bank, metadata)
}
overworld_areas = {
area.area_id: area
for area in _read_overworld_areas(rom_bytes, bush_spawn_table_address, overworld_metadata)
}
sprite_groups = {
group.group_id: group
for group in _read_sprite_groups(rom_bytes)
}
_setup_required_dungeon_groups(world, sprite_groups, metadata["room_requirements"])
_apply_selected_boss_group_requirements(world, sprite_groups, sprite_requirements)
_randomize_dungeon_groups(world, sprite_groups)
randomized_dungeon_rooms = _randomize_dungeon_rooms(
world,
dungeon_rooms,
sprite_groups,
sprite_requirements,
)
_setup_required_overworld_groups(sprite_groups, overworld_metadata["forced_group_requirements"])
_randomize_overworld_groups(world, sprite_groups)
randomized_overworld_areas = _randomize_overworld_areas(
world,
overworld_areas,
sprite_groups,
sprite_requirements,
overworld_metadata["forced_group_requirements"],
)
state = EnemyShuffleState(
dungeon_rooms=dungeon_rooms,
overworld_areas=overworld_areas,
sprite_groups=sprite_groups,
sprite_requirements=sprite_requirements,
room_group_requirements=metadata["room_requirements"],
overworld_group_requirements=overworld_metadata["forced_group_requirements"],
shutter_room_ids=metadata["shutter_room_ids"],
water_room_ids=metadata["water_room_ids"],
dont_randomize_room_ids=metadata["dont_randomize_room_ids"],
no_special_enemies_standard_room_ids=metadata["no_special_enemies_standard_room_ids"],
boss_room_ids=metadata["boss_room_ids"],
dont_randomize_overworld_area_ids=overworld_metadata["do_not_randomize_area_ids"],
randomized_dungeon_rooms=randomized_dungeon_rooms,
randomized_overworld_areas=randomized_overworld_areas,
)
validate_enemy_shuffle_state(state, is_standard_mode=world.options.mode == "standard")
return state
def _get_base_patched_rom_bytes() -> bytes:
patched_rom_bytes = getattr(_get_base_patched_rom_bytes, "patched_rom_bytes", None)
if patched_rom_bytes is None:
patched_rom = LocalRom(get_base_rom_path())
apply_enemizer_base_patch(patched_rom)
patched_rom_bytes = bytes(patched_rom.buffer)
_get_base_patched_rom_bytes.patched_rom_bytes = patched_rom_bytes
return patched_rom_bytes
def _read_dungeon_rooms(rom_bytes: bytes, moved_header_bank_address: int, metadata: dict[str, object]) -> list[DungeonEnemyRoom]:
rooms: list[DungeonEnemyRoom] = []
room_header_bank = _get_room_header_bank(rom_bytes, moved_header_bank_address)
dungeon_sprite_metadata = _load_dungeon_sprite_metadata()
shutter_room_ids = metadata["shutter_room_ids"]
water_room_ids = metadata["water_room_ids"]
dont_randomize_room_ids = metadata["dont_randomize_room_ids"]
no_special_enemies_standard_room_ids = metadata["no_special_enemies_standard_room_ids"]
room_requirements = metadata["room_requirements"]
for room_id in range(TOTAL_DUNGEON_ROOMS):
room_header_address = _read_room_header_address(rom_bytes, room_id, room_header_bank)
sprite_table_address = _read_room_sprite_table_address(rom_bytes, room_id)
merged_requirement = _merge_room_requirements(room_id, room_requirements)
rooms.append(
DungeonEnemyRoom(
room_id=room_id,
room_header_address=room_header_address,
sprite_table_address=sprite_table_address,
graphics_block_id=rom_bytes[room_header_address + 3],
tag_1=rom_bytes[room_header_address + 5],
tag_2=rom_bytes[room_header_address + 6],
sort_sprites_value=rom_bytes[sprite_table_address],
sprites=_read_room_sprites(rom_bytes, room_id, sprite_table_address, dungeon_sprite_metadata),
required_group_id=merged_requirement.group_id,
required_subgroup_0=merged_requirement.subgroup_0,
required_subgroup_1=merged_requirement.subgroup_1,
required_subgroup_2=merged_requirement.subgroup_2,
required_subgroup_3=merged_requirement.subgroup_3,
is_shutter_room=room_id in shutter_room_ids,
is_water_room=room_id in water_room_ids,
do_not_randomize=room_id in dont_randomize_room_ids,
no_special_enemies_standard=room_id in no_special_enemies_standard_room_ids,
)
)
return rooms
def _get_room_header_bank(rom_bytes: bytes, moved_header_bank_address: int) -> int:
if 0 <= moved_header_bank_address < len(rom_bytes):
moved_header_bank = rom_bytes[moved_header_bank_address]
if moved_header_bank:
return moved_header_bank
return rom_bytes[ROOM_HEADER_BANK_LOCATION]
def _read_sprite_groups(rom_bytes: bytes) -> tuple[DungeonSpriteGroup, ...]:
groups = []
for group_id in range(TOTAL_SPRITE_GROUPS):
groups.append(
DungeonSpriteGroup(
group_id=group_id,
dungeon_group_id=group_id - 0x40,
subgroup_0=rom_bytes[SPRITE_GROUP_BASE_ADDRESS + (group_id * 4)],
subgroup_1=rom_bytes[SPRITE_GROUP_BASE_ADDRESS + (group_id * 4) + 1],
subgroup_2=rom_bytes[SPRITE_GROUP_BASE_ADDRESS + (group_id * 4) + 2],
subgroup_3=rom_bytes[SPRITE_GROUP_BASE_ADDRESS + (group_id * 4) + 3],
)
)
return tuple(groups)
def _setup_required_dungeon_groups(
world: "ALTTPWorld",
sprite_groups: dict[int, DungeonSpriteGroup],
room_requirements: tuple[RoomGroupRequirement, ...],
) -> None:
for requirement in room_requirements:
if requirement.group_id is None:
continue
group = sprite_groups.get(requirement.group_id + 0x40)
if group is None:
continue
_apply_required_subgroups(group, requirement)
merged_room_requirements = {
room_id: _merge_room_requirements(room_id, room_requirements)
for requirement in room_requirements
for room_id in requirement.rooms
}
for merged_requirement in merged_room_requirements.values():
if merged_requirement.group_id is not None:
continue
if _has_preserved_group_for_room_requirement(sprite_groups, merged_requirement):
continue
possible_groups = [
group for group in sprite_groups.values()
if 0 < group.dungeon_group_id < 60
and (
not group.preserve_subgroup_0
or not group.preserve_subgroup_1
or not group.preserve_subgroup_2
or not group.preserve_subgroup_3
)
and (not merged_requirement.subgroup_0 or not group.preserve_subgroup_0)
and (not merged_requirement.subgroup_1 or not group.preserve_subgroup_1)
and (not merged_requirement.subgroup_2 or not group.preserve_subgroup_2)
and (not merged_requirement.subgroup_3 or not group.preserve_subgroup_3)
]
if not possible_groups:
continue
selected_group = world.random.choice(possible_groups)
_apply_merged_room_requirement(selected_group, merged_requirement)
def _apply_selected_boss_group_requirements(
world: "ALTTPWorld",
sprite_groups: dict[int, DungeonSpriteGroup],
sprite_requirements: tuple[EnemySpriteRequirement, ...],
) -> None:
requirement_by_sprite_id = {requirement.sprite_id: requirement for requirement in sprite_requirements}
for boss_name in _get_selected_boss_names(world):
boss_group_data = SELECTED_BOSS_GROUP_REQUIREMENTS.get(boss_name)
if boss_group_data is None:
continue
dungeon_group_id, sprite_id = boss_group_data
group = sprite_groups.get(dungeon_group_id + 0x40)
requirement = requirement_by_sprite_id.get(sprite_id)
if group is None or requirement is None:
continue
_apply_selected_boss_requirement(group, requirement)
def _get_selected_boss_names(world: "ALTTPWorld") -> tuple[str, ...]:
dungeons = getattr(world, "dungeons", None)
if not dungeons:
return tuple()
gt_dungeon_name = "Ganons Tower" if world.options.mode != "inverted" else "Inverted Ganons Tower"
gt_dungeon = dungeons.get(gt_dungeon_name)
gt_bosses = getattr(gt_dungeon, "bosses", {}) if gt_dungeon is not None else {}
selected_bosses = [
dungeons["Eastern Palace"].boss.enemizer_name,
dungeons["Desert Palace"].boss.enemizer_name,
dungeons["Tower of Hera"].boss.enemizer_name,
dungeons["Palace of Darkness"].boss.enemizer_name,
dungeons["Swamp Palace"].boss.enemizer_name,
dungeons["Skull Woods"].boss.enemizer_name,
dungeons["Thieves Town"].boss.enemizer_name,
dungeons["Ice Palace"].boss.enemizer_name,
dungeons["Misery Mire"].boss.enemizer_name,
dungeons["Turtle Rock"].boss.enemizer_name,
]
for gt_slot in ("bottom", "middle", "top"):
if gt_slot in gt_bosses:
selected_bosses.append(gt_bosses[gt_slot].enemizer_name)
return tuple(selected_bosses)
def _apply_selected_boss_requirement(group: DungeonSpriteGroup, requirement: EnemySpriteRequirement) -> None:
if requirement.subgroup_0:
group.subgroup_0 = requirement.subgroup_0[0]
group.preserve_subgroup_0 = True
if requirement.subgroup_1:
group.subgroup_1 = requirement.subgroup_1[0]
group.preserve_subgroup_1 = True
if requirement.subgroup_2:
group.subgroup_2 = requirement.subgroup_2[0]
group.preserve_subgroup_2 = True
if requirement.subgroup_3:
group.subgroup_3 = requirement.subgroup_3[0]
group.preserve_subgroup_3 = True
def _setup_required_overworld_groups(
sprite_groups: dict[int, DungeonSpriteGroup],
overworld_group_requirements: tuple[OverworldGroupRequirement, ...],
) -> None:
for requirement in overworld_group_requirements:
if requirement.group_id is None:
continue
group = sprite_groups.get(requirement.group_id)
if group is None:
continue
if (
requirement.subgroup_0 is None
and requirement.subgroup_1 is None
and requirement.subgroup_2 is None
and requirement.subgroup_3 is None
):
group.preserve_subgroup_0 = True
group.preserve_subgroup_1 = True
group.preserve_subgroup_2 = True
group.preserve_subgroup_3 = True
continue
_apply_required_subgroups(group, requirement)
def _apply_required_subgroups(group: DungeonSpriteGroup, requirement: RoomGroupRequirement | OverworldGroupRequirement) -> None:
if requirement.subgroup_0 is not None:
group.subgroup_0 = requirement.subgroup_0
group.preserve_subgroup_0 = True
if requirement.subgroup_1 is not None:
group.subgroup_1 = requirement.subgroup_1
group.preserve_subgroup_1 = True
if requirement.subgroup_2 is not None:
group.subgroup_2 = requirement.subgroup_2
group.preserve_subgroup_2 = True
if requirement.subgroup_3 is not None:
group.subgroup_3 = requirement.subgroup_3
group.preserve_subgroup_3 = True
def _apply_merged_room_requirement(group: DungeonSpriteGroup, requirement: MergedRoomRequirement) -> None:
if requirement.subgroup_0:
group.subgroup_0 = requirement.subgroup_0[0]
group.preserve_subgroup_0 = True
if requirement.subgroup_1:
group.subgroup_1 = requirement.subgroup_1[0]
group.preserve_subgroup_1 = True
if requirement.subgroup_2:
group.subgroup_2 = requirement.subgroup_2[0]
group.preserve_subgroup_2 = True
if requirement.subgroup_3:
group.subgroup_3 = requirement.subgroup_3[0]
group.preserve_subgroup_3 = True
def _has_preserved_group_for_room_requirement(
sprite_groups: dict[int, DungeonSpriteGroup],
requirement: MergedRoomRequirement,
) -> bool:
for group in sprite_groups.values():
if not (0 < group.dungeon_group_id < 60):
continue
if requirement.subgroup_0 and (group.subgroup_0 != requirement.subgroup_0[0] or not group.preserve_subgroup_0):
continue
if requirement.subgroup_1 and (group.subgroup_1 != requirement.subgroup_1[0] or not group.preserve_subgroup_1):
continue
if requirement.subgroup_2 and (group.subgroup_2 != requirement.subgroup_2[0] or not group.preserve_subgroup_2):
continue
if requirement.subgroup_3 and (group.subgroup_3 != requirement.subgroup_3[0] or not group.preserve_subgroup_3):
continue
return True
return False
def _randomize_dungeon_groups(world: "ALTTPWorld", sprite_groups: dict[int, DungeonSpriteGroup]) -> None:
for group in sprite_groups.values():
if not (0 < group.dungeon_group_id < 60):
continue
if not group.preserve_subgroup_1 and group.dungeon_group_id in GUARD_SUBGROUP_1_DUNGEON_GROUP_IDS:
group.preserve_subgroup_1 = True
group.subgroup_1 = world.random.choice((73, 13))
if not group.preserve_subgroup_0:
group.subgroup_0 = world.random.choice(POTENTIAL_SUBGROUP_0)
if not group.preserve_subgroup_1:
group.subgroup_1 = world.random.choice(POTENTIAL_SUBGROUP_1)
if not group.preserve_subgroup_2:
group.subgroup_2 = world.random.choice(POTENTIAL_SUBGROUP_2)
if not group.preserve_subgroup_3:
group.subgroup_3 = world.random.choice(POTENTIAL_SUBGROUP_3)
def _randomize_overworld_groups(world: "ALTTPWorld", sprite_groups: dict[int, DungeonSpriteGroup]) -> None:
for group in sprite_groups.values():
if not (0 < group.group_id < 0x40):
continue
if not group.preserve_subgroup_0:
group.subgroup_0 = world.random.choice(POTENTIAL_SUBGROUP_0)
if not group.preserve_subgroup_1:
group.subgroup_1 = world.random.choice(POTENTIAL_SUBGROUP_1)
if not group.preserve_subgroup_2:
group.subgroup_2 = world.random.choice(POTENTIAL_SUBGROUP_2)
if not group.preserve_subgroup_3:
group.subgroup_3 = world.random.choice(POTENTIAL_SUBGROUP_3)
def _read_room_header_address(rom_bytes: bytes, room_id: int, room_header_bank: int) -> int:
pointer_address = DUNGEON_HEADER_POINTER_TABLE_BASE + (room_id * 2)
snes_address = (
rom_bytes[pointer_address]
| (rom_bytes[pointer_address + 1] << 8)
| (room_header_bank << 16)
)
return snes_to_pc(snes_address)
def _read_room_sprite_table_address(rom_bytes: bytes, room_id: int) -> int:
pointer_address = DUNGEON_SPRITE_POINTER_TABLE_BASE + (room_id * 2)
snes_address = (
rom_bytes[pointer_address]
| (rom_bytes[pointer_address + 1] << 8)
| (0x09 << 16)
)
return snes_to_pc(snes_address)
def _read_overworld_areas(
rom_bytes: bytes,
bush_spawn_table_address: int,
metadata: dict[str, object],
) -> list[OverworldEnemyArea]:
areas: list[OverworldEnemyArea] = []
do_not_randomize_area_ids = metadata["do_not_randomize_area_ids"]
for area_id in metadata["area_ids"]:
sprite_table_address = _read_overworld_sprite_table_address(rom_bytes, area_id)
graphics_block_address = _get_overworld_graphics_block_address(area_id)
areas.append(
OverworldEnemyArea(
area_id=area_id,
sprite_table_address=sprite_table_address,
graphics_block_address=graphics_block_address,
graphics_block_id=rom_bytes[graphics_block_address],
bush_sprite_id=rom_bytes[bush_spawn_table_address + area_id],
sprites=_read_overworld_sprites(rom_bytes, sprite_table_address),
do_not_randomize=area_id in do_not_randomize_area_ids,
)
)
return areas
def _read_overworld_sprite_table_address(rom_bytes: bytes, area_id: int) -> int:
pointer_address = OVERWORLD_SPRITE_POINTER_TABLE_BASE + (area_id * 2)
snes_address = (
rom_bytes[pointer_address]
| (rom_bytes[pointer_address + 1] << 8)
| (0x09 << 16)
)
return snes_to_pc(snes_address)
def _get_overworld_graphics_block_address(area_id: int) -> int:
if area_id in {0x80, 0x81}:
return 0x16576 + (area_id - 0x80)
if area_id in {0x110, 0x111}:
return 0x16576 + (area_id - 0x110)
address = OVERWORLD_AREA_GRAPHICS_BLOCK_BASE + area_id
if 0x40 <= area_id < 0x80:
address += 0x40
if 0x90 <= area_id < 0x110:
address -= 0x50
return address
def _read_overworld_sprites(rom_bytes: bytes, sprite_table_address: int) -> tuple[OverworldEnemySprite, ...]:
sprites: list[OverworldEnemySprite] = []
index = sprite_table_address
while rom_bytes[index] != 0xFF:
sprites.append(
OverworldEnemySprite(
address=index,
y_coord=rom_bytes[index],
x_coord=rom_bytes[index + 1],
sprite_id=rom_bytes[index + 2],
)
)
index += 3
return tuple(sprites)
def _read_room_sprites(
rom_bytes: bytes,
room_id: int,
sprite_table_address: int,
dungeon_sprite_metadata: dict[str, object],
) -> tuple[DungeonEnemySprite, ...]:
sprites: list[DungeonEnemySprite] = []
keyed_sprite_id_addresses = dungeon_sprite_metadata["keyed_sprite_id_addresses"]
editable_sprite_id_addresses = dungeon_sprite_metadata["room_sprite_id_addresses"].get(room_id)
if editable_sprite_id_addresses is None:
sprite_addresses = []
index = sprite_table_address + 1 # byte 0 is sort-sprites metadata
while rom_bytes[index] != 0xFF:
sprite_addresses.append(index)
index += 3
else:
sprite_addresses = [sprite_id_address - 2 for sprite_id_address in editable_sprite_id_addresses]
seen_sprite_addresses: set[int] = set()
unique_sprite_addresses = []
for address in sprite_addresses:
if address in seen_sprite_addresses:
continue
seen_sprite_addresses.add(address)
unique_sprite_addresses.append(address)
for index in unique_sprite_addresses:
byte_0 = rom_bytes[index]
byte_1 = rom_bytes[index + 1]
sprite_id = rom_bytes[index + 2]
is_overlord = (byte_1 & SPRITE_OVERLORD_MASK) == SPRITE_OVERLORD_MASK and (
(byte_0 & SPRITE_SUBTYPE_BYTE_0_MASK) != SPRITE_SUBTYPE_BYTE_0_MASK
)
if not is_overlord and sprite_id not in {KEY_SPRITE_ID, WALLMASTER_SPRITE_ID}:
byte_0 &= 0x9F
byte_1 &= SPRITE_OVERLORD_REMOVE_MASK
has_key = (index + 2) in keyed_sprite_id_addresses
sprites.append(
DungeonEnemySprite(
address=index,
byte_0=byte_0,
byte_1=byte_1,
sprite_id=sprite_id + (0x100 if is_overlord else 0),
is_overlord=is_overlord,
has_key=has_key,
)
)
return tuple(sprites)
def _get_enemizer_symbol(symbol_name: str) -> int:
return snes_to_pc(ENEMIZER_SYMBOLS[symbol_name])
def _load_enemy_room_metadata() -> dict[str, object]:
return {
"shutter_room_ids": SHUTTER_ROOM_IDS,
"water_room_ids": WATER_ROOM_IDS,
"dont_randomize_room_ids": DONT_RANDOMIZE_ROOM_IDS,
"no_special_enemies_standard_room_ids": NO_SPECIAL_ENEMIES_STANDARD_ROOM_IDS,
"boss_room_ids": BOSS_ROOM_IDS,
"room_requirements": tuple(
RoomGroupRequirement(
group_id=requirement.group_id,
subgroup_0=requirement.subgroup_0,
subgroup_1=requirement.subgroup_1,
subgroup_2=requirement.subgroup_2,
subgroup_3=requirement.subgroup_3,
rooms=requirement.rooms,
)
for requirement in ROOM_GROUP_REQUIREMENTS
),
}
def _load_dungeon_sprite_metadata() -> dict[str, object]:
return {
"room_sprite_id_addresses": {
room.room_id: room.sprite_id_addresses
for room in DUNGEON_SPRITE_ADDRESSES
},
"keyed_sprite_id_addresses": KEYED_SPRITE_ID_ADDRESSES,
}
def _load_enemy_sprite_requirements() -> tuple[EnemySpriteRequirement, ...]:
return tuple(
EnemySpriteRequirement(
sprite_name=entry.sprite_name,
sprite_id=entry.sprite_id,
boss=entry.boss,
overlord=entry.overlord,
do_not_randomize=entry.do_not_randomize,
killable=entry.killable,
npc=entry.npc,
never_use_dungeon=entry.never_use_dungeon,
never_use_overworld=entry.never_use_overworld,
cannot_have_key=entry.cannot_have_key,
is_object=entry.is_object,
absorbable=entry.absorbable,
is_water_sprite=entry.is_water_sprite,
is_enemy_sprite=entry.is_enemy_sprite,
group_ids=entry.group_ids,
subgroup_0=entry.subgroup_0,
subgroup_1=entry.subgroup_1,
subgroup_2=entry.subgroup_2,
subgroup_3=entry.subgroup_3,
parameters=entry.parameters,
special_glitched=entry.special_glitched,
excluded_rooms=entry.excluded_rooms,
dont_randomize_rooms=entry.dont_randomize_rooms,
spawnable_rooms=entry.spawnable_rooms,
)
for entry in ENEMY_SPRITE_REQUIREMENTS
)
def _load_overworld_enemy_metadata() -> dict[str, object]:
return {
"area_ids": AREA_IDS,
"do_not_randomize_area_ids": DO_NOT_RANDOMIZE_AREA_IDS,
"forced_group_requirements": tuple(
OverworldGroupRequirement(
group_id=requirement.group_id,
subgroup_0=requirement.subgroup_0,
subgroup_1=requirement.subgroup_1,
subgroup_2=requirement.subgroup_2,
subgroup_3=requirement.subgroup_3,
areas=requirement.areas,
)
for requirement in FORCED_GROUP_REQUIREMENTS
),
}
@dataclass(frozen=True)
class MergedRoomRequirement:
group_id: Optional[int]
subgroup_0: tuple[int, ...]
subgroup_1: tuple[int, ...]
subgroup_2: tuple[int, ...]
subgroup_3: tuple[int, ...]
def _merge_room_requirements(room_id: int, room_requirements: tuple[RoomGroupRequirement, ...]) -> MergedRoomRequirement:
group_id: Optional[int] = None
subgroup_0: list[int] = []
subgroup_1: list[int] = []
subgroup_2: list[int] = []
subgroup_3: list[int] = []
for requirement in room_requirements:
if room_id not in requirement.rooms:
continue
if requirement.group_id is not None:
group_id = requirement.group_id
if requirement.subgroup_0 is not None:
subgroup_0.append(requirement.subgroup_0)
if requirement.subgroup_1 is not None:
subgroup_1.append(requirement.subgroup_1)
if requirement.subgroup_2 is not None:
subgroup_2.append(requirement.subgroup_2)
if requirement.subgroup_3 is not None:
subgroup_3.append(requirement.subgroup_3)
return MergedRoomRequirement(
group_id=group_id,
subgroup_0=tuple(subgroup_0),
subgroup_1=tuple(subgroup_1),
subgroup_2=tuple(subgroup_2),
subgroup_3=tuple(subgroup_3),
)
def get_room_do_not_update_requirements(state: EnemyShuffleState, room: DungeonEnemyRoom) -> tuple[EnemySpriteRequirement, ...]:
room_sprite_ids = {sprite.sprite_id for sprite in room.sprites}
return tuple(
requirement for requirement in state.sprite_requirements
if (requirement.do_not_randomize or room.room_id in requirement.dont_randomize_rooms)
and requirement.sprite_id in room_sprite_ids
and can_spawn_in_room(requirement, room)
)
def get_possible_dungeon_sprite_groups(state: EnemyShuffleState, room: DungeonEnemyRoom) -> tuple[DungeonSpriteGroup, ...]:
do_not_update = get_room_do_not_update_requirements(state, room)
usable_groups = tuple(
group for group in state.sprite_groups.values()
if 0 < group.dungeon_group_id < 60
and _get_possible_enemy_requirements_for_group(state, room, group)
)
needs_key = any(sprite.has_key for sprite in room.sprites)
needs_killable = room.is_shutter_room
needs_water = room.is_water_room
room_requirements = _get_requirements_for_usable_dungeon_enemies(state)
water_requirements = tuple(requirement for requirement in room_requirements if requirement.is_water_sprite)
killable_requirements = tuple(
requirement for requirement in state.sprite_requirements
if _is_effectively_killable(requirement) and requirement.sprite_id != STAL_SPRITE_ID
)
key_requirements = tuple(requirement for requirement in killable_requirements if not requirement.cannot_have_key)
if (
not needs_key and not needs_killable and not needs_water
and not do_not_update
and room.required_group_id is None
and not room.required_subgroup_0
and not room.required_subgroup_1
and not room.required_subgroup_2
and not room.required_subgroup_3
):
return _get_unconstrained_possible_dungeon_sprite_groups(usable_groups, room_requirements, water_requirements)
return tuple(
group for group in usable_groups
if (
(not do_not_update or _build_requirement_group_matcher(do_not_update)(group))
and _group_matches_room_requirement(group, room)
and (
lambda possible_requirements: (
(not needs_killable or any(
_is_effectively_killable(requirement) and requirement.sprite_id != STAL_SPRITE_ID
for requirement in _filter_requirements_for_room_water_state(room, possible_requirements)
))
and (not needs_key or any(
_is_effectively_killable(requirement)
and not requirement.cannot_have_key
and requirement.sprite_id != STAL_SPRITE_ID
for requirement in _filter_requirements_for_room_water_state(room, possible_requirements)
))
and (not needs_water or any(
requirement.is_water_sprite
for requirement in _filter_requirements_for_room_water_state(room, possible_requirements)
))
)
)(_get_possible_enemy_requirements_for_group(state, room, group))
)
)
def can_spawn_in_room(requirement: EnemySpriteRequirement, room: DungeonEnemyRoom) -> bool:
return (
room.room_id not in requirement.excluded_rooms
and (not requirement.spawnable_rooms or room.room_id in requirement.spawnable_rooms)
and (requirement.sprite_id != WALLMASTER_SPRITE_ID or room.room_id < 0x100)
)
def _get_requirements_for_usable_dungeon_enemies(state: EnemyShuffleState) -> tuple[EnemySpriteRequirement, ...]:
return tuple(
requirement for requirement in state.sprite_requirements
if not requirement.npc
and requirement.is_enemy_sprite
and not requirement.boss
and not requirement.overlord
and not requirement.is_object
and not requirement.absorbable
and not requirement.never_use_dungeon
)
def _get_requirements_for_usable_overworld_enemies(state: EnemyShuffleState) -> tuple[EnemySpriteRequirement, ...]:
return tuple(
requirement for requirement in state.sprite_requirements
if not requirement.npc
and requirement.is_enemy_sprite
and not requirement.boss
and not requirement.overlord
and not requirement.is_object
and not requirement.absorbable
and not requirement.never_use_overworld
)
def _filter_requirements_for_room_water_state(
room: DungeonEnemyRoom,
requirements: tuple[EnemySpriteRequirement, ...],
) -> tuple[EnemySpriteRequirement, ...]:
if room.is_water_room:
return tuple(requirement for requirement in requirements if requirement.is_water_sprite)
return tuple(requirement for requirement in requirements if not requirement.is_water_sprite)
def _is_effectively_killable(requirement: EnemySpriteRequirement) -> bool:
return requirement.killable or requirement.sprite_id == WATER_TEKTITE_SPRITE_ID
def _get_effectively_killable_sprite_ids(requirements: tuple[EnemySpriteRequirement, ...]) -> set[int]:
return {
requirement.sprite_id for requirement in requirements
if _is_effectively_killable(requirement) and requirement.sprite_id != STAL_SPRITE_ID
}
def _get_unconstrained_possible_dungeon_sprite_groups(
usable_groups: tuple[DungeonSpriteGroup, ...],
room_requirements: tuple[EnemySpriteRequirement, ...],
water_requirements: tuple[EnemySpriteRequirement, ...],
) -> tuple[DungeonSpriteGroup, ...]:
water_subgroup_3 = set(_flatten_requirement_values(water_requirements, "subgroup_3"))
included_group_ids = set(_flatten_requirement_values(room_requirements, "group_ids"))
included_subgroup_0 = set(_flatten_requirement_values(room_requirements, "subgroup_0"))
included_subgroup_1 = set(_flatten_requirement_values(room_requirements, "subgroup_1"))
included_subgroup_2 = set(_flatten_requirement_values(room_requirements, "subgroup_2"))
included_subgroup_3 = {
subgroup for subgroup in _flatten_requirement_values(room_requirements, "subgroup_3")
if subgroup not in water_subgroup_3 and subgroup not in {54, 80}
}
return tuple(
group for group in usable_groups
if group.group_id in included_group_ids
or group.subgroup_0 in included_subgroup_0
or group.subgroup_1 in included_subgroup_1
or group.subgroup_2 in included_subgroup_2
or group.subgroup_3 in included_subgroup_3
)
def _build_requirement_group_matcher(requirements: tuple[EnemySpriteRequirement, ...]):
allowed_group_ids = set(_flatten_requirement_values(requirements, "group_ids"))
allowed_subgroup_0 = set(_flatten_requirement_values(requirements, "subgroup_0"))
allowed_subgroup_1 = set(_flatten_requirement_values(requirements, "subgroup_1"))
allowed_subgroup_2 = set(_flatten_requirement_values(requirements, "subgroup_2"))
allowed_subgroup_3 = set(_flatten_requirement_values(requirements, "subgroup_3"))
def matches(group: DungeonSpriteGroup) -> bool:
return (
not allowed_group_ids or group.group_id in allowed_group_ids
) and (
not allowed_subgroup_0 or group.subgroup_0 in allowed_subgroup_0
) and (
not allowed_subgroup_1 or group.subgroup_1 in allowed_subgroup_1
) and (
not allowed_subgroup_2 or group.subgroup_2 in allowed_subgroup_2
) and (
not allowed_subgroup_3 or group.subgroup_3 in allowed_subgroup_3
)
return matches
def _build_overworld_requirement_group_matcher(requirements: tuple[EnemySpriteRequirement, ...]):
allowed_group_ids = set(_flatten_requirement_values(requirements, "group_ids"))
allowed_subgroup_0 = set(_flatten_requirement_values(requirements, "subgroup_0"))
allowed_subgroup_1 = set(_flatten_requirement_values(requirements, "subgroup_1"))
allowed_subgroup_2 = set(_flatten_requirement_values(requirements, "subgroup_2"))
allowed_subgroup_3 = set(_flatten_requirement_values(requirements, "subgroup_3"))
def matches(group: DungeonSpriteGroup) -> bool:
return (
not allowed_group_ids or group.group_id in allowed_group_ids
) and (
not allowed_subgroup_0 or group.subgroup_0 in allowed_subgroup_0
) and (
not allowed_subgroup_1 or group.subgroup_1 in allowed_subgroup_1
) and (
not allowed_subgroup_2 or group.subgroup_2 in allowed_subgroup_2
) and (
not allowed_subgroup_3 or group.subgroup_3 in allowed_subgroup_3
)
return matches
def _build_requirement_group_presence_matcher(requirements: tuple[EnemySpriteRequirement, ...]):
allowed_group_ids = set(_flatten_requirement_values(requirements, "group_ids"))
allowed_subgroup_0 = set(_flatten_requirement_values(requirements, "subgroup_0"))
allowed_subgroup_1 = set(_flatten_requirement_values(requirements, "subgroup_1"))
allowed_subgroup_2 = set(_flatten_requirement_values(requirements, "subgroup_2"))
allowed_subgroup_3 = set(_flatten_requirement_values(requirements, "subgroup_3"))
def matches(group: DungeonSpriteGroup) -> bool:
return (
group.group_id in allowed_group_ids
or group.subgroup_0 in allowed_subgroup_0
or group.subgroup_1 in allowed_subgroup_1
or group.subgroup_2 in allowed_subgroup_2
or group.subgroup_3 in allowed_subgroup_3
)
return matches
def _flatten_requirement_values(requirements: tuple[EnemySpriteRequirement, ...], attribute: str) -> tuple[int, ...]:
return tuple(
value
for requirement in requirements
for value in getattr(requirement, attribute)
)
def _group_matches_room_requirement(group: DungeonSpriteGroup, room: DungeonEnemyRoom) -> bool:
return (
(room.required_group_id is None or room.required_group_id == group.dungeon_group_id)
and (not room.required_subgroup_0 or group.subgroup_0 in room.required_subgroup_0)
and (not room.required_subgroup_1 or group.subgroup_1 in room.required_subgroup_1)
and (not room.required_subgroup_2 or group.subgroup_2 in room.required_subgroup_2)
and (not room.required_subgroup_3 or group.subgroup_3 in room.required_subgroup_3)
)
def get_overworld_do_not_update_requirements(
state: EnemyShuffleState,
area: OverworldEnemyArea,
) -> tuple[EnemySpriteRequirement, ...]:
area_sprite_ids = {sprite.sprite_id for sprite in area.sprites}
return tuple(
requirement for requirement in state.sprite_requirements
if requirement.do_not_randomize and requirement.sprite_id in area_sprite_ids
)
def get_possible_overworld_sprite_groups(
state: EnemyShuffleState,
area: OverworldEnemyArea,
) -> tuple[DungeonSpriteGroup, ...]:
usable_groups = tuple(
group for group in state.sprite_groups.values()
if 0 < group.group_id < 0x40
and _get_possible_enemy_requirements_for_overworld_group(state, group)
)
do_not_update = get_overworld_do_not_update_requirements(state, area)
if not do_not_update:
return usable_groups
do_not_update_matcher = _build_overworld_requirement_group_matcher(do_not_update)
return tuple(group for group in usable_groups if do_not_update_matcher(group))
def _get_possible_enemy_requirements_for_group(
state: EnemyShuffleState,
room: DungeonEnemyRoom,
group: DungeonSpriteGroup,
) -> tuple[EnemySpriteRequirement, ...]:
dungeon_requirements = _get_requirements_for_usable_dungeon_enemies(state)
return tuple(
requirement for requirement in dungeon_requirements
if can_spawn_in_room(requirement, room)
and (
not requirement.group_ids or group.dungeon_group_id in requirement.group_ids
)
and (not requirement.subgroup_0 or group.subgroup_0 in requirement.subgroup_0)
and (not requirement.subgroup_1 or group.subgroup_1 in requirement.subgroup_1)
and (not requirement.subgroup_2 or group.subgroup_2 in requirement.subgroup_2)
and (not requirement.subgroup_3 or group.subgroup_3 in requirement.subgroup_3)
)
def _get_randomizable_sprites_in_room(
state: EnemyShuffleState,
room: DungeonEnemyRoom,
) -> tuple[DungeonEnemySprite, ...]:
randomizable_sprite_ids = {
requirement.sprite_id for requirement in state.sprite_requirements
if not requirement.do_not_randomize and room.room_id not in requirement.dont_randomize_rooms
}
return tuple(sprite for sprite in room.sprites if sprite.sprite_id in randomizable_sprite_ids)
def _get_possible_enemy_requirements_for_overworld_group(
state: EnemyShuffleState,
group: DungeonSpriteGroup,
) -> tuple[EnemySpriteRequirement, ...]:
overworld_requirements = _get_requirements_for_usable_overworld_enemies(state)
return tuple(
requirement for requirement in overworld_requirements
if (
not requirement.group_ids or group.group_id in requirement.group_ids
)
and (not requirement.subgroup_0 or group.subgroup_0 in requirement.subgroup_0)
and (not requirement.subgroup_1 or group.subgroup_1 in requirement.subgroup_1)
and (not requirement.subgroup_2 or group.subgroup_2 in requirement.subgroup_2)
and (not requirement.subgroup_3 or group.subgroup_3 in requirement.subgroup_3)
)
def _get_randomizable_sprites_in_overworld_area(
state: EnemyShuffleState,
area: OverworldEnemyArea,
) -> tuple[OverworldEnemySprite, ...]:
randomizable_sprite_ids = {
requirement.sprite_id for requirement in state.sprite_requirements
if not requirement.do_not_randomize
}
return tuple(sprite for sprite in area.sprites if sprite.sprite_id in randomizable_sprite_ids)
def _randomize_dungeon_rooms(
world: "ALTTPWorld",
dungeon_rooms: dict[int, DungeonEnemyRoom],
sprite_groups: dict[int, DungeonSpriteGroup],
sprite_requirements: tuple[EnemySpriteRequirement, ...],
) -> dict[int, RandomizedDungeonEnemyRoom]:
state = EnemyShuffleState(
dungeon_rooms=dungeon_rooms,
overworld_areas={},
sprite_groups=sprite_groups,
sprite_requirements=sprite_requirements,
room_group_requirements=tuple(),
overworld_group_requirements=tuple(),
shutter_room_ids=frozenset(room.room_id for room in dungeon_rooms.values() if room.is_shutter_room),
water_room_ids=frozenset(room.room_id for room in dungeon_rooms.values() if room.is_water_room),
dont_randomize_room_ids=frozenset(room.room_id for room in dungeon_rooms.values() if room.do_not_randomize),
no_special_enemies_standard_room_ids=frozenset(
room.room_id for room in dungeon_rooms.values() if room.no_special_enemies_standard
),
boss_room_ids=frozenset(),
dont_randomize_overworld_area_ids=frozenset(),
randomized_dungeon_rooms={},
randomized_overworld_areas={},
)
randomized_rooms: dict[int, RandomizedDungeonEnemyRoom] = {}
for room_id in sorted(dungeon_rooms):
room = dungeon_rooms[room_id]
skip_randomization = room.do_not_randomize or (
world.options.mode == "standard" and room.no_special_enemies_standard
)
selected_group = sprite_groups.get(room.graphics_block_id + 0x40)
if not skip_randomization:
possible_groups = get_possible_dungeon_sprite_groups(state, room)
if possible_groups:
selected_group = world.random.choice(possible_groups)
if selected_group is None:
selected_group = sprite_groups[room.graphics_block_id + 0x40]
randomized_rooms[room_id] = _randomize_room_sprites(
world,
state,
room,
selected_group,
skip_randomization,
)
return randomized_rooms
def _randomize_overworld_areas(
world: "ALTTPWorld",
overworld_areas: dict[int, OverworldEnemyArea],
sprite_groups: dict[int, DungeonSpriteGroup],
sprite_requirements: tuple[EnemySpriteRequirement, ...],
forced_group_requirements: tuple[OverworldGroupRequirement, ...],
) -> dict[int, RandomizedOverworldEnemyArea]:
state = EnemyShuffleState(
dungeon_rooms={},
overworld_areas=overworld_areas,
sprite_groups=sprite_groups,
sprite_requirements=sprite_requirements,
room_group_requirements=tuple(),
overworld_group_requirements=forced_group_requirements,
shutter_room_ids=frozenset(),
water_room_ids=frozenset(),
dont_randomize_room_ids=frozenset(),
no_special_enemies_standard_room_ids=frozenset(),
boss_room_ids=frozenset(),
dont_randomize_overworld_area_ids=frozenset(area.area_id for area in overworld_areas.values() if area.do_not_randomize),
randomized_dungeon_rooms={},
randomized_overworld_areas={},
)
randomized_areas: dict[int, RandomizedOverworldEnemyArea] = {}
for area_id in sorted(overworld_areas):
area = overworld_areas[area_id]
selected_group = sprite_groups.get(area.graphics_block_id)
if not area.do_not_randomize:
possible_groups = get_possible_overworld_sprite_groups(state, area)
if possible_groups:
selected_group = world.random.choice(possible_groups)
forced_group = _get_forced_overworld_group(area.area_id, forced_group_requirements, sprite_groups)
if forced_group is not None:
selected_group = forced_group
if selected_group is None:
selected_group = sprite_groups[area.graphics_block_id]
randomized_areas[area_id] = _randomize_overworld_area_sprites(
world,
state,
area,
selected_group,
area.do_not_randomize,
)
return randomized_areas
def _get_forced_overworld_group(
area_id: int,
forced_group_requirements: tuple[OverworldGroupRequirement, ...],
sprite_groups: dict[int, DungeonSpriteGroup],
) -> Optional[DungeonSpriteGroup]:
for requirement in forced_group_requirements:
if area_id not in requirement.areas or requirement.group_id is None:
continue
return sprite_groups.get(requirement.group_id)
return None
def _randomize_room_sprites(
world: "ALTTPWorld",
state: EnemyShuffleState,
room: DungeonEnemyRoom,
selected_group: DungeonSpriteGroup,
skip_randomization: bool,
) -> RandomizedDungeonEnemyRoom:
randomized_sprites = list(_clone_room_sprites(room))
if not skip_randomization:
possible_requirements = _get_possible_enemy_requirements_for_group(state, room, selected_group)
sprites_to_update = _get_randomizable_sprites_in_room(state, room)
sprites_to_update_addresses = {sprite.address for sprite in sprites_to_update}
if possible_requirements:
water_sprite_ids = [
requirement.sprite_id for requirement in possible_requirements
if requirement.is_water_sprite
]
if room.is_water_room:
if water_sprite_ids:
replacement_water_sprite_ids = water_sprite_ids
if room.is_shutter_room:
killable_water_sprite_ids = [
requirement.sprite_id for requirement in possible_requirements
if requirement.is_water_sprite
and _is_effectively_killable(requirement)
and requirement.sprite_id != STAL_SPRITE_ID
]
if killable_water_sprite_ids:
replacement_water_sprite_ids = killable_water_sprite_ids
for sprite in randomized_sprites:
if sprite.address in sprites_to_update_addresses:
_set_randomized_sprite_id(
randomized_sprites,
sprite.address,
world.random.choice(replacement_water_sprite_ids),
)
return _build_randomized_room(room, selected_group, randomized_sprites, False)
non_water_requirements = _filter_requirements_for_room_water_state(room, possible_requirements)
possible_sprite_ids = [requirement.sprite_id for requirement in non_water_requirements]
if not possible_sprite_ids:
return _build_randomized_room(room, selected_group, randomized_sprites, False)
killable_sprite_ids = [
requirement.sprite_id for requirement in non_water_requirements
if _is_effectively_killable(requirement) and requirement.sprite_id != STAL_SPRITE_ID
]
killable_key_sprite_ids = [
requirement.sprite_id for requirement in non_water_requirements
if _is_effectively_killable(requirement) and not requirement.cannot_have_key and requirement.sprite_id != STAL_SPRITE_ID
]
stal_count = 0
for sprite in sprites_to_update:
replacement_sprite_id: int
if sprite.has_key and killable_key_sprite_ids:
replacement_sprite_id = world.random.choice(killable_key_sprite_ids)
elif room.is_shutter_room and killable_sprite_ids:
replacement_sprite_id = world.random.choice(killable_sprite_ids)
elif not room.is_shutter_room and world.random.randrange(100) < 5:
replacement_sprite_id = STAL_SPRITE_ID
else:
replacement_sprite_id = world.random.choice(possible_sprite_ids)
_set_randomized_sprite_id(randomized_sprites, sprite.address, replacement_sprite_id)
if replacement_sprite_id == STAL_SPRITE_ID:
stal_count += 1
if stal_count > 2:
possible_sprite_ids = [sprite_id for sprite_id in possible_sprite_ids if sprite_id != STAL_SPRITE_ID]
return _build_randomized_room(room, selected_group, randomized_sprites, skip_randomization)
def _randomize_overworld_area_sprites(
world: "ALTTPWorld",
state: EnemyShuffleState,
area: OverworldEnemyArea,
selected_group: DungeonSpriteGroup,
skip_randomization: bool,
) -> RandomizedOverworldEnemyArea:
randomized_sprites = list(_clone_overworld_area_sprites(area))
bush_sprite_id = area.bush_sprite_id
if not skip_randomization:
possible_requirements = _get_possible_enemy_requirements_for_overworld_group(state, selected_group)
possible_sprite_ids = [requirement.sprite_id for requirement in possible_requirements]
sprites_to_update = _get_randomizable_sprites_in_overworld_area(state, area)
sprites_to_update_addresses = {sprite.address for sprite in sprites_to_update}
if possible_sprite_ids:
for sprite in sprites_to_update:
_set_randomized_overworld_sprite_id(
randomized_sprites,
sprite.address,
world.random.choice(possible_sprite_ids),
)
flopping_fish_addresses = [
sprite.address for sprite in randomized_sprites
if sprite.address in sprites_to_update_addresses and sprite.sprite_id == FLOPPING_FISH_SPRITE_ID
]
if len(flopping_fish_addresses) > 1:
non_fish_sprite_ids = [
sprite_id for sprite_id in possible_sprite_ids if sprite_id != FLOPPING_FISH_SPRITE_ID
]
for address in flopping_fish_addresses[1:]:
if non_fish_sprite_ids:
_set_randomized_overworld_sprite_id(
randomized_sprites,
address,
world.random.choice(non_fish_sprite_ids),
)
bush_candidates = [
requirement.sprite_id for requirement in possible_requirements
if not requirement.overlord
]
if bush_candidates:
bush_sprite_id = world.random.choice(bush_candidates)
return RandomizedOverworldEnemyArea(
area_id=area.area_id,
sprite_table_address=area.sprite_table_address,
graphics_block_address=area.graphics_block_address,
original_graphics_block_id=area.graphics_block_id,
graphics_block_id=selected_group.group_id,
original_bush_sprite_id=area.bush_sprite_id,
bush_sprite_id=bush_sprite_id,
sprites=tuple(randomized_sprites),
skipped_randomization=skip_randomization,
)
def _clone_room_sprites(room: DungeonEnemyRoom) -> list[RandomizedDungeonEnemySprite]:
return [
RandomizedDungeonEnemySprite(
address=sprite.address,
byte_0=sprite.byte_0,
byte_1=sprite.byte_1,
original_sprite_id=sprite.sprite_id,
sprite_id=sprite.sprite_id,
is_overlord=sprite.is_overlord,
has_key=sprite.has_key,
)
for sprite in room.sprites
]
def _clone_overworld_area_sprites(area: OverworldEnemyArea) -> list[RandomizedOverworldEnemySprite]:
return [
RandomizedOverworldEnemySprite(
address=sprite.address,
y_coord=sprite.y_coord,
x_coord=sprite.x_coord,
original_sprite_id=sprite.sprite_id,
sprite_id=sprite.sprite_id,
)
for sprite in area.sprites
]
def _set_randomized_sprite_id(
randomized_sprites: list[RandomizedDungeonEnemySprite],
address: int,
sprite_id: int,
) -> None:
for index, sprite in enumerate(randomized_sprites):
if sprite.address != address:
continue
randomized_sprites[index] = RandomizedDungeonEnemySprite(
address=sprite.address,
byte_0=sprite.byte_0,
byte_1=sprite.byte_1,
original_sprite_id=sprite.original_sprite_id,
sprite_id=sprite_id,
is_overlord=sprite.is_overlord,
has_key=sprite.has_key,
)
return
def _set_randomized_overworld_sprite_id(
randomized_sprites: list[RandomizedOverworldEnemySprite],
address: int,
sprite_id: int,
) -> None:
for index, sprite in enumerate(randomized_sprites):
if sprite.address != address:
continue
randomized_sprites[index] = RandomizedOverworldEnemySprite(
address=sprite.address,
y_coord=sprite.y_coord,
x_coord=sprite.x_coord,
original_sprite_id=sprite.original_sprite_id,
sprite_id=sprite_id,
)
return
def _build_randomized_room(
room: DungeonEnemyRoom,
selected_group: DungeonSpriteGroup,
sprites: list[RandomizedDungeonEnemySprite],
skipped_randomization: bool,
) -> RandomizedDungeonEnemyRoom:
return RandomizedDungeonEnemyRoom(
room_id=room.room_id,
room_header_address=room.room_header_address,
sprite_table_address=room.sprite_table_address,
original_graphics_block_id=room.graphics_block_id,
graphics_block_id=selected_group.dungeon_group_id,
tag_1=room.tag_1,
tag_2=room.tag_2,
sort_sprites_value=room.sort_sprites_value,
sprites=tuple(sprites),
skipped_randomization=skipped_randomization,
)
def validate_enemy_shuffle_state(state: EnemyShuffleState, is_standard_mode: bool) -> None:
for room_id, room in state.dungeon_rooms.items():
randomized_room = state.randomized_dungeon_rooms[room_id]
_validate_dungeon_room(state, room, randomized_room, is_standard_mode)
for area_id, area in state.overworld_areas.items():
randomized_area = state.randomized_overworld_areas[area_id]
_validate_overworld_area(state, area, randomized_area)
def _validate_dungeon_room(
state: EnemyShuffleState,
room: DungeonEnemyRoom,
randomized_room: RandomizedDungeonEnemyRoom,
is_standard_mode: bool,
) -> None:
selected_group = state.sprite_groups.get(randomized_room.graphics_block_id + 0x40)
if selected_group is None:
raise ValueError(f"Enemy shuffle produced unknown dungeon sprite group {randomized_room.graphics_block_id} for room {room.room_id}")
skipped = room.do_not_randomize or (is_standard_mode and room.no_special_enemies_standard)
if skipped and randomized_room.graphics_block_id != room.graphics_block_id:
raise ValueError(f"Enemy shuffle changed skipped room {room.room_id} graphics block")
if not skipped:
possible_groups = get_possible_dungeon_sprite_groups(state, room)
if possible_groups and selected_group not in possible_groups:
raise ValueError(f"Enemy shuffle selected illegal sprite group {selected_group.group_id} for room {room.room_id}")
possible_requirements = _get_possible_enemy_requirements_for_group(state, room, selected_group)
possible_sprite_ids = {requirement.sprite_id for requirement in possible_requirements}
killable_sprite_ids = _get_effectively_killable_sprite_ids(possible_requirements)
killable_key_sprite_ids = {
requirement.sprite_id for requirement in possible_requirements
if _is_effectively_killable(requirement) and not requirement.cannot_have_key and requirement.sprite_id != STAL_SPRITE_ID
}
water_sprite_ids = {
requirement.sprite_id for requirement in possible_requirements
if requirement.is_water_sprite
}
if not room.is_water_room:
possible_sprite_ids -= water_sprite_ids
killable_sprite_ids -= water_sprite_ids
killable_key_sprite_ids -= water_sprite_ids
do_not_randomize_sprite_ids = {
requirement.sprite_id for requirement in state.sprite_requirements
if requirement.do_not_randomize or room.room_id in requirement.dont_randomize_rooms
}
randomized_by_address = {sprite.address: sprite for sprite in randomized_room.sprites}
for original_sprite in room.sprites:
randomized_sprite = randomized_by_address[original_sprite.address]
if original_sprite.sprite_id in do_not_randomize_sprite_ids and randomized_sprite.sprite_id != original_sprite.sprite_id:
raise ValueError(f"Enemy shuffle changed do-not-randomize sprite in room {room.room_id} at {hex(original_sprite.address)}")
if original_sprite.sprite_id in do_not_randomize_sprite_ids or skipped:
continue
if room.is_water_room:
if randomized_sprite.sprite_id not in water_sprite_ids:
raise ValueError(f"Enemy shuffle placed non-water enemy {hex(randomized_sprite.sprite_id)} in water room {room.room_id}")
continue
if randomized_sprite.sprite_id in water_sprite_ids:
raise ValueError(f"Enemy shuffle placed water enemy {hex(randomized_sprite.sprite_id)} in non-water room {room.room_id}")
if original_sprite.has_key:
if randomized_sprite.sprite_id not in killable_key_sprite_ids:
raise ValueError(f"Enemy shuffle placed invalid key enemy {hex(randomized_sprite.sprite_id)} in room {room.room_id}")
continue
if room.is_shutter_room and randomized_sprite.sprite_id not in killable_sprite_ids:
raise ValueError(f"Enemy shuffle placed non-killable shutter enemy {hex(randomized_sprite.sprite_id)} in room {room.room_id}")
if randomized_sprite.sprite_id != STAL_SPRITE_ID and randomized_sprite.sprite_id not in possible_sprite_ids:
raise ValueError(f"Enemy shuffle placed illegal sprite {hex(randomized_sprite.sprite_id)} in room {room.room_id}")
if room.is_shutter_room and _get_randomizable_sprites_in_room(state, room):
all_killable_sprite_ids = _get_effectively_killable_sprite_ids(
_filter_requirements_for_room_water_state(room, state.sprite_requirements)
)
randomized_sprite_ids = {sprite.sprite_id for sprite in randomized_room.sprites}
if not (randomized_sprite_ids & all_killable_sprite_ids):
raise ValueError(f"Enemy shuffle left shutter room {room.room_id} without any killable enemies")
def _validate_overworld_area(
state: EnemyShuffleState,
area: OverworldEnemyArea,
randomized_area: RandomizedOverworldEnemyArea,
) -> None:
selected_group = state.sprite_groups.get(randomized_area.graphics_block_id)
if selected_group is None:
raise ValueError(f"Enemy shuffle produced unknown overworld sprite group {randomized_area.graphics_block_id} for area {hex(area.area_id)}")
if area.do_not_randomize and randomized_area.graphics_block_id != area.graphics_block_id:
raise ValueError(f"Enemy shuffle changed skipped overworld area {hex(area.area_id)} graphics block")
forced_group = _get_forced_overworld_group(area.area_id, state.overworld_group_requirements, state.sprite_groups)
if forced_group is not None and randomized_area.graphics_block_id != forced_group.group_id:
raise ValueError(f"Enemy shuffle failed forced overworld group for area {hex(area.area_id)}")
if not area.do_not_randomize and forced_group is None:
possible_groups = get_possible_overworld_sprite_groups(state, area)
if possible_groups and selected_group not in possible_groups:
raise ValueError(f"Enemy shuffle selected illegal overworld group {selected_group.group_id} for area {hex(area.area_id)}")
possible_requirements = _get_possible_enemy_requirements_for_overworld_group(state, selected_group)
possible_sprite_ids = {requirement.sprite_id for requirement in possible_requirements}
bush_sprite_ids = {
requirement.sprite_id for requirement in possible_requirements
if not requirement.overlord
}
known_sprite_ids = {requirement.sprite_id for requirement in state.sprite_requirements}
do_not_randomize_sprite_ids = {
requirement.sprite_id for requirement in state.sprite_requirements
if requirement.do_not_randomize
}
randomized_by_address = {sprite.address: sprite for sprite in randomized_area.sprites}
for original_sprite in area.sprites:
randomized_sprite = randomized_by_address[original_sprite.address]
if original_sprite.sprite_id not in known_sprite_ids:
continue
if original_sprite.sprite_id in do_not_randomize_sprite_ids and randomized_sprite.sprite_id != original_sprite.sprite_id:
raise ValueError(f"Enemy shuffle changed do-not-randomize overworld sprite in area {hex(area.area_id)} at {hex(original_sprite.address)}")
if original_sprite.sprite_id in do_not_randomize_sprite_ids or area.do_not_randomize:
continue
if randomized_sprite.sprite_id not in possible_sprite_ids:
raise ValueError(f"Enemy shuffle placed illegal overworld sprite {hex(randomized_sprite.sprite_id)} in area {hex(area.area_id)}")
randomizable_addresses = {sprite.address for sprite in _get_randomizable_sprites_in_overworld_area(state, area)}
non_fish_sprite_ids = possible_sprite_ids - {FLOPPING_FISH_SPRITE_ID}
if non_fish_sprite_ids and sum(
1 for sprite in randomized_area.sprites
if sprite.address in randomizable_addresses and sprite.sprite_id == FLOPPING_FISH_SPRITE_ID
) > 1:
raise ValueError(f"Enemy shuffle placed multiple flopping fish in area {hex(area.area_id)}")
if area.do_not_randomize and randomized_area.bush_sprite_id != area.bush_sprite_id:
raise ValueError(f"Enemy shuffle changed skipped overworld bush sprite in area {hex(area.area_id)}")
if not area.do_not_randomize and bush_sprite_ids and randomized_area.bush_sprite_id not in bush_sprite_ids:
raise ValueError(f"Enemy shuffle placed illegal bush enemy {hex(randomized_area.bush_sprite_id)} in area {hex(area.area_id)}")
def apply_enemy_shuffle(rom: "LocalRom", state: EnemyShuffleState) -> None:
for group in state.sprite_groups.values():
_write_sprite_group(rom, group)
for room in state.randomized_dungeon_rooms.values():
rom.write_byte(room.room_header_address + 3, room.graphics_block_id)
for sprite in room.sprites:
_write_dungeon_sprite(rom, sprite)
rom.write_byte(0x04CF4F, 0x10)
for area in state.randomized_overworld_areas.values():
rom.write_byte(area.graphics_block_address, area.graphics_block_id)
for sprite in area.sprites:
_write_overworld_sprite(rom, sprite)
bush_spawn_table_address = _get_enemizer_symbol("sprite_bush_spawn_table_overworld")
for area in state.randomized_overworld_areas.values():
rom.write_byte(bush_spawn_table_address + area.area_id, area.bush_sprite_id)
def _write_sprite_group(rom: "LocalRom", group: DungeonSpriteGroup) -> None:
address = SPRITE_GROUP_BASE_ADDRESS + (group.group_id * 4)
rom.write_byte(address, group.subgroup_0)
rom.write_byte(address + 1, group.subgroup_1)
rom.write_byte(address + 2, group.subgroup_2)
rom.write_byte(address + 3, group.subgroup_3)
def _write_dungeon_sprite(rom: "LocalRom", sprite: RandomizedDungeonEnemySprite) -> None:
sprite_id = sprite.sprite_id
byte_1 = sprite.byte_1
if sprite_id == WALLMASTER_SPRITE_ID:
sprite_id = 0x09
byte_1 |= SPRITE_OVERLORD_MASK
rom.write_byte(sprite.address, sprite.byte_0)
rom.write_byte(sprite.address + 1, byte_1)
rom.write_byte(sprite.address + 2, sprite_id & 0xFF)
def _write_overworld_sprite(rom: "LocalRom", sprite: RandomizedOverworldEnemySprite) -> None:
sprite_id = sprite.sprite_id
if sprite_id == OW_FALLING_ROCKS_SPRITE_ID:
rom.write_byte(sprite.address, 0)
rom.write_byte(sprite.address + 1, 0)
if sprite_id == WALLMASTER_SPRITE_ID:
sprite_id = OW_WALLMASTER_TO_HOULIHAN_SPRITE_ID
rom.write_byte(sprite.address + 2, sprite_id & 0xFF)