Wolfenstein 3D: Экспортируем карты уровней и объекты на них

Wolfenstein 3D (Wolf 3D) — компьютерная игра, родоначальник жанра «шутер от первого лица». Wolfenstein 3D разработана компанией id Software и издана компанией Apogee Software 5 мая 1992 под DOS.
C#

Решил я перепройти эту игру в очередной раз, но не просто так, а открыв все секреты… Но, естественно, бегать и прозванивать все стены мне не очень хочется.

Решение простое — пойти в Google и найти карты (хардкорный прогейминг во всей красе). Но не тут‑то было. Нормальных карт я не нашёл, а ломать глаза над картинкой в 300×300 пикселей… Спасибо, не надо.

Вторая итерация простого решения — сделать карты самому. Для этого в Google ищем, как и где хранятся данные об уровнях…

Нам понадобятся два файла: GAMEMAPS.WL6 и MAPHEAD.WL6. WL6 — значит, в файле хранятся данные о шести актах (говорят, есть ещё WL1 и WL3).

Алгоритмы декодирования и декомпрессии расписывать, думаю, не стоит. Лучше сразу перейти к их реализации (DecodeMapDataFile и DecompressMapDataFile) на C# (люблю и почитаю этот язык, но кодировать на нём не умею :D).

main.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Threading.Tasks;

namespace Wolfenstein3DMapGrabber
{
    class Wolfenstein3DMapGrabber
    {

        /*
         * Start
         */
        static void Main(string[] args)
        {
            string ProjectPath = System.IO.Directory.GetCurrentDirectory();
            Int32[] Offsets;

            if ((Offsets = Wolfenstein3DMapGrabber.ParseMapHeadFile(ProjectPath + "/Wolfenstein3DData/MAPHEAD.WL6")).Length > 0)
            {
                Console.WriteLine("MAPHEAD.WL6: Data received (Blocks count = " + Offsets.Length + ")");
                Wolfenstein3DMapGrabber.ParseMapDataFile(ProjectPath + "/Wolfenstein3DData/GAMEMAPS.WL6", Offsets);

            }
            Console.WriteLine("Something done! Press any key...");
            Console.ReadKey();
        }


        /*
         * Прочитать блок заданной длинны из стрима
         */
        private static byte[] ReadBytes(FileStream Stream, int Offset, int Length)
        {
            byte[] BuffBytes = new byte[Length];
            int BytesRead = 0;

            Stream.Seek(Offset, SeekOrigin.Begin);
            if ((BytesRead = Stream.Read(BuffBytes, 0, Length)) < Length)
            {
                Stream.Close();
                throw new Exception("FATAL: IO error (" + Length + " vs. " + BytesRead + ")");
            }
            return BuffBytes;
        }


