Skip to content

Commit

Permalink
fixed D4 16-bit win support
Browse files Browse the repository at this point in the history
  • Loading branch information
59de44955ebd committed Feb 22, 2023
1 parent 41639e3 commit 14d6172
Show file tree
Hide file tree
Showing 9 changed files with 91 additions and 36 deletions.
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ It supports Director versions 4 to 12 and the following flavors:
* Mac OS X projectors (Intel/PPC/Universal)
* macOS projectors (.app bundle, Intel)

## Setup

macOS:
```
$ chmod +x bin/darwin/ProjectorRays
```

Linux:
```
$ chmod +x bin/linux/ProjectorRays
```

## Usage

```
Expand All @@ -24,7 +36,7 @@ Running
$ python tests.py
```

will unpack the 27 example projectors found in "test_files" to a new folder "test_files_unpacked".
will unpack the 29 example projectors found in "test_files" to a new folder "test_files_unpacked".

## Notes

Expand Down
Binary file modified bin/darwin/ProjectorRays
Binary file not shown.
Binary file modified bin/linux/ProjectorRays
Binary file not shown.
Binary file modified bin/win32/ProjectorRays.exe
Binary file not shown.
Binary file added test_files/projector_d120_compressed.exe
Binary file not shown.
Binary file modified test_files/projector_d40_16bit.exe
Binary file not shown.
Binary file added test_files/projector_d60_16bit_compressed.exe
Binary file not shown.
19 changes: 15 additions & 4 deletions tests.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
import logging
import os
import shutil
import colorama as c
from unpacker import unpack

PATH = os.path.dirname(os.path.realpath(__file__))

if __name__ == '__main__':
PATH = os.path.dirname(os.path.realpath(__file__))
logging.basicConfig(level = logging.INFO, format = '[%(levelname)s] %(message)s')
c.init()

dest_dir = os.path.join(PATH, 'test_files_unpacked')
if os.path.isdir(dest_dir):
shutil.rmtree(dest_dir)
os.mkdir(dest_dir)

test_files = list(os.listdir(os.path.join(PATH, 'test_files')))
failed = 0
for fn in test_files:
try:
unpack(os.path.join(PATH, 'test_files', fn), dest_dir=dest_dir, do_decompile=True)
output_dir, num_dirs, num_xtras = unpack(os.path.join(PATH, 'test_files', fn), dest_dir=dest_dir, do_decompile=True)
print(f'{c.Fore.LIGHTGREEN_EX}Done. {num_dirs+num_xtras} files were extracted to "{output_dir}".{c.Fore.RESET}')
except Exception as e:
print('[ERROR]', e)
print(f'{c.Fore.RED}[ERROR] {e}{c.Fore.RESET}')
failed += 1
print()

if failed == 0:
print(f'{c.Fore.LIGHTGREEN_EX}All {len(test_files)} projectors were successfully unpacked!{c.Fore.RESET}')
else:
print(f'{c.Fore.RED}{failed} of {len(test_files)} projectors could not be unpacked.{c.Fore.RESET}')
94 changes: 63 additions & 31 deletions unpacker.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
import os
import re
import shutil
Expand All @@ -8,7 +9,7 @@

def unpack(f, dest_dir=None, do_decompile=False):
if not os.path.exists(f):
return print('Error: file does not exist.')
raise(OSError('File does not exist'))

os.environ['PATH'] = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'bin', sys.platform) + os.pathsep + os.environ['PATH']

Expand All @@ -25,15 +26,15 @@ def unpack(f, dest_dir=None, do_decompile=False):
if len(l):
bin_file = os.path.join(f, 'Contents', 'MacOS', l[0])
if bin_file is not None:
print(f'Unpacking Mac OS X/macOS projector "{fn}"...')
logging.info(f'Unpacking Mac OS X/macOS projector "{fn}"...')
output_dir = os.path.join(dest_dir if dest_dir else os.path.dirname(f), bn + '_contents')
if os.path.isdir(output_dir):
shutil.rmtree(output_dir)
os.mkdir(output_dir)
return unpack_projector(bin_file, output_dir, do_decompile)

