Skip to content

Commit c40f19e

Browse files
committed
WINTERMUTE: Add a DCP extractor script
1 parent 6542f4d commit c40f19e

1 file changed

Lines changed: 154 additions & 0 deletions

File tree

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
#! /usr/bin/env python3
2+
3+
# Sources:
4+
# https://archive.softwareheritage.org/browse/content/sha1_git:0bc8340ae58e9d88a23e52b0723b9a92e14f4e62/?origin_url=https://bitbucket.org/MnemonicWME/wme1&path=src/engine_core/wme_base/dcpackage.h
5+
# https://archive.softwareheritage.org/browse/content/sha1_git:4fd95c6076f4f2ce5dbbd1eead41b357bd232c66/?origin_url=https://bitbucket.org/MnemonicWME/wme1&path=src/engine_core/wme_base/BFileManager.cpp
6+
7+
import collections
8+
from datetime import datetime
9+
import functools
10+
import pathlib
11+
import struct
12+
import zlib
13+
14+
Header = collections.namedtuple('Header', ['magic1', 'magic2', 'pkg_version', 'game_version', 'priority', 'cd', 'master_index', 'creation_time', 'desc', 'num_dirs'])
15+
DirEntry = collections.namedtuple('DirEntry', ['name', 'cd', 'num_entries', 'files'])
16+
FileEntry = collections.namedtuple('FileEntry', ['name', 'offset', 'length', 'comp_length', 'flags', 'timedate1', 'timedate2'], defaults=(0, 0))
17+
18+
def read_struct(f, fmt, constructor):
19+
if type(fmt) is str:
20+
fmt = struct.Struct(fmt)
21+
22+
buf = f.read(fmt.size)
23+
if len(buf) != fmt.size:
24+
raise Exception("File too small")
25+
26+
return constructor(*fmt.unpack(buf))
27+
28+
def read_str(f):
29+
sz = f.read(1)
30+
if len(sz) != 1:
31+
raise Exception("File too small")
32+
sz, = struct.unpack('<B', sz)
33+
s = f.read(sz)
34+
if len(s) != sz:
35+
raise Exception("File too small")
36+
return s
37+
38+
def read_headers(f, abs_offset = 0):
39+
f.seek(abs_offset)
40+
41+
header = read_struct(f, '<L4sLLBBBxL100sL', Header)
42+
if header.magic1 != 0xdec0adde:
43+
raise Exception("Invalid magic")
44+
if header.magic2 != b'JUNK':
45+
raise Exception("Invalid magic")
46+
if header.pkg_version > 0x200:
47+
raise Exception("Invalid version")
48+
49+
if header.pkg_version == 0x200:
50+
dir_offset, = struct.unpack('<L', f.read(4))
51+
dir_offset += abs_offset
52+
f.seek(dir_offset)
53+
54+
dirs = []
55+
for pkg in range(header.num_dirs):
56+
files = []
57+
dir_name = read_str(f)
58+
dir_name = dir_name.rstrip(b'\x00')
59+
dirent = read_struct(f, '<BL', functools.partial(DirEntry, dir_name, files=files))
60+
dirs.append(dirent)
61+
62+
for i in range(dirent.num_entries):
63+
fname = read_str(f)
64+
fname = bytes(b ^ 0x44 for b in fname)
65+
fname = fname.rstrip(b'\x00')
66+
67+
if header.pkg_version == 0x200:
68+
fmt = '<LLLLLL'
69+
else:
70+
fmt = '<LLLL'
71+
fileent = read_struct(f, fmt, functools.partial(FileEntry, fname))
72+
fileent = fileent._replace(offset=fileent.offset + abs_offset)
73+
files.append(fileent)
74+
75+
return header, dirs
76+
77+
def read_file(f, fileent):
78+
f.seek(fileent.offset)
79+
if fileent.comp_length:
80+
buf = f.read(fileent.comp_length)
81+
buf = zlib.decompress(buf)
82+
else:
83+
buf = f.read(fileent.length)
84+
if len(buf) != fileent.length:
85+
raise Exception("Invalid file size")
86+
return buf
87+
88+
def lookup_sig(f):
89+
import mmap
90+
with mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as mm:
91+
offset = mm.find(b'\xde\xad\xc0\xdeJUNK')
92+
if offset == -1:
93+
raise Exception("Signature not found")
94+
return offset
95+
96+
def dcp_list(options, offset=0):
97+
header, dirs = read_headers(options.input, offset)
98+
for dirent in dirs:
99+
print("Directory {0} from CD {1} with {2} entries".format(dirent.name.decode('utf-8'), dirent.cd, dirent.num_entries))
100+
print("{0:<8}\t@{1:<8}\t{2:<8}\t{3:<19}\t{4}".format('sz', 'offset ', 'compsz', 'date', 'name'))
101+
for fl in dirent.files:
102+
print("{0:<8}\t@{1:<8}\t{2:<8}\t{3}\t{4}".format(
103+
fl.length, fl.offset, fl.length if fl.comp_length == 0 else fl.comp_length,
104+
datetime.fromtimestamp(fl.timedate1 | (fl.timedate2 << 64)).isoformat(), fl.name.decode('utf-8')))
105+
106+
def dcp_extract(options, offset=0):
107+
header, dirs = read_headers(options.input, offset)
108+
109+
output_dir = options.output_dir
110+
for dirent in dirs:
111+
print("Directory {0} from CD {1} with {2} entries".format(dirent.name.decode('utf-8'), dirent.cd, dirent.num_entries))
112+
print("{0:<8}\t@{1:<8}\t{2:<8}\t{3:<19}\t{4}".format('sz', 'offset ', 'compsz', 'date', 'name'))
113+
output_int = output_dir / dirent.name.decode('utf-8')
114+
for fl in dirent.files:
115+
print("{0:<8}\t@{1:<8}\t{2:<8}\t{3}\t{4}".format(
116+
fl.length, fl.offset, fl.length if fl.comp_length == 0 else fl.comp_length,
117+
datetime.fromtimestamp(fl.timedate1 | (fl.timedate2 << 64)).isoformat(), fl.name.decode('utf-8')))
118+
output_file = output_int / pathlib.Path(fl.name.decode('utf-8').replace('\\', '/'))
119+
output_file.parent.mkdir(parents=True, exist_ok=True)
120+
121+
with output_file.open('wb') as output_f:
122+
buf = read_file(options.input, fl)
123+
output_f.write(buf)
124+
125+
def main():
126+
import argparse
127+
128+
parser = argparse.ArgumentParser(
129+
prog='dcp_extractor.py',
130+
description='Wintermute DCP archive extractor')
131+
132+
parser.add_argument('--sfx',
133+
action='store_true')
134+
135+
action_parsers = parser.add_subparsers(required=True)
136+
list_parser = action_parsers.add_parser('list', help='list archive contents')
137+
list_parser.add_argument('input', type=argparse.FileType('rb'), metavar='dcp file')
138+
list_parser.set_defaults(action=dcp_list)
139+
extract_parser = action_parsers.add_parser('extract', help='extract archive contents')
140+
extract_parser.add_argument('input', type=argparse.FileType('rb'), metavar='dcp file')
141+
extract_parser.add_argument('output_dir', type=pathlib.Path, metavar='output directory')
142+
extract_parser.set_defaults(action=dcp_extract)
143+
144+
options = parser.parse_args()
145+
146+
offset = 0
147+
if options.sfx:
148+
offset = lookup_sig(options.input)
149+
print("Found signature at {}".format(offset))
150+
151+
options.action(options, offset=offset)
152+
153+
if __name__ == "__main__":
154+
main()

0 commit comments

Comments
 (0)