Skip to content

Instantly share code, notes, and snippets.

@FelixWolf
Last active December 10, 2023 14:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save FelixWolf/cfed435cd08cefb421d8d9e0e4de6f19 to your computer and use it in GitHub Desktop.
Save FelixWolf/cfed435cd08cefb421d8d9e0e4de6f19 to your computer and use it in GitHub Desktop.
Elf bowling .gfx unpacker. Mostly working, as well as a documentation for the (terrible) format.
#!/usr/bin/env python3
"""
zlib License
(C) 2023 Kyler "Félix" Eastridge
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
"""
import argparse
import os
class GFXFileError(Exception):
pass
"""
Format specification:
char[256] filename
char[256] secondaryFilename
char[256] lengthString
char[int(lengthString)] data
if secondaryFilename:
char[256] lengthString
char[int(lengthString)] data
char[256] resourceName
lengthString is a literal number string representing the length of data.
it must be passed to int() before using.
if secondaryFilename is set, it is a paired file, and immediately after
data, is another lengthString and data pair.
Both pairs "share" a resource name. This is used for stuff like alpha
channels of images.
Occasionally, there may be invalid length values, that are sometimes just
strings. I honestly have no idea what they are trying to do here, but I
just skip over it. There isn't any data to extract there.
"""
GFX_EOF = "< THIS IS THE END OF A GFX FILE >"
def getGFXIndex(handle):
result = []
while True:
fName = handle.read(256)
rSize = len(fName)
try:
fName = fName.rstrip(b"\0").decode()
except Exception as e:
raise e
if fName == GFX_EOF:
return result
elif rSize != 256:
raise GFXFileError("Possibly corrupted archive! EOF encountered before EOF marker!")
#Secondary file name
fName2 = handle.read(256)
if len(fName2) != 256:
raise GFXFileError("Possibly corrupted archive! EOF encountered before EOF marker!")
try:
fName2 = fName2.rstrip(b"\0").decode()
except Exception as e:
raise e
#Yes, you are reading this right
#File size is stored as a char[256], and needs to be parsed as a integer
try:
fSize = handle.read(256)
fSize = int(fSize.rstrip(b"\0"))
except ValueError as e:
#Apparently, some gfx files are malformed.. go figure..
#Behavior in the engine is that these are skipped.
f.seek(-256, 1)
continue
fPointer = f.tell()
f.seek(fSize, 1) #Skip over data
if fName2:
fSize2 = handle.read(256)
fSize2 = int(fSize2.rstrip(b"\0"))
fPointer2 = f.tell()
f.seek(fSize2, 1) #Skip over data
fResourceName = handle.read(256).rstrip(b"\0").decode()
if fName2:
result.append({
"filename": fName2,
"offset": fPointer2,
"size": fSize2,
"resourcename": fResourceName,
"pair": fName
})
result.append({
"filename": fName,
"offset": fPointer,
"size": fSize,
"resourcename": fResourceName,
"pair": fName2
})
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Elf Bowling 7 1/7 GFX Extractor')
parser.add_argument("mode", choices = ("e", "l"),
help="Mode: e = extract, l = list")
parser.add_argument("input",
help="Input GFX file")
parser.add_argument("-o", "--output", default=".",
help="Output folder (Default PWD)")
args = parser.parse_args()
with open(args.input, "rb") as f:
files = getGFXIndex(f)
if args.mode == "l":
for file in files:
if file["pair"]:
print("{:<8} {} <> {} = {}".format(file["size"], file["filename"], file["pair"], file["resourcename"]))
else:
print("{:<8} {} = {}".format(file["size"], file["filename"], file["resourcename"]))
elif args.mode == "e":
for file in files:
outpath = os.path.join(args.output, file["filename"])
os.makedirs(os.path.dirname(outpath), exist_ok=True)
with open(outpath, "wb") as outfile:
f.seek(file["offset"])
outfile.write(f.read(file["size"]))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment