diff --git a/ff5reader.py b/ff5reader.py old mode 100644 new mode 100755 index 12229e7..6507ad9 --- a/ff5reader.py +++ b/ff5reader.py @@ -1,4 +1,4 @@ -#!python3 -i +#!/usr/bin/python3 -i ''' No license for now ''' @@ -13,9 +13,10 @@ import const import time pyqt_version = 0 -skip_pyqt5 = "PYQT4" in os.environ -filename_en = "Final Fantasy V (Japan) [En by RPGe v1.1].sfc" -filename_jp = "Final Fantasy V (Japan).sfc" +skip_pyqt5 = 'PYQT4' in os.environ +filename_en = 'Final Fantasy V (Japan) [En by RPGe v1.1].sfc' +filename_jp = 'Final Fantasy V (Japan).sfc' +filename_jp_ff4 = 'Final Fantasy IV (Japan) (Rev A).sfc' if not skip_pyqt5: try: @@ -39,7 +40,7 @@ if not skip_pyqt5: pyqt_version = 5 except ImportError: print("Couldn't import Qt5 dependencies. " - "Make sure you installed the PyQt5 package.") + 'Make sure you installed the PyQt5 package.') if pyqt_version == 0: try: import sip @@ -66,18 +67,20 @@ if pyqt_version == 0: pyqt_version = 4 except ImportError: print("Couldn't import Qt dependencies. " - "Make sure you installed the PyQt4 package.") + 'Make sure you installed the PyQt4 package.') sys.exit(-1) bg_color = QColor(0, 0, 128) bg_trans = QColor(0, 0, 0, 0) +HEX_PREFIX = '#' # '$' or '0x' are also nice + monofont = QFont() monofont.setStyleHint(QFont.Monospace) if not monofont.fixedPitch(): monofont.setStyleHint(QFont.TypeWriter) if not monofont.fixedPitch(): - monofont.setFamily("Monospace") + monofont.setFamily('Monospace') def divceil(numerator, denominator): # Reverse floor division for ceil @@ -86,439 +89,469 @@ def divceil(numerator, denominator): def hex_length(i): return divceil(i.bit_length(), 4) -with open(filename_en, 'rb') as file1: - ROM_en = file1.read() +with open(filename_en, 'rb') as file: + ROM_en = file.read() print(len(ROM_en), filename_en) -with open(filename_jp, 'rb') as file2: - ROM_jp = file2.read() +with open(filename_jp, 'rb') as file: + ROM_jp = file.read() +print(len(ROM_jp), filename_jp) +with open(filename_jp_ff4, 'rb') as file: + ROM_FF4jp = file.read() print(len(ROM_jp), filename_jp) -stringlist_headers = ["Address", "ID", "Name"] -imglist_headers = stringlist_headers + ["Img", "Name JP", "Img JP"] +stringlist_headers = ['Address', 'ID', 'Name'] +imglist_headers = stringlist_headers + ['Img', 'Name JP', 'Img JP'] class FF5Reader(QMainWindow): - """ - Main GUI class - """ - def __init__(self): - QMainWindow.__init__(self, None) - global glyph_sprites_en_large, glyph_sprites_en_small, glyph_sprites_jp_small, glyph_sprites_jp_large, glyph_sprites_kanji, glyph_sprites_jp_dialogue - glyph_sprites_en_small = generate_glyphs(ROM_en, 0x11F000) - glyph_sprites_en_large = generate_glyphs_large(ROM_en, 0x03E800) - glyph_sprites_jp_small = generate_glyphs(ROM_jp, 0x11F000) - glyph_sprites_jp_large = generate_glyphs_large(ROM_jp, 0x03E800) - glyph_sprites_kanji = generate_glyphs_large(ROM_jp, 0x1BD000, 0x1AA) # Kanji are unchanged in EN version + """ + Main GUI class + """ + def __init__(self): + QMainWindow.__init__(self, None) + global glyph_sprites_en_large, glyph_sprites_en_small, glyph_sprites_jp_small, glyph_sprites_jp_large, glyph_sprites_kanji, glyph_sprites_jp_dialogue + glyph_sprites_en_small = generate_glyphs(ROM_en, 0x11F000) + glyph_sprites_en_large = generate_glyphs_large(ROM_en, 0x03E800) + glyph_sprites_jp_small = generate_glyphs(ROM_jp, 0x11F000) + glyph_sprites_jp_large = generate_glyphs_large(ROM_jp, 0x03E800) + glyph_sprites_kanji = generate_glyphs_large(ROM_jp, 0x1BD000, 0x1AA) # Kanji are unchanged in EN version - global zone_names - zone_names = make_string_img_list(0x107000, 2, 0x100, start_str=0x270000, start_jp_str=0x107200, indirect=True, large=True) - items = make_string_img_list(0x111380, 9, 256) - magics = make_string_img_list(0x111C80, 6, 87) - more_magics = make_string_img_list(0x111E8A, 9, 73) - enemy_names = make_string_img_list(0x200050, 10, 0x180, 0x105C00, 8) - character_names = make_string_img_list(0x115500, 6, 5) - job_names = make_string_img_list(0x115600, 8, 22) - ability_names = make_string_img_list(0x116200, 8, 33) - battle_commands = make_string_img_list(0x201150, 7, 0x60, 0x115800, 5) - dialogue = make_string_img_list(0x2013F0, 3, 0x900, start_jp=0x082220, len_jp=2, start_str=0x0, start_jp_str=0x0A0000, indirect=True, large=True, macros_en=const.Dialogue_Macros_EN, macros_jp=const.Dialogue_Macros_JP) + global zone_names + zone_names = make_string_img_list(0x107000, 2, 0x100, start_str=0x270000, start_jp_str=0x107200, indirect=True, large=True) + items = make_string_img_list(0x111380, 9, 256) + magics = make_string_img_list(0x111C80, 6, 87) + more_magics = make_string_img_list(0x111E8A, 9, 73) + enemy_names = make_string_img_list(0x200050, 10, 0x180, 0x105C00, 8) + character_names = make_string_img_list(0x115500, 6, 5) + job_names = make_string_img_list(0x115600, 8, 22) + ability_names = make_string_img_list(0x116200, 8, 33) + battle_commands = make_string_img_list(0x201150, 7, 0x60, 0x115800, 5) + dialogue = make_string_img_list(0x2013F0, 3, 0x900, start_jp=0x082220, len_jp=2, start_str=0x0, start_jp_str=0x0A0000, indirect=True, large=True, macros_en=const.Dialogue_Macros_EN, macros_jp=const.Dialogue_Macros_JP) - zone_structure = [("NPC Layer", 2, None), - ("Name", 1, [z[2] for z in zone_names]), - ("ShadowFlags", 1, None), - ("0x04", 1, None), - ("0x05", 1, None), - ("Flags 0x06", 1, None), - ("0x07", 1, None), - ("Tileset", 1, None), - ("Tileset2", 2, None), - #("0x0A", 1, None), - ("0x0B", 1, None), - ("Collision Layer",1, None), - ("0x0D", 1, None), - ("0x0E", 1, None), - ("0x0F", 1, None), - ("0x10", 1, None), - ("0x11", 1, None), - ("0x12", 1, None), - ("0x13", 1, None), - ("0x14", 1, None), - ("0x15", 1, None), - ("Palette", 1, None), - ("0x17", 1, None), - ("0x18", 1, None), - ("Music", 1, const.BGM_Tracks)] - zone_headers = ["Address"] + [z[0] for z in zone_structure] + zone_structure = [('NPC Layer', 2, None), + ('Name', 1, [z[2] for z in zone_names]), + ('ShadowFlags', 1, None), + (hex(4, 2), 1, None), + (hex(5, 2), 1, None), + ('Flags '+hex(6,2),1, None), + (hex(7, 2), 1, None), + ('Tileset', 1, None), + ('Tileset2', 2, None), + #('0x0A', 1, None), + (hex(11, 2), 1, None), + ('Collision Layer',1, None), + (hex(13, 2), 1, None), + (hex(14, 2), 1, None), + (hex(15, 2), 1, None), + (hex(16, 2), 1, None), + (hex(17, 2), 1, None), + (hex(18, 2), 1, None), + (hex(19, 2), 1, None), + (hex(20, 2), 1, None), + (hex(21, 2), 1, None), + ('Palette', 1, None), + (hex(23, 2), 1, None), + (hex(24, 2), 1, None), + ('Music', 1, const.BGM_Tracks)] + zone_headers = ['Address'] + [z[0] for z in zone_structure] - zone_data = [] - for i in range(const.zone_count): - offset = 0x0E9C00 + (i*0x1A) - zone_data.append(parse_struct(ROM_en, offset, zone_structure)) + zone_data = [] + for i in range(const.zone_count): + offset = 0x0E9C00 + (i*0x1A) + zone_data.append(parse_struct(ROM_en, offset, zone_structure)) - tileset_headers = ("ID", "Offset", "Pointer", "Expected Length") - tileset_data = [] - for i in range(0x1C): - offset = 0x0F0000 + (i*2) - pointer = 0x0F0000 + int.from_bytes(ROM_en[offset:offset+2],'little') - length = int.from_bytes(ROM_en[offset+2:offset+4],'little') - int.from_bytes(ROM_en[offset:offset+2],'little') - tileset_data.append(('0x{:02X}'.format(i), '0x{:06X}'.format(offset), - '0x{:06X}'.format(pointer), '0x{:04X}'.format(length))) + tileset_headers = ("ID", "Offset", "Pointer", "Expected Length") + tileset_data = [] + for i in range(0x1C): + offset = 0x0F0000 + (i*2) + pointer = 0x0F0000 + int.from_bytes(ROM_en[offset:offset+2],'little') + length = int.from_bytes(ROM_en[offset+2:offset+4],'little') - int.from_bytes(ROM_en[offset:offset+2],'little') + tileset_data.append((hex(i, 2), hex(offset, 6), hex(pointer, 6), hex(length, 4))) - npc_layers = [] - offset = 0x0E59C0 - for layer in range(const.npc_layer_count): - i = offset + (layer*2) - start = int.from_bytes(ROM_en[i:i+2],'little') + offset - next = int.from_bytes(ROM_en[i+2:i+4],'little') + offset - npcs = (next - start) // 7 - for npc in range(npcs): - address = start + (npc*7) - npc_layers.append(["0x{0:06X}".format(i), "0x{0:03X}".format(layer)] + parse_struct(ROM_en, address, const.npc_layer_structure)) + npc_layers = [] + offset = 0x0E59C0 + for layer in range(const.npc_layer_count): + i = offset + (layer*2) + start = int.from_bytes(ROM_en[i:i+2],'little') + offset + next = int.from_bytes(ROM_en[i+2:i+4],'little') + offset + npcs = (next - start) // 7 + for npc in range(npcs): + address = start + (npc*7) + npc_layers.append([hex(i, 6), hex(layer, 3)] + parse_struct(ROM_en, address, const.npc_layer_structure)) - enemy_tile_layouts = [] - address = 0x10D004 - for i in range(0x66): - offset = address + (i*8) - img = QImage(8, 8, QImage.Format_Mono) - img.setColorTable(const.mono_palette) - for i in range(8): - ptr = img.scanLine(i) - ptr.setsize(32) - ptr[0:1] = ROM_en[offset+i:offset+i+1] - pixmap = QPixmap.fromImage(img) - enemy_tile_layouts.append(pixmap.scaled(16, 16)) + #enemy_tile_layouts = [] + #address = 0x10D004 + #for i in range(0x66): + #offset = address + (i*8) + #img = QImage(8, 8, QImage.Format_Mono) + #img.setColorTable(const.mono_palette) + #for i in range(8): + #ptr = img.scanLine(i) + #ptr.setsize(32) + #ptr[0:1] = ROM_en[offset+i:offset+i+1] + #pixmap = QPixmap.fromImage(img) + #enemy_tile_layouts.append(pixmap.scaled(16, 16)) - enemy_sprite_data = [] - enemy_sprite_structure = [ - ("Sprite data offset", 2, None), - ("Multiple things", 2, None), - ("Tile Layout ID", 1, enemy_tile_layouts) - ] - enemy_sprite_headers = ["Address"]+[i[0] for i in enemy_sprite_structure]+["EN Name","EN Name"] - address = 0x14B180 - for i in range(0x180): - enemy_sprite_data.append(parse_struct(ROM_en, address + (i*5), enemy_sprite_structure) + enemy_names[i][2:4]) + enemy_sprite_data = [] + enemy_sprite_structure = [ + ('Sprite data offset', 2, None), + ('Multiple things', 2, None), + ('Tile Layout ID', 1, None) + ] + enemy_sprite_headers = ['Address']+[i[0] for i in enemy_sprite_structure]+['EN Name','EN Name'] + address = 0x14B180 + for i in range(0x180): + enemy_sprite_data.append(parse_struct(ROM_en, address + (i*5), enemy_sprite_structure) + enemy_names[i][2:4]) - self.battle_strips = make_character_battle_sprites(ROM_en) - status_strips = make_character_status_sprites(ROM_en) - enemy_sprites = make_enemy_sprites(ROM_en) + self.battle_strips = make_character_battle_sprites(ROM_en) + status_strips = make_character_status_sprites(ROM_en) + enemy_sprites = make_enemy_sprites(ROM_en) + self.battle_strips_ff4 = make_character_battle_sprites_ff4(ROM_FF4jp) - self.tabwidget = QTabWidget() - strings_tab = QTabWidget() - structs_tab = QTabWidget() - sprites_tab = QTabWidget() - self.tabwidget.addTab(strings_tab, "Strings") - self.tabwidget.addTab(structs_tab, "Structs") - self.tabwidget.addTab(sprites_tab, "Images") + enemy_sprites_named = [stack_labels(s, d[-2]) for s, d in zip(enemy_sprites, enemy_sprite_data)] - sprites_tab.addTab(make_pixmap_table(glyph_sprites_en_small, scale=4), "Glyphs (EN)") - sprites_tab.addTab(make_pixmap_table(glyph_sprites_en_large, scale=2), "Glyphs (Dialogue EN)") - sprites_tab.addTab(make_pixmap_table(glyph_sprites_jp_small, scale=4), "Glyphs (JP)") - sprites_tab.addTab(make_pixmap_table(glyph_sprites_jp_large, scale=2), "Glyphs (Large JP)") - sprites_tab.addTab(make_pixmap_table(glyph_sprites_kanji, scale=2), "Glyphs (Kanji)") - sprites_tab.addTab(make_pixmap_table(self.battle_strips, cols=22, scale=2), "Character Battle Sprites") - sprites_tab.addTab(make_pixmap_table(status_strips, cols=22, scale=2), "Status Sprites") - sprites_tab.addTab(make_pixmap_table(enemy_sprites, scale=1), "Enemy Sprites") + self.tabwidget = QTabWidget() + strings_tab = QTabWidget() + structs_tab = QTabWidget() + sprites_tab = QTabWidget() + self.tabwidget.addTab(strings_tab, 'Strings') + self.tabwidget.addTab(structs_tab, 'Structs') + self.tabwidget.addTab(sprites_tab, 'Images') - structs_tab.addTab(make_table(zone_headers, zone_data, True), "Zones") - structs_tab.addTab(make_table(tileset_headers, tileset_data, True), "Tilesets") - structs_tab.addTab(make_table(const.npc_layer_headers, npc_layers, True), "NPC Layers") - structs_tab.addTab(make_table(enemy_sprite_headers, enemy_sprite_data, True), "Enemy Sprites") + sprites_tab.addTab(make_pixmap_table(glyph_sprites_en_small, scale=4), 'Glyphs (EN)') + sprites_tab.addTab(make_pixmap_table(glyph_sprites_en_large, scale=2), 'Glyphs (Dialogue EN)') + sprites_tab.addTab(make_pixmap_table(glyph_sprites_jp_small, scale=4), 'Glyphs (JP)') + sprites_tab.addTab(make_pixmap_table(glyph_sprites_jp_large, scale=2), 'Glyphs (Large JP)') + sprites_tab.addTab(make_pixmap_table(glyph_sprites_kanji, scale=2), 'Glyphs (Kanji)') + sprites_tab.addTab(make_pixmap_table(self.battle_strips, cols=22, scale=2), 'Character Battle Sprites') + sprites_tab.addTab(make_pixmap_table(status_strips, cols=22, scale=2), 'Status Sprites') + #sprites_tab.addTab(make_pixmap_table(enemy_sprites, scale=1), 'Enemy Sprites') + sprites_tab.addTab(make_pixmap_table(enemy_sprites_named, cols=32, scale=1), 'Enemy Sprites') + sprites_tab.addTab(make_pixmap_table(self.battle_strips_ff4, cols=16, scale=2), 'FF4 Character Battle Sprites') - strings_tab.addTab(make_table(imglist_headers, items, row_labels=False), "Items") - strings_tab.addTab(make_table(imglist_headers, magics, row_labels=False), "Magics") - strings_tab.addTab(make_table(imglist_headers, more_magics, row_labels=False), "More Magics") - strings_tab.addTab(make_table(imglist_headers, enemy_names, row_labels=False), "Enemy Names") - strings_tab.addTab(make_table(imglist_headers, character_names, row_labels=False), "Character Names") - strings_tab.addTab(make_table(imglist_headers, job_names, row_labels=False), "Job Names") - strings_tab.addTab(make_table(imglist_headers, ability_names, row_labels=False), "Ability Names") - strings_tab.addTab(make_table(imglist_headers, battle_commands, row_labels=False), "Battle Commands") - strings_tab.addTab(make_table(imglist_headers, zone_names, True, scale=1), "Zone Names") - strings_tab.addTab(make_table(imglist_headers+['JP address'], dialogue, scale=1), "Dialogue") - self.string_decoder = QWidget() - self.decoder_input = QLineEdit() - self.decoder_input.returnPressed.connect(self._string_decode) - self.decoder_layout = QVBoxLayout() - self.decoder_layout.addWidget(self.decoder_input) - self.string_decoder.setLayout(self.decoder_layout) - strings_tab.addTab(self.string_decoder, "String Decoder") + structs_tab.addTab(make_table(zone_headers, zone_data, True), 'Zones') + structs_tab.addTab(make_table(tileset_headers, tileset_data, True), 'Tilesets') + structs_tab.addTab(make_table(const.npc_layer_headers, npc_layers, True), 'NPC Layers') + structs_tab.addTab(make_table(enemy_sprite_headers, enemy_sprite_data, True), 'Enemy Sprites') - layout = QHBoxLayout() - layout.addWidget(self.tabwidget) - self.main_widget = QWidget(self) - self.main_widget.setLayout(layout) - self.main_widget.setMinimumSize(800,600) - self.setCentralWidget(self.main_widget) - self.show() + strings_tab.addTab(make_table(imglist_headers, items, row_labels=False), 'Items') + strings_tab.addTab(make_table(imglist_headers, magics, row_labels=False), 'Magics') + strings_tab.addTab(make_table(imglist_headers, more_magics, row_labels=False), 'More Magics') + strings_tab.addTab(make_table(imglist_headers, enemy_names, row_labels=False), 'Enemy Names') + strings_tab.addTab(make_table(imglist_headers, character_names, row_labels=False), 'Character Names') + strings_tab.addTab(make_table(imglist_headers, job_names, row_labels=False), 'Job Names') + strings_tab.addTab(make_table(imglist_headers, ability_names, row_labels=False), 'Ability Names') + strings_tab.addTab(make_table(imglist_headers, battle_commands, row_labels=False), 'Battle Commands') + strings_tab.addTab(make_table(imglist_headers, zone_names, True, scale=1), 'Zone Names') + strings_tab.addTab(make_table(imglist_headers+['JP address'], dialogue, scale=1), 'Dialogue') - def _string_decode(self): - string = ''.join(self.decoder_input.text().split()) - if len(string) % 1: - string += '0' - bytelist = [int(string[i:i+2], 16) for i in range(0, len(string), 2)] - tups = make_string_img_small(bytes(bytelist)) - print(tups[0]) - img = QLabel() - img.setPixmap(tups[1]) - self.decoder_layout.addWidget(img) - self.decoder_input.setText('') + self.string_decoder = QWidget() + self.decoder_input = QLineEdit() + self.decoder_input.returnPressed.connect(self._string_decode) + self.decoder_layout = QVBoxLayout() + self.decoder_layout.addWidget(self.decoder_input) + self.string_decoder.setLayout(self.decoder_layout) + strings_tab.addTab(self.string_decoder, 'String Decoder') + + layout = QHBoxLayout() + layout.addWidget(self.tabwidget) + self.main_widget = QWidget(self) + self.main_widget.setLayout(layout) + self.main_widget.setMinimumSize(800,600) + self.setCentralWidget(self.main_widget) + self.show() + + def _string_decode(self): + string = ''.join(self.decoder_input.text().split()) + if len(string) % 1: + string += '0' + bytelist = [int(string[i:i+2], 16) for i in range(0, len(string), 2)] + tups = make_string_img_small(bytes(bytelist)) + print(tups[0]) + img = QLabel() + img.setPixmap(tups[1]) + self.decoder_layout.addWidget(img) + self.decoder_input.setText('') class Canvas: - def __init__(self, rows, columns, color=bg_trans): - self.image = QImage(8*rows, 8*columns, QImage.Format_ARGB32_Premultiplied) - self.image.fill(color) - self.painter = QtGui.QPainter(self.image) - self.max_x = 1 - self.max_y = 1 + def __init__(self, cols, rows, color=bg_trans): + self.image = QImage(8*cols, 8*rows, QImage.Format_ARGB32_Premultiplied) + self.image.fill(color) + self.painter = QtGui.QPainter(self.image) + self.max_x = 1 + self.max_y = 1 - def __del__(self): - del self.painter + def __del__(self): + del self.painter - def draw_pixmap(self, row, column, pixmap): - self.painter.drawPixmap(row*8, column*8, pixmap) - if row > self.max_x: - self.max_x = row - if column > self.max_y: - self.max_y = column + def draw_pixmap(self, row, column, pixmap): + self.painter.drawPixmap(row*8, column*8, pixmap) + if row > self.max_x: + self.max_x = row + if column > self.max_y: + self.max_y = column - def pixmap(self, trim=False): - if trim: - return QPixmap.fromImage(self.image.copy(0, 0, self.max_x*8+8, self.max_y*8+8)) - return QPixmap.fromImage(self.image) + def pixmap(self, trim=False): + if trim: + return QPixmap.fromImage(self.image.copy(0, 0, self.max_x*8+8, self.max_y*8+8)) + return QPixmap.fromImage(self.image) def parse_struct(rom, offset, structure): - out = ["0x{:06X}".format(offset)] - j = 0 - for z in structure: - val = int.from_bytes(rom[offset+j:offset+j+z[1]],'little') - if z[2] and val < len(z[2]): - out.append(z[2][val]) - else: - out.append("0x{:0{}X}".format(val, z[1]*2)) - j += z[1] - return out + out = [hex(offset, 6)] + j = 0 + for z in structure: + val = int.from_bytes(rom[offset+j:offset+j+z[1]],'little') + if z[2] and val < len(z[2]): + out.append(z[2][val]) + else: + out.append(hex(val, z[1]*2)) + j += z[1] + return out def make_enemy_sprites(rom): - sprites = [] - for e_id in range(0, 0x180*5, 5): - triplane = bool(rom[0x14B180+e_id]&0x80) # True if 3 planes, False if 4 - bytes_per_tile = 24 if triplane else 32 - tile_offset = ((((rom[0x14B180+e_id]&0x7F)<<8)| rom[0x14B181+e_id]) << 3) + 0x150000 # For whatever reason this is big endian - pal_offset = ((((rom[0x14B182+e_id]&0x03)<<8)| rom[0x14B183+e_id]) << 4) + 0x0ED000 # For whatever reason this is big endian - pal_size = 16 if triplane else 32 - palette = generate_palette(rom, pal_offset, pal_size) - palette[0] = 0 - layout_id = rom[0x14B184+e_id] - boss_layout = bool(rom[0x14B182+e_id]&0x80) - if boss_layout: - layout = rom[0x10D334+(layout_id<<5):0x10D334+(layout_id<<5)+32] - sprite = Canvas(16, 16) - for x, y in [(x,y) for y in range(16) for x in range(16)]: - if (int.from_bytes(layout[y*2:y*2+2], 'little') & (0x8000 >> x)): - sprite.draw_pixmap(x, y, create_tile(rom[tile_offset:tile_offset+bytes_per_tile], palette)) - tile_offset += bytes_per_tile - else: - layout = rom[0x10D004+(layout_id<<3):0x10D004+(layout_id<<3)+8] - sprite = Canvas(8, 8) - for x, y in [(x,y) for y in range(8) for x in range(8)]: - if (layout[y] & (0x80 >> x)): - sprite.draw_pixmap(x, y, create_tile(rom[tile_offset:tile_offset+bytes_per_tile], palette)) - tile_offset += bytes_per_tile + sprites = [] + for e_id in range(0, 0x180*5, 5): + triplane = bool(rom[0x14B180+e_id]&0x80) # True if 3 planes, False if 4 + bytes_per_tile = 24 if triplane else 32 + tile_offset = ((((rom[0x14B180+e_id]&0x7F)<<8)| rom[0x14B181+e_id]) << 3) + 0x150000 # For whatever reason this is big endian + pal_offset = ((((rom[0x14B182+e_id]&0x03)<<8)| rom[0x14B183+e_id]) << 4) + 0x0ED000 # For whatever reason this is big endian + pal_size = 16 if triplane else 32 + palette = generate_palette(rom, pal_offset, pal_size) + palette[0] = 0 + layout_id = rom[0x14B184+e_id] + boss_layout = bool(rom[0x14B182+e_id]&0x80) + if boss_layout: + layout = rom[0x10D334+(layout_id<<5):0x10D334+(layout_id<<5)+32] + sprite = Canvas(16, 16) + for x, y in [(x,y) for y in range(16) for x in range(16)]: + if (int.from_bytes(layout[y*2:y*2+2], 'little') & (0x8000 >> x)): + sprite.draw_pixmap(x, y, create_tile(rom[tile_offset:tile_offset+bytes_per_tile], palette)) + tile_offset += bytes_per_tile + else: + layout = rom[0x10D004+(layout_id<<3):0x10D004+(layout_id<<3)+8] + sprite = Canvas(8, 8) + for x, y in [(x,y) for y in range(8) for x in range(8)]: + if (layout[y] & (0x80 >> x)): + sprite.draw_pixmap(x, y, create_tile(rom[tile_offset:tile_offset+bytes_per_tile], palette)) + tile_offset += bytes_per_tile - # TODO: Shadow stuff - sprites.append(sprite.pixmap(True)) - return sprites + # TODO: Shadow stuff + sprites.append(sprite.pixmap(True)) + return sprites + + +def make_battle_strip(rom, palette_address, tile_address, num_tiles): + palette = generate_palette(rom, palette_address) + # We don't want the background drawn, so we'll make that colour transparent + palette[0] = 0 + battle_strip = Canvas(2, divceil(num_tiles, 2)) # KO sprites are here which means more tiles than FFV + for j in range(num_tiles): + offset = tile_address+(j*32) + battle_strip.draw_pixmap(j%2, j//2, create_tile(rom[offset:offset+32], palette)) + return battle_strip.pixmap() + + +def make_character_battle_sprites_ff4(rom): + tile_address = 0x0D0000 + pig_tile_address = 0x0D7000 + golbez_tile_address = 0x0D7600 + anna_tile_address = 0x0D7960 + palette_address = 0x0E7D00 + golbez_palette_address = 0x0E7EC0 + anna_palette_address = 0x0E7EE0 + battle_strips = [] + for i in range(0, 14*32, 32): # 14 regular characters. Pig, Golbez and Anna follow with different tile spacing and palette order. + battle_strips.append(make_battle_strip(rom, palette_address+i, tile_address+(i*64), 64)) # KO sprites are here which means more tiles per strip than FFV + battle_strips.append(make_battle_strip(rom, golbez_palette_address, golbez_tile_address, 27)) + battle_strips.append(make_battle_strip(rom, anna_palette_address, anna_tile_address, 14)) + for i in range(0, 16*32, 32): # 16 pigs. + battle_strips.append(make_battle_strip(rom, palette_address+i, pig_tile_address, 48)) + return battle_strips def make_character_battle_sprites(rom): - tile_address = 0x120000 - palette_address = 0x14A3C0 - battle_strips = [] - for i in range(0, 110*32, 32): - palette = generate_palette(rom, palette_address + i) - # We don't want the background drawn, so we'll make that colour transparent - palette[0] = 0 - battle_strip = Canvas(2, 24) - for j in range(48): - offset = tile_address+(i*48)+(j*32) - battle_strip.draw_pixmap(j%2, j//2, create_tile(rom[offset:offset+32], palette)) - battle_strips.append(battle_strip.pixmap()) - return battle_strips + tile_address = 0x120000 + palette_address = 0x14A3C0 + battle_strips = [] + for i in range(0, (22*5)*32, 32): # 22 jobs 5 characters + battle_strips.append(make_battle_strip(rom, palette_address+i, tile_address+(i*48), 48)) + return battle_strips def make_character_status_sprites(rom): - tile_address = 0x149400 - palette_address = 0x14A660 - pixmaps = [] - for i in range(5): - palette = generate_palette(rom, palette_address + (i*22*32)) # Freelance palette per character - # We don't want the background drawn, so we'll make that colour transparent - palette[0] = 0 - wounded = Canvas(3, 2) - for j in range(6): - offset = tile_address+(i*192)+(j*32) - wounded.draw_pixmap(j%3, j//3, create_tile(rom[offset:offset+32], palette)) - pixmaps.append(wounded.pixmap()) - mini_strip = Canvas(2, 19) - for j in range(38): - offset = tile_address+0x3C0+(j*24) - mini_strip.draw_pixmap(j%2, j//2, create_tile(rom[offset:offset+24], palette)) - pixmaps.append(mini_strip.pixmap()) - frog_strip = Canvas(2, 15) - for j in range(30): - offset = tile_address+0x750+(j*24) - frog_strip.draw_pixmap(j%2, j//2, create_tile(rom[offset:offset+24], palette)) - pixmaps.append(frog_strip.pixmap()) - return pixmaps + tile_address = 0x149400 + palette_address = 0x14A660 + pixmaps = [] + for i in range(5): + palette = generate_palette(rom, palette_address + (i*22*32)) # Freelance palette per character + # We don't want the background drawn, so we'll make that colour transparent + palette[0] = 0 + wounded = Canvas(3, 2) + for j in range(6): + offset = tile_address+(i*192)+(j*32) + wounded.draw_pixmap(j%3, j//3, create_tile(rom[offset:offset+32], palette)) + pixmaps.append(wounded.pixmap()) + mini_strip = Canvas(2, 19) + for j in range(38): + offset = tile_address+0x3C0+(j*24) + mini_strip.draw_pixmap(j%2, j//2, create_tile(rom[offset:offset+24], palette)) + pixmaps.append(mini_strip.pixmap()) + frog_strip = Canvas(2, 15) + for j in range(30): + offset = tile_address+0x750+(j*24) + frog_strip.draw_pixmap(j%2, j//2, create_tile(rom[offset:offset+24], palette)) + pixmaps.append(frog_strip.pixmap()) + return pixmaps def make_string_img_small(bytestring, jp=False): - if len(bytestring) < 1: - raise ValueError('Empty bytestring was passed') - string = "" - img = QImage(len(bytestring)*8, 10, QImage.Format_RGB16) - img.fill(bg_color) - painter = QtGui.QPainter(img) - if jp: - for x, j in enumerate(bytestring): - string = string + const.Glyphs_JP2[j] - if 0x20 <= j < 0x52: - if j > 0x48: - painter.drawPixmap(x*8, 2, glyph_sprites_jp_small[j+0x17]) - painter.drawPixmap(x*8+1,-5, glyph_sprites_jp_small[0x52]) - else: - painter.drawPixmap(x*8, 2, glyph_sprites_jp_small[j+0x40]) - painter.drawPixmap(x*8+1,-6, glyph_sprites_jp_small[0x51]) - else: - painter.drawPixmap(x*8, 2, glyph_sprites_jp_small[j]) - else: - for x, j in enumerate(bytestring): - string = string + const.Glyphs[j] - painter.drawPixmap(x*8, 1, glyph_sprites_en_small[j]) - del painter - return string, QPixmap.fromImage(img) + if len(bytestring) < 1: + raise ValueError('Empty bytestring was passed') + string = "" + img = QImage(len(bytestring)*8, 10, QImage.Format_RGB16) + img.fill(bg_color) + painter = QtGui.QPainter(img) + if jp: + for x, j in enumerate(bytestring): + string = string + const.Glyphs_JP2[j] + if 0x20 <= j < 0x52: + if j > 0x48: + painter.drawPixmap(x*8, 2, glyph_sprites_jp_small[j+0x17]) + painter.drawPixmap(x*8+1,-5, glyph_sprites_jp_small[0x52]) + else: + painter.drawPixmap(x*8, 2, glyph_sprites_jp_small[j+0x40]) + painter.drawPixmap(x*8+1,-6, glyph_sprites_jp_small[0x51]) + else: + painter.drawPixmap(x*8, 2, glyph_sprites_jp_small[j]) + else: + for x, j in enumerate(bytestring): + string = string + const.Glyphs[j] + painter.drawPixmap(x*8, 1, glyph_sprites_en_small[j]) + del painter + return string, QPixmap.fromImage(img) def make_string_img_large(bytestring, macros=None, jp=False): - ''' - This is how we decipher dialogue data, which has multiple lines, macro expansions and kanji. - English characters have varying widths. In the japanese version, everything is fullwidth (16px) - Kanji aren't used in English dialogue but the cost is likely the same in checking either way. - ''' - if len(bytestring) < 1: - raise ValueError('Empty bytestring was passed') + ''' + This is how we decipher dialogue data, which has multiple lines, macro expansions and kanji. + English characters have varying widths. In the japanese version, everything is fullwidth (16px) + Kanji aren't used in English dialogue but the cost is likely the same in checking either way. + ''' + if len(bytestring) < 1: + raise ValueError('Empty bytestring was passed') - newstring = [] - bytes = iter(bytestring) - for b in bytes: - if b in const.DoubleChars: - b2 = next(bytes) - newstring.append((b<<8) + b2) - elif macros and b in macros: - newstring.extend(macros[b]) - else: - newstring.append(b) + newstring = [] + bytes = iter(bytestring) + for b in bytes: + if b in const.DoubleChars: + b2 = next(bytes) + newstring.append((b<<8) + b2) + elif macros and b in macros: + newstring.extend(macros[b]) + else: + newstring.append(b) - string = "" - # Because the length of the input has little bearing on the size of the image thanks to linebreaks and macros, we overprovision then clip away. - max_width = 256 # This seems to check out, but the EN dialogue has linebreaks virtually everywhere anyway - max_height = 1024 # I've seen up to 58 rows in EN, 36 in JP. Stay safe. - img = QImage(max_width, max_height, QImage.Format_RGB16) - img.fill(bg_color) - painter = QtGui.QPainter(img) + string = "" + # Because the length of the input has little bearing on the size of the image thanks to linebreaks and macros, we overprovision then clip away. + max_width = 256 # This seems to check out, but the EN dialogue has linebreaks virtually everywhere anyway + max_height = 1024 # I've seen up to 58 rows in EN, 36 in JP. Stay safe. + img = QImage(max_width, max_height, QImage.Format_RGB16) + img.fill(bg_color) + painter = QtGui.QPainter(img) - x = xmax = y = 0 - for j in newstring: - if x >= max_width: # Wrap on long line - string += '[wr]\n' - xmax = max_width # Can't go higher than this anyway - x = 0 - y += 16 - if j == 0x01: # Line break - string += '[br]\n' - xmax = x if x > xmax else xmax - x = 0 - y += 16 - elif 0x1E00 <= j < 0x1FAA: # Kanji live in this range - string += const.Glyphs_Kanji[j-0x1E00] - painter.drawPixmap(x, y+2, glyph_sprites_kanji[j-0x1E00]) - x += 16 - elif j < 0x13 or j > 0xFF: # Everything remaining outside this is a control char - string += '[0x{:02X}]'.format(j) - else: - if jp: - string += const.Glyphs_JP_large[j] - painter.drawPixmap(x, y+2, glyph_sprites_jp_large[j]) - x += 16 - else: - string += const.Glyphs[j] - painter.drawPixmap(x, y+4, glyph_sprites_en_large[j]) - x += const.Dialogue_Width[j] - del painter - xmax = x if x > xmax else xmax - return string, QPixmap.fromImage(img.copy(0, 0, xmax, y+16)) + x = xmax = y = 0 + for j in newstring: + if x >= max_width: # Wrap on long line + string += '[wr]\n' + xmax = max_width # Can't go higher than this anyway + x = 0 + y += 16 + if j == 0x01: # Line break + string += '[br]\n' + xmax = x if x > xmax else xmax + x = 0 + y += 16 + elif 0x1E00 <= j < 0x1FAA: # Kanji live in this range + string += const.Glyphs_Kanji[j-0x1E00] + painter.drawPixmap(x, y+2, glyph_sprites_kanji[j-0x1E00]) + x += 16 + elif j < 0x13 or j > 0xFF: # Everything remaining outside this is a control char + string += '[0x{:02X}]'.format(j) + else: + if jp: + string += const.Glyphs_JP_large[j] + painter.drawPixmap(x, y+2, glyph_sprites_jp_large[j]) + x += 16 + else: + string += const.Glyphs[j] + painter.drawPixmap(x, y+4, glyph_sprites_en_large[j]) + x += const.Dialogue_Width[j] + del painter + xmax = x if x > xmax else xmax + return string, QPixmap.fromImage(img.copy(0, 0, xmax, y+16)) def make_string_img_list(start, length, num, start_jp=None, len_jp=None, start_str=None, start_jp_str=None, indirect=False, large=False, macros_en=None, macros_jp=None): - start_jp = start if start_jp is None else start_jp - len_jp = length if len_jp is None else len_jp - start_str = start if start_str is None else start_str - start_jp_str = start_str if start_jp_str is None else start_jp_str - stringlist = [] - id_digits = hex_length(num-1) + start_jp = start if start_jp is None else start_jp + len_jp = length if len_jp is None else len_jp + start_str = start if start_str is None else start_str + start_jp_str = start_str if start_jp_str is None else start_jp_str + stringlist = [] + id_digits = hex_length(num-1) - if indirect: - for id in range(num): - en = start + (id*length) - jp = start_jp + (id*len_jp) - en_start = int.from_bytes(ROM_en[en:en+length],'little') + start_str - en_end = int.from_bytes(ROM_en[en+length:en+(length*2)],'little') + start_str - if en_start >= 0xC00000: # SNES memory space has the ROM starting at 0xC00000 in HiROM mode. - en_start -= 0xC00000 - en_end -= 0xC00000 - jp_start = int.from_bytes(ROM_jp[jp:jp+len_jp],'little') + start_jp_str - jp_end = int.from_bytes(ROM_jp[jp+len_jp:jp+(len_jp*2)],'little') + start_jp_str - if jp_start >= 0xC00000: # SNES memory space has the ROM starting at 0xC00000 in HiROM mode. - jp_start -= 0xC00000 - jp_end -= 0xC00000 - if (en_end == start_str) or (jp_end == start_jp_str): - break - try: # When dealing with pointer redirection we might end up passing empty strings - if large: - str_en, img_en = make_string_img_large(ROM_en[en_start:en_end], macros_en) - else: - str_en, img_en = make_string_img_small(ROM_en[en_start:en_end]) - except ValueError: - str_en = '' - img_en = None - try: - if large: - str_jp, img_jp = make_string_img_large(ROM_jp[jp_start:jp_end], macros_jp, jp=True) - else: - str_jp, img_jp = make_string_img_small(ROM_jp[jp_start:jp_end], jp=True) - except ValueError: - str_jp = '' - img_jp = None - stringlist.append([ - "0x{:06X}".format(en), "0x{:0{}X}".format(id, id_digits), - str_en, img_en, str_jp, img_jp, "0x{:06X}".format(jp_start) - ]) - else: - for id in range(num): - j1 = start + (id*length) - j2 = start_jp + (id*len_jp) - if large: - str_en, img_en = make_string_img_large(ROM_en[j1:j1+length], macros_en) - str_jp, img_jp = make_string_img_large(ROM_jp[j2:j2+len_jp], macros_jp, jp=True) - else: - str_en, img_en = make_string_img_small(ROM_en[j1:j1+length]) - str_jp, img_jp = make_string_img_small(ROM_jp[j2:j2+len_jp], jp=True) - stringlist.append(["0x{:06X}".format(j1), "0x{:0{}X}".format(id, id_digits), str_en, img_en, str_jp, img_jp]) - return stringlist + if indirect: + for id in range(num): + en = start + (id*length) + jp = start_jp + (id*len_jp) + en_start = int.from_bytes(ROM_en[en:en+length],'little') + start_str + en_end = int.from_bytes(ROM_en[en+length:en+(length*2)],'little') + start_str + if en_start >= 0xC00000: # SNES memory space has the ROM starting at 0xC00000 in HiROM mode. + en_start -= 0xC00000 + en_end -= 0xC00000 + jp_start = int.from_bytes(ROM_jp[jp:jp+len_jp],'little') + start_jp_str + jp_end = int.from_bytes(ROM_jp[jp+len_jp:jp+(len_jp*2)],'little') + start_jp_str + if jp_start >= 0xC00000: # SNES memory space has the ROM starting at 0xC00000 in HiROM mode. + jp_start -= 0xC00000 + jp_end -= 0xC00000 + if (en_end == start_str) or (jp_end == start_jp_str): + break + try: # When dealing with pointer redirection we might end up passing empty strings + if large: + str_en, img_en = make_string_img_large(ROM_en[en_start:en_end], macros_en) + else: + str_en, img_en = make_string_img_small(ROM_en[en_start:en_end]) + except ValueError: + str_en = '' + img_en = None + try: + if large: + str_jp, img_jp = make_string_img_large(ROM_jp[jp_start:jp_end], macros_jp, jp=True) + else: + str_jp, img_jp = make_string_img_small(ROM_jp[jp_start:jp_end], jp=True) + except ValueError: + str_jp = '' + img_jp = None + stringlist.append([ + hex(en, 6), hex(id, id_digits), + str_en, img_en, str_jp, img_jp, hex(jp_start, 6) + ]) + else: + for id in range(num): + j1 = start + (id*length) + j2 = start_jp + (id*len_jp) + if large: + str_en, img_en = make_string_img_large(ROM_en[j1:j1+length], macros_en) + str_jp, img_jp = make_string_img_large(ROM_jp[j2:j2+len_jp], macros_jp, jp=True) + else: + str_en, img_en = make_string_img_small(ROM_en[j1:j1+length]) + str_jp, img_jp = make_string_img_small(ROM_jp[j2:j2+len_jp], jp=True) + stringlist.append([hex(j1, 6), hex(id, id_digits), str_en, img_en, str_jp, img_jp]) + return stringlist def table_size_to_contents(table): @@ -533,56 +566,92 @@ def table_size_to_contents(table): def make_table(headers, items, sortable=False, row_labels=True, scale=2): - """ - Helper function to tabulate 2d lists - """ - cols = len(headers) - rows = len(items) - rd = hex_length(rows-1) - table = QTableWidget(rows, cols) - if row_labels: - table.setVerticalHeaderLabels(['0x{:0{}X}'.format(v, rd) for v in range(rows)]) - else: - table.verticalHeader().setVisible(False) - table.setHorizontalHeaderLabels(headers) - for row, col, item in [(x,y,items[x][y]) for x in range(rows) for y in range(cols)]: - if type(item) == type(QPixmap()): - lab = QLabel() - lab.setPixmap(item.scaled(item.size() * scale)) - table.setCellWidget(row, col, lab) - elif item is not None: - q_item = QTableWidgetItem(item) - if item[:2] == "0x": - q_item.setFont(monofont) - table.setItem(row, col, q_item) - table_size_to_contents(table) - if sortable: - table.setSortingEnabled(True) - table.sortItems(0) - return table + ''' + Helper function to tabulate 2d lists + ''' + cols = len(headers) + rows = len(items) + rd = hex_length(rows-1) + table = QTableWidget(rows, cols) + if row_labels: + table.setVerticalHeaderLabels([hex(v, rd) for v in range(rows)]) + else: + table.verticalHeader().setVisible(False) + table.setHorizontalHeaderLabels(headers) + for row, col, item in [(x,y,items[x][y]) for x in range(rows) for y in range(cols)]: + if isinstance(item, QWidget): + table.setCellWidget(row, col, item) + elif isinstance(item, QPixmap): + pix = item.scaled(item.size() * scale) + lab = QLabel() + lab.setPixmap(pix) + table.setCellWidget(row, col, lab) + elif item is not None: + if item.endswith('₁₆'): + s = '{}16'.format(item[:-2]) + lab = QLabel(s) + table.setCellWidget(row, col, lab) + else: + q_item = QTableWidgetItem(item) + if item.startswith(HEX_PREFIX): + q_item.setFont(monofont) + table.setItem(row, col, q_item) + table_size_to_contents(table) + if sortable: + table.setSortingEnabled(True) + table.sortItems(0) + return table def make_pixmap_table(items, cols=16, scale=4): - rows = divceil(len(items), cols) - rd = hex_length(rows-1)+1 - cd = hex_length(cols-1) - table = QTableWidget(rows, cols) - table.setVerticalHeaderLabels(['0x{:0{}X}'.format(v*cols, rd) for v in range(rows)]) - table.setHorizontalHeaderLabels(['0x{:0{}X}'.format(v, cd) for v in range(cols)]) - for i in range(len(items)): - item = items[i] - lab = QLabel() - lab.setPixmap(item.scaled(item.size() * scale)) - lab.setAlignment(QtCore.Qt.AlignCenter) - table.setCellWidget(i // cols, i % cols, lab) - table_size_to_contents(table) - return table + rows = divceil(len(items), cols) + rd = hex_length(rows-1)+1 + cd = hex_length(cols-1) + table = QTableWidget(rows, cols) + table.setVerticalHeaderLabels([hex(v*cols, rd) for v in range(rows)]) + table.setHorizontalHeaderLabels([hex(v, cd) for v in range(cols)]) + for i, item in enumerate(items): + if isinstance(item, QWidget): + table.setCellWidget(i // cols, i % cols, item) + elif isinstance(item, QPixmap): + lab = QLabel() + lab.setPixmap(item.scaled(item.size() * scale)) + lab.setAlignment(QtCore.Qt.AlignCenter) + table.setCellWidget(i // cols, i % cols, lab) + table_size_to_contents(table) + return table + + +def stack_labels(*items): + w = QWidget() + w.setContentsMargins(0, 0, 0, 0) + l = QVBoxLayout() + l.setAlignment(QtCore.Qt.AlignCenter) + l.setSpacing(0) + l.setContentsMargins(0, 0, 0, 0) + for item in items: + lab = QLabel() + if isinstance(item, QPixmap): + lab.setPixmap(item) + else: + lab.setText(item.strip()) + lab.setAlignment(QtCore.Qt.AlignCenter) + lab.setMargin(0) + l.addWidget(lab) + w.setLayout(l) + return w + + +def hex(num, digits): + # Consolidate formatting for consistency + #return '{:0{}X}₁₆'.format(num, digits) + return HEX_PREFIX + '{:0{}X}'.format(num, digits) def main(): - app = QApplication(sys.argv) - mainwindow = FF5Reader() - sys.exit(app.exec_()) + app = QApplication(sys.argv) + mainwindow = FF5Reader() + sys.exit(app.exec_()) if __name__ == '__main__': - main() + main()