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