commit f25913a79f1a56073c75734730ca803f32983e58 Author: Luke Hubmayer-Werner Date: Sat Mar 11 19:18:33 2017 +1030 Initial commit diff --git a/ff5reader.py b/ff5reader.py new file mode 100644 index 0000000..7151c59 --- /dev/null +++ b/ff5reader.py @@ -0,0 +1,280 @@ +#!python3 -i +''' +No license for now +''' + +import sys +import os +import struct +from array import array + +pyqt_version = 0 +skip_pyqt5 = True # "PYQT4" in os.environ + +if not skip_pyqt5: + try: + from PyQt5 import QtGui, QtCore + from PyQt5.QtGui import QIcon, QPalette, QColor + from PyQt5.QtWidgets import ( + QApplication, QMainWindow, QFormLayout, + QGridLayout, QHBoxLayout, QVBoxLayout, + QAbstractItemView, QHeaderView, QListWidget, + QListWidgetItem, QTabWidget, QTableWidget, + QTableWidgetItem, QFrame, QScrollArea, + QStackedWidget, QWidget, QCheckBox, QComboBox, + QDoubleSpinBox, QGroupBox, QLineEdit, + QPushButton, QRadioButton, QSpinBox, + QStyleOptionButton, QToolButton, QProgressBar, + QDialog, QColorDialog, QDialogButtonBox, + QFileDialog, QInputDialog, QMessageBox, + QAction, QActionGroup, QLabel, QMenu, QStyle, + QSystemTrayIcon, QStyleOptionProgressBar + ) + pyqt_version = 5 + except ImportError: + print("Couldn't import Qt5 dependencies. " + "Make sure you installed the PyQt5 package.") +if pyqt_version is 0: + try: + import sip + sip.setapi('QVariant', 2) + from PyQt4 import QtGui, QtCore + from PyQt4.QtGui import ( + QApplication, QMainWindow, QFormLayout, + QGridLayout, QHBoxLayout, QVBoxLayout, + QAbstractItemView, QHeaderView, QListWidget, + QListWidgetItem, QTabWidget, QTableWidget, + QTableWidgetItem, QFrame, QScrollArea, + QStackedWidget, QWidget, QCheckBox, + QComboBox, QDoubleSpinBox, QGroupBox, + QLineEdit, QPushButton, QRadioButton, + QSpinBox, QStyleOptionButton, QToolButton, + QProgressBar, QDialog, QColorDialog, + QDialogButtonBox, QFileDialog, QInputDialog, + QMessageBox, QAction, QActionGroup, + QLabel, QMenu, QStyle, + QSystemTrayIcon, QIcon, QPalette, QColor, + QValidator + ) + from PyQt4.QtGui import QStyleOptionProgressBarV2 as QStyleOptionProgressBar + pyqt_version = 4 + except ImportError: + print("Couldn't import Qt dependencies. " + "Make sure you installed the PyQt4 package.") + sys.exit(-1) + + +def divceil(numerator, denominator): + # Reverse floor division for ceil + return -(-numerator // denominator) + +def hex_length(i): + return divceil(i.bit_length(), 4) + +filename = "Final Fantasy V (Japan) [En by RPGe v1.1].sfc" +with open(filename, 'rb') as file1: + ROM = file1.read() +print(len(ROM)) + +Glyphs = [' ',' ',' ',' ', ' ',' ',' ',' ', ' ',' ',' ',' ', ' ',' ',' ',' ', # 0x00 + ' ',' ',' ',' ', ' ',' ',' ',' ', ' ',' ',' ',' ', ' ',' ',' ',' ', # 0x10 + 'A','B','C','D', 'E','F','G','H', 'I','J','K','L', 'M','N','O','P', # 0x20 + 'Q','R','S','T', 'U','V','W','X', 'Y','Z','a','b', 'c','d','e','f', # 0x30 + 'g','h','i','j', 'k','l','m','n', 'o','A','B','C', 'D','E','F','G', # 0x40 + 'H','I','J','0', '1','2','3','4', '5','6','7','8', '9','_m','_H','_P', # 0x50 + 'A','B','C','D', 'E','F','G','H', 'I','J','K','L', 'M','N','O','P', # 0x60 + 'Q','R','S','T', 'U','V','W','X', 'Y','Z','a','b', 'c','d','e','f', # 0x70 + 'g','h','i','j', 'k','l','m','n', 'o','p','q','r', 's','t','u','v', # 0x80 + 'w','x','y','z', 'il','it','','li', 'll','\'','"',':', ';',',','(',')', # 0x90 + '/','!','?','0', 'ti','fi','Bl','a', 'pe','l','\'','"', 'if','lt','tl','ir', # 0xA0 + 'tt','や','ユ','ゆ', 'ヨ', 'よ', 'ワ', 'わ', 'ン', 'ん', 'ヲ','を', '[key]', '[shoe]', '[diamond?]', '[hammer]', # 0xB0 + '[tent]', '[ribbon]', '[potion]', '[shirt]', '[song]', '-', '[shuriken]', '・・', '[scroll]', '!', '[claw]', '?', '[glove]', 'pickaxe head??', '/', ':', # 0xC0 + '「', '」', '0', 'A', 'B', 'X', 'Y', 'L', 'R', 'E', 'H', 'M', 'P', 'S', 'C', 'T', # 0xD0 + ' ', ' ', '+', '[sword]', '[wh.mag]', '[blk.mag]', '[t.mag]', '[knife]', '[spear]', '[axe]', '[katana]', '[rod]', '[staff]', '[bow]', '[harp]', '[whip]', # 0xE0 + '[bell]', '[shield]', '[helmet]', '[armor]', '[ring]', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '] # F0 + +BGM_Tracks = ["Ahead on our way", "The Fierce Battle", "A Presentiment", "Go Go Boko!", + "Pirates Ahoy", "Tenderness in the Air", "Fate in Haze", "Moogle theme", + "Prelude/Crystal Room", "The Last Battle", "Requiem", "Nostalgia", + "Cursed Earths", "Lenna's Theme", "Victory's Fanfare", "Deception", + "The Day Will Come", "Nothing?", "ExDeath's Castle", "My Home, Sweet Home", + "Waltz Suomi", "Sealed Away", "The Four Warriors of Dawn", "Danger", + "The Fire Powered Ship", "As I Feel, You Feel", "Mambo de Chocobo!", "Music Box", + "Intension of the Earth", "The Dragon Spreads its Wings", "Beyond the Deep Blue Sea", "The Prelude of Empty Skies", + "Searching the Light", "Harvest", "Battle with Gilgamesh", "Four Valiant Hearts", + "The Book of Sealings", "What?", "Hurry! Hurry!", "Unknown Lands", + "The Airship", "Fanfare 1", "Fanfare 2", "The Battle", + "Walking the Snowy Mountains", "The Evil Lord Exdeath", "The Castle of Dawn", "I'm a Dancer", + "Reminiscence", "Run!", "The Ancient Library", "Royal Palace", + "Good Night!", "Piano Lesson 1", "Piano Lesson 2", "Piano Lesson 3", + "Piano Lesson 4", "Piano Lesson 5", "Piano Lesson 6", "Piano Lesson 7", + "Piano Lesson 8", "Musica Machina", "Meteor falling?", "The Land Unknown", + "The Decisive Battle", "The Silent Beyond", "Dear Friends", "Final Fantasy", + "A New Origin", "Chirping sound"] + +def MakeStringList(start, end, length): + stringlist = [] + id = 0 + ids = (end-start)//length + id_digits = hex_length(ids-1) + for i in range(start, end, length): + stringROM = ROM[i:i+length] + string = "" + for j in stringROM: + string = string + Glyphs[j] + stringlist.append(("0x{:06X}".format(i), "0x{0:0{1}X}".format(id, id_digits), string)) + id += 1 + return stringlist + +items = MakeStringList(0x111380, 0x111C80, 9) +magics = MakeStringList(0x111C80, 0x111E8A, 6) +more_magics = MakeStringList(0x111E8A, 0x11211B, 9) +enemy_names = MakeStringList(0x200050, 0x200F50, 10) +stringlist_headers = ["Address", "ID", "Name"] + +zone_names_count = 0x100 +zone_names = [] +zone_names_full = [] +for id in range(zone_names_count): + i = 0x107000 + (id*2) + string = "" + offset = 0x270000 + start = int.from_bytes(ROM[i:i+2],'little') + offset + next = int.from_bytes(ROM[i+2:i+4],'little') + offset + if next == offset: + break + stringROM = ROM[start:next] + for j in stringROM: + string = string + Glyphs[j] + zone_names.append(string) + zone_names_full.append("0x{:06X}->0x{:06X}: Zone Name 0x{:02X} - {name}".format(i, start, id, name=string)) + +zone_count = 0x200 +zone_data = [] +zone_structure = [("NPC Layer", 2, None), + ("Name", 1, 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, BGM_Tracks)] +zone_headers = ["Address"] + [z[0] for z in zone_structure] +for i in range(zone_count): + zone_data.append([]) + offset = 0x0E9C00 + (i*0x1A) + zone_data[-1].append("0x{:06X}".format(offset)) + j = 0 + for z in zone_structure: + val = int.from_bytes(ROM[offset+j:offset+j+z[1]],'little') + if z[2] and val < len(z[2]): + zone_data[-1].append(z[2][val]) + else: + zone_data[-1].append("0x{0:0{1}X}".format(val, z[1]*2)) + j += z[1] + + +npc_layer_count = 0x200 +npc_layers = [] +npc_layer_structure = [("Dialogue/trigger ID", 1, None), + ("0x01", 1, None), + ("Sprite ID", 1, None), + ("X", 1, None), + ("Y", 1, None), + ("Move Pattern", 1, None), + ("Palette", 1, None)] +npc_layer_headers = ["Address", "Layer"] + [x[0] for x in npc_layer_structure] +for layer in range(npc_layer_count): + offset = 0x0E59C0 + i = offset + (layer*2) + start = int.from_bytes(ROM[i:i+2],'little') + offset + next = int.from_bytes(ROM[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(address), "0x{0:03X}".format(layer)]) + j = 0 + for z in npc_layer_structure: + val = int.from_bytes(ROM[start+j:start+j+z[1]],'little') + if z[2] and val < len(z[2]): + npc_layers[-1].append(z[2][val]) + else: + npc_layers[-1].append("0x{0:0{1}X}".format(val, z[1]*2)) + j += z[1] + + +def MakeTable(headers, items, sortable=False, row_labels=True): + """ + Helper function to tabulate 2d lists + """ + table = QTableWidget() + rows = len(items) + rd = hex_length(rows-1) + cols = len(headers) + table.setRowCount(rows) + if row_labels: + table.setVerticalHeaderLabels(['0x{0:0{1}X}'.format(v, rd) for v in range(rows)]) + else: + table.verticalHeader().setVisible(False) + table.setColumnCount(cols) + table.setHorizontalHeaderLabels(headers) + for row, col, item in [(x,y,items[x][y]) for x in range(rows) for y in range(cols)]: + table.setItem(row, col, QTableWidgetItem(item)) + table.resizeColumnsToContents() + if sortable: + table.setSortingEnabled(True) + table.sortItems(0) + return table + + +class FF5Reader(QMainWindow): + """ + Main GUI class + """ + def __init__(self): + QMainWindow.__init__(self, None) + + self.tabwidget = QTabWidget() + self.enemy_sprites = QFrame() + self.tabwidget.addTab(self.enemy_sprites, "Enemy Sprites") + self.tabwidget.addTab(MakeTable(zone_headers, zone_data, True), "Zones") + self.tabwidget.addTab(MakeTable(npc_layer_headers, npc_layers, True), "NPC Layers") + self.tabwidget.addTab(MakeTable(stringlist_headers, items, row_labels=False), "Items") + self.tabwidget.addTab(MakeTable(stringlist_headers, magics, row_labels=False), "Magics") + self.tabwidget.addTab(MakeTable(stringlist_headers, more_magics, row_labels=False), "More Magics") + self.tabwidget.addTab(MakeTable(stringlist_headers, enemy_names, row_labels=False), "Enemy Names") + + layout = QHBoxLayout() + layout.addWidget(self.tabwidget) + self.main_widget = QWidget(self) + self.main_widget.setLayout(layout) + self.setCentralWidget(self.main_widget) + self.show() + + + +def main(): + app = QApplication(sys.argv) + + mainwindow = FF5Reader() + sys.exit(app.exec_()) + +if __name__ == '__main__': + main()