From 88cd7adf23e4bfa0099340897af8d47a3c96cffd Mon Sep 17 00:00:00 2001 From: modeco80 Date: Thu, 9 Nov 2023 18:18:05 -0500 Subject: [PATCH] init versions --- README.md | 3 + rolling_wad_extract.py | 163 +++++++++++++++++++++++++++++++++++++++++ tg_dir_xtract.py | 91 +++++++++++++++++++++++ tg_menuhelpers.py | 112 ++++++++++++++++++++++++++++ 4 files changed, 369 insertions(+) create mode 100644 README.md create mode 100755 rolling_wad_extract.py create mode 100755 tg_dir_xtract.py create mode 100755 tg_menuhelpers.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..8458794 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Misc + +Misc tools that don't really deserve their own repository yet diff --git a/rolling_wad_extract.py b/rolling_wad_extract.py new file mode 100755 index 0000000..d483400 --- /dev/null +++ b/rolling_wad_extract.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 + +# PoC mostly working roll_p.wad extractor +# (C) 2023 Lily Tsuru + +# The following ImHex pattern was used to help write this tool. +""" +#pragma eval_depth 2048 + +struct PakEnt { + u32 blockOffset; // 0 = directory, in "blocks" 0x2000 byte units + u32 nextEnt; // next file (bytes) + u32 nextLink; // next directory (bytes) + u32 byteSize; // -1 = directory (in bytes) + char name[]; // filename + + if(nextLink != 0x0) + PakEnt nextLinkEnt @ nextLink; + + if(nextEnt != 0x0) + PakEnt nextEnt @ nextEnt; + +}; + +u32 headerBlockSize @ 0x0; +PakEnt ent @ 0x4; +""" + +import struct +import sys +import os +from pathlib import Path, PurePath +from enum import Enum + +# converts a block count to bytes. +def blocks_to_bytes(block_count: int): + return block_count * 2048 + +def read_asciiz(file): # FUCK python i swear + lastpos = file.tell() + len = 0 + c = 0 + s = "" + + while True: + c = file.read(1) + if c[0] == 0: + break + s += c.decode('ascii') + + # go back + file.seek(lastpos, os.SEEK_SET) + return s + +class WadEntryType(Enum): + """ A file. """ + FILE = 0 + + """ A directory. """ + DIRECTORY = 1 + + # ugliness for argparse + def __str__(self): + return self.name + + @staticmethod + def from_string(s): + try: + return WadEntryType[s] + except KeyError: + raise ValueError() + +class WadHeader: # wad file header + def __init__(self, file): + data = struct.unpack("I", file.read(4)) + self.header_block_count = data[0] + +class WadEntry: # a wad file entry + def __init__(self, file): + self._file = file + data = struct.unpack("IIII", file.read(4*4)) + self.block_offset = data[0] # offset in blocks (see above) + self.next_file = data[1] # used in files to go to next file in folder + self.next_link = data[2] # used in folders to go to first entry + self.size_bytes = data[3] # data size in bytes. 0x80000000 for folders + self.name = read_asciiz(file) # pretty self explainatory + + if self.size_bytes == 0x80000000: + self.type = WadEntryType.DIRECTORY + else: + self.type = WadEntryType.FILE + + self.children = [] + + + # Bit of a bug right now but it seems to work. + def read_children(self): + ofs = self.next_link + + #print(f'seeking to {ofs:x}') + self._file.seek(ofs, os.SEEK_SET) + + # read all of the children nodes + entry = WadEntry(self._file) + entry.parent = self + + while True: + #print(entry.name, entry.type, "parent:", entry.parent.name) + # let child directory read its children + if entry.type == WadEntryType.DIRECTORY: + #print(entry.name, "reading children") + entry.read_children() + + + if entry.next_file != 0: + #print(f'seeking to {entry.next_file:x} (loop)') + self._file.seek(entry.next_file, os.SEEK_SET) + + if entry.next_file == 0 and entry.next_link == 0: + #print ("done reading", self.name) + self._file.seek(ofs, os.SEEK_SET) + self.children.append(entry) + return + else: + self.children.append(entry) + entry = WadEntry(self._file) + entry.parent = self + + def dump(self, pathObj): + self._file.seek(blocks_to_bytes(self.block_offset), os.SEEK_SET) + path = str(pathObj) + with(open(path, "wb")) as file: + file.write(self._file.read(self.size_bytes)) + print(f'Wrote file {self.name} ({self.size_bytes} bytes) to {path}.') + file.close() + + def walk(self, rootPath): + result = [] + for child in self.children: + child_path = rootPath / child.name.lower() + result.append((child_path, child)) + result.extend(child.walk(child_path)) + return result + + +# """""main loop""""" + +rootPath = Path(os.getcwd()) / Path(sys.argv[1]).stem + +# make root first +rootPath.mkdir(exist_ok=True) + +with open(sys.argv[1], "rb") as file: + header = WadHeader(file) + root = WadEntry(file) + root.read_children() + + for tup in root.walk(rootPath): + if tup[1].type == WadEntryType.FILE: + tup[1].dump(tup[0]) + else: + #print("mkdir:", tup[0], tup[1].name) + (tup[0]).mkdir(parents=True, exist_ok=True) diff --git a/tg_dir_xtract.py b/tg_dir_xtract.py new file mode 100755 index 0000000..3a24dff --- /dev/null +++ b/tg_dir_xtract.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 + +# A very very POC .DIR extractor +# I probably won't continue using python +# this is just a test because I hate quickbms dearly + +import ctypes +import sys +import os +from pathlib import Path, PurePath + + +class CoolStructure(ctypes.Structure): + def __repr__(self) -> str: + values = ", ".join(f"{name}={value}" for name, value in self._asdict().items()) + return f"<{self.__class__.__name__}: {values}>" + def _asdict(self) -> dict: + return {field[0]: getattr(self, field[0]) for field in self._fields_} + +# DIR structures. Note that I'm not using the Python `struct` module for brevity. + +class DirHeader(CoolStructure): + _fields_ = [ + ('magic', ctypes.c_char * 4), # "DIRF" + ('pad', ctypes.c_uint32), + ('version', ctypes.c_char * 4), # version (2.2d) + ('fileCount', ctypes.c_uint32), # count of files + ('pad_2', ctypes.c_char * 0x30), + ] + + def valid(self) -> bool: + return self.magic == b'DIRF' + + def version(self) -> str: + return self.version.decode('ascii') + + +class DirTocEntry(CoolStructure): + _fields_ = [ + ('blockOffset', ctypes.c_uint32), # offset in 0x800 "blocks" + ('size', ctypes.c_uint32), # size in bytes + ('_fileName', ctypes.c_char * 0x38) # filename + ] + + def fileName(self) -> str: + return self._fileName.decode('ascii') + + def byteOffset(self) -> int: + return self.blockOffset * 0x800 + + def getData(self, file) -> bytes: + ret = bytearray(self.size) + lastOffset = file.tell() + file.seek(self.byteOffset(), os.SEEK_SET) + file.readinto(ret) + file.seek(lastOffset, os.SEEK_SET) + return ret + + + +# """""main loop""""" + + +def makePath(filename): + return Path(os.getcwd()) / Path(sys.argv[1]).stem / filename + +def makeFolderPath(): + (Path(os.getcwd()) / Path(sys.argv[1]).stem).mkdir(exist_ok=True) + +with open(sys.argv[1], "rb") as file: + header = DirHeader() + file.readinto(header) + if header.valid(): + makeFolderPath() + + toc = [] + for i in range(0, header.fileCount): # hacky and slow probably but whatever + entry = DirTocEntry() + file.readinto(entry) + toc.append(entry) + + # read the TOC out + print(f'going to extract {header.fileCount} files') + for item in toc: + name = item.fileName() + data = item.getData(file) + path = makePath(name) + + with open(str(path), "wb") as outFile: + outFile.write(data) + print(f' wrote \'{str(path)}\' size {item.size} bytes') diff --git a/tg_menuhelpers.py b/tg_menuhelpers.py new file mode 100755 index 0000000..3f147d6 --- /dev/null +++ b/tg_menuhelpers.py @@ -0,0 +1,112 @@ +import gdb +import struct + +# address for all platforms (us only atm) +TG_MENU_ID_ADDR = { + 'us': 0x0036b1e8 +} +tg_lang = 'us' + +# TODO: maybe symbolize common states + +STATE_NAME_TABLE = { + 0: 'Invalid', + 1: 'Pause (Underground)', + 2: 'Pause (Arcade)', + 3: 'Pause (Unknown?)', # dunno bout these ones + 4: 'Pause (Unknown 2?)', + 5: 'Invalid', + 6: 'Rerun Ended', + + 0x7: 'Confirm Quit', + 0x8: 'Confirm Restart', # dups? + 0x9: 'Confirm Restart', + + 0xa: 'Save Game', + 0xb: 'Overwrite Game', + + 0xc: 'Debug Menu', + 0xd: 'Other Debug Settings', + 0xe: 'Camera Debug', + 0xf: 'Invalid', # maybe another debug options if I had to guess + 0x10: 'Rendering Options', + + 0x11: 'Controller Configuration', + + 0x12: 'Arcade High Scores', + 0x14: 'Audio Settings', + 0x15: 'Preferences', + + 0x18: 'Debug Shell', + 0x19: 'Shell', + + 0x1a: 'Stats Debug', + + 0x1b: 'Check Poly Totals', + 0x1c: 'Memory/Polygon Budget', + 0x1d: 'Permanent Allocation', + 0x1e: 'Shell Allocation', + 0x1f: 'Game Allocation', + + 0x20: 'Win', + 0x21: 'Failed', + 0x22: 'Survived', + 0x23: 'Dead', + 0x24: 'Won', + 0x25: 'Lost', + 0x26: 'Won dupe', + 0x27: 'Lost dupe', + + 0x28: 'Rerun Finished', + + 0x29: 'Time Up', + 0x30: 'Arcade' +} + +def read_4b_value(address): + inferior = gdb.selected_inferior() + data = inferior.read_memory(address, 4) + return struct.unpack('