Files
tp/tools/extract_game_assets.py
T

274 lines
8.1 KiB
Python
Raw Normal View History

2021-06-13 19:07:06 -04:00
import os
import sys
2023-01-23 20:45:57 -07:00
import libarc
from pathlib import Path
from oead import yaz0
2021-06-13 19:07:06 -04:00
"""
Extracts the game assets and stores them in the game folder
Usage: `python tools/extract_game_assets.py`
"""
fstInfoPosition = 0x424
2022-01-15 17:59:59 -07:00
bootPosition = 0x0
bootSize = 0x440
bi2Position = 0x440
bi2Size = 0x2000
apploaderPosition = 0x2440
dolInfoPosition = 0x420
2021-06-13 19:07:06 -04:00
numFileEntries = 0
"""
Returns the offset address and size of fst.bin
"""
2021-12-02 23:38:37 +01:00
2021-06-13 19:07:06 -04:00
def getFstInfo(handler, fstOffsetPosition):
2021-12-02 23:38:37 +01:00
fstOffset = int.from_bytes(bytearray(handler.read(4)), byteorder="big")
handler.seek(
fstOffsetPosition + 4
) # Get the size which is 4 bytes after the offset
fstSize = int.from_bytes(bytearray(handler.read(4)), byteorder="big")
2021-06-13 19:07:06 -04:00
return fstOffset, fstSize
2021-12-02 23:38:37 +01:00
2021-06-13 19:07:06 -04:00
"""
Parses the fst.bin into a list of dictionaries containing
2021-06-13 19:47:12 -04:00
the file entry type, the file/folder name, the ISO file offset/parent file entry, the file size/last file entry
2021-06-13 19:07:06 -04:00
"""
2021-12-02 23:38:37 +01:00
2021-06-13 19:07:06 -04:00
def parseFstBin(fstBinBytes):
currentByte = 0
2021-12-02 23:38:37 +01:00
numFileEntries = int.from_bytes(
fstBinBytes[10:12], byteorder="big"
) # fst.bin offset
2021-06-13 19:07:06 -04:00
stringTableOffset = numFileEntries * 0xC
ret = []
2021-12-02 23:38:37 +01:00
while currentByte != (numFileEntries * 12):
2021-06-13 19:07:06 -04:00
currentByte += 12
# lazy
2021-12-02 23:38:37 +01:00
if currentByte == (numFileEntries * 12):
2021-06-13 19:07:06 -04:00
break
fileFolder = fstBinBytes[currentByte]
2021-12-02 23:38:37 +01:00
filenameOffset = int.from_bytes(
fstBinBytes[currentByte + 1 : currentByte + 4], byteorder="big"
)
fileOffsetOrParentEntryNum = int.from_bytes(
fstBinBytes[currentByte + 4 : currentByte + 8], byteorder="big"
)
fileSizeOrLastEntryNum = int.from_bytes(
fstBinBytes[currentByte + 8 : currentByte + 12], byteorder="big"
)
currentFilenameOffset = stringTableOffset + filenameOffset
2021-06-13 19:07:06 -04:00
# Figure out the filename by checking for null string terminator
i = 0
2021-12-02 23:38:37 +01:00
while fstBinBytes[currentFilenameOffset + i] != 0:
2021-06-13 19:07:06 -04:00
i += 1
2021-12-02 23:38:37 +01:00
fileName = (
fstBinBytes[currentFilenameOffset : currentFilenameOffset + i]
).decode()
2021-06-13 19:07:06 -04:00
if fileFolder == 0:
2021-12-02 23:38:37 +01:00
ret.append(
{
"type": "File",
"fileName": fileName,
"fileOffset": fileOffsetOrParentEntryNum,
"fileSize": fileSizeOrLastEntryNum,
}
)
2021-06-13 19:07:06 -04:00
else:
2021-12-02 23:38:37 +01:00
ret.append(
{
"type": "Folder",
"folderName": fileName,
"parentFolderEntryNumber": fileOffsetOrParentEntryNum,
"lastEntryNumber": fileSizeOrLastEntryNum,
}
)
2021-06-13 19:07:06 -04:00
return ret
2021-12-02 23:38:37 +01:00
2021-06-13 19:07:06 -04:00
"""
Write the current folder to disk and return it's name/last entry number
"""
2021-12-02 23:38:37 +01:00
def writeFolder(parsedFstBin, i):
2022-12-30 19:00:42 -07:00
folderPath = i["folderName"] + "/"
2021-06-13 19:07:06 -04:00
lastEntryNumber = i["lastEntryNumber"]
if i["parentFolderEntryNumber"] == 0:
if not os.path.exists(folderPath):
os.makedirs(folderPath)
else:
2021-12-02 23:38:37 +01:00
parentFolderEntry = parsedFstBin[i["parentFolderEntryNumber"] - 1]
2021-06-13 19:07:06 -04:00
while True:
folderPath = parentFolderEntry["folderName"] + "/" + folderPath
if parentFolderEntry["parentFolderEntryNumber"] == 0:
break
nextParentFolderEntryNumber = parentFolderEntry["parentFolderEntryNumber"]
2021-12-02 23:38:37 +01:00
parentFolderEntry = parsedFstBin[nextParentFolderEntryNumber - 1]
2021-06-13 19:07:06 -04:00
if not os.path.exists(folderPath):
os.makedirs(folderPath)
return folderPath, lastEntryNumber
2021-12-02 23:38:37 +01:00
2021-06-13 19:07:06 -04:00
"""
Use the parsed fst.bin contents to write assets to file
"""
2021-12-02 23:38:37 +01:00
2023-01-23 20:45:57 -07:00
convertDefinitions = [
2023-01-23 21:33:56 -07:00
{
"extension": ".arc",
"function": libarc.extract_to_directory,
"exceptions": ["archive/dat/speakerse.arc"],
}
2023-01-23 20:45:57 -07:00
]
2023-01-23 21:33:56 -07:00
def writeFile(name, data):
if data[0:4] == bytes("Yaz0", "ascii"):
2023-01-23 20:45:57 -07:00
splitName = os.path.splitext(name)
2023-01-23 21:33:56 -07:00
name = splitName[0] + ".c" + splitName[1]
2023-01-23 20:45:57 -07:00
data = yaz0.decompress(data)
extractDef = None
splitName = os.path.splitext(name)
ext = splitName[1]
for extractData in convertDefinitions:
if ext == extractData["extension"]:
extractDef = extractData
if extractData["exceptions"] != None:
for exception in extractData["exceptions"]:
2023-01-23 21:33:56 -07:00
if str(name) == exception:
2023-01-23 20:45:57 -07:00
extractDef = None
break
2023-01-23 21:33:56 -07:00
2023-01-23 20:45:57 -07:00
if extractDef == None:
2023-01-23 21:33:56 -07:00
file = open(name, "wb")
2023-01-23 20:45:57 -07:00
file.write(data)
file.close()
else:
2023-01-23 21:33:56 -07:00
name = extractDef["function"](name, data, writeFile)
2023-01-23 20:45:57 -07:00
return name
2023-01-23 21:33:56 -07:00
2021-12-02 23:38:37 +01:00
2021-06-13 19:07:06 -04:00
def writeAssets(parsedFstBin, handler):
# Write the folder structure and files to disc
j = 0
folderStack = []
2022-01-15 17:59:59 -07:00
if not os.path.exists("./files/"):
2022-12-30 19:00:42 -07:00
os.makedirs("./files/")
os.chdir("./files/")
2021-06-13 19:07:06 -04:00
folderStack.append({"folderName": "./", "lastEntryNumber": numFileEntries})
for i in parsedFstBin:
j += 1
if i["type"] == "Folder":
2021-12-02 23:38:37 +01:00
currentFolder, lastEntryNumber = writeFolder(parsedFstBin, i)
folderStack.append(
{"folderName": currentFolder, "lastEntryNumber": lastEntryNumber}
)
2021-06-13 19:07:06 -04:00
else:
handler.seek(i["fileOffset"])
2023-01-23 21:33:56 -07:00
writeFile(
folderStack[-1]["folderName"] + i["fileName"],
bytearray(handler.read(i["fileSize"])),
)
# with open(
2023-01-23 20:45:57 -07:00
# (folderStack[-1]["folderName"] + i["fileName"]), "wb"
2023-01-23 21:33:56 -07:00
# ) as currentFile:
2023-01-23 20:45:57 -07:00
# currentFile.write(bytearray(handler.read(i["fileSize"])))
2021-12-02 23:38:37 +01:00
while folderStack[-1]["lastEntryNumber"] == j + 1:
2021-06-13 19:07:06 -04:00
folderStack.pop()
2022-12-30 19:00:42 -07:00
def writeSys(boot, bi2, apploader, dol, fst):
2022-01-15 17:59:59 -07:00
if not os.path.exists("./sys/"):
os.makedirs("./sys/")
2022-12-30 19:00:42 -07:00
open("./sys/boot.bin", "wb").write(boot)
open("./sys/bi2.bin", "wb").write(bi2)
open("./sys/apploader.img", "wb").write(apploader)
open("./sys/main.dol", "wb").write(dol)
open("./sys/fst.bin", "wb").write(fst)
2022-01-15 17:59:59 -07:00
def getDolInfo(disc):
disc.seek(dolInfoPosition)
dolOffset = int.from_bytes(bytearray(disc.read(4)), byteorder="big")
dolSize = 0
for i in range(7):
2022-12-30 19:00:42 -07:00
disc.seek(dolOffset + (i * 4))
2022-01-15 17:59:59 -07:00
segmentOffset = int.from_bytes(bytearray(disc.read(4)), byteorder="big")
2022-12-30 19:00:42 -07:00
disc.seek(dolOffset + 0x90 + (i * 4))
2022-01-15 17:59:59 -07:00
segmentSize = int.from_bytes(bytearray(disc.read(4)), byteorder="big")
2022-12-30 19:00:42 -07:00
if (segmentOffset + segmentSize) > dolSize:
2022-01-15 17:59:59 -07:00
dolSize = segmentOffset + segmentSize
2022-12-30 19:00:42 -07:00
for i in range(11):
disc.seek(dolOffset + 0x1C + (i * 4))
dataOffset = int.from_bytes(bytearray(disc.read(4)), byteorder="big")
disc.seek(dolOffset + 0xAC + (i * 4))
dataSize = int.from_bytes(bytearray(disc.read(4)), byteorder="big")
if (dataOffset + dataSize) > dolSize:
dolSize = dataOffset + dataSize
return dolOffset, dolSize
2022-01-15 17:59:59 -07:00
2021-12-02 23:38:37 +01:00
def extract(path):
with open(path, "rb") as f:
2021-06-13 19:07:06 -04:00
# Seek to fst offset information and retrieve it
f.seek(fstInfoPosition)
2021-12-02 23:38:37 +01:00
fstOffset, fstSize = getFstInfo(f, fstInfoPosition)
2021-06-13 19:07:06 -04:00
2022-01-15 17:59:59 -07:00
f.seek(bootPosition)
bootBytes = bytearray(f.read(bootSize))
f.seek(bi2Position)
bi2Bytes = bytearray(f.read(bi2Size))
2022-12-30 19:00:42 -07:00
f.seek(apploaderPosition + 0x14)
2022-01-15 17:59:59 -07:00
apploaderSize = int.from_bytes(bytearray(f.read(4)), byteorder="big")
2022-12-30 19:00:42 -07:00
f.seek(apploaderPosition + 0x18)
2022-01-15 17:59:59 -07:00
trailerSize = int.from_bytes(bytearray(f.read(4)), byteorder="big")
apploaderMainSize = 0x20 + apploaderSize + trailerSize
f.seek(apploaderPosition)
apploaderBytes = bytearray(f.read(apploaderMainSize))
dolOffset, dolSize = getDolInfo(f)
f.seek(dolOffset)
dolBytes = bytearray(f.read(dolSize))
2021-06-13 19:07:06 -04:00
# Seek to fst.bin and retrieve it
f.seek(fstOffset)
fstBinBytes = bytearray(f.read(fstSize))
2022-12-30 19:00:42 -07:00
writeSys(bootBytes, bi2Bytes, apploaderBytes, dolBytes, fstBinBytes)
2022-01-15 17:59:59 -07:00
2021-06-13 19:07:06 -04:00
# Parse fst.bin
parsedFstBin = parseFstBin(fstBinBytes)
# Write assets to file
writeAssets(parsedFstBin, f)
2021-12-02 23:38:37 +01:00
def main():
2022-01-15 17:59:59 -07:00
extract(sys.argv[1])
2021-12-02 23:38:37 +01:00
2023-01-23 21:33:56 -07:00
2021-06-13 19:07:06 -04:00
if __name__ == "__main__":
2021-12-02 23:38:37 +01:00
main()