        /*
         * Декодировать ("декармакизировать") данные по карте (unhuffman)
         * а-ля CAL_CarmackExpand в оригинале
         */
        private static ushort[] DecodeMapDataFile(byte[] Input, short NumberBytesAfterDecoding)
        {
            int WordCounter;
            int InCounter;
            int OutCounter;
            ushort Copy;
            ushort[] Output;
            byte Low;
            byte High;
            int Offset;
            
            WordCounter = NumberBytesAfterDecoding / 2;
            InCounter = 0;
            OutCounter = 0;
            Output = new ushort[WordCounter];

            do
            {
                Low = Input[InCounter++];
                High = Input[InCounter++];
                if (High == 0xA7)
                {
                    // If high byte of value is A7 then:
                    if (Low == 0)
                    {
                        // If low byte is 00 then:
                        //  - high byte of value is A7
                        //  - low  byte of value is the next byte
                        Output[OutCounter++] = (ushort)((High << 8) | Input[InCounter]);
                        ++InCounter;
                        --WordCounter;
                    }
                    else
                    {
                         // - the low  byte is a count (ie. # of integers we want to reuse).
                         // - the next byte is the # of integers we want to move back in order to reuse data we have already decoded.
                        Offset = Input[InCounter];
                        ++InCounter;
                        Copy = (ushort)(OutCounter - Offset);
                        WordCounter -= Low;
                        while (Low-- != 0)
                        {
                            Output[OutCounter++] = Output[Copy++];
                        }
                    }
                }
                else if (High == 0xA8)
                {
                    // If high byte of value is A8 then:
                    if (Low == 0)
                    {
                        // If low byte is 00 then:
                        //  - high byte of value is A8
                        //  - low  byte of value is the next byte
                        Output[OutCounter++] = (ushort)((High << 8) | Input[InCounter]);
                        ++InCounter;
                        --WordCounter;
                    }
                    else
                    {
                        // - the low  byte is a count (ie. # of integers we want to reuse).
                        // - the next integer (2 bytes) is the # of integers
                        //   we want to skip over, starting at the beginning of the buffer being used to hold data we have already decoded 
                        //   (the first integer in the buffer being the # of bytes after decompressing).
                        Offset = (ushort)((Input[InCounter + 1] << 8) | Input[InCounter]);
                        InCounter += 2;
                        Copy = (ushort)(0 + Offset - 1);
                        WordCounter -= Low;
                        while (Low-- != 0)
                        {
                            Output[OutCounter++] = Output[Copy++];
                        }
                    }
                }
                else
                {
                    // If high byte is not A7 or A8, then store the value as is.
                    Output[OutCounter++] = (ushort)((High << 8) | Low);
                    --WordCounter;
                }
            } while (WordCounter != 0);
            return Output;
        }


        /*
         * Распаковать данные (unRLEW)
         */
        private static ushort[] DecompressMapDataFile(ushort[] Data, short NumberOfBytesAfterDecompressing)
        {
            int WordsCount = NumberOfBytesAfterDecompressing / 2;
            ushort[] Output = new ushort[WordsCount];
            uint InCounter = 0;
            uint OutCounter = 0;
            ushort Number = 0;
            ushort Repeat;
            
            do
            {
                Number = Data[InCounter++];
                if (Number == 0xABCD)
                {
                    // If value == ABCD
                    //  - the next integer is # of repetitions
                    //  - the next integer is value to repeat
                    Repeat = Data[InCounter++];
                    Number = Data[InCounter++];
                    WordsCount -= Repeat;
                    while (Repeat-- > 0) 
                    {
                        Output[OutCounter++] = Number;
                    }
                }
                else 
                {
                    // If value is not ABCD then store the value as is.
                    Output[OutCounter++] = Number;
                    WordsCount--;
                }
            } while (WordsCount > 0);

            return Output;
        }


