def add_credits(rom: Rom) -> Rom: credits_lut = rom.get_lut(0x1D871C, 128) base_addr = credits_lut[0] new_lut = OutputStream() data_stream = OutputStream() for index, line in enumerate(CREDITS_TEXT.splitlines()[1:]): line = line.strip() if len(line) > 0: encoded = TextBlock.encode_text(line) new_lut.put_u32(base_addr + data_stream.size()) data_stream.put_bytes(encoded) else: new_lut.put_u32(0x0) # And EOF marker new_lut.put_u32(0xffffffff) # Change the duration so it doesn't take so long to scroll duration = OutputStream() duration.put_u16(60 * 60) return rom.apply_patches({ 0x016848: duration.get_buffer(), 0x1D871C: new_lut.get_buffer(), Rom.pointer_to_offset(base_addr): data_stream.get_buffer() })
def write(self, rom: Rom) -> Rom: # Since there's a LUT and the data that it points to, create two output streams. # This should work because both are continuous. data_lut_stream = OutputStream() shop_inventory = OutputStream() next_shop_addr = self.shop_data_pointers[0].pointer start_size = 0 for index in range(51): new_shop_length = self.shop_inventories[index].write( shop_inventory) sdp = self.shop_data_pointers[index] sdp.contents = ( (sdp.shop_graphic << 4) & 0xf0) | (new_shop_length & 0x0f) sdp.pointer = next_shop_addr sdp.write(data_lut_stream) # Because the size of the data varies by shop, we have to keep track of how # large the output buffer was and move the pointer up by the difference. next_shop_addr += (shop_inventory.size() - start_size) start_size = shop_inventory.size() # Make a dictionary for the two parts so we only have to write the new Rom once. patches = { 0x1E070C: data_lut_stream.get_buffer(), Rom.pointer_to_offset(self.shop_data_pointers[0].pointer): shop_inventory.get_buffer() } return rom.apply_patches(patches)
def write(self, rom: Rom) -> Rom: patches = {} for index, map in enumerate(self._maps): # TODO: Figure out what breaks the Caravan. if index == 0x73: continue data = OutputStream() map.write(data) patches[Rom.pointer_to_offset( self._map_lut[index])] = data.get_buffer() return rom.apply_patches(patches)
def pack(self, rom: Rom) -> Rom: text_block = OutputStream() text_lut = OutputStream() next_addr = self.lut[0] text_block_offset = Rom.pointer_to_offset(next_addr) for index, data in enumerate(self.strings): if data is not None: text_lut.put_u32(next_addr) text_block.put_bytes(data) next_addr += len(data) else: text_lut.put_u32(self.lut[0]) patches = { self.lut_offset: text_lut.get_buffer(), text_block_offset: text_block.get_buffer() } return rom.apply_patches(patches)
class TestRom(unittest.TestCase): def setUp(self): self.rom = Rom(data=bytearray([ # String (0x0) 0x82, 0x66, 0x82, 0x8f, 0x82, 0x82, 0x82, 0x8c, 0x82, 0x89, 0x82, 0x8e, 0x00, 0x00, 0x00, 0x00, # LUT (0x10) 0x34, 0x12, 0x00, 0x08, 0x78, 0x56, 0x00, 0x08, 0xbc, 0x9a, 0x00, 0x08, 0xf0, 0xde, 0x00, 0x08 ])) def test_open_bytestream(self): stream = self.rom.open_bytestream(0x0, 0x4) self.assertEqual(stream.size(), 4) self.assertEqual(stream.get_u16(), 0x6682) def test_open_bytestream_out_of_bounds(self): with self.assertRaises(RuntimeError): stream = self.rom.open_bytestream(0x100, 0x4) def test_get_lut(self): lut = self.rom.get_lut(0x10, 4) self.assertEqual(len(lut), 4) addressses = [ 0x8001234, 0x8005678, 0x8009abc, 0x800def0, ] for index, address in enumerate(lut): self.assertEqual(address, addressses[index]) def test_get_lut_misaligned(self): with self.assertRaises(RuntimeError): lut = self.rom.get_lut(0x12, 2) def test_get_string(self): a_str = self.rom.get_string(0x0) self.assertEqual(len(a_str), 0xd) def test_patch(self): patch = OutputStream() patch.put_u32(0x12345678) patched = self.rom.apply_patch(0x0, patch.get_buffer()) confirm = patched.open_bytestream(0x0, 0x4) self.assertEqual(confirm.get_u32(), 0x12345678) def test_patches(self): patch = OutputStream() patch.put_u32(0x12345678) patched = self.rom.apply_patches({ 0x0: patch.get_buffer(), 0x10: patch.get_buffer() }) confirm = patched.open_bytestream(0x0, 0x4) self.assertEqual(confirm.get_u32(), 0x12345678) confirm = patched.open_bytestream(0x10, 0x4) self.assertEqual(confirm.get_u32(), 0x12345678) def test_overlap_patch(self): patch = OutputStream() patch.put_u32(0x12345678) patch.put_u32(0x12345678) with self.assertRaises(RuntimeError): patched = self.rom.apply_patches({ 0x0: patch.get_buffer(), 0x4: patch.get_buffer() }) self.assertNotEqual(patched, patched)
def randomize_rom(rom: Rom, flags: Flags, rom_seed: str) -> Rom: rng = random.Random() rng.seed(rom_seed) print(f"Randomize ROM: {flags.text()}, seed='{rom_seed}'") patches_to_load = BASE_PATCHES if flags.encounters is not None: patches_to_load.append("data/FF1EncounterToggle.ips") if flags.default_party is not None: patches_to_load.append("data/RandomDefault.ips") patched_rom_data = rom.rom_data for patch_path in patches_to_load: patch = Patch.load(patch_path) patched_rom_data = patch.apply(patched_rom_data) rom = Rom(data=bytearray(patched_rom_data)) rom = init_free_airship(rom) rom = add_credits(rom) event_text_block = EventTextBlock(rom) event_text_block.shrink() rom = event_text_block.pack(rom) rom = update_xp_requirements(rom, flags.exp_mult) if flags.key_item_shuffle is not None: placement = KeyItemPlacement(rom, rng.randint(0, 0xffffffff)) else: placement = KeyItemPlacement(rom) rom = placement.rom if flags.magic is not None: shuffle_magic = SpellShuffle(rom, rng) rom = shuffle_magic.write(rom) if flags.treasures is not None: if flags.treasures == "shuffle": rom = treasure_shuffle(rom, rng) else: rom = random_bucketed_treasures(rom, rng, flags.wealth) if flags.debug is not None: class_stats_stream = rom.open_bytestream(0x1E1354, 96) class_stats = [] while not class_stats_stream.is_eos(): class_stats.append(JobClass(class_stats_stream)) class_out_stream = OutputStream() for job_class in class_stats: # Set the starting weapon and armor for all classes to something # very fair and balanced: Masamune + Diamond Armlet. :) job_class.weapon_id = 0x28 job_class.armor_id = 0x0e # Write the (very balanced) new data out job_class.write(class_out_stream) rom = rom.apply_patch(0x1E1354, class_out_stream.get_buffer()) if flags.shuffle_formations: formation = FormationRandomization(rom, rng) rom = rom.apply_patches(formation.patches()) if True: enemy_data_stream = rom.open_bytestream(0x1DE044, 0x1860) enemies = [] while not enemy_data_stream.is_eos(): enemies.append(EnemyStats(enemy_data_stream)) # Rebalance (Revisited) Fiend HP enemies[0x78].max_hp = enemies[0x77].max_hp * 2 enemies[0x7a].max_hp = enemies[0x79].max_hp * 2 enemies[0x7c].max_hp = enemies[0x7b].max_hp * 2 enemies[0x7e].max_hp = enemies[0x7d].max_hp * 2 # And Chaos enemies[0x7f].max_hp = enemies[0x7e].max_hp * 2 # Finally, Piscodemons can suck it enemies[0x67].atk = int(enemies[0x67].atk / 2) # We'll also lower everyone's INT just to see how that works for index in range(0x80): enemies[index].intel = int(.666 * enemies[index].intel) # print(f"{hex(index)} HP: {enemies[index].max_hp}, INT: {enemies[index].intel}") out = OutputStream() for enemy in enemies: enemy.write(out) rom = rom.apply_patch(0x1DE044, out.get_buffer()) # Add the seed + flags to the party creation screen. seed_str = TextBlock.encode_text(f"Seed:\n{rom_seed}\nFlags:\n{flags}\x00") pointer = OutputStream() pointer.put_u32(0x8227054) rom = rom.apply_patches({ 0x227054: seed_str, 0x4d8d4: pointer.get_buffer() }) return rom