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 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 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 update_xp_requirements(rom: Rom, value) -> Rom: level_data = rom.open_bytestream(0x1BE3B4, 396) new_table = OutputStream() next_value = level_data.get_u32() while next_value is not None: new_table.put_u32(int(next_value * value)) next_value = level_data.get_u32() rom = rom.apply_patch(0x1BE3B4, new_table.get_buffer()) return rom
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 __init__(self, rom: Rom): self._maps = [] self.dummy_chests = [] self._map_lut = rom.get_lut(0x1E4F40, 124) for map_id, map_addr in enumerate(self._map_lut): map_stream = rom.get_stream(Rom.pointer_to_offset(map_addr), bytearray.fromhex("ffff")) map = MapFeatures(map_id, map_stream) self._maps.append(map) # Collect the dummy chests together self.dummy_chests += map.dummy_chests
def __init__(self, rom: Rom): data_lut_stream = rom.open_bytestream(0x1DFB04, 0x198) self.shop_data_pointers = [] self.shop_inventories = [] for index in range(51): shop = ShopDataPointer(data_lut_stream) self.shop_data_pointers.append(shop) # This is overkill for vanilla, but is still small. Since code bytes don't count, the # value in shop.shop_data_length isn't quite as useful as it could be. inventory = ShopInventory( rom.open_bytestream(Rom.pointer_to_offset(shop.pointer), 0x20), shop.shop_data_length) self.shop_inventories.append(inventory)
def __init__(self, rom: Rom, table_offset: int, table_size: int, base_event_id=0): self._base_event_id = base_event_id self._lut = list(rom.get_lut(table_offset, table_size))
def init_free_airship(rom: Rom) -> Rom: # Move the airship's start location to right outside of Coneria Castle. airship_start = OutputStream() airship_start.put_u32(0x918) airship_start.put_u32(0x998) return rom.apply_patch(0x65280, airship_start.get_buffer())
def random_bucketed_treasures(rom: Rom, rng: Random, wealth_level: int = 0) -> Rom: """Randomly generates and shuffles treasured based on wealth_level""" bucket_data = TreasureBuckets() chest_stream = rom.open_bytestream(0x217FB4, 0x400) items = [(0, x + 1) for x in list(range(0x45))] items = items + [(1, x + 1) for x in list(range(0x3F))] items = items + [(2, x + 1) for x in list(range(0x2A))] itemCount = len(items) chests_to_shuffle = [] original_list = [] moneyCount = 0 itemTotal = 0 for index in range(256): chest = TreasureChest.read(chest_stream) original_list.append(chest) if isinstance(chest, ItemChest): if chest.item_type != 0: item_bucket = bucket_data.getBucket(chest.item_type, chest.item_id) if wealth_level == 1: item_bucket = bucket_data.up_one(item_bucket) if wealth_level == -1: item_bucket = bucket_data.down_one(item_bucket) new_item = bucket_data.pullFromBucket(item_bucket, rng, 1) chest.item_id = new_item[0] itemTotal += 1 chests_to_shuffle.append(chest) elif isinstance(chest, MoneyChest): chest.qty = rng.randint(1, 0xfff) * rng.randint(1, 6) chests_to_shuffle.append(chest) moneyCount += 1 else: print("BAD CHEST") rng.shuffle(chests_to_shuffle) chest_data = OutputStream() for chest in original_list: if isinstance(chest, MoneyChest) or chest.item_type != 0: new_chest = chests_to_shuffle.pop() new_chest.write(chest_data) else: chest.write(chest_data) return rom.apply_patch(0x217FB4, chest_data.get_buffer())
def write(self, rom: Rom) -> Rom: permissions_stream = OutputStream() for permission in self._permissions: permissions_stream.put_u16(permission) rom = rom.apply_patch(0x1A20C0, permissions_stream.get_buffer()) rom = self._spells.write(rom) return self._shops.write(rom)
def main(argv): if len(argv) != 1: raise ValueError("Please pass ROM path and event ID parameters") rom = Rom(argv[0]) event_ranges = [(0x0, 0xD4), (0xFA0, 0xFAB), (0x1388, 0x13CD), (0x1F40, 0x2030), (0x2328, 0x2405)] find_jump_cmds(rom, event_ranges)
def __init__(self, rom: Rom, rng: Random): self._shops = ShopData(rom) self._spells = Spells(rom) self._permissions = [] permissions_stream = rom.open_bytestream(0x1A20C0, 0x82) while not permissions_stream.is_eos(): self._permissions.append(permissions_stream.get_u16()) self._do_shuffle(rng)
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)
def __init__(self, rom: Rom): # 130 Because: # 8 levels of magic, 8 spells per level (white + black) = 64 spells. # Spell name + help text for each = 64 x 2 = 128 # Slot 0 is skipped = 128 + 2 (blank name + empty help) = 130 self._name_help = TextBlock(rom, 0x1A1650, 130) spell_data_stream = rom.open_bytestream(0x1A1980, 0x740) self._spell_data = [] for index in range(65): self._spell_data.append(SpellData(spell_data_stream))
def random_treasures(rom: Rom, rng: Random) -> Rom: chest_stream = rom.open_bytestream(0x217FB4, 0x400) items = [(0, x + 1) for x in list(range(0x45))] items = items + [(1, x + 1) for x in list(range(0x3F))] items = items + [(2, x + 1) for x in list(range(0x2A))] itemCount = len(items) print(itemCount) chests_to_shuffle = [] original_list = [] moneyCount = 0 itemTotal = 0 for index in range(256): chest = TreasureChest.read(chest_stream) original_list.append(chest) if isinstance(chest, ItemChest): if chest.item_type != 0: new_item = items[rng.randint(0, itemCount - 1)] chest.item_type = new_item[0] chest.item_id = new_item[1] itemTotal += 1 chests_to_shuffle.append(chest) elif isinstance(chest, MoneyChest): chest.qty = rng.randint(1, 0xfff) * rng.randint(1, 6) chests_to_shuffle.append(chest) print(chest.qty) moneyCount += 1 else: print("BAD CHEST") return rng.shuffle(chests_to_shuffle) chest_data = OutputStream() for chest in original_list: if isinstance(chest, MoneyChest) or chest.item_type != 0: new_chest = chests_to_shuffle.pop() new_chest.write(chest_data) else: chest.write(chest_data) return rom.apply_patch(0x217FB4, chest_data.get_buffer())
def _do_placement(self, key_item_locations: tuple): source_headers = self._prepare_header(key_item_locations) patches = {} for event_id, source in EVENT_SOURCE_MAP.items(): event_source = pparse(f"{source_headers}\n\n{source}") event_addr = self.events.get_addr(event_id) event_space = self.rom.get_event( Rom.pointer_to_offset(event_addr)).size() # See if the event fits into it's vanilla location. event = easm.parse(event_source, event_addr) if len(event) > event_space: # Didn't fit. Move it to our space. event_addr = self.our_events.current_addr() self.events.set_addr(event_id, event_addr) # We'll write all of our events together at the end event = easm.parse(event_source, event_addr) self.our_events.put_bytes(event) else: # Add the event to the vanilla patches. patches[Rom.pointer_to_offset(event_addr)] = event self._update_npcs(key_item_locations) self._unite_mystic_key_doors() self._better_earth_plate() self._rewrite_give_texts() self._save_chests() # Append our new (relocated) events in the patch data. patches[0x223F4C] = self.our_events.get_buffer() # And then get all the patch data for the LUTs for offset, patch in self.events.get_patches().items(): patches[offset] = patch self.rom = self.rom.apply_patches(patches) self.rom = self.maps.write(self.rom)
def treasure_shuffle(rom: Rom, rng: Random) -> Rom: chest_stream = rom.open_bytestream(0x217FB4, 0x400) chests_to_shuffle = [] original_list = [] for index in range(256): chest = TreasureChest.read(chest_stream) original_list.append(chest) if isinstance(chest, MoneyChest) or chest.item_type != 0: chests_to_shuffle.append(chest) rng.shuffle(chests_to_shuffle) chest_data = OutputStream() for chest in original_list: if isinstance(chest, MoneyChest) or chest.item_type != 0: new_chest = chests_to_shuffle.pop() new_chest.write(chest_data) else: chest.write(chest_data) return rom.apply_patch(0x217FB4, chest_data.get_buffer())
def create_patch(): vanilla_rom = Rom("ff-dos.gba") flags_string = request.form['flags'] flags = Flags(flags_string) rom_seed = gen_seed(request.form['seed']) rom = randomize_rom(vanilla_rom, flags, rom_seed) gba_name = get_filename("ff-dos", flags, rom_seed) filename = parse.quote(gba_name[:len(gba_name) - 4] + ".ips") patch = Patch.create(vanilla_rom.rom_data, rom.rom_data) response = make_response(patch.encode()) response.headers['Content-Type'] = "application/octet-stream" response.headers['Content-Disposition'] = f"inline; filename={filename}" return response
def randomize(): uploaded_rom = request.files['rom'] rom = Rom(data=uploaded_rom.read()) flags_string = request.form['flags'] flags = Flags(flags_string) rom_seed = gen_seed(request.form['seed']) rom = randomize_rom(rom, flags, rom_seed) filename = uploaded_rom.filename index = filename.lower().rfind(".gba") if index < 0: return f"Bad filename: {filename}" filename = parse.quote(get_filename(filename, flags, rom_seed)) response = make_response(rom.rom_data) response.headers['Content-Type'] = "application/octet-stream" response.headers['Content-Disposition'] = f"inline; filename={filename}" return response
def main(): parser = ArgumentParser( description="Final Fantasy: Dawn of Souls Event->Script") parser.add_argument("rom", metavar="ROM file", type=str, help="ROM source file") parser.add_argument("--event", dest="event", type=str, help="Event to disassemble") parsed = parser.parse_args() # Opening the ROM is simple. rom = Rom(parsed.rom) # The event id is a bit trickier. The parser won't recognize hex values, so we need to accept it as a # string and convert it ourselves. if parsed.event.startswith("0x"): event_id = int(parsed.event, 16) else: event_id = int(parsed.event) disassemble_event(rom, event_id)
def main(argv): if len(argv) != 2: raise ValueError("Please pass ROM path and event ID parameters") global rom, event_text rom = Rom(argv[0]) event_text = EventTextBlock(rom) if argv[1] == "--strings": for idx, estr in enumerate(event_text): print(f"({idx}) @ {hex(event_text.lut[idx])}: {estr}") elif argv[1] == "--all": with open("labels/locations.txt", "r") as f: locations = f.readlines() for loc in locations: loc = loc.rstrip().lstrip() data = loc.split() index = int(data[0], 0) print(f"Map init event {loc}") decompile_event(index) print(f"\n* * *\n") with open("labels/game_events.txt", "r") as f: events = f.readlines() for event in events: event = event.rstrip().lstrip() data = event.split(":") index = int(data[0], 0) print(f"Main game event {event}") decompile_event(index) print(f"\n* * *\n") elif argv[1] == "--max": event_desc = {} with open("labels/event_desc.txt", "r") as f: events = f.readlines() for event in events: event = event.rstrip().lstrip() data = event.split(":") event_desc[data[0].lower()] = data[1] event_ranges = [(0x0, 0xD4), (0xFA0, 0xFAB), (0x1388, 0x13CD), (0x1F40, 0x2030), (0x2328, 0x2405)] for event_range in event_ranges: for event_id in range(event_range[0], event_range[1]): if hex(event_id) in event_desc: title = f"Event: {hex(event_id)} - {event_desc[hex(event_id)]}" else: title = f"Event: {hex(event_id)}" print(title) decompile_event(event_id) print(f"\n* * *\n") else: if argv[1].startswith("0x"): event_id = int(argv[1], 0) else: event_id = int(argv[1], 16) # Decompile the event in a function so it can recurse. decompile_event(event_id)
def __init__(self, rom: Rom, lut_offset: int, count: int): self.lut_offset = lut_offset self.lut = list(rom.get_lut(lut_offset, count)) self.strings = [] for addr in self.lut: self.strings.append(rom.get_string(Rom.pointer_to_offset(addr)))
def __init__(self, rom: Rom, rng: Random): size_stream = rom.open_bytestream(0x223644, 0xc3) sizes = [] while not size_stream.is_eos(): sizes.append(size_stream.get_u8()) small_enemies = [] large_enemies = [] enemy_id_map = [] for index, size in enumerate(sizes): enemy_id_map.append(index) if index >= 0x80: break if index not in FormationRandomization.MINI_BOSS_IDS: if size == 0: small_enemies.append(index) elif size == 1: large_enemies.append(index) shuffled_small = ShuffledList(small_enemies, rng) shuffled_large = ShuffledList(large_enemies, rng) enemy_data_stream = rom.open_bytestream(0x1DE044, 0x1860) enemies = [] scaled_enemies = [] while not enemy_data_stream.is_eos(): enemy = EnemyStats(enemy_data_stream) enemies.append(enemy) scaled_enemies.append(enemy) for index in range(len(small_enemies)): original = shuffled_small.original(index) replacement = shuffled_small[index] scaled_enemy = FormationRandomization.scale_enemy( enemies[original], enemies[replacement]) scaled_enemies[replacement] = scaled_enemy enemy_id_map[original] = replacement for index in range(len(large_enemies)): original = shuffled_large.original(index) replacement = shuffled_large[index] scaled_enemy = FormationRandomization.scale_enemy( enemies[original], enemies[replacement]) scaled_enemies[replacement] = scaled_enemy enemy_id_map[original] = replacement self._scaled_enemies_out = OutputStream() for enemy in scaled_enemies: enemy.write(self._scaled_enemies_out) formations_stream = rom.open_bytestream(0x2288B4, 0x14 * 0x171) self._formations_out = OutputStream() while not formations_stream.is_eos(): formation = Encounter(formations_stream) has_land = False has_flying = False for index, group in enumerate(formation.groups): if group.enemy_id < len(enemy_id_map): group.enemy_id = enemy_id_map[group.enemy_id] if group.max_count == 0: group.enemy_id = 0 if group.enemy_id in FormationRandomization.FLYING_MONSTER_IDS: has_flying = True else: has_land = True if formation.config in [0x0, 0x05]: if has_flying and has_land: # Both land and flying -> Mixed formation.config = 0x5 else: # Even if they're all flying, they still get mapped to "1-9 small" formation.config = 0x0 # Save the new formations formation.write(self._formations_out)
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 write(self, rom: Rom) -> Rom: spell_stream = OutputStream() for spell in self._spell_data: spell.write(spell_stream) return rom.apply_patch(0x1A1980, spell_stream.get_buffer())
# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # Designed to parse the Map data in a human reable format. # Takes in a FF DoS rom named 'FF1.gba' because lazy import sys from doslib.rom import Rom if sys.argv[1] is not None: rom = Rom(sys.argv[1]) else: rom = Rom('./primary_code/FF1.gba') locations = '' temp_spirtes = '' with open("labels/locations.txt", "r") as f: locations = f.readlines() with open("labels/sprites.txt", "r") as f: temp_sprites = f.readlines() sprites = dict() for s in temp_sprites: index = int(s.split(' ')[0], 16) sprites[index] = s.split(' ')[1].strip() + ' (' + s.split(
def get_map_offset(self, map_id: int) -> int: return Rom.pointer_to_offset(self._map_lut[map_id])
def randomize(rom_path: str, flags: Flags, rom_seed: str): rom_seed = gen_seed(rom_seed) vanilla_rom = Rom(rom_path) rom = randomize_rom(vanilla_rom, flags, rom_seed) rom.write(get_filename(rom_path, flags, rom_seed))
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