elif ext == '.exe': # Windows projector.exe?
print(f'Unpacking Windows projector "{fn}"...')
logging.info(f'Unpacking Windows projector "{fn}"...')
output_dir = os.path.join(dest_dir if dest_dir else os.path.dirname(f), bn + '_contents')
if os.path.isdir(output_dir):
shutil.rmtree(output_dir)
Expand All @@ -56,14 +57,14 @@ def unpack(f, dest_dir=None, do_decompile=False):
is_macos_bin = True
break
if is_macos_bin:
print(f'Unpacking Mac OS projector "{fn}"...')
logging.info(f'Unpacking Mac OS projector "{fn}"...')
output_dir = os.path.join(dest_dir if dest_dir else os.path.dirname(f), bn + '_contents')
if os.path.isdir(output_dir):
shutil.rmtree(output_dir)
os.mkdir(output_dir)
return unpack_projector(f, output_dir, do_decompile)

print('Error: File not supported.')
raise(TypeError('File not supported'))

def unpack_projector (exe_file, output_dir, do_decompile=False):
with open(exe_file, 'rb') as fh:
Expand All @@ -82,7 +83,7 @@ def unpack_projector (exe_file, output_dir, do_decompile=False):
start_pos = m2.start()
byteorder = 'little'
else:
return print(f'Error: Could not identify "{exe_file}" as Director projector.')
raise(TypeError('Could not identify file as Director projector'))

offset = 32
data_full = data_full[offset:]
Expand All @@ -97,6 +98,7 @@ def unpack_projector (exe_file, output_dir, do_decompile=False):

data = data_full[start_pos:]

# find embedded movies/castlibs
if byteorder == 'big':
for m in re.finditer(b'RIFX([\x00-\xFF]{4})(MV93|MC95)', data):
res.append([m.start(), m.group()[8:]])
Expand All @@ -108,9 +110,6 @@ def unpack_projector (exe_file, output_dir, do_decompile=False):
for m in re.finditer(b'XFIR([\x00-\xFF]{4})(MDGF|CDGF)', data):
res2.append([m.start(), m.group()[8:]])

for m in re.finditer(b'RIFF([\x00-\xFF]{4})XtraFILE', data):
xres.append(m.start())

if len(res) == 0:
if len(res2) == 0:
return print('Nothing found to extract!')
Expand All @@ -120,6 +119,16 @@ def unpack_projector (exe_file, output_dir, do_decompile=False):
else:
compressed = False

# find embedded xtras
for m in re.finditer(b'RIFF([\x00-\xFF]{4})XtraFILE', data):
xres.append(m.start())

is_16bit = False
if len(xres) == 0:
for m in re.finditer(b'XFIR([\x00-\xFF]{4})artX', data):
xres.append(m.start())
is_16bit = len(xres) > 0

