[Scummvm-git-logs] scummvm-tools master -> c40f19e3fb0272dd2e3a4b0068ebff2124269d11
lephilousophe
noreply at scummvm.org
Mon Apr 1 15:52:23 UTC 2024
This automated email contains information about 1 new commit which have been
pushed to the 'scummvm-tools' repo located at https://github.com/scummvm/scummvm-tools .
Summary:
c40f19e3fb WINTERMUTE: Add a DCP extractor script
Commit: c40f19e3fb0272dd2e3a4b0068ebff2124269d11
https://github.com/scummvm/scummvm-tools/commit/c40f19e3fb0272dd2e3a4b0068ebff2124269d11
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2024-04-01T17:52:00+02:00
Commit Message:
WINTERMUTE: Add a DCP extractor script
Changed paths:
A engines/wintermute/dcp_extractor.py
diff --git a/engines/wintermute/dcp_extractor.py b/engines/wintermute/dcp_extractor.py
new file mode 100755
index 00000000..b8ddf13e
--- /dev/null
+++ b/engines/wintermute/dcp_extractor.py
@@ -0,0 +1,154 @@
+#! /usr/bin/env python3
+
+# Sources:
+# https://archive.softwareheritage.org/browse/content/sha1_git:0bc8340ae58e9d88a23e52b0723b9a92e14f4e62/?origin_url=https://bitbucket.org/MnemonicWME/wme1&path=src/engine_core/wme_base/dcpackage.h
+# https://archive.softwareheritage.org/browse/content/sha1_git:4fd95c6076f4f2ce5dbbd1eead41b357bd232c66/?origin_url=https://bitbucket.org/MnemonicWME/wme1&path=src/engine_core/wme_base/BFileManager.cpp
+
+import collections
+from datetime import datetime
+import functools
+import pathlib
+import struct
+import zlib
+
+Header = collections.namedtuple('Header', ['magic1', 'magic2', 'pkg_version', 'game_version', 'priority', 'cd', 'master_index', 'creation_time', 'desc', 'num_dirs'])
+DirEntry = collections.namedtuple('DirEntry', ['name', 'cd', 'num_entries', 'files'])
+FileEntry = collections.namedtuple('FileEntry', ['name', 'offset', 'length', 'comp_length', 'flags', 'timedate1', 'timedate2'], defaults=(0, 0))
+
+def read_struct(f, fmt, constructor):
+ if type(fmt) is str:
+ fmt = struct.Struct(fmt)
+
+ buf = f.read(fmt.size)
+ if len(buf) != fmt.size:
+ raise Exception("File too small")
+
+ return constructor(*fmt.unpack(buf))
+
+def read_str(f):
+ sz = f.read(1)
+ if len(sz) != 1:
+ raise Exception("File too small")
+ sz, = struct.unpack('<B', sz)
+ s = f.read(sz)
+ if len(s) != sz:
+ raise Exception("File too small")
+ return s
+
+def read_headers(f, abs_offset = 0):
+ f.seek(abs_offset)
+
+ header = read_struct(f, '<L4sLLBBBxL100sL', Header)
+ if header.magic1 != 0xdec0adde:
+ raise Exception("Invalid magic")
+ if header.magic2 != b'JUNK':
+ raise Exception("Invalid magic")
+ if header.pkg_version > 0x200:
+ raise Exception("Invalid version")
+
+ if header.pkg_version == 0x200:
+ dir_offset, = struct.unpack('<L', f.read(4))
+ dir_offset += abs_offset
+ f.seek(dir_offset)
+
+ dirs = []
+ for pkg in range(header.num_dirs):
+ files = []
+ dir_name = read_str(f)
+ dir_name = dir_name.rstrip(b'\x00')
+ dirent = read_struct(f, '<BL', functools.partial(DirEntry, dir_name, files=files))
+ dirs.append(dirent)
+
+ for i in range(dirent.num_entries):
+ fname = read_str(f)
+ fname = bytes(b ^ 0x44 for b in fname)
+ fname = fname.rstrip(b'\x00')
+
+ if header.pkg_version == 0x200:
+ fmt = '<LLLLLL'
+ else:
+ fmt = '<LLLL'
+ fileent = read_struct(f, fmt, functools.partial(FileEntry, fname))
+ fileent = fileent._replace(offset=fileent.offset + abs_offset)
+ files.append(fileent)
+
+ return header, dirs
+
+def read_file(f, fileent):
+ f.seek(fileent.offset)
+ if fileent.comp_length:
+ buf = f.read(fileent.comp_length)
+ buf = zlib.decompress(buf)
+ else:
+ buf = f.read(fileent.length)
+ if len(buf) != fileent.length:
+ raise Exception("Invalid file size")
+ return buf
+
+def lookup_sig(f):
+ import mmap
+ with mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as mm:
+ offset = mm.find(b'\xde\xad\xc0\xdeJUNK')
+ if offset == -1:
+ raise Exception("Signature not found")
+ return offset
+
+def dcp_list(options, offset=0):
+ header, dirs = read_headers(options.input, offset)
+ for dirent in dirs:
+ print("Directory {0} from CD {1} with {2} entries".format(dirent.name.decode('utf-8'), dirent.cd, dirent.num_entries))
+ print("{0:<8}\t@{1:<8}\t{2:<8}\t{3:<19}\t{4}".format('sz', 'offset ', 'compsz', 'date', 'name'))
+ for fl in dirent.files:
+ print("{0:<8}\t@{1:<8}\t{2:<8}\t{3}\t{4}".format(
+ fl.length, fl.offset, fl.length if fl.comp_length == 0 else fl.comp_length,
+ datetime.fromtimestamp(fl.timedate1 | (fl.timedate2 << 64)).isoformat(), fl.name.decode('utf-8')))
+
+def dcp_extract(options, offset=0):
+ header, dirs = read_headers(options.input, offset)
+
+ output_dir = options.output_dir
+ for dirent in dirs:
+ print("Directory {0} from CD {1} with {2} entries".format(dirent.name.decode('utf-8'), dirent.cd, dirent.num_entries))
+ print("{0:<8}\t@{1:<8}\t{2:<8}\t{3:<19}\t{4}".format('sz', 'offset ', 'compsz', 'date', 'name'))
+ output_int = output_dir / dirent.name.decode('utf-8')
+ for fl in dirent.files:
+ print("{0:<8}\t@{1:<8}\t{2:<8}\t{3}\t{4}".format(
+ fl.length, fl.offset, fl.length if fl.comp_length == 0 else fl.comp_length,
+ datetime.fromtimestamp(fl.timedate1 | (fl.timedate2 << 64)).isoformat(), fl.name.decode('utf-8')))
+ output_file = output_int / pathlib.Path(fl.name.decode('utf-8').replace('\\', '/'))
+ output_file.parent.mkdir(parents=True, exist_ok=True)
+
+ with output_file.open('wb') as output_f:
+ buf = read_file(options.input, fl)
+ output_f.write(buf)
+
+def main():
+ import argparse
+
+ parser = argparse.ArgumentParser(
+ prog='dcp_extractor.py',
+ description='Wintermute DCP archive extractor')
+
+ parser.add_argument('--sfx',
+ action='store_true')
+
+ action_parsers = parser.add_subparsers(required=True)
+ list_parser = action_parsers.add_parser('list', help='list archive contents')
+ list_parser.add_argument('input', type=argparse.FileType('rb'), metavar='dcp file')
+ list_parser.set_defaults(action=dcp_list)
+ extract_parser = action_parsers.add_parser('extract', help='extract archive contents')
+ extract_parser.add_argument('input', type=argparse.FileType('rb'), metavar='dcp file')
+ extract_parser.add_argument('output_dir', type=pathlib.Path, metavar='output directory')
+ extract_parser.set_defaults(action=dcp_extract)
+
+ options = parser.parse_args()
+
+ offset = 0
+ if options.sfx:
+ offset = lookup_sig(options.input)
+ print("Found signature at {}".format(offset))
+
+ options.action(options, offset=offset)
+
+if __name__ == "__main__":
+ main()
More information about the Scummvm-git-logs
mailing list