A small freedom area.

Reverse in Arakis

Mon 18 Apr 2011

reverse, game, dune, python, prog

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.

centerimg

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^Wown the game already using the script.

index