Skip to content

Instantly share code, notes, and snippets.

@DaZombieKiller
Created January 5, 2025 18:21
Show Gist options
  • Select an option

  • Save DaZombieKiller/808f03383659547e8d7f5e544e88f06a to your computer and use it in GitHub Desktop.

Select an option

Save DaZombieKiller/808f03383659547e8d7f5e544e88f06a to your computer and use it in GitHub Desktop.
Duke Nukem 1+2 Remastered .DUKE archive extractor script
#
# Extracts files from Duke Nukem 1+2 Remastered .DUKE archives
#
import sys
import zlib
import struct
from pathlib import Path
from Crypto.Cipher import AES
def unpack_archive(archive, output_dir):
magic, version, entry_count, _ = struct.unpack("<4s3i", archive.read(0x10))
if magic != b'DUKE':
print("invalid data archive")
return
names = {}
entries = []
for _ in range(entry_count):
entry = struct.unpack("<6i8s", archive.read(0x20))
entries.append(entry)
for entry in entries:
_, _, _, offset, length, _, _ = entry
if length > 0x64:
print("invalid file name length")
return
archive.seek(offset)
names[entry] = archive.read(length).decode('utf8')
for entry in entries:
offset, length, uncompressed_length, _, _, _, nonce = entry
output_path = Path(output_dir) / names[entry]
output_path.parent.mkdir(parents=True, exist_ok=True)
archive.seek(offset)
key = bytes.fromhex("5755FB6CB9A571BC4F5562EE9299E808241594D1DFA675BEE9DBB51AC087ECCD")
cipher = AES.new(key, AES.MODE_CTR, nonce=nonce)
data = cipher.decrypt(archive.read(length))
if length != uncompressed_length:
data = zlib.decompress(data, bufsize=uncompressed_length)
with open(output_path, "wb") as output_file:
output_file.write(data)
print(names[entry])
def main():
if len(sys.argv) != 3:
print(f"{sys.argv[0]} <archive path> <output dir>")
return
with open(sys.argv[1], "rb") as archive:
unpack_archive(archive, sys.argv[2])
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment