Add GUI version of scrapper.py, needs PyQt5 and construct
This commit is contained in:
		
							parent
							
								
									88737f29e4
								
							
						
					
					
						commit
						9f69934a32
					
				
					 1 changed files with 667 additions and 0 deletions
				
			
		
							
								
								
									
										667
									
								
								tools/scrapper_gui.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										667
									
								
								tools/scrapper_gui.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,667 @@ | ||||||
|  | import sys | ||||||
|  | from PyQt5.QtWidgets import * | ||||||
|  | from PyQt5.QtCore import * | ||||||
|  | from PyQt5.QtGui import * | ||||||
|  | from PyQt5 import QtGui, QtCore, QtWidgets | ||||||
|  | from threading import Thread | ||||||
|  | from queue import Queue | ||||||
|  | from construct import * | ||||||
|  | from glob import glob | ||||||
|  | import os | ||||||
|  | 
 | ||||||
|  | ScrapFile = Struct( | ||||||
|  |     'path'/PrefixedArray(Int32ul, Byte), | ||||||
|  |     'size'/Int32ul, | ||||||
|  |     'offset'/Int32ul, | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | PackedFile = Struct( | ||||||
|  |     Const(b'BFPK'), | ||||||
|  |     Const(b'\0\0\0\0'), | ||||||
|  |     'files'/PrefixedArray(Int32ul, ScrapFile), | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_path(file): | ||||||
|  |     return str(bytes(file.path), "utf-8", | ||||||
|  |                "backslashreplace") | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def fsize(n): | ||||||
|  |     idx = 0 | ||||||
|  |     l = ["", "K", "M", "G", "T", "P", "E"] | ||||||
|  |     while n > 1024: | ||||||
|  |         idx += 1 | ||||||
|  |         n /= 1024 | ||||||
|  |     return "{:.02f} {}B".format(n, l[idx]) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Tree(dict): | ||||||
|  |     def __init__(self, *args, **kwargs): | ||||||
|  |         self._size = 0 | ||||||
|  |         self.data = None | ||||||
|  |         self.path = None | ||||||
|  |         super(type(self), self).__init__(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     def __missing__(self, k): | ||||||
|  |         self[k] = type(self)() | ||||||
|  |         return self[k] | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def size(self): | ||||||
|  |         if self.data: | ||||||
|  |             ret = self.data.size | ||||||
|  |         else: | ||||||
|  |             ret = 0 | ||||||
|  |         for v in self.values(): | ||||||
|  |             ret += v.size | ||||||
|  |         return ret | ||||||
|  | 
 | ||||||
|  |     @size.setter | ||||||
|  |     def size(self, s): | ||||||
|  |         self._size = s | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def load_data(path): | ||||||
|  |     ret = Tree() | ||||||
|  |     fn = os.path.split(path)[-1] | ||||||
|  |     ret[fn] = Tree() | ||||||
|  |     with open(path, "rb") as fh: | ||||||
|  |         data = PackedFile.parse_stream(fh) | ||||||
|  |         for entry in data.files: | ||||||
|  |             path = get_path(entry).split("/") | ||||||
|  |             root = ret[fn] | ||||||
|  |             for elem in path: | ||||||
|  |                 root = root[elem] | ||||||
|  |             root.data = entry | ||||||
|  |     return ret | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Patcher(object): | ||||||
|  |     def __init__(self, ed): | ||||||
|  |         self.editor = ed | ||||||
|  | 
 | ||||||
|  |     def debug(self, *args): | ||||||
|  |         "Enable Scengraph Debugging Console" | ||||||
|  |         print("DBG") | ||||||
|  | 
 | ||||||
|  |     def test(self, *args): | ||||||
|  |         "TEST!" | ||||||
|  |         print(self.editor.selected()) | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class PackerThread(QThread): | ||||||
|  |     signal = pyqtSignal('PyQt_PyObject') | ||||||
|  | 
 | ||||||
|  |     def __init__(self, node, path): | ||||||
|  |         super().__init__() | ||||||
|  | 
 | ||||||
|  |         self.node = node | ||||||
|  |         self.path = path | ||||||
|  | 
 | ||||||
|  |     def func(self, *args, **kwargs): | ||||||
|  |         self.signal.emit(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     def run(self): | ||||||
|  |         try: | ||||||
|  |             self.__run() | ||||||
|  |         except: | ||||||
|  |             (type, value, traceback) = sys.exc_info() | ||||||
|  |             print(type, value, traceback) | ||||||
|  |             sys.excepthook(type, value, traceback) | ||||||
|  | 
 | ||||||
|  |     def __run(self): | ||||||
|  |         node = self.node | ||||||
|  |         path = self.path | ||||||
|  |         PAK = node.pak | ||||||
|  |         file_list = [] | ||||||
|  | 
 | ||||||
|  |         self.func(("wtl", "Preprocessing")) | ||||||
|  |         self.func(("range", 0,  0)) | ||||||
|  |         for item in walk(node): | ||||||
|  |             if item.struct: | ||||||
|  |                 file_list.append(item) | ||||||
|  |                 self.func(("upd", get_loc(item)[1], 1)) | ||||||
|  | 
 | ||||||
|  |         header = PackedFile.build(dict(files=[f.struct for f in file_list])) | ||||||
|  |         total_size = len(header) | ||||||
|  |         self.func(("wtl", "Updating offsets")) | ||||||
|  |         self.func(("range", 0,  len(file_list))) | ||||||
|  |         for idx, file in enumerate(file_list): | ||||||
|  |             file.struct.offset = total_size | ||||||
|  |             total_size += file.struct.size | ||||||
|  |             self.func(("upd", get_loc(file)[1], idx)) | ||||||
|  | 
 | ||||||
|  |         header = PackedFile.build(dict(files=[f.struct for f in file_list])) | ||||||
|  |         cnt = 0 | ||||||
|  |         if path: | ||||||
|  |             self.func(("wtl", "Writing output")) | ||||||
|  |             self.func(("range", 0, total_size)) | ||||||
|  |             with open(path, "wb") as of: | ||||||
|  |                 of.write(header) | ||||||
|  |                 written = len(header) | ||||||
|  |                 self.func(("upd", "Header", written)) | ||||||
|  |                 for file in file_list: | ||||||
|  |                     loc = get_loc(file) | ||||||
|  |                     data = get_data(file)[0] | ||||||
|  |                     written += len(data) | ||||||
|  |                     self.func(("upd", get_loc(file)[1], written)) | ||||||
|  |                     of.write(data) | ||||||
|  |             print("XXXXXXXXXXXXXXXXXXXXX") | ||||||
|  |         self.func(("close",)) | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class PackerWindow(QProgressDialog): | ||||||
|  | 
 | ||||||
|  |     def __init__(self, node, path, *args, **kwargs): | ||||||
|  |         self.node = node | ||||||
|  |         self.path = path | ||||||
|  |         self.mtx = QMutex() | ||||||
|  |         super().__init__() | ||||||
|  |         self.initUI() | ||||||
|  |         self.setAutoClose(True) | ||||||
|  |         self.setWindowTitle("Packing {}".format(node.pak)) | ||||||
|  |         self.setCancelButtonText("Stop") | ||||||
|  |         self.run() | ||||||
|  | 
 | ||||||
|  |     def initUI(self): | ||||||
|  |         self.show() | ||||||
|  |         self.ensurePolished() | ||||||
|  |         self.setFixedSize(300, self.height()) | ||||||
|  | 
 | ||||||
|  |     def closeEvent(self, event): | ||||||
|  |         print("BYE") | ||||||
|  |         if self.th: | ||||||
|  |             self.th.wait() | ||||||
|  |         event.accept() | ||||||
|  | 
 | ||||||
|  |     def sig(self, val): | ||||||
|  |         lock = QMutexLocker(self.mtx) | ||||||
|  |         #print("SIG:", val) | ||||||
|  |         typ, *args = val | ||||||
|  |         if typ == "wtl": | ||||||
|  |             self.setWindowTitle(*args) | ||||||
|  |         elif typ == "close": | ||||||
|  |             self.th.wait() | ||||||
|  |             self.th = None | ||||||
|  |             self.close() | ||||||
|  |         elif typ == "range": | ||||||
|  |             self.setRange(*args) | ||||||
|  |         elif typ == "upd": | ||||||
|  |             self.setLabelText(args[0]) | ||||||
|  |             self.setValue(args[1]) | ||||||
|  |         else: | ||||||
|  |             raise ValueError("Unknown signal: {}".format(val)) | ||||||
|  | 
 | ||||||
|  |     def run(self): | ||||||
|  |         self.queue = Queue() | ||||||
|  |         self.th = PackerThread(self.node, self.path) | ||||||
|  |         self.th.signal.connect(self.sig) | ||||||
|  |         self.th.start() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class MainWindow(QMainWindow): | ||||||
|  |     def __init__(self, parent=None): | ||||||
|  |         super(MainWindow, self).__init__(parent) | ||||||
|  |         self.setWindowTitle("SMT - Scrap Mod Tool") | ||||||
|  |         self.resize(800, 600) | ||||||
|  |         self.center() | ||||||
|  | 
 | ||||||
|  |         menu_close_all = QAction("Close &All", self) | ||||||
|  |         menu_close_all.setShortcut("Ctrl+Shift+C") | ||||||
|  |         menu_close_all.triggered.connect(self.menu_close_all) | ||||||
|  | 
 | ||||||
|  |         menu_create = QAction("&Create new .packed file", self) | ||||||
|  |         menu_create.setShortcut("Ctrl+N") | ||||||
|  |         menu_create.triggered.connect(self.menu_new) | ||||||
|  | 
 | ||||||
|  |         menu_load = QAction("&Open .packed file", self) | ||||||
|  |         menu_load.setShortcut("Ctrl+O") | ||||||
|  |         menu_load.triggered.connect(self.menu_load) | ||||||
|  | 
 | ||||||
|  |         menu_exit = QAction("&Exit", self) | ||||||
|  |         menu_exit.setShortcut("Ctrl+Q") | ||||||
|  |         menu_exit.triggered.connect(qApp.quit) | ||||||
|  | 
 | ||||||
|  |         menu = self.menuBar() | ||||||
|  |         fileMenu = menu.addMenu('&File') | ||||||
|  |         fileMenu.addAction(menu_load) | ||||||
|  |         fileMenu.addAction(menu_create) | ||||||
|  |         fileMenu.addSeparator() | ||||||
|  |         fileMenu.addAction(menu_close_all) | ||||||
|  |         fileMenu.addSeparator() | ||||||
|  |         fileMenu.addAction(menu_exit) | ||||||
|  | 
 | ||||||
|  |         self.editor = PackedEditor(self) | ||||||
|  |         self.setCentralWidget(self.editor) | ||||||
|  |         self.show() | ||||||
|  |         self.menu_load(sys.argv[1:]) | ||||||
|  | 
 | ||||||
|  |     def menu_new(self): | ||||||
|  |         data = PackedFile.build({'files': []}) | ||||||
|  |         loc = QFileDialog.getSaveFileName( | ||||||
|  |             self, 'Save file', os.getcwd(), "Scrapland Data Files (*.packed)")[0] | ||||||
|  |         with open(loc, "wb") as of: | ||||||
|  |             of.write(data) | ||||||
|  |         self.editor.clear() | ||||||
|  |         self.editor.load(loc) | ||||||
|  | 
 | ||||||
|  |     def menu_load(self, paths=None): | ||||||
|  |         if not paths: | ||||||
|  |             paths = QFileDialog.getOpenFileNames( | ||||||
|  |                 self, 'Open file', os.getcwd(), "Scrapland Data Files (*.packed)")[0] | ||||||
|  |         for path in paths: | ||||||
|  |             path = os.path.abspath(path) | ||||||
|  |             self.editor.load(path) | ||||||
|  | 
 | ||||||
|  |     def menu_close_all(self): | ||||||
|  |         self.editor.clear() | ||||||
|  | 
 | ||||||
|  |     def center(self): | ||||||
|  |         qr = self.frameGeometry() | ||||||
|  |         cp = QDesktopWidget().availableGeometry().center() | ||||||
|  |         qr.moveCenter(cp) | ||||||
|  |         self.move(qr.topLeft()) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_loc(item): | ||||||
|  |     node = item | ||||||
|  |     path = [] | ||||||
|  |     while node: | ||||||
|  |         path.insert(0, node.text(0)) | ||||||
|  |         node = node.parent() | ||||||
|  |     if path: | ||||||
|  |         file = path.pop(0) | ||||||
|  |     else: | ||||||
|  |         file = None | ||||||
|  |     path = "/".join(path) | ||||||
|  |     return (file, path) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def walk(node): | ||||||
|  |     yield node | ||||||
|  |     for child_idx in range(node.childCount()): | ||||||
|  |         yield from walk(node.child(child_idx)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def read_file(path, size=None, offset=None): | ||||||
|  |     data = None | ||||||
|  |     with open(path, "rb") as fh: | ||||||
|  |         if offset: | ||||||
|  |             fh.seek(offset) | ||||||
|  |         data = fh.read(size) | ||||||
|  |     return data | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_data(node, size=None): | ||||||
|  |     bin_data = None | ||||||
|  |     more = None | ||||||
|  |     if node.struct: | ||||||
|  |         if size: | ||||||
|  |             if node.struct.size > size: | ||||||
|  |                 more = True | ||||||
|  |             size = min(node.struct.size, size) | ||||||
|  |         else: | ||||||
|  |             size = node.struct.size | ||||||
|  |         if node.path: | ||||||
|  |             bin_data = read_file(node.path, size) | ||||||
|  |         else: | ||||||
|  |             bin_data = read_file(node.pak, size, node.struct.offset) | ||||||
|  |     return bin_data, more | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def hexdump(data, addr=0): | ||||||
|  |     res = [] | ||||||
|  |     bin_data = list(data) | ||||||
|  |     while bin_data: | ||||||
|  |         chunk = bin_data[:16] | ||||||
|  |         bin_data = bin_data[16:] | ||||||
|  |         res.append("0x{:04x} | {}".format( | ||||||
|  |             addr, " ".join("{:02x}".format(b) for b in chunk))) | ||||||
|  |         addr += len(chunk) | ||||||
|  |     return "\n".join(res) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def build_tree(node, url): | ||||||
|  |     pak = get_loc(node)[0] | ||||||
|  |     subtree = Tree() | ||||||
|  |     path = url.toLocalFile().replace("/", "\\") | ||||||
|  |     if os.path.isfile(path): | ||||||
|  |         full_path = os.path.abspath(path) | ||||||
|  |         path, file = os.path.split(full_path) | ||||||
|  |         loc = os.path.join(get_loc(node)[1], file) | ||||||
|  |         root_node = subtree[pak] | ||||||
|  |         for elem in loc.split("/"): | ||||||
|  |             root_node = root_node[elem] | ||||||
|  |         root_node.data = Container(path=bytes(loc, "utf-8"), size=os.path.getsize( | ||||||
|  |             full_path), offset=0) | ||||||
|  |         root_node.size = root_node.data.size | ||||||
|  |         root_node.path = full_path | ||||||
|  |     else: | ||||||
|  |         for root, _, files in os.walk(path): | ||||||
|  |             root = root.replace("/", os.sep) | ||||||
|  |             for file in files: | ||||||
|  |                 full_path = os.path.join(root, file) | ||||||
|  |                 loc = full_path.replace(path.replace( | ||||||
|  |                     "/", os.sep), "").strip(os.sep) | ||||||
|  |                 loc = os.path.join(os.path.split( | ||||||
|  |                     path)[-1], loc).replace(os.sep, "/") | ||||||
|  |                 root_node = subtree[pak] | ||||||
|  |                 for elem in loc.split("/"): | ||||||
|  |                     root_node = root_node[elem] | ||||||
|  |                 root_node.data = Container(path=bytes(loc, "utf-8"), size=os.path.getsize( | ||||||
|  |                     full_path), offset=0) | ||||||
|  |                 root_node.size = root_node.data.size | ||||||
|  |                 root_node.path = full_path | ||||||
|  |     return subtree[pak] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class DataTree(QTreeWidget): | ||||||
|  |     def dragEnterEvent(self, event): | ||||||
|  |         m = event.mimeData() | ||||||
|  |         if m.hasUrls(): | ||||||
|  |             for url in m.urls(): | ||||||
|  |                 if url.isLocalFile(): | ||||||
|  |                     event.accept() | ||||||
|  |                     return | ||||||
|  |         event.accept() | ||||||
|  | 
 | ||||||
|  |     def dragMoveEvent(self, event): | ||||||
|  |         if event.mimeData().hasUrls: | ||||||
|  |             event.setDropAction(QtCore.Qt.CopyAction) | ||||||
|  |         else: | ||||||
|  |             event.setDropAction(QtCore.Qt.MoveAction) | ||||||
|  |         event.accept() | ||||||
|  | 
 | ||||||
|  |     def dropEvent(self, event): | ||||||
|  |         editor = self.window().editor | ||||||
|  |         target = self.itemAt(event.pos()) | ||||||
|  |         if target.struct: | ||||||
|  |             return | ||||||
|  |         print("Drop: ", get_loc(target)) | ||||||
|  |         if event.source(): | ||||||
|  |             selected = event.source().selectedItems() | ||||||
|  |             for item in selected: | ||||||
|  |                 labels = [item.text(i) for i in range(item.columnCount())] | ||||||
|  |                 node = QTreeWidgetItem(target, labels) | ||||||
|  |                 node.struct = item.struct | ||||||
|  |                 node.struct.offset = 0 | ||||||
|  |                 node.path = item.path | ||||||
|  |                 node.pak = item.pak | ||||||
|  |         else: | ||||||
|  |             m = event.mimeData() | ||||||
|  |             if m.hasUrls(): | ||||||
|  |                 for url in m.urls(): | ||||||
|  |                     tree = build_tree(target, url) | ||||||
|  |                     editor.make_subtree(target, tree, target.pak) | ||||||
|  |         editor.update_tree() | ||||||
|  |         event.accept() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class DataLoader(QThread): | ||||||
|  |     signal = pyqtSignal('PyQt_PyObject') | ||||||
|  | 
 | ||||||
|  |     def __init__(self, path, subtree=None): | ||||||
|  |         super().__init__() | ||||||
|  |         self.path = path | ||||||
|  | 
 | ||||||
|  |     def run(self): | ||||||
|  |         self.signal.emit((self.path, load_data(self.path))) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class DataExtractor(QThread): | ||||||
|  |     signal = pyqtSignal('PyQt_PyObject') | ||||||
|  | 
 | ||||||
|  |     def __init__(self, nodes, dest): | ||||||
|  |         super().__init__() | ||||||
|  |         self.dest = dest | ||||||
|  |         self.nodes = nodes | ||||||
|  | 
 | ||||||
|  |     def run(self): | ||||||
|  |         for node in self.nodes: | ||||||
|  |             for ch in walk(node): | ||||||
|  |                 if ch.struct: | ||||||
|  |                     self.__extract(ch, self.dest) | ||||||
|  |         self.signal.emit("Done!") | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  |     def __extract(self, node, dest): | ||||||
|  |         path = get_path(node.struct) | ||||||
|  |         self.signal.emit("Extracting {}".format(path)) | ||||||
|  |         folder, file = os.path.split(path) | ||||||
|  |         folder = os.path.join(dest, folder) | ||||||
|  |         folder = folder.replace("/", os.sep).replace("\\", os.sep) | ||||||
|  |         file = os.path.join(folder, file) | ||||||
|  |         os.makedirs(folder, exist_ok=True) | ||||||
|  |         with open(node.pak, "rb") as pak: | ||||||
|  |             with open(file, "wb") as of: | ||||||
|  |                 pak.seek(node.struct.offset) | ||||||
|  |                 of.write(pak.read(node.struct.size)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class PackedEditor(QWidget): | ||||||
|  |     def __init__(self, parent=None): | ||||||
|  |         super(PackedEditor, self).__init__(parent) | ||||||
|  |         self.initUI() | ||||||
|  |         self.data = {} | ||||||
|  |         self.threads = [] | ||||||
|  |         self.log_mutex = QMutex() | ||||||
|  | 
 | ||||||
|  |     def initUI(self): | ||||||
|  |         self.initLayout() | ||||||
|  |         self.show() | ||||||
|  | 
 | ||||||
|  |     def initLayout(self): | ||||||
|  |         grid = QGridLayout() | ||||||
|  |         hgrid = QSplitter(Qt.Horizontal) | ||||||
|  |         vgrid = QSplitter(Qt.Vertical) | ||||||
|  |         patcher_grid = QGridLayout() | ||||||
|  | 
 | ||||||
|  |         self.info_tab_hex = QTextEdit() | ||||||
|  |         self.info_tab_text = QTextEdit() | ||||||
|  |         self.info_tab_info = QTextEdit() | ||||||
|  |         self.info_tabs = QTabWidget() | ||||||
|  | 
 | ||||||
|  |         self.info_tab_hex.setReadOnly(True) | ||||||
|  |         self.info_tab_text.setReadOnly(True) | ||||||
|  |         self.info_tab_info.setReadOnly(True) | ||||||
|  | 
 | ||||||
|  |         self.info_tabs.addTab(self.info_tab_info, "Info") | ||||||
|  |         self.info_tabs.addTab(self.info_tab_text, "Text") | ||||||
|  |         self.info_tabs.addTab(self.info_tab_hex, "Hexdump") | ||||||
|  | 
 | ||||||
|  |         self.util_tabs = QTabWidget() | ||||||
|  | 
 | ||||||
|  |         self.util_tab_log = QTextEdit() | ||||||
|  |         self.util_tab_log.setReadOnly(True) | ||||||
|  | 
 | ||||||
|  |         self.util_tab_patch = QWidget() | ||||||
|  | 
 | ||||||
|  |         self.util_tab_patch.setLayout(patcher_grid) | ||||||
|  |         self.util_tabs.addTab(self.util_tab_log, "Log") | ||||||
|  |         self.util_tabs.addTab(self.util_tab_patch, "Patcher") | ||||||
|  |         self.patcher = Patcher(self) | ||||||
|  |         i = 0 | ||||||
|  |         patcher_grid_w = 0 | ||||||
|  |         for func in dir(self.patcher): | ||||||
|  |             if func.startswith("__") or not hasattr(getattr(self.patcher, func), "__call__"): | ||||||
|  |                 continue | ||||||
|  |             patcher_grid_w += 1 | ||||||
|  |         patcher_grid_w = int(patcher_grid_w**0.5) | ||||||
|  |         for func in dir(self.patcher): | ||||||
|  |             if func.startswith("__") or not hasattr(getattr(self.patcher, func), "__call__"): | ||||||
|  |                 continue | ||||||
|  |             func = getattr(self.patcher, func) | ||||||
|  |             x, y = divmod(i, patcher_grid_w) | ||||||
|  |             button = QPushButton(func.__name__.replace("_", " ").title()) | ||||||
|  |             button.setToolTip(func.__doc__) | ||||||
|  |             button.clicked.connect(func) | ||||||
|  |             patcher_grid.addWidget(button, x, y) | ||||||
|  |             i += 1 | ||||||
|  | 
 | ||||||
|  |         self.tree = DataTree() | ||||||
|  |         self.tree.setHeaderLabels(["Path", "Size", "Offset"]) | ||||||
|  |         self.tree.setSelectionMode(self.tree.ExtendedSelection) | ||||||
|  |         self.tree.currentItemChanged.connect(self.tree_changed) | ||||||
|  |         self.tree.setDragDropMode(self.tree.DragDrop | self.tree.InternalMove) | ||||||
|  |         self.tree.setDragEnabled(True) | ||||||
|  |         self.tree.setAcceptDrops(True) | ||||||
|  |         self.tree.setDropIndicatorShown(True) | ||||||
|  | 
 | ||||||
|  |         hgrid.addWidget(self.tree) | ||||||
|  |         hgrid.addWidget(self.info_tabs) | ||||||
|  |         vgrid.addWidget(hgrid) | ||||||
|  |         vgrid.addWidget(self.util_tabs) | ||||||
|  |         grid.addWidget(vgrid) | ||||||
|  |         self.setLayout(grid) | ||||||
|  | 
 | ||||||
|  |     def info(self, msg): | ||||||
|  |         lock = QMutexLocker(self.log_mutex) | ||||||
|  |         self.util_tab_log.insertPlainText(msg + "\n") | ||||||
|  |         self.util_tab_log.moveCursor(QTextCursor.End) | ||||||
|  | 
 | ||||||
|  |     def load(self, path): | ||||||
|  |         if path in self.data: | ||||||
|  |             return | ||||||
|  |         self.data[path] = DataLoader(path) | ||||||
|  |         self.data[path].signal.connect(self.done_loading) | ||||||
|  |         self.data[path].start() | ||||||
|  |         self.info("Loading {}".format(path)) | ||||||
|  | 
 | ||||||
|  |     def done_loading(self, result): | ||||||
|  |         path, data = result | ||||||
|  |         self.data[path] = data | ||||||
|  |         self.make_subtree(self.tree, data, path) | ||||||
|  | 
 | ||||||
|  |     def clear(self): | ||||||
|  |         self.data.clear() | ||||||
|  |         self.tree.clear() | ||||||
|  |         self.info_tab_hex.setText("") | ||||||
|  |         self.info_tab_text.setText("") | ||||||
|  |         self.info_tab_info.setText("") | ||||||
|  |         self.util_tab_log.setText("") | ||||||
|  | 
 | ||||||
|  |     def make_tree(self): | ||||||
|  |         for pak in self.data: | ||||||
|  |             if isinstance(self.data[pak], DataLoader): | ||||||
|  |                 continue | ||||||
|  |             self.make_subtree(self.tree, self.data[pak], pak) | ||||||
|  | 
 | ||||||
|  |     def update_tree(self, node=None): | ||||||
|  |         total_size = 0 | ||||||
|  |         if node is None: | ||||||
|  |             node = self.tree.invisibleRootItem() | ||||||
|  |         else: | ||||||
|  |             if node.struct: | ||||||
|  |                 total_size += node.struct.size | ||||||
|  |         for child_idx in range(node.childCount()): | ||||||
|  |             total_size += self.update_tree(node.child(child_idx)) | ||||||
|  |         node.setText(1, fsize(total_size)) | ||||||
|  |         return total_size | ||||||
|  | 
 | ||||||
|  |     @classmethod | ||||||
|  |     def make_subtree(cls, tree, data, pak): | ||||||
|  |         total_size = 0 | ||||||
|  |         for name, children in sorted(data.items()): | ||||||
|  |             total_size += children.size | ||||||
|  |             if children.data: | ||||||
|  |                 labels = [name, fsize(children.size), | ||||||
|  |                           hex(children.data.offset)] | ||||||
|  |             else: | ||||||
|  |                 labels = [name, fsize(children.size), ""] | ||||||
|  |             node = QTreeWidgetItem(tree, labels) | ||||||
|  |             node.struct = children.data | ||||||
|  |             node.path = children.path | ||||||
|  |             node.pak = pak | ||||||
|  |             cls.make_subtree(node, children, pak) | ||||||
|  | 
 | ||||||
|  |     def contextMenuEvent(self, event): | ||||||
|  |         cmenu = QMenu(self) | ||||||
|  |         actions = {} | ||||||
|  |         actions[cmenu.addAction("Extract")] = self.extract_handler | ||||||
|  |         actions[cmenu.addAction("Delete")] = self.del_handler | ||||||
|  |         if all(node.parent() == None for node in self.selected()): | ||||||
|  |             actions[cmenu.addAction("Save")] = self.save_handler | ||||||
|  |         try: | ||||||
|  |             act_fn = actions.get( | ||||||
|  |                 cmenu.exec_(self.mapToGlobal(event.pos()))) | ||||||
|  |         except TypeError: | ||||||
|  |             return | ||||||
|  |         if not act_fn: | ||||||
|  |             return | ||||||
|  |         th = act_fn(self.selected()) | ||||||
|  |         if th: | ||||||
|  |             th.signal.connect(self.info) | ||||||
|  |             th.start() | ||||||
|  |             self.threads.append(th) | ||||||
|  | 
 | ||||||
|  |     def extract_handler(self, nodes): | ||||||
|  |         dest = QFileDialog.getExistingDirectory( | ||||||
|  |             self, 'Open file', os.getcwd()) | ||||||
|  |         if not nodes: | ||||||
|  |             return | ||||||
|  |         if not dest: | ||||||
|  |             return | ||||||
|  |         return DataExtractor(nodes, dest) | ||||||
|  | 
 | ||||||
|  |     def make_packed(self, node, path=None): | ||||||
|  |         a = PackerWindow(node, path) | ||||||
|  |         a.exec_() | ||||||
|  | 
 | ||||||
|  |     def save_handler(self, nodes): | ||||||
|  |         for node in nodes: | ||||||
|  |             path = QFileDialog.getSaveFileName( | ||||||
|  |                 self, 'Save file', node.pak, "Scrapland Data Files (*.packed)")[0] | ||||||
|  |             self.make_packed(node, path) | ||||||
|  | 
 | ||||||
|  |     def del_handler(self, nodes): | ||||||
|  |         if not nodes: | ||||||
|  |             return | ||||||
|  |         root = self.tree.invisibleRootItem() | ||||||
|  |         for node in nodes: | ||||||
|  |             (node.parent() or root).removeChild(node) | ||||||
|  |         self.update_tree() | ||||||
|  | 
 | ||||||
|  |     def get_info(self, node): | ||||||
|  |         info = [] | ||||||
|  |         if node.struct: | ||||||
|  |             info.append(("Size", node.struct.size)) | ||||||
|  |             info.append(("Offset", hex(node.struct.offset))) | ||||||
|  |             info.append(("Path", get_path(node.struct))) | ||||||
|  |         ret = "" | ||||||
|  |         for k, v in info: | ||||||
|  |             ret += "{}:\t{}\n".format(k, v) | ||||||
|  |         return ret.strip() | ||||||
|  | 
 | ||||||
|  |     def tree_changed(self, new, old): | ||||||
|  |         if not new: | ||||||
|  |             return | ||||||
|  |         self.info_tab_text.setText("") | ||||||
|  |         self.info_tab_hex.setText("") | ||||||
|  |         self.info_tab_info.setText(self.get_info(new)) | ||||||
|  |         data, more = get_data(new, 1024) | ||||||
|  |         if data: | ||||||
|  |             more_text = "\n<...>" if more else "" | ||||||
|  |             self.info_tab_hex.setText(hexdump(data)+more_text) | ||||||
|  |             try: | ||||||
|  |                 data = str(data, "cp1252") | ||||||
|  |             except UnicodeDecodeError: | ||||||
|  |                 data = repr(data) | ||||||
|  |             self.info_tab_text.setText(data+more_text) | ||||||
|  |             self.info_tab_info.setText(self.get_info(new)) | ||||||
|  | 
 | ||||||
|  |     def selected(self): | ||||||
|  |         return self.tree.selectedItems() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def main(): | ||||||
|  |     app = QApplication(sys.argv) | ||||||
|  |     win = MainWindow() | ||||||
|  |     win.show() | ||||||
|  |     sys.exit(app.exec_()) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | if __name__ == '__main__': | ||||||
|  |     main() | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue