diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index 644aa1ed..6bf66a11 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -18,7 +18,7 @@ from .regions import create_regions from .options import PokemonRBOptions from .rom_addresses import rom_addresses from .text import encode_text -from .rom import generate_output, get_base_rom_bytes, get_base_rom_path, RedDeltaPatch, BlueDeltaPatch +from .rom import generate_output, PokemonRedProcedurePatch, PokemonBlueProcedurePatch from .pokemon import process_pokemon_data, process_move_data, verify_hm_moves from .encounters import process_pokemon_locations, process_trainer_data from .rules import set_rules @@ -33,12 +33,12 @@ class PokemonSettings(settings.Group): """File names of the Pokemon Red and Blue roms""" description = "Pokemon Red (UE) ROM File" copy_to = "Pokemon Red (UE) [S][!].gb" - md5s = [RedDeltaPatch.hash] + md5s = [PokemonRedProcedurePatch.hash] class BlueRomFile(settings.UserFilePath): description = "Pokemon Blue (UE) ROM File" copy_to = "Pokemon Blue (UE) [S][!].gb" - md5s = [BlueDeltaPatch.hash] + md5s = [PokemonBlueProcedurePatch.hash] red_rom_file: RedRomFile = RedRomFile(RedRomFile.copy_to) blue_rom_file: BlueRomFile = BlueRomFile(BlueRomFile.copy_to) @@ -113,16 +113,6 @@ class PokemonRedBlueWorld(World): self.local_locs = [] self.pc_item = None - @classmethod - def stage_assert_generate(cls, multiworld: MultiWorld): - versions = set() - for player in multiworld.player_ids: - if multiworld.worlds[player].game == "Pokemon Red and Blue": - versions.add(multiworld.worlds[player].options.game_version.current_key) - for version in versions: - if not os.path.exists(get_base_rom_path(version)): - raise FileNotFoundError(get_base_rom_path(version)) - @classmethod def stage_generate_early(cls, multiworld: MultiWorld): diff --git a/worlds/pokemon_rb/pokemon.py b/worlds/pokemon_rb/pokemon.py index 32c0e368..e5d161a4 100644 --- a/worlds/pokemon_rb/pokemon.py +++ b/worlds/pokemon_rb/pokemon.py @@ -1,9 +1,17 @@ from copy import deepcopy +import typing + +from worlds.Files import APTokenTypes + from . import poke_data, logic from .rom_addresses import rom_addresses +if typing.TYPE_CHECKING: + from . import PokemonRedBlueWorld + from .rom import PokemonRedProcedurePatch, PokemonBlueProcedurePatch -def set_mon_palettes(world, random, data): + +def set_mon_palettes(world: "PokemonRedBlueWorld", patch: "PokemonRedProcedurePatch | PokemonBlueProcedurePatch"): if world.options.randomize_pokemon_palettes == "vanilla": return pallet_map = { @@ -31,12 +39,9 @@ def set_mon_palettes(world, random, data): poke_data.evolves_from and poke_data.evolves_from[mon] != "Eevee"): pallet = palettes[-1] else: # completely_random or follow_evolutions and it is not an evolved form (except eeveelutions) - pallet = random.choice(list(pallet_map.values())) + pallet = world.random.choice(list(pallet_map.values())) palettes.append(pallet) - address = rom_addresses["Mon_Palettes"] - for pallet in palettes: - data[address] = pallet - address += 1 + patch.write_token(APTokenTypes.WRITE, rom_addresses["Mon_Palettes"], bytes(palettes)) def choose_forced_type(chances, random): @@ -253,9 +258,9 @@ def process_pokemon_data(self): mon_data[f"start move {i}"] = learnsets[mon].pop(0) if self.options.randomize_pokemon_catch_rates: - mon_data["catch rate"] = self.random.randint(self.options.minimum_catch_rate, 255) + mon_data["catch rate"] = self.random.randint(self.options.minimum_catch_rate.value, 255) else: - mon_data["catch rate"] = max(self.options.minimum_catch_rate, mon_data["catch rate"]) + mon_data["catch rate"] = max(self.options.minimum_catch_rate.value, mon_data["catch rate"]) def roll_tm_compat(roll_move): if self.local_move_data[roll_move]["type"] in [mon_data["type1"], mon_data["type2"]]: diff --git a/worlds/pokemon_rb/rock_tunnel.py b/worlds/pokemon_rb/rock_tunnel.py index 46b2be30..83b59255 100644 --- a/worlds/pokemon_rb/rock_tunnel.py +++ b/worlds/pokemon_rb/rock_tunnel.py @@ -1,5 +1,55 @@ +import random +import typing + +from worlds.Files import APTokenTypes + from .rom_addresses import rom_addresses +if typing.TYPE_CHECKING: + from .rom import PokemonBlueProcedurePatch, PokemonRedProcedurePatch + + +layout1F = [ + [20, 22, 32, 34, 20, 25, 22, 32, 34, 20, 25, 25, 25, 22, 20, 25, 22, 2, 2, 2], + [24, 26, 40, 1, 24, 25, 26, 62, 1, 28, 29, 29, 29, 30, 28, 29, 30, 1, 40, 2], + [28, 30, 1, 1, 28, 29, 30, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 23], + [23, 1, 1, 1, 1, 1, 23, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 31], + [31, 1, 1, 1, 1, 1, 31, 32, 34, 2, 1, 1, 2, 32, 34, 32, 34, 1, 1, 23], + [23, 1, 1, 23, 1, 1, 23, 1, 40, 23, 1, 1, 1, 1, 1, 1, 1, 1, 1, 31], + [31, 1, 1, 31, 1, 1, 31, 1, 1, 31, 1, 1, 1, 1, 1, 1, 1, 1, 1, 23], + [23, 1, 1, 23, 1, 1, 1, 1, 1, 2, 32, 34, 32, 34, 32, 34, 32, 34, 2, 31], + [31, 1, 1, 31, 1, 1, 1, 1, 1, 1, 1, 23, 1, 1, 1, 23, 1, 1, 40, 23], + [23, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 31, 1, 1, 1, 31, 1, 1, 1, 31], + [31, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 23, 1, 1, 1, 23, 1, 1, 1, 23], + [23, 32, 34, 32, 34, 32, 34, 32, 34, 32, 34, 31, 1, 1, 1, 31, 1, 1, 1, 31], + [31, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 23, 1, 1, 1, 23], + [ 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 31, 1, 1, 1, 31], + [20, 21, 21, 21, 22, 42, 1, 1, 1, 1, 20, 21, 22, 1, 1, 1, 1, 1, 1, 23], + [24, 25, 25, 25, 26, 1, 1, 1, 1, 1, 24, 25, 26, 1, 1, 1, 1, 1, 1, 31], + [24, 25, 25, 25, 26, 1, 1, 62, 1, 1, 24, 25, 26, 20, 21, 21, 21, 21, 21, 22], + [28, 29, 29, 29, 30, 78, 81, 82, 77, 78, 28, 29, 30, 28, 29, 29, 29, 29, 29, 30], +] +layout2F = [ + [23, 2, 32, 34, 32, 34, 32, 34, 32, 34, 32, 34, 32, 34, 32, 34, 32, 34, 32, 34], + [31, 62, 1, 23, 1, 1, 23, 1, 1, 1, 1, 1, 23, 62, 1, 1, 1, 1, 1, 2], + [23, 1, 1, 31, 1, 1, 31, 1, 1, 1, 1, 1, 31, 1, 1, 1, 1, 1, 1, 23], + [31, 1, 1, 23, 1, 1, 23, 1, 1, 23, 1, 1, 23, 1, 1, 23, 23, 1, 1, 31], + [23, 1, 1, 31, 1, 1, 31, 1, 1, 31, 2, 2, 31, 1, 1, 31, 31, 1, 1, 23], + [31, 1, 1, 1, 1, 1, 23, 1, 1, 1, 1, 62, 23, 1, 1, 1, 1, 1, 1, 31], + [23, 1, 1, 1, 1, 1, 31, 1, 1, 1, 1, 1, 31, 1, 1, 1, 1, 1, 1, 23], + [31, 1, 1, 23, 1, 1, 1, 1, 1, 23, 32, 34, 32, 34, 32, 34, 1, 1, 1, 31], + [23, 1, 1, 31, 1, 1, 1, 1, 1, 31, 1, 1, 1, 1, 1, 1, 1, 1, 1, 23], + [31, 1, 1, 23, 1, 1, 2, 1, 1, 23, 1, 1, 1, 1, 1, 1, 1, 1, 1, 31], + [23, 1, 1, 31, 1, 1, 2, 1, 1, 31, 1, 1, 1, 32, 34, 32, 34, 32, 34, 23], + [31, 2, 2, 2, 1, 1, 32, 34, 32, 34, 1, 1, 1, 23, 1, 1, 1, 1, 1, 31], + [23, 1, 1, 1, 1, 1, 23, 1, 1, 1, 1, 1, 1, 31, 1, 1, 62, 1, 1, 23], + [31, 1, 1, 1, 1, 1, 31, 1, 1, 1, 1, 1, 1, 23, 1, 1, 1, 1, 1, 31], + [23, 32, 34, 32, 34, 32, 34, 1, 1, 32, 34, 32, 34, 31, 1, 1, 1, 1, 1, 23], + [31, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 31], + [ 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 23], + [32, 34, 32, 34, 32, 34, 32, 34, 32, 34, 32, 34, 32, 34, 32, 34, 32, 34, 2, 31] +] + disallowed1F = [[2, 2], [3, 2], [1, 8], [2, 8], [7, 7], [8, 7], [10, 4], [11, 4], [11, 12], [11, 13], [16, 10], [17, 10], [18, 10], [16, 12], [17, 12], [18, 12]] disallowed2F = [[16, 2], [17, 2], [18, 2], [15, 5], [15, 6], [10, 10], [11, 10], [12, 10], [7, 14], [8, 14], [1, 15], @@ -7,29 +57,12 @@ disallowed2F = [[16, 2], [17, 2], [18, 2], [15, 5], [15, 6], [10, 10], [11, 10], [11, 1]] -def randomize_rock_tunnel(data, random): - +def randomize_rock_tunnel(patch: "PokemonRedProcedurePatch | PokemonBlueProcedurePatch", random: random.Random): seed = random.randint(0, 999999999999999999) random.seed(seed) - map1f = [] - map2f = [] - - address = rom_addresses["Map_Rock_Tunnel1F"] - for y in range(0, 18): - row = [] - for x in range(0, 20): - row.append(data[address]) - address += 1 - map1f.append(row) - - address = rom_addresses["Map_Rock_TunnelB1F"] - for y in range(0, 18): - row = [] - for x in range(0, 20): - row.append(data[address]) - address += 1 - map2f.append(row) + map1f = [row.copy() for row in layout1F] + map2f = [row.copy() for row in layout2F] current_map = map1f @@ -305,14 +338,6 @@ def randomize_rock_tunnel(data, random): current_map = map2f check_addable_block(map2f, disallowed2F) - address = rom_addresses["Map_Rock_Tunnel1F"] - for y in map1f: - for x in y: - data[address] = x - address += 1 - address = rom_addresses["Map_Rock_TunnelB1F"] - for y in map2f: - for x in y: - data[address] = x - address += 1 - return seed \ No newline at end of file + patch.write_token(APTokenTypes.WRITE, rom_addresses["Map_Rock_Tunnel1F"], bytes([b for row in map1f for b in row])) + patch.write_token(APTokenTypes.WRITE, rom_addresses["Map_Rock_TunnelB1F"], bytes([b for row in map2f for b in row])) + return seed diff --git a/worlds/pokemon_rb/rom.py b/worlds/pokemon_rb/rom.py index 5ebd204c..e49da82c 100644 --- a/worlds/pokemon_rb/rom.py +++ b/worlds/pokemon_rb/rom.py @@ -1,21 +1,66 @@ import os -import hashlib -import Utils -import bsdiff4 import pkgutil -from worlds.Files import APDeltaPatch -from .text import encode_text +import typing + +import Utils +from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes + +from . import poke_data from .items import item_table +from .text import encode_text from .pokemon import set_mon_palettes +from .regions import PokemonRBWarp, map_ids, town_map_coords from .rock_tunnel import randomize_rock_tunnel from .rom_addresses import rom_addresses -from .regions import PokemonRBWarp, map_ids, town_map_coords -from . import poke_data + +if typing.TYPE_CHECKING: + from . import PokemonRedBlueWorld -def write_quizzes(world, data, random): +class PokemonRedProcedurePatch(APProcedurePatch, APTokenMixin): + game = "Pokemon Red and Blue" + hash = "3d45c1ee9abd5738df46d2bdda8b57dc" + patch_file_ending = ".apred" + result_file_ending = ".gb" - def get_quiz(q, a): + procedure = [ + ("apply_bsdiff4", ["base_patch.bsdiff4"]), + ("apply_tokens", ["token_data.bin"]), + ] + + @classmethod + def get_source_data(cls) -> bytes: + from . import PokemonRedBlueWorld + with open(PokemonRedBlueWorld.settings.red_rom_file, "rb") as infile: + base_rom_bytes = bytes(infile.read()) + + return base_rom_bytes + + +class PokemonBlueProcedurePatch(APProcedurePatch, APTokenMixin): + game = "Pokemon Red and Blue" + hash = "50927e843568814f7ed45ec4f944bd8b" + patch_file_ending = ".apblue" + result_file_ending = ".gb" + + procedure = [ + ("apply_bsdiff4", ["base_patch.bsdiff4"]), + ("apply_tokens", ["token_data.bin"]), + ] + + @classmethod + def get_source_data(cls) -> bytes: + from . import PokemonRedBlueWorld + with open(PokemonRedBlueWorld.settings.blue_rom_file, "rb") as infile: + base_rom_bytes = bytes(infile.read()) + + return base_rom_bytes + + +def write_quizzes(world: "PokemonRedBlueWorld", patch: PokemonBlueProcedurePatch | PokemonRedProcedurePatch): + random = world.random + + def get_quiz(q: int, a: int): if q == 0: r = random.randint(0, 3) if r == 0: @@ -122,13 +167,13 @@ def write_quizzes(world, data, random): elif q2 == 1: if a: state = random.choice( - ['Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 'Colorado', 'Connecticut', - 'Delaware', 'Florida', 'Georgia', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas', - 'Kentucky', 'Louisiana', 'Maine', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota', - 'Mississippi', 'Missouri', 'Montana', 'Nebraska', 'Nevada', 'New Jersey', 'New Mexico', - 'New York', 'North Carolina', 'North Dakota', 'Ohio', 'Oklahoma', 'Oregon', 'Pennsylvania', - 'Rhode Island', 'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont', - 'Virginia', 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming']) + ["Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado", "Connecticut", + "Delaware", "Florida", "Georgia", "Hawaii", "Idaho", "Illinois", "Indiana", "Iowa", "Kansas", + "Kentucky", "Louisiana", "Maine", "Maryland", "Massachusetts", "Michigan", "Minnesota", + "Mississippi", "Missouri", "Montana", "Nebraska", "Nevada", "New Jersey", "New Mexico", + "New York", "North Carolina", "North Dakota", "Ohio", "Oklahoma", "Oregon", "Pennsylvania", + "Rhode Island", "South Carolina", "South Dakota", "Tennessee", "Texas", "Utah", "Vermont", + "Virginia", "Washington", "West Virginia", "Wisconsin", "Wyoming"]) else: state = "New Hampshire" return encode_text( @@ -209,7 +254,7 @@ def write_quizzes(world, data, random): return encode_text(f"{type1} deals{eff}damage to{type2} type?") elif q == 14: fossil_level = world.multiworld.get_location("Fossil Level - Trainer Parties", - world.player).party_data[0]['level'] + world.player).party_data[0]["level"] if not a: fossil_level += random.choice((-5, 5)) return encode_text(f"Fossil #MONrevive at level{fossil_level}?") @@ -224,46 +269,49 @@ def write_quizzes(world, data, random): return encode_text(f"According toMonash Uni.,{fodmap} {are_is}considered highin FODMAPs?") answers = [random.randint(0, 1) for _ in range(6)] - questions = random.sample((range(0, 16)), 6) - - question_texts = [] + question_texts: list[bytearray] = [] for i, question in enumerate(questions): question_texts.append(get_quiz(question, answers[i])) for i, quiz in enumerate(["A", "B", "C", "D", "E", "F"]): - data[rom_addresses[f"Quiz_Answer_{quiz}"]] = int(not answers[i]) << 4 | (i + 1) - write_bytes(data, question_texts[i], rom_addresses[f"Text_Quiz_{quiz}"]) + patch.write_token(APTokenTypes.WRITE, rom_addresses[f"Quiz_Answer_{quiz}"], bytes([int(not answers[i]) << 4 | (i + 1)])) + patch.write_token(APTokenTypes.WRITE, rom_addresses[f"Text_Quiz_{quiz}"], bytes(question_texts[i])) -def generate_output(world, output_directory: str): - random = world.random +def generate_output(world: "PokemonRedBlueWorld", output_directory: str): game_version = world.options.game_version.current_key - data = bytes(get_base_rom_bytes(game_version)) - base_patch = pkgutil.get_data(__name__, f'basepatch_{game_version}.bsdiff4') + patch_type = PokemonBlueProcedurePatch if game_version == "blue" else PokemonRedProcedurePatch + patch = patch_type(player=world.player, player_name=world.player_name) + patch.write_file("base_patch.bsdiff4", pkgutil.get_data(__name__, f"basepatch_{game_version}.bsdiff4")) - data = bytearray(bsdiff4.patch(data, base_patch)) + def write_bytes(address: int, data: typing.Sequence[int] | int): + if isinstance(data, int): + data = bytes([data]) + else: + data = bytes(data) - basemd5 = hashlib.md5() - basemd5.update(data) + patch.write_token(APTokenTypes.WRITE, address, data) pallet_connections = {entrance: world.multiworld.get_entrance(f"Pallet Town to {entrance}", - world.player).connected_region.name for - entrance in ["Player's House 1F", "Oak's Lab", - "Rival's House"]} + world.player).connected_region.name + for entrance in ["Player's House 1F", "Oak's Lab", "Rival's House"]} paths = None + if pallet_connections["Player's House 1F"] == "Oak's Lab": - paths = ((0x00, 4, 0x80, 5, 0x40, 1, 0xE0, 1, 0xFF), (0x40, 2, 0x20, 5, 0x80, 5, 0xFF)) + paths = (bytes([0x00, 4, 0x80, 5, 0x40, 1, 0xE0, 1, 0xFF]), bytes([0x40, 2, 0x20, 5, 0x80, 5, 0xFF])) elif pallet_connections["Rival's House"] == "Oak's Lab": - paths = ((0x00, 4, 0xC0, 3, 0x40, 1, 0xE0, 1, 0xFF), (0x40, 2, 0x10, 3, 0x80, 5, 0xFF)) + paths = (bytes([0x00, 4, 0xC0, 3, 0x40, 1, 0xE0, 1, 0xFF]), bytes([0x40, 2, 0x10, 3, 0x80, 5, 0xFF])) + if paths: - write_bytes(data, paths[0], rom_addresses["Path_Pallet_Oak"]) - write_bytes(data, paths[1], rom_addresses["Path_Pallet_Player"]) + write_bytes(rom_addresses["Path_Pallet_Oak"], paths[0]) + write_bytes(rom_addresses["Path_Pallet_Player"], paths[1]) + if pallet_connections["Rival's House"] == "Player's House 1F": - write_bytes(data, [0x2F, 0xC7, 0x06, 0x0D, 0x00, 0x01], rom_addresses["Pallet_Fly_Coords"]) + write_bytes(rom_addresses["Pallet_Fly_Coords"], [0x2F, 0xC7, 0x06, 0x0D, 0x00, 0x01]) elif pallet_connections["Oak's Lab"] == "Player's House 1F": - write_bytes(data, [0x5F, 0xC7, 0x0C, 0x0C, 0x00, 0x00], rom_addresses["Pallet_Fly_Coords"]) + write_bytes(rom_addresses["Pallet_Fly_Coords"], [0x5F, 0xC7, 0x0C, 0x0C, 0x00, 0x00]) for region in world.multiworld.get_regions(world.player): for entrance in region.exits: @@ -281,16 +329,18 @@ def generate_output(world, output_directory: str): while i > len(warp_to_ids) - 1: i -= len(warp_to_ids) connected_map_name = entrance.connected_region.name.split("-")[0] - data[address] = 0 if "Elevator" in connected_map_name else warp_to_ids[i] - data[address + 1] = map_ids[connected_map_name] + write_bytes(address, [ + 0 if "Elevator" in connected_map_name else warp_to_ids[i], + map_ids[connected_map_name] + ]) if world.options.door_shuffle == "simple": for (entrance, _, _, map_coords_entries, map_name, _) in town_map_coords.values(): destination = world.multiworld.get_entrance(entrance, world.player).connected_region.name (_, x, y, _, _, map_order_entry) = town_map_coords[destination] for map_coord_entry in map_coords_entries: - data[rom_addresses["Town_Map_Coords"] + (map_coord_entry * 4) + 1] = (y << 4) | x - data[rom_addresses["Town_Map_Order"] + map_order_entry] = map_ids[map_name] + write_bytes(rom_addresses["Town_Map_Coords"] + (map_coord_entry * 4) + 1, (y << 4) | x) + write_bytes(rom_addresses["Town_Map_Order"] + map_order_entry, map_ids[map_name]) if not world.options.key_items_only: for i, gym_leader in enumerate(("Pewter Gym - Brock TM", "Cerulean Gym - Misty TM", @@ -302,13 +352,13 @@ def generate_output(world, output_directory: str): try: tm = int(item_name[2:4]) move = poke_data.moves[world.local_tms[tm - 1]]["id"] - data[rom_addresses["Gym_Leader_Moves"] + (2 * i)] = move + write_bytes(rom_addresses["Gym_Leader_Moves"] + (2 * i), move) except KeyError: pass def set_trade_mon(address, loc): mon = world.multiworld.get_location(loc, world.player).item.name - data[rom_addresses[address]] = poke_data.pokemon_data[mon]["id"] + write_bytes(rom_addresses[address], poke_data.pokemon_data[mon]["id"]) world.trade_mons[address] = mon if game_version == "red": @@ -325,141 +375,139 @@ def generate_output(world, output_directory: str): set_trade_mon("Trade_Doris", "Cerulean Cave 1F - Wild Pokemon - 9") set_trade_mon("Trade_Crinkles", "Route 12 - Wild Pokemon - 4") - data[rom_addresses['Fly_Location']] = world.fly_map_code - data[rom_addresses['Map_Fly_Location']] = world.town_map_fly_map_code + write_bytes(rom_addresses["Fly_Location"], world.fly_map_code) + write_bytes(rom_addresses["Map_Fly_Location"], world.town_map_fly_map_code) if world.options.fix_combat_bugs: - data[rom_addresses["Option_Fix_Combat_Bugs"]] = 1 - data[rom_addresses["Option_Fix_Combat_Bugs_Focus_Energy"]] = 0x28 # jr z - data[rom_addresses["Option_Fix_Combat_Bugs_HP_Drain_Dream_Eater"]] = 0x1A # ld a, (de) - data[rom_addresses["Option_Fix_Combat_Bugs_PP_Restore"]] = 0xe6 # and a, direct - data[rom_addresses["Option_Fix_Combat_Bugs_PP_Restore"] + 1] = 0b0011111 - data[rom_addresses["Option_Fix_Combat_Bugs_Struggle"]] = 0xe6 # and a, direct - data[rom_addresses["Option_Fix_Combat_Bugs_Struggle"] + 1] = 0x3f - data[rom_addresses["Option_Fix_Combat_Bugs_Dig_Fly"]] = 0b10001100 - data[rom_addresses["Option_Fix_Combat_Bugs_Heal_Effect"]] = 0x20 # jr nz, - data[rom_addresses["Option_Fix_Combat_Bugs_Heal_Effect"] + 1] = 5 # 5 bytes ahead - data[rom_addresses["Option_Fix_Combat_Bugs_Heal_Stat_Modifiers"]] = 1 + write_bytes(rom_addresses["Option_Fix_Combat_Bugs"], 1) + write_bytes(rom_addresses["Option_Fix_Combat_Bugs_Focus_Energy"], 0x28) # jr z + write_bytes(rom_addresses["Option_Fix_Combat_Bugs_HP_Drain_Dream_Eater"], 0x1A) # ld a, (de) + write_bytes(rom_addresses["Option_Fix_Combat_Bugs_PP_Restore"], 0xe6) # and a, direct + write_bytes(rom_addresses["Option_Fix_Combat_Bugs_PP_Restore"] + 1, 0b0011111) + write_bytes(rom_addresses["Option_Fix_Combat_Bugs_Struggle"], 0xe6) # and a, direct + write_bytes(rom_addresses["Option_Fix_Combat_Bugs_Struggle"] + 1, 0x3f) + write_bytes(rom_addresses["Option_Fix_Combat_Bugs_Dig_Fly"], 0b10001100) + write_bytes(rom_addresses["Option_Fix_Combat_Bugs_Heal_Effect"], 0x20) # jr nz, + write_bytes(rom_addresses["Option_Fix_Combat_Bugs_Heal_Effect"] + 1, 5) # 5 bytes ahead + write_bytes(rom_addresses["Option_Fix_Combat_Bugs_Heal_Stat_Modifiers"], 1) if world.options.poke_doll_skip == "in_logic": - data[rom_addresses["Option_Silph_Scope_Skip"]] = 0x00 # nop - data[rom_addresses["Option_Silph_Scope_Skip"] + 1] = 0x00 # nop - data[rom_addresses["Option_Silph_Scope_Skip"] + 2] = 0x00 # nop + write_bytes(rom_addresses["Option_Silph_Scope_Skip"], 0x00) # nop + write_bytes(rom_addresses["Option_Silph_Scope_Skip"] + 1, 0x00) # nop + write_bytes(rom_addresses["Option_Silph_Scope_Skip"] + 2, 0x00) # nop if world.options.bicycle_gate_skips == "patched": - data[rom_addresses["Option_Route_16_Gate_Fix"]] = 0x00 # nop - data[rom_addresses["Option_Route_16_Gate_Fix"] + 1] = 0x00 # nop - data[rom_addresses["Option_Route_18_Gate_Fix"]] = 0x00 # nop - data[rom_addresses["Option_Route_18_Gate_Fix"] + 1] = 0x00 # nop + write_bytes(rom_addresses["Option_Route_16_Gate_Fix"], 0x00) # nop + write_bytes(rom_addresses["Option_Route_16_Gate_Fix"] + 1, 0x00) # nop + write_bytes(rom_addresses["Option_Route_18_Gate_Fix"], 0x00) # nop + write_bytes(rom_addresses["Option_Route_18_Gate_Fix"] + 1, 0x00) # nop if world.options.door_shuffle: - data[rom_addresses["Entrance_Shuffle_Fuji_Warp"]] = 1 # prevent warping to Fuji's House from Pokemon Tower 7F + write_bytes(rom_addresses["Entrance_Shuffle_Fuji_Warp"], 1) # prevent warping to Fuji's House from Pokemon Tower 7F if world.options.all_elevators_locked: - data[rom_addresses["Option_Locked_Elevator_Celadon"]] = 0x20 # jr nz - data[rom_addresses["Option_Locked_Elevator_Silph"]] = 0x20 # jr nz + write_bytes(rom_addresses["Option_Locked_Elevator_Celadon"], 0x20) # jr nz + write_bytes(rom_addresses["Option_Locked_Elevator_Silph"], 0x20) # jr nz if world.options.tea: - data[rom_addresses["Option_Tea"]] = 1 - data[rom_addresses["Guard_Drink_List"]] = 0x54 - data[rom_addresses["Guard_Drink_List"] + 1] = 0 - data[rom_addresses["Guard_Drink_List"] + 2] = 0 - write_bytes(data, encode_text("Gee, I have theworst caffeineheadache though." - "Oh wait there,the road's closed."), - rom_addresses["Text_Saffron_Gate"]) + write_bytes(rom_addresses["Option_Tea"], 1) + write_bytes(rom_addresses["Guard_Drink_List"], 0x54) + write_bytes(rom_addresses["Guard_Drink_List"] + 1, 0) + write_bytes(rom_addresses["Guard_Drink_List"] + 2, 0) + write_bytes(rom_addresses["Text_Saffron_Gate"], + encode_text("Gee, I have theworst caffeineheadache though." + "Oh wait there,the road's closed.")) - data[rom_addresses["Tea_Key_Item_A"]] = 0x28 # jr .z - data[rom_addresses["Tea_Key_Item_B"]] = 0x28 # jr .z - data[rom_addresses["Tea_Key_Item_C"]] = 0x28 # jr .z + write_bytes(rom_addresses["Tea_Key_Item_A"], 0x28) # jr .z + write_bytes(rom_addresses["Tea_Key_Item_B"], 0x28) # jr .z + write_bytes(rom_addresses["Tea_Key_Item_C"], 0x28) # jr .z - data[rom_addresses["Fossils_Needed_For_Second_Item"]] = ( - world.options.second_fossil_check_condition.value) + write_bytes(rom_addresses["Fossils_Needed_For_Second_Item"], world.options.second_fossil_check_condition.value) - data[rom_addresses["Option_Lose_Money"]] = int(not world.options.lose_money_on_blackout.value) + write_bytes(rom_addresses["Option_Lose_Money"], int(not world.options.lose_money_on_blackout.value)) if world.options.extra_key_items: - data[rom_addresses['Option_Extra_Key_Items_A']] = 1 - data[rom_addresses['Option_Extra_Key_Items_B']] = 1 - data[rom_addresses['Option_Extra_Key_Items_C']] = 1 - data[rom_addresses['Option_Extra_Key_Items_D']] = 1 - data[rom_addresses["Option_Split_Card_Key"]] = world.options.split_card_key.value - data[rom_addresses["Option_Blind_Trainers"]] = round(world.options.blind_trainers.value * 2.55) - data[rom_addresses["Option_Cerulean_Cave_Badges"]] = world.options.cerulean_cave_badges_condition.value - data[rom_addresses["Option_Cerulean_Cave_Key_Items"]] = world.options.cerulean_cave_key_items_condition.total - write_bytes(data, encode_text(str(world.options.cerulean_cave_badges_condition.value)), rom_addresses["Text_Cerulean_Cave_Badges"]) - write_bytes(data, encode_text(str(world.options.cerulean_cave_key_items_condition.total) + " key items."), rom_addresses["Text_Cerulean_Cave_Key_Items"]) - data[rom_addresses['Option_Encounter_Minimum_Steps']] = world.options.minimum_steps_between_encounters.value - data[rom_addresses['Option_Route23_Badges']] = world.options.victory_road_condition.value - data[rom_addresses['Option_Victory_Road_Badges']] = world.options.route_22_gate_condition.value - data[rom_addresses['Option_Elite_Four_Pokedex']] = world.options.elite_four_pokedex_condition.total - data[rom_addresses['Option_Elite_Four_Key_Items']] = world.options.elite_four_key_items_condition.total - data[rom_addresses['Option_Elite_Four_Badges']] = world.options.elite_four_badges_condition.value - write_bytes(data, encode_text(str(world.options.elite_four_badges_condition.value)), rom_addresses["Text_Elite_Four_Badges"]) - write_bytes(data, encode_text(str(world.options.elite_four_key_items_condition.total) + " key items, and"), rom_addresses["Text_Elite_Four_Key_Items"]) - write_bytes(data, encode_text(str(world.options.elite_four_pokedex_condition.total) + " #MON"), rom_addresses["Text_Elite_Four_Pokedex"]) - write_bytes(data, encode_text(str(world.total_key_items), length=2), rom_addresses["Trainer_Screen_Total_Key_Items"]) + write_bytes(rom_addresses["Option_Extra_Key_Items_A"], 1) + write_bytes(rom_addresses["Option_Extra_Key_Items_B"], 1) + write_bytes(rom_addresses["Option_Extra_Key_Items_C"], 1) + write_bytes(rom_addresses["Option_Extra_Key_Items_D"], 1) + write_bytes(rom_addresses["Option_Split_Card_Key"], world.options.split_card_key.value) + write_bytes(rom_addresses["Option_Blind_Trainers"], round(world.options.blind_trainers.value * 2.55)) + write_bytes(rom_addresses["Option_Cerulean_Cave_Badges"], world.options.cerulean_cave_badges_condition.value) + write_bytes(rom_addresses["Option_Cerulean_Cave_Key_Items"], world.options.cerulean_cave_key_items_condition.total) + write_bytes(rom_addresses["Text_Cerulean_Cave_Badges"], encode_text(str(world.options.cerulean_cave_badges_condition.value))) + write_bytes(rom_addresses["Text_Cerulean_Cave_Key_Items"], encode_text(str(world.options.cerulean_cave_key_items_condition.total) + " key items.")) + write_bytes(rom_addresses["Option_Encounter_Minimum_Steps"], world.options.minimum_steps_between_encounters.value) + write_bytes(rom_addresses["Option_Route23_Badges"], world.options.victory_road_condition.value) + write_bytes(rom_addresses["Option_Victory_Road_Badges"], world.options.route_22_gate_condition.value) + write_bytes(rom_addresses["Option_Elite_Four_Pokedex"], world.options.elite_four_pokedex_condition.total) + write_bytes(rom_addresses["Option_Elite_Four_Key_Items"], world.options.elite_four_key_items_condition.total) + write_bytes(rom_addresses["Option_Elite_Four_Badges"], world.options.elite_four_badges_condition.value) + write_bytes(rom_addresses["Text_Elite_Four_Badges"], encode_text(str(world.options.elite_four_badges_condition.value))) + write_bytes(rom_addresses["Text_Elite_Four_Key_Items"], encode_text(str(world.options.elite_four_key_items_condition.total) + " key items, and")) + write_bytes(rom_addresses["Text_Elite_Four_Pokedex"], encode_text(str(world.options.elite_four_pokedex_condition.total) + " #MON")) + write_bytes(rom_addresses["Trainer_Screen_Total_Key_Items"], encode_text(str(world.total_key_items), length=2)) - data[rom_addresses['Option_Viridian_Gym_Badges']] = world.options.viridian_gym_condition.value - data[rom_addresses['Option_EXP_Modifier']] = world.options.exp_modifier.value + write_bytes(rom_addresses["Option_Viridian_Gym_Badges"], world.options.viridian_gym_condition.value) + write_bytes(rom_addresses["Option_EXP_Modifier"], world.options.exp_modifier.value) if not world.options.require_item_finder: - data[rom_addresses['Option_Itemfinder']] = 0 # nop + write_bytes(rom_addresses["Option_Itemfinder"], 0) # nop if world.options.extra_strength_boulders: for i in range(0, 3): - data[rom_addresses['Option_Boulders'] + (i * 3)] = 0x15 + write_bytes(rom_addresses["Option_Boulders"] + (i * 3), 0x15) if world.options.extra_key_items: for i in range(0, 4): - data[rom_addresses['Option_Rock_Tunnel_Extra_Items'] + (i * 3)] = 0x15 + write_bytes(rom_addresses["Option_Rock_Tunnel_Extra_Items"] + (i * 3), 0x15) if world.options.old_man == "open_viridian_city": - data[rom_addresses['Option_Old_Man']] = 0x11 - data[rom_addresses['Option_Old_Man_Lying']] = 0x15 - data[rom_addresses['Option_Route3_Guard_B']] = world.options.route_3_condition.value + write_bytes(rom_addresses["Option_Old_Man"], 0x11) + write_bytes(rom_addresses["Option_Old_Man_Lying"], 0x15) + write_bytes(rom_addresses["Option_Route3_Guard_B"], world.options.route_3_condition.value) if world.options.route_3_condition == "open": - data[rom_addresses['Option_Route3_Guard_A']] = 0x11 + write_bytes(rom_addresses["Option_Route3_Guard_A"], 0x11) if not world.options.robbed_house_officer: - data[rom_addresses['Option_Trashed_House_Guard_A']] = 0x15 - data[rom_addresses['Option_Trashed_House_Guard_B']] = 0x11 + write_bytes(rom_addresses["Option_Trashed_House_Guard_A"], 0x15) + write_bytes(rom_addresses["Option_Trashed_House_Guard_B"], 0x11) if world.options.require_pokedex: - data[rom_addresses["Require_Pokedex_A"]] = 1 - data[rom_addresses["Require_Pokedex_B"]] = 1 - data[rom_addresses["Require_Pokedex_C"]] = 1 + write_bytes(rom_addresses["Require_Pokedex_A"], 1) + write_bytes(rom_addresses["Require_Pokedex_B"], 1) + write_bytes(rom_addresses["Require_Pokedex_C"], 1) else: - data[rom_addresses["Require_Pokedex_D"]] = 0x18 # jr + write_bytes(rom_addresses["Require_Pokedex_D"], 0x18) # jr if world.options.dexsanity: - data[rom_addresses["Option_Dexsanity_A"]] = 1 - data[rom_addresses["Option_Dexsanity_B"]] = 1 + write_bytes(rom_addresses["Option_Dexsanity_A"], 1) + write_bytes(rom_addresses["Option_Dexsanity_B"], 1) if world.options.all_pokemon_seen: - data[rom_addresses["Option_Pokedex_Seen"]] = 1 + write_bytes(rom_addresses["Option_Pokedex_Seen"], 1) money = str(world.options.starting_money.value).zfill(6) - data[rom_addresses["Starting_Money_High"]] = int(money[:2], 16) - data[rom_addresses["Starting_Money_Middle"]] = int(money[2:4], 16) - data[rom_addresses["Starting_Money_Low"]] = int(money[4:], 16) - data[rom_addresses["Text_Badges_Needed_Viridian_Gym"]] = encode_text( - str(world.options.viridian_gym_condition.value))[0] - data[rom_addresses["Text_Rt23_Badges_A"]] = encode_text( - str(world.options.victory_road_condition.value))[0] - data[rom_addresses["Text_Rt23_Badges_B"]] = encode_text( - str(world.options.victory_road_condition.value))[0] - data[rom_addresses["Text_Rt23_Badges_C"]] = encode_text( - str(world.options.victory_road_condition.value))[0] - data[rom_addresses["Text_Rt23_Badges_D"]] = encode_text( - str(world.options.victory_road_condition.value))[0] - data[rom_addresses["Text_Badges_Needed"]] = encode_text( - str(world.options.elite_four_badges_condition.value))[0] - write_bytes(data, encode_text( - " ".join(world.multiworld.get_location("Route 4 Pokemon Center - Pokemon For Sale", world.player).item.name.upper().split()[1:])), - rom_addresses["Text_Magikarp_Salesman"]) + write_bytes(rom_addresses["Starting_Money_High"], int(money[:2], 16)) + write_bytes(rom_addresses["Starting_Money_Middle"], int(money[2:4], 16)) + write_bytes(rom_addresses["Starting_Money_Low"], int(money[4:], 16)) + write_bytes(rom_addresses["Text_Badges_Needed_Viridian_Gym"], + encode_text(str(world.options.viridian_gym_condition.value))[0]) + write_bytes(rom_addresses["Text_Rt23_Badges_A"], + encode_text(str(world.options.victory_road_condition.value))[0]) + write_bytes(rom_addresses["Text_Rt23_Badges_B"], + encode_text(str(world.options.victory_road_condition.value))[0]) + write_bytes(rom_addresses["Text_Rt23_Badges_C"], + encode_text(str(world.options.victory_road_condition.value))[0]) + write_bytes(rom_addresses["Text_Rt23_Badges_D"], + encode_text(str(world.options.victory_road_condition.value))[0]) + write_bytes(rom_addresses["Text_Badges_Needed"], + encode_text(str(world.options.elite_four_badges_condition.value))[0]) + write_bytes(rom_addresses["Text_Magikarp_Salesman"], + encode_text(" ".join(world.multiworld.get_location("Route 4 Pokemon Center - Pokemon For Sale", world.player).item.name.upper().split()[1:]))) if world.options.badges_needed_for_hm_moves.value == 0: for hm_move in poke_data.hm_moves: - write_bytes(data, bytearray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - rom_addresses["HM_" + hm_move + "_Badge_a"]) + write_bytes(rom_addresses["HM_" + hm_move + "_Badge_a"], [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) elif world.extra_badges: written_badges = {} + badge_codes = {"Boulder Badge": 0x47, "Cascade Badge": 0x4F, + "Thunder Badge": 0x57, "Rainbow Badge": 0x5F, + "Soul Badge": 0x67, "Marsh Badge": 0x6F, + "Volcano Badge": 0x77, "Earth Badge": 0x7F} for hm_move, badge in world.extra_badges.items(): - data[rom_addresses["HM_" + hm_move + "_Badge_b"]] = {"Boulder Badge": 0x47, "Cascade Badge": 0x4F, - "Thunder Badge": 0x57, "Rainbow Badge": 0x5F, - "Soul Badge": 0x67, "Marsh Badge": 0x6F, - "Volcano Badge": 0x77, "Earth Badge": 0x7F}[badge] + write_bytes(rom_addresses["HM_" + hm_move + "_Badge_b"], badge_codes[badge]) move_text = hm_move if badge not in ["Marsh Badge", "Volcano Badge", "Earth Badge"]: move_text = ", " + move_text @@ -467,62 +515,58 @@ def generate_output(world, output_directory: str): if badge in written_badges: rom_address += len(written_badges[badge]) move_text = ", " + move_text - write_bytes(data, encode_text(move_text.upper()), rom_address) + write_bytes(rom_address, encode_text(move_text.upper())) written_badges[badge] = move_text for badge in ["Marsh Badge", "Volcano Badge", "Earth Badge"]: if badge not in written_badges: - write_bytes(data, encode_text("Nothing"), rom_addresses["Badge_Text_" + badge.replace(" ", "_")]) + write_bytes(rom_addresses["Badge_Text_" + badge.replace(" ", "_")], encode_text("Nothing")) type_loc = rom_addresses["Type_Chart"] for matchup in world.type_chart: if matchup[2] != 10: # don't needlessly divide damage by 10 and multiply by 10 - data[type_loc] = poke_data.type_ids[matchup[0]] - data[type_loc + 1] = poke_data.type_ids[matchup[1]] - data[type_loc + 2] = matchup[2] + write_bytes(type_loc, [poke_data.type_ids[matchup[0]], poke_data.type_ids[matchup[1]], matchup[2]]) type_loc += 3 - data[type_loc] = 0xFF - data[type_loc + 1] = 0xFF - data[type_loc + 2] = 0xFF + write_bytes(type_loc, b"\xFF\xFF\xFF") if world.options.normalize_encounter_chances.value: chances = [25, 51, 77, 103, 129, 155, 180, 205, 230, 255] for i, chance in enumerate(chances): - data[rom_addresses['Encounter_Chances'] + (i * 2)] = chance + write_bytes(rom_addresses["Encounter_Chances"] + (i * 2), chance) for mon, mon_data in world.local_poke_data.items(): if mon == "Mew": address = rom_addresses["Base_Stats_Mew"] else: address = rom_addresses["Base_Stats"] + (28 * (mon_data["dex"] - 1)) - data[address + 1] = world.local_poke_data[mon]["hp"] - data[address + 2] = world.local_poke_data[mon]["atk"] - data[address + 3] = world.local_poke_data[mon]["def"] - data[address + 4] = world.local_poke_data[mon]["spd"] - data[address + 5] = world.local_poke_data[mon]["spc"] - data[address + 6] = poke_data.type_ids[world.local_poke_data[mon]["type1"]] - data[address + 7] = poke_data.type_ids[world.local_poke_data[mon]["type2"]] - data[address + 8] = world.local_poke_data[mon]["catch rate"] - data[address + 15] = poke_data.moves[world.local_poke_data[mon]["start move 1"]]["id"] - data[address + 16] = poke_data.moves[world.local_poke_data[mon]["start move 2"]]["id"] - data[address + 17] = poke_data.moves[world.local_poke_data[mon]["start move 3"]]["id"] - data[address + 18] = poke_data.moves[world.local_poke_data[mon]["start move 4"]]["id"] - write_bytes(data, world.local_poke_data[mon]["tms"], address + 20) + write_bytes(address + 1, world.local_poke_data[mon]["hp"]) + write_bytes(address + 2, world.local_poke_data[mon]["atk"]) + write_bytes(address + 3, world.local_poke_data[mon]["def"]) + write_bytes(address + 4, world.local_poke_data[mon]["spd"]) + write_bytes(address + 5, world.local_poke_data[mon]["spc"]) + write_bytes(address + 6, poke_data.type_ids[world.local_poke_data[mon]["type1"]]) + write_bytes(address + 7, poke_data.type_ids[world.local_poke_data[mon]["type2"]]) + write_bytes(address + 8, world.local_poke_data[mon]["catch rate"]) + write_bytes(address + 15, poke_data.moves[world.local_poke_data[mon]["start move 1"]]["id"]) + write_bytes(address + 16, poke_data.moves[world.local_poke_data[mon]["start move 2"]]["id"]) + write_bytes(address + 17, poke_data.moves[world.local_poke_data[mon]["start move 3"]]["id"]) + write_bytes(address + 18, poke_data.moves[world.local_poke_data[mon]["start move 4"]]["id"]) + write_bytes(address + 20, world.local_poke_data[mon]["tms"]) if mon in world.learnsets and world.learnsets[mon]: address = rom_addresses["Learnset_" + mon.replace(" ", "")] for i, move in enumerate(world.learnsets[mon]): - data[(address + 1) + i * 2] = poke_data.moves[move]["id"] + write_bytes((address + 1) + i * 2, poke_data.moves[move]["id"]) - data[rom_addresses["Option_Aide_Rt2"]] = world.options.oaks_aide_rt_2.value - data[rom_addresses["Option_Aide_Rt11"]] = world.options.oaks_aide_rt_11.value - data[rom_addresses["Option_Aide_Rt15"]] = world.options.oaks_aide_rt_15.value + write_bytes(rom_addresses["Option_Aide_Rt2"], world.options.oaks_aide_rt_2.value) + write_bytes(rom_addresses["Option_Aide_Rt11"], world.options.oaks_aide_rt_11.value) + write_bytes(rom_addresses["Option_Aide_Rt15"], world.options.oaks_aide_rt_15.value) if world.options.safari_zone_normal_battles.value == 1: - data[rom_addresses["Option_Safari_Zone_Battle_Type"]] = 255 + write_bytes(rom_addresses["Option_Safari_Zone_Battle_Type"], 255) if world.options.reusable_tms.value: - data[rom_addresses["Option_Reusable_TMs"]] = 0xC9 + write_bytes(rom_addresses["Option_Reusable_TMs"], 0xC9) - data[rom_addresses["Option_Always_Half_STAB"]] = int(not world.options.same_type_attack_bonus.value) + write_bytes(rom_addresses["Option_Always_Half_STAB"], int(not world.options.same_type_attack_bonus.value)) if world.options.better_shops: inventory = ["Poke Ball", "Great Ball", "Ultra Ball"] @@ -531,43 +575,45 @@ def generate_output(world, output_directory: str): inventory += ["Potion", "Super Potion", "Hyper Potion", "Max Potion", "Full Restore", "Revive", "Antidote", "Awakening", "Burn Heal", "Ice Heal", "Paralyze Heal", "Full Heal", "Repel", "Super Repel", "Max Repel", "Escape Rope"] - shop_data = bytearray([0xFE, len(inventory)]) - shop_data += bytearray([item_table[item].id - 172000000 for item in inventory]) + shop_data = [0xFE, len(inventory)] + shop_data += [item_table[item].id - 172000000 for item in inventory] shop_data.append(0xFF) for shop in range(1, 11): - write_bytes(data, shop_data, rom_addresses[f"Shop{shop}"]) + write_bytes(rom_addresses[f"Shop{shop}"], shop_data) if world.options.stonesanity: - write_bytes(data, bytearray([0xFE, 1, item_table["Poke Doll"].id - 172000000, 0xFF]), rom_addresses[f"Shop_Stones"]) + write_bytes(rom_addresses["Shop_Stones"], [0xFE, 1, item_table["Poke Doll"].id - 172000000, 0xFF]) price = str(world.options.master_ball_price.value).zfill(6) - price = bytearray([int(price[:2], 16), int(price[2:4], 16), int(price[4:], 16)]) - write_bytes(data, price, rom_addresses["Price_Master_Ball"]) # Money values in Red and Blue are weird + price = [int(price[:2], 16), int(price[2:4], 16), int(price[4:], 16)] + write_bytes(rom_addresses["Price_Master_Ball"], price) # Money values in Red and Blue are weird - for item in reversed(world.multiworld.precollected_items[world.player]): - if data[rom_addresses["Start_Inventory"] + item.code - 172000000] < 255: - data[rom_addresses["Start_Inventory"] + item.code - 172000000] += 1 + from collections import Counter + start_inventory = Counter(item.code for item in reversed(world.multiworld.precollected_items[world.player])) + for item, value in start_inventory.items(): + write_bytes(rom_addresses["Start_Inventory"] + item - 172000000, min(value, 255)) - set_mon_palettes(world, random, data) + set_mon_palettes(world, patch) for move_data in world.local_move_data.values(): if move_data["id"] == 0: continue address = rom_addresses["Move_Data"] + ((move_data["id"] - 1) * 6) - write_bytes(data, bytearray([move_data["id"], move_data["effect"], move_data["power"], - poke_data.type_ids[move_data["type"]], round(move_data["accuracy"] * 2.55), move_data["pp"]]), address) + write_bytes(address, [move_data["id"], move_data["effect"], move_data["power"], + poke_data.type_ids[move_data["type"]], round(move_data["accuracy"] * 2.55), + move_data["pp"]]) - TM_IDs = bytearray([poke_data.moves[move]["id"] for move in world.local_tms]) - write_bytes(data, TM_IDs, rom_addresses["TM_Moves"]) + TM_IDs = [poke_data.moves[move]["id"] for move in world.local_tms] + write_bytes(rom_addresses["TM_Moves"], TM_IDs) if world.options.randomize_rock_tunnel: - seed = randomize_rock_tunnel(data, random) - write_bytes(data, encode_text(f"SEED: {seed}"), rom_addresses["Text_Rock_Tunnel_Sign"]) + seed = randomize_rock_tunnel(patch, world.random) + write_bytes(rom_addresses["Text_Rock_Tunnel_Sign"], encode_text(f"SEED: {seed}")) mons = [mon["id"] for mon in poke_data.pokemon_data.values()] - random.shuffle(mons) - data[rom_addresses['Title_Mon_First']] = mons.pop() + world.random.shuffle(mons) + write_bytes(rom_addresses["Title_Mon_First"], mons.pop()) for mon in range(0, 16): - data[rom_addresses['Title_Mons'] + mon] = mons.pop() + write_bytes(rom_addresses["Title_Mons"] + mon, mons.pop()) if world.options.game_version.value: mons.sort(key=lambda mon: 0 if mon == world.multiworld.get_location("Oak's Lab - Starter 1", world.player).item.name else 1 if mon == world.multiworld.get_location("Oak's Lab - Starter 2", world.player).item.name else @@ -576,34 +622,34 @@ def generate_output(world, output_directory: str): mons.sort(key=lambda mon: 0 if mon == world.multiworld.get_location("Oak's Lab - Starter 2", world.player).item.name else 1 if mon == world.multiworld.get_location("Oak's Lab - Starter 1", world.player).item.name else 2 if mon == world.multiworld.get_location("Oak's Lab - Starter 3", world.player).item.name else 3) - write_bytes(data, encode_text(world.multiworld.seed_name[-20:], 20, True), rom_addresses['Title_Seed']) + write_bytes(rom_addresses["Title_Seed"], encode_text(world.multiworld.seed_name[-20:], 20, True)) slot_name = world.multiworld.player_name[world.player] slot_name.replace("@", " ") slot_name.replace("<", " ") slot_name.replace(">", " ") - write_bytes(data, encode_text(slot_name, 16, True, True), rom_addresses['Title_Slot_Name']) + write_bytes(rom_addresses["Title_Slot_Name"], encode_text(slot_name, 16, True, True)) if world.trainer_name == "choose_in_game": - data[rom_addresses["Skip_Player_Name"]] = 0 + write_bytes(rom_addresses["Skip_Player_Name"], 0) else: - write_bytes(data, world.trainer_name, rom_addresses['Player_Name']) + write_bytes(rom_addresses["Player_Name"], world.trainer_name) if world.rival_name == "choose_in_game": - data[rom_addresses["Skip_Rival_Name"]] = 0 + write_bytes(rom_addresses["Skip_Rival_Name"], 0) else: - write_bytes(data, world.rival_name, rom_addresses['Rival_Name']) + write_bytes(rom_addresses["Rival_Name"], world.rival_name) - data[0xFF00] = 2 # client compatibility version - rom_name = bytearray(f'AP{Utils.__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0', - 'utf8')[:21] + write_bytes(0xFF00, 2) # client compatibility version + rom_name = bytearray(f"AP{Utils.__version__.replace('.', '')[0:3]}_{world.player}_{world.multiworld.seed:11}\0", + "utf8")[:21] rom_name.extend([0] * (21 - len(rom_name))) - write_bytes(data, rom_name, 0xFFC6) - write_bytes(data, world.multiworld.seed_name.encode(), 0xFFDB) - write_bytes(data, world.multiworld.player_name[world.player].encode(), 0xFFF0) + write_bytes(0xFFC6, rom_name) + write_bytes(0xFFDB, world.multiworld.seed_name.encode()) + write_bytes(0xFFF0, world.multiworld.player_name[world.player].encode()) world.finished_level_scaling.wait() - write_quizzes(world, data, random) + write_quizzes(world, patch) for location in world.multiworld.get_locations(world.player): if location.party_data: @@ -617,18 +663,18 @@ def generate_output(world, output_directory: str): levels = party["level"] for address, party in zip(addresses, parties): if isinstance(levels, int): - data[address] = levels + write_bytes(address, levels) address += 1 for mon in party: - data[address] = poke_data.pokemon_data[mon]["id"] + write_bytes(address, poke_data.pokemon_data[mon]["id"]) address += 1 else: address += 1 for level, mon in zip(levels, party): - data[address] = level - data[address + 1] = poke_data.pokemon_data[mon]["id"] + write_bytes(address, [level, poke_data.pokemon_data[mon]["id"]]) address += 2 - assert data[address] == 0 or location.name == "Fossil Level - Trainer Parties" + # This assert can't be done with procedure patch tokens. + # assert data[address] == 0 or location.name == "Fossil Level - Trainer Parties" continue elif location.rom_address is None: continue @@ -639,85 +685,24 @@ def generate_output(world, output_directory: str): rom_address = [rom_address] for address in rom_address: if location.item.name in poke_data.pokemon_data.keys(): - data[address] = poke_data.pokemon_data[location.item.name]["id"] + write_bytes(address, poke_data.pokemon_data[location.item.name]["id"]) elif " ".join(location.item.name.split()[1:]) in poke_data.pokemon_data.keys(): - data[address] = poke_data.pokemon_data[" ".join(location.item.name.split()[1:])]["id"] + write_bytes(address, poke_data.pokemon_data[" ".join(location.item.name.split()[1:])]["id"]) else: item_id = world.item_name_to_id[location.item.name] - 172000000 if item_id > 255: item_id -= 256 - data[address] = item_id + write_bytes(address, item_id) if location.level: - data[location.level_address] = location.level + write_bytes(location.level_address, location.level) else: rom_address = location.rom_address if not isinstance(rom_address, list): rom_address = [rom_address] for address in rom_address: - data[address] = 0x2C # AP Item + write_bytes(address, 0x2C) # AP Item - outfilepname = f'_P{world.player}' - outfilepname += f"_{world.multiworld.get_file_safe_player_name(world.player).replace(' ', '_')}" \ - if world.multiworld.player_name[world.player] != 'Player%d' % world.player else '' - rompath = os.path.join(output_directory, f'AP_{world.multiworld.seed_name}{outfilepname}.gb') - with open(rompath, 'wb') as outfile: - outfile.write(data) - if world.options.game_version.current_key == "red": - patch = RedDeltaPatch(os.path.splitext(rompath)[0] + RedDeltaPatch.patch_file_ending, player=world.player, - player_name=world.multiworld.player_name[world.player], patched_path=rompath) - else: - patch = BlueDeltaPatch(os.path.splitext(rompath)[0] + BlueDeltaPatch.patch_file_ending, player=world.player, - player_name=world.multiworld.player_name[world.player], patched_path=rompath) - - patch.write() - os.unlink(rompath) - - -def write_bytes(data, byte_array, address): - for byte in byte_array: - data[address] = byte - address += 1 - - -def get_base_rom_bytes(game_version: str, hash: str="") -> bytes: - file_name = get_base_rom_path(game_version) - with open(file_name, "rb") as file: - base_rom_bytes = bytes(file.read()) - if hash: - basemd5 = hashlib.md5() - basemd5.update(base_rom_bytes) - if hash != basemd5.hexdigest(): - raise Exception(f"Supplied Base Rom does not match known MD5 for Pokémon {game_version.title()} UE " - "release. Get the correct game and version, then dump it") - return base_rom_bytes - - -def get_base_rom_path(game_version: str) -> str: - options = Utils.get_options() - file_name = options["pokemon_rb_options"][f"{game_version}_rom_file"] - if not os.path.exists(file_name): - file_name = Utils.user_path(file_name) - return file_name - - -class BlueDeltaPatch(APDeltaPatch): - patch_file_ending = ".apblue" - hash = "50927e843568814f7ed45ec4f944bd8b" - game_version = "blue" - game = "Pokemon Red and Blue" - result_file_ending = ".gb" - @classmethod - def get_source_data(cls) -> bytes: - return get_base_rom_bytes(cls.game_version, cls.hash) - - -class RedDeltaPatch(APDeltaPatch): - patch_file_ending = ".apred" - hash = "3d45c1ee9abd5738df46d2bdda8b57dc" - game_version = "red" - game = "Pokemon Red and Blue" - result_file_ending = ".gb" - @classmethod - def get_source_data(cls) -> bytes: - return get_base_rom_bytes(cls.game_version, cls.hash) + patch.write_file("token_data.bin", patch.get_token_binary()) + out_file_name = world.multiworld.get_out_file_name_base(world.player) + patch.write(os.path.join(output_directory, f"{out_file_name}{patch.patch_file_ending}"))