diff --git a/.gitignore b/.gitignore index b176aae8fd7..ae9d90c75bd 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,10 @@ vtable.lcf # Game Assets game/ +# Disk Images +*.iso +*.gcm + # Generated documentation docs/doxygen/ diff --git a/tools/extract_game_assets.py b/tools/extract_game_assets.py new file mode 100644 index 00000000000..b15e7408cb7 --- /dev/null +++ b/tools/extract_game_assets.py @@ -0,0 +1,123 @@ +import os +import sys + +""" +Extracts the game assets and stores them in the game folder +Usage: `python tools/extract_game_assets.py` +""" + +fstInfoPosition = 0x424 +numFileEntries = 0 + +""" +Returns the offset address and size of fst.bin +""" +def getFstInfo(handler, fstOffsetPosition): + 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') + return fstOffset, fstSize + +""" +Parses the fst.bin into a list of dictionaries containing +the file name, file offset into the ISO, the file size, +and the folder structure +""" +def parseFstBin(fstBinBytes): + currentByte = 0 + numFileEntries = int.from_bytes(fstBinBytes[10:12], byteorder='big') # fst.bin offset + stringTableOffset = numFileEntries * 0xC + + ret = [] + + while currentByte != (numFileEntries*12): + currentByte += 12 + + # lazy + if currentByte == (numFileEntries*12): + break + + fileFolder = fstBinBytes[currentByte] + 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 + + # Figure out the filename by checking for null string terminator + i = 0 + while fstBinBytes[currentFilenameOffset+i] != 0: + i += 1 + + fileName = (fstBinBytes[currentFilenameOffset:currentFilenameOffset+i]).decode() + + if fileFolder == 0: + ret.append({"type": "File","fileName": fileName,"fileOffset":fileOffsetOrParentEntryNum,"fileSize":fileSizeOrLastEntryNum}) + else: + ret.append({"type": "Folder","folderName": fileName,"parentFolderEntryNumber": fileOffsetOrParentEntryNum, "lastEntryNumber": fileSizeOrLastEntryNum}) + + return ret + +""" +Write the current folder to disk and return it's name/last entry number +""" +def writeFolder(parsedFstBin,i): + folderPath = i["folderName"]+"/" + lastEntryNumber = i["lastEntryNumber"] + + if i["parentFolderEntryNumber"] == 0: + if not os.path.exists(folderPath): + os.makedirs(folderPath) + else: + parentFolderEntry = parsedFstBin[i["parentFolderEntryNumber"]-1] + while True: + folderPath = parentFolderEntry["folderName"] + "/" + folderPath + if parentFolderEntry["parentFolderEntryNumber"] == 0: + break + + nextParentFolderEntryNumber = parentFolderEntry["parentFolderEntryNumber"] + parentFolderEntry = parsedFstBin[nextParentFolderEntryNumber-1] + + if not os.path.exists(folderPath): + os.makedirs(folderPath) + + return folderPath, lastEntryNumber + +""" +Use the parsed fst.bin contents to write assets to file +""" +def writeAssets(parsedFstBin, handler): + # Write the folder structure and files to disc + j = 0 + folderStack = [] + folderStack.append({"folderName": "./", "lastEntryNumber": numFileEntries}) + for i in parsedFstBin: + j += 1 + if i["type"] == "Folder": + currentFolder, lastEntryNumber = writeFolder(parsedFstBin,i) + folderStack.append({"folderName": currentFolder, "lastEntryNumber": lastEntryNumber}) + else: + handler.seek(i["fileOffset"]) + with open((folderStack[-1]["folderName"]+i["fileName"]), "wb") as currentFile: + currentFile.write(bytearray(handler.read(i["fileSize"]))) + + while folderStack[-1]["lastEntryNumber"] == j+1: + folderStack.pop() + +def main(): + with open(sys.argv[1], "rb") as f: + # Seek to fst offset information and retrieve it + f.seek(fstInfoPosition) + fstOffset,fstSize = getFstInfo(f,fstInfoPosition) + + # Seek to fst.bin and retrieve it + f.seek(fstOffset) + fstBinBytes = bytearray(f.read(fstSize)) + + # Parse fst.bin + parsedFstBin = parseFstBin(fstBinBytes) + + # Write assets to file + writeAssets(parsedFstBin, f) + +if __name__ == "__main__": + main() \ No newline at end of file