mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-05-21 16:11:46 -07:00
1708 lines
68 KiB
Python
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)
|