init versions
This commit is contained in:
commit
88cd7adf23
|
@ -0,0 +1,3 @@
|
|||
# Misc
|
||||
|
||||
Misc tools that don't really deserve their own repository yet
|
|
@ -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)
|
|
@ -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')
|
|
@ -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()
|
Loading…
Reference in New Issue