def rip_colordata(rom, offset=None, count=1): """Rip color data from ROM. Returns list of palettes, which are four RGB hexcodes.""" cloc = rom.tell() if offset is not None: rom.seek(offset) output = [] for i in range(0, count): out_col = [] for j in range(0, 4): color_bin = PTR.unpack(rom.read(2))[0] red = color_bin & 0x001F green = (color_bin & 0x03E0) >> 5 blue = (color_bin & 0x7C00) >> 10 colorcode = "dcolor {0}, {1}, {2}".format(red, green, blue) out_col.append(colorcode) output.append(out_col) rom.seek(cloc) return output
def rip_msprite_mtable(rom, offset=0x094D, count=9): """Rip an entire ROM's metasprite data.""" cloc = rom.tell() rom.seek(offset) #The metasprite metatable is oddly organized here. #Banks in one place, pointers in the other. banks = [] ptrs = [] for i in range(0, count): banks.append(CHARA.unpack(rom.read(1))[0]) for i in range(0, count): ptrs.append(PTR.unpack(rom.read(2))[0]) asmsrc = "SECTION \"MetaSprite metatable\", " + format_sectionaddr_rom( offset) + "\n" asmsrc += "MetaspriteBankMetatable::\n" for bank in banks: asmsrc += " db BANK(MetaSprite_" + "{0:x}".format(bank) + ")\n" asmsrc += "MetaspriteAddressMetatable::\n" for bank, ptr in zip(banks, ptrs): asmsrc += " dw MetaSprite_" + "{0:x}".format(bank) + "\n" asmsrc += "\n" files = {} for bank, ptr in zip(banks, ptrs): table_asmsrc, table_files = rip_msprite_table(rom, flat(bank, ptr)) asmsrc += table_asmsrc + "\n" files.update(table_files) return (asmsrc, files)
def extract(args): charmap = parse_charmap(args.charmap) tablenames = parse_tablenames(args.tablenames) with open(args.rom, 'rb') as rom: #Extract a list of pointers each index is expecting #This is used for trash byte detection later for table in tablenames: if table["format"] != "index": continue try: all_ptrs = tablenames[table["foreign_id"]]["expected_ptrs"] except KeyError: all_ptrs = [] rom.seek(flat(table["basebank"], table["baseaddr"])) for i in range(table["count"]): ptr = PTR.unpack(rom.read(2))[0] addr = flat(table["basebank"], ptr) if addr not in all_ptrs: all_ptrs.append(addr) all_ptrs.sort() tablenames[table["foreign_id"]]["expected_ptrs"] = all_ptrs for table in tablenames: #Indexes are extracted in a second pass if table["format"] == "index": continue entries = [] reverse_entries = {} try: expected_ptrs = table["expected_ptrs"] except KeyError: expected_ptrs = None csvdir = os.path.join(args.input, table["basedir"]) csvpath = os.path.join(args.input, table["filename"]) install_path(csvdir) with open(csvpath, "w+", encoding="utf-8") as table_csvfile: csvwriter = csv.writer(table_csvfile) csvwriter.writerow(["#", args.language]) if table["format"] == "table": for i in range(table["count"]): rom.seek( flat(table["basebank"], table["baseaddr"] + i * table["stride"])) reverse_entries[rom.tell()] = len(entries) entries.append(rom.tell()) data = extract_string(rom, charmap, table["stride"], expected_ptrs).encode("utf-8") idx = "{0}".format(i + 1).encode("utf-8") csvwriter.writerow([idx, data]) elif table["format"] == "block": rom.seek(flat(table["basebank"], table["baseaddr"])) for i in range(table["count"]): reverse_entries[rom.tell()] = len(entries) entries.append(rom.tell()) data = extract_string(rom, charmap, None, expected_ptrs).encode("utf-8") idx = "{0}".format(i + 1).encode("utf-8") csvwriter.writerow([idx, data]) #Save these for later table["entries"] = entries table["reverse_entries"] = reverse_entries #OK, now we can extract the indexes for table in tablenames: if table["format"] != "index": continue foreign_ptrs = tablenames[table["foreign_id"]]["reverse_entries"] rom.seek(flat(table["basebank"], table["baseaddr"])) csvdir = os.path.join(args.input, table["basedir"]) csvpath = os.path.join(args.input, table["filename"]) install_path(csvdir) with open(csvpath, "w+", encoding="utf-8") as table_csvfile: csvwriter = csv.writer(table_csvfile) pretty_row_length = math.ceil(math.sqrt(table["count"])) cur_row = [] for i in range(table["count"]): ptr = PTR.unpack(rom.read(2))[0] addr = flat(table["basebank"], ptr) cur_row.append("{0}".format(foreign_ptrs[addr] + 1).encode("utf-8")) if len(cur_row) >= pretty_row_length: csvwriter.writerow(cur_row) cur_row = [] if len(cur_row) > 0: csvwriter.writerow(cur_row)
def decompress_bank(rom, offset=None): """Decompress an entire table of compressed data. First return value is an array of data, individual data items match the return value of decompress_tilemap. Data is ordered in the same order that the ROM orders it. Second return value is the extracted table, represented as indexes into the data array. If offset is given, ROM will be read from that position. Your existing ROM position will be preserved.""" if offset is None: offset = rom.tell() last = rom.tell() rom.seek(offset) ret_dat = [] order_dat = [] ptr_table = [] #List of PTRs as specified in the table ptr_data = [] #List of PTRs containing data, some unused ptr_data_org = {} #Decompressed data ptr_data_index = {} #The table consists of multiple pointers followed immediately by compressed #tilemap data. There is no explicit length, so we discover the length of #the pointer table by reading and decompressing pointers until we overrun #already-decompressed data. Oh, and they don't come in a specified order #either. first_data_ptr = 0xFFFF last_data_ptr = 0x0000 last_ptr = None start_ptr, this_bank = banked(offset) #Extract the table first. while banked(rom.tell())[0] < first_data_ptr: this_ptr = PTR.unpack(rom.read(2))[0] ptr_table.append(this_ptr) if this_ptr <= first_data_ptr: first_data_ptr = this_ptr if last_data_ptr <= this_ptr: last_data_ptr = this_ptr #Extract the data second this_ptr = first_data_ptr while this_ptr <= last_data_ptr: decomp_data, decomp_len = decompress_tilemap(rom, flat(this_bank, this_ptr)) ptr_data.append(this_ptr) ptr_data_org[this_ptr] = decomp_data this_ptr += decomp_len #Resolve PTRs into numerical indexes and return them. for data_index, data_ptr in enumerate(ptr_data): ret_dat.append(ptr_data_org[data_ptr]) ptr_data_index[data_ptr] = data_index for table_index, table_ptr in enumerate(ptr_table): order_dat.append(ptr_data_index[table_ptr]) rom.seek(last) return ret_dat, order_dat
def rip_msprite_table(rom, offset=None): """Given a ROM and an offset, rip a metatable of metasprites. Ripped data will be returned as an array of ASM and a dict listing files to save.""" cloc = rom.tell() if offset is not None: rom.seek(offset) else: offset = cloc table_ptr, bank = banked(offset) ptrs = [] end = 0x7FFF cur = table_ptr table_ptr_list = [] ripped_sprites = {} while cur < end: offset_ptr = PTR.unpack(rom.read(2))[0] cur += 2 end = min(offset_ptr, end) if offset_ptr not in list(ripped_sprites.keys()): try: ripped_sprites[offset_ptr] = rip_msprite( rom, flat(bank, offset_ptr)) except KeyError: #If a metasprite points at undecipherable data, assume it's a #dummy pointer and don't generate a metasprite for it, since #it's clearly run off the end of the table. ripped_sprites[offset_ptr] = [] table_ptr_list.append(offset_ptr) #Now that we have the tables and their data, generate the ASM for it... data_asmsrc = "" table_asmsrc = "" files = {} table_symbol = "MetaSprite_" + "{0:x}".format(bank) table_asmsrc += "SECTION \"" + table_symbol + "\", ROMX[$" + "{0:x}".format( table_ptr) + "], BANK[$" + "{0:x}".format(bank) + "]\n" table_asmsrc += table_symbol + "::\n" sorted_ptrs = list(ripped_sprites.keys()) sorted_ptrs.sort() last_ptrkey_end = 0x0000 for ptrkey in sorted_ptrs: val = ripped_sprites[ptrkey] symbol = table_symbol + "_" + "{0:x}".format(ptrkey) file = "gfx/unknown/metasprite_" + "{0:x}".format( bank) + "/" + "{0:x}".format(ptrkey) + ".sprite.csv" binfile = "gfx/unknown/metasprite_" + "{0:x}".format( bank) + "/" + "{0:x}".format(ptrkey) + ".sprite.bin" if last_ptrkey_end != ptrkey: data_asmsrc += "SECTION \"" + symbol + "\", ROMX[$" + "{0:x}".format( ptrkey) + "], BANK[$" + "{0:x}".format(bank) + "]\n" data_asmsrc += symbol + "::\n" #ASSUMPTION: All 0 size metasprites are dummy pointers if len(val) > 0: data_asmsrc += " INCBIN \"" + binfile + "\"\n" data_asmsrc += symbol + "_END::\n" files[file] = "Y,X,Tile Offset,Attribute Mixing,Attributes\n" for spritecfg in val: files[file] += "{0:x},{1:x},{2:x},{3},{4:x}\n".format( spritecfg['y'], spritecfg['x'], spritecfg['tile'], spritecfg['attrib_mode'], spritecfg['attribs']) last_ptrkey_end = ptrkey + 1 + len(val) * 5 for ptr in table_ptr_list: symbol = table_symbol + "_" + "{0:x}".format(ptr) table_asmsrc += " dw " + symbol + "\n" asm_src = table_asmsrc + "\n" + data_asmsrc rom.seek(cloc) return (asm_src, files)
def extract(args): charmap = parse_charmap(args.charmap) banknames = parse_bank_names(args.banknames) banknames = extract_metatable_from_rom(args.rom, charmap, banknames, args) with open(args.rom, 'rb') as rom: for bank in banknames: wikitext = ["{|", "|-", "!Pointer", "!" + args.language] csvdata = [["Pointer", args.language]] rom.seek(flat(bank["basebank"], bank["baseaddr"])) addr = bank["baseaddr"] end = 0x8000 #Autodetect the end/length of the table by finding the lowest #pointer that isn't stored after an existing pointer while addr < end: next_ptr = PTR.unpack(rom.read(2))[0] #Reject obviously invalid pointers if (next_ptr < addr or next_ptr > 0x7FFF): break end = min(end, next_ptr) addr += 2 tbl_length = (addr - bank["baseaddr"]) // 2 #Actually extract our strings string = [] #Stores the actual end of the last string, used for alias detection last_start = 0xFFFF last_end = 0xFFFF last_nonaliasing_row = -1 #Also store if a redirected/overflowed row is being extracted redirected = False old_loc = None for i in range(tbl_length): csvrow = [ "0x{0:x}".format( flat(bank["basebank"], bank["baseaddr"] + i * 2)) ] wikitext.append("|-") wikitext.append("|0x{0:x}".format( flat(bank["basebank"], bank["baseaddr"] + i * 2))) rom.seek(flat(bank["basebank"], bank["baseaddr"] + i * 2)) read_ptr = PTR.unpack(rom.read(2))[0] #Attempt to autodetect "holes" in the text data. next_ptr = PTR.unpack(rom.read(2))[0] expected_length = next_ptr - read_ptr if i >= tbl_length - 1: expected_length = -1 #maximum length by far #Two different alias detects: #First, we try to see if this pointer matches another pointer #in the table. rom.seek(flat(bank["basebank"], bank["baseaddr"])) for j in range(i): if read_ptr == PTR.unpack(rom.read(2))[0]: #Aliased pointer! csvrow.append("<ALIAS ROW 0x{0:x}>".format(j)) wikitext.append("|«ALIAS ROW 0x{0:x}»".format(j)) print( "Pointer at 0x{0:x} fully aliases pointer 0x{1:x}". format( flat(bank["basebank"], bank["baseaddr"] + i * 2), flat(bank["basebank"], bank["baseaddr"] + j * 2))) break else: #Second, we try to see if this pointer is in the middle of #the last string. #This alias detection breaks when the previous row uses the #overflow code, so disable it if so. if i > 0 and read_ptr < last_end - 1 and not redirected: print( "Pointer at 0x{0:x} partially aliases previous pointer" .format(rom.tell() - 2)) csvrow.append( "<ALIAS ROW 0x{0:x} INTO 0x{1:x}>".format( last_nonaliasing_row, read_ptr - last_start)) wikitext.append( "|«ALIAS ROW 0x{0:x} INTO 0x{1:x}»".format( last_nonaliasing_row, read_ptr - last_start)) continue read_length = 1 first_read = True rom.seek(flat(bank["basebank"], read_ptr)) #Now we can initialize these... redirected = False old_loc = None while (rom.tell() % 0x4000 < 0x3FFF or rom.tell() == flat( bank["basebank"], bank["baseaddr"])): next_chara = CHARA.unpack(rom.read(1))[0] while (rom.tell() % 0x4000 < 0x3FFF or rom.tell() == flat(bank["basebank"], bank["baseaddr"])) and ( read_length <= expected_length or first_read or redirected ) and next_chara != 0xE0: #E0 is end-of-string if next_chara < 0xE0 and next_chara in charmap[ 1]: #Control codes are the E0 block string.append(charmap[1][next_chara]) elif next_chara in reverse_specials and specials[ reverse_specials[next_chara]].redirect: #Redirecting opcodes are transparently removed from the extracted text. this_special = specials[ reverse_specials[next_chara]] if this_special.bts: read_length += this_special.bts fmt = "<" + ("", "B", "H")[this_special.bts] word = struct.unpack( fmt, rom.read(this_special.bts))[0] if word < 0x4000 or word > 0x7FFF: #Overflowing into RAM is illegal - use the jump opcode. #Overflowing into ROM0 is technically not illegal, but #unorthodox enough that we're going to disallow it. string.append( format_literal(this_special.byte)) string.append( format_literal( word & 0xFF, charmap[1])) string.append( format_literal( word >> 8, charmap[1])) else: #We need to do this right now to avoid breaking hole detection old_loc = rom.tell() read_length = rom.tell() - flat( bank["basebank"], read_ptr) rom.seek(flat(args.overflow_bank, word)) redirected = True else: raise RuntimeError( "Invalid specials dictionary. Redirecting special character is missing bts." ) elif next_chara in reverse_specials: #This must be the work of an 「ENEMY STAND」 this_special = specials[ reverse_specials[next_chara]] if this_special.bts: read_length += this_special.bts fmt = "<" + ("", "B", "H")[this_special.bts] word = struct.unpack( fmt, rom.read(this_special.bts))[0] string.append( format_control_code( reverse_specials[next_chara], word)) else: string.append( format_control_code( reverse_specials[next_chara])) if this_special.end: first_read = False break #elif next_chara == 0xE2: #Literal newline # string.append(u"\n") else: #Literal specials string.append(format_literal(next_chara)) next_chara = CHARA.unpack(rom.read(1))[0] #Explicitly stop updating read_length if the #overflow opcode is used. Otherwise we'd think we #read thousands or negative thousands of chars if not redirected: read_length = rom.tell() - flat( bank["basebank"], read_ptr) #After the main extraction loop if read_length >= expected_length: break else: #Detect nulls (spaces) after the end of a string #and append them to avoid creating a new pointer row loc = rom.tell() if redirected: loc = old_loc while CHARA.unpack(rom.read(1))[0] == charmap[0][ " "] and read_length < expected_length: string.append(" ") loc += 1 read_length += 1 rom.seek(loc) #cleanup if read_length >= expected_length: break else: #There's a hole in the ROM! #Disassemble the next string. print("Inaccessible data found at 0x{0:x}". format(flat(bank["basebank"], read_ptr))) csvrow.append("".join(string)) wikitext.append("|" + "".join(string)) string = [] csvdata.append(csvrow) csvrow = ["(No pointer)"] wikitext.append("|-") wikitext.append("|(No pointer)") read_length += 1 csvrow.append("".join(string)) wikitext.append("|" + "".join(string)) string = [] #Store the actual end pointer for later use. last_start = read_ptr last_end = read_ptr + read_length last_nonaliasing_row = i csvdata.append(csvrow) wikitext.append("|-") wikitext.append("|}") wikitext = "\n".join(wikitext) wikidir = os.path.join(args.input, bank["basedir"]) wikipath = os.path.join(args.input, bank["legacy_filename"]) csvpath = os.path.join(args.input, bank["filename"]) install_path(wikidir) #with open(wikipath, "w+", encoding="utf-8") as bank_wikitext: #bank_wikitext.write(wikitext) with open(csvpath, "w+", encoding="utf-8") as bank_csvtext: csvwriter = csv.writer(bank_csvtext) for csvrow in csvdata: csvwriter.writerow(csvrow)