Skip to content

Instantly share code, notes, and snippets.

@AzureDVBB
Last active May 6, 2021 21:42
Show Gist options
  • Save AzureDVBB/3df6de89a0d290d75dfbcd06f46210fc to your computer and use it in GitHub Desktop.
Save AzureDVBB/3df6de89a0d290d75dfbcd06f46210fc to your computer and use it in GitHub Desktop.
A commented version of the python dungeondraft unpacker this link (with minor improvements): https://www.reddit.com/r/dungeondraft/comments/gjvlud/python_script_to_unpack_dungeondraft_pack_assets/
# source: https://www.reddit.com/r/dungeondraft/comments/gjvlud/python_script_to_unpack_dungeondraft_pack_assets/
# dungeondraft_pack-unpacker.py
# version 0.1
# Based upon: https://github.com/tehskai/godot-unpacker
import sys
import os
import pathlib
import mmap
import struct
def main(args):
rip_textures = True # change to False if you want textures untouched in godot .tex format
if not args:
return "Usage: python godot-unpacker.py data.pck"
arg_location_name = args[0]
print(arg_location_name)
if not os.path.exists(arg_location_name):
return "Error: file not found"
if os.path.isdir(arg_location_name):
with os.scandir(arg_location_name) as folder:
for entry in folder:
if entry.name.endswith(".dungeondraft_pack") and entry.is_file():
extract_pck(entry.path, rip_textures)
elif os.path.isfile(arg_location_name):
extract_pck(arg_location_name)
def rip_texture(data): # this actually converts '.tex' texture files inside the godot '.pck' files. Unnecessary for dungeondraft files
# files seem to be stored in three seperate pieces:
# extension at the front (and at the end in the case of png/jpg formats)
# size in bytes at after the extension (with a different byte order for the size in case of webp format)
# the data crammed somewhere in between the start and the end marker bytes
# note webp has no end marker bytes but use a different byte order for the size for some reason >.>
# webp
start = data.find(bytes.fromhex("52 49 46 46")) # find the start of the file using the extension as guidance
if start >= 0: # check if this is the correct filetype (by determining if the file starts later in the datastream)
size = int.from_bytes(data[start+4:start+8], byteorder="little") # read the filesize
return [".webp", data[start:start+8+size]] # return the extension and the data
# png
start = data.find(bytes.fromhex("89 50 4E 47 0D 0A 1A 0A"))
if start >= 0:
# need to find the end of the data in this format
end = data.find(bytes.fromhex("49 45 4E 44 AE 42 60 82")) + 8 # offset by 8 bytes as 'find' returns the start of the pattern
return [".png", data[start:end]]
# jpg
start = data.find(bytes.fromhex("FF D8 FF"))
if start >= 0:
end = data.find(bytes.fromhex("FF D9")) + 2 # and here the end is offset by 2 bytes as 'find' returns the start of the pattern
return [".jpg", data[start:end]]
# none of the above
return False
def extract_pck(pck_file_location, rip_textures = False, output_folder_name = None):
pck_folder_path, pck_file_name = os.path.split(pck_file_location) # uneccessary (just for filewrites)
if output_folder_name is None: # uneccessary (just for output folder determining)
output_folder_name, _ = os.path.splitext(pck_file_name)
file_list = []
with open(pck_file_location, "r+b") as d:
with mmap.mmap(d.fileno(), 0) as f:
magic = bytes.fromhex('47 44 50 43') # GDPC
if f.read(4) == magic: # check if it is a pck archive (Godot Package) by reading 'GDPC' in hex at the file start
print(pck_file_location + " looks like a pck archive")
f.seek(0) # reset pointer to file start
else: # check if it is a self-contained exe (godot game to decompile) // unneccessary
f.seek(-4, os.SEEK_END)
if f.read(4) == magic:
print(pck_file_location + " looks like a self-contained exe")
f.seek(-12, os.SEEK_END)
main_offset = int.from_bytes(f.read(8), byteorder='little')
f.seek(f.tell()-main_offset-8)
if f.read(4) == magic:
f.seek(f.tell()-4)
else: # f.close() uneccessary due to context manager, error in case file is not godot .pck file
f.close()
return "Error: file not supported"
# using struct library to interpret bytes as packed binary data
# first argument is the format
# second argument is data (reading the header that is 88 bytes long)
package_headers = struct.unpack_from("IIIII16II", f.read(20+64+4)) # read package header (advancing file pointer)
file_count = package_headers[-1] # get file count from header
print (pck_file_location + " info:", package_headers) # uncessessary printing
for file_num in range(1, file_count+1): # step through each file
# read how many bytes the file_path is encoded in (advancing the file pointer)
filepath_length = int.from_bytes(f.read(4), byteorder="little")
# read the file info using struct library (advancing the file pointer)
# file_info = struct.unpack_from("<{}sQQ16B".format(filepath_length), f.read(filepath_length+8+8+16)) # outdated .format() use
file_info = struct.unpack_from(f"<{filepath_length}sQQ16B", f.read(filepath_length+8+8+16)) # replaced with f-string
path, offset, size = file_info[0:3] # extract the filepath, the offset and the filesize from file header
# decode raw path data into utf-8 text, replacing the standard godot 'res://' root folder with the output folder path
path = path.decode("utf-8").replace("res://",output_folder_name + "/")
# md5 sum of the file, used to check file integrity // and is always 0 and never used with dungeondraft it seems
md5 = "".join([format(x, 'x') for x in file_info[-16:]])
# add this file to the list of files (path to file, offset number of bytes to file start, filesize in bytes, md5 sum)
file_list.append({ 'path': path, 'offset': offset, 'size': size, 'md5': md5 })
# uneccessary printing
print(file_num, "/", file_count, sep="", end=" ")
print(path, offset, size, md5)
# extraction takes place here, creating folder structure, reading and writing all the files from assetpack to disk
for packed_file in file_list:
path = os.path.join(pck_folder_path, os.path.dirname(packed_file['path'])) # sets up the output directory
file_name_full = os.path.basename(packed_file['path']) # full relative path of file
file_name, file_extension = os.path.splitext(file_name_full) # seperate file name from extension for some reason?
pathlib.Path(path).mkdir(parents=True, exist_ok=True) # using pathlib just to make/ensure the filestructure?
f.seek(packed_file['offset']) # seek file read pointer to the file-start offset
file_data = f.read(packed_file['size']) # read the entire file (using the file size in bytes to guide us)
# do md5 check here /// they never do though?
if file_extension == '.tex' and rip_textures:
data = rip_texture(file_data) # convert file data to just data necessary to write the file and it's extension
print(f"Weird file type for '{file_name}.{file_extension}' ... attempting to convert.")
if isinstance(data, list): # just checking if the return is a list and not False (replace with 'if data:' instead)
file_extension, file_data = data # unpack return value
print(f"converted '{file_name}' from '.tex' to '.{file_extension}'")
file_name_full = file_name + data[0] # add extension to the filename, but then why seperate the extension?
# finally writing the unpacked file to disk
with open(os.path.join(path, file_name_full.rstrip("\0")), "w+b") as p:
p.write(file_data)
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment