Last active
January 27, 2024 15:07
-
-
Save daemongh/1eb663f7592487d44071dfdf9df1f4d9 to your computer and use it in GitHub Desktop.
Unpack .fuk files (Railway Empire, Patrician, Port Royale)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import os | |
import shutil | |
import sys | |
import zlib | |
def read_uint(file): | |
read_bytes = file.read(4) | |
result = 0 | |
result += read_bytes[0] | |
result += read_bytes[1] * 256 | |
result += read_bytes[2] * 256 * 256 | |
result += read_bytes[3] * 256 * 256 * 256 | |
# FFFF FFFF check, we'll set it to negative -1 | |
# this is only appears in the descriptor's parent element | |
if result == 4294967295: | |
result = -1 | |
return result | |
def read_uints(file, size): | |
result = [] | |
for x in range(size): | |
result.append(read_uint(file)) | |
return result | |
def read_string(file): | |
result_bytes = b"" | |
byte = file.read(1) | |
while byte != b"\x00": | |
result_bytes += byte | |
byte = file.read(1) | |
return str(result_bytes, "utf-8", "ignore") | |
def read_file_list(file, length): | |
index = 0 | |
file_list = {} | |
while index < length: | |
string = read_string(file) | |
file_list[index] = string | |
index += len(string) + 1 | |
return file_list | |
def read_descriptors(descriptor_data, file_metadata, file_offsets, file_list): | |
length = int(len(descriptor_data) / 4) | |
descriptors = [] | |
offset_index = 0 | |
for block_size in range(0, length): | |
descriptor_index = block_size * 4 | |
data_index = block_size * 3 | |
descriptor = { | |
"next": descriptor_data[descriptor_index], | |
"child": descriptor_data[descriptor_index + 1], | |
"parent": descriptor_data[descriptor_index + 2], | |
"length": file_metadata[data_index], | |
"compressed_length": file_metadata[data_index + 1], | |
"name": file_list[descriptor_data[descriptor_index + 3]], | |
"offset": file_metadata[data_index + 2] | |
} | |
if file_offsets and descriptor["offset"] != 0: | |
descriptor["offset"] = file_offsets[offset_index] * 16 | |
offset_index += 1 | |
descriptors.append(descriptor) | |
return descriptors | |
def parent_name(index, descriptors): | |
return descriptors[index]["name"] | |
def descriptor_path(index, descriptors): | |
path = [descriptors[index]["name"]] | |
parent_index = descriptors[index]["parent"] | |
while parent_index != -1: | |
path.append(parent_name(parent_index, descriptors)) | |
parent_index = descriptors[parent_index]["parent"] | |
return "/".join(path[::-1]) | |
def extract_files(filename, file, descriptors): | |
directory = filename.replace(".fuk", "") | |
if os.path.exists(directory): | |
print("Directory {} exists, deleting it first...".format(directory)) | |
shutil.rmtree(directory) | |
os.makedirs(directory) | |
count = len(descriptors) | |
for index in range(count): | |
descriptor = descriptors[index] | |
output_filename = "{d}/{p}".format(d=directory, p=descriptor_path(index, descriptors)).replace("//", "/") | |
output_directory = os.path.dirname(output_filename) | |
if descriptor["length"] == 0 and descriptor["compressed_length"] == 0: | |
print("({}/{}) Creating directory {}".format(index + 1, count, output_filename)) | |
if not os.path.exists(output_filename): | |
os.makedirs(output_filename) | |
else: | |
length = min(descriptor["length"], descriptor["compressed_length"]) | |
if not os.path.exists(output_directory): | |
os.makedirs(output_directory) | |
with open(output_filename, "wb") as output_file: | |
file.seek(descriptor["offset"]) | |
read_file_data = file.read(length) | |
print("({}/{}) Extracting {}...".format(index + 1, count, output_filename)) | |
if descriptor["compressed_length"] < descriptor["length"]: | |
data = zlib.decompress(read_file_data) | |
else: | |
data = read_file_data | |
output_file.write(data) | |
def process_file(filename): | |
if not os.path.exists(filename): | |
print("File {} does not exists...".format(filename)) | |
exit() | |
with open(filename, "rb") as file: | |
# first four bytes should be 78 56 34 12 (305419896 as an unsigned decimal) | |
signature = read_uint(file) | |
if signature != 305419896: | |
print("Invalid file signature: {0}".format(signature)) | |
exit() | |
version = read_uint(file) | |
print("Version: {0}".format(version)) | |
if version == 32 or version == 0: | |
flag = version == 32 | |
block_size = read_uint(file) | |
count = 0 if not flag else read_uint(file) | |
if block_size <= 0 or flag and count <= 0: | |
print("File empty") | |
exit() | |
descriptor_data_length = read_uint(file) | |
padding = 0 if not flag else read_uint(file) | |
# print("Padding: {}".format(padding)) | |
descriptor_data = read_uints(file, block_size * 4) | |
file_metadata = read_uints(file, block_size * 3) | |
file_offsets = [] if not flag else read_uints(file, count) | |
file_list = read_file_list(file, descriptor_data_length) | |
descriptors = read_descriptors(descriptor_data, file_metadata, file_offsets, file_list) | |
extract_files(filename, file, descriptors) | |
else: | |
print("Unknown file version: {0}".format(version)) | |
exit() | |
if len(sys.argv) < 2: | |
print("Need a .fuk file passed in as an argument.") | |
exit() | |
fuk_filename = sys.argv[1] | |
if __name__ == "__main__": | |
process_file(fuk_filename) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
thank you my friend!!!