Added FF4 character battle sprites

This commit is contained in:
Luke Hubmayer-Werner 2018-03-19 17:13:04 +10:30
parent 2d286d48ef
commit d32ca0608e
1 changed files with 500 additions and 431 deletions

289
ff5reader.py Normal file → Executable file
View File

@ -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,15 +89,18 @@ 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):
@ -122,32 +128,32 @@ class FF5Reader(QMainWindow):
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):
@ -160,8 +166,7 @@ class FF5Reader(QMainWindow):
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_data.append((hex(i, 2), hex(offset, 6), hex(pointer, 6), hex(length, 4)))
npc_layers = []
offset = 0x0E59C0
@ -172,28 +177,28 @@ class FF5Reader(QMainWindow):
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.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)
('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"]
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])
@ -201,39 +206,45 @@ class FF5Reader(QMainWindow):
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)
enemy_sprites_named = [stack_labels(s, d[-2]) for s, d in zip(enemy_sprites, enemy_sprite_data)]
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")
self.tabwidget.addTab(strings_tab, 'Strings')
self.tabwidget.addTab(structs_tab, 'Structs')
self.tabwidget.addTab(sprites_tab, 'Images')
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(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')
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")
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")
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')
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()
@ -241,7 +252,7 @@ class FF5Reader(QMainWindow):
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")
strings_tab.addTab(self.string_decoder, 'String Decoder')
layout = QHBoxLayout()
layout.addWidget(self.tabwidget)
@ -265,8 +276,8 @@ class FF5Reader(QMainWindow):
class Canvas:
def __init__(self, rows, columns, color=bg_trans):
self.image = QImage(8*rows, 8*columns, QImage.Format_ARGB32_Premultiplied)
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
@ -289,14 +300,14 @@ class Canvas:
def parse_struct(rom, offset, structure):
out = ["0x{:06X}".format(offset)]
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("0x{:0{}X}".format(val, z[1]*2))
out.append(hex(val, z[1]*2))
j += z[1]
return out
@ -333,19 +344,41 @@ def make_enemy_sprites(rom):
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())
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
@ -504,8 +537,8 @@ def make_string_img_list(start, length, num, start_jp=None, len_jp=None, start_s
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)
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):
@ -517,7 +550,7 @@ def make_string_img_list(start, length, num, start_jp=None, len_jp=None, start_s
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])
stringlist.append([hex(j1, 6), hex(id, id_digits), str_en, img_en, str_jp, img_jp])
return stringlist
@ -533,26 +566,34 @@ 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)])
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 type(item) == type(QPixmap()):
if isinstance(item, QWidget):
table.setCellWidget(row, col, item)
elif isinstance(item, QPixmap):
pix = item.scaled(item.size() * scale)
lab = QLabel()
lab.setPixmap(item.scaled(item.size() * scale))
lab.setPixmap(pix)
table.setCellWidget(row, col, lab)
elif item is not None:
if item.endswith('₁₆'):
s = '<tt>{}</tt><sub>16</sub>'.format(item[:-2])
lab = QLabel(s)
table.setCellWidget(row, col, lab)
else:
q_item = QTableWidgetItem(item)
if item[:2] == "0x":
if item.startswith(HEX_PREFIX):
q_item.setFont(monofont)
table.setItem(row, col, q_item)
table_size_to_contents(table)
@ -567,10 +608,12 @@ def make_pixmap_table(items, cols=16, scale=4):
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]
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)
@ -579,6 +622,32 @@ def make_pixmap_table(items, cols=16, scale=4):
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()