""" Classes and functions related to creating a ROM patch """ import copy import os import struct from typing import TYPE_CHECKING, Dict, List, Tuple from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes from settings import get_settings from .data import TrainerPokemonDataTypeEnum, BASE_OFFSET, data from .items import reverse_offset_item_value from .options import (RandomizeWildPokemon, RandomizeTrainerParties, EliteFourRequirement, NormanRequirement, MatchTrainerLevels) from .pokemon import HM_MOVES, get_random_move from .util import bool_array_to_int, encode_string, get_easter_egg if TYPE_CHECKING: from . import PokemonEmeraldWorld _LOOPING_MUSIC = [ "MUS_GSC_ROUTE38", "MUS_GSC_PEWTER", "MUS_ROUTE101", "MUS_ROUTE110", "MUS_ROUTE120", "MUS_ROUTE122", "MUS_PETALBURG", "MUS_OLDALE", "MUS_GYM", "MUS_SURF", "MUS_PETALBURG_WOODS", "MUS_LILYCOVE_MUSEUM", "MUS_OCEANIC_MUSEUM", "MUS_ENCOUNTER_GIRL", "MUS_ENCOUNTER_MALE", "MUS_ABANDONED_SHIP", "MUS_FORTREE", "MUS_BIRCH_LAB", "MUS_B_TOWER_RS", "MUS_ENCOUNTER_SWIMMER", "MUS_CAVE_OF_ORIGIN", "MUS_ENCOUNTER_RICH", "MUS_VERDANTURF", "MUS_RUSTBORO", "MUS_POKE_CENTER", "MUS_CAUGHT", "MUS_VICTORY_GYM_LEADER", "MUS_VICTORY_LEAGUE", "MUS_VICTORY_WILD", "MUS_C_VS_LEGEND_BEAST", "MUS_ROUTE104", "MUS_ROUTE119", "MUS_CYCLING", "MUS_POKE_MART", "MUS_LITTLEROOT", "MUS_MT_CHIMNEY", "MUS_ENCOUNTER_FEMALE", "MUS_LILYCOVE", "MUS_DESERT", "MUS_HELP", "MUS_UNDERWATER", "MUS_VICTORY_TRAINER", "MUS_ENCOUNTER_MAY", "MUS_ENCOUNTER_INTENSE", "MUS_ENCOUNTER_COOL", "MUS_ROUTE113", "MUS_ENCOUNTER_AQUA", "MUS_FOLLOW_ME", "MUS_ENCOUNTER_BRENDAN", "MUS_EVER_GRANDE", "MUS_ENCOUNTER_SUSPICIOUS", "MUS_VICTORY_AQUA_MAGMA", "MUS_GAME_CORNER", "MUS_DEWFORD", "MUS_SAFARI_ZONE", "MUS_VICTORY_ROAD", "MUS_AQUA_MAGMA_HIDEOUT", "MUS_SAILING", "MUS_MT_PYRE", "MUS_SLATEPORT", "MUS_MT_PYRE_EXTERIOR", "MUS_SCHOOL", "MUS_HALL_OF_FAME", "MUS_FALLARBOR", "MUS_SEALED_CHAMBER", "MUS_CONTEST_WINNER", "MUS_CONTEST", "MUS_ENCOUNTER_MAGMA", "MUS_ABNORMAL_WEATHER", "MUS_WEATHER_GROUDON", "MUS_SOOTOPOLIS", "MUS_HALL_OF_FAME_ROOM", "MUS_TRICK_HOUSE", "MUS_ENCOUNTER_TWINS", "MUS_ENCOUNTER_ELITE_FOUR", "MUS_ENCOUNTER_HIKER", "MUS_CONTEST_LOBBY", "MUS_ENCOUNTER_INTERVIEWER", "MUS_ENCOUNTER_CHAMPION", "MUS_B_FRONTIER", "MUS_B_ARENA", "MUS_B_PYRAMID", "MUS_B_PYRAMID_TOP", "MUS_B_PALACE", "MUS_B_TOWER", "MUS_B_DOME", "MUS_B_PIKE", "MUS_B_FACTORY", "MUS_VS_RAYQUAZA", "MUS_VS_FRONTIER_BRAIN", "MUS_VS_MEW", "MUS_B_DOME_LOBBY", "MUS_VS_WILD", "MUS_VS_AQUA_MAGMA", "MUS_VS_TRAINER", "MUS_VS_GYM_LEADER", "MUS_VS_CHAMPION", "MUS_VS_REGI", "MUS_VS_KYOGRE_GROUDON", "MUS_VS_RIVAL", "MUS_VS_ELITE_FOUR", "MUS_VS_AQUA_MAGMA_LEADER", "MUS_RG_FOLLOW_ME", "MUS_RG_GAME_CORNER", "MUS_RG_ROCKET_HIDEOUT", "MUS_RG_GYM", "MUS_RG_CINNABAR", "MUS_RG_LAVENDER", "MUS_RG_CYCLING", "MUS_RG_ENCOUNTER_ROCKET", "MUS_RG_ENCOUNTER_GIRL", "MUS_RG_ENCOUNTER_BOY", "MUS_RG_HALL_OF_FAME", "MUS_RG_VIRIDIAN_FOREST", "MUS_RG_MT_MOON", "MUS_RG_POKE_MANSION", "MUS_RG_ROUTE1", "MUS_RG_ROUTE24", "MUS_RG_ROUTE3", "MUS_RG_ROUTE11", "MUS_RG_VICTORY_ROAD", "MUS_RG_VS_GYM_LEADER", "MUS_RG_VS_TRAINER", "MUS_RG_VS_WILD", "MUS_RG_VS_CHAMPION", "MUS_RG_PALLET", "MUS_RG_OAK_LAB", "MUS_RG_OAK", "MUS_RG_POKE_CENTER", "MUS_RG_SS_ANNE", "MUS_RG_SURF", "MUS_RG_POKE_TOWER", "MUS_RG_SILPH", "MUS_RG_FUCHSIA", "MUS_RG_CELADON", "MUS_RG_VICTORY_TRAINER", "MUS_RG_VICTORY_WILD", "MUS_RG_VICTORY_GYM_LEADER", "MUS_RG_VERMILLION", "MUS_RG_PEWTER", "MUS_RG_ENCOUNTER_RIVAL", "MUS_RG_RIVAL_EXIT", "MUS_RG_CAUGHT", "MUS_RG_POKE_JUMP", "MUS_RG_UNION_ROOM", "MUS_RG_NET_CENTER", "MUS_RG_MYSTERY_GIFT", "MUS_RG_BERRY_PICK", "MUS_RG_SEVII_CAVE", "MUS_RG_TEACHY_TV_SHOW", "MUS_RG_SEVII_ROUTE", "MUS_RG_SEVII_DUNGEON", "MUS_RG_SEVII_123", "MUS_RG_SEVII_45", "MUS_RG_SEVII_67", "MUS_RG_VS_DEOXYS", "MUS_RG_VS_MEWTWO", "MUS_RG_VS_LEGEND", "MUS_RG_ENCOUNTER_GYM_LEADER", "MUS_RG_ENCOUNTER_DEOXYS", "MUS_RG_TRAINER_TOWER", "MUS_RG_SLOW_PALLET", "MUS_RG_TEACHY_TV_MENU", ] _FANFARES: Dict[str, int] = { "MUS_LEVEL_UP": 80, "MUS_OBTAIN_ITEM": 160, "MUS_EVOLVED": 220, "MUS_OBTAIN_TMHM": 220, "MUS_HEAL": 160, "MUS_OBTAIN_BADGE": 340, "MUS_MOVE_DELETED": 180, "MUS_OBTAIN_BERRY": 120, "MUS_AWAKEN_LEGEND": 710, "MUS_SLOTS_JACKPOT": 250, "MUS_SLOTS_WIN": 150, "MUS_TOO_BAD": 160, "MUS_RG_POKE_FLUTE": 450, "MUS_RG_OBTAIN_KEY_ITEM": 170, "MUS_RG_DEX_RATING": 196, "MUS_OBTAIN_B_POINTS": 313, "MUS_OBTAIN_SYMBOL": 318, "MUS_REGISTER_MATCH_CALL": 135, } CAVE_EVENT_NAME_TO_ID = { "TERRA_CAVE_ROUTE_114_1": 1, "TERRA_CAVE_ROUTE_114_2": 2, "TERRA_CAVE_ROUTE_115_1": 3, "TERRA_CAVE_ROUTE_115_2": 4, "TERRA_CAVE_ROUTE_116_1": 5, "TERRA_CAVE_ROUTE_116_2": 6, "TERRA_CAVE_ROUTE_118_1": 7, "TERRA_CAVE_ROUTE_118_2": 8, "MARINE_CAVE_ROUTE_105_1": 9, "MARINE_CAVE_ROUTE_105_2": 10, "MARINE_CAVE_ROUTE_125_1": 11, "MARINE_CAVE_ROUTE_125_2": 12, "MARINE_CAVE_ROUTE_127_1": 13, "MARINE_CAVE_ROUTE_127_2": 14, "MARINE_CAVE_ROUTE_129_1": 15, "MARINE_CAVE_ROUTE_129_2": 16, } class PokemonEmeraldProcedurePatch(APProcedurePatch, APTokenMixin): game = "Pokemon Emerald" hash = "605b89b67018abcea91e693a4dd25be3" patch_file_ending = ".apemerald" result_file_ending = ".gba" procedure = [ ("apply_bsdiff4", ["base_patch.bsdiff4"]), ("apply_tokens", ["token_data.bin"]) ] @classmethod def get_source_data(cls) -> bytes: with open(get_settings().pokemon_emerald_settings.rom_file, "rb") as infile: base_rom_bytes = bytes(infile.read()) return base_rom_bytes def write_tokens(world: "PokemonEmeraldWorld", patch: PokemonEmeraldProcedurePatch) -> None: # TODO: Remove when the base patch is updated to include this change # Moves an NPC to avoid overlapping people during trainersanity patch.write_token( APTokenTypes.WRITE, 0x53A298 + (0x18 * 7) + 4, # Space Center 1F event address + 8th event + 4-byte offset for x coord struct.pack("= 50: continue player_name_ids[player_name] = len(player_name_ids) for j, b in enumerate(encode_string(player_name, 17)): patch.write_token( APTokenTypes.WRITE, data.rom_addresses["gArchipelagoPlayerNames"] + (player_name_ids[player_name] * 17) + j, struct.pack(" 35: item_name = item_name[:34] + "…" # Only 36 * 250 bytes for item names if next_item_name_offset + len(item_name) + 1 > 36 * 250: continue item_name_offsets[item_name] = next_item_name_offset next_item_name_offset += len(item_name) + 1 patch.write_token( APTokenTypes.WRITE, data.rom_addresses["gArchipelagoItemNames"] + (item_name_offsets[item_name]), encode_string(item_name) + b"\xFF" ) # There should always be enough space for one entry per location patch.write_token( APTokenTypes.WRITE, data.rom_addresses["gArchipelagoNameTable"] + (i * 5) + 0, struct.pack(" 0: starting_badges |= (1 << 0) if start_inventory.pop("Knuckle Badge", 0) > 0: starting_badges |= (1 << 1) if start_inventory.pop("Dynamo Badge", 0) > 0: starting_badges |= (1 << 2) if start_inventory.pop("Heat Badge", 0) > 0: starting_badges |= (1 << 3) if start_inventory.pop("Balance Badge", 0) > 0: starting_badges |= (1 << 4) if start_inventory.pop("Feather Badge", 0) > 0: starting_badges |= (1 << 5) if start_inventory.pop("Mind Badge", 0) > 0: starting_badges |= (1 << 6) if start_inventory.pop("Rain Badge", 0) > 0: starting_badges |= (1 << 7) pc_slots: List[Tuple[str, int]] = [] while any(qty > 0 for qty in start_inventory.values()): if len(pc_slots) >= 19: break for i, item_name in enumerate(start_inventory.keys()): if len(pc_slots) >= 19: break quantity = min(start_inventory[item_name], 999) if quantity == 0: continue start_inventory[item_name] -= quantity pc_slots.append((item_name, quantity)) pc_slots.sort(reverse=True) for i, slot in enumerate(pc_slots): address = data.rom_addresses["sNewGamePCItems"] + (i * 4) item = reverse_offset_item_value(world.item_name_to_id[slot[0]]) patch.write_token(APTokenTypes.WRITE, address + 0, struct.pack(" None: """ Encounter tables are lists of struct { min_level: 0x01 bytes, max_level: 0x01 bytes, species_id: 0x02 bytes } """ for map_data in world.modified_maps.values(): tables = [map_data.land_encounters, map_data.water_encounters, map_data.fishing_encounters] for table in tables: if table is not None: for i, species_id in enumerate(table.slots): address = table.address + 2 + (4 * i) patch.write_token(APTokenTypes.WRITE, address, struct.pack(" None: for species in world.modified_species.values(): patch.write_token(APTokenTypes.WRITE, species.address + 6, struct.pack(" None: for trainer in world.modified_trainers: party_address = trainer.party.address pokemon_data_size: int if trainer.party.pokemon_data_type in {TrainerPokemonDataTypeEnum.NO_ITEM_DEFAULT_MOVES, TrainerPokemonDataTypeEnum.ITEM_DEFAULT_MOVES}: pokemon_data_size = 8 else: # Custom Moves pokemon_data_size = 16 for i, pokemon in enumerate(trainer.party.pokemon): pokemon_address = party_address + (i * pokemon_data_size) # Replace species patch.write_token(APTokenTypes.WRITE, pokemon_address + 0x04, struct.pack(" None: for encounter in world.modified_legendary_encounters: patch.write_token(APTokenTypes.WRITE, encounter.address, struct.pack(" None: for encounter in world.modified_misc_pokemon: patch.write_token(APTokenTypes.WRITE, encounter.address, struct.pack(" None: patch.write_token(APTokenTypes.WRITE, data.rom_addresses["sStarterMon"] + 0, struct.pack(" None: tmhm_list_address = data.rom_addresses["sTMHMMoves"] for i, move in enumerate(world.modified_tmhm_moves): # Don't modify HMs if i >= 50: break if easter_egg[0] == 2: patch.write_token(APTokenTypes.WRITE, tmhm_list_address + (i * 2), struct.pack(" None: learnsets_address = data.rom_addresses["gTMHMLearnsets"] for species in world.modified_species.values(): patch.write_token( APTokenTypes.WRITE, learnsets_address + (species.species_id * 8), struct.pack(" None: probability = world.options.double_battle_chance.value / 100 battle_type_map = { 0: 4, 1: 8, 2: 6, 3: 13, } for trainer_data in data.trainers: if trainer_data.script_address != 0 and len(trainer_data.party.pokemon) > 1: original_battle_type = trainer_data.battle_type if original_battle_type in battle_type_map: # Don't touch anything other than regular single battles if world.random.random() < probability: # Set the trainer to be a double battle patch.write_token(APTokenTypes.WRITE, trainer_data.address + 0x18, struct.pack(" None: FORTREE_MOVE_TUTOR_INDEX = 24 if easter_egg[0] == 2: for i in range(30): patch.write_token( APTokenTypes.WRITE, data.rom_addresses["gTutorMoves"] + (i * 2), struct.pack("=50%) compatibility if world.options.tm_tutor_compatibility.value < 50: compatibility &= ~(1 << FORTREE_MOVE_TUTOR_INDEX) if world.random.random() < 0.5: compatibility |= 1 << FORTREE_MOVE_TUTOR_INDEX patch.write_token( APTokenTypes.WRITE, data.rom_addresses["sTutorLearnsets"] + (species.species_id * 4), struct.pack("