While tidying my room last time, I ran into an "old" game made by WideScreen Game (a French company which went bankrupt recently), and produced by Cryo Interactive (which also went bankrupt). Since I wasn't able to finish the game, I thought about trying again.
The installation went fine with wine, but the game is unplayable: the colorspace is wrong (rendering is awful), the player makes 15 long strides before reaching a single meter ahead, etc. So without any regrets (after all, the game sucks pretty hard), I start digging into the files…
And here is what I found:
% ls -l ~/.wine/drive_c/Program Files/Cryo/Dune
total 536M
-rw-r--r-- 1 ubitux ubitux 693 Sep 7 00:26 configpc.cfg
-rwxr-xr-x 1 ubitux ubitux 3.7M Oct 29 2001 Dune.exe
-rw-r--r-- 1 ubitux ubitux 2.8K Oct 29 2001 dunepc.ini
-rw-r--r-- 1 ubitux ubitux 428M Oct 29 2001 globals.dun
-r--r--r-- 1 ubitux ubitux 105M Oct 29 2001 locale.dun
-rw-r--r-- 1 ubitux ubitux 28K Sep 7 00:24 Uninst.isu
-rwxr-xr-x 1 ubitux ubitux 412K Oct 29 2001 VideoCfg.exe
We note two configuration files (configpc.cfg
for the video, and dunepc.ini
for the game), two executables (Dune.exe
for the game, VideoCfg.exe
to
handle the configuration), two huge .dun
files, and the last one in order to
make a proper uninstall (Uninst.isu
).
It goes without saying all the game content is located in globals.dun
and
locale.dun
. So I open the first one with my text editor, and here I am,
swimming in a mixed mess of binary and text data, until I ran into this:
# SP : Anims deja definies plus bas !!!!!!!! Putain C Horrible !!!!
which means something like:
# SP: Anims are already defined below !!!!!!!! Fuck that shit !!!!
Then I realized I wanted more of that stuff.
Seeking spice
Let's first extract what's contained in the .dun
files. The file(1)
command
does not give any hint:
% file globals.dun locale.dun
globals.dun: data
locale.dun: data
Let's compare the different headers:
% hexdump -Cv globals.dun|head -n 3
00000000 72 10 ea f4 00 00 00 00 6e ab ae 1a 55 0c 00 00 |r.......n...U...|
00000010 0a 23 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d |.#--------------|
00000020 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d |----------------|
% hexdump -Cv locale.dun|head -n 3
00000000 72 10 ea f4 00 00 00 00 cd 4e 8a 06 06 07 00 00 |r........N......|
00000010 23 20 43 49 4e 45 4d 41 54 49 51 55 45 5f 31 5f |# CINEMATIQUE_1_|
00000020 53 43 45 4e 45 31 5f 31 0a 43 49 4e 45 4d 41 54 |SCENE1_1.CINEMAT|
The 32 (maybe 64) first bits looks like a magic. Looking for 72 10 EA F4
on
Google does not a single hint, so it confirms the exotic format.
In the 16 first bytes, we should have all the necessary information in order to
extract the archive, since the content of the first file starts at 0x10
. So we
start with the following information for globals.dun
:
72 10 EA F4 # Magic
00 00 00 00 # Unused?
6E AB AE 1A # V1
55 0C 00 00 # V2
And for locale.dun
:
72 10 EA F4 # Magic
00 00 00 00 # Unused?
CD 4E 8A 06 # V1
06 07 00 00 # V2
V1
and V2
don't looks like the first size of the first file, but V1
actually looks like to be the offset of the header containing the list of all
the files. V2
may be the size of the header.
Each filename in the header is followed by 0x0A
(end line character) and two
32 bits values. Just like the header, there is a big one, and a smaller one.
Not hard to deduce they are the offset
and the size
of the file.
Princess Irulan's writings
It's time to write some code to extract all this stuff. We can notice some
filenames referring to R:
and some others to a parent directory, so we'll put
all the R:
files into a CD
directory, and those trying to go up in the file
tree in a PARENT
directory. Here is the script:
#!/usr/bin/env python
import sys, os
from struct import unpack
if len(sys.argv) < 2:
print 'Usage: %s x.dun' % sys.argv[0]
sys.exit(1)
f = open(sys.argv[1], 'rb')
chksum, u1, seek_to, u2 = unpack('IIII', f.read(16))
if chksum != 0xf4ea1072:
print 'Bad checksum: [0x%08X]' % chksum
sys.exit(1)
f.seek(seek_to)
while True:
try:
path, offset, size = (f.readline()[:-1],) + unpack('II', f.read(8))
except:
break
print('offset: [%08X] size: [%08X] path: [%s]' % (offset, size, path))
path = path.replace('\\', '/').replace('R:/', 'CD/').replace('../', 'PARENT/')
bak_pos = f.tell()
f.seek(offset)
try:
os.makedirs('dump/' + os.path.dirname(path))
except:
pass
out_file = open('dump/%s' % path, 'w')
out_file.write(f.read(size))
out_file.close()
f.seek(bak_pos)
Spice extraction
% ./extract.py globals.dun
offset: [023E2F18] size: [00003177] path: [..\code_scenarique\Actors\ActorFile.pyo]
offset: [00099786] size: [00000077] path: [..\code_scenarique\Actors\Box\__init__.pyo]
offset: [000997FD] size: [000009E1] path: [..\code_scenarique\Actors\Box\basicBox.pyo]
offset: [00071C2E] size: [0000007A] path: [..\code_scenarique\Actors\Camera\__init__.pyo]
offset: [00071CA8] size: [000015F9] path: [..\code_scenarique\Actors\Camera\basicCamera.pyo]
offset: [039A4561] size: [00000079] path: [..\code_scenarique\Actors\Civil\__init__.pyo]
offset: [039A45DA] size: [0000247A] path: [..\code_scenarique\Actors\Civil\basicCivil.pyo]
offset: [000862DF] size: [0000007C] path: [..\code_scenarique\Actors\Director\__init__.pyo]
offset: [0008635B] size: [00004901] path: [..\code_scenarique\Actors\Director\basicDirector.pyo]
[…]
offset: [15E77043] size: [000016D7] path: [R:\dune\new_runtime\font\Arial16.met]
offset: [15E7871A] size: [000020DE] path: [R:\dune\new_runtime\font\Arial16.png]
offset: [15E60FE6] size: [00012239] path: [R:\dune\new_runtime\font\courier.met]
offset: [15E60613] size: [000009D3] path: [R:\dune\new_runtime\font\t10.met]
offset: [15E5FC40] size: [000009D3] path: [R:\dune\new_runtime\font\t12.met]
offset: [15E5F26D] size: [000009D3] path: [R:\dune\new_runtime\font\t14.met]
offset: [15E5E89A] size: [000009D3] path: [R:\dune\new_runtime\font\t16.met]
offset: [15E5E23E] size: [0000065C] path: [R:\dune\new_runtime\level\testLevel\typecode.dff]
offset: [17409793] size: [0000049A] path: [R:\dune\new_runtime\sound\maula_click.wav]
offset: [17409C2D] size: [00001260] path: [R:\dune\new_runtime\sound\menu_click.wav]
offset: [00000010] size: [00028C5D] path: [resource.dat]
~Pyo~ ~pyo~
Amongst all those files, we can notice the models (.mdl
), animations (.anm
),
textures, songs in .wav
files, and a lot of treasures I let you uncover by
yourselves.
Something more interesting grabbed my attention:
% file dump/PARENT/code_scenarique/Actors/ActorFile.pyo
dump/PARENT/code_scenarique/Actors/ActorFile.pyo: python 1.5/1.6 byte-compiled
Wow, it's some old Python code!
Alright then, we need to decompile this. The only tool able to do that was Decompyle. Unfortunately, at the moment the available version on Sourceforge does not work. After applying the debian patches I was almost able to build its module and make it work.
Since misfortune has no limit, it still didn't work, so I had to hack a bit into the code. Here are the two necessary patches:
Be prepared to appreciate what you meet
And then I was able to read all the screenplay code of the game:
#!/usr/bin/env bash
for f in `find dump/ -iname '*.pyo'`; do
dest=`dirname $f | sed 's/^dump/sources/'`/`basename ${f%\.*}.py`
decompyle -o $dest $f
done
After extraction, we can count around 52000 lines of code, which is quite a treasure.
Mission complete
Once again, this was a rewarding adventure, and useless as ever. I hope I gave you the desire to do the same with your old games, we can really find some curious things doing this.
Also, for legal reasons I won't share the extracted code, but you should be able
to do it yourselves if you downl^W
own the game already using the script.