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_()