init versions

This commit is contained in:
Lily Tsuru 2023-11-09 18:18:05 -05:00
commit 88cd7adf23
4 changed files with 369 additions and 0 deletions

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# Misc
Misc tools that don't really deserve their own repository yet

163
rolling_wad_extract.py Executable file
View File

@ -0,0 +1,163 @@
#!/usr/bin/env python3
# PoC mostly working roll_p.wad extractor
# (C) 2023 Lily Tsuru <lily@crustywindo.ws>
# 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)

91
tg_dir_xtract.py Executable file
View File

@ -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')

112
tg_menuhelpers.py Executable file
View File

@ -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('<I', data)[0]
# or probably a class which wraps over gdb inferior to do stuff with it
# a bit easier. idk.
def write_4b_value(address, value):
inferior = gdb.selected_inferior()
packed_byte = struct.pack('<I', value)
inferior.write_memory(address, packed_byte)
class tgGetMenuState(gdb.Command):
def __init__(self):
super(tgGetMenuState, self).__init__("toxic-get-menu", gdb.COMMAND_USER)
def invoke(self, arg, tty):
state = read_4b_value(TG_MENU_ID_ADDR[tg_lang])
if state in STATE_NAME_TABLE:
print(f'Menu state is currently: {STATE_NAME_TABLE[state]} (0x{state:08x})')
else:
print(f'Menu state is currently: 0x{state:08x}')
class tgSetMenuState(gdb.Command):
def __init__(self):
super(tgSetMenuState, self).__init__("toxic-set-menu", gdb.COMMAND_USER)
def invoke(self, arg, tty):
args = gdb.string_to_argv(arg)
if len(args) == 1:
val = 0
try:
val = int(args[0])
except:
try:
val = int(args[0], base=16)
except:
raise ValueError('argument is not an number')
pass
write_4b_value(TG_MENU_ID_ADDR[tg_lang], val)
print(f'set Menu state 0x{val:08x}')
gdb.execute("cont", True) # continue immediately, a quick nicity
else:
raise ValueError('i need one argument please')
tgGetMenuState()
tgSetMenuState()