        /*
         * Разбираем GAMEMAPS файл
         */
        private static bool ParseMapDataFile(string FileName, Int32[] Offsets)
        {
            int Length = 0;
            FileStream Stream;
            byte[] BuffBytes;
            Int32 DataShift = 0;
            Int32 DataLength = 0;
            short NumberBytesAfterDecoding;
            short NumberOfBytesAfterDecompressing;
            ushort[] Data;
            string ExportData;
            string LevelName;
            int[] Shifts;
            string FileNameSuffix;

            Stream = new FileStream(FileName, FileMode.Open, FileAccess.Read);
            Length = (int)Stream.Length;
            Shifts = new int[] { 0, 1 };
            // забираем данные по заголовкам карт
            for (int i = 0; i < Offsets.Length; ++i)
            {
                if (Offsets[i] > 0)
                {
                    for (int k = 0; k < Shifts.Length; ++k)
                    {
                        if (Shifts[k] == 0)
                        {
                            FileNameSuffix = "-Map";
                        }
                        else 
                        {
                            FileNameSuffix = "-Objects";
                        }

                        /*
                        Level
                        Header  Data
                        Offset  Type  Description
                        ------  ----  -------------------------------
                        0000   long  File offset of Map_Block
                        0004   long  File offset of Object_Block
                        0008   long  File offset of Unknown_Block
                        000C   int   File size   of Map_Block
                        000E   int   File size   of Object_Block
                        0010   int   File size   of Unknown_Block
                        0012   int   Horizontal map size (# of squares)
                        0014   int   Vertical   map size (# of squares)
                        0016   char  Level name (ascii) (null terminated)
                        0026   char  "!ID!" (4 bytes)
                        */

                        DataShift = BitConverter.ToInt32(Wolfenstein3DMapGrabber.ReadBytes(Stream, Offsets[i] + Shifts[k] * 4, 4), 0);
                        DataLength = BitConverter.ToInt16(Wolfenstein3DMapGrabber.ReadBytes(Stream, Offsets[i] + 12 + Shifts[k] * 2, 2), 0);

                        /*
                        Block  Data
                        Offset Type Description
                        ------ ---- --------------------------------------------
                        0000  int  # of data bytes after decoding (count includes the two bytes at Block Offset 0002).
                        0002  int  # of data bytes after decompresssing.
                        0004  int  Data bytes. Values are stored as 2 byte integers (low byte first).
                        */

                        NumberBytesAfterDecoding = BitConverter.ToInt16(Wolfenstein3DMapGrabber.ReadBytes(Stream, DataShift, 2), 0);
                        NumberOfBytesAfterDecompressing = BitConverter.ToInt16(Wolfenstein3DMapGrabber.ReadBytes(Stream, DataShift + 2, 2), 0);
                        BuffBytes = Wolfenstein3DMapGrabber.ReadBytes(Stream, DataShift + 4, DataLength);
                        LevelName = System.Text.Encoding.ASCII.GetString(Wolfenstein3DMapGrabber.ReadBytes(Stream, Offsets[i] + 22, 16)).Replace("\0", "");

                        Console.WriteLine("GAMEMAPS.WL6:");
                        Console.WriteLine("     LEVEL " + i);
                        Console.WriteLine("     DATA OFFSET: " + Offsets[i]);
                        Console.WriteLine("     NAME: " + LevelName);
                        Console.WriteLine("     NUMBER OF BYTES AFTER DECODING: " + NumberBytesAfterDecoding);
                        Console.WriteLine("     NUMBER OF BYTES AFTER DECOMPRESSING: " + NumberOfBytesAfterDecompressing);
                        Console.WriteLine("     BUFFER LENGTH: " + BuffBytes.Length);

                        // Pass1 (decoding):
                        Data = Wolfenstein3DMapGrabber.DecodeMapDataFile(BuffBytes, NumberBytesAfterDecoding);
                        // Pass2 (decompressing):
                        Data = Wolfenstein3DMapGrabber.DecompressMapDataFile(Data, NumberOfBytesAfterDecompressing);
                        // Export decompressing data
                        ExportData = "";
                        for (int j = 0; j < Data.Length; j++)
                        {
                            if (ExportData != "")
                            {
                                ExportData += ",";
                            }
                            ExportData += Data[j].ToString();
                        }
                        System.IO.StreamWriter OutputFile = new System.IO.StreamWriter(@".\" + LevelName + FileNameSuffix + ".txt");
                        OutputFile.WriteLine(ExportData);
                        OutputFile.Close();
                    }
                }
            }
            Stream.Close();
            return false;
        }


        /*
         * Разобрать MAPHEAD файл
         */
        private static Int32[] ParseMapHeadFile(string FileName)
        {
            int Length = 0;
            int Iteration = 0;
            int DataLength = 0;
            int BlockLengthInBytes = 4;
            FileStream Stream;
            Int32[] Offsets;
            byte[] BuffBytes;

            try
            {
                if (!File.Exists(FileName))
                {
                    throw new Exception("MAPHEAD.WL6: File not exists");
                }
                Stream = new FileStream(FileName, FileMode.Open, FileAccess.Read);
                Length = (int)Stream.Length;
                BuffBytes = Wolfenstein3DMapGrabber.ReadBytes(Stream, 0, 2);
                // первые 2 байта - ключ для декомпрессии (см. DecompressMapDataFile)
                if (BuffBytes[0] != 0xCD || BuffBytes[1] != 0xAB)
                {
                    throw new Exception("MAPHEAD.WL6: Wrong file format");
                }
                DataLength = Length - 2;
                if (DataLength <= 0 || DataLength % BlockLengthInBytes != 0)
                {
                    throw new Exception("MAPHEAD.WL6: Wrong file format");
                }
                Offsets = new Int32[DataLength / BlockLengthInBytes];
                Length = 2;
                // файл содержит в себе набор int'ов (смещений в файле GAMEMAPS)
                while ((BuffBytes = Wolfenstein3DMapGrabber.ReadBytes(Stream, Length, BlockLengthInBytes)).Length > 0)
                {
                    Length += BuffBytes.Length;
                    Offsets[Iteration] = BitConverter.ToInt32(BuffBytes, 0);
                    Iteration++;
                    if (Iteration == Offsets.Length)
                    {
                        Stream.Close();
                        return Offsets;
                    }
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
            return new Int32[0];
        }

    }
}

На выходе получаем набор файлов (по 2 на уровень: *-Map.txt и *-Objects.txt). Они содержат (64 * 64) чисел. Как вы догадались - уровень представляет из себя матрицу 64x64 клетки.

На гитхабе можно посмотреть листинг этих кодов: https://github.com/id-Software/wolf3d/blob/master/WOLFSRC/WL_GAME.C

Чуть более понятнный листинг ниже.

[MAP VALUES]

01  Wall: Grey stone cube
02  Wall: Grey stone cube
03  Wall: Grey stone cube with flag
04  Wall: Grey stone cube with picture
05  Wall: Blue stone cube with cell door
06  Wall: Grey stone cube with bird and archway
07  Wall: Blue stone cube with cell door and skeleton
08  Wall: Blue stone cube
09  Wall: Blue stone cube
0a  Wall: Wood cube with picture of bird
0b  Wall: Wood cube with picture
0c  Wall: Wood cube
0d  Eleva: Elevator door (no red door handle) (ie. from prvs level)
0e  Wall: Steel cube (N/S="Verbotem", E/W="Achtung")
0f  Wall: Steel cube
10  Exit: Landscape view (N/S=sky & green land, E/W=dark & stars?)
11  Wall: Red brick cube
12  Wall: Red brick cube with green wreath
13  Wall: Purple and green cube
14  Wall: Red brick cube with tapestry of bird
15  Wall: Inside elevator (N/S=hand rail,  E/W=controls in down position)
16  Wall: Inside elevator (N/S=blank wall, E/W=controls in up position)
17  Wall: Wood cube with green branches over a cross
18  Wall: (v1.1+) Grey stone cube with green moss/slime
19  Wall: Pink and green cube
1a  Wall: (v1.1+) Grey stone cube with green moss/slime
1b  Wall: Grey stone cube
1c  Wall: Grey stone cube (N/S="Verbotem", E/W="Achtung")
1d  Wall: (Eps.2+) Brown cave
1e  Wall: (Eps.2+) Brown cave with blood
1f  Wall: (Eps.2+) Brown cave with blood
20  Wall: (Eps.2+) Brown cave with blood
21  Wall: (Eps.2+) Stained glass window of Hitler
22  Wall: (Eps.2+) Blue brick wall with skulls
23  Wall: (Eps.2+) Grey brick wall
24  Wall: (Eps.2+) Blue brick wall with swastikas
25  Wall: (Eps.2+) Grey brick wall with hole
26  Wall: (Eps.2+) Red/grey/brown wall
27  Wall: (Eps.2+) Grey brick wall with crack
28  Wall: (Eps.2+) Blue brick wall
29  Wall: (Eps.2+) Blue stone wall with verboten sign
2a  Wall: (Eps.2+) Brown tiles
2b  Wall: (Eps.2+) Grey brick wall with map
2c  Wall: (Eps.2+) Orange stone wall
2d  Wall: (Eps.2+) Orange stone wall
2e  Wall: (Eps.2+) Brown tiles
2f  Wall: (Eps.2+) Brown tiles with banner
30  Wall: (Eps.2+) Orange panel on wood wall
31  Wall: (Eps.2+) Grey brick wall with Hitler
40  Wall: Grey stone cube
41  Wall: Grey stone cube
42  Wall: Grey stone cube
43  Wall: Grey stone cube with flag
44  Wall: Grey stone cube with picture
45  Wall: Blue stone cube with cell door
46  Wall: Grey stone cube with bird and archway
47  Wall: Blue stone cube with cell door and skeleton
48  Wall: Blue stone cube
49  Wall: Blue stone cube
4a  Wall: Wood cube with picture of bird
4b  Wall: Wood cube with picture
4c  Wall: Wood cube
4d  Eleva: Elevator door (no red door handle)
4e  Wall: Steel cube (N/S="Verbotem", E/W="Achtung")
4f  Wall: Steel cube
50  Exit: Exit (N/S=sky & green land, E/W=dark & stars?)
51  Wall: Red brick cube
52  Wall: Red brick cube with green wreath
53  Wall: Pink and green cube
54  Wall: Red brick cube with tapestry of bird
55  Wall: Inside elevator (N/S=hand rail,  E/W=controls in down position)
56  Wall: Inside elevator (N/S=blank wall, E/W=controls in up position)
57  Wall: Wood cube with green branches over a cross
59  Wall: Pink and green cube
5a  VDoor: Steel door (east/west doorway) (vertical on map)
5b  HDoor: Steel door (north/south doorway) (horizontal on map)
5c  Lock: Locked version of 5a (need gold key to open)
5d  Lock: Locked version of 5b (need gold key to open)
5e  Lock: Locked version of 5a (need silver key to open)
5f  Lock: Locked version of 5b (need silver key to open)
60  Lock: Locked version of 5a (can't open)
61  Lock: Locked version of 5b (can't open)
62  Lock: Locked version of 5a (can't open)
63  Lock: Locked version of 5b (can't open)
64  Eleva: Elevator door with a grey stone cube on north and south side
65  Eleva: Elevator door with a grey stone cube on east  and west  side
6b  Floor: Floor
6c  Floor: Floor
6d  Floor: Floor
6e  Floor: Floor
6f  Floor: Floor
70  Floor: Floor
71  Floor: Floor
72  Floor: Floor
73  Floor: Floor
74  Floor: Floor
75  Floor: Floor
76  Floor: Floor
77  Floor: Floor
78  Floor: Floor
79  Floor: Floor
7a  Floor: Floor
7b  Floor: Floor
7c  Floor: Floor
7d  Floor: Floor
7e  Floor: Floor
7f  Floor: Floor
80  Floor: Floor
81  Floor: Floor
82  Floor: Floor
83  Floor: Floor
84  Floor: Floor
85  Floor: Floor
86  Floor: Floor
87  Floor: Floor
88  Floor: Floor
89  Floor: Floor
8a  Floor: Floor
8b  Floor: Floor
8c  Floor: Floor
8d  Floor: Floor
8e  Floor: Floor
8f  Floor: Floor

[OBJECT VALUES]

00  Nothi: Nothing
13  Start: Starting location, facing north
14  Start: Starting location, facing east
15  Start: Starting location, facing south
16  Start: Starting location, facing west
17  Objec: Puddle of water
18  Objec: Green barrel
19  Objec: Table and two chairs
1a  Objec: Floor lamp
1b  Objec: Chandelier
1c  Objec: Skeleton handing from hook in ceiling
1d  Food: Bowl of dog food
1e  Objec: Stone pillar
1f  Objec: Potted tree
20  Objec: Skeleton lying on ground
21  Objec: Sink
22  Objec: Potted plant
23  Objec: Blue vase
24  Objec: Round table
25  Objec: Ceiling light
26  Objec: 5 Pots and pans hanging from wood beam attached to ceiling
27  Objec: Suit of armour
28  Objec: Hanging cage
29  Objec: Hanging cage with skeleton inside
2a  Objec: Pile of bones
2b  GKey: Gold key
2c  SKey: Silver key
2d  Objec: Cot
2e  Objec: Bucket
2f  Food: Plate of food
30  Aid: First aid kit
31  Ammo: Ammo
32  Weap3: Weapon 3
33  Weap4: Weapon 4
34  Treas: Jewelled cross
35  Treas: Gold chalice
36  Treas: Jewelled box
37  Treas: Crown
38  Mirro: Blue mirror with face
39  Objec: Bones and blood
3a  Objec: Wood barrel
3b  Objec: Stone well with water
3c  Objec: Stone well no water
3d  Objec: Blood
3e  Objec: Flag pole and flag
3f  Objec: (v1.1)  Floating sign: "CALL APOGEE. SAY "SNAPPITY"  (v1.11) Floating sign: "CALL APOGEE. SAY "AARDWOLF"
40  Objec: (v1.1+) Broken glass on floor (or crushed bones?)
41  Objec: (v1.1+) Broken glass on floor (or crushed bones?)
42  Objec: (v1.1+) Broken glass on floor (or crushed bones?)
43  Objec: (v1.1+) Pot, pan, and ladle hanging from ceiling
44  Objec: (v1.1+) Iron wood-burning stove
45  Objec: (v1.1+) Rack of poles (spears ?)
46  Objec: (v1.1+) Green vines hanging from ceiling
49  ?????: Unknown purpose (found in episode 1, on level 3)
5a  Face: Makes enemy face east
5b  Face: Makes enemy face north east
5c  Face: Makes enemy face north
5d  Face: Makes enemy face north west
5e  Face: Makes enemy face west
5f  Face: Makes enemy face south west
60  Face: Makes enemy face south
61  Face: Makes enemy face south east
62  Secre: Block is a secret passage
63  End: Ends game
6c  Guard: Tan soldier (skill level 1 & 2), standing still facing east
6d  Guard: Tan soldier (skill level 1 & 2), standing still facing north
6e  Guard: Tan soldier (skill level 1 & 2), standing still facing west
6f  Guard: Tan soldier (skill level 1 & 2), standing still facing south
70  Guard: Tan soldier (skill level 1 & 2), walking east
71  Guard: Tan soldier (skill level 1 & 2), walking north
72  Guard: Tan soldier (skill level 1 & 2), walking west
73  Guard: Tan soldier (skill level 1 & 2), walking south
74  WGuar: (Eps.2+) White guard (skill level 1 & 2), standing facing east
75  WGuar: (Eps.2+) White guard (skill level 1 & 2), standing facing north
76  WGuar: (Eps.2+) White guard (skill level 1 & 2), standing facing west
77  WGuar: (Eps.2+) White guard (skill level 1 & 2), standing facing south
7c  Objec: Dead tan soldier
7e  SS: Blue soldier (skill level 1 & 2), standing still, facing east
7f  SS: Blue soldier (skill level 1 & 2), standing still, facing north
80  SS: Blue soldier (skill level 1 & 2), standing still, facing west
81  SS: Blue soldier (skill level 1 & 2), standing still, facing south
82  SS: Blue soldier (skill level 1 & 2), walking east
83  SS: Blue soldier (skill level 1 & 2), walking north
84  SS: Blue soldier (skill level 1 & 2), walking west
85  SS: Blue soldier (skill level 1 & 2), walking south
8a  Dog: Dog (skill level 1 & 2), running east
8b  Dog: Dog (skill level 1 & 2), running north
8c  Dog: Dog (skill level 1 & 2), running west
8d  Dog: Dog (skill level 1 & 2), running south
90  Guar3: Tan soldier (skill level 3), standing still, facing east
91  Guar3: Tan soldier (skill level 3), standing still, facing north
92  Guar3: Tan soldier (skill level 3), standing still, facing west
93  Guar3: Tan soldier (skill level 3), standing still, facing south
94  Guar3: Tan soldier (skill level 3), walking east
95  Guar3: Tan soldier (skill level 3), walking north
96  Guar3: Tan soldier (skill level 3), walking west
97  Guar3: Tan soldier (skill level 3), walking south
98  WGua3: (Eps.2+) White guard (skill level 3), standing, facing east
99  WGua3: (Eps.2+) White guard (skill level 3), standing, facing north
9a  WGua3: (Eps.2+) White guard (skill level 3), standing, facing west
9b  WGua3: (Eps.2+) White guard (skill level 3), standing, facing south
a0  Pries: (Eps.2+) Black priest
a2  SS3: Blue soldier (skill level 3), standing still, facing east
a3  SS3: Blue soldier (skill level 3), standing still, facing north
a4  SS3: Blue soldier (skill level 3), standing still, facing west
a5  SS3: Blue soldier (skill level 3), standing still, facing south
a6  SS3: Blue soldier (skill level 3), walking east
a7  SS3: Blue soldier (skill level 3), walking north
a8  SS3: Blue soldier (skill level 3), walking west
a9  SS3: Blue soldier (skill level 3), walking south
ae  Dog3: Dog (skill level 3), running east
af  Dog3: Dog (skill level 3), running north
b0  Dog3: Dog (skill level 3), running west
b1  Dog3: Dog (skill level 3), running south
b2  Hitlr: (Eps.2+) Hitler
b4  Guar4: Tan soldier (skill level 4), standing still, facing east
b5  Guar4: Tan soldier (skill level 4), standing still, facing north
b6  Guar4: Tan soldier (skill level 4), standing still, facing west
b7  Guar4: Tan soldier (skill level 4), standing still, facing south
b8  Guar4: Tan soldier (skill level 4), walking east
b9  Guar4: Tan soldier (skill level 4), walking north
ba  Guar4: Tan soldier (skill level 4), walking west
bb  Guar4: Tan soldier (skill level 4), walking south
bc  WGua4: (Eps.2+) White guard (skill level 4), standing, facing east 
bd  WGua4: (Eps.2+) White guard (skill level 4), standing, facing north
be  WGua4: (Eps.2+) White guard (skill level 4), standing, facing west 
bf  WGua4: (Eps.2+) White guard (skill level 4), standing, facing south
c4  Schab: (Eps.2+) Dr. Schabbs
c6  SS4: Blue soldier (skill level 4), standing still, facing east
c7  SS4: Blue soldier (skill level 4), standing still, facing north
c8  SS4: Blue soldier (skill level 4), standing still, facing west
c9  SS4: Blue soldier (skill level 4), standing still, facing south
ca  SS4: Blue soldier (skill level 4), walking east
cb  SS4: Blue soldier (skill level 4), walking north
cc  SS4: Blue soldier (skill level 4), walking west
cd  SS4: Blue soldier (skill level 4), walking south
d2  Dog4: Dog (skill level 4), running east
d3  Dog4: Dog (skill level 4), running north
d4  Dog4: Dog (skill level 4), running west
d5  Dog4: Dog (skill level 4), running south
d6  Boss: Final boss (blue) ("Hans")
d8  Zomb1: (Eps.2+) Zombie (skill level 1 & 2), standing facing east
d9  Zomb1: (Eps.2+) Zombie (skill level 1 & 2), standing facing north
da  Zomb1: (Eps.2+) Zombie (skill level 1 & 2), standing facing west
db  Zomb1: (Eps.2+) Zombie (skill level 1 & 2), standing facing south
ea  Zomb3: (Eps.2+) Zombie (skill level 3), standing facing east 
eb  Zomb3: (Eps.2+) Zombie (skill level 3), standing facing north
ec  Zomb3: (Eps.2+) Zombie (skill level 3), standing facing west 
ed  Zomb3: (Eps.2+) Zombie (skill level 3), standing facing south
fc  Zomb4: (Eps.2+) Zombie (skill level 4), standing facing east 
fd  Zomb4: (Eps.2+) Zombie (skill level 4), standing facing north
fe  Zomb4: (Eps.2+) Zombie (skill level 4), standing facing west 
ff  Zomb4: (Eps.2+) Zombie (skill level 4), standing facing south