# header template
header = ((b'RIFX' if byteorder == 'big' else b'XFIR') +
int.to_bytes(exe_size - 8 + offset, 4, byteorder) +
Expand All @@ -129,7 +138,7 @@ def unpack_projector (exe_file, output_dir, do_decompile=False):

# extract file names from Dict chunk
dir_names = []
x32_names = []
xtra_names = []

pos = res[0][0] # position of first XFIR/RIFX

Expand All @@ -142,28 +151,32 @@ def unpack_projector (exe_file, output_dir, do_decompile=False):
dict = data[m.start():pos]
cnt = int.from_bytes(dict[24:28], byteorder)

# no idea what's going on here
if cnt > 0xFFFF:
cnt = int.from_bytes(dict[24:28], 'little' if byteorder == 'big' else 'big')
byteorder_dict = byteorder
offset_dict = 0

if cnt > 0xFFFF: # 16-bit win projector
byteorder_dict = 'big' #'little' if byteorder == 'big' else 'big'
cnt = int.from_bytes(dict[24:28], byteorder_dict)
offset_dict = 2

if cnt == 1:
# finding actual original filename would require parsing .dir file, so we use the projector name instead
bn, _ = os.path.splitext(os.path.basename(exe_file))
dir_names.append(bn + '.dxr')
else:
pt = cnt * 8 + 64
pt = cnt * 8 + 64 - offset_dict
for i in range(cnt):
flen = int.from_bytes(dict[pt:pt+4], byteorder)
flen = int.from_bytes(dict[pt:pt+4], byteorder_dict)
fn = dict[pt + 4:pt + 4 + flen]
if b'Xtras:' in fn or fn.endswith(b' Xtra') or fn.endswith(b'.x32') or fn.endswith(b'.cpio'):
x32_names.append(get_filename(fn.decode()))
if b'Xtras:' in fn or fn.endswith(b' Xtra') or fn.lower().endswith(b'.x32') or fn.lower().endswith(b'.x16') or fn.endswith(b'.cpio'):
xtra_names.append(get_filename(fn.decode()))
else:
dir_names.append(get_filename(fn.decode()))
if i < cnt - 1:
pt += 4 + flen + (4 - flen % 4 if flen % 4 else 0)

if compressed: # compressed files
print('Notice: files in projector are compressed.')
logging.info('Files in projector are compressed.')

file_num = 0
for r in res:
Expand All @@ -188,7 +201,7 @@ def unpack_projector (exe_file, output_dir, do_decompile=False):
rebuild(fn)

else: # non-compressed files
print('Notice: files in projector are not compressed.')
logging.info('Files in projector are not compressed.')

file_num = 0
for r in res:
Expand All @@ -215,28 +228,46 @@ def unpack_projector (exe_file, output_dir, do_decompile=False):

# extract xtras
file_num = 0
for pos in xres:
chunk_size = int.from_bytes(data[pos:pos+4], 'big') # always bigEndian!
xdata = data[pos + 48:pos + 48 + chunk_size]
fn = x32_names[file_num]
file_num += 1
xdata = zlib.decompress(xdata)
with open(os.path.join(output_dir, sanitize_filename(fn)), 'wb') as fh:
fh.write(xdata)
if is_16bit:
for pos in xres:
pos += 49
xnam_len = int.from_bytes(data[pos:pos+4], byteorder)
pos += xnam_len + 8 + xnam_len % 2
data_len = int.from_bytes(data[pos:pos+4], byteorder)
pos += 4
xdata = data[pos:pos + data_len]
fn = xtra_names[file_num]
file_num += 1
xdata = zlib.decompress(xdata)
with open(os.path.join(output_dir, sanitize_filename(fn)), 'wb') as fh:
fh.write(xdata)
else:
for pos in xres:
chunk_size = int.from_bytes(data[pos:pos+4], 'big') # always bigEndian!
xdata = data[pos + 48:pos + 48 + chunk_size]
fn = xtra_names[file_num]
file_num += 1
xdata = zlib.decompress(xdata)
with open(os.path.join(output_dir, sanitize_filename(fn)), 'wb') as fh:
fh.write(xdata)

print(f'Done. {len(res) + len(xres)} files were extracted to "{output_dir}".')
return output_dir, len(res), len(xres)

def rebuild(fn):
dest_file = fn + '.tmp'
os.system(f'ProjectorRays --rebuild-only "{fn}" "{dest_file}" >{DEV_NULL}')
exit_code = os.system(f'ProjectorRays --rebuild-only "{fn}" "{dest_file}" >{DEV_NULL}')
if exit_code != 0:
raise(OSError('Rebuilding failed'))
if os.path.isfile(dest_file):
os.unlink(fn)
os.rename(dest_file, fn)

def decompile(fn):
dest_file, ext = os.path.splitext(fn)
dest_file += ('_decompiled.cst' if ext == '.cxt' or ext == '.cct' else '_decompiled.dir')
os.system(f'ProjectorRays "{fn}" "{dest_file}" >{DEV_NULL}')
exit_code = os.system(f'ProjectorRays "{fn}" "{dest_file}" >{DEV_NULL}')
if exit_code != 0:
raise(OSError('Decompiling failed'))

def get_filename(fn):
''' cross-platform, extracts filename of Windows, POSIX or Mac OS path '''
Expand All @@ -260,6 +291,7 @@ def sanitize_filename(fn):
if do_decompile:
args.remove('-decompile')
if len(args):
unpack(args[0], do_decompile=do_decompile)
output_dir, num_dirs, num_xtras = unpack(args[0], do_decompile=do_decompile)
print(f'Done. {num_dirs+num_xtras} files were extracted to "{output_dir}".')
else:
print('Usage: python unpacker.py [-decompile] <projector-file>')

0 comments on commit 14d6172

Please sign in to comment.