jak-project/decompiler/gui/decompiler_gui.py

348 lines
10 KiB
Python
Raw Normal View History

from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow, QTreeView, QVBoxLayout, QWidget, QPlainTextEdit, QLineEdit, QListView, QDialog, QSplitter, QSizePolicy
from PyQt5.Qt import QStandardItemModel, QStandardItem, QFont, QModelIndex
import json
import re
import os
def get_monospaced_font():
"""
Get a monospaced font. Should work on both windows and linux.
"""
font = QFont("monospace")
font.setStyleHint(QFont.TypeWriter)
return font
def get_jak_path():
"""
Get a path to jak-project/
"""
return os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "..")
def segment_id_to_name(sid):
"""
GOAL segment ID to name string
"""
if sid == 0:
return "main"
elif sid == 1:
return "debug"
elif sid == 2:
return "top-level"
else:
return "INVALID-SEGMENT"
# Hold all the metadata for an object file
class ObjFile():
def __init__(self, obj):
"""
Convert the json data in obj.txt.
If the format changes or we add new fields, they should be added here.
"""
self.unique_name = obj[0] # The unique name that's used by the decompiler.
self.name_in_dgo = obj[1] # Name in the game.
self.version = obj[2] # GOAL object file format version
def get_description(self):
return "Name: {}\n Version: {}\n Name in game: {}".format(self.unique_name, self.version, self.name_in_dgo)
# Hold all of the object files in a dgo.
class DgoFile():
def __init__(self):
self.obj_files = dict()
def add_obj(self, obj):
self.obj_files[obj[0]] = ObjFile(obj)
# Hold all DGOs/Object files.
class FileMap():
def __init__(self):
self.dgo_files = dict()
self.all_objs = dict()
def add_obj_to_dgo(self, dgo, obj):
if not(dgo in self.dgo_files):
self.dgo_files[dgo] = DgoFile()
self.dgo_files[dgo].add_obj(obj)
self.all_objs[obj[0]] = ObjFile(obj)
def get_objs_matching_regex(self, regex):
"""
Get a list of object files with a name that matches the given regex.
"""
try:
r = re.compile(regex)
except:
return []
return list(filter(r.match, self.all_objs.keys()))
def load_obj_map_file(file_path):
"""
Load the obj.txt file generated by the decompiler.
Return a FileMap.
"""
file_map = FileMap()
with open(file_path) as f:
json_data = json.loads(f.read())
for obj_file in json_data:
for dgo in obj_file[3]:
file_map.add_obj_to_dgo(dgo, obj_file)
return file_map
class ObjectFileView(QDialog):
def __init__(self, name):
super().__init__()
self.setWindowTitle(name)
with open(os.path.join(get_jak_path(), "decompiler_out", "{}_asm.json".format(name))) as f:
self.asm_data = json.loads(f.read())
main_layout = QVBoxLayout()
monospaced_font = get_monospaced_font()
self.header_label = QLabel()
main_layout.addWidget(self.header_label)
function_splitter = QSplitter()
function_splitter.setSizePolicy(QSizePolicy(QSizePolicy.Expanding,QSizePolicy.Expanding))
self.function_list = QTreeView()
self.function_list_model = QStandardItemModel()
self.functions_by_name = dict()
root = self.function_list_model.invisibleRootItem()
seg_roots = []
for i in range(3):
seg_entry = QStandardItem(segment_id_to_name(i))
seg_entry.setFont(monospaced_font)
seg_entry.setEditable(False)
root.appendRow(seg_entry)
seg_roots.append(seg_entry)
for f in self.asm_data["functions"]:
function_entry = QStandardItem(f["name"])
function_entry.setFont(monospaced_font)
function_entry.setEditable(False)
seg_roots[f["segment"]].appendRow(function_entry)
self.functions_by_name[f["name"]] = f
self.header_label.setText("Object File {} Functions ({} total):".format(name, len(self.asm_data["functions"])))
self.function_list.setModel(self.function_list_model)
self.function_list.clicked.connect(self.display_function)
function_splitter.addWidget(self.function_list)
layout = QVBoxLayout()
self.function_header_label = QLabel("No function selected")
self.function_header_label.setFont(monospaced_font)
self.header_label.setSizePolicy(QSizePolicy(QSizePolicy.Minimum,
QSizePolicy.Minimum))
layout.addWidget(self.function_header_label)
self.op_asm_split_view = QSplitter()
self.op_asm_split_view.setSizePolicy(QSizePolicy(QSizePolicy.Expanding,QSizePolicy.Expanding))
self.basic_op_pane = QListView()
self.basic_op_pane.clicked.connect(self.basic_op_clicked)
#layout.addWidget(self.basic_op_pane)
self.op_asm_split_view.addWidget(self.basic_op_pane)
self.asm_pane = QListView()
self.op_asm_split_view.addWidget(self.asm_pane)
layout.addWidget(self.op_asm_split_view)
self.asm_display = QPlainTextEdit()
self.asm_display.setMaximumHeight(80)
layout.addWidget(self.asm_display)
self.warnings_label = QLabel()
layout.addWidget(self.warnings_label)
widget = QWidget()
widget.setLayout(layout)
function_splitter.addWidget(widget)
main_layout.addWidget(function_splitter)
# add it to the window!
self.setLayout(main_layout)
def display_function(self, item):
name = item.data()
monospaced_font = get_monospaced_font()
func = self.functions_by_name[name]
basic_op_model = QStandardItemModel()
basic_op_root = basic_op_model.invisibleRootItem()
asm_model = QStandardItemModel()
asm_root = asm_model.invisibleRootItem()
self.basic_id_to_asm = []
self.current_function = name
op_idx = 0
basic_idx = 0
for op in func["asm"]:
if "label" in op:
asm_item = QStandardItem(op["label"] + "\n " + op["asm_op"])
else:
asm_item = QStandardItem(" " + op["asm_op"])
asm_item.setFont(monospaced_font)
asm_item.setEditable(False)
asm_root.appendRow(asm_item)
if "basic_op" in op:
if "label" in op:
basic_item = QStandardItem(op["label"] + "\n " + op["basic_op"])
else:
basic_item = QStandardItem(" " + op["basic_op"])
basic_item.setFont(monospaced_font)
basic_item.setEditable(False)
basic_op_root.appendRow(basic_item)
self.basic_id_to_asm.append(op_idx)
basic_idx = basic_idx + 1
op_idx = op_idx + 1
self.basic_id_to_asm.append(op_idx)
self.basic_op_pane.setModel(basic_op_model)
self.asm_pane.setModel(asm_model)
self.warnings_label.setText(func["warnings"])
self.asm_display.setPlainText("")
self.function_header_label.setText("{}, type: {}\nfunc: {} obj: {}".format(name, func["type"], func["name"], func["parent_object"]))
def basic_op_clicked(self, item):
text = ""
added_reg = 0
asm_idx = self.basic_id_to_asm[item.row()]
asm_op = self.functions_by_name[self.current_function]["asm"][asm_idx]
if "type_map" in asm_op:
for reg, type_name in asm_op["type_map"].items():
text += "{}: {} ".format(reg, type_name)
added_reg += 1
if added_reg >= 4:
text += "\n"
added_reg = 0
text += "\n"
for i in range(asm_idx, self.basic_id_to_asm[item.row() + 1]):
text += self.functions_by_name[self.current_function]["asm"][i]["asm_op"] + "\n"
op = self.functions_by_name[self.current_function]["asm"][asm_idx]
if "referenced_string" in op:
text += op["referenced_string"]
self.asm_display.setPlainText(text)
self.asm_display.setFont(get_monospaced_font())
self.asm_pane.setCurrentIndex(self.asm_pane.model().index(asm_idx, 0))
# A window for browsing all the object files.
# Doesn't actually know anything about what's in the files, it's just used to select a file.
class ObjectFileBrowser(QMainWindow):
def __init__(self, obj_map):
self.obj_map = obj_map
super().__init__()
self.setWindowTitle("Object File Browser")
self.childen_windows = []
layout = QVBoxLayout()
monospaced_font = get_monospaced_font()
layout.addWidget(QLabel("Browse object files by dgo..."))
# Set up the tree view
self.tree = QTreeView()
self.tree_model = QStandardItemModel()
self.tree_root = self.tree_model.invisibleRootItem()
for dgo_name, dgo in obj_map.dgo_files.items():
dgo_entry = QStandardItem(dgo_name)
dgo_entry.setFont(monospaced_font)
dgo_entry.setEditable(False)
for obj_name, obj in dgo.obj_files.items():
obj_entry = QStandardItem(obj_name)
obj_entry.setFont(monospaced_font)
obj_entry.setEditable(False)
dgo_entry.appendRow(obj_entry)
self.tree_root.appendRow(dgo_entry)
self.tree.setModel(self.tree_model)
self.tree.clicked.connect(self.handle_tree_click)
self.tree.doubleClicked.connect(self.handle_tree_double_click)
layout.addWidget(self.tree)
# Set up the Search Box
layout.addWidget(QLabel("Or search for object (regex):"))
self.search_box = QLineEdit()
self.search_box.textChanged.connect(self.handle_search_change)
layout.addWidget(self.search_box)
# Set up Search Results
self.search_result = QListView()
layout.addWidget(self.search_result)
self.search_result.clicked.connect(self.handle_search_result_click)
self.search_result.doubleClicked.connect(self.handle_search_result_double_click)
self.search_result.setMaximumHeight(200)
# Set up the info box at the bottom
self.text_box = QPlainTextEdit()
self.text_box.setReadOnly(True)
self.text_box.setFont(monospaced_font)
layout.addWidget(self.text_box)
self.text_box.setMaximumHeight(100)
self.text_box.setPlainText("Select an object file to see details. Double click to open.")
# add it to the window!
widget = QWidget()
widget.setLayout(layout)
self.setCentralWidget(widget)
def handle_tree_click(self, val):
if not(val.parent().isValid()):
return
dgo = val.parent().data()
obj = val.data()
obj_info = self.obj_map.dgo_files[dgo].obj_files[obj]
self.text_box.setPlainText("{}\n DGO: {}".format(obj_info.get_description(), dgo))
def handle_search_change(self, text):
objs = self.obj_map.get_objs_matching_regex(text)
model = QStandardItemModel()
root = model.invisibleRootItem()
monospaced_font = get_monospaced_font()
for x in objs:
entry = QStandardItem(x)
entry.setFont(monospaced_font)
entry.setEditable(False)
root.appendRow(entry)
self.search_result.setModel(model)
def handle_search_result_click(self, val):
obj = val.data()
obj_info = self.obj_map.all_objs[obj]
self.text_box.setPlainText(obj_info.get_description())
def handle_search_result_double_click(self, val):
obj = val.data()
window = ObjectFileView(obj)
window.show()
# prevents window from being GC'd and closed.
self.childen_windows.append(window)
def handle_tree_double_click(self, val):
if not(val.parent().isValid()):
return
obj = val.data()
window = ObjectFileView(obj)
window.show()
# prevents window from being GC'd and closed.
self.childen_windows.append(window)
map_file = load_obj_map_file(os.path.join(get_jak_path(), "decompiler_out", "obj.txt"))
app = QApplication([])
app.setStyle('Windows')
window = ObjectFileBrowser(map_file)
window.show()
app.